Просмотр исходного кода

HL-01: 健康方案默认选本人+问卷代入自己+健康阅卷入口(2026-11-07)

Agnes Agent 5 дней назад
Родитель
Сommit
77374f45da

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -3471,6 +3471,9 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
         // 迁移456: family_join_requests 记录审批时指定的身份(可追溯)
         // 迁移456: family_join_requests 记录审批时指定的身份(可追溯)
         ensureColumn("family_join_requests", "assigned_generation", "INT NULL COMMENT '审批时指定的绝对辈分'");
         ensureColumn("family_join_requests", "assigned_generation", "INT NULL COMMENT '审批时指定的绝对辈分'");
         ensureColumn("family_join_requests", "assigned_peer_type", "VARCHAR(10) NULL COMMENT '审批时指定的同辈类型: sibling/spouse'");
         ensureColumn("family_join_requests", "assigned_peer_type", "VARCHAR(10) NULL COMMENT '审批时指定的同辈类型: sibling/spouse'");
+
+        // 迁移457: survey_records 添加 source 列(区分定期调研/健康阅卷,HL-01 健康阅卷入口)
+        ensureColumn("survey_records", "source", "VARCHAR(32) DEFAULT 'periodic' COMMENT '记录来源: periodic=定期调研 / health_review=健康阅卷'");
     }
     }
 
 
     /**
     /**

+ 12 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/SurveyController.java

@@ -122,12 +122,23 @@ public class SurveyController {
         Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         Long templateId = ParamUtils.getLong(params.get("templateId"));
         Long templateId = ParamUtils.getLong(params.get("templateId"));
         String answersJson = (String) params.get("answersJson");
         String answersJson = (String) params.get("answersJson");
+        String source = (String) params.get("source");
         Date ps = params.get("periodStart") != null ? new Date(ParamUtils.getLong(params.get("periodStart"))) : null;
         Date ps = params.get("periodStart") != null ? new Date(ParamUtils.getLong(params.get("periodStart"))) : null;
         Date pe = params.get("periodEnd") != null ? new Date(ParamUtils.getLong(params.get("periodEnd"))) : null;
         Date pe = params.get("periodEnd") != null ? new Date(ParamUtils.getLong(params.get("periodEnd"))) : null;
         @SuppressWarnings("unchecked")
         @SuppressWarnings("unchecked")
         List<Map<String, Object>> riskHits = params.get("riskHits") instanceof List
         List<Map<String, Object>> riskHits = params.get("riskHits") instanceof List
                 ? (List<Map<String, Object>>) params.get("riskHits") : null;
                 ? (List<Map<String, Object>>) params.get("riskHits") : null;
-        return Result.success(surveyService.submitSurvey(userId, memberId, templateId, answersJson, ps, pe, riskHits));
+        return Result.success(surveyService.submitSurvey(userId, memberId, templateId, answersJson, ps, pe, riskHits, source));
+    }
+
+    @Operation(summary = "查询最后一条健康阅卷记录(HL-01)")
+    @PostMapping("/health-review/last")
+    public Result<SurveyRecord> lastHealthReview(
+            @RequestBody Map<String, Object> params,
+            @RequestAttribute("userId") Long userId,
+            @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
+        return Result.success(surveyService.getLastHealthReview(userId, memberId));
     }
     }
 
 
     @Operation(summary = "查询调研历史")
     @Operation(summary = "查询调研历史")

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/SurveyRecord.java

@@ -19,4 +19,6 @@ public class SurveyRecord implements Serializable {
     private Date submittedAt;
     private Date submittedAt;
     private Date periodStart;
     private Date periodStart;
     private Date periodEnd;
     private Date periodEnd;
+    /** 记录来源: periodic=定期调研 / health_review=健康阅卷(缺省 periodic) */
+    private String source;
 }
 }

+ 18 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/SurveyService.java

@@ -28,6 +28,24 @@ public interface SurveyService {
                                       Date periodStart, Date periodEnd, List<Map<String, Object>> riskHits) {
                                       Date periodStart, Date periodEnd, List<Map<String, Object>> riskHits) {
         return submitSurvey(userId, memberId, templateId, answersJson, periodStart, periodEnd);
         return submitSurvey(userId, memberId, templateId, answersJson, periodStart, periodEnd);
     }
     }
+
+    /**
+     * 提交调研答案(HL-01:source 区分定期调研/健康阅卷)。
+     *
+     * <p>source 为 {@code health_review} 时仅写 survey_records(复用健康问卷内容),
+     * 不校验模板/周期;其余值按定期调研处理(向后兼容,缺省 {@code periodic})。
+     */
+    default SurveyRecord submitSurvey(Long userId, Long memberId, Long templateId, String answersJson,
+                                      Date periodStart, Date periodEnd, List<Map<String, Object>> riskHits, String source) {
+        return submitSurvey(userId, memberId, templateId, answersJson, periodStart, periodEnd, riskHits);
+    }
+
+    /**
+     * 查询最后一条健康阅卷记录(source=health_review,按提交时间倒序取最近一条)。
+     *
+     * @return 最近一条健康阅卷;无记录返回 null
+     */
+    SurveyRecord getLastHealthReview(Long userId, Long memberId);
     List<SurveyRecord> getSurveyHistory(Long userId, Long memberId);
     List<SurveyRecord> getSurveyHistory(Long userId, Long memberId);
     String aiGenerateQuestions(String dimension, String topic);
     String aiGenerateQuestions(String dimension, String topic);
 }
 }

+ 49 - 12
cfc-backend/src/main/java/com/etotem/cfc/service/impl/SurveyServiceImpl.java

@@ -152,13 +152,41 @@ public class SurveyServiceImpl implements SurveyService {
 
 
     @Override
     @Override
     public SurveyRecord submitSurvey(Long userId, Long memberId, Long templateId, String answersJson, Date periodStart, Date periodEnd) {
     public SurveyRecord submitSurvey(Long userId, Long memberId, Long templateId, String answersJson, Date periodStart, Date periodEnd) {
-        if (templateId == null) {
+        return doSubmit(userId, memberId, templateId, answersJson, periodStart, periodEnd, null);
+    }
+
+    @Override
+    public SurveyRecord submitSurvey(Long userId, Long memberId, Long templateId, String answersJson,
+                                     Date periodStart, Date periodEnd, List<Map<String, Object>> riskHits) {
+        String merged = mergeRiskHits(answersJson, riskHits);
+        return doSubmit(userId, memberId, templateId, merged, periodStart, periodEnd, null);
+    }
+
+    @Override
+    public SurveyRecord submitSurvey(Long userId, Long memberId, Long templateId, String answersJson,
+                                     Date periodStart, Date periodEnd, List<Map<String, Object>> riskHits, String source) {
+        String merged = mergeRiskHits(answersJson, riskHits);
+        return doSubmit(userId, memberId, templateId, merged, periodStart, periodEnd, source);
+    }
+
+    /**
+     * 核心落库:source=health_review 时不校验模板/周期(健康阅卷复用问卷但不挂模板),
+     * 其余按定期调研处理(templateId 必填,period 由服务端按模板 cycle_type 计算)。
+     */
+    private SurveyRecord doSubmit(Long userId, Long memberId, Long templateId, String answersJson,
+                                  Date periodStart, Date periodEnd, String source) {
+        String src = (source == null || source.trim().isEmpty()) ? "periodic" : source.trim();
+        boolean isHealthReview = "health_review".equals(src);
+        if (!isHealthReview && templateId == null) {
             throw new IllegalArgumentException("缺少调研模板ID,请从调研列表进入");
             throw new IllegalArgumentException("缺少调研模板ID,请从调研列表进入");
         }
         }
         memberId = resolveMemberId(userId, memberId);
         memberId = resolveMemberId(userId, memberId);
-        SurveyTemplate template = surveyTemplateMapper.selectById(templateId);
-        if (template == null) {
-            throw new IllegalArgumentException("调研模板不存在");
+        SurveyTemplate template = null;
+        if (templateId != null) {
+            template = surveyTemplateMapper.selectById(templateId);
+            if (template == null) {
+                throw new IllegalArgumentException("调研模板不存在");
+            }
         }
         }
         if (memberId == null) {
         if (memberId == null) {
             throw new IllegalArgumentException("无法确定成员ID,请先完善家庭成员信息");
             throw new IllegalArgumentException("无法确定成员ID,请先完善家庭成员信息");
@@ -169,19 +197,28 @@ public class SurveyServiceImpl implements SurveyService {
         record.setMemberId(memberId);
         record.setMemberId(memberId);
         record.setTemplateId(templateId);
         record.setTemplateId(templateId);
         record.setAnswersJson(answersJson);
         record.setAnswersJson(answersJson);
-        // 周期由服务端按模板 cycle_type 统一计算并落库,忽略客户端传参(防止前端时钟偏差/篡改导致周期错乱)
-        Date[] period = computeCurrentPeriod(template.getCycleType());
-        record.setPeriodStart(period[0]);
-        record.setPeriodEnd(period[1]);
+        record.setSource(src);
+        if (template != null) {
+            // 周期由服务端按模板 cycle_type 统一计算并落库,忽略客户端传参
+            Date[] period = computeCurrentPeriod(template.getCycleType());
+            record.setPeriodStart(period[0]);
+            record.setPeriodEnd(period[1]);
+        }
         surveyRecordMapper.insert(record);
         surveyRecordMapper.insert(record);
         return surveyRecordMapper.selectById(record.getId());
         return surveyRecordMapper.selectById(record.getId());
     }
     }
 
 
     @Override
     @Override
-    public SurveyRecord submitSurvey(Long userId, Long memberId, Long templateId, String answersJson,
-                                     Date periodStart, Date periodEnd, List<Map<String, Object>> riskHits) {
-        String merged = mergeRiskHits(answersJson, riskHits);
-        return submitSurvey(userId, memberId, templateId, merged, periodStart, periodEnd);
+    public SurveyRecord getLastHealthReview(Long userId, Long memberId) {
+        memberId = resolveMemberId(userId, memberId);
+        if (memberId == null) return null;
+        List<SurveyRecord> list = surveyRecordMapper.selectList(
+                new LambdaQueryWrapper<SurveyRecord>()
+                        .eq(SurveyRecord::getMemberId, memberId)
+                        .eq(SurveyRecord::getSource, "health_review")
+                        .orderByDesc(SurveyRecord::getSubmittedAt)
+                        .last("LIMIT 1"));
+        return (list != null && !list.isEmpty()) ? list.get(0) : null;
     }
     }
 
 
     /**
     /**

+ 1 - 0
cfc-backend/src/main/resources/schema.sql

@@ -5247,6 +5247,7 @@ CREATE TABLE IF NOT EXISTS survey_records (
     submitted_at  DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '提交时间',
     submitted_at  DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '提交时间',
     period_start  DATE COMMENT '周期开始',
     period_start  DATE COMMENT '周期开始',
     period_end    DATE COMMENT '周期结束',
     period_end    DATE COMMENT '周期结束',
+    source        VARCHAR(32) DEFAULT 'periodic' COMMENT '记录来源: periodic=定期调研 / health_review=健康阅卷',
     INDEX idx_member (member_id),
     INDEX idx_member (member_id),
     INDEX idx_template (template_id)
     INDEX idx_template (template_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='定期调研记录';
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='定期调研记录';

+ 20 - 0
cfc-frontend/pages/health-main/index.vue

@@ -214,6 +214,16 @@
             <button v-else class="quiz-btn" @click.stop="goQuiz(quiz.code)">开始</button>
             <button v-else class="quiz-btn" @click.stop="goQuiz(quiz.code)">开始</button>
           </view>
           </view>
         </view>
         </view>
+
+        <!-- 健康阅卷入口(HL-01):问卷式记录,多次填写以最后一次为准 -->
+        <view class="data-card" @click="goHealthReview">
+          <view class="data-card-icon">📝</view>
+          <view class="data-card-info">
+            <text class="data-card-title">健康阅卷</text>
+            <text class="data-card-desc">逐项填写健康问卷 · 多次填写保留历史记录</text>
+          </view>
+          <text class="data-card-arrow">›</text>
+        </view>
       </view>
       </view>
 
 
       <view class="section section-data-view">
       <view class="section section-data-view">
@@ -711,6 +721,16 @@ export default {
       }
       }
       uni.navigateTo({ url: url })
       uni.navigateTo({ url: url })
     },
     },
+    goHealthReview: function() {
+      // 健康阅卷(HL-01):问卷式记录,多次填写以最后一次为准;需登录后填写
+      var url = '/pages/health/health-survey?source=health_review&entry=health_review'
+      if (!uni.getStorageSync('token')) {
+        this._pendingSurveyUrl = url
+        this.openLoginSheet({ redirect: url })
+        return
+      }
+      uni.navigateTo({ url: url })
+    },
     // 拉取本期待填调研(服务端按 cycle_type 计算当前周期)
     // 拉取本期待填调研(服务端按 cycle_type 计算当前周期)
     loadPendingSurveys: function() {
     loadPendingSurveys: function() {
       var self = this
       var self = this

+ 10 - 1
cfc-frontend/pages/health/components/MemberChipPicker.vue

@@ -85,6 +85,8 @@ export default {
           nickname: m.nickname,
           nickname: m.nickname,
           gender: m.gender || '',
           gender: m.gender || '',
           age: m.age != null ? m.age : null,
           age: m.age != null ? m.age : null,
+          isSelf: !!m.isSelf,
+          userId: m.userId != null ? m.userId : '',
           selected: !!selected[String(m.id)]
           selected: !!selected[String(m.id)]
         })
         })
       }
       }
@@ -142,6 +144,8 @@ export default {
           nickname: String(nm),
           nickname: String(nm),
           gender: m.gender || '',
           gender: m.gender || '',
           age: m.age != null ? m.age : null,
           age: m.age != null ? m.age : null,
+          isSelf: !!m.isSelf,
+          userId: m.userId != null ? m.userId : '',
           selected: false
           selected: false
         })
         })
       }
       }
@@ -150,7 +154,12 @@ export default {
       for (i = 0; i < mapped.length; i++) idSet[String(mapped[i].id)] = true
       for (i = 0; i < mapped.length; i++) idSet[String(mapped[i].id)] = true
       selectedIds = selectedIds.filter(function(id) { return !!idSet[String(id)] })
       selectedIds = selectedIds.filter(function(id) { return !!idSet[String(id)] })
       if (selectedIds.length === 0 && mapped.length > 0) {
       if (selectedIds.length === 0 && mapped.length > 0) {
-        selectedIds = [String(mapped[0].id)]
+        // 默认优先选「本人(isSelf)」,无 isSelf 时回退列表第一个成员
+        var selfIndex = 0
+        for (i = 0; i < mapped.length; i++) {
+          if (mapped[i].isSelf) { selfIndex = i; break }
+        }
+        selectedIds = [String(mapped[selfIndex].id)]
       }
       }
       for (i = 0; i < mapped.length; i++) {
       for (i = 0; i < mapped.length; i++) {
         mapped[i].selected = selectedIds.indexOf(String(mapped[i].id)) >= 0
         mapped[i].selected = selectedIds.indexOf(String(mapped[i].id)) >= 0

+ 18 - 5
cfc-frontend/pages/health/health-plan-summary.vue

@@ -441,9 +441,20 @@ selectedMembers: function() {
     },
     },
     onPickerLoaded: function(payload) {
     onPickerLoaded: function(payload) {
       if (!payload) return
       if (!payload) return
-      this.selectedMemberIds = payload.selectedIds || []
-      this.pickerMembers = payload.members || []
-      var formId = (payload.selectedIds && payload.selectedIds[0]) || ''
+      var ids = (payload.selectedIds || []).slice()
+      var members = payload.members || []
+      // 防御:组件默认选中未生效(selectedIds 为空)且成员列表含「本人」→ 补选 isSelf
+      if (ids.length === 0 && members.length > 0) {
+        for (var i = 0; i < members.length; i++) {
+          if (members[i] && members[i].isSelf) {
+            ids.push(String(members[i].id))
+            break
+          }
+        }
+      }
+      this.selectedMemberIds = ids
+      this.pickerMembers = members
+      var formId = (ids && ids[0]) || ''
       if (formId) {
       if (formId) {
         this.activeFormMemberId = String(formId)
         this.activeFormMemberId = String(formId)
         var self = this
         var self = this
@@ -484,7 +495,8 @@ selectedMembers: function() {
       var key = String(mid || '__account')
       var key = String(mid || '__account')
       // 本地已有该成员快照 → 无需重复请求
       // 本地已有该成员快照 → 无需重复请求
       if (self.memberFormMap[key]) return
       if (self.memberFormMap[key]) return
-      getHealthStatus(mid || undefined).then(function(res) {
+      // 预填数据源 = 当前登录用户自己(账号级,不传 memberId);保存仍按选中成员(payload.memberId = mid)
+      getHealthStatus().then(function(res) {
         if (res && res.code === 200 && res.data) {
         if (res && res.code === 200 && res.data) {
           var d = res.data
           var d = res.data
           self.$set(self.memberFormMap, key, {
           self.$set(self.memberFormMap, key, {
@@ -511,7 +523,8 @@ selectedMembers: function() {
       var mid = memberId || self.activeFormMemberId || ''
       var mid = memberId || self.activeFormMemberId || ''
       var key = String(mid || '__account')
       var key = String(mid || '__account')
       if (self.memberDietMap[key]) return
       if (self.memberDietMap[key]) return
-      getDietPreferences({ memberId: mid }).then(function(res) {
+      // 预填数据源 = 当前登录用户自己(账号级,不传 memberId);保存仍按选中成员
+      getDietPreferences({}).then(function(res) {
         var d = (res && res.code === 200 && res.data) ? res.data : null
         var d = (res && res.code === 200 && res.data) ? res.data : null
         self.$set(self.memberDietMap, key, self.normalizeDiet(d))
         self.$set(self.memberDietMap, key, self.normalizeDiet(d))
       }).catch(function() {
       }).catch(function() {

+ 79 - 4
cfc-frontend/pages/health/health-survey.vue

@@ -20,7 +20,7 @@
 </template>
 </template>
 
 
 <script>
 <script>
-import { getHealthStatus, saveHealthStatus, getDietPreferences, saveDietPreferences, getMyMembership, getFamilyMemberList } from '../../utils/api'
+import { getHealthStatus, saveHealthStatus, getDietPreferences, saveDietPreferences, getMyMembership, getFamilyMemberList, submitSurvey, getLastHealthReview } from '../../utils/api'
 import ComprehensiveSurvey from '@/pages/health/components/ComprehensiveSurvey.vue'
 import ComprehensiveSurvey from '@/pages/health/components/ComprehensiveSurvey.vue'
 
 
 var ALL_MODULES = ['basic', 'status', 'diet', 'disease', 'bowel', 'sleep', 'med', 'allergy']
 var ALL_MODULES = ['basic', 'status', 'diet', 'disease', 'bowel', 'sleep', 'med', 'allergy']
@@ -108,10 +108,14 @@ export default {
       },
       },
       dietSnapshot: normalizeDiet(null),
       dietSnapshot: normalizeDiet(null),
       memberProfile: {},
       memberProfile: {},
-      _loaded: false
+      _loaded: false,
+      // 健康阅卷模式标记(source=health_review,HL-01)
+      reviewSource: ''
     }
     }
   },
   },
   onLoad: function(options) {
   onLoad: function(options) {
+    // 0. 解析来源:health_review=健康阅卷模式(仅写 survey_records,不自检/不写 health_status)
+    this.reviewSource = (options && options.source) || ''
     // 1. 解析 modules(缺省/空串/全非法 → 全量 8 模块)
     // 1. 解析 modules(缺省/空串/全非法 → 全量 8 模块)
     if (options && options.modules) {
     if (options && options.modules) {
       var list = String(options.modules).split(',').filter(function(m) { return ALL_MODULES.indexOf(m) >= 0 })
       var list = String(options.modules).split(',').filter(function(m) { return ALL_MODULES.indexOf(m) >= 0 })
@@ -119,7 +123,12 @@ export default {
     }
     }
     // 2. 当前成员快照(与 preferences.vue 一致)
     // 2. 当前成员快照(与 preferences.vue 一致)
     this.familyMemberId = uni.getStorageSync('currentChildId') || uni.getStorageSync('currentMemberId') || uni.getStorageSync('userId') || ''
     this.familyMemberId = uni.getStorageSync('currentChildId') || uni.getStorageSync('currentMemberId') || uni.getStorageSync('userId') || ''
-    // 3. 会员门槛 + 加载
+    // 3. 健康阅卷:直接加载最后一条记录预填(无记录则空表),不走会员门槛/自检数据
+    if (this.reviewSource === 'health_review') {
+      this.loadHealthReviewData()
+      return
+    }
+    // 4. 会员门槛 + 加载
     this.checkGateAndLoad()
     this.checkGateAndLoad()
   },
   },
   methods: {
   methods: {
@@ -206,6 +215,58 @@ export default {
         self._loaded = true
         self._loaded = true
       })
       })
     },
     },
+    // 健康阅卷(HL-01):加载最后一条 health_review 记录并预填 formSnapshot/dietSnapshot
+    loadHealthReviewData: function() {
+      var self = this
+      self.loading = true
+      var tasks = [
+        getLastHealthReview({}).then(function(res) {
+          var d = (res && res.code === 200 && res.data) ? res.data : null
+          if (d && d.answersJson) {
+            self.applyHealthReviewJson(d.answersJson)
+          }
+        }).catch(function() {}),
+        getFamilyMemberList({}).then(function(res) {
+          var list = (res && res.code === 200 && res.data) ? res.data : []
+          var arr = list instanceof Array ? list : []
+          var found = null
+          for (var j = 0; j < arr.length; j++) {
+            if (arr[j].isSelf) { found = arr[j]; break }
+          }
+          if (found) {
+            self.memberProfile = { gender: found.gender || '', age: found.age != null ? found.age : '' }
+          }
+        }).catch(function() {})
+      ]
+      Promise.all(tasks).then(function() {
+        self.loading = false
+        self._loaded = true
+      })
+    },
+    // 健康阅卷 answersJson 还原:{form: <与 hsPayload 同构>, diet: <与 dietPayload 同构>}
+    applyHealthReviewJson: function(answersJson) {
+      var parsed = null
+      try {
+        parsed = JSON.parse(answersJson || '{}')
+      } catch (e) {
+        parsed = null
+      }
+      if (!parsed || typeof parsed !== 'object') return
+      var f = parsed.form || {}
+      var d = parsed.diet || {}
+      var self = this
+      if (f.heightCm != null) self.$set(self.formSnapshot, 'heightCm', f.heightCm)
+      if (f.weightKg != null) self.$set(self.formSnapshot, 'weightKg', f.weightKg)
+      if (f.bloodPressure != null) self.$set(self.formSnapshot, 'bloodPressure', f.bloodPressure)
+      if (f.bloodGlucose != null) self.$set(self.formSnapshot, 'bloodGlucose', f.bloodGlucose)
+      if (f.notes != null) self.$set(self.formSnapshot, 'notes', f.notes)
+      if (f.lipidCholesterol != null) self.$set(self.formSnapshot, 'lipidCholesterol', f.lipidCholesterol)
+      if (Array.isArray(f.selectedDiseases)) self.$set(self.formSnapshot, 'selectedDiseases', f.selectedDiseases)
+      if (Array.isArray(f.medRows)) self.$set(self.formSnapshot, 'medRows', f.medRows)
+      if (f.noMedication != null) self.$set(self.formSnapshot, 'noMedication', !!f.noMedication)
+      if (f.lifestyle && typeof f.lifestyle === 'object') self.$set(self.formSnapshot, 'lifestyle', f.lifestyle)
+      self.dietSnapshot = normalizeDiet(d)
+    },
     // 组件 patch → 合并到快照(patch 为结构化字段副本,不含 lifestyle)
     // 组件 patch → 合并到快照(patch 为结构化字段副本,不含 lifestyle)
     onUpdateForm: function(patch) {
     onUpdateForm: function(patch) {
       var self = this
       var self = this
@@ -277,7 +338,21 @@ export default {
         else if (v !== '' && v !== 'none') dietPayload[k] = v
         else if (v !== '' && v !== 'none') dietPayload[k] = v
       })
       })
       if (diet.noPreference) dietPayload.noPreference = true
       if (diet.noPreference) dietPayload.noPreference = true
-      // 3. 组装保存任务
+      // 3. 健康阅卷(source=health_review):仅写 survey_records(多次填写多条记录),不动 health_status/diet_preferences
+      if (self.reviewSource === 'health_review') {
+        var reviewAnswer = JSON.stringify({ form: hsPayload, diet: dietPayload })
+        self.submitting = true
+        submitSurvey({ memberId: self.familyMemberId, source: 'health_review', answersJson: reviewAnswer }).then(function() {
+          uni.showToast({ title: '已保存', icon: 'success' })
+          self.submitting = false
+          setTimeout(function() { uni.navigateBack() }, 800)
+        }).catch(function(err) {
+          self.submitting = false
+          uni.showToast({ title: (err && err.message) || '保存失败,请重试', icon: 'none' })
+        })
+        return
+      }
+      // 4. 组装保存任务(既有自检/方案流程,零改动)
       var saveTasks = []
       var saveTasks = []
       if (Object.keys(hsPayload).length > 0) {
       if (Object.keys(hsPayload).length > 0) {
         hsPayload.memberId = self.familyMemberId
         hsPayload.memberId = self.familyMemberId

+ 3 - 0
cfc-frontend/utils/api.js

@@ -2255,8 +2255,11 @@ export const getBodyAbnormalIndicators = (memberId) => request('/api/health/repo
 // ===== 定期调研 =====
 // ===== 定期调研 =====
 export const getSurveyStatus = (data) => request('/api/survey/status', 'POST', data)
 export const getSurveyStatus = (data) => request('/api/survey/status', 'POST', data)
 export const getActiveTemplates = () => request('/api/survey/template/active', 'POST', {})
 export const getActiveTemplates = () => request('/api/survey/template/active', 'POST', {})
+// submitSurvey(data):data.source 透传(periodic=定期调研 / health_review=健康阅卷,HL-01)
 export const submitSurvey = (data) => request('/api/survey/submit', 'POST', data)
 export const submitSurvey = (data) => request('/api/survey/submit', 'POST', data)
 export const getSurveyHistory = (data) => request('/api/survey/history', 'POST', data)
 export const getSurveyHistory = (data) => request('/api/survey/history', 'POST', data)
+// 查询最后一条健康阅卷记录(无则返回 null,HL-01)
+export const getLastHealthReview = (data) => request('/api/survey/health-review/last', 'POST', data || {})
 
 
 // ===== 寰鍔紙P0 鏈夋晥鍚姩锛� =====
 // ===== 寰鍔紙P0 鏈夋晥鍚姩锛� =====
 export const microActionList = () => request('/api/micro-action/list', 'POST', {})
 export const microActionList = () => request('/api/micro-action/list', 'POST', {})