|
|
@@ -0,0 +1,204 @@
|
|
|
+# 「当前状态调研」健康现状档案 设计
|
|
|
+
|
|
|
+> 日期:2026-08-13
|
|
|
+> 分支:cfclub
|
|
|
+> 状态:待评审
|
|
|
+
|
|
|
+## 1. 背景与目标
|
|
|
+
|
|
|
+当前出方案流程(`health-plan-summary.vue`:选报告 → 提需求 → AI 生成方案)只接收**健康报告 + 用户自由文本需求**,没有结构化的"当前健康现状"输入。AI 出方案时不知道用户身高体重、血压血糖血脂、疾病史、用药情况、过敏源等关键信息,方案精度受限。
|
|
|
+
|
|
|
+**用户需求**(已确认):
|
|
|
+
|
|
|
+1. 在用户要出方案时进行"当前状态调研",以更准确地出方案。
|
|
|
+2. 用户**可以不填**,但要提醒"不保证准确"。
|
|
|
+3. 调研字段:身高、体重、血压、血糖、血脂、过敏源、食物喜好、在服药物/治疗、疾病史等。
|
|
|
+
|
|
|
+**设计目标**:
|
|
|
+
|
|
|
+- 出方案前收集结构化健康现状,随 AI 请求注入上下文,提升方案精准度。
|
|
|
+- 双入口:出方案流程内提示填写 + 独立档案页随时维护。
|
|
|
+- 不填可跳过,但需明确提醒精度风险。
|
|
|
+
|
|
|
+## 2. 需求决策(头脑风暴确认)
|
|
|
+
|
|
|
+| # | 问题 | 决策 |
|
|
|
+|---|------|------|
|
|
|
+| 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) |
|
|
|
+
|
|
|
+## 3. 目标架构
|
|
|
+
|
|
|
+```
|
|
|
+独立档案页 (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 生成方案
|
|
|
+```
|
|
|
+
|
|
|
+- 健康现状按账号一份;饮食偏好按家庭成员(现有逻辑不变)。
|
|
|
+- 出方案针对某个成员时:healthStatus 取当前账号,dietPrefs 取该成员的 `diet_preferences`(无则空)。
|
|
|
+- 提醒逻辑:出方案时**健康现状未填**(或已超过 6 个月未更新)→ `uni.showModal` 弹窗 →「去填写」跳档案页 /「直接生成」继续。
|
|
|
+
|
|
|
+## 4. 数据库设计
|
|
|
+
|
|
|
+### 4.1 新表 `health_status`
|
|
|
+
|
|
|
+```sql
|
|
|
+CREATE 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`。迁移编号按现有最大编号递增。
|
|
|
+
|
|
|
+### 4.2 JSON 结构约定
|
|
|
+
|
|
|
+**blood_lipids**:
|
|
|
+```json
|
|
|
+[
|
|
|
+ {"name": "总胆固醇", "value": 4.5, "unit": "mmol/L", "note": ""},
|
|
|
+ {"name": "甘油三酯", "value": 1.8, "unit": "mmol/L", "note": "偏高,医生建议控制"}
|
|
|
+]
|
|
|
+```
|
|
|
+
|
|
|
+**disease_history / medications**:
|
|
|
+```json
|
|
|
+[{"name": "过敏性鼻炎", "note": "三年前确诊,春季易发"}]
|
|
|
+```
|
|
|
+
|
|
|
+## 5. 后端设计
|
|
|
+
|
|
|
+### 5.1 新 Entity / Mapper / Service
|
|
|
+
|
|
|
+- `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` 仅首次填写时赋值)
|
|
|
+
|
|
|
+### 5.2 新 Controller `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`),不新增重复端点。
|
|
|
+
|
|
|
+### 5.3 修改 `AIChatController.sendMessage`
|
|
|
+
|
|
|
+当前方法 `@RequestBody Map<String, String> params` 已解析 `reportId`/`surveyId`。新增:
|
|
|
+
|
|
|
+```java
|
|
|
+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()); }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- 解析失败静默降级(记 warn 日志),不阻塞出方案。
|
|
|
+
|
|
|
+## 6. 前端设计
|
|
|
+
|
|
|
+### 6.1 独立档案页 `pages/health/health-status-form.vue`(新增)
|
|
|
+
|
|
|
+- 注册到 `pages.json`(health 分包)。
|
|
|
+- 表单字段:身高、体重、血压、血糖、血脂(多行,每行 name/value/unit/note)、疾病史(标签+自定义)、用药/治疗(标签+自定义)、备注。
|
|
|
+- 进入时 `POST /api/health-status/get` 回显;保存 `POST /api/health-status/save`。
|
|
|
+- 校验:身高 30–250、体重 3–300、血压 `\d{2,3}/\d{2,3}`、血糖 1–30;前端校验 + 后端兜底。
|
|
|
+- 至少填写 1 项才可保存(避免空记录)。
|
|
|
+- 入口:健康主页(`health-main/index.vue`)「方案制定」旁加「健康档案」按钮。
|
|
|
+
|
|
|
+### 6.2 出方案流程 `health-plan-summary.vue`(修改)
|
|
|
+
|
|
|
+- `onLoad` 并行读取:`getHealthStatus()` + 目标成员 `getDietPreferences()`(现有接口,后端按所选成员返回)。
|
|
|
+- 点「生成方案」时若**健康现状未填**(`get` 返回 null)→ `uni.showModal`:
|
|
|
+
|
|
|
+ ```
|
|
|
+ 标题:填写健康现状
|
|
|
+ 内容:尚未填写健康现状,方案可能不够精准。是否先填写?
|
|
|
+ [去填写] [直接生成]
|
|
|
+ ```
|
|
|
+
|
|
|
+ - 「去填写」→ `uni.navigateTo` 档案页,返回后重新读取;「直接生成」→ 继续。
|
|
|
+- 生成时传参:`aiSendMessage({ query, reportId, healthStatus, dietPrefs })`。
|
|
|
+- 目标成员选择:现有 `subjectId` 或所选报告归属成员;`dietPrefs` 按该成员读取。
|
|
|
+
|
|
|
+### 6.3 `utils/api.js`(修改)
|
|
|
+
|
|
|
+- 新增:`getHealthStatus`、`saveHealthStatus`。
|
|
|
+- 复用现有:`getDietPreferences`(/api/diet/preferences/current-member)。
|
|
|
+- `aiSendMessage` 支持透传 `healthStatus`/`dietPrefs`(langgraph body 与 dify fallback data 均携带)。
|
|
|
+
|
|
|
+## 7. 错误处理与边界
|
|
|
+
|
|
|
+| 场景 | 处理 |
|
|
|
+|------|------|
|
|
|
+| 未填任何调研数据点生成方案 | 弹窗提醒,可「直接生成」继续 |
|
|
|
+| 调研数据读取失败(网络/接口) | 静默降级,不阻塞出方案 |
|
|
|
+| healthStatus/dietPrefs JSON 解析失败 | 后端记 warn 日志,跳过该 inputs,不抛 500 |
|
|
|
+| JSON 字段损坏(DB 中) | 读取时按空处理 |
|
|
|
+| 数值非法(身高 999、血压 abc) | 前端校验拦截 + 后端返回 `Result.error` |
|
|
|
+| 空提交 | 至少填 1 项,否则返回 `Result.error("请至少填写一项")` |
|
|
|
+| 超过 6 个月未更新 | 出方案弹窗提示「上次填写时间较早,建议更新」(可跳过) |
|
|
|
+
|
|
|
+## 8. 测试策略
|
|
|
+
|
|
|
+- **后端**:`mvn clean compile` 通过;新增 Service 单测(可选,现有测试较少):
|
|
|
+ - upsert 幂等(重复 save 不产生多行,user_id 唯一)
|
|
|
+ - get 未填返回 null
|
|
|
+- **前端**:`node --check` 语法校验(不打包,HBuilderX 打包)。
|
|
|
+- **手工验证清单**:
|
|
|
+ 1. 档案页首次填写 → 保存 → 重新进入回显
|
|
|
+ 2. 重复保存 → 仍只有一行记录
|
|
|
+ 3. 未填状态点生成方案 → 弹窗出现 → 直接生成可继续
|
|
|
+ 4. 填写后点生成方案 → 不再弹窗 → AI 返回方案
|
|
|
+ 5. 校验非法值(身高 999、血压 abc)→ 拦截提示
|
|
|
+
|
|
|
+## 9. 非目标(本次不做)
|
|
|
+
|
|
|
+- 不做"调研模板配置化"(复用 `survey_templates` 机制管理题目)——字段固定,本期仅按本设计固化。
|
|
|
+- 不做过敏源/食物喜好的新表迁移——继续用 `diet_preferences`。
|
|
|
+- 不做 AiContextService 新 intent——采用前端随请求传参方案。
|
|
|
+- 不做 LangGraph 侧改造(langgraph 服务当前为空壳,实际走 Dify fallback;若后续启用 langgraph,传参透传已在前端 api.js 层预留)。
|