Explorar el Código

Merge branch 'cfclub' of http://git.iwintrue.com/liaoxg/cfc into cfclub

iwt hace 1 mes
padre
commit
8510bc9566

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

@@ -1076,6 +1076,8 @@ log.info("已添加template_id列到tasks表");
             insertProductSeed("肠道菌群检测套装", "家用检测 + 报告解读", "physical", 59900, "body", "budget");
             insertProductSeed("儿童多模态认知测评", "在线测评 + 个性化建议", "digital", 9900, "wisdom", "none");
             insertProductSeed("成长规划师1对1咨询", "60分钟专业规划", "service", 50000, "wealth", "budget");
+            insertProductSeed("21天科学减重套餐", "基于肠道菌群的个性化减重方案,21天找到适合自己体质的减重方式,不再反弹", "physical", 49900, "body", "all");
+            insertProductSeed("28天血糖平稳管理套餐", "餐后血糖生活方式管理,基于菌群的个性化饮食方案,帮助糖前期人群平稳血糖", "physical", 49900, "body", "all");
             log.info("Product 种子数据已加载");
         } catch (Exception e) {
             log.warn("Product 种子数据初始化失败: {}", e.getMessage());

+ 10 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthMealController.java

@@ -12,6 +12,7 @@ import javax.annotation.Resource;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
+import com.etotem.cfc.service.PointsService;
 import com.etotem.cfc.util.SortUtil;
 
 @Tag(name = "饮食打卡")
@@ -22,6 +23,9 @@ public class HealthMealController {
     @Resource
     private HealthMealRecordMapper healthMealRecordMapper;
 
+    @Resource
+    private PointsService pointsService;
+
     @Operation(summary = "获取饮食打卡列表")
     @PostMapping("/list")
     public Result<List<HealthMealRecord>> list(
@@ -40,6 +44,12 @@ public class HealthMealController {
     public Result<HealthMealRecord> create(@RequestBody HealthMealRecord record) {
         record.setCreatedAt(new Date());
         healthMealRecordMapper.insert(record);
+        // 饮食打卡返积分:每次记录奖励 5 积分(不影响打卡主流程)
+        try {
+            pointsService.awardCheckinPoints(record.getMemberId(), 5, "饮食打卡");
+        } catch (Exception e) {
+            // 积分发放失败不影响打卡记录
+        }
         return Result.success(record);
     }
 }

+ 4 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/diet/DietPreferencesController.java

@@ -16,7 +16,10 @@ public class DietPreferencesController {
 
     @PostMapping("/current-member")
     public Result<DietPreferencesDTO> getCurrentMemberPreferences(
-            @RequestAttribute("familyMemberId") Long familyMemberId) {
+            @RequestAttribute(value = "familyMemberId", required = false) Long familyMemberId) {
+        if (familyMemberId == null) {
+            return Result.success(null);
+        }
         DietPreferencesDTO dto = dietPreferencesService.getCurrentMemberPreferences(familyMemberId);
         return Result.success(dto);
     }

+ 13 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/DailyCheckinService.java

@@ -6,16 +6,21 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
 
+@Slf4j
 @Service
 public class DailyCheckinService {
 
     @Resource
     private DailyCheckinMapper dailyCheckinMapper;
 
+    @Resource
+    private PointsService pointsService;
+
     public List<DailyHealthCheckin> getCheckinsByMember(Long memberId) {
         return dailyCheckinMapper.selectList(new LambdaQueryWrapper<DailyHealthCheckin>()
                 .eq(DailyHealthCheckin::getMemberId, memberId)
@@ -30,6 +35,14 @@ public class DailyCheckinService {
         checkin.setCreatedAt(new Date());
         checkin.setUpdatedAt(new Date());
         dailyCheckinMapper.insert(checkin);
+
+        // 打卡返积分:每日健康打卡奖励 10 积分(不影响打卡主流程)
+        try {
+            pointsService.awardCheckinPoints(checkin.getMemberId(), 10, "每日健康打卡");
+            log.info("成员{}完成每日打卡,奖励10积分", checkin.getMemberId());
+        } catch (Exception e) {
+            log.warn("打卡积分发放失败,不影响打卡记录: {}", e.getMessage());
+        }
         return checkin;
     }
 

+ 8 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/PointsService.java

@@ -126,6 +126,14 @@ public class PointsService implements PointsServiceInterface {
         return newSystemPoints;
     }
 
+    /**
+     * 打卡返积分(复用 awardSystemPoints 逻辑,仅 category 不同)
+     */
+    @Transactional
+    public int awardCheckinPoints(Long memberId, int amount, String reason) {
+        return awardSystemPoints(memberId, amount, reason);
+    }
+
     /**
      * 心愿兑换扣除积分(家长审批通过后调用)
      * @param memberId 孩子ID

+ 23 - 1
cfc-frontend/pages/health/health-plan-summary.vue

@@ -162,7 +162,8 @@ export default {
       taskSubmitted: false,
       taskSubmittedCount: 0,
       healthStatus: null,
-      dietPrefs: null
+      dietPrefs: null,
+      autoExecute: false
     }
   },
   computed: {
@@ -179,8 +180,17 @@ export default {
       this.selectedMemberIds = this.initialMemberIds.slice()
     }
     this.subjectId = options.subjectId || ''
+    this.autoExecute = options.autoExecute === '1'
+    this.loadFamilyReports()
     this.loadExecutors()
     this.loadHealthContext()
+    // 自动出方案模式:等待异步加载完成后执行
+    if (this.autoExecute) {
+      var self = this
+      setTimeout(function() {
+        self.doAutoExecuteFlow()
+      }, 2500)
+    }
   },
   onShow: function() {
     this.loadHealthContext()
@@ -263,6 +273,14 @@ export default {
       sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6)
       return d.getTime() < sixMonthsAgo.getTime()
     },
+    doAutoExecuteFlow: function() {
+      if (this.selectedMemberIds.length === 0) return
+      // 自动填充默认目标(菌群报告场景)
+      if (!this.userGoal || this.userGoal.trim().length === 0) {
+        this.userGoal = '基于菌群检测报告的个性化健康管理方案'
+      }
+      this.doGeneratePlan()
+    },
     doGeneratePlan: function() {
       var self = this
       self.loading = true
@@ -284,6 +302,10 @@ export default {
           self.planContent = answer
           self.parseTasksFromPlan(answer)
           self.step = 3
+          // 自动出方案模式:方案生成后自动提交任务
+          if (self.autoExecute && self.parsedTasks.length > 0) {
+            setTimeout(function() { self.submitTasks() }, 500)
+          }
         } else {
           uni.showToast({ title: '生成失败,请重试', icon: 'none' })
         }

+ 4 - 5
cfc-frontend/pages/health/report-confirm.vue

@@ -814,11 +814,10 @@ export default {
           var reportType = res.data && res.data.reportType || ''
           setTimeout(function() {
             if (reportId) {
-              if (reportType === 'gut_flora') {
-                uni.redirectTo({ url: '/pages/health/report-detail?reportType=gut_flora&reportId=' + reportId })
-              } else {
-                uni.redirectTo({ url: '/pages/health/health-plan-summary?initialMemberIds=' + (postData.subjectId || '') + '&subjectId=' + (postData.subjectId || '') })
-              }
+              // 所有报告确认入库后统一进入方案制定流程(菌群报告自动出方案)
+              var subjectId = postData.subjectId || ''
+              var autoParam = reportType === 'gut_flora' ? '&autoExecute=1' : ''
+              uni.redirectTo({ url: '/pages/health/health-plan-summary?initialMemberIds=' + subjectId + '&subjectId=' + subjectId + autoParam })
             } else {
               uni.navigateBack()
             }

+ 28 - 5
cfc-frontend/pages/health/report-detail.vue

@@ -14,9 +14,30 @@
       <!-- view mode -->
       <view v-if="pageMode==='view'">
         <report-blocks-renderer v-if="blocks && blocks.length > 0" :blocks="blocks" />
+        <view v-else-if="hasPayloadData" class="payload-fallback">
+          <view class="card" v-if="payload.summary && payload.summary.overallScore">
+            <view class="card-title">综合评分</view>
+            <text class="score-text">{{ payload.summary.overallScore }}</text>
+          </view>
+          <view class="card" v-if="payload.indicators && payload.indicators.length">
+            <view class="card-title">健康指标({{ payload.indicators.length }} 项)</view>
+            <view class="indicator-item" v-for="(ind, idx) in payload.indicators" :key="'fallback-' + idx">
+              <text class="ind-name">{{ ind.indicatorName || ind.name || '' }}</text>
+              <text class="ind-value">{{ ind.indicatorValue || ind.value || '' }}{{ ind.unit || '' }}</text>
+              <text class="ind-status" :class="'status-' + _cssClass(ind.status)">{{ statusText(ind.status) }}</text>
+            </view>
+          </view>
+          <view class="card" v-if="payload.diseaseRisks && payload.diseaseRisks.length">
+            <view class="card-title">疾病风险</view>
+            <view class="risk-item" v-for="(risk, idx) in payload.diseaseRisks" :key="'risk-fb-' + idx">
+              <text class="risk-name">{{ risk.diseaseName || '' }}</text>
+              <view class="risk-level" :class="'risk-' + (risk.riskLevel || 'low')">{{ riskLevelText(risk.riskLevel) }}</view>
+            </view>
+          </view>
+        </view>
         <view class="empty-hint" v-else>暂无报告内容</view>
-        <view class="edit-btn" v-if="isGutFlora && hasRiskBlocks && pageMode==='view'">
-          <view class="nav-edit" @tap="enterEditMode"><text>编辑</text></view>
+        <view class="edit-btn" v-if="isGutFlora && pageMode==='view'">
+          <view class="nav-edit" @tap="enterEditMode"><text>{{ blocks && blocks.length > 0 ? '编辑' : '查看完整数据 ›' }}</text></view>
         </view>
       </view>
 
@@ -433,9 +454,9 @@ export default {
     isGutFlora() {
       return this.reportType === 'gut_flora'
     },
-    hasRiskBlocks() {
-      var list = this.blocks || []
-      return list.some(function(b) { return b.riskLevel === 'high' || b.riskLevel === 'medium' || b.riskLevel === 'moderate' })
+    hasPayloadData() {
+      var p = this.payload
+      return p && ((p.indicators && p.indicators.length > 0) || (p.diseaseRisks && p.diseaseRisks.length > 0) || (p.gutFlora && p.gutFlora.length > 0))
     },
     totalAbnormalCount() {
       var list = this.payload.indicators || []
@@ -899,6 +920,8 @@ export default {
   font-size: 26rpx;
   padding: 60rpx 0;
 }
+.payload-fallback { padding: 0; }
+.payload-fallback .score-text { font-size: 48rpx; font-weight: bold; color: #4A9BD7; text-align: center; display: block; padding: 20rpx 0; }
 .edit-btn {
   margin: 30rpx 0;
   display: flex;