Jelajahi Sumber

chore: auto bump version and changelog [skip ci]

iwt 1 Minggu lalu
induk
melakukan
d6e1cb8641

+ 7 - 0
cfc-frontend/pages.json

@@ -1121,6 +1121,13 @@
             "navigationStyle": "default"
           }
         },
+        {
+          "path": "health-survey",
+          "style": {
+            "navigationBarTitleText": "综合健康调研",
+            "navigationStyle": "default"
+          }
+        },
         {
           "path": "survey-questionnaire",
           "style": {

+ 3 - 1
cfc-frontend/pages/health-main/index.vue

@@ -515,7 +515,9 @@ export default {
     _statusClass: function(status) {
       return status === 'resolved' ? 'resolved' : 'pending'
     },
-    goSelfTest: function() { uni.navigateTo({ url: '/pages/health/health-plan-summary' }) },
+    goSelfTest: function() {
+      uni.navigateTo({ url: '/pages/health/health-survey?modules=basic,status,diet,disease,bowel,sleep,med,allergy' })
+    },
     // 拉取本期待填调研(服务端按 cycle_type 计算当前周期)
     loadPendingSurveys: function() {
       var self = this

+ 313 - 0
cfc-frontend/pages/health/health-survey.vue

@@ -0,0 +1,313 @@
+<template>
+  <view class="survey-page">
+    <!-- 加载中 -->
+    <view v-if="loading" class="loading-state">
+      <text class="loading-text">加载中...</text>
+    </view>
+    <!-- 调研组件(会员门槛未过时由 checkGateAndLoad 弹窗拦截,此处不渲染) -->
+    <comprehensive-survey
+      v-else
+      :modules="activeModules"
+      :form="formSnapshot"
+      :diet="dietSnapshot"
+      :profile="memberProfile"
+      :submitting="submitting"
+      @update-form="onUpdateForm"
+      @update-diet="onUpdateDiet"
+      @submit="onSubmit"
+    />
+  </view>
+</template>
+
+<script>
+import { getHealthStatus, saveHealthStatus, getDietPreferences, saveDietPreferences, getMyMembership, getFamilyMemberList } from '../../utils/api'
+import ComprehensiveSurvey from '@/pages/health/components/ComprehensiveSurvey.vue'
+
+var ALL_MODULES = ['basic', 'status', 'diet', 'disease', 'bowel', 'sleep', 'med', 'allergy']
+
+// 本地解析工具(与 health-plan-summary.vue 的同名方法保持一致)
+function parseLipidCholesterol(json) {
+  try {
+    var arr = JSON.parse(json || '[]')
+    if (arr instanceof Array && arr.length > 0 && arr[0].value) return String(arr[0].value)
+  } catch (e) {}
+  return ''
+}
+function parseDiseases(json) {
+  var list = []
+  try {
+    var arr = JSON.parse(json || '[]')
+    if (arr instanceof Array) {
+      arr.forEach(function(item) {
+        if (item.name) list.push({ name: item.name, diagnosedAt: item.diagnosedAt || '', custom: false })
+      })
+    }
+  } catch (e) {}
+  return list
+}
+function parseMeds(json) {
+  var list = []
+  try {
+    var arr = JSON.parse(json || '[]')
+    if (arr instanceof Array) {
+      arr.forEach(function(item) {
+        list.push({ name: item.name || '', startDate: item.startDate || '', durationText: item.durationText || '', time: item.time || '' })
+      })
+    }
+  } catch (e) {}
+  return list
+}
+function parseLifestyle(json) {
+  var obj = {}
+  try {
+    var parsed = JSON.parse(json)
+    if (parsed && typeof parsed === 'object') {
+      var keys = ['mood', 'sleepDuration', 'bedtime', 'bowelFrequency', 'bowelForm', 'antibioticHistory',
+        'currentStatus', 'exerciseFrequency', 'exerciseDuration',
+        'allergySymptoms', 'menstrualCycle', 'specialPeriod', 'growthDevelopment',
+        'pickyEating', 'osteoporosisRisk', 'fallRisk', 'screenTime', 'teenMood']
+      keys.forEach(function(k) {
+        obj[k] = parsed[k] != null ? parsed[k] : (k === 'allergySymptoms' ? [] : '')
+      })
+    }
+  } catch (e) {}
+  return obj
+}
+function normalizeDiet(d) {
+  return {
+    allergies: (d && d.allergies) || [],
+    absoluteAvoid: (d && d.absoluteAvoid) || [],
+    mustEat: (d && d.mustEat) || [],
+    religiousDiet: (d && d.religiousDiet) || 'none',
+    spiceLevel: (d && d.spiceLevel) || 0,
+    flavorPref: (d && d.flavorPref) || [],
+    cuisinePref: (d && d.cuisinePref) || [],
+    cookingMethods: (d && d.cookingMethods) || [],
+    healthGoals: (d && d.healthGoals) || [],
+    noPreference: !!(d && d.noPreference),
+    vegetableFruit: (d && d.vegetableFruit) || '',
+    dairy: (d && d.dairy) || '',
+    sugaryDrinks: (d && d.sugaryDrinks) || ''
+  }
+}
+
+export default {
+  components: { ComprehensiveSurvey },
+  data: function() {
+    return {
+      loading: true,
+      submitting: false,
+      activeModules: ALL_MODULES.slice(),
+      familyMemberId: '',
+      formSnapshot: {
+        heightCm: '', weightKg: '', bloodPressure: '', bloodGlucose: '', notes: '',
+        lipidCholesterol: '', selectedDiseases: [], medRows: [], noMedication: false,
+        lifestyle: {}
+      },
+      dietSnapshot: normalizeDiet(null),
+      memberProfile: {},
+      _loaded: false
+    }
+  },
+  onLoad: function(options) {
+    // 1. 解析 modules(缺省/空串/全非法 → 全量 8 模块)
+    if (options && options.modules) {
+      var list = String(options.modules).split(',').filter(function(m) { return ALL_MODULES.indexOf(m) >= 0 })
+      if (list.length > 0) this.activeModules = list
+    }
+    // 2. 当前成员快照(与 preferences.vue 一致)
+    this.familyMemberId = uni.getStorageSync('currentChildId') || uni.getStorageSync('currentMemberId') || uni.getStorageSync('userId') || ''
+    // 3. 会员门槛 + 加载
+    this.checkGateAndLoad()
+  },
+  methods: {
+    // 会员门槛(语义与 health-plan-summary.checkQuestionnaireGate 一致)
+    checkGateAndLoad: function() {
+      var self = this
+      getMyMembership().then(function(res) {
+        var m = (res && res.data) || {}
+        var level = m.memberLevel || m.levelCode || ''
+        if (level && level !== 'FREE') {
+          self.loadData()
+          return
+        }
+        uni.showModal({
+          title: '开通 9.9 元/月体验会员',
+          content: '填写健康调研需先开通体验会员(9.9 元/月),开通后可填写并保存。',
+          confirmText: '去开通',
+          cancelText: '暂不',
+          success: function(r) {
+            if (r.confirm) {
+              uni.setStorageSync('SURVEY_RESUME_AFTER_PURCHASE', '1')
+              uni.navigateTo({ url: '/pages/membership/pay?levelCode=BASIC&period=monthly' })
+            } else {
+              uni.navigateBack({ delta: 1 })
+            }
+          }
+        })
+      }).catch(function() {
+        // 网络失败放行(保存时由后端兜底校验)
+        self.loadData()
+      })
+    },
+    // 购买返回:重新走门槛(未开通仍会再弹,开通后放行加载)
+    resumeAfterPurchase: function() {
+      this.checkGateAndLoad()
+    },
+    // 并行加载:health_status + diet_preferences + 家庭成员(取当前成员性别/年龄供条件渲染)
+    loadData: function() {
+      var self = this
+      self.loading = true
+      var memberId = self.familyMemberId || ''
+      var tasks = [
+        getHealthStatus(memberId || undefined).then(function(res) {
+          var d = (res && res.code === 200 && res.data) ? res.data : null
+          if (d) {
+            self.$set(self.formSnapshot, 'heightCm', d.heightCm || '')
+            self.$set(self.formSnapshot, 'weightKg', d.weightKg || '')
+            self.$set(self.formSnapshot, 'bloodPressure', d.bloodPressure || '')
+            self.$set(self.formSnapshot, 'bloodGlucose', d.bloodGlucose || '')
+            self.$set(self.formSnapshot, 'notes', d.notes || '')
+            self.$set(self.formSnapshot, 'lipidCholesterol', parseLipidCholesterol(d.bloodLipids))
+            self.$set(self.formSnapshot, 'selectedDiseases', parseDiseases(d.diseaseHistory))
+            self.$set(self.formSnapshot, 'medRows', parseMeds(d.medications))
+            self.$set(self.formSnapshot, 'lifestyle', d.lifestyle ? parseLifestyle(d.lifestyle) : {})
+          }
+        }).catch(function() {}),
+        getDietPreferences({ memberId: memberId }).then(function(res) {
+          var d = (res && res.code === 200 && res.data) ? res.data : null
+          self.dietSnapshot = normalizeDiet(d)
+        }).catch(function() {
+          self.dietSnapshot = normalizeDiet(null)
+        }),
+        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 i = 0; i < arr.length; i++) {
+            var mid = arr[i].memberId || arr[i].id
+            if (memberId && String(mid) === String(memberId)) { found = arr[i]; break }
+          }
+          // 未指定成员(账号级)时取 isSelf 成员兜底
+          if (!found) {
+            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
+      })
+    },
+    // 组件 patch → 合并到快照(patch 为结构化字段副本,不含 lifestyle)
+    onUpdateForm: function(patch) {
+      var self = this
+      Object.keys(patch || {}).forEach(function(k) {
+        self.$set(self.formSnapshot, k, patch[k])
+      })
+    },
+    onUpdateDiet: function(patch) {
+      var self = this
+      Object.keys(patch || {}).forEach(function(k) {
+        self.$set(self.dietSnapshot, k, patch[k])
+      })
+    },
+    // 提交:health_status 侧组装 + lifestyle merge + diet_preferences 侧组装(合并式增量)
+    onSubmit: function(payload) {
+      var self = this
+      if (self.submitting) return
+      var form = payload.form || {}
+      // 血压格式兜底
+      if (form.bloodPressure && !/^\d{2,3}\/\d{2,3}$/.test(form.bloodPressure)) {
+        uni.showToast({ title: '血压格式应为 120/80', icon: 'none' })
+        return
+      }
+      // 1) health_status payload:扁平体征 + JSON 列(疾病史/用药/血脂)+ lifestyle merge
+      var hsPayload = {}
+      var enabled = self.activeModules
+      if (enabled.indexOf('basic') >= 0) {
+        if (form.heightCm != null && form.heightCm !== '') hsPayload.heightCm = Number(form.heightCm)
+        if (form.weightKg != null && form.weightKg !== '') hsPayload.weightKg = Number(form.weightKg)
+        if (form.bloodPressure != null && form.bloodPressure !== '') hsPayload.bloodPressure = form.bloodPressure
+        if (form.bloodGlucose != null && form.bloodGlucose !== '') hsPayload.bloodGlucose = Number(form.bloodGlucose)
+        if (form.notes != null && form.notes !== '') hsPayload.notes = form.notes
+        if (form.lipidCholesterol != null && form.lipidCholesterol !== '') {
+          hsPayload.bloodLipids = JSON.stringify([{ name: '总胆固醇', value: parseFloat(form.lipidCholesterol), unit: 'mmol/L', note: '' }])
+        }
+      }
+      if (enabled.indexOf('disease') >= 0) {
+        var diseases = (form.selectedDiseases || []).map(function(item) {
+          var d = { name: item.name }
+          if (item.diagnosedAt) d.diagnosedAt = item.diagnosedAt
+          return d
+        })
+        hsPayload.diseaseHistory = JSON.stringify(diseases)
+      }
+      if (enabled.indexOf('med') >= 0) {
+        var meds = (form.medRows || []).filter(function(item) { return item.name }).map(function(item) {
+          var m = { name: item.name }
+          if (item.startDate) m.startDate = item.startDate
+          if (item.durationText) m.durationText = item.durationText
+          if (item.time) m.time = item.time
+          return m
+        })
+        hsPayload.medications = JSON.stringify(meds)
+      }
+      // lifestyle merge:现有 lifestyle + 本次 diff(新 key 合并,未调研模块 key 不动)
+      var existing = self.formSnapshot.lifestyle || {}
+      var merged = Object.assign({}, existing)
+      Object.keys(payload.lifestyleDiff || {}).forEach(function(k) { merged[k] = payload.lifestyleDiff[k] })
+      if (Object.keys(merged).length > 0) hsPayload.lifestyle = JSON.stringify(merged)
+      // 2) diet payload:仅启用模块涉及字段(allergy 模块的 allergies 双写 diet_preferences)
+      var diet = payload.diet || {}
+      var dietModules = { allergies: 'allergy', absoluteAvoid: 'diet', mustEat: 'diet', flavorPref: 'diet', cuisinePref: 'diet', cookingMethods: 'diet', healthGoals: 'diet', religiousDiet: 'diet', spiceLevel: 'diet', vegetableFruit: 'diet', dairy: 'diet', sugaryDrinks: 'diet' }
+      var dietPayload = {}
+      Object.keys(dietModules).forEach(function(k) {
+        if (enabled.indexOf(dietModules[k]) < 0) return
+        var v = diet[k]
+        if (v == null) return
+        if (v instanceof Array) { if (v.length > 0) dietPayload[k] = v.slice() }
+        else if (v !== '' && v !== 'none') dietPayload[k] = v
+      })
+      if (diet.noPreference) dietPayload.noPreference = true
+      // 3. 组装保存任务
+      var saveTasks = []
+      if (Object.keys(hsPayload).length > 0) {
+        hsPayload.memberId = self.familyMemberId
+        saveTasks.push(saveHealthStatus(hsPayload))
+      }
+      if (Object.keys(dietPayload).length > 0) {
+        dietPayload.familyMemberId = self.familyMemberId
+        saveTasks.push(saveDietPreferences(dietPayload))
+      }
+      self.submitting = true
+      Promise.all(saveTasks).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' })
+      })
+    }
+  },
+  onShow: function() {
+    // 从购买页返回:重新走会员门槛(首次 onLoad 已加载时跳过)
+    if (uni.getStorageSync('SURVEY_RESUME_AFTER_PURCHASE') === '1') {
+      uni.removeStorageSync('SURVEY_RESUME_AFTER_PURCHASE')
+      if (!this._loaded) this.resumeAfterPurchase()
+    }
+  }
+}
+</script>
+
+<style>
+.survey-page { min-height: 100vh; background: linear-gradient(180deg, #FFF7ED, #FFF3E6); }
+.loading-state { padding: 200rpx 0; text-align: center; }
+.loading-text { font-size: 28rpx; color: #B08968; }
+</style>

+ 2 - 2
cfc-frontend/pages/index-home/index.vue

@@ -1644,8 +1644,8 @@ title: '添加家庭成员',
           '&periodEnd=' + (firstPending.periodEnd || '')
         uni.navigateTo({ url: url })
       } else {
-        // 无待填调研 → 进入健康自检问卷(会员门槛由问卷页 onLoad 把关)
-        uni.navigateTo({ url: '/pages/health/health-plan-summary' })
+        // 无待填调研 → 进入独立综合调研页(仅调研不生成方案;会员门槛由调研页 onLoad 把关)
+        uni.navigateTo({ url: '/pages/health/health-survey?modules=basic,status,diet,disease,bowel,sleep,med,allergy' })
       }
     },
     // ===== Next Best Action 引导条点击 =====

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-de023c6c5e4c346cf71df0f3d5aa0994ec3de3a0
+b5578f5d2411a355f362357a524e2ed4a0efdf4b

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1635",
+  "version": "1.0.1636",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.1635",
+      "version": "1.0.1636",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1636",
+  "version": "1.0.1637",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

+ 17 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,23 @@
 
 ---
 
+## v1.0.1637 (2026-10-16)
+
+### 新功能
+- 新增 ComprehensiveSurvey 综合调研组件(8模块step+合并式增量保存)(任务1/3)
+
+### Bug 修复
+- 会员JOIN赠券结算可选可用(家庭券状态/ID契约/下单核销三处修复)
+
+### 其他
+-   status=AVAILABLE),修复前端 status!=AVAILABLE 过滤导致券不可见 + 前端传券ID
+-   与 apply/markUsed 契约不一致
+- - ProductOrderService.create/createMultiItem: 下单时校验并计入优惠券抵扣金额
+- - ProductOrderService 支付成功 markUsed 核销、取消订单 revert 归还
+- - checkout.vue: 下单请求携带 couponId=selectedCoupon.id
+- 
+
+
 ## v1.0.1636 (2026-10-16)
 
 ### 文档

+ 18 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.1636
+> 当前版本: v1.0.1637
 
 ## 历史版本
 
@@ -8,6 +8,23 @@
 
 ---
 
+## v1.0.1637 (2026-10-16)
+
+### 新功能
+- 新增 ComprehensiveSurvey 综合调研组件(8模块step+合并式增量保存)(任务1/3)
+
+### Bug 修复
+- 会员JOIN赠券结算可选可用(家庭券状态/ID契约/下单核销三处修复)
+
+### 其他
+-   status=AVAILABLE),修复前端 status!=AVAILABLE 过滤导致券不可见 + 前端传券ID
+-   与 apply/markUsed 契约不一致
+- - ProductOrderService.create/createMultiItem: 下单时校验并计入优惠券抵扣金额
+- - ProductOrderService 支付成功 markUsed 核销、取消订单 revert 归还
+- - checkout.vue: 下单请求携带 couponId=selectedCoupon.id
+- 
+
+
 ## v1.0.1636 (2026-10-16)
 
 ### 文档