# 「当前状态调研」健康现状档案 实现计划 > **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 **目标:** 新增 `health_status` 表与档案页(按 userId 一份),出方案流程(`health-plan-summary.vue`)读取健康现状并在未填时弹窗提醒,健康现状+饮食偏好随 AI 请求注入 Dify inputs,提升方案精准度。 **架构:** 后端新增 Entity/Mapper/Service/Controller(`/api/health-status` get+save,统一 POST),`DatabaseInitializer` 加迁移 229 建表并同步 `schema.sql`;修改 `AIChatController.sendMessage` 解析前端传来的 `healthStatus`/`dietPrefs` JSON 注入 inputs。前端新增独立档案页 `health-status-form.vue`,修改 `health-plan-summary.vue`(读取+弹窗+传参)与 `api.js`(新增 API + aiSendMessage 透传)。饮食偏好复用现有 `/api/diet/preferences/current-member`,不新增端点。 **技术栈:** Java 8 + Spring Boot 2.7.18 + MyBatis-Plus + MySQL 8(JSON 列);uni-app Vue 2 小程序(Options API)。 **规格:** `docs/superpowers/specs/2026-08-13-health-status-survey-design.md` --- ## 文件结构 **后端(cfc-backend/src/main/java/com/etotem/cfc/):** - 创建 `entity/HealthStatus.java` — 实体,`@TableName("health_status")` - 创建 `mapper/HealthStatusMapper.java` — BaseMapper - 创建 `service/HealthStatusService.java` — 接口(getByUserId/save) - 创建 `service/impl/HealthStatusServiceImpl.java` — upsert 实现 - 创建 `controller/HealthStatusController.java` — `/api/health-status` get/save - 修改 `config/DatabaseInitializer.java` — 迁移 229 建表 - 修改 `controller/ai/AIChatController.java` — 注入 health_status/diet_preferences 到 inputs **资源:** - 修改 `cfc-backend/src/main/resources/schema.sql` — 追加 `health_status` 建表语句 **前端(cfc-frontend/):** - 创建 `pages/health/health-status-form.vue` — 独立档案页 - 修改 `pages/health/health-plan-summary.vue` — 读取/弹窗/传参 - 修改 `pages/health-main/index.vue` — 加「健康档案」入口 - 修改 `pages.json` — 注册 health-status-form - 修改 `utils/api.js` — 新增 getHealthStatus/saveHealthStatus + aiSendMessage 透传 **文档:** - 创建 `docs/superpowers/plans/2026-08-13-health-status-survey.md`(本文件) --- ### 任务 1:数据库迁移 + schema.sql **文件:** - 修改:`cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`(文件末尾 `runMigrations()` 内,`// 迁移228` 之后) - 修改:`cfc-backend/src/main/resources/schema.sql`(文件末尾追加) - [ ] **步骤 1:在 DatabaseInitializer 末尾添加迁移 229** 在 `// 迁移228: file_record 添加 file_type 和 description 列` 之后、`runMigrations()` 的闭合 `}` 之前添加: ```java // 迁移229: 创建 health_status 表(当前状态调研-健康现状档案,SS-2026-08-13) try { jdbcTemplate.execute( "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='当前状态调研-健康现状档案'" ); log.info("已创建health_status表"); } catch (Exception e) { log.warn("创建health_status表失败: " + e.getMessage()); } ``` - [ ] **步骤 2:同步 schema.sql** 在 `cfc-backend/src/main/resources/schema.sql` 末尾追加: ```sql -- 当前状态调研-健康现状档案(迁移229) 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='当前状态调研-健康现状档案'; ``` - [ ] **步骤 3:验证编译** 运行:`cd cfc-backend && mvn clean compile` 预期:BUILD SUCCESS(迁移是幂等 try-catch,无需运行时验证) - [ ] **步骤 4:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java cfc-backend/src/main/resources/schema.sql git commit -m "feat(db): health_status 表迁移229 + schema 同步" ``` --- ### 任务 2:Entity + Mapper **文件:** - 创建:`cfc-backend/src/main/java/com/etotem/cfc/entity/HealthStatus.java` - 创建:`cfc-backend/src/main/java/com/etotem/cfc/mapper/HealthStatusMapper.java` - [ ] **步骤 1:创建 HealthStatus 实体** ```java package com.etotem.cfc.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.io.Serializable; import java.math.BigDecimal; import java.util.Date; @Data @TableName("health_status") public class HealthStatus implements Serializable { @TableId(type = IdType.AUTO) private Long id; /** 所属账号(一个账号一份) */ private Long userId; /** 身高(cm) */ private BigDecimal heightCm; /** 体重(kg) */ private BigDecimal weightKg; /** 血压,如 120/80 */ private String bloodPressure; /** 空腹血糖(mmol/L) */ private BigDecimal bloodGlucose; /** 血脂 JSON: [{name,value,unit,note}] */ private String bloodLipids; /** 疾病史 JSON: [{name,note}] */ private String diseaseHistory; /** 在服药物/治疗 JSON: [{name,note}] */ private String medications; /** 其他补充说明 */ private String notes; /** 首次填写时间 */ private Date filledAt; /** 更新时间 */ private Date updatedAt; } ``` - [ ] **步骤 2:创建 Mapper** ```java package com.etotem.cfc.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.etotem.cfc.entity.HealthStatus; import org.apache.ibatis.annotations.Mapper; @Mapper public interface HealthStatusMapper extends BaseMapper { } ``` - [ ] **步骤 3:验证编译** 运行:`cd cfc-backend && mvn clean compile` 预期:BUILD SUCCESS - [ ] **步骤 4:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/entity/HealthStatus.java cfc-backend/src/main/java/com/etotem/cfc/mapper/HealthStatusMapper.java git commit -m "feat(health-status): 新增 HealthStatus 实体与 Mapper" ``` --- ### 任务 3:Service 接口 + 实现(upsert) **文件:** - 创建:`cfc-backend/src/main/java/com/etotem/cfc/service/HealthStatusService.java` - 创建:`cfc-backend/src/main/java/com/etotem/cfc/service/impl/HealthStatusServiceImpl.java` - [ ] **步骤 1:创建 Service 接口** ```java package com.etotem.cfc.service; import com.etotem.cfc.entity.HealthStatus; public interface HealthStatusService { /** 查询指定账号的健康现状,未填写返回 null */ HealthStatus getByUserId(Long userId); /** 保存/更新(upsert:有则更新,无则插入;filledAt 仅首次赋值) */ HealthStatus save(Long userId, HealthStatus status); } ``` - [ ] **步骤 2:创建实现类** ```java package com.etotem.cfc.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.etotem.cfc.entity.HealthStatus; import com.etotem.cfc.mapper.HealthStatusMapper; import com.etotem.cfc.service.HealthStatusService; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.Date; @Service public class HealthStatusServiceImpl implements HealthStatusService { @Resource private HealthStatusMapper healthStatusMapper; @Override public HealthStatus getByUserId(Long userId) { if (userId == null) return null; return healthStatusMapper.selectOne( new LambdaQueryWrapper() .eq(HealthStatus::getUserId, userId)); } @Override public HealthStatus save(Long userId, HealthStatus status) { if (userId == null) throw new IllegalArgumentException("userId不能为空"); if (status == null) throw new IllegalArgumentException("健康现状不能为空"); HealthStatus existing = getByUserId(userId); status.setUserId(userId); if (existing == null) { status.setId(null); status.setFilledAt(new Date()); status.setUpdatedAt(new Date()); healthStatusMapper.insert(status); } else { status.setId(existing.getId()); status.setFilledAt(existing.getFilledAt()); status.setUpdatedAt(new Date()); healthStatusMapper.updateById(status); } return getByUserId(userId); } } ``` - [ ] **步骤 3:验证编译** 运行:`cd cfc-backend && mvn clean compile` 预期:BUILD SUCCESS - [ ] **步骤 4:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/service/HealthStatusService.java cfc-backend/src/main/java/com/etotem/cfc/service/impl/HealthStatusServiceImpl.java git commit -m "feat(health-status): HealthStatusService upsert 实现" ``` --- ### 任务 4:HealthStatusController **文件:** - 创建:`cfc-backend/src/main/java/com/etotem/cfc/controller/HealthStatusController.java` > 检查路由冲突:现有 Controller 无 `/api/health-status` 前缀(可用 `grep -rn 'health-status' cfc-backend/src/main/java/com/etotem/cfc/controller/` 确认)。类名 `HealthStatusController` 无 Bean 冲突(全库唯一)。 - [ ] **步骤 1:创建 Controller** ```java package com.etotem.cfc.controller; import com.etotem.cfc.common.Result; import com.etotem.cfc.entity.HealthStatus; import com.etotem.cfc.service.HealthStatusService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestAttribute; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.math.BigDecimal; @Tag(name = "健康现状档案", description = "当前状态调研-健康现状档案接口") @RestController @RequestMapping("/api/health-status") public class HealthStatusController { @Resource private HealthStatusService healthStatusService; @Operation(summary = "查询当前账号健康现状(未填写返回 null)") @PostMapping("/get") public Result get(@RequestAttribute("userId") Long userId) { return Result.success(healthStatusService.getByUserId(userId)); } @Operation(summary = "保存/更新当前账号健康现状(upsert)") @PostMapping("/save") public Result save( @RequestAttribute("userId") Long userId, @RequestBody HealthStatus status) { if (status == null || isEmpty(status)) { return Result.error("请至少填写一项"); } String validateMsg = validate(status); if (validateMsg != null) { return Result.error(validateMsg); } return Result.success(healthStatusService.save(userId, status)); } /** 至少填写一项才算有效 */ private boolean isEmpty(HealthStatus s) { return s.getHeightCm() == null && s.getWeightKg() == null && s.getBloodPressure() == null && s.getBloodGlucose() == null && s.getBloodLipids() == null && s.getDiseaseHistory() == null && s.getMedications() == null && s.getNotes() == null; } /** 数值校验,返回错误信息或 null */ private String validate(HealthStatus s) { if (s.getHeightCm() != null && (s.getHeightCm().compareTo(BigDecimal.valueOf(30)) < 0 || s.getHeightCm().compareTo(BigDecimal.valueOf(250)) > 0)) { return "身高需在 30-250 cm 之间"; } if (s.getWeightKg() != null && (s.getWeightKg().compareTo(BigDecimal.valueOf(3)) < 0 || s.getWeightKg().compareTo(BigDecimal.valueOf(300)) > 0)) { return "体重需在 3-300 kg 之间"; } if (s.getBloodPressure() != null && !s.getBloodPressure().matches("\\d{2,3}/\\d{2,3}")) { return "血压格式应为 120/80"; } if (s.getBloodGlucose() != null && (s.getBloodGlucose().compareTo(BigDecimal.ONE) < 0 || s.getBloodGlucose().compareTo(BigDecimal.valueOf(30)) > 0)) { return "血糖需在 1-30 mmol/L 之间"; } return null; } } ``` - [ ] **步骤 2:检查路由冲突** 运行:`grep -rn '@Mapping' cfc-backend/src/main/java/com/etotem/cfc/controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u | grep health-status` 预期:仅输出 `/api/health-status/get` 与 `/api/health-status/save` - [ ] **步骤 3:验证编译** 运行:`cd cfc-backend && mvn clean compile` 预期:BUILD SUCCESS - [ ] **步骤 4:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/controller/HealthStatusController.java git commit -m "feat(health-status): HealthStatusController get/save 接口" ``` --- ### 任务 5:AIChatController 注入健康现状到 Dify inputs **文件:** - 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java` > 当前 `sendMessage` 已用 `@RequestBody Map params` 解析 `query/conversationId/reportId/surveyId`,在 `inputs = familyContextService.buildContext(...)` 之后、`aiService.enrichInputsWithMemory(...)` 之前插入解析逻辑。`ObjectMapper` 用新建实例(与 `AIService` 中 `private final ObjectMapper objectMapper = new ObjectMapper();` 同模式)。 - [ ] **步骤 1:添加 import 与 ObjectMapper 字段** 在类头部现有 import 之后添加: ```java import com.fasterxml.jackson.databind.ObjectMapper; ``` 在类内字段区(`private TaskParseService taskParseService;` 之后)添加: ```java private final ObjectMapper objectMapper = new ObjectMapper(); ``` - [ ] **步骤 2:在 sendMessage 中解析并注入** 在 `Map inputs;` 与 `if (reportId != null && surveyId != null) {...}` 组装块**之后**、`// 注入mascot信息到Dify inputs` 之前,插入: ```java // 注入当前状态调研数据(健康现状 + 饮食偏好) String healthStatusStr = params.get("healthStatus"); String dietPrefsStr = params.get("dietPrefs"); 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()); } } ``` - [ ] **步骤 3:验证编译** 运行:`cd cfc-backend && mvn clean compile` 预期:BUILD SUCCESS - [ ] **步骤 4:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java git commit -m "feat(ai): AIChatController 注入 health_status/diet_preferences 到 Dify inputs" ``` --- ### 任务 6:前端 api.js **文件:** - 修改:`cfc-frontend/utils/api.js` - [ ] **步骤 1:新增健康现状 API** 在 `getDietPreferences`(约 2614 行)附近添加: ```js export const getHealthStatus = () => request('/api/health-status/get', 'POST', {}) export const saveHealthStatus = (data) => request('/api/health-status/save', 'POST', data) ``` - [ ] **步骤 2:修改 aiSendMessage 透传 healthStatus/dietPrefs** 当前代码(约 1317-1345 行): ```js export const aiSendMessage = (data) => { var query = data && data.query ? data.query : '' var conversationId = data && data.conversationId ? data.conversationId : '' var userId = uni.getStorageSync('userId') || '' var langgraphBody = { query: query, user: userId, user_id: userId, conversation_id: conversationId, response_mode: 'blocking', bot_name: 'AI管家' } if (data && data.reportId) { langgraphBody.inputs = { report_id: data.reportId } } return langgraphRequest({ langgraphPath: '/api/v1/chat/completion', langgraphBody: langgraphBody, difyFallbackPath: '/api/ai/chat/send', difyFallbackData: data, transform: ... }) } ``` 将 `if (data && data.reportId) { langgraphBody.inputs = { report_id: data.reportId } }` 替换为: ```js var inputs = {} if (data && data.reportId) { inputs.report_id = data.reportId } if (data && data.healthStatus) { inputs.health_status = data.healthStatus } if (data && data.dietPrefs) { inputs.diet_preferences = data.dietPrefs } if (Object.keys(inputs).length > 0) { langgraphBody.inputs = inputs } ``` > `difyFallbackData: data` 已透传整个 data 对象,`healthStatus`/`dietPrefs` 会随 POST body 到达 `/api/ai/chat/send`。**注意:** 前端在 Task 8 中组装 `sendData` 时必须用 `JSON.stringify()` 把对象转成 JSON 字符串(后端 `Map` 接收,再 `readValue` 解析回对象),否则 Jackson 无法把 JSON 对象反序列化进 String 字段会报错。 - [ ] **步骤 3:语法校验** 运行:`cd cfc-frontend && node --check utils/api.js` 预期:无输出(exit code 0)。若 node 版本对 ESM/`import` 报错(`--check` 在 CommonJS 下会报 `Cannot use import statement`),则用 `npx babel-node` 或跳过该文件级校验,改为人工核对修改段括号/逗号闭合。 - [ ] **步骤 4:Commit** ```bash git add cfc-frontend/utils/api.js git commit -m "feat(frontend): api.js 新增 health-status API 并透传 healthStatus/dietPrefs" ``` --- ### 任务 7:独立档案页 health-status-form.vue + 注册 + 入口 **文件:** - 创建:`cfc-frontend/pages/health/health-status-form.vue` - 修改:`cfc-frontend/pages.json`(health 分包,`health-plan-summary` 之后注册) - 修改:`cfc-frontend/pages/health-main/index.vue`(「智能健康方案」feat-card 旁加「健康档案」card) - [ ] **步骤 1:创建档案页** > 遵循小程序限制:禁止可选链 `?.`(用 `&&`)、禁止 `:key` 表达式(用 `:key="idx"` 索引或方法)、禁止 `new Date(string)`。Vue 2 Options API。 ```vue