Sfoglia il codice sorgente

feat(signature): 签署时间入库 + 确认页摘要精简 + 签名前同意勾选

- 后端:迁移383 加 health_status.signed_at;updateSignature 同时记签署时间;
  saveForMember 在问卷内容变更时显式置空 signature/signed_at(updateById 默认
  跳过 null 字段,必须用 UpdateWrapper 置空),使原签名不再对应当前回答
- 组件:新增 requireAgree/agreeText props + 同意勾选 UI,未勾选不可确认签名,
  取消勾选 emit agree-change(调用方作废已确认签名)
- 问卷确认页:摘要从 30 项精简为 12 个关键项(身高体重/血压/血糖胆固醇/疾病史/
  在服药物/睡眠/排便/心情/抗生素/过敏/饮食结构/饮食忌口),其余项收进
  「展开其余 N 项」;成员头部回显已签署时间;取消勾选同意即作废签名
- 购买页/收货人页:同样接入同意勾选
- 文档:API_REFERENCE 说明同步 signed_at;PROJECT-OVERVIEW 追加 4.52
Sisyphus 1 settimana fa
parent
commit
af8b5e0dfb

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

@@ -3094,6 +3094,9 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
 
         // 迁移382: health_status 添加 signature 列(用户手签名,base64 PNG data URL,用于问卷回答确认)
         ensureColumn("health_status", "signature", "MEDIUMTEXT DEFAULT NULL COMMENT '用户手签名(base64 PNG data URL),确认问卷回答准确性'");
+
+        // 迁移383: health_status 添加 signed_at 列(手签名时间;问卷内容变更后置空,需重新签署)
+        ensureColumn("health_status", "signed_at", "DATETIME DEFAULT NULL COMMENT '手签名时间(问卷变更后置空,需重新签署)'");
     }
 
     private void migrateHealthDimensionsTable() {

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/HealthStatus.java

@@ -52,6 +52,9 @@ public class HealthStatus implements Serializable {
     /** 用户手签名(base64 PNG data URL),用于确认问卷回答准确性 */
     private String signature;
 
+    /** 手签名时间(问卷内容变更后置空,需重新签署) */
+    private Date signedAt;
+
     /** 首次填写时间 */
     private Date filledAt;
 

+ 13 - 4
cfc-backend/src/main/java/com/etotem/cfc/service/impl/HealthStatusServiceImpl.java

@@ -57,16 +57,23 @@ public class HealthStatusServiceImpl implements HealthStatusService {
         HealthStatus existing = getByMemberId(userId, memberId);
         status.setUserId(userId);
         status.setMemberId(memberId);
+        Date now = new Date();
         if (existing == null) {
             status.setId(null);
-            status.setFilledAt(new Date());
-            status.setUpdatedAt(new Date());
+            status.setFilledAt(now);
+            status.setUpdatedAt(now);
             healthStatusMapper.insert(status);
         } else {
             status.setId(existing.getId());
             status.setFilledAt(existing.getFilledAt());
-            status.setUpdatedAt(new Date());
+            status.setUpdatedAt(now);
             healthStatusMapper.updateById(status);
+            // 问卷内容变更后,原手签名不再对应当前回答 → 置空要求重新签署
+            // (updateById 默认跳过 null 字段,须用 UpdateWrapper 显式置空)
+            healthStatusMapper.update(null, new LambdaUpdateWrapper<HealthStatus>()
+                    .eq(HealthStatus::getId, existing.getId())
+                    .set(HealthStatus::getSignature, null)
+                    .set(HealthStatus::getSignedAt, null));
         }
         return getByMemberId(userId, memberId);
     }
@@ -78,10 +85,12 @@ public class HealthStatusServiceImpl implements HealthStatusService {
         if (existing == null) {
             throw new IllegalArgumentException("该成员暂无健康现状记录,请先完成问卷");
         }
+        Date now = new Date();
         LambdaUpdateWrapper<HealthStatus> wrapper = new LambdaUpdateWrapper<HealthStatus>()
                 .eq(HealthStatus::getId, existing.getId())
                 .set(HealthStatus::getSignature, signature)
-                .set(HealthStatus::getUpdatedAt, new Date());
+                .set(HealthStatus::getSignedAt, now)
+                .set(HealthStatus::getUpdatedAt, now);
         healthStatusMapper.update(null, wrapper);
     }
 }

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

@@ -5394,6 +5394,7 @@ CREATE TABLE IF NOT EXISTS health_status (
     notes VARCHAR(500) DEFAULT NULL COMMENT '其他补充说明',
     lifestyle JSON DEFAULT NULL COMMENT '生活方式与肠道健康 {mood,sleepDuration,bedtime,bowelFrequency,bowelForm,antibioticHistory,allergySymptoms}',
     signature MEDIUMTEXT DEFAULT NULL COMMENT '用户手签名(base64 PNG data URL),确认问卷回答准确性',
+    signed_at DATETIME DEFAULT NULL COMMENT '手签名时间(问卷变更后置空,需重新签署)',
     filled_at DATETIME DEFAULT NULL COMMENT '首次填写时间',
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
     INDEX idx_user_member (user_id, member_id)

+ 67 - 3
cfc-frontend/components/hand-signature/hand-signature.vue

@@ -5,15 +5,25 @@
     strokeColor    string   笔触颜色,默认 '#1a1a1a'
     strokeWidth    number   笔触宽度(逻辑像素),默认 2
     height         number   画布高度(px),默认 200
+    requireAgree   boolean  是否要求先勾选同意声明才能确认签名,默认 false
+    agreeText      string   同意声明文案,默认 '我已阅读并确认以上内容真实、准确'
   Emits:
-    confirm  dataUrl: string  画布转 base64 PNG data URL(点击「确认签名」时触发)
-    clear                画布被清空
+    confirm      dataUrl: string  画布转 base64 PNG data URL(点击「确认签名」时触发)
+    clear                          画布被清空
+    agree-change agreed: boolean   同意勾选状态变化(取消勾选时调用方应作废已确认签名)
   用法:
     <hand-signature @confirm="onSign" @clear="onClear" />
     onSign(url) { this.signatureDataUrl = url }
 -->
 <template>
   <view class="hand-signature">
+    <!-- 同意声明(requireAgree=true 时显示,未勾选不可确认签名) -->
+    <view v-if="requireAgree" class="signature-agree" @click="toggleAgree">
+      <view class="agree-box" :class="{ 'agree-box-on': agreed }">
+        <text v-if="agreed" class="agree-tick">✓</text>
+      </view>
+      <text class="agree-text">{{ agreeText }}</text>
+    </view>
     <view class="signature-canvas-wrap">
       <canvas
         canvas-id="hsCanvas"
@@ -35,7 +45,7 @@
       <button class="btn-sign-clear" :disabled="savingLive || !hasContent && !currentDataUrl" @click="handleClear">
         {{ savingLive ? '处理中...' : '重写' }}
       </button>
-      <button class="btn-sign-confirm" :disabled="savingLive || !hasContent" @click="handleConfirm">
+      <button class="btn-sign-confirm" :disabled="savingLive || !hasContent || (requireAgree && !agreed)" @click="handleConfirm">
         {{ savingLive ? '处理中...' : '确认签名' }}
       </button>
     </view>
@@ -61,6 +71,16 @@ export default {
     height: {
       type: Number,
       default: 200
+    },
+    /** 是否要求先勾选同意声明才能确认签名 */
+    requireAgree: {
+      type: Boolean,
+      default: false
+    },
+    /** 同意声明文案(requireAgree=true 时生效) */
+    agreeText: {
+      type: String,
+      default: '我已阅读并确认以上内容真实、准确'
     }
   },
   data: function() {
@@ -71,6 +91,7 @@ export default {
       lastY: 0,
       hasContent: false,
       hasDrawn: false,
+      agreed: false,
       currentDataUrl: this.value || '',
       savingLive: false,
       rect: null
@@ -159,9 +180,18 @@ export default {
     onTouchEnd: function() {
       this.drawing = false
     },
+    // 切换同意勾选;取消勾选时通知父组件作废已确认签名
+    toggleAgree: function() {
+      this.agreed = !this.agreed
+      this.$emit('agree-change', this.agreed)
+    },
     handleConfirm: function() {
       var self = this
       if (this.savingLive) return
+      if (this.requireAgree && !this.agreed) {
+        uni.showToast({ title: '请先勾选同意声明', icon: 'none' })
+        return
+      }
       // 未在画布上绘制、但有预填签名时:直接透传原签名,避免导出空白画布覆盖
       if (!this.hasDrawn && this.currentDataUrl) {
         this.$emit('confirm', this.currentDataUrl)
@@ -233,6 +263,40 @@ export default {
 .hand-signature {
   width: 100%;
 }
+/* 同意声明 */
+.signature-agree {
+  display: flex;
+  align-items: flex-start;
+  margin-bottom: 16rpx;
+}
+.agree-box {
+  flex: 0 0 32rpx;
+  width: 32rpx;
+  height: 32rpx;
+  margin-right: 12rpx;
+  margin-top: 2rpx;
+  border: 2rpx solid #c8c9cc;
+  border-radius: 6rpx;
+  background: #fff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.agree-box-on {
+  background: #667eea;
+  border-color: #667eea;
+}
+.agree-tick {
+  font-size: 22rpx;
+  color: #fff;
+  line-height: 1;
+}
+.agree-text {
+  flex: 1;
+  font-size: 24rpx;
+  color: #666;
+  line-height: 1.5;
+}
 .signature-canvas-wrap {
   position: relative;
   width: 100%;

+ 139 - 48
cfc-frontend/pages/health/health-plan-summary.vue

@@ -74,14 +74,26 @@
       <view class="section-title">📝 确认问卷内容</view>
       <view class="hint-text">请核对以下问卷信息准确无误后,手写签名确认</view>
 
-      <!-- 问卷内容摘要(按成员) -->
+      <!-- 问卷内容摘要(按成员,默认只展示关键项,可展开全部) -->
       <view class="confirm-survey-list">
         <view v-for="m in selectedMembers" :key="m.memberId" class="confirm-survey-member">
-          <view class="confirm-member-name">{{ m.name }}</view>
+          <view class="confirm-member-head">
+            <text class="confirm-member-name">{{ m.name }}</text>
+            <text v-if="getMemberSignedAt(m.memberId)" class="confirm-member-signed">已签署 {{ getMemberSignedAt(m.memberId) }}</text>
+          </view>
           <view class="confirm-summary-row" v-for="(line, li) in buildSurveySummary(m.memberId)" :key="li">
             <text class="confirm-label">{{ line.label }}</text>
             <text class="confirm-value">{{ line.value }}</text>
           </view>
+          <view v-if="showFullSummary" class="confirm-extra-block">
+            <view class="confirm-summary-row" v-for="(line, li) in buildSurveyExtraSummary(m.memberId)" :key="li">
+              <text class="confirm-label">{{ line.label }}</text>
+              <text class="confirm-value">{{ line.value }}</text>
+            </view>
+          </view>
+          <view v-if="extraCount(m.memberId) > 0" class="confirm-toggle" @click="toggleFullSummary">
+            <text class="confirm-toggle-text">{{ showFullSummary ? '收起' : '展开其余 ' + extraCount(m.memberId) + ' 项' }}</text>
+          </view>
           <view v-if="!hasMemberData(m.memberId)" class="confirm-empty">该成员未填写问卷内容</view>
         </view>
       </view>
@@ -92,8 +104,11 @@
       <hand-signature
         ref="signaturePad"
         :value="signatureDataUrl"
+        :require-agree="true"
+        agree-text="我已阅读并确认上述问卷信息真实、准确,且由本人填写"
         @confirm="onSignatureConfirm"
         @clear="onSignatureClear"
+        @agree-change="onSignatureAgreeChange"
       />
 
       <view class="btn-pair">
@@ -264,7 +279,9 @@ export default {
       loading: false,
       taskSubmitted: false,
       // Step 3 签名确认
-      signatureDataUrl: ''
+      signatureDataUrl: '',
+      // 确认页:是否展开非关键项
+      showFullSummary: false
     }
   },
   computed: {
@@ -481,7 +498,8 @@ selectedMembers: function() {
             selectedDiseases: self.parseDiseases(d.diseaseHistory),
             medRows: self.parseMeds(d.medications),
             noMedication: false,
-            lifestyle: d.lifestyle ? self.parseLifestyle(d.lifestyle) : {}
+            lifestyle: d.lifestyle ? self.parseLifestyle(d.lifestyle) : {},
+            signedAt: d.signedAt || ''
           })
         }
       }).catch(function() {})
@@ -760,67 +778,100 @@ selectedMembers: function() {
     },
 
     // ===== Step 3 签名确认 =====
-    // 按成员生成问卷内容摘要(键值对列表)
-    buildSurveySummary: function(memberId) {
+    // 构建某成员的全部问卷条目(primary=true 为确认页默认展示的关键项)
+    buildSurveyLines: function(memberId) {
       var s = this.memberFormMap[String(memberId)] || null
       var d = this.memberDietMap[String(memberId)] || null
       var lines = []
-      var push = function(label, value) {
+      var push = function(primary, label, value) {
         if (value && String(value).trim() !== '' && value !== false && !(Array.isArray(value) && value.length === 0)) {
-          lines.push({ label: label, value: Array.isArray(value) ? value.join('、') : String(value) })
+          lines.push({ primary: !!primary, label: label, value: Array.isArray(value) ? value.join('、') : String(value) })
         }
       }
+      // 多值合并为一行(忽略空值)
+      var join = function() {
+        var parts = []
+        for (var i = 0; i < arguments.length; i++) {
+          if (arguments[i]) parts.push(arguments[i])
+        }
+        return parts.join(' · ')
+      }
       if (s) {
         var f = s.form || {}
-        push('身高', f.heightCm ? f.heightCm + ' cm' : '')
-        push('体重', f.weightKg ? f.weightKg + ' kg' : '')
-        push('血压', f.bloodPressure)
-        push('空腹血糖', f.bloodGlucose ? f.bloodGlucose + ' mmol/L' : '')
-        push('总胆固醇', s.lipidCholesterol ? s.lipidCholesterol + ' mmol/L' : '')
+        var ls = s.lifestyle || {}
+        // —— 关键项(默认展示)——
+        push(true, '身高/体重', join(f.heightCm ? f.heightCm + ' cm' : '', f.weightKg ? f.weightKg + ' kg' : '', ' / '))
+        push(true, '血压', f.bloodPressure)
+        push(true, '血糖/胆固醇', join(f.bloodGlucose ? '血糖 ' + f.bloodGlucose + ' mmol/L' : '',
+          s.lipidCholesterol ? '总胆固醇 ' + s.lipidCholesterol + ' mmol/L' : ''))
         if (s.selectedDiseases && s.selectedDiseases.length > 0) {
-          push('疾病史', s.selectedDiseases.map(function(x) { return x.name }).join('、'))
+          push(true, '疾病史', s.selectedDiseases.map(function(x) { return x.name }).join('、'))
         }
-        if (s.noMedication) push('用药', '无')
+        if (s.noMedication) push(true, '在服药物', '无')
         else if (s.medRows && s.medRows.length > 0) {
-          push('在服药物', s.medRows.map(function(x) { return x.name || '' }).filter(function(n) { return n }).join('、'))
+          push(true, '在服药物', s.medRows.map(function(x) { return x.name || '' }).filter(function(n) { return n }).join('、'))
         }
-        var ls = s.lifestyle || {}
-        push('心情', ls.mood)
-        push('睡眠时长', ls.sleepDuration)
-        push('入睡时间', ls.bedtime)
-        push('排便频率', ls.bowelFrequency)
-        push('排便性状', ls.bowelForm)
-        push('抗生素用药史', ls.antibioticHistory)
-        if (ls.allergySymptoms && ls.allergySymptoms.length > 0) push('过敏表现', ls.allergySymptoms.join('、'))
-        push('月经周期', ls.menstrualCycle)
-        push('特殊时期', ls.specialPeriod)
-        push('生长发育', ls.growthDevelopment)
-        push('挑食情况', ls.pickyEating)
-        push('骨质疏松风险', ls.osteoporosisRisk)
-        push('跌倒风险', ls.fallRisk)
-        push('屏幕时间', ls.screenTime)
-        push('情绪状态', ls.teenMood)
-        push('当前状态', ls.currentStatus)
-        push('运动频率', ls.exerciseFrequency)
-        push('运动时长', ls.exerciseDuration)
-        if (f.notes) push('补充说明', f.notes)
+        push(true, '睡眠', join(ls.bedtime ? ls.bedtime + ' 入睡' : '', ls.sleepDuration))
+        push(true, '排便', join(ls.bowelFrequency, ls.bowelForm))
+        push(true, '心情', ls.mood)
+        push(true, '近2年抗生素', ls.antibioticHistory)
+        if (ls.allergySymptoms && ls.allergySymptoms.length > 0) push(true, '过敏表现', ls.allergySymptoms.join('、'))
+        if (d) {
+          push(true, '饮食结构', join(d.vegetableFruit, d.dairy, d.sugaryDrinks))
+          push(true, '饮食忌口', join(d.allergies, d.absoluteAvoid))
+        }
+        // —— 其余项(展开后展示)——
+        push(false, '月经周期', ls.menstrualCycle)
+        push(false, '特殊时期', ls.specialPeriod)
+        push(false, '生长发育', ls.growthDevelopment)
+        push(false, '挑食情况', ls.pickyEating)
+        push(false, '骨质疏松风险', ls.osteoporosisRisk)
+        push(false, '跌倒风险', ls.fallRisk)
+        push(false, '屏幕时间', ls.screenTime)
+        push(false, '情绪状态', ls.teenMood)
+        push(false, '当前状态', ls.currentStatus)
+        push(false, '运动频率', ls.exerciseFrequency)
+        push(false, '运动时长', ls.exerciseDuration)
+        push(false, '补充说明', f.notes)
+      } else if (d) {
+        push(true, '饮食结构', join(d.vegetableFruit, d.dairy, d.sugaryDrinks))
+        push(true, '饮食忌口', join(d.allergies, d.absoluteAvoid))
       }
       if (d) {
-        push('食物过敏', d.allergies)
-        push('绝对忌口', d.absoluteAvoid)
-        push('必吃食物', d.mustEat)
-        push('宗教饮食', d.religiousDiet && d.religiousDiet !== 'none' ? d.religiousDiet : '')
-        if (Number(d.spiceLevel) > 0) push('辣度', d.spiceLevel)
-        push('口味偏好', d.flavorPref)
-        push('菜系偏好', d.cuisinePref)
-        push('烹饪方式', d.cookingMethods)
-        if (d.noPreference) push('饮食偏好', '无特殊偏好')
-        push('蔬果量', d.vegetableFruit)
-        push('乳制品', d.dairy)
-        push('含糖饮料', d.sugaryDrinks)
+        push(false, '必吃食物', d.mustEat)
+        push(false, '宗教饮食', d.religiousDiet && d.religiousDiet !== 'none' ? d.religiousDiet : '')
+        if (Number(d.spiceLevel) > 0) push(false, '辣度', d.spiceLevel)
+        push(false, '口味偏好', d.flavorPref)
+        push(false, '菜系偏好', d.cuisinePref)
+        push(false, '烹饪方式', d.cookingMethods)
+        if (d.noPreference) push(false, '饮食偏好', '无特殊偏好')
       }
       return lines
     },
+    // 关键项(确认页默认展示)
+    buildSurveySummary: function(memberId) {
+      return this.buildSurveyLines(memberId).filter(function(l) { return l.primary })
+    },
+    // 其余项(点击展开后展示)
+    buildSurveyExtraSummary: function(memberId) {
+      return this.buildSurveyLines(memberId).filter(function(l) { return !l.primary })
+    },
+    extraCount: function(memberId) {
+      return this.buildSurveyExtraSummary(memberId).length
+    },
+    toggleFullSummary: function() {
+      this.showFullSummary = !this.showFullSummary
+    },
+    // 该成员已签署时间(问卷内容变更后后端会置空)
+    getMemberSignedAt: function(memberId) {
+      var s = this.memberFormMap[String(memberId)]
+      var v = s && s.signedAt
+      return v ? this.formatTime(v) : ''
+    },
+    // 取消勾选同意 → 作废已确认签名
+    onSignatureAgreeChange: function(agreed) {
+      if (!agreed) this.signatureDataUrl = ''
+    },
     // 该成员是否有问卷/饮食数据可供确认
     hasMemberData: function(memberId) {
       var s = this.memberFormMap[String(memberId)]
@@ -859,6 +910,8 @@ selectedMembers: function() {
       }
       Promise.all(sigTasks).then(function() {
         self.loading = false
+        // 本地回显签署时间(服务端以 health_status.signed_at 为准)
+        self.stampLocalSignedAt()
         self.step = 4
         self.generatePlan()
       }).catch(function() {
@@ -868,6 +921,18 @@ selectedMembers: function() {
         self.generatePlan()
       })
     },
+    // 签名成功后本地回显签署时间(格式 yyyy-MM-dd HH:mm:ss)
+    stampLocalSignedAt: function() {
+      var self = this
+      var now = new Date()
+      var pad = function(n) { return n < 10 ? '0' + n : '' + n }
+      var str = now.getFullYear() + '-' + pad(now.getMonth() + 1) + '-' + pad(now.getDate()) +
+        ' ' + pad(now.getHours()) + ':' + pad(now.getMinutes()) + ':' + pad(now.getSeconds())
+      self.selectedMemberIds.forEach(function(mid) {
+        var key = String(mid)
+        if (self.memberFormMap[key]) self.$set(self.memberFormMap[key], 'signedAt', str)
+      })
+    },
 
     // ===== 方案生成与编辑 (保持原有逻辑) =====
     generatePlan: function() {
@@ -1777,6 +1842,32 @@ selectedMembers: function() {
   color: #F97316;
   margin-bottom: 12rpx;
 }
+.confirm-member-head {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 12rpx;
+}
+.confirm-member-head .confirm-member-name {
+  margin-bottom: 0;
+}
+.confirm-member-signed {
+  font-size: calc(22rpx * var(--font-scale, 1));
+  color: #67c23a;
+}
+.confirm-extra-block {
+  border-top: 1rpx dashed #E5E5E5;
+  margin-top: 8rpx;
+  padding-top: 4rpx;
+}
+.confirm-toggle {
+  padding: 12rpx 0 4rpx;
+  text-align: center;
+}
+.confirm-toggle-text {
+  font-size: calc(24rpx * var(--font-scale, 1));
+  color: #667eea;
+}
 .confirm-summary-row {
   display: flex;
   font-size: calc(24rpx * var(--font-scale, 1));

+ 7 - 0
cfc-frontend/pages/shop/checkout/checkout.vue

@@ -132,8 +132,11 @@
           <view class="purchase-signature-title">✍️ 手写签名</view>
           <hand-signature
             :value="purchaseForm.hand_signature"
+            :require-agree="true"
+            agree-text="我已阅读并同意,确认以上填写信息真实、准确,且由本人签署"
             @confirm="onHandSignatureConfirm"
             @clear="onHandSignatureClear"
+            @agree-change="onHandSignatureAgreeChange"
           />
         </view>
       </view>
@@ -870,6 +873,10 @@ export default {
       form.hand_signature = ''
       this.purchaseForm = form
     },
+    onHandSignatureAgreeChange(agreed) {
+      // 取消勾选同意 → 作废已确认签名,需重新签署
+      if (!agreed) this.onHandSignatureClear()
+    },
     openAddressPicker() {
       if (this.addressList.length === 0) {
         uni.showToast({ title: '暂无地址,请先添加', icon: 'none' })

+ 7 - 0
cfc-frontend/pages/shop/consignee-edit/consignee-edit.vue

@@ -68,8 +68,11 @@
       <view class="signature-block">
         <hand-signature
           :value="form.handSignature"
+          :require-agree="true"
+          agree-text="我已阅读并同意,本人确认以上信息真实、准确"
           @confirm="onHandSignatureConfirm"
           @clear="onHandSignatureClear"
+          @agree-change="onHandSignatureAgreeChange"
         />
       </view>
 
@@ -149,6 +152,10 @@ export default {
     onHandSignatureClear() {
       this.form.handSignature = ''
     },
+    onHandSignatureAgreeChange(agreed) {
+      // 取消勾选同意 → 作废已确认签名
+      if (!agreed) this.form.handSignature = ''
+    },
     onDefaultChange(e) {
       this.form.isDefault = e.detail.value ? 1 : 0
     },

File diff suppressed because it is too large
+ 0 - 0
docs/superpowers/PROJECT-OVERVIEW.md


+ 1 - 1
docs/superpowers/api/API_REFERENCE.md

@@ -592,7 +592,7 @@ find cfc-backend/src/main/java -name "*XxxService.java" -o -name "*XxxController
 | `POST /api/mind/emotion/analyze-url` | 照片情绪识别(URL方式,走 LangGraph DeepFace) |
 | `POST /api/health-status/get` | 健康现状档案获取 |
 | `POST /api/health-status/save` | 健康现状档案保存 |
-| `POST /api/health-status/signature` | 提交手签名(仅更新 health_status.signature 列,不影响其他字段;body: `{memberId, signature}`,signature 为 base64 PNG data URL) |
+| `POST /api/health-status/signature` | 提交手签名(仅更新 `signature` + `signed_at` 列,不影响其他字段;body: `{memberId, signature}`,signature 为 base64 PNG data URL) |
 | `POST /api/health/exercise/sync-wechat-steps` | 同步微信运动步数(传 encryptedData+iv,返回当日步数与近30天数据) |
 | `POST /api/health/meal/list` | 饮食打卡列表 |
 | `POST /api/health/meal/create` | 创建饮食打卡(支持 taskId 任务联动 + isBackfill 补打) |

Some files were not shown because too many files changed in this diff