日期:2026-08-13 分支:cfclub 状态:待评审
当前出方案流程(health-plan-summary.vue:选报告 → 提需求 → AI 生成方案)只接收健康报告 + 用户自由文本需求,没有结构化的"当前健康现状"输入。AI 出方案时不知道用户身高体重、血压血糖血脂、疾病史、用药情况、过敏源等关键信息,方案精度受限。
用户需求(已确认):
设计目标:
| # | 问题 | 决策 |
|---|---|---|
| 1 | 调研触发形式 | 两者都要:出方案流程内提示填写 + 独立档案页随时维护;填过一次后出方案自动读取,可更新 |
| 2 | 数据归属对象 | 按账号(user_id)存一份 |
| 3 | 过敏/食物喜好与现有表关系 | 复用现有 diet_preferences(按 family_member_id 存),新表不重复存;出方案时一并读取传入 |
| 4 | 指标值形态 | 结构化数值 + 可选备注(血压 120/80、血糖 5.6 + 备注补充诊断信息) |
| 5 | 疾病史/用药形态 | JSON 数组 [{name, note}],前端标签式多选+自定义输入 |
| 6 | 不填的提醒方式 | 弹窗确认:「未填写健康现状,方案可能不精准」→「去填写」/「直接生成」 |
| 7 | AI 接入方式 | 前端随请求传参:前端把健康现状+饮食偏好随 aiSendMessage 请求传给 AI(Dify inputs) |
独立档案页 (pages/health/health-status-form.vue) 出方案流程 (health-plan-summary.vue)
│ 读/写 │ 读取
▼ ▼
health_status 表 (新, user_id 唯一) diet_preferences (现有, family_member_id)
│ │
└──────────────────┬──────────────────────────────────┘
▼ 前端组装 healthStatus + dietPrefs
aiSendMessage({query, reportId, healthStatus, dietPrefs})
▼
AIChatController.sendMessage
▼
inputs.put("health_status", ...) / inputs.put("diet_preferences", ...)
▼
Dify 生成方案
diet_preferences(无则空)。uni.showModal 弹窗 →「去填写」跳档案页 /「直接生成」继续。health_statusCREATE TABLE IF NOT EXISTS health_status (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL COMMENT '所属账号(一个账号一份)',
height_cm DECIMAL(5,1) DEFAULT NULL COMMENT '身高(cm)',
weight_kg DECIMAL(5,1) DEFAULT NULL COMMENT '体重(kg)',
blood_pressure VARCHAR(20) DEFAULT NULL COMMENT '血压,如 120/80',
blood_glucose DECIMAL(4,1) DEFAULT NULL COMMENT '空腹血糖(mmol/L)',
blood_lipids JSON DEFAULT NULL COMMENT '血脂 [{name,value,unit,note}]',
disease_history JSON DEFAULT NULL COMMENT '疾病史 [{name,note}]',
medications JSON DEFAULT NULL COMMENT '在服药物/治疗 [{name,note}]',
notes VARCHAR(500) DEFAULT NULL COMMENT '其他补充说明',
filled_at DATETIME DEFAULT NULL COMMENT '首次填写时间',
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY uk_user (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='当前状态调研-健康现状档案';
diet_preferences。DatabaseInitializer.runMigrations() 新增迁移(CREATE TABLE IF NOT EXISTS),同步 schema.sql。迁移编号按现有最大编号递增。blood_lipids:
[
{"name": "总胆固醇", "value": 4.5, "unit": "mmol/L", "note": ""},
{"name": "甘油三酯", "value": 1.8, "unit": "mmol/L", "note": "偏高,医生建议控制"}
]
disease_history / medications:
[{"name": "过敏性鼻炎", "note": "三年前确诊,春季易发"}]
entity/HealthStatus.java — @TableName("health_status"),字段对应上表。mapper/HealthStatusMapper.java — MyBatis-Plus BaseMapper。service/HealthStatusService.java(接口)+ impl/HealthStatusServiceImpl.java:
HealthStatus getByUserId(Long userId)HealthStatus save(Long userId, HealthStatus status)(upsert:有则更新,无则插入;filledAt 仅首次填写时赋值)HealthStatusController路由前缀 /api/health-status,统一 @PostMapping:
| 接口 | 说明 | 参数 | 返回 |
|---|---|---|---|
POST /api/health-status/get |
查询当前账号健康现状 | {}(从 JWT 取 userId) |
HealthStatus 或 null |
POST /api/health-status/save |
保存/更新(upsert) | HealthStatus 字段(JSON body) | 保存后的 HealthStatus |
成员饮食偏好复用现有接口:
POST /api/diet/preferences/current-member(已有DietPreferencesService.getCurrentMemberPreferences,前端已有getDietPreferences),不新增重复端点。
AIChatController.sendMessage当前方法 @RequestBody Map<String, String> params 已解析 reportId/surveyId。新增:
String healthStatusStr = params.get("healthStatus"); // JSON 字符串(可空)
String dietPrefsStr = params.get("dietPrefs"); // JSON 字符串(可空)
...
if (healthStatusStr != null && !healthStatusStr.trim().isEmpty()) {
try {
inputs.put("health_status", objectMapper.readValue(healthStatusStr, Map.class));
} catch (Exception e) { log.warn("healthStatus解析失败: {}", e.getMessage()); }
}
if (dietPrefsStr != null && !dietPrefsStr.trim().isEmpty()) {
try {
inputs.put("diet_preferences", objectMapper.readValue(dietPrefsStr, Map.class));
} catch (Exception e) { log.warn("dietPrefs解析失败: {}", e.getMessage()); }
}
pages/health/health-status-form.vue(新增)pages.json(health 分包)。POST /api/health-status/get 回显;保存 POST /api/health-status/save。\d{2,3}/\d{2,3}、血糖 1–30;前端校验 + 后端兜底。health-main/index.vue)「方案制定」旁加「健康档案」按钮。health-plan-summary.vue(修改)onLoad 并行读取:getHealthStatus() + 目标成员 getDietPreferences()(现有接口,后端按所选成员返回)。点「生成方案」时若健康现状未填(get 返回 null)→ uni.showModal:
标题:填写健康现状
内容:尚未填写健康现状,方案可能不够精准。是否先填写?
[去填写] [直接生成]
uni.navigateTo 档案页,返回后重新读取;「直接生成」→ 继续。生成时传参:aiSendMessage({ query, reportId, healthStatus, dietPrefs })。
目标成员选择:现有 subjectId 或所选报告归属成员;dietPrefs 按该成员读取。
utils/api.js(修改)getHealthStatus、saveHealthStatus。getDietPreferences(/api/diet/preferences/current-member)。aiSendMessage 支持透传 healthStatus/dietPrefs(langgraph body 与 dify fallback data 均携带)。| 场景 | 处理 |
|---|---|
| 未填任何调研数据点生成方案 | 弹窗提醒,可「直接生成」继续 |
| 调研数据读取失败(网络/接口) | 静默降级,不阻塞出方案 |
| healthStatus/dietPrefs JSON 解析失败 | 后端记 warn 日志,跳过该 inputs,不抛 500 |
| JSON 字段损坏(DB 中) | 读取时按空处理 |
| 数值非法(身高 999、血压 abc) | 前端校验拦截 + 后端返回 Result.error |
| 空提交 | 至少填 1 项,否则返回 Result.error("请至少填写一项") |
| 超过 6 个月未更新 | 出方案弹窗提示「上次填写时间较早,建议更新」(可跳过) |
mvn clean compile 通过;新增 Service 单测(可选,现有测试较少):
node --check 语法校验(不打包,HBuilderX 打包)。survey_templates 机制管理题目)——字段固定,本期仅按本设计固化。diet_preferences。