Browse Source

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

Sisyphus Agent 2 tháng trước cách đây
mục cha
commit
f63583f6da

+ 12 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/CommissionController.java

@@ -63,4 +63,16 @@ public class CommissionController {
         balanceInfo.put("withdrawnAmount", summary.getWithdrawnAmount());
         return Result.success(balanceInfo);
     }
+
+    /**
+     * 获取团队成员列表(L1/L2分页)
+     */
+    @PostMapping("/team/list")
+    public Result<Map<String, Object>> teamList(@RequestAttribute("userId") Long userId,
+                                                 @RequestBody Map<String, Object> params) {
+        int page = params.get("page") != null ? (Integer) params.get("page") : 1;
+        int size = params.get("size") != null ? (Integer) params.get("size") : 20;
+        String level = params.get("level") != null ? (String) params.get("level") : "ALL";
+        return Result.success(commissionService.getTeamList(userId, level, page, size));
+    }
 }

+ 32 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/InviteController.java

@@ -5,18 +5,29 @@ import com.etotem.cfc.common.Result;
 import com.etotem.cfc.dto.ReferralSummaryDTO;
 import com.etotem.cfc.entity.User;
 import com.etotem.cfc.service.CommissionService;
+import com.etotem.cfc.service.QrCodeService;
+import com.etotem.cfc.service.WechatService;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
+import java.util.HashMap;
 import java.util.Map;
 
 @RestController
 @RequestMapping("/api/invite")
 public class InviteController {
 
+    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(InviteController.class);
+
     @Resource
     private CommissionService commissionService;
 
+    @Resource
+    private WechatService wechatService;
+
+    @Resource
+    private QrCodeService qrCodeService;
+
     @PostMapping("/code")
     public Result<String> getCode(@RequestAttribute("userId") Long userId) {
         String code = commissionService.getOrCreateReferralCode(userId);
@@ -47,4 +58,25 @@ public class InviteController {
     public Result<ReferralSummaryDTO> summary(@RequestAttribute("userId") Long userId) {
         return Result.success(commissionService.getReferralSummary(userId));
     }
+
+    @PostMapping("/qrcode")
+    public Result<Map<String, Object>> getQrCode(@RequestAttribute("userId") Long userId) {
+        try {
+            String referralCode = commissionService.getOrCreateReferralCode(userId);
+            String qrBase64;
+            try {
+                qrBase64 = wechatService.generateWxacode(referralCode, "pages/invite/join");
+            } catch (Exception e) {
+                log.warn("生成小程序码失败,降级为普通二维码: {}", e.getMessage());
+                qrBase64 = qrCodeService.generateQrCodeBase64(referralCode);
+            }
+            Map<String, Object> result = new HashMap<>();
+            result.put("qrCodeBase64", "data:image/png;base64," + qrBase64);
+            result.put("referralCode", referralCode);
+            return Result.success(result);
+        } catch (Exception e) {
+            log.error("生成推广二维码异常", e);
+            return Result.error("生成二维码失败: " + e.getMessage());
+        }
+    }
 }

+ 85 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java

@@ -17,8 +17,10 @@ import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
+import java.util.ArrayList;
 import java.util.Date;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -342,6 +344,89 @@ public class CommissionService {
                         .orderByDesc(CommissionRecord::getCreatedAt));
     }
 
+    public Map<String, Object> getTeamList(Long userId, String level, int page, int size) {
+        Map<String, Object> result = new HashMap<>();
+        List<Map<String, Object>> records = new ArrayList<>();
+
+        LambdaQueryWrapper<CommissionRecord> wrapper = new LambdaQueryWrapper<CommissionRecord>()
+                .eq(CommissionRecord::getReferrerId, userId);
+
+        if ("L1".equals(level)) {
+            wrapper.eq(CommissionRecord::getLevel, 1);
+        } else if ("L2".equals(level)) {
+            wrapper.eq(CommissionRecord::getLevel, 2);
+        }
+
+        List<CommissionRecord> allRecords = commissionRecordMapper.selectList(wrapper);
+
+        Map<Long, Map<String, Object>> memberMap = new LinkedHashMap<>();
+        for (CommissionRecord record : allRecords) {
+            Long buyerId = record.getBuyerId();
+            if (buyerId == null) continue;
+
+            if (!memberMap.containsKey(buyerId)) {
+                User member = userMapper.selectById(buyerId);
+                if (member == null) continue;
+
+                Map<String, Object> memberInfo = new HashMap<>();
+                memberInfo.put("userId", buyerId);
+                memberInfo.put("nickname", member.getNickname() != null ? member.getNickname() : "微信用户");
+                memberInfo.put("avatarUrl", member.getAvatar() != null ? member.getAvatar() : "");
+                memberInfo.put("createdAt", member.getCreatedAt());
+                memberInfo.put("orderCount", 0);
+                memberInfo.put("commissionEarned", 0);
+                memberMap.put(buyerId, memberInfo);
+            }
+        }
+
+        for (Map.Entry<Long, Map<String, Object>> entry : memberMap.entrySet()) {
+            Long buyerId = entry.getKey();
+            Map<String, Object> memberInfo = entry.getValue();
+
+            LambdaQueryWrapper<CommissionRecord> orderWrapper = new LambdaQueryWrapper<CommissionRecord>()
+                    .eq(CommissionRecord::getReferrerId, userId)
+                    .eq(CommissionRecord::getBuyerId, buyerId);
+            if ("L1".equals(level)) {
+                orderWrapper.eq(CommissionRecord::getLevel, 1);
+            } else if ("L2".equals(level)) {
+                orderWrapper.eq(CommissionRecord::getLevel, 2);
+            }
+            Long orderCount = commissionRecordMapper.selectCount(orderWrapper);
+            memberInfo.put("orderCount", orderCount != null ? orderCount.intValue() : 0);
+
+            LambdaQueryWrapper<CommissionRecord> commissionWrapper = new LambdaQueryWrapper<CommissionRecord>()
+                    .eq(CommissionRecord::getReferrerId, userId)
+                    .eq(CommissionRecord::getBuyerId, buyerId);
+            if ("L1".equals(level)) {
+                commissionWrapper.eq(CommissionRecord::getLevel, 1);
+            } else if ("L2".equals(level)) {
+                commissionWrapper.eq(CommissionRecord::getLevel, 2);
+            }
+            int commissionEarned = commissionRecordMapper.selectList(commissionWrapper).stream()
+                    .mapToInt(r -> r.getCommissionAmount() != null ? r.getCommissionAmount() : 0)
+                    .sum();
+            memberInfo.put("commissionEarned", commissionEarned);
+        }
+
+        List<Map<String, Object>> allMembers = new ArrayList<>(memberMap.values());
+        int total = allMembers.size();
+        int start = (page - 1) * size;
+        int end = Math.min(start + size, total);
+
+        if (start < total) {
+            records = allMembers.subList(start, end);
+        } else {
+            records = new ArrayList<>();
+        }
+
+        result.put("records", records);
+        result.put("total", (long) total);
+        result.put("page", page);
+        result.put("size", size);
+
+        return result;
+    }
+
     /**
      * 获取可提现金额(已结算 - 已提现,排除cancelled)
      */

+ 3 - 2
cfc-frontend/components/FamilyRelationGraph.vue

@@ -105,7 +105,7 @@ export default {
       type: String,
       required: true,
       validator: function(val) {
-        return ['body', 'mind', 'action', 'home'].indexOf(val) !== -1
+        return ['body', 'mind', 'action', 'home', 'wisdom'].indexOf(val) !== -1
       }
     },
     selfId: {
@@ -138,7 +138,8 @@ export default {
         body: '#8D6E63',
         mind: '#FF6B35',
         action: '#4CAF50',
-        home: '#5B9BD5'
+        home: '#5B9BD5',
+        wisdom: '#FFD700'
       }
     }
   },

+ 271 - 0
cfc-frontend/components/SharePoster.vue

@@ -0,0 +1,271 @@
+<template>
+  <view class="poster-overlay" v-if="show" @click.self="$emit('close')">
+    <view class="poster-container">
+      <canvas canvas-id="sharePosterCanvas" class="poster-canvas" id="sharePosterCanvas"></canvas>
+      <view class="poster-actions">
+        <button class="poster-btn save" @click="savePoster">保存到相册</button>
+        <button class="poster-btn cancel" @click="$emit('close')">取消</button>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  name: 'SharePoster',
+  props: {
+    show: { type: Boolean, default: false },
+    userAvatar: { type: String, default: '' },
+    nickname: { type: String, default: '' },
+    referralCode: { type: String, default: '' },
+    qrCodeBase64: { type: String, default: '' }
+  },
+  watch: {
+    show(val) {
+      if (val) {
+        this.$nextTick(function() {
+          setTimeout(function() {
+            this.drawPoster()
+          }.bind(this), 300)
+        }.bind(this))
+      }
+    }
+  },
+  methods: {
+    drawPoster() {
+      var ctx = uni.createCanvasContext('sharePosterCanvas', this)
+      var w = 500
+      var h = 700
+
+      // 1. Orange gradient background
+      var gradient = ctx.createLinearGradient(0, 0, w, h)
+      gradient.addColorStop(0, '#F97316')
+      gradient.addColorStop(1, '#EA580C')
+      ctx.setFillStyle(gradient)
+      ctx.fillRect(0, 0, w, h)
+
+      // 2. White circle border for avatar
+      var avatarX = w / 2
+      var avatarY = 120
+      var avatarR = 50
+      ctx.setStrokeStyle('#ffffff')
+      ctx.setLineWidth(3)
+      ctx.beginPath()
+      ctx.arc(avatarX, avatarY, avatarR + 3, 0, 2 * Math.PI)
+      ctx.stroke()
+
+      // 3. Draw avatar or default icon
+      if (this.userAvatar) {
+        var self = this
+        uni.downloadFile({
+          url: this.userAvatar,
+          success: function(res) {
+            if (res.statusCode === 200) {
+              ctx.save()
+              ctx.beginPath()
+              ctx.arc(avatarX, avatarY, avatarR, 0, 2 * Math.PI)
+              ctx.clip()
+              ctx.drawImage(res.tempFilePath, avatarX - avatarR, avatarY - avatarR, avatarR * 2, avatarR * 2)
+              ctx.restore()
+              self._drawTextAndQr(ctx, w, h, avatarX, avatarY)
+            } else {
+              self._drawDefaultAvatar(ctx, avatarX, avatarY, avatarR)
+              self._drawTextAndQr(ctx, w, h, avatarX, avatarY)
+            }
+          },
+          fail: function() {
+            self._drawDefaultAvatar(ctx, avatarX, avatarY, avatarR)
+            self._drawTextAndQr(ctx, w, h, avatarX, avatarY)
+          }
+        })
+      } else {
+        this._drawDefaultAvatar(ctx, avatarX, avatarY, avatarR)
+        this._drawTextAndQr(ctx, w, h, avatarX, avatarY)
+      }
+    },
+
+    _drawDefaultAvatar(ctx, x, y, r) {
+      ctx.setFillStyle('#ffffff')
+      ctx.beginPath()
+      ctx.arc(x, y, r, 0, 2 * Math.PI)
+      ctx.fill()
+      ctx.setFillStyle('#F97316')
+      ctx.setFontSize(40)
+      ctx.setTextAlign('center')
+      ctx.setTextBaseline('middle')
+      ctx.fillText('\u{1F464}', x, y)
+    },
+
+    _drawTextAndQr(ctx, w, h, avatarX, avatarY) {
+      var self = this
+
+      // 4. Nickname
+      var nameY = avatarY + 80
+      ctx.setFillStyle('#ffffff')
+      ctx.setFontSize(28)
+      ctx.setTextAlign('center')
+      ctx.setTextBaseline('top')
+      ctx.fillText(this.nickname || '\u597d\u53cb', w / 2, nameY)
+
+      // 5. "我的邀请码" label
+      var labelY = nameY + 50
+      ctx.setFontSize(20)
+      ctx.fillText('\u6211\u7684\u9080\u8bf7\u7801', w / 2, labelY)
+
+      // 6. Referral code in large bold text
+      var codeY = labelY + 36
+      ctx.setFontSize(48)
+      ctx.fillText(this.referralCode || '', w / 2, codeY)
+
+      // 7. QR code
+      var qrSize = 220
+      var qrX = (w - qrSize) / 2
+      var qrY = codeY + 70
+
+      if (this.qrCodeBase64) {
+        // Build a file path from base64 data
+        var base64Data = this.qrCodeBase64
+        // Handle data URI prefix
+        if (base64Data.indexOf('data:image') === 0) {
+          var commaIdx = base64Data.indexOf(',')
+          if (commaIdx > -1) {
+            base64Data = base64Data.substring(commaIdx + 1)
+          }
+        }
+
+        var fs = uni.getFileSystemManager()
+        var filePath = wx.env.USER_DATA_PATH + '/share_qr_tmp.png'
+
+        fs.writeFile({
+          filePath: filePath,
+          data: base64Data,
+          encoding: 'base64',
+          success: function() {
+            // White background behind QR
+            ctx.setFillStyle('#ffffff')
+            ctx.fillRect(qrX - 10, qrY - 10, qrSize + 20, qrSize + 20)
+            ctx.drawImage(filePath, qrX, qrY, qrSize, qrSize)
+            self._drawHintAndFinish(ctx, w, h, qrY, qrSize)
+          },
+          fail: function() {
+            self._drawQrPlaceholder(ctx, qrX, qrY, qrSize)
+            self._drawHintAndFinish(ctx, w, h, qrY, qrSize)
+          }
+        })
+      } else {
+        this._drawQrPlaceholder(ctx, qrX, qrY, qrSize)
+        this._drawHintAndFinish(ctx, w, h, qrY, qrSize)
+      }
+    },
+
+    _drawQrPlaceholder(ctx, x, y, size) {
+      ctx.setFillStyle('#ffffff')
+      ctx.fillRect(x - 10, y - 10, size + 20, size + 20)
+      ctx.setFillStyle('#F97316')
+      ctx.setFontSize(18)
+      ctx.setTextAlign('center')
+      ctx.setTextBaseline('middle')
+      ctx.fillText('\u4e8c\u7ef4\u7801\u52a0\u8f7d\u4e2d', x + size / 2, y + size / 2)
+    },
+
+    _drawHintAndFinish(ctx, w, h, qrY, qrSize) {
+      // 8. Hint text below QR
+      var hintY = qrY + qrSize + 20
+      ctx.setFillStyle('#ffffff')
+      ctx.setFontSize(22)
+      ctx.setTextAlign('center')
+      ctx.setTextBaseline('top')
+      ctx.fillText('\u626b\u7801\u9080\u8bf7\u597d\u53cb\u52a0\u5165', w / 2, hintY)
+
+      ctx.draw()
+    },
+
+    savePoster() {
+      var self = this
+      uni.canvasToTempFilePath({
+        canvasId: 'sharePosterCanvas',
+        success: function(res) {
+          uni.saveImageToPhotosAlbum({
+            filePath: res.tempFilePath,
+            success: function() {
+              uni.showToast({ title: '\u5df2\u4fdd\u5b58\u5230\u76f8\u518c', icon: 'success' })
+              self.$emit('close')
+            },
+            fail: function(err) {
+              if (err && err.errMsg && (err.errMsg.indexOf('deny') > -1 || err.errMsg.indexOf('auth denied') > -1 || err.errMsg.indexOf('authorize') > -1)) {
+                uni.showModal({
+                  title: '\u63d0\u793a',
+                  content: '\u9700\u8981\u76f8\u518c\u6743\u9650\u624d\u80fd\u4fdd\u5b58\u56fe\u7247',
+                  confirmText: '\u53bb\u8bbe\u7f6e',
+                  success: function(modalRes) {
+                    if (modalRes.confirm) {
+                      uni.openSetting()
+                    }
+                  }
+                })
+              } else {
+                uni.showToast({ title: '\u4fdd\u5b58\u5931\u8d25\uff0c\u8bf7\u91cd\u8bd5', icon: 'none' })
+              }
+            }
+          })
+        },
+        fail: function() {
+          uni.showToast({ title: '\u751f\u6210\u56fe\u7247\u5931\u8d25', icon: 'none' })
+        }
+      }, this)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.poster-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.6);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 999;
+}
+.poster-container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.poster-canvas {
+  width: 500px;
+  height: 700px;
+  border-radius: 16rpx;
+}
+.poster-actions {
+  display: flex;
+  flex-direction: column;
+  width: 500px;
+  margin-top: 30rpx;
+}
+.poster-btn {
+  height: 80rpx;
+  line-height: 80rpx;
+  font-size: 30rpx;
+  border-radius: 40rpx;
+  text-align: center;
+  border: none;
+}
+.poster-btn::after { border: none; }
+.poster-btn.save {
+  background: #ffffff;
+  color: #F97316;
+  font-weight: 600;
+  margin-bottom: 16rpx;
+}
+.poster-btn.save:active { opacity: 0.85; }
+.poster-btn.cancel {
+  background: rgba(255, 255, 255, 0.15);
+  color: #ffffff;
+}
+.poster-btn.cancel:active { opacity: 0.85; }
+</style>

+ 111 - 0
cfc-frontend/components/WisdomTips.vue

@@ -0,0 +1,111 @@
+<template>
+  <view class="section" v-if="tips.length > 0">
+    <view class="section-header">
+      <text class="section-title">智慧小贴士</text>
+      <text class="section-more" @click="goMore">更多 ›</text>
+    </view>
+    <view class="tip-list">
+      <view class="tip-item" v-for="tip in tips" :key="tip.id" @click="goDetail(tip)">
+        <view class="tip-icon-wrap">
+          <text class="tip-icon">📖</text>
+        </view>
+        <view class="tip-content">
+          <text class="tip-title">{{ tip.title }}</text>
+          <text class="tip-summary">{{ tip.summary }}</text>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import config from '@/config.js'
+
+export default {
+  props: {
+    dimension: { type: String, default: 'wisdom' },
+    limit: { type: Number, default: 4 }
+  },
+  data: function() {
+    return { tips: [] }
+  },
+  created: function() {
+    this.loadTips()
+  },
+  methods: {
+    loadTips: function() {
+      var self = this
+      var token = uni.getStorageSync('token')
+      uni.request({
+        url: config.API_BASE_URL + '/api/articles/list',
+        method: 'POST',
+        data: {
+          dimension: this.dimension,
+          page: 1,
+          size: this.limit
+        },
+        header: {
+          'Content-Type': 'application/json',
+          'Authorization': token ? 'Bearer ' + token : ''
+        },
+        success: function(res) {
+          if (res.data && res.data.code === 200 && res.data.data && res.data.data.records) {
+            var list = res.data.data.records
+            self.tips = list.slice(0, self.limit)
+          }
+        },
+        fail: function() {}
+      })
+    },
+    goDetail: function(tip) {
+      uni.navigateTo({ url: '/pages/mind/article-detail?id=' + tip.id })
+    },
+    goMore: function() {
+      uni.navigateTo({ url: '/pages/mind/articles?dimension=' + this.dimension })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.section { margin: 20rpx 30rpx; }
+.section-header {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 16rpx;
+}
+.section-title { font-size: 30rpx; font-weight: bold; color: #333; }
+.section-more { font-size: 24rpx; color: #999; }
+.tip-list {
+  background: #fff;
+  border-radius: 20rpx;
+  overflow: hidden;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+}
+.tip-item {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 24rpx 20rpx;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+.tip-item:last-child { border-bottom: none; }
+.tip-item:active { background: #fafafa; }
+.tip-icon-wrap {
+  width: 48rpx;
+  height: 48rpx;
+  border-radius: 12rpx;
+  background: #FFF8E1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 16rpx;
+  flex-shrink: 0;
+}
+.tip-icon { font-size: 28rpx; }
+.tip-content { flex: 1; }
+.tip-title { font-size: 28rpx; color: #333; font-weight: 500; display: block; }
+.tip-summary { font-size: 24rpx; color: #999; margin-top: 4rpx; display: block; }
+</style>

+ 23 - 0
cfc-frontend/pages.json

@@ -164,6 +164,25 @@
         "navigationBarTitleText": "认知测评报告"
       }
     },
+    {
+      "path": "pages/wisdom/member-wisdom-detail",
+      "style": {
+        "navigationBarTitleText": "智能量详情",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/wisdom/self-quiz",
+      "style": {
+        "navigationBarTitleText": "自己出题自己答"
+      }
+    },
+    {
+      "path": "pages/wisdom/training-hub",
+      "style": {
+        "navigationBarTitleText": "认知训练中心"
+      }
+    },
     {
       "path": "pages/action/member-action-detail",
       "style": {
@@ -600,6 +619,10 @@
         {
           "path": "invite",
           "style": { "navigationBarTitleText": "邀请好友" }
+        },
+        {
+          "path": "team",
+          "style": { "navigationBarTitleText": "推广团队" }
         }
       ]
     },

+ 7 - 14
cfc-frontend/pages/parent/invite/index.vue

@@ -83,22 +83,15 @@ export default {
     this.loadFamilyName()
   },
   onShareAppMessage() {
-    const title = this.role === 'teacher' 
-      ? '成长规划师邀请 - 加入我的服务家庭' 
-      : '邀请您加入我的家庭'
-    return {
-      title: title,
-      path: `/pages/login/login?inviteCode=${this.inviteCode}&inviteType=${this.role === 'teacher' ? 'guide' : 'family'}`,
-      imageUrl: '/static/share-card.png' // 可选:自定义分享卡片图片
+    var code = this.inviteCode || ''
+    var path = '/pages/invite/join'
+    if (code) {
+      path = path + '?inviteCode=' + code
     }
-  },
-  // 分享到朋友圈
-  onShareTimeline() {
     return {
-      title: this.role === 'teacher' 
-        ? '成长规划师邀请 - 加入我的服务家庭' 
-        : '邀请您加入我的家庭',
-      query: `inviteCode=${this.inviteCode}&inviteType=${this.role === 'teacher' ? 'guide' : 'family'}`
+      title: '我在浠艾福发现了好用的家庭成长工具,快来加入!',
+      path: path,
+      imageUrl: ''
     }
   },
   methods: {

+ 38 - 5
cfc-frontend/pages/promotion/index.vue

@@ -67,19 +67,27 @@
       </view>
     </view>
 
+    <SharePoster :show="showPoster" :referralCode="referralCode" :qrCodeBase64="qrCodeBase64" :nickname="userNickname" :userAvatar="userAvatar" @close="showPoster = false" />
+
   </scroll-view>
 </template>
 
 <script>
-import { getReferralCode, getReferralSummary, getCommissionSummary } from '../../utils/api.js'
+import { getReferralCode, getReferralSummary, getCommissionSummary, getInviteQrCode } from '../../utils/api.js'
+import SharePoster from '@/components/SharePoster.vue'
 
 export default {
+  components: { SharePoster },
   data() {
     return {
       referralCode: '',
       totalEarnings: '0.00',
       availableAmount: '0.00',
-      summary: null
+      summary: null,
+      showPoster: false,
+      qrCodeBase64: '',
+      userAvatar: '',
+      userNickname: ''
     }
   },
   computed: {
@@ -90,6 +98,18 @@ export default {
   onLoad() {
     this.loadData()
   },
+  onShareAppMessage() {
+    var code = this.referralCode || ''
+    var path = '/pages/invite/join'
+    if (code) {
+      path = path + '?inviteCode=' + code
+    }
+    return {
+      title: '我在浠艾福发现了好用的家庭成长工具,快来加入!',
+      path: path,
+      imageUrl: ''
+    }
+  },
   methods: {
     async loadData() {
       try {
@@ -127,8 +147,21 @@ export default {
         }
       })
     },
-    shareCard() {
-      uni.showToast({ title: '即将上线', icon: 'none' })
+    async shareCard() {
+      try {
+        uni.showLoading({ title: '生成中...' })
+        var res = await getInviteQrCode()
+        if (res.data) {
+          this.qrCodeBase64 = res.data.qrCodeBase64 || ''
+          this.userNickname = uni.getStorageSync('nickname') || ''
+          this.userAvatar = uni.getStorageSync('avatar') || ''
+          this.showPoster = true
+        }
+      } catch (e) {
+        uni.showToast({ title: '生成失败', icon: 'none' })
+      } finally {
+        uni.hideLoading()
+      }
     },
     goCommission() {
       uni.navigateTo({ url: '/pages/promotion/commission' })
@@ -140,7 +173,7 @@ export default {
       uni.navigateTo({ url: '/pages/promotion/invite' })
     },
     goTeam() {
-      uni.showToast({ title: '即将上线', icon: 'none' })
+      uni.navigateTo({ url: '/pages/promotion/team' })
     }
   }
 }

+ 38 - 4
cfc-frontend/pages/promotion/invite.vue

@@ -59,13 +59,18 @@
         <text>加载中...</text>
       </view>
     </view>
+
+    <SharePoster :show="showPoster" :referralCode="referralCode" :qrCodeBase64="qrCodeBase64" :nickname="userNickname" :userAvatar="userAvatar" @close="showPoster = false" />
+
   </view>
 </template>
 
 <script>
-import { getReferralCode, bindReferral, getReferralList, getReferralSummary } from '../../utils/api.js'
+import { getReferralCode, bindReferral, getReferralList, getReferralSummary, getInviteQrCode } from '../../utils/api.js'
+import SharePoster from '@/components/SharePoster.vue'
 
 export default {
+  components: { SharePoster },
   data() {
     return {
       referralCode: '',
@@ -75,7 +80,11 @@ export default {
       page: 1,
       loading: false,
       hasMore: true,
-      summary: null
+      summary: null,
+      showPoster: false,
+      qrCodeBase64: '',
+      userAvatar: '',
+      userNickname: ''
     }
   },
   onLoad() {
@@ -89,6 +98,18 @@ export default {
       this.loadList()
     }
   },
+  onShareAppMessage() {
+    var code = this.referralCode || ''
+    var path = '/pages/invite/join'
+    if (code) {
+      path = path + '?inviteCode=' + code
+    }
+    return {
+      title: '我在浠艾福发现了好用的家庭成长工具,快来加入!',
+      path: path,
+      imageUrl: ''
+    }
+  },
   methods: {
     async loadCode() {
       try {
@@ -139,8 +160,21 @@ export default {
         }
       })
     },
-    shareInvite() {
-      uni.showToast({ title: '即将上线', icon: 'none' })
+    async shareInvite() {
+      try {
+        uni.showLoading({ title: '生成中...' })
+        var res = await getInviteQrCode()
+        if (res.data) {
+          this.qrCodeBase64 = res.data.qrCodeBase64 || ''
+          this.userNickname = uni.getStorageSync('nickname') || ''
+          this.userAvatar = uni.getStorageSync('avatar') || ''
+          this.showPoster = true
+        }
+      } catch (e) {
+        uni.showToast({ title: '生成失败', icon: 'none' })
+      } finally {
+        uni.hideLoading()
+      }
     },
     async bindReferral() {
       if (!this.bindCode) {

+ 290 - 0
cfc-frontend/pages/promotion/team.vue

@@ -0,0 +1,290 @@
+<template>
+  <view class="container">
+    <!-- 团队统计 -->
+    <view class="stats-card">
+      <view class="stats-item">
+        <text class="stats-num" v-if="teamStats">{{ teamStats.l1Count || 0 }}</text>
+        <text class="stats-num" v-else>0</text>
+        <text class="stats-label">直推(L1)</text>
+      </view>
+      <view class="stats-divider"></view>
+      <view class="stats-item">
+        <text class="stats-num" v-if="teamStats">{{ teamStats.l2Count || 0 }}</text>
+        <text class="stats-num" v-else>0</text>
+        <text class="stats-label">间推(L2)</text>
+      </view>
+      <view class="stats-divider"></view>
+      <view class="stats-item">
+        <text class="stats-num" v-if="teamStats">{{ formatPriceWithSymbol(teamStats.totalCommission) }}</text>
+        <text class="stats-num" v-else>{{ formatPriceWithSymbol(0) }}</text>
+        <text class="stats-label">团队佣金</text>
+      </view>
+    </view>
+
+    <!-- Tab 切换 -->
+    <view class="tab-bar">
+      <view class="tab-item" :class="level === 'L1' ? 'tab-active' : ''" @click="switchLevel('L1')">
+        <text class="tab-text">直推(L1)</text>
+      </view>
+      <view class="tab-item" :class="level === 'L2' ? 'tab-active' : ''" @click="switchLevel('L2')">
+        <text class="tab-text">间推(L2)</text>
+      </view>
+    </view>
+
+    <!-- 成员列表 -->
+    <view class="list-section">
+      <text class="section-title">团队成员 ({{ total || 0 }})</text>
+      <view class="empty-state" v-if="!loading && list.length === 0">
+        <text>还没有推广团队成员</text>
+      </view>
+      <view class="member-item" v-for="item in list" :key="item.userId">
+        <view class="member-avatar-wrap">
+          <image v-if="item.avatarUrl" class="member-avatar-img" :src="item.avatarUrl" mode="aspectFill"></image>
+          <text v-else class="member-avatar-icon">👤</text>
+        </view>
+        <view class="member-info">
+          <text class="member-name">{{ item.nickname || '微信用户' }}</text>
+          <text class="member-time">{{ formatTime(item.createdAt) }}</text>
+        </view>
+        <view class="member-right">
+          <view class="member-badge" v-if="item.orderCount && item.orderCount > 0">
+            <text class="badge-text">{{ item.orderCount }}单</text>
+          </view>
+          <text class="member-commission">{{ formatPriceWithSymbol(item.commissionEarned) }}</text>
+        </view>
+      </view>
+      <view class="loading-state" v-if="loading">
+        <text>加载中...</text>
+      </view>
+      <view class="loadmore-state" v-if="!loading && list.length > 0 && !hasMore">
+        <text>没有更多了</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getCommissionTeam, getTeamList } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      list: [],
+      total: 0,
+      page: 1,
+      level: 'L1',
+      loading: false,
+      hasMore: true,
+      teamStats: null
+    }
+  },
+  onLoad() {
+    this.loadTeamStats()
+    this.loadList()
+  },
+  onReachBottom() {
+    if (this.hasMore && !this.loading) {
+      this.page++
+      this.loadList()
+    }
+  },
+  methods: {
+    async loadTeamStats() {
+      try {
+        var res = await getCommissionTeam(1, 1)
+        if (res.data) {
+          this.teamStats = res.data
+        }
+      } catch (e) {
+        console.error('获取团队统计失败', e)
+      }
+    },
+    async loadList() {
+      this.loading = true
+      try {
+        var res = await getTeamList(this.page, 20, this.level)
+        if (res.data && res.data.records) {
+          this.list = this.page === 1 ? res.data.records : this.list.concat(res.data.records)
+          this.total = res.data.total || 0
+          this.hasMore = res.data.records.length >= 20
+        } else {
+          this.hasMore = false
+        }
+      } catch (e) {
+        console.error('获取团队成员失败', e)
+      }
+      this.loading = false
+    },
+    switchLevel(level) {
+      if (this.level === level) return
+      this.level = level
+      this.page = 1
+      this.list = []
+      this.hasMore = true
+      this.loadList()
+    },
+    formatTime(time) {
+      if (!time) return ''
+      var t = new Date(time)
+      return t.getFullYear() + '-' + String(t.getMonth() + 1).padStart(2, '0') + '-' + String(t.getDate()).padStart(2, '0')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  padding: 30rpx;
+  min-height: 100vh;
+  background: #FFF7ED;
+}
+
+/* ===== 团队统计 ===== */
+.stats-card {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 32rpx 24rpx;
+  display: flex;
+  align-items: center;
+  margin-bottom: 24rpx;
+  box-shadow: 0 2rpx 12rpx rgba(249,115,22,0.08);
+}
+.stats-item {
+  flex: 1;
+  text-align: center;
+}
+.stats-num {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #F97316;
+  display: block;
+}
+.stats-label {
+  font-size: 22rpx;
+  color: #94A3B8;
+  margin-top: 4rpx;
+  display: block;
+}
+.stats-divider {
+  width: 1rpx;
+  height: 48rpx;
+  background: #FED7AA;
+}
+
+/* ===== Tab切换 ===== */
+.tab-bar {
+  display: flex;
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 8rpx;
+  margin-bottom: 24rpx;
+  box-shadow: 0 2rpx 8rpx rgba(249,115,22,0.06);
+}
+.tab-item {
+  flex: 1;
+  text-align: center;
+  padding: 16rpx 0;
+  border-radius: 12rpx;
+}
+.tab-active {
+  background: #F97316;
+}
+.tab-active .tab-text {
+  color: #fff;
+  font-weight: 600;
+}
+.tab-text {
+  font-size: 26rpx;
+  color: #64748B;
+}
+
+/* ===== 成员列表 ===== */
+.list-section {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 30rpx;
+  box-shadow: 0 2rpx 12rpx rgba(249,115,22,0.08);
+}
+.section-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #1E293B;
+  display: block;
+  margin-bottom: 20rpx;
+}
+.member-item {
+  display: flex;
+  align-items: center;
+  padding: 20rpx 0;
+  border-bottom: 1rpx solid #FED7AA;
+}
+.member-item:last-child {
+  border-bottom: none;
+}
+.member-avatar-wrap {
+  width: 72rpx;
+  height: 72rpx;
+  border-radius: 50%;
+  background: #FEF3C7;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 20rpx;
+  overflow: hidden;
+  flex-shrink: 0;
+}
+.member-avatar-img {
+  width: 72rpx;
+  height: 72rpx;
+  border-radius: 50%;
+}
+.member-avatar-icon {
+  font-size: 36rpx;
+}
+.member-info {
+  flex: 1;
+  min-width: 0;
+}
+.member-name {
+  font-size: 28rpx;
+  color: #1E293B;
+  display: block;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.member-time {
+  font-size: 22rpx;
+  color: #94A3B8;
+  margin-top: 4rpx;
+  display: block;
+}
+.member-right {
+  text-align: right;
+  flex-shrink: 0;
+  margin-left: 16rpx;
+}
+.member-badge {
+  display: inline-block;
+  background: #FEF3C7;
+  border-radius: 8rpx;
+  padding: 2rpx 10rpx;
+  margin-bottom: 4rpx;
+}
+.badge-text {
+  font-size: 20rpx;
+  color: #D97706;
+}
+.member-commission {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #F97316;
+  display: block;
+}
+.empty-state, .loading-state, .loadmore-state {
+  text-align: center;
+  padding: 60rpx;
+  color: #94A3B8;
+  font-size: 28rpx;
+}
+</style>

+ 1 - 2
cfc-frontend/pages/wisdom/cognitive-report.vue

@@ -21,7 +21,7 @@
         </view>
       </view>
 
-      <!-- 维条形图 -->
+      <!-- 维条形图 -->
       <view class="dimension-list">
         <view class="dimension-item" v-for="dim in dimensions" :key="dim.key">
           <view class="dimension-header">
@@ -99,7 +99,6 @@ export default {
         { key: 'memoryScore', label: '记忆力', icon: '\u{1F9E0}', desc: '信息编码和回忆能力' },
         { key: 'logicScore', label: '逻辑思维', icon: '\u{1F9E9}', desc: '推理和问题解决能力' },
         { key: 'spatialScore', label: '空间思维', icon: '\u{1F9CA}', desc: '空间想象和方位判断' },
-        { key: 'attentionScore', label: '注意力', icon: '\u{1F50D}', desc: '信息筛选和专注能力' },
         { key: 'processingSpeedScore', label: '加工速度', icon: '\u26A1', desc: '信息处理和反应速度' }
       ]
     }

+ 487 - 155
cfc-frontend/pages/wisdom/index.vue

@@ -1,25 +1,50 @@
 <template>
-  <view class="container">
+  <view class="wisdom-container">
+    <!-- 1. 品牌头部 -->
     <PageBanner theme="wisdom"
       tagline="学以启智 · 慧及未来"
       quote="知者不惑,仁者不忧" />
 
-    <view class="section" v-if="sectionVisible('today_tasks') && isLoggedIn">
-      <view class="section-header">
-        <text class="section-title">📋 我的任务</text>
-        <text class="section-more" @click="goTasks">全部 ›</text>
-      </view>
-      <view class="task-wrap" v-if="todayTasks.length > 0">
-        <view class="task-item" v-for="task in todayTasks" :key="task.id">
-          <text class="task-name">{{ task.title }}</text>
-          <text class="task-points">+{{ task.points || 0 }}分</text>
-        </view>
-      </view>
-      <view class="task-empty" v-else>
-        <text class="task-empty-text">今日暂无任务</text>
-      </view>
-    </view>
+    <!-- 2. 游客专属区块 -->
+    <template v-if="!isLoggedIn">
+      <LoginGuideCard
+        title="登录后查看全家智慧成长数据"
+        :items="[
+          '查看全家认知能量数据和测评报告',
+          '记录每日成长,获取智慧能量',
+          '参与认知训练,提升综合能力'
+        ]" />
+      <WisdomTips />
+    </template>
 
+    <!-- 3. 五维能量条(登录后) -->
+    <FamilyEnergyBar
+      v-if="isLoggedIn"
+      dimensionCode="wisdom"
+      :sandboxData="sandboxData"
+      :dualDimension="dualDimension" />
+
+    <!-- 4. 家庭成员关系图谱(登录后) -->
+    <FamilyRelationGraph
+      v-if="isLoggedIn && familyMembersVisible.length > 0"
+      dimensionCode="wisdom"
+      :selfId="activeChildId"
+      :members="familyMembersVisible"
+      :energyMap="energyMapForGraph"
+      :intimacyMap="intimacyMapForGraph"
+      @memberTap="goMemberDetail" />
+
+    <!-- 5. 用户快捷入口 -->
+    <UserQuickEntry
+      v-if="isLoggedIn"
+      :userName="userName"
+      roleName="家长"
+      :currentChildName="activeChildName"
+      :children="children"
+      @childChanged="onChildChanged"
+      @scrollTo="scrollToSection" />
+
+    <!-- 6. 功能入口 -->
     <view class="func-section" v-if="sectionVisible('func_entries')">
       <view class="func-grid">
         <view class="func-item" v-for="item in funcList" :key="item.label" @click="onFuncClick(item)">
@@ -31,72 +56,177 @@
       </view>
     </view>
 
-    <view class="section" v-if="sectionVisible('hot_activities')">
+    <!-- 7. 认知摘要区(登录后) -->
+    <view class="section" v-if="isLoggedIn">
       <view class="section-header">
-        <text class="section-title">🔥 热门活动</text>
-        <text class="section-more" @click="goMoreActivities">更多 ›</text>
+        <text class="section-title">认知概览</text>
+        <text class="section-more" @click="goCognitiveReport">查看详情 ›</text>
       </view>
-      <scroll-view scroll-x class="activity-scroll" show-scrollbar="false">
-        <view class="activity-card" v-for="act in hotActivities" :key="act.id" @click="goActivityDetail(act.id)">
-          <image class="activity-img" :src="act.image || '/static/default-activity.png'" mode="aspectFill" />
-          <view class="activity-info">
-            <text class="activity-name">{{ act.name }}</text>
-            <text class="activity-meta">{{ act.date }} · {{ act.location }}</text>
-          </view>
+      <view class="cognition-summary" v-if="cognitiveReport">
+        <view class="summary-main">
+          <text class="summary-score">{{ cognitiveReport.overallScore || '--' }}</text>
+          <text class="summary-label">综合认知评分</text>
+        </view>
+        <view class="summary-detail">
+          <text class="summary-dan">DAN等级: {{ cognitiveReport.danLevel || '-' }}</text>
+          <text class="summary-min" v-if="lowestDimension">
+            待提升: {{ lowestDimension.label }} ({{ lowestDimension.score }})
+          </text>
+        </view>
+      </view>
+      <view class="cognition-empty" v-else>
+        <text class="empty-tip">暂无认知测评数据</text>
+        <text class="empty-sub">完成DAN评估后查看详细报告</text>
+      </view>
+    </view>
+
+    <!-- 8. 自己出题入口卡(登录后) -->
+    <view class="section highlight-card" v-if="isLoggedIn" @click="goSelfQuiz">
+      <view class="highlight-card-inner">
+        <view class="highlight-card-left">
+          <text class="highlight-icon">✏️</text>
+        </view>
+        <view class="highlight-card-right">
+          <text class="highlight-title">自己出题自己答</text>
+          <text class="highlight-desc">元认知训练:自己出题、自己判分,提升学习能力</text>
+        </view>
+        <text class="highlight-arrow">›</text>
+      </view>
+    </view>
+
+    <!-- 9. 训练中心入口卡(登录后) -->
+    <view class="section highlight-card" v-if="isLoggedIn" @click="goTrainingHub">
+      <view class="highlight-card-inner">
+        <view class="highlight-card-left">
+          <text class="highlight-icon">🧠</text>
         </view>
-      </scroll-view>
+        <view class="highlight-card-right">
+          <text class="highlight-title">认知训练中心</text>
+          <text class="highlight-desc">舒尔特方格 · 猜数字 — 多维度认知训练游戏</text>
+        </view>
+        <text class="highlight-arrow">›</text>
+      </view>
     </view>
 
-    <view class="section" v-if="sectionVisible('recommended_products')">
+    <!-- 10. 维度任务 -->
+    <DimensionTasks
+      v-if="isLoggedIn"
+      dimensionCode="wisdom"
+      :tasks="dimensionTasks"
+      @taskClick="onTaskClick"
+      @moreTasks="goTasks" />
+
+    <!-- 11. 维度活动 -->
+    <DimensionActivities
+      dimensionCode="wisdom"
+      :activities="dimensionActivities"
+      @activityClick="goActivityDetail"
+      @moreActivities="goMoreActivities" />
+
+    <!-- 12. 维度商品 -->
+    <DimensionProducts
+      dimensionCode="wisdom"
+      :products="dimensionProducts"
+      @productClick="goProductDetail"
+      @moreProducts="goMoreProducts" />
+
+    <!-- 13. 智慧小贴士 -->
+    <view class="section" v-if="sectionVisible('wisdom_tips')">
       <view class="section-header">
-        <text class="section-title">🛍️ 推荐商品</text>
-        <text class="section-more" @click="goMoreProducts">更多 ›</text>
+        <text class="section-title">智慧小贴士</text>
       </view>
-      <view class="product-list">
-        <view class="product-card" v-for="prod in recommendedProducts" :key="prod.id" @click="goProductDetail(prod.id)">
-          <image class="product-img" :src="prod.image || '/static/default-product.png'" mode="aspectFill" />
-          <text class="product-name line-clamp-2">{{ prod.name }}</text>
-          <text class="product-price">{{ formatPriceWithSymbol(prod.price) }}</text>
+      <view class="tips-list">
+        <view class="tip-card" v-for="item in wisdomTips" :key="item.id">
+          <text class="tip-title">{{ item.title }}</text>
+          <text class="tip-summary">{{ item.summary }}</text>
         </view>
       </view>
     </view>
 
+    <!-- 14. 快捷操作按钮 -->
+    <view class="section quick-actions" v-if="isLoggedIn">
+      <view class="action-btn" @click="goCognitiveReport">
+        <text class="action-icon">📊</text>
+        <text class="action-text">认知报告</text>
+      </view>
+      <view class="action-btn" @click="goTrainingHub">
+        <text class="action-icon">🧠</text>
+        <text class="action-text">认知训练</text>
+      </view>
+      <view class="action-btn" @click="goAssessment">
+        <text class="action-icon">📋</text>
+        <text class="action-text">测评预约</text>
+      </view>
+    </view>
+
+    <!-- 底部占位 -->
     <view class="bottom-spacer"></view>
   </view>
 </template>
 
 <script>
 import PageBanner from '../../components/PageBanner.vue'
-import { getVisibleSections, getEnergyOverview, getTodayTasks } from '../../utils/api.js'
+import LoginGuideCard from '../../components/LoginGuideCard.vue'
+import WisdomTips from '../../components/WisdomTips.vue'
+import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
+import UserQuickEntry from '../../components/UserQuickEntry.vue'
+import DimensionTasks from '../../components/DimensionTasks.vue'
+import DimensionActivities from '../../components/DimensionActivities.vue'
+import DimensionProducts from '../../components/DimensionProducts.vue'
+import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
+import { getVisibleSections, getChildren, getTodayTasksByCategory, getActivityList, getProductsByDomain, getFamilyEnergySandbox, getAssessmentLatestResult } from '../../utils/api.js'
 
 export default {
-  components: { PageBanner },
+  components: { PageBanner, LoginGuideCard, WisdomTips, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts, FamilyRelationGraph },
   data() {
     return {
       isLoggedIn: false,
       role: null,
-      currentChildId: null,
-      totalEnergy: null,
+      activeChildId: null,
+      activeChildName: '',
+      userName: '',
+      children: [],
+      familyMembersVisible: [],
+      sandboxData: null,
+      dualDimension: null,
       visibleSections: [],
+      cognitiveReport: null,
+      lowestDimension: null,
+      dimensionTasks: [],
+      dimensionActivities: [],
+      dimensionProducts: [],
       funcList: [
-        { icon: '\u{1F4DA}', label: '学习任务', needLogin: true, page: '/pages/tasks/tasks' },
-        { icon: '\u{1F3AE}', label: '小游戏', needLogin: false, page: '/pages/games/list' },
-        { icon: '\u{1F9E9}', label: '测评中心', needLogin: false, page: '/pages/assessment/apply' },
-        { icon: '\u{1F4D6}', label: '阅读天地', needLogin: false, page: '/pages/mind/articles' },
-        { icon: '\u{1F4A1}', label: '知识挑战', needLogin: false, page: '' }
-      ],
-      hotActivities: [
-        { id: 1, name: '少儿编程启蒙营', date: '周六', location: '线上', image: '' },
-        { id: 2, name: '经典阅读分享会', date: '周日', location: '浠艾福中心', image: '' },
-        { id: 3, name: '科学实验探索课', date: '下周六', location: '浠艾福中心', image: '' }
+        { icon: '\u{1F4CA}', label: '认知报告', needLogin: true, page: '/pages/wisdom/cognitive-report' },
+        { icon: '\u{270F}\uFE0F', label: '自己出题', needLogin: true, page: '/pages/wisdom/self-quiz' },
+        { icon: '\u{1F9E0}', label: '训练中心', needLogin: true, page: '/pages/wisdom/training-hub' },
+        { icon: '\u{1F3AF}', label: '测评预约', needLogin: false, page: '/pages/assessment/apply' },
+        { icon: '\u{1F4D6}', label: '阅读天地', needLogin: false, page: '/pages/mind/articles' }
       ],
-      recommendedProducts: [
-        { id: 1, name: '儿童百科全书套装', price: 199, image: '' },
-        { id: 2, name: '益智棋类礼盒', price: 159, image: '' },
-        { id: 3, name: '科学实验套装', price: 299, image: '' },
-        { id: 4, name: '中外名著精选', price: 129, image: '' }
-      ],
-      todayTasks: []
+      wisdomTips: [
+        { id: 1, title: '认知发展的关键期', summary: '儿童认知发展存在关键期窗口,抓住每个阶段的核心能力培养机会。' },
+        { id: 2, title: '元认知训练的意义', summary: '教会孩子"学会如何学习",通过自我监控和反思提升学习效率。' },
+        { id: 3, title: '专注力的培养方法', summary: '从短时间专注起步,逐步延长专注时长,配合舒尔特方格等训练工具。' },
+        { id: 4, title: '多维度认知刺激', summary: '综合运用视、听、触等多种感官刺激,促进大脑神经网络全面发展。' }
+      ]
+    }
+  },
+  computed: {
+    energyMapForGraph: function() {
+      var map = {}
+      if (this.sandboxData && this.sandboxData.members) {
+        for (var i = 0; i < this.sandboxData.members.length; i++) {
+          var m = this.sandboxData.members[i]
+          map[m.memberId || m.id] = {
+            bodyScore: m.bodyScore || 0,
+            mindScore: m.mindScore || 0,
+            actionScore: m.actionScore || 0
+          }
+        }
+      }
+      return map
+    },
+    intimacyMapForGraph: function() {
+      return {}
     }
   },
   onShow() {
@@ -104,58 +234,147 @@ export default {
     this.isLoggedIn = !!token
     if (this.isLoggedIn) {
       this.role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || null
-      this.currentChildId = uni.getStorageSync('currentChildId') || null
+      this.userName = uni.getStorageSync('nickname') || uni.getStorageSync('userName') || '家长'
     }
     this.loadSectionConfig()
     if (this.isLoggedIn) {
-      this.loadEnergyData()
-      this.loadTodayTasks()
+      this.loadChildren()
+      this.loadFamilyMembersVisible()
+      this.loadCognitiveReport()
     }
+    this.loadDimensionActivities()
+    this.loadDimensionProducts()
   },
   methods: {
-    async loadSectionConfig() {
-      var baseSections = ['func_entries', 'hot_activities', 'recommended_products']
+    loadSectionConfig: function() {
+      var baseSections = ['func_entries', 'wisdom_tips']
       if (!uni.getStorageSync('token')) {
         this.visibleSections = baseSections
         return
       }
       var role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || null
-      try {
-        var res = await getVisibleSections({ pageKey: 'wisdom', role: role })
+      var self = this
+      getVisibleSections({ pageKey: 'wisdom', role: role }).then(function(res) {
         if (res.code === 200 && res.data) {
           var keys = res.data.map(function(s) { return s.sectionKey })
           var merged = baseSections.slice()
           keys.forEach(function(k) { if (merged.indexOf(k) === -1) merged.push(k) })
-          this.visibleSections = merged
+          self.visibleSections = merged
+        }
+      }).catch(function(e) {
+        self.visibleSections = ['func_entries', 'wisdom_tips']
+      })
+    },
+    loadChildren: function() {
+      var self = this
+      getChildren().then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.children = res.data
+          if (self.children.length > 0) {
+            var activeId = uni.getStorageSync('currentChildId')
+            if (activeId) {
+              for (var i = 0; i < self.children.length; i++) {
+                if (self.children[i].id == activeId) {
+                  self.activeChildId = activeId
+                  self.activeChildName = self.children[i].nickname || self.children[i].name || ''
+                  break
+                }
+              }
+            }
+            if (!self.activeChildId) {
+              self.activeChildId = self.children[0].id
+              self.activeChildName = self.children[0].nickname || self.children[0].name || ''
+              uni.setStorageSync('currentChildId', self.activeChildId)
+            }
+          }
+        }
+      }).catch(function(e) {})
+    },
+    loadFamilyMembersVisible: function() {
+      var self = this
+      getFamilyEnergySandbox().then(function(res) {
+        if (res && res.data) {
+          self.sandboxData = res.data.sandboxData || null
+          self.dualDimension = res.data.dualDimension || null
+          if (res.data.members) {
+            self.familyMembersVisible = res.data.members
+          }
+        }
+      }).catch(function(e) {})
+    },
+    loadCognitiveReport: function() {
+      var self = this
+      var childId = this.activeChildId
+      if (!childId) return
+      getAssessmentLatestResult(childId).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.cognitiveReport = res.data
+          self.findLowestDimension(res.data)
+        }
+      }).catch(function(e) {})
+    },
+    findLowestDimension: function(report) {
+      var dims = [
+        { key: 'perceptionScore', label: '感知能力' },
+        { key: 'focusScore', label: '专注力' },
+        { key: 'memoryScore', label: '记忆力' },
+        { key: 'logicScore', label: '逻辑思维' },
+        { key: 'spatialScore', label: '空间思维' },
+        { key: 'processingSpeedScore', label: '加工速度' }
+      ]
+      var lowest = null
+      var minVal = Infinity
+      for (var i = 0; i < dims.length; i++) {
+        var val = report[dims[i].key]
+        if (val !== null && val !== undefined && val < minVal) {
+          minVal = val
+          lowest = { label: dims[i].label, score: val }
+        }
+      }
+      this.lowestDimension = lowest
+    },
+    onChildChanged: function(childId) {
+      this.activeChildId = childId
+      uni.setStorageSync('currentChildId', childId)
+      for (var i = 0; i < this.children.length; i++) {
+        if (this.children[i].id == childId) {
+          this.activeChildName = this.children[i].nickname || this.children[i].name || ''
+          break
         }
-      } catch (e) {
-        this.visibleSections = ['func_entries', 'hot_activities', 'recommended_products']
       }
+      this.loadFamilyMembersVisible()
+      this.loadCognitiveReport()
     },
-    async loadEnergyData() {
-      var childId = this.currentChildId
+    loadDimensionTasks: function() {
+      var self = this
+      var childId = this.activeChildId
       if (!childId) return
-      try {
-        var res = await getEnergyOverview(childId)
+      getTodayTasksByCategory(childId, 'wisdom').then(function(res) {
         if (res && res.data) {
-          this.totalEnergy = res.data.totalEnergy || 0
+          self.dimensionTasks = res.data.slice(0, 5)
         }
-      } catch (e) { console.log('获取能量概览失败', e) }
+      }).catch(function(e) {})
     },
-    async loadTodayTasks() {
-      var childId = this.currentChildId
-      if (!childId) return
-      try {
-        var res = await getTodayTasks(childId)
+    loadDimensionActivities: function() {
+      var self = this
+      getActivityList({ dimensionCode: 'wisdom', page: 1, size: 5 }).then(function(res) {
         if (res && res.data) {
-          this.todayTasks = res.data.slice(0, 5)
+          self.dimensionActivities = res.data.records || res.data
         }
-      } catch (e) { console.log('获取今日任务失败', e) }
+      }).catch(function(e) {})
     },
-    sectionVisible(key) {
+    loadDimensionProducts: function() {
+      var self = this
+      getProductsByDomain('wisdom', 1, 4).then(function(res) {
+        if (res && res.data) {
+          self.dimensionProducts = res.data.records || res.data
+        }
+      }).catch(function(e) {})
+    },
+    sectionVisible: function(key) {
       return this.visibleSections.length === 0 || this.visibleSections.indexOf(key) !== -1
     },
-    onFuncClick(item) {
+    onFuncClick: function(item) {
       if (item.needLogin && !this.isLoggedIn) {
         uni.showModal({
           title: '提示',
@@ -173,10 +392,27 @@ export default {
         }
       }
     },
-    goMoreActivities() {},
-    goActivityDetail(id) {},
-    goMoreProducts() {},
-    goProductDetail(id) {
+    scrollToSection: function(section) {},
+    goMemberDetail: function(memberId) {
+      uni.navigateTo({ url: '/pages/wisdom/member-wisdom-detail?memberId=' + memberId })
+    },
+    goCognitiveReport: function() {
+      uni.navigateTo({ url: '/pages/wisdom/cognitive-report' })
+    },
+    goSelfQuiz: function() {
+      uni.navigateTo({ url: '/pages/wisdom/self-quiz' })
+    },
+    goTrainingHub: function() {
+      uni.navigateTo({ url: '/pages/wisdom/training-hub' })
+    },
+    goAssessment: function() {
+      uni.navigateTo({ url: '/pages/assessment/apply' })
+    },
+    onTaskClick: function(task) {},
+    goTasks: function() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
+    goActivityDetail: function(id) {},
+    goMoreActivities: function() {},
+    goProductDetail: function(id) {
       var token = uni.getStorageSync('token')
       if (token) {
         uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + id })
@@ -184,14 +420,13 @@ export default {
         uni.navigateTo({ url: '/pages/login/login?redirect=' + encodeURIComponent('/pages/wisdom/index') })
       }
     },
-    goTasks() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
-    goLogin() { uni.navigateTo({ url: '/pages/login/login' }) }
+    goMoreProducts: function() {}
   }
 }
 </script>
 
 <style scoped>
-.container {
+.wisdom-container {
   min-height: 100vh;
   background: #f5f7fa;
   padding-bottom: 120rpx;
@@ -202,6 +437,7 @@ export default {
   position: relative;
   z-index: 2;
 }
+
 .func-grid {
   background: #fff;
   border-radius: 24rpx;
@@ -209,15 +445,18 @@ export default {
   display: flex;
   box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
 }
+
 .func-item {
   flex: 1;
   display: flex;
   flex-direction: column;
   align-items: center;
 }
+
 .func-item:active {
   opacity: 0.7;
 }
+
 .func-icon-wrap {
   width: 88rpx;
   height: 88rpx;
@@ -228,137 +467,230 @@ export default {
   justify-content: center;
   margin-bottom: 10rpx;
 }
+
 .func-icon {
   font-size: 40rpx;
 }
+
 .func-label {
   font-size: 24rpx;
   color: #333;
   font-weight: 500;
 }
 
+/* ===== Section ===== */
 .section {
   margin: 20rpx 20rpx;
 }
+
 .section-header {
   display: flex;
   justify-content: space-between;
   align-items: center;
   margin-bottom: 20rpx;
 }
+
 .section-title {
   font-size: 30rpx;
   font-weight: bold;
   color: #333;
 }
+
 .section-more {
   font-size: 24rpx;
   color: #999;
 }
 
-.activity-scroll {
-  white-space: nowrap;
+/* ===== 认知概览 ===== */
+.cognition-summary {
+  background: linear-gradient(135deg, #FFD700, #FFA500);
+  border-radius: 20rpx;
+  padding: 30rpx;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.summary-main {
+  text-align: center;
+  margin-right: 30rpx;
+  padding-right: 30rpx;
+  border-right: 2rpx solid rgba(255,255,255,0.3);
 }
-.activity-card {
-  display: inline-block;
-  width: 280rpx;
-  margin-right: 20rpx;
+
+.summary-score {
+  font-size: 60rpx;
+  font-weight: bold;
+  color: #fff;
+  display: block;
+}
+
+.summary-label {
+  font-size: 22rpx;
+  color: rgba(255,255,255,0.85);
+  display: block;
+  margin-top: 4rpx;
+}
+
+.summary-detail {
+  flex: 1;
+}
+
+.summary-dan {
+  font-size: 28rpx;
+  color: #fff;
+  font-weight: bold;
+  display: block;
+  margin-bottom: 8rpx;
+}
+
+.summary-min {
+  font-size: 24rpx;
+  color: rgba(255,255,255,0.85);
+  display: block;
+}
+
+.cognition-empty {
   background: #fff;
   border-radius: 16rpx;
-  overflow: hidden;
+  padding: 40rpx;
+  text-align: center;
   box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
 }
-.activity-img {
-  width: 280rpx;
-  height: 180rpx;
-  background: #f0f0f0;
-}
-.activity-info {
-  padding: 16rpx;
-}
-.activity-name {
+
+.empty-tip {
   font-size: 26rpx;
-  font-weight: bold;
-  color: #333;
+  color: #999;
   display: block;
 }
-.activity-meta {
+
+.empty-sub {
   font-size: 22rpx;
-  color: #999;
-  margin-top: 8rpx;
+  color: #ccc;
+  margin-top: 6rpx;
   display: block;
 }
 
-.product-list {
+/* ===== 高亮卡片 ===== */
+.highlight-card {
+  margin: 20rpx;
+}
+
+.highlight-card-inner {
   display: flex;
-  flex-wrap: wrap;
-  gap: 20rpx;
+  flex-direction: row;
+  align-items: center;
+  background: linear-gradient(135deg, #FFF8E1, #fff);
+  border: 2rpx solid #FFD700;
+  border-radius: 20rpx;
+  padding: 24rpx;
 }
-.product-card {
-  width: calc(50% - 10rpx);
-  background: #fff;
-  border-radius: 16rpx;
-  overflow: hidden;
-  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+
+.highlight-card:active {
+  opacity: 0.8;
 }
-.product-img {
-  width: 100%;
-  height: 240rpx;
-  background: #f0f0f0;
+
+.highlight-card-left {
+  width: 72rpx;
+  height: 72rpx;
+  border-radius: 18rpx;
+  background: #FFD700;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 20rpx;
 }
-.product-name {
-  font-size: 26rpx;
-  color: #333;
-  padding: 12rpx 16rpx 0;
+
+.highlight-icon {
+  font-size: 36rpx;
+}
+
+.highlight-card-right {
+  flex: 1;
 }
-.product-price {
+
+.highlight-title {
   font-size: 28rpx;
-  color: #F97316;
   font-weight: bold;
-  padding: 8rpx 16rpx 16rpx;
+  color: #B8860B;
   display: block;
+  margin-bottom: 4rpx;
 }
-.line-clamp-2 {
-  overflow: hidden;
-  text-overflow: ellipsis;
-  display: -webkit-box;
-  -webkit-line-clamp: 2;
-  -webkit-box-orient: vertical;
+
+.highlight-desc {
+  font-size: 22rpx;
+  color: #999;
+  display: block;
 }
 
-.task-wrap {
-  background: #fff;
-  border-radius: 16rpx;
-  overflow: hidden;
-  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+.highlight-arrow {
+  font-size: 36rpx;
+  color: #ccc;
+  margin-left: 10rpx;
 }
-.task-item {
+
+/* ===== 小贴士 ===== */
+.tips-list {
   display: flex;
-  justify-content: space-between;
-  padding: 28rpx 24rpx;
-  border-bottom: 1rpx solid #f0f0f0;
+  flex-direction: column;
 }
-.task-item:last-child {
-  border-bottom: none;
+
+.tip-card {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  margin-bottom: 16rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+  border-left: 6rpx solid #FFD700;
 }
-.task-name {
+
+.tip-title {
   font-size: 26rpx;
+  font-weight: bold;
   color: #333;
+  display: block;
+  margin-bottom: 8rpx;
 }
-.task-points {
+
+.tip-summary {
   font-size: 24rpx;
-  color: #F97316;
+  color: #999;
+  line-height: 1.5;
+  display: block;
 }
-.task-empty {
+
+/* ===== 快捷操作 ===== */
+.quick-actions {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-around;
+  margin: 20rpx;
+}
+
+.action-btn {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
   background: #fff;
   border-radius: 16rpx;
-  padding: 60rpx;
-  text-align: center;
+  padding: 20rpx 30rpx;
   box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+  flex: 1;
+  margin: 0 8rpx;
 }
-.task-empty-text {
-  font-size: 26rpx;
-  color: #999;
+
+.action-btn:active {
+  opacity: 0.7;
+}
+
+.action-icon {
+  font-size: 36rpx;
+  margin-bottom: 8rpx;
+}
+
+.action-text {
+  font-size: 22rpx;
+  color: #333;
 }
 
 .bottom-spacer {

+ 267 - 0
cfc-frontend/pages/wisdom/member-wisdom-detail.vue

@@ -0,0 +1,267 @@
+<template>
+  <view class="detail-container">
+    <!-- 顶部导航 -->
+    <view class="nav-bar">
+      <view class="nav-back" @click="goBack">
+        <text class="nav-back-icon">&#x2190;</text>
+      </view>
+      <text class="nav-title">智 · 成员详情</text>
+    </view>
+
+    <!-- 五维能量条 -->
+    <FamilyEnergyBar
+      dimensionCode="wisdom"
+      :sandboxData="sandboxData"
+      :dualDimension="dualDimension" />
+
+    <!-- 成员信息卡 -->
+    <view class="member-card" v-if="memberInfo">
+      <view class="member-avatar wisdom-avatar">
+        <text class="avatar-text">{{ memberInfo.name && memberInfo.name.charAt(0) || '孩' }}</text>
+      </view>
+      <view class="member-info">
+        <text class="member-name">{{ memberInfo.name || '孩子' }}</text>
+        <text class="member-role">智慧维度</text>
+      </view>
+      <view class="member-score" v-if="dualDimension">
+        <text class="score-value wisdom-score">{{ dualDimension.energy || 0 }}</text>
+        <text class="score-label">能量值</text>
+      </view>
+    </view>
+
+    <!-- 家庭成员关系图谱(只读) -->
+    <FamilyRelationGraph
+      dimensionCode="wisdom"
+      :selfId="selfId"
+      :members="graphMembers"
+      :energyMap="energyMapForGraph"
+      :intimacyMap="intimacyMapForGraph"
+      :interactive="false" />
+
+    <!-- 今日任务 -->
+    <DimensionTasks
+      dimensionCode="wisdom"
+      :tasks="dimensionTasks"
+      @taskClick="onTaskClick"
+      @moreTasks="goTasks" />
+
+    <!-- 活动 -->
+    <DimensionActivities
+      dimensionCode="wisdom"
+      :activities="dimensionActivities"
+      @activityClick="goActivityDetail"
+      @moreActivities="goMoreActivities" />
+
+    <!-- 商品 -->
+    <DimensionProducts
+      dimensionCode="wisdom"
+      :products="dimensionProducts"
+      @productClick="goProductDetail"
+      @moreProducts="goMoreProducts" />
+
+    <!-- 底部占位 -->
+    <view class="bottom-spacer"></view>
+  </view>
+</template>
+
+<script>
+import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
+import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
+import DimensionTasks from '../../components/DimensionTasks.vue'
+import DimensionActivities from '../../components/DimensionActivities.vue'
+import DimensionProducts from '../../components/DimensionProducts.vue'
+import { getEnergyOverview, getFamilyEnergySandbox, getTodayTasksByCategory, getActivityList, getProductsByDomain, getChildren } from '../../utils/api.js'
+
+export default {
+  components: { FamilyEnergyBar, FamilyRelationGraph, DimensionTasks, DimensionActivities, DimensionProducts },
+  data() {
+    return {
+      childId: null,
+      selfId: null,
+      memberInfo: null,
+      graphMembers: [],
+      energyMapForGraph: {},
+      intimacyMapForGraph: {},
+      dimensionTasks: [],
+      dimensionActivities: [],
+      dimensionProducts: [],
+      dualDimension: null,
+      sandboxData: null
+    }
+  },
+  onLoad: function(options) {
+    if (options && options.memberId) {
+      this.selfId = options.memberId
+      this.childId = options.memberId
+      this.loadMemberData()
+    }
+  },
+  methods: {
+    goBack: function() {
+      uni.navigateBack()
+    },
+    loadMemberData: function() {
+      var self = this
+      var childId = this.childId
+      if (!childId) return
+
+      getEnergyOverview(childId).then(function(res) {
+        if (res && res.data) {
+          self.dualDimension = res.data
+        }
+      }).catch(function() {})
+
+      getFamilyEnergySandbox(childId).then(function(res) {
+        if (res && res.data) {
+          self.sandboxData = res.data.sandboxData || null
+          self.graphMembers = res.data.members || []
+          self.energyMapForGraph = res.data.energyMap || {}
+          self.intimacyMapForGraph = res.data.intimacyMap || {}
+        }
+      }).catch(function() {})
+
+      getTodayTasksByCategory({ childId: childId, dimension: 'wisdom' }).then(function(res) {
+        if (res && res.data) {
+          self.dimensionTasks = res.data.slice(0, 5)
+        }
+      }).catch(function() {})
+
+      getActivityList({ dimension: 'wisdom', page: 1, size: 5 }).then(function(res) {
+        if (res && res.data) {
+          self.dimensionActivities = res.data.records || res.data
+        }
+      }).catch(function() {})
+
+      getProductsByDomain({ domain: 'wisdom', page: 1, size: 4 }).then(function(res) {
+        if (res && res.data) {
+          self.dimensionProducts = res.data.records || res.data
+        }
+      }).catch(function() {})
+    },
+    onTaskClick: function(task) {},
+    goTasks: function() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
+    goActivityDetail: function(id) {},
+    goMoreActivities: function() {},
+    goProductDetail: function(id) {
+      var token = uni.getStorageSync('token')
+      if (token) {
+        uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + id })
+      } else {
+        uni.navigateTo({ url: '/pages/login/login' })
+      }
+    },
+    goMoreProducts: function() {}
+  }
+}
+</script>
+
+<style scoped>
+.detail-container {
+  min-height: 100vh;
+  background: #f5f7fa;
+  padding-bottom: 120rpx;
+}
+
+.nav-bar {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 80rpx 30rpx 20rpx;
+  background: linear-gradient(135deg, #FFD700, #FFA500);
+}
+
+.nav-back {
+  width: 60rpx;
+  height: 60rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.nav-back-icon {
+  font-size: 36rpx;
+  color: #fff;
+}
+
+.nav-title {
+  flex: 1;
+  text-align: center;
+  font-size: 34rpx;
+  font-weight: bold;
+  color: #fff;
+  margin-right: 60rpx;
+}
+
+.member-card {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  background: #fff;
+  margin: 20rpx 30rpx;
+  padding: 30rpx;
+  border-radius: 20rpx;
+  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
+}
+
+.member-avatar {
+  width: 100rpx;
+  height: 100rpx;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 24rpx;
+}
+
+.wisdom-avatar {
+  background: linear-gradient(135deg, #FFD700, #FFA500);
+}
+
+.avatar-text {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #fff;
+}
+
+.member-info {
+  flex: 1;
+}
+
+.member-name {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+}
+
+.member-role {
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 6rpx;
+  display: block;
+}
+
+.member-score {
+  text-align: center;
+}
+
+.score-value {
+  font-size: 40rpx;
+  font-weight: bold;
+  display: block;
+}
+
+.wisdom-score {
+  color: #B8860B;
+}
+
+.score-label {
+  font-size: 22rpx;
+  color: #999;
+  display: block;
+}
+
+.bottom-spacer {
+  height: 40rpx;
+}
+</style>

+ 849 - 0
cfc-frontend/pages/wisdom/self-quiz.vue

@@ -0,0 +1,849 @@
+<template>
+  <view class="container">
+    <!-- 顶部导航 -->
+    <view class="nav-bar">
+      <view class="nav-back" @click="goBack">
+        <text class="nav-back-icon">&#x2190;</text>
+      </view>
+      <text class="nav-title">自己出题自己答</text>
+      <view class="nav-placeholder"></view>
+    </view>
+
+    <!-- 步骤进度 -->
+    <view class="step-progress">
+      <view class="step-item" v-for="(s, i) in steps" :key="i"
+        :class="{ 'step-active': currentStep === i + 1, 'step-done': currentStep > i + 1 }">
+        <view class="step-circle">
+          <text class="step-num">{{ currentStep > i + 1 ? '✓' : (i + 1) }}</text>
+        </view>
+        <text class="step-label">{{ s }}</text>
+      </view>
+    </view>
+
+    <!-- ===== Step 1: 设定目标 ===== -->
+    <view class="step-content" v-if="currentStep === 1">
+      <view class="content-card">
+        <text class="content-title">设定挑战目标</text>
+        <text class="content-desc">选择你要连续答对多少题才能通关</text>
+
+        <view class="target-selector">
+          <view class="target-label">
+            <text class="target-label-text">连续答对目标</text>
+          </view>
+          <view class="target-control">
+            <view class="target-btn" @click="adjustTarget(-1)">−</view>
+            <view class="target-value-wrap">
+              <text class="target-value">{{ targetN }}</text>
+              <text class="target-unit">题</text>
+            </view>
+            <view class="target-btn" @click="adjustTarget(1)">+</view>
+          </view>
+        </view>
+
+        <view class="target-range">
+          <text class="range-text">范围: 2 - 15 题</text>
+        </view>
+
+        <view class="start-btn-wrap">
+          <view class="start-btn" @click="startQuiz">
+            <text class="start-btn-text">确认开始</text>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- ===== Step 2: 选择题型 ===== -->
+    <view class="step-content" v-if="currentStep === 2">
+      <view class="content-card">
+        <text class="content-title">选择题型</text>
+        <text class="content-desc">选择一个认知维度开始出题</text>
+
+        <view class="type-grid">
+          <view class="type-card" v-for="t in questionTypes" :key="t.key"
+            :class="{ 'type-recommended': t.recommended }"
+            @click="selectType(t)">
+            <text class="type-icon">{{ t.icon }}</text>
+            <text class="type-name">{{ t.label }}</text>
+            <text class="type-desc">{{ t.desc }}</text>
+            <view class="type-recommend-badge" v-if="t.recommended">
+              <text class="badge-text">推荐</text>
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- ===== Step 3: 出题 + 答题 ===== -->
+    <view class="step-content" v-if="currentStep === 3">
+      <view class="content-card">
+        <view class="step3-header">
+          <text class="selected-type-label">{{ selectedType && selectedType.label }}</text>
+          <text class="step3-progress">连续答对 {{ correctCount }}/{{ targetN }}</text>
+        </view>
+
+        <view class="question-section">
+          <text class="question-label">输入你的题目</text>
+          <textarea class="question-input" v-model="currentQuestion" placeholder="在这里写下你的题目..." maxlength="200"></textarea>
+          <text class="char-count">{{ currentQuestion.length }}/200</text>
+        </view>
+
+        <view class="answer-section">
+          <text class="answer-label">输入正确答案</text>
+          <input class="answer-input" v-model="currentAnswer" placeholder="在这里写下正确答案..." maxlength="50" />
+          <text class="char-count">{{ currentAnswer.length }}/50</text>
+        </view>
+
+        <view class="submit-btn-wrap">
+          <view class="submit-btn" @click="submitAnswer" v-if="currentQuestion.trim() && currentAnswer.trim()">
+            <text class="submit-btn-text">提交答案</text>
+          </view>
+          <view class="submit-btn disabled" v-else>
+            <text class="submit-btn-text">请填写题目和答案</text>
+          </view>
+        </view>
+
+        <!-- 答题结果反馈 -->
+        <view class="feedback-wrap" v-if="showFeedback">
+          <view class="feedback-card" :class="lastCorrect ? 'feedback-correct' : 'feedback-wrong'">
+            <text class="feedback-icon">{{ lastCorrect ? '🎉' : '💪' }}</text>
+            <view class="feedback-info">
+              <text class="feedback-title">{{ lastCorrect ? '回答正确!' : '加油,再试试!' }}</text>
+              <text class="feedback-subtitle">{{ lastCorrect ? ('连续答对 ' + correctCount + '/' + targetN) : '连续计数已重置,重新开始' }}</text>
+            </view>
+          </view>
+          <view class="next-btn-wrap" v-if="showFeedback">
+            <view class="next-btn" @click="continueAfterFeedback">
+              <text class="next-btn-text">{{ lastCorrect && correctCount >= targetN ? '查看结果' : '继续出题' }}</text>
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- ===== Step 4: 达标通过 ===== -->
+    <view class="step-content" v-if="currentStep === 4">
+      <view class="celebration-card">
+        <text class="celebration-icon">🏆</text>
+        <text class="celebration-title">恭喜通关!</text>
+        <text class="celebration-subtitle">你成功完成了挑战</text>
+
+        <view class="stats-grid">
+          <view class="stat-item">
+            <text class="stat-value">{{ sessionStats.totalTime }}</text>
+            <text class="stat-label">用时</text>
+          </view>
+          <view class="stat-item">
+            <text class="stat-value">{{ sessionStats.totalQuestions }}</text>
+            <text class="stat-label">总答题数</text>
+          </view>
+          <view class="stat-item">
+            <text class="stat-value">{{ sessionStats.accuracy }}%</text>
+            <text class="stat-label">正确率</text>
+          </view>
+        </view>
+
+        <view class="celebration-actions">
+          <view class="action-btn primary" @click="playAgain">
+            <text class="action-btn-text">再来一轮</text>
+          </view>
+          <view class="action-btn secondary" @click="goBack">
+            <text class="action-btn-text secondary-text">返回智维度</text>
+          </view>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+var STORAGE_KEY = 'selfQuizSession'
+
+var DAN_TYPES = [
+  { key: 'perception', label: '感知能力', icon: '\u{1F441}', desc: '感官信息处理', recommended: false },
+  { key: 'focus', label: '专注力', icon: '\u{1F3AF}', desc: '持续集中注意力', recommended: false },
+  { key: 'memory', label: '记忆力', icon: '\u{1F9E0}', desc: '信息编码和回忆', recommended: false },
+  { key: 'logic', label: '逻辑思维', icon: '\u{1F9E9}', desc: '推理和问题解决', recommended: false },
+  { key: 'spatial', label: '空间思维', icon: '\u{1F9CA}', desc: '空间想象和判断', recommended: false },
+  { key: 'processing', label: '加工速度', icon: '\u26A1', desc: '信息处理速度', recommended: false }
+]
+
+export default {
+  data: function() {
+    return {
+      steps: ['设定目标', '选择题型', '出题答题', '达标通过'],
+      currentStep: 1,
+      targetN: 5,
+      questionTypes: DAN_TYPES,
+      selectedType: null,
+      currentQuestion: '',
+      currentAnswer: '',
+      correctCount: 0,
+      totalAttempts: 0,
+      showFeedback: false,
+      lastCorrect: false,
+      sessionStats: {
+        totalTime: '0秒',
+        totalQuestions: 0,
+        accuracy: 0
+      },
+      sessionStartTime: null
+    }
+  },
+  onLoad: function() {
+    this.restoreSession()
+  },
+  onUnload: function() {
+    this.saveSession()
+  },
+  methods: {
+    goBack: function() {
+      uni.navigateBack()
+    },
+    adjustTarget: function(delta) {
+      var newVal = this.targetN + delta
+      if (newVal >= 2 && newVal <= 15) {
+        this.targetN = newVal
+      }
+    },
+    startQuiz: function() {
+      this.currentStep = 2
+      this.sessionStartTime = Date.now()
+      this.saveSession()
+    },
+    selectType: function(type) {
+      this.selectedType = type
+      this.currentStep = 3
+      this.currentQuestion = ''
+      this.currentAnswer = ''
+      this.showFeedback = false
+      this.saveSession()
+    },
+    submitAnswer: function() {
+      this.totalAttempts++
+      var isCorrect = true
+
+      if (isCorrect) {
+        this.correctCount++
+        this.lastCorrect = true
+      } else {
+        this.correctCount = 0
+        this.lastCorrect = false
+      }
+
+      this.showFeedback = true
+      this.saveSession()
+
+      if (this.correctCount >= this.targetN) {
+        // Step transition handled in continueAfterFeedback
+      }
+    },
+    continueAfterFeedback: function() {
+      if (this.correctCount >= this.targetN) {
+        var elapsed = Math.floor((Date.now() - this.sessionStartTime) / 1000)
+        var minutes = Math.floor(elapsed / 60)
+        var seconds = elapsed % 60
+        this.sessionStats.totalTime = (minutes > 0 ? minutes + '分' : '') + seconds + '秒'
+        this.sessionStats.totalQuestions = this.totalAttempts
+        this.sessionStats.accuracy = Math.round((this.correctCount / this.totalAttempts) * 100)
+        this.currentStep = 4
+        this.clearSession()
+      } else {
+        this.currentQuestion = ''
+        this.currentAnswer = ''
+        this.showFeedback = false
+      }
+    },
+    playAgain: function() {
+      this.currentStep = 1
+      this.correctCount = 0
+      this.totalAttempts = 0
+      this.showFeedback = false
+      this.currentQuestion = ''
+      this.currentAnswer = ''
+      this.selectedType = null
+      this.sessionStartTime = null
+      this.clearSession()
+    },
+    saveSession: function() {
+      var session = {
+        step: this.currentStep,
+        targetN: this.targetN,
+        selectedType: this.selectedType,
+        correctCount: this.correctCount,
+        totalAttempts: this.totalAttempts,
+        currentQuestion: this.currentQuestion,
+        currentAnswer: this.currentAnswer,
+        sessionStartTime: this.sessionStartTime
+      }
+      try {
+        uni.setStorageSync(STORAGE_KEY, session)
+      } catch (e) {}
+    },
+    restoreSession: function() {
+      try {
+        var session = uni.getStorageSync(STORAGE_KEY)
+        if (session) {
+          this.currentStep = session.step || 1
+          this.targetN = session.targetN || 5
+          this.selectedType = session.selectedType || null
+          this.correctCount = session.correctCount || 0
+          this.totalAttempts = session.totalAttempts || 0
+          this.currentQuestion = session.currentQuestion || ''
+          this.currentAnswer = session.currentAnswer || ''
+          this.sessionStartTime = session.sessionStartTime || null
+        }
+      } catch (e) {}
+    },
+    clearSession: function() {
+      try {
+        uni.removeStorageSync(STORAGE_KEY)
+      } catch (e) {}
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background: linear-gradient(180deg, #FFF8E1 0%, #f5f7fa 100%);
+  padding-bottom: 100rpx;
+}
+
+.nav-bar {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 80rpx 30rpx 20rpx;
+  background: linear-gradient(135deg, #FFD700, #FFA500);
+}
+
+.nav-back {
+  width: 60rpx;
+  height: 60rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.nav-back-icon {
+  font-size: 36rpx;
+  color: #fff;
+}
+
+.nav-title {
+  flex: 1;
+  text-align: center;
+  font-size: 34rpx;
+  font-weight: bold;
+  color: #fff;
+}
+
+.nav-placeholder {
+  width: 60rpx;
+}
+
+/* ===== 步骤进度 ===== */
+.step-progress {
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  padding: 30rpx 40rpx;
+  background: #fff;
+}
+
+.step-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  flex: 1;
+  position: relative;
+}
+
+.step-item + .step-item::before {
+  content: '';
+  position: absolute;
+  top: 18rpx;
+  left: -50%;
+  width: 100%;
+  height: 4rpx;
+  background: #e0e0e0;
+  z-index: 0;
+}
+
+.step-done + .step-item::before,
+.step-item.step-active + .step-item::before {
+  background: #e0e0e0;
+}
+
+.step-item.step-done + .step-item::before {
+  background: #FFD700;
+}
+
+.step-circle {
+  width: 36rpx;
+  height: 36rpx;
+  border-radius: 50%;
+  background: #e0e0e0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: relative;
+  z-index: 1;
+}
+
+.step-active .step-circle {
+  background: #FFD700;
+}
+
+.step-done .step-circle {
+  background: #52c41a;
+}
+
+.step-num {
+  font-size: 20rpx;
+  color: #fff;
+  font-weight: bold;
+}
+
+.step-label {
+  font-size: 22rpx;
+  color: #999;
+  margin-top: 10rpx;
+}
+
+.step-active .step-label {
+  color: #B8860B;
+  font-weight: bold;
+}
+
+.step-done .step-label {
+  color: #52c41a;
+}
+
+/* ===== 步骤内容 ===== */
+.step-content {
+  padding: 30rpx;
+}
+
+.content-card {
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 40rpx;
+  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
+}
+
+.content-title {
+  font-size: 34rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  text-align: center;
+  margin-bottom: 10rpx;
+}
+
+.content-desc {
+  font-size: 26rpx;
+  color: #999;
+  display: block;
+  text-align: center;
+  margin-bottom: 40rpx;
+}
+
+/* ===== Step1: 目标选择 ===== */
+.target-selector {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 20rpx;
+}
+
+.target-label {
+  margin-right: 30rpx;
+}
+
+.target-label-text {
+  font-size: 28rpx;
+  color: #666;
+}
+
+.target-control {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.target-btn {
+  width: 60rpx;
+  height: 60rpx;
+  border-radius: 50%;
+  background: #FFF8E1;
+  border: 2rpx solid #FFD700;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 36rpx;
+  color: #B8860B;
+  font-weight: bold;
+}
+
+.target-btn:active {
+  opacity: 0.7;
+}
+
+.target-value-wrap {
+  margin: 0 30rpx;
+  text-align: center;
+}
+
+.target-value {
+  font-size: 72rpx;
+  font-weight: bold;
+  color: #B8860B;
+  display: block;
+}
+
+.target-unit {
+  font-size: 28rpx;
+  color: #999;
+}
+
+.target-range {
+  text-align: center;
+  margin-bottom: 40rpx;
+}
+
+.range-text {
+  font-size: 24rpx;
+  color: #ccc;
+}
+
+.start-btn-wrap {
+  text-align: center;
+}
+
+.start-btn {
+  display: inline-block;
+  background: linear-gradient(135deg, #FFD700, #FFA500);
+  padding: 20rpx 80rpx;
+  border-radius: 44rpx;
+  box-shadow: 0 4rpx 20rpx rgba(255, 215, 0, 0.4);
+}
+
+.start-btn:active {
+  opacity: 0.8;
+}
+
+.start-btn-text {
+  font-size: 30rpx;
+  color: #fff;
+  font-weight: bold;
+}
+
+/* ===== Step2: 题型选择 ===== */
+.type-grid {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  justify-content: space-between;
+}
+
+.type-card {
+  width: calc(50% - 10rpx);
+  padding: 24rpx;
+  margin-bottom: 20rpx;
+  background: #f9f9f9;
+  border-radius: 16rpx;
+  border: 2rpx solid transparent;
+  position: relative;
+  text-align: center;
+}
+
+.type-card:active {
+  border-color: #FFD700;
+}
+
+.type-recommended {
+  border-color: #FFD700;
+  background: #FFF8E1;
+}
+
+.type-icon {
+  font-size: 40rpx;
+  display: block;
+  margin-bottom: 8rpx;
+}
+
+.type-name {
+  font-size: 26rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+}
+
+.type-desc {
+  font-size: 22rpx;
+  color: #999;
+  display: block;
+  margin-top: 4rpx;
+}
+
+.type-recommend-badge {
+  position: absolute;
+  top: -8rpx;
+  right: -8rpx;
+  background: #FF6B35;
+  border-radius: 12rpx;
+  padding: 2rpx 12rpx;
+}
+
+.badge-text {
+  font-size: 20rpx;
+  color: #fff;
+  font-weight: bold;
+}
+
+/* ===== Step3: 出题 ===== */
+.step3-header {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 30rpx;
+}
+
+.selected-type-label {
+  font-size: 28rpx;
+  color: #B8860B;
+  font-weight: bold;
+}
+
+.step3-progress {
+  font-size: 26rpx;
+  color: #666;
+}
+
+.question-section, .answer-section {
+  margin-bottom: 30rpx;
+}
+
+.question-label, .answer-label {
+  font-size: 28rpx;
+  color: #333;
+  font-weight: 500;
+  display: block;
+  margin-bottom: 12rpx;
+}
+
+.question-input {
+  width: 100%;
+  min-height: 160rpx;
+  background: #f5f7fa;
+  border-radius: 12rpx;
+  padding: 20rpx;
+  font-size: 26rpx;
+  color: #333;
+  box-sizing: border-box;
+}
+
+.answer-input {
+  width: 100%;
+  height: 72rpx;
+  background: #f5f7fa;
+  border-radius: 12rpx;
+  padding: 0 20rpx;
+  font-size: 26rpx;
+  color: #333;
+  box-sizing: border-box;
+}
+
+.char-count {
+  font-size: 22rpx;
+  color: #ccc;
+  text-align: right;
+  display: block;
+  margin-top: 6rpx;
+}
+
+.submit-btn-wrap {
+  text-align: center;
+  margin-top: 20rpx;
+}
+
+.submit-btn {
+  display: inline-block;
+  background: linear-gradient(135deg, #FFD700, #FFA500);
+  padding: 20rpx 80rpx;
+  border-radius: 44rpx;
+  box-shadow: 0 4rpx 20rpx rgba(255, 215, 0, 0.4);
+}
+
+.submit-btn.disabled {
+  opacity: 0.5;
+}
+
+.submit-btn-text {
+  font-size: 30rpx;
+  color: #fff;
+  font-weight: bold;
+}
+
+/* ===== 反馈 ===== */
+.feedback-wrap {
+  margin-top: 30rpx;
+}
+
+.feedback-card {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 24rpx;
+  border-radius: 16rpx;
+  margin-bottom: 20rpx;
+}
+
+.feedback-correct {
+  background: #f0fff4;
+  border: 2rpx solid #52c41a;
+}
+
+.feedback-wrong {
+  background: #fff5f5;
+  border: 2rpx solid #ff4d4f;
+}
+
+.feedback-icon {
+  font-size: 48rpx;
+  margin-right: 20rpx;
+}
+
+.feedback-info {
+  flex: 1;
+}
+
+.feedback-title {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+}
+
+.feedback-subtitle {
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 4rpx;
+  display: block;
+}
+
+.next-btn-wrap {
+  text-align: center;
+}
+
+.next-btn {
+  display: inline-block;
+  background: linear-gradient(135deg, #FFD700, #FFA500);
+  padding: 16rpx 60rpx;
+  border-radius: 40rpx;
+}
+
+.next-btn:active {
+  opacity: 0.8;
+}
+
+.next-btn-text {
+  font-size: 28rpx;
+  color: #fff;
+  font-weight: bold;
+}
+
+/* ===== Step4: 庆祝 ===== */
+.celebration-card {
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 60rpx 40rpx;
+  text-align: center;
+  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
+}
+
+.celebration-icon {
+  font-size: 120rpx;
+  display: block;
+  margin-bottom: 20rpx;
+}
+
+.celebration-title {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #B8860B;
+  display: block;
+  margin-bottom: 8rpx;
+}
+
+.celebration-subtitle {
+  font-size: 28rpx;
+  color: #999;
+  display: block;
+  margin-bottom: 40rpx;
+}
+
+.stats-grid {
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  margin-bottom: 40rpx;
+}
+
+.stat-item {
+  text-align: center;
+  margin: 0 30rpx;
+}
+
+.stat-value {
+  font-size: 48rpx;
+  font-weight: bold;
+  color: #B8860B;
+  display: block;
+}
+
+.stat-label {
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 6rpx;
+  display: block;
+}
+
+.celebration-actions {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.action-btn {
+  width: 60%;
+  padding: 20rpx 0;
+  border-radius: 44rpx;
+  margin-bottom: 20rpx;
+  text-align: center;
+}
+
+.action-btn.primary {
+  background: linear-gradient(135deg, #FFD700, #FFA500);
+  box-shadow: 0 4rpx 20rpx rgba(255, 215, 0, 0.4);
+}
+
+.action-btn.secondary {
+  background: #fff;
+  border: 2rpx solid #ddd;
+}
+
+.action-btn-text {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #fff;
+}
+
+.secondary-text {
+  color: #666;
+}
+</style>

+ 178 - 0
cfc-frontend/pages/wisdom/training-hub.vue

@@ -0,0 +1,178 @@
+<template>
+  <view class="container">
+    <view class="header-bar">
+      <text class="back-btn" @click="goBack">← 返回</text>
+      <text class="header-title">认知训练中心</text>
+      <text class="header-placeholder"></text>
+    </view>
+
+    <view class="desc-section">
+      <text class="desc-text">选择训练游戏,提升你的认知能力</text>
+    </view>
+
+    <view class="game-card" @click="goGame('schulte')">
+      <view class="game-card-left">
+        <text class="game-icon">🎯</text>
+      </view>
+      <view class="game-card-center">
+        <text class="game-name">舒尔特方格</text>
+        <text class="game-desc">提升专注力和反应速度</text>
+      </view>
+      <text class="game-arrow">›</text>
+    </view>
+
+    <view class="game-card" @click="goGame('1a2b')">
+      <view class="game-card-left">
+        <text class="game-icon">🔢</text>
+      </view>
+      <view class="game-card-center">
+        <text class="game-name">猜数字</text>
+        <text class="game-desc">锻炼逻辑思维和推理能力</text>
+      </view>
+      <text class="game-arrow">›</text>
+    </view>
+
+    <view class="note-section">
+      <text class="note-text">更多训练游戏正在开发中,敬请期待</text>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  data: function() {
+    return {
+      childId: null
+    }
+  },
+  onLoad: function(options) {
+    if (options && options.childId) {
+      this.childId = options.childId
+    }
+  },
+  methods: {
+    goBack: function() {
+      uni.navigateBack()
+    },
+    goGame: function(game) {
+      var url = ''
+      if (game === 'schulte') {
+        url = '/pages/games/schulte'
+      } else if (game === '1a2b') {
+        url = '/pages/games/1a2b'
+      }
+      if (url) {
+        uni.navigateTo({ url: url })
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background: linear-gradient(180deg, #FFF8E1 0%, #f5f7fa 100%);
+  padding-bottom: 60rpx;
+}
+
+.header-bar {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  padding: 80rpx 30rpx 30rpx;
+}
+
+.back-btn {
+  font-size: 30rpx;
+  color: #B8860B;
+  padding: 10rpx;
+}
+
+.header-title {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #B8860B;
+}
+
+.header-placeholder {
+  width: 60rpx;
+}
+
+.desc-section {
+  padding: 0 30rpx 30rpx;
+}
+
+.desc-text {
+  font-size: 26rpx;
+  color: #999;
+  text-align: center;
+  display: block;
+}
+
+.game-card {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  background: #fff;
+  margin: 0 30rpx 20rpx;
+  padding: 30rpx;
+  border-radius: 20rpx;
+  box-shadow: 0 4rpx 20rpx rgba(255, 215, 0, 0.12);
+  border-left: 8rpx solid #FFD700;
+}
+
+.game-card:active {
+  opacity: 0.8;
+}
+
+.game-card-left {
+  width: 80rpx;
+  height: 80rpx;
+  border-radius: 20rpx;
+  background: #FFF8E1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 20rpx;
+}
+
+.game-icon {
+  font-size: 40rpx;
+}
+
+.game-card-center {
+  flex: 1;
+}
+
+.game-name {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 6rpx;
+}
+
+.game-desc {
+  font-size: 24rpx;
+  color: #999;
+  display: block;
+}
+
+.game-arrow {
+  font-size: 40rpx;
+  color: #ccc;
+  margin-left: 10rpx;
+}
+
+.note-section {
+  padding: 40rpx 30rpx;
+  text-align: center;
+}
+
+.note-text {
+  font-size: 24rpx;
+  color: #ccc;
+}
+</style>

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

@@ -1117,6 +1117,11 @@ export const getReferralCode = () => {
   return request('/api/invite/code', 'POST')
 }
 
+// 获取邀请二维码
+export const getInviteQrCode = () => {
+  return request('/api/invite/qrcode', 'POST')
+}
+
 // 绑定推荐人
 export const bindReferral = (referralCode) => {
   return request('/api/invite/bind', 'POST', { referralCode })
@@ -1237,6 +1242,11 @@ export const getCommissionTeam = (page = 1, size = 10) => {
   return request('/api/commission/team', 'POST', { page, size })
 }
 
+// 推广团队成员列表(分页)
+export const getTeamList = (page = 1, size = 20, level = 'ALL') => {
+  return request('/api/commission/team/list', 'POST', { page, size, level })
+}
+
 // 佣金统计数据
 export const getCommissionStats = () => {
   return request('/api/commission/stats', 'POST')

+ 1466 - 0
docs/superpowers/plans/2026-06-26-health-dimensions-refactor.md

@@ -0,0 +1,1466 @@
+# 身体健康7维模型 — 重构对齐计划(Phase 1)
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 将现有7维健康实现(body/fitness/cardio/blood/sleep/psyche/diet)重构对齐到设计文档规范的7维模型(growth/sleep/vision/immunity/nutrition/gut/exercise),包括数据库表结构、评分引擎、常模百分位系统、菌群报告映射。
+
+**核心架构变更:**
+- 表 `health_dimension_score` → `health_dimension_scores`,增加 percentile/tier/raw_data/expire_date 字段
+- 表 `health_norm_reference` 改为百分位结构(percentile_5~95),按性别+年龄月查询
+- 表 `health_data_source_record` 增加 sourceType/dimensionCodes/parsedResult/confidence
+- `DimensionScoreServiceImpl` 核心聚合器:替换7维列表 + 实现百分位计算 + 数据源Tier优先级 + 菌群映射集成
+
+**Tech Stack:** Java 8, Spring Boot 2.7.18, MyBatis-Plus, MySQL 8.0, uni-app Vue 2
+
+---
+
+## Context
+
+### 设计文档中定义的7维
+```java
+// 枚举值(维度code → 中文名)
+growth    → "生长发育"
+sleep     → "睡眠质量"
+vision    → "视力健康"
+immunity  → "免疫力"
+nutrition → "营养均衡"
+gut       → "肠胃健康"
+exercise  → "运动活力"
+```
+
+### 设计文档中的Phase 1范围
+1. 核心框架:`health_dimension_scores` / `health_data_sources` / `health_norm_reference` 3张表
+2. `DimensionScoreService` 聚合逻辑(Tier 3 问卷兜底 + Tier 1 报告覆盖)
+3. `HealthDimensionController` — 获取7维 / 手动输入问卷
+4. 前端:7维雷达图 + 每个维度简单问卷
+5. **已有菌群报告数据直接映射**(PdfParseService 解析结果 → 维度评分)
+6. 部署常模种子数据(BMI/睡眠/体测等国家标准)
+
+### 数据源优先级
+- Tier 1 (REPORT): 菌群报告/体检报告 → 覆盖低层级
+- Tier 2 (VOICE/PHOTO): 语音解析/拍照 → 置信度<0.6降级
+- Tier 3 (MANUAL): 问卷/手动 → 兜底
+
+### 常模百分位颜色
+- ≥85 → 优秀 (深绿)
+- 60-84 → 良好 (浅绿)
+- 40-59 → 一般 (黄)
+- <40 → 关注 (橙/红)
+
+### 菌群报告 → 7维映射矩阵(关键)
+
+| 菌群指标 | 生长 | 睡眠 | 视力 | 免疫 | 营养 | 肠胃 | 运动 |
+|---------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| gutHealthScore | | | | ✅ | | ✅ | |
+| balanceScore | | | | | | ✅ | |
+| diversityScore | | | | ✅ | | ✅ | |
+| nutritionScore | | | | | ✅ | | |
+| chronicDiseaseScore | ✅ | | | | ✅ | | |
+| beneficialBacteriaScore | | | | ✅ | | ✅ | |
+| harmfulBacteriaScore | | | | ✅ | | ✅ | |
+| coreGenusScore | | | | | | ✅ | |
+| gutAge | | ✅ | | | | ✅ | |
+| 神经递质(indicators) | | ✅ | | | | | |
+| 肠道屏障(indicators) | | | | ✅ | | ✅ | |
+| 短链脂肪酸(indicators) | | | | | ✅ | ✅ | |
+| 疾病风险(diseaseRisks) | ✅ | | | ✅ | | | |
+| 菌种丰度(populationLevel) | ✅ | | | ✅ | ✅ | ✅ | |
+| 微量指标(维生素/矿物质) | | | | | ✅ | | |
+
+### 已存在的文件(重构对象)
+```
+后端:
+  entity/    HealthDimensionScore.java   HealthNormReference.java   HealthDataSourceRecord.java
+  mapper/    HealthDimensionScoreMapper.java   HealthNormReferenceMapper.java   HealthDataSourceRecordMapper.java
+  service/   HealthDimensionScoreService.java + impl/
+             DimensionScoreService.java + impl/DimensionScoreServiceImpl.java
+             HealthDataSourceRecordService.java (interface)
+             PdfParseService.java  ← 菌群报告解析器(已有)
+  controller/HealthDimensionController.java
+  dto/       HealthDimensionVO.java   DimensionUploadVO.java   HealthDimensionQuestionnaireVO.java   DimensionScoreVO.java
+  config/    DatabaseInitializer.java
+
+前端:
+  pages/body/health-dimensions.vue
+  pages.json (路由已注册,无需修改)
+```
+
+---
+
+## File Structure
+
+### Modified Backend Files
+| # | File | Change |
+|:-:|------|--------|
+| 1 | `entity/HealthDimensionScore.java` | 表名→`health_dimension_scores`,增 `percentile`/`tier`/`rawData`/`assessDate`/`expireDate`/`updatedAt` |
+| 2 | `entity/HealthNormReference.java` | 改为百分位字段 `percentile5-95` + `gender`/`ageMin`/`ageMax`(月) |
+| 3 | `entity/HealthDataSourceRecord.java` | 改为 `sourceType`/`dimensionCodes`/`parsedResult`/`fileUrl`/`confidence` |
+| 4 | `mapper/HealthDimensionScoreMapper.java` | SQL适配新表名和字段,新增百分位查询 |
+| 5 | `mapper/HealthNormReferenceMapper.java` | 新查询方法 `selectPercentile(dimension, gender, ageMonths, score)` |
+| 6 | `mapper/HealthDataSourceRecordMapper.java` | SQL适配新表结构 |
+| 7 | `service/HealthDimensionScoreService.java` | 接口更新 |
+| 8 | `service/impl/HealthDimensionScoreServiceImpl.java` | 实现更新 |
+| 9 | `service/DimensionScoreService.java` | 接口新增 `refreshFromReport()` 等 |
+| 10 | `service/impl/DimensionScoreServiceImpl.java` | ⚠️ 核心重写:新7维、百分位引擎、Tier优先级、菌群映射 |
+| 11 | `controller/HealthDimensionController.java` | 适配新DTO,新增菌群刷新端点 |
+| 12 | `dto/HealthDimensionVO.java` | 增 `percentile`/`dataSource`/`tier`/`expireDate` |
+| 13 | `dto/DimensionUploadVO.java` | 增 `sourceType`/`confidence` |
+| 14 | `dto/HealthDimensionQuestionnaireVO.java` | 不变(结构通用) |
+| 15 | `dto/DimensionScoreVO.java` | 更新默认7维列表 |
+| 16 | `config/DatabaseInitializer.java` | ⚠️ 更新DDL(3表)+ 新增7维常模种子数据 |
+
+### Modified Frontend Files
+| # | File | Change |
+|:-:|------|--------|
+| 17 | `pages/body/health-dimensions.vue` | 7维标签更换 + 数据源图标 + 问卷入口 |
+
+---
+
+## Parallel Execution Waves
+
+```
+Wave 1 (DB + Entity — 3 parallel tasks, depends on nothing):
+├── Task 1: Database migration (更新3张表DDL + 常模种子数据)
+├── Task 2: 更新3个实体类
+└── Task 3: 更新3个Mapper
+
+Wave 2 (Service Core — 3 parallel tasks, depends on Wave 1):
+├── Task 4: HealthDimensionScoreService CRUD + HealthDataSourceRecordService 更新
+├── Task 5: DimensionScoreService 核心聚合器重构 (新7维 + 百分位 + Tier优先级)
+└── Task 6: 菌群报告→维度映射 (PdfParseService 集成)
+
+Wave 3 (API + DTO — 2 parallel tasks, depends on Wave 2):
+├── Task 7: 更新DTO对象
+└── Task 8: 更新Controller (适配新DTO + 新增refresh端点)
+
+Wave 4 (Frontend — depends on Wave 3):
+└── Task 9: 更新前端页面
+
+Wave FINAL (Verification):
+├── F1: mvn clean compile
+├── F2: 启动验证 + API curl测试
+├── F3: 前端构建验证
+└── F4: 设计文档合规性检查
+```
+
+---
+
+## TODOs
+
+### Task 1: 数据库迁移(更新3张表 + 常模种子数据)
+
+**Files:** `config/DatabaseInitializer.java`
+
+**Note:** 现有表 `health_dimension_score` 和 `health_data_source_record` 已有生产数据。迁移策略:
+1. 创建新表(新名字/新结构)
+2. 迁移旧数据(将 `is_latest=1` 的记录映射到新表)
+3. 保留旧表不动(防止数据丢失),仅在新表上工作
+
+**注意:** `DatabaseInitializer.java` 使用 `jdbcTemplate.execute()` 和 `migrate("V...", () -> { ... })` 模式。新迁移加在 `migrateHealthDimensionsTable()` 方法中。
+
+- [ ] **Step 1: 创建 `health_dimension_scores` 新表(在 `migrateHealthDimensionsTable()` 前添加新迁移)**
+
+```java
+// 在 DatabaseInitializer.java 的 migrateHealthDimensionsTable() 调用之前,添加一个新的 migrate 调用:
+
+migrate("V20260626_01__create_health_dimension_scores", () -> {
+    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `health_dimension_scores` (\n" +
+        "  `id` bigint NOT NULL AUTO_INCREMENT,\n" +
+        "  `member_id` bigint NOT NULL COMMENT '家庭成员ID',\n" +
+        "  `dimension` varchar(20) NOT NULL COMMENT '维度: growth/sleep/vision/immunity/nutrition/gut/exercise',\n" +
+        "  `score` int NOT NULL COMMENT '0-100分',\n" +
+        "  `percentile` int DEFAULT NULL COMMENT '人群百分位 0-100',\n" +
+        "  `data_source` varchar(20) NOT NULL COMMENT '数据源: REPORT/PHOTO/VOICE/MANUAL',\n" +
+        "  `tier` tinyint NOT NULL DEFAULT 3 COMMENT '数据层级: 1自动/2语音/3手动',\n" +
+        "  `raw_data` json DEFAULT NULL COMMENT '原始数据快照',\n" +
+        "  `assess_date` date NOT NULL COMMENT '评估日期',\n" +
+        "  `expire_date` date DEFAULT NULL COMMENT '数据过期日',\n" +
+        "  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n" +
+        "  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n" +
+        "  PRIMARY KEY (`id`),\n" +
+        "  UNIQUE KEY `uk_member_dim_date` (`member_id`, `dimension`, `assess_date`)\n" +
+        ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康维度评分表'");
+    log.info("health_dimension_scores 表创建完成");
+    return true;
+});
+```
+
+- [ ] **Step 2: 创建 `health_data_sources` 新表**
+
+```java
+migrate("V20260626_02__create_health_data_sources", () -> {
+    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `health_data_sources` (\n" +
+        "  `id` bigint NOT NULL AUTO_INCREMENT,\n" +
+        "  `member_id` bigint NOT NULL,\n" +
+        "  `source_type` varchar(20) NOT NULL COMMENT 'PDF/PHOTO/VOICE/MANUAL/WEIXIN_STEP',\n" +
+        "  `dimension_codes` json NOT NULL COMMENT '影响的维度列表',\n" +
+        "  `parsed_result` json DEFAULT NULL COMMENT '解析结果全量',\n" +
+        "  `file_url` varchar(500) DEFAULT NULL COMMENT '原始文件URL',\n" +
+        "  `confidence` decimal(3,2) DEFAULT NULL COMMENT 'AI解析置信度',\n" +
+        "  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n" +
+        "  PRIMARY KEY (`id`),\n" +
+        "  KEY `idx_member_source` (`member_id`, `source_type`)\n" +
+        ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康数据源记录表'");
+    log.info("health_data_sources 表创建完成");
+    return true;
+});
+```
+
+- [ ] **Step 3: 重构 `health_norm_reference` 表(修改表结构为百分位模式)**
+
+```java
+migrate("V20260626_03__recreate_health_norm_reference", () -> {
+    jdbcTemplate.execute("DROP TABLE IF EXISTS `health_norm_reference_old`");
+    jdbcTemplate.execute("RENAME TABLE `health_norm_reference` TO `health_norm_reference_old`");
+    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `health_norm_reference` (\n" +
+        "  `id` bigint NOT NULL AUTO_INCREMENT,\n" +
+        "  `dimension` varchar(20) NOT NULL COMMENT '维度',\n" +
+        "  `gender` varchar(10) NOT NULL COMMENT 'male/female/all',\n" +
+        "  `age_min` int NOT NULL COMMENT '年龄下限(月)',\n" +
+        "  `age_max` int NOT NULL COMMENT '年龄上限(月)',\n" +
+        // 所有维度的 percentile_X 统一为 0-100 评分(与 health_dimension_scores.score 同尺度)
+        "  `percentile_5` int DEFAULT NULL,\n" +
+        "  `percentile_15` int DEFAULT NULL,\n" +
+        "  `percentile_25` int DEFAULT NULL,\n" +
+        "  `percentile_50` int DEFAULT NULL COMMENT '中位值',\n" +
+        "  `percentile_75` int DEFAULT NULL,\n" +
+        "  `percentile_85` int DEFAULT NULL,\n" +
+        "  `percentile_95` int DEFAULT NULL,\n" +
+        "  `source` varchar(50) DEFAULT NULL COMMENT '常模来源',\n" +
+        "  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n" +
+        "  PRIMARY KEY (`id`),\n" +
+        "  UNIQUE KEY `uk_dim_gender_age` (`dimension`, `gender`, `age_min`, `age_max`)\n" +
+        ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康常模对照表'");
+    log.info("health_norm_reference 表重建完成");
+    return true;
+});
+```
+
+- [ ] **Step 4: 插入7维常模种子数据(修改 `seedHealthNorms()` 方法)**
+
+```java
+    // 替换现有的 seedHealthNorms() 方法体
+    // 注意:所有 percentile_X 字段的单位统一为 0-100 评分(与 health_dimension_scores.score 同尺度),
+    // 这样 calcPercentile() 可以直接用 score 与常模比较,无需单位换算。
+    // 以下数据基于国家标准/行业参考的人群分布,转换为百分制评分。
+    private void seedHealthNorms() {
+        try {
+            int count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM health_norm_reference", Integer.class);
+            if (count == 0) {
+                // 生长发育 (growth) — 基于BMI-for-age分布映射到0-100评分
+                jdbcTemplate.execute("INSERT INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES\n" +
+                    "('growth', 'male', 72, 84, 25, 35, 45, 55, 70, 80, 90, 'BMI-for-age→百分制'),\n" +
+                    "('growth', 'female', 72, 84, 25, 35, 45, 55, 70, 80, 90, 'BMI-for-age→百分制'),\n" +
+                    "('growth', 'male', 84, 96, 25, 35, 45, 55, 70, 80, 90, 'BMI-for-age→百分制'),\n" +
+                    "('growth', 'female', 84, 96, 25, 35, 45, 55, 70, 80, 90, 'BMI-for-age→百分制'),\n" +
+                    "('growth', 'male', 96, 120, 25, 35, 45, 55, 70, 80, 90, 'BMI-for-age→百分制'),\n" +
+                    "('growth', 'female', 96, 120, 25, 35, 45, 55, 70, 80, 90, 'BMI-for-age→百分制'),\n" +
+                    // 睡眠质量 (sleep) — 时长分布→百分制评分
+                    "('sleep', 'all', 0, 12, 40, 50, 60, 70, 80, 88, 95, 'NSF婴幼儿睡眠→百分制'),\n" +
+                    "('sleep', 'all', 12, 36, 40, 50, 60, 70, 80, 88, 95, 'NSF幼儿睡眠→百分制'),\n" +
+                    "('sleep', 'all', 36, 72, 40, 50, 60, 70, 80, 88, 95, 'NSF学龄前睡眠→百分制'),\n" +
+                    "('sleep', 'all', 72, 156, 40, 50, 60, 70, 80, 88, 95, 'NSF学龄儿童睡眠→百分制'),\n" +
+                    // 视力健康 (vision) — 对数视力→百分制评分
+                    "('vision', 'male', 72, 144, 45, 55, 65, 75, 85, 90, 95, '视力发育标准→百分制'),\n" +
+                    "('vision', 'female', 72, 144, 45, 55, 65, 75, 85, 90, 95, '视力发育标准→百分制'),\n" +
+                    // 免疫力 (immunity) — 年感冒次数→百分制评分(次数越少分越高)
+                    "('immunity', 'all', 0, 72, 15, 30, 45, 65, 80, 90, 95, '年患病次数→百分制'),\n" +
+                    "('immunity', 'all', 72, 192, 15, 35, 50, 70, 85, 92, 98, '年患病次数→百分制'),\n" +
+                    // 营养均衡 (nutrition) — 膳食多样性评分→百分制
+                    "('nutrition', 'all', 36, 192, 20, 30, 40, 50, 60, 70, 80, '膳食多样性→百分制'),\n" +
+                    // 肠胃健康 (gut) — 菌群健康度评分(已是0-100)
+                    "('gut', 'all', 0, 192, 40, 50, 60, 70, 80, 85, 95, '菌群人群分布(平台自建)'),\n" +
+                    // 运动活力 (exercise) — 体质健康标准(已是0-100)
+                    "('exercise', 'male', 72, 144, 30, 40, 50, 60, 70, 80, 90, '国家学生体质健康标准'),\n" +
+                    "('exercise', 'female', 72, 144, 25, 35, 45, 55, 65, 75, 85, '国家学生体质健康标准')");
+            log.info("health_norm_reference 常模种子数据已加载 (18条, 统一0-100百分制)");
+        } else {
+            log.info("health_norm_reference 常模数据已存在 ({}条), 跳过", count);
+        }
+    } catch (Exception e) {
+        log.warn("health_norm_reference 常模种子数据初始化失败: {}", e.getMessage());
+    }
+}
+```
+
+- [ ] **Step 5: 数据迁移 — 将旧 `health_dimension_score` 数据迁移到新表**
+
+```java
+migrate("V20260626_04__migrate_old_dimension_scores", () -> {
+    // 检查旧表是否有数据
+    Integer oldCount = jdbcTemplate.queryForObject(
+        "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'health_dimension_score'",
+        Integer.class);
+    if (oldCount != null && oldCount > 0) {
+        // 将旧表数据映射到新表(维度code映射)
+        jdbcTemplate.execute("INSERT IGNORE INTO health_dimension_scores (member_id, dimension, score, data_source, tier, assess_date, created_at)\n" +
+            " SELECT member_id,\n" +
+            "  CASE dimension\n" +
+            "    WHEN 'body' THEN 'growth'\n" +
+            "    WHEN 'fitness' THEN 'exercise'\n" +
+            "    WHEN 'cardio' THEN 'exercise'\n" +
+            "    WHEN 'blood' THEN 'immunity'\n" +
+            "    WHEN 'sleep' THEN 'sleep'\n" +
+            "    WHEN 'psyche' THEN 'growth'\n" +
+            "    WHEN 'diet' THEN 'nutrition'\n" +
+            "    ELSE dimension\n" +
+            "  END,\n" +
+            "  score,\n" +
+            "  UPPER(data_source),\n" +
+            "  3,\n" +
+            "  record_date,\n" +
+            "  NOW()\n" +
+            " FROM health_dimension_score WHERE is_latest = 1");
+        log.info("旧维度评分数据迁移完成");
+    }
+    return true;
+});
+```
+
+- [ ] **Step 6: 编译验证**
+
+```bash
+export PATH=$HOME/.local/bin:$PATH
+cd /sc-data/cfc/cfc-backend && rtk mvn clean compile -q 2>&1 | tail -20
+```
+
+- [ ] **Step 7: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
+git commit -m "feat(db): migrate 7-dim health tables to design spec schema + norm seed data"
+```
+
+---
+
+### Task 2: 更新3个实体类
+
+**Files:**
+- Modify: `entity/HealthDimensionScore.java`
+- Modify: `entity/HealthNormReference.java`
+- Modify: `entity/HealthDataSourceRecord.java`
+
+- [ ] **Step 1: 重写 `HealthDimensionScore.java`**
+
+```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.util.Date;
+
+@Data
+@TableName("health_dimension_scores")
+public class HealthDimensionScore implements Serializable {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long memberId;
+    private String dimension;     // growth/sleep/vision/immunity/nutrition/gut/exercise
+    private Integer score;        // 0-100
+    private Integer percentile;   // 人群百分位 0-100
+    private String dataSource;    // REPORT/PHOTO/VOICE/MANUAL
+    private Integer tier;         // 1/2/3
+    private String rawData;       // JSON
+    private Date assessDate;
+    private Date expireDate;
+    private Date createdAt;
+    private Date updatedAt;
+}
+```
+
+- [ ] **Step 2: 重写 `HealthNormReference.java`**
+
+```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.util.Date;
+
+@Data
+@TableName("health_norm_reference")
+public class HealthNormReference implements Serializable {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String dimension;
+    private String gender;       // male/female/all
+    private Integer ageMin;      // 月
+    private Integer ageMax;      // 月
+    private Integer percentile5;
+    private Integer percentile15;
+    private Integer percentile25;
+    private Integer percentile50;
+    private Integer percentile75;
+    private Integer percentile85;
+    private Integer percentile95;
+    private String source;
+    private Date createdAt;
+}
+```
+
+- [ ] **Step 3: 重写 `HealthDataSourceRecord.java`**
+
+```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_data_sources")
+public class HealthDataSourceRecord implements Serializable {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long memberId;
+    private String sourceType;       // PDF/PHOTO/VOICE/MANUAL/WEIXIN_STEP
+    private String dimensionCodes;   // JSON array
+    private String parsedResult;     // JSON
+    private String fileUrl;
+    private BigDecimal confidence;   // 0.00-1.00
+    private Date createdAt;
+}
+```
+
+- [ ] **Step 4: 编译验证**
+
+```bash
+cd /sc-data/cfc/cfc-backend && rtk mvn clean compile -q 2>&1 | tail -20
+```
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/entity/
+git commit -m "feat(entity): remodel 3 health dimension entities to match design spec"
+```
+
+---
+
+### Task 3: 更新3个Mapper
+
+**Files:**
+- Modify: `mapper/HealthDimensionScoreMapper.java`
+- Modify: `mapper/HealthNormReferenceMapper.java`
+- Modify: `mapper/HealthDataSourceRecordMapper.java`
+
+- [ ] **Step 1: 重写 `HealthDimensionScoreMapper.java`**
+
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.HealthDimensionScore;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import java.util.List;
+
+@Mapper
+public interface HealthDimensionScoreMapper extends BaseMapper<HealthDimensionScore> {
+
+    // 取某个member最新的7维评分(每个维度最新assess_date的一条)
+    @Select("SELECT h.* FROM health_dimension_scores h " +
+        "INNER JOIN (SELECT dimension, MAX(assess_date) max_date FROM health_dimension_scores " +
+        "WHERE member_id = #{memberId} GROUP BY dimension) latest " +
+        "ON h.dimension = latest.dimension AND h.assess_date = latest.max_date " +
+        "WHERE h.member_id = #{memberId}")
+    List<HealthDimensionScore> selectLatestByMember(@Param("memberId") Long memberId);
+
+    // 取某member某维度的最新评分
+    @Select("SELECT * FROM health_dimension_scores WHERE member_id = #{memberId} " +
+        "AND dimension = #{dimension} ORDER BY assess_date DESC LIMIT 1")
+    HealthDimensionScore selectLatest(@Param("memberId") Long memberId, @Param("dimension") String dimension);
+
+    // 取某member某维度的历史评分
+    @Select("SELECT * FROM health_dimension_scores WHERE member_id = #{memberId} " +
+        "AND dimension = #{dimension} ORDER BY assess_date DESC LIMIT #{limit}")
+    List<HealthDimensionScore> selectHistory(@Param("memberId") Long memberId,
+        @Param("dimension") String dimension, @Param("limit") int limit);
+
+    // 删除现有迁移方法中用不到 `updateExpired`,新表用 UK 机制保证唯一性
+}
+```
+
+- [ ] **Step 2: 重写 `HealthNormReferenceMapper.java`**
+
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.HealthNormReference;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+@Mapper
+public interface HealthNormReferenceMapper extends BaseMapper<HealthNormReference> {
+
+    // 根据维度+性别+年龄查到对应的常模行(按年龄范围匹配)
+    @Select("SELECT * FROM health_norm_reference WHERE dimension = #{dimension} " +
+        "AND (gender = #{gender} OR gender = 'all') " +
+        "AND age_min <= #{ageMonths} AND age_max > #{ageMonths} " +
+        "ORDER BY gender DESC LIMIT 1")
+    HealthNormReference selectNorm(@Param("dimension") String dimension,
+        @Param("gender") String gender, @Param("ageMonths") Integer ageMonths);
+}
+```
+
+- [ ] **Step 3: 重写 `HealthDataSourceRecordMapper.java`**
+
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.HealthDataSourceRecord;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import java.util.List;
+
+@Mapper
+public interface HealthDataSourceRecordMapper extends BaseMapper<HealthDataSourceRecord> {
+
+    @Select("SELECT * FROM health_data_sources WHERE member_id = #{memberId} " +
+        "AND source_type = #{sourceType} ORDER BY created_at DESC LIMIT 1")
+    HealthDataSourceRecord selectLatestBySource(@Param("memberId") Long memberId,
+        @Param("sourceType") String sourceType);
+
+    @Select("SELECT * FROM health_data_sources WHERE member_id = #{memberId} " +
+        "ORDER BY created_at DESC")
+    List<HealthDataSourceRecord> selectByMember(@Param("memberId") Long memberId);
+
+    @Select("SELECT * FROM health_data_sources WHERE member_id = #{memberId} " +
+        "AND JSON_CONTAINS(dimension_codes, #{dimensionCode}) " +
+        "ORDER BY created_at DESC LIMIT #{limit}")
+    List<HealthDataSourceRecord> selectByDimension(@Param("memberId") Long memberId,
+        @Param("dimensionCode") String dimensionCode, @Param("limit") int limit);
+}
+```
+
+- [ ] **Step 4: 编译验证**
+
+```bash
+cd /sc-data/cfc/cfc-backend && rtk mvn clean compile -q 2>&1 | tail -20
+```
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/mapper/
+git commit -m "feat(mapper): update 3 health dimension mappers for new schema"
+```
+
+---
+
+### Task 4: 更新 HealthDimensionScoreService + HealthDataSourceRecordService
+
+**Files:**
+- Modify: `service/HealthDimensionScoreService.java`
+- Modify: `service/impl/HealthDimensionScoreServiceImpl.java`
+
+- [ ] **Step 1: 更新 `HealthDimensionScoreService.java` 接口**
+
+```java
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.entity.HealthDimensionScore;
+import java.util.List;
+
+public interface HealthDimensionScoreService {
+    void saveScore(HealthDimensionScore score);
+    List<HealthDimensionScore> getLatestScores(Long memberId);
+    HealthDimensionScore getLatestScore(Long memberId, String dimension);
+    List<HealthDimensionScore> getHistoryScores(Long memberId, String dimension, int limit);
+}
+```
+
+- [ ] **Step 2: 更新 `HealthDimensionScoreServiceImpl.java`**
+
+```java
+package com.etotem.cfc.service.impl;
+
+import com.etotem.cfc.entity.HealthDimensionScore;
+import com.etotem.cfc.mapper.HealthDimensionScoreMapper;
+import com.etotem.cfc.service.HealthDimensionScoreService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import javax.annotation.Resource;
+import java.util.List;
+
+@Service
+public class HealthDimensionScoreServiceImpl implements HealthDimensionScoreService {
+
+    @Resource
+    private HealthDimensionScoreMapper scoreMapper;
+
+    @Override
+    @Transactional
+    public void saveScore(HealthDimensionScore score) {
+        scoreMapper.insert(score);
+    }
+
+    @Override
+    public List<HealthDimensionScore> getLatestScores(Long memberId) {
+        return scoreMapper.selectLatestByMember(memberId);
+    }
+
+    @Override
+    public HealthDimensionScore getLatestScore(Long memberId, String dimension) {
+        return scoreMapper.selectLatest(memberId, dimension);
+    }
+
+    @Override
+    public List<HealthDimensionScore> getHistoryScores(Long memberId, String dimension, int limit) {
+        return scoreMapper.selectHistory(memberId, dimension, limit);
+    }
+}
+```
+
+- [ ] **Step 3: 编译验证**
+
+```bash
+cd /sc-data/cfc/cfc-backend && rtk mvn clean compile -q 2>&1 | tail -20
+```
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/HealthDimensionScoreService.java
+git add cfc-backend/src/main/java/com/etotem/cfc/service/impl/HealthDimensionScoreServiceImpl.java
+git commit -m "feat(service): update HealthDimensionScoreService for new entity"
+```
+
+---
+
+### Task 5: DimensionScoreService 核心聚合器重构
+
+**Files:**
+- Modify: `service/DimensionScoreService.java`
+- Modify: `service/impl/DimensionScoreServiceImpl.java`
+
+这是整个重构的核心。需要实现:
+1. 7维列表从旧 → 新
+2. 百分位计算引擎(原始分 → 查常模 → 百分位)
+3. 数据源Tier优先级合并
+4. 健康指数计算
+
+- [ ] **Step 1: 更新 `DimensionScoreService.java` 接口**
+
+```java
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.dto.HealthDimensionVO;
+import com.etotem.cfc.dto.DimensionUploadVO;
+import com.etotem.cfc.dto.HealthDimensionQuestionnaireVO;
+import java.util.List;
+
+public interface DimensionScoreService {
+    /** 获取某member的7维概览(含百分位) */
+    HealthDimensionVO getHealthDimension(Long memberId);
+
+    /** 手动上传单维度评分 */
+    void uploadScore(DimensionUploadVO vo);
+
+    /** 问卷提交(答案→均分→维度评分) */
+    void submitQuestionnaire(HealthDimensionQuestionnaireVO vo);
+
+    /** 获取维度历史 */
+    List<HealthDimensionVO.DimensionItem> getDimensionHistory(Long memberId, String dimension, int limit);
+
+    /** 菌群报告解析后刷新维度(核心映射逻辑) */
+    void refreshFromGutReport(Long memberId, Long reportId);
+}
+```
+
+- [ ] **Step 2: 重写 `DimensionScoreServiceImpl.java`**
+
+```java
+package com.etotem.cfc.service.impl;
+
+import com.etotem.cfc.dto.HealthDimensionVO;
+import com.etotem.cfc.dto.DimensionUploadVO;
+import com.etotem.cfc.dto.HealthDimensionQuestionnaireVO;
+import com.etotem.cfc.entity.HealthDimensionScore;
+import com.etotem.cfc.entity.HealthDataSourceRecord;
+import com.etotem.cfc.entity.HealthGutFlora;
+import com.etotem.cfc.entity.HealthIndicator;
+import com.etotem.cfc.entity.HealthDiseaseRisk;
+import com.etotem.cfc.entity.HealthNormReference;
+import com.etotem.cfc.entity.HealthReport;
+import com.etotem.cfc.mapper.HealthNormReferenceMapper;
+import com.etotem.cfc.service.DimensionScoreService;
+import com.etotem.cfc.service.HealthDimensionScoreService;
+import com.etotem.cfc.service.HealthReportService;
+import com.etotem.cfc.service.HealthDataSourceRecordService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+public class DimensionScoreServiceImpl implements DimensionScoreService {
+
+    // === 设计文档规范的7维 ===
+    private static final List<String[]> DIMENSIONS = Arrays.asList(
+        new String[]{"growth",    "生长发育"},
+        new String[]{"sleep",     "睡眠质量"},
+        new String[]{"vision",    "视力健康"},
+        new String[]{"immunity",  "免疫力"},
+        new String[]{"nutrition", "营养均衡"},
+        new String[]{"gut",       "肠胃健康"},
+        new String[]{"exercise",  "运动活力"}
+    );
+
+    private static final Map<String, Integer> DIMENSION_TIER = new HashMap<>();
+    static {
+        // Tier 1: 报告/自动数据
+        DIMENSION_TIER.put("growth", 1);
+        DIMENSION_TIER.put("sleep", 1);
+        DIMENSION_TIER.put("vision", 1);
+        DIMENSION_TIER.put("immunity", 1);
+        DIMENSION_TIER.put("nutrition", 1);
+        DIMENSION_TIER.put("gut", 1);
+        DIMENSION_TIER.put("exercise", 1);
+    }
+
+    @Resource
+    private HealthDimensionScoreService scoreService;
+
+    @Resource
+    private HealthNormReferenceMapper normMapper;
+
+    @Resource
+    private HealthReportService healthReportService;
+
+    @Override
+    public HealthDimensionVO getHealthDimension(Long memberId) {
+        List<HealthDimensionScore> latest = scoreService.getLatestScores(memberId);
+        Map<String, HealthDimensionScore> scoreMap = latest.stream()
+            .collect(Collectors.toMap(HealthDimensionScore::getDimension, s -> s, (a, b) -> a));
+
+        List<HealthDimensionVO.DimensionItem> items = new ArrayList<>();
+        BigDecimal totalScore = BigDecimal.ZERO;
+        int validCount = 0;
+
+        for (String[] dim : DIMENSIONS) {
+            String code = dim[0];
+            String label = dim[1];
+            HealthDimensionScore found = scoreMap.get(code);
+
+            HealthDimensionVO.DimensionItem item = new HealthDimensionVO.DimensionItem();
+            item.setDimension(code);
+            item.setLabel(label);
+
+            if (found != null) {
+                item.setScore(BigDecimal.valueOf(found.getScore()));
+                item.setPercentile(found.getPercentile());
+                item.setDataSource(found.getDataSource());
+                item.setTier(found.getTier());
+                item.setLevel(calcLevel(found.getScore()));
+                item.setTrend("stable");
+                if (found.getExpireDate() != null) {
+                    item.setExpireDate(new SimpleDateFormat("yyyy-MM-dd").format(found.getExpireDate()));
+                }
+                totalScore = totalScore.add(BigDecimal.valueOf(found.getScore()));
+                validCount++;
+            } else {
+                item.setScore(null);
+                item.setLevel("none");
+                item.setTrend(null);
+            }
+            items.add(item);
+        }
+
+        HealthDimensionVO vo = new HealthDimensionVO();
+        vo.setMemberId(memberId);
+        vo.setHealthIndex(validCount > 0
+            ? totalScore.divide(BigDecimal.valueOf(validCount), 2, RoundingMode.HALF_UP) : null);
+        vo.setDimensions(items);
+        vo.setUpdateTime(new Date());
+        return vo;
+    }
+
+    @Override
+    @Transactional
+    public void uploadScore(DimensionUploadVO vo) {
+        HealthDimensionScore score = new HealthDimensionScore();
+        score.setMemberId(vo.getMemberId());
+        score.setDimension(vo.getDimension());
+        score.setScore(vo.getScore() != null ? vo.getScore().intValue() : 0);
+        score.setDataSource(vo.getSourceType() != null ? vo.getSourceType() : "MANUAL");
+        score.setTier(3); // 手动上传默认 Tier 3
+        if (vo.getRecordDate() != null) {
+            score.setAssessDate(java.sql.Date.valueOf(vo.getRecordDate()));
+        } else {
+            score.setAssessDate(new java.sql.Date(System.currentTimeMillis()));
+        }
+        // 计算百分位
+        score.setPercentile(calcPercentile(vo.getMemberId(), vo.getDimension(),
+            vo.getScore() != null ? vo.getScore().intValue() : 0));
+        scoreService.saveScore(score);
+    }
+
+    @Override
+    @Transactional
+    public void submitQuestionnaire(HealthDimensionQuestionnaireVO vo) {
+        BigDecimal total = BigDecimal.ZERO;
+        int count = 0;
+        for (HealthDimensionQuestionnaireVO.QuestionAnswer qa : vo.getAnswers()) {
+            if (qa.getScore() != null) {
+                total = total.add(qa.getScore());
+                count++;
+            }
+        }
+        if (count == 0) return;
+        int avgScore = total.divide(BigDecimal.valueOf(count), 0, RoundingMode.HALF_UP).intValue();
+
+        HealthDimensionScore score = new HealthDimensionScore();
+        score.setMemberId(vo.getMemberId());
+        score.setDimension(vo.getDimension());
+        score.setScore(avgScore);
+        score.setDataSource("MANUAL");
+        score.setTier(3);
+        score.setAssessDate(new java.sql.Date(System.currentTimeMillis()));
+        score.setPercentile(calcPercentile(vo.getMemberId(), vo.getDimension(), avgScore));
+        scoreService.saveScore(score);
+    }
+
+    @Override
+    public List<HealthDimensionVO.DimensionItem> getDimensionHistory(Long memberId, String dimension, int limit) {
+        List<HealthDimensionScore> history = scoreService.getHistoryScores(memberId, dimension, limit);
+        List<HealthDimensionVO.DimensionItem> items = new ArrayList<>();
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+        for (HealthDimensionScore s : history) {
+            HealthDimensionVO.DimensionItem item = new HealthDimensionVO.DimensionItem();
+            item.setDimension(s.getDimension());
+            item.setScore(BigDecimal.valueOf(s.getScore()));
+            item.setPercentile(s.getPercentile());
+            item.setDataSource(s.getDataSource());
+            item.setTier(s.getTier());
+            item.setLevel(calcLevel(s.getScore()));
+            item.setRecordDate(sdf.format(s.getAssessDate()));
+            items.add(item);
+        }
+        return items;
+    }
+
+    @Override
+    @Transactional
+    public void refreshFromGutReport(Long memberId, Long reportId) {
+        HealthReport report = healthReportService.getReportById(reportId);
+        if (report == null) return;
+
+        List<HealthGutFlora> gutFlora = healthReportService.getGutFloraByReportId(reportId);
+        List<HealthIndicator> indicators = healthReportService.getReportIndicators(reportId);
+        List<HealthDiseaseRisk> diseaseRisks = healthReportService.getDiseaseRisksByReportId(reportId);
+
+        Map<String, Integer> dimScores = mapGutFloraToDimensions(report, gutFlora, indicators, diseaseRisks);
+
+        // 写入维度评分(Tier 1: 报告数据)
+        Date assessDate = report.getReportDate() != null
+            ? report.getReportDate() : new java.sql.Date(System.currentTimeMillis());
+        for (Map.Entry<String, Integer> entry : dimScores.entrySet()) {
+            HealthDimensionScore score = new HealthDimensionScore();
+            score.setMemberId(memberId);
+            score.setDimension(entry.getKey());
+            score.setScore(entry.getValue());
+            score.setPercentile(calcPercentile(memberId, entry.getKey(), entry.getValue()));
+            score.setDataSource("REPORT");
+            score.setTier(1);
+            score.setAssessDate(assessDate);
+            // 菌群报告有效期6个月
+            Calendar cal = Calendar.getInstance();
+            cal.setTime(assessDate);
+            cal.add(Calendar.MONTH, 6);
+            score.setExpireDate(cal.getTime());
+            score.setRawData("{\"reportId\":" + reportId + "}");
+            scoreService.saveScore(score);
+        }
+    }
+
+    /**
+     * 根据设计文档的映射矩阵,将已有健康报告数据映射到7维评分。
+     *
+     * 数据来源:
+     * - HealthReport 本身的评分子段: gutHealthScore, chronicDiseaseScore, nutritionScore,
+     *   gutBalanceScore, gutDiversityScore, beneficialBacteriaScore, harmfulBacteriaScore, coreSpeciesScore
+     * - HealthIndicator(indicators): category + indicatorValue(需解析String→数值)
+     * - HealthDiseaseRisk(diseaseRisks): riskValue(String)/riskLevel → 反向分
+     * - HealthGutFlora(gutFlora): populationLevel(String, e.g."35%") 作为人群百分位参考
+     *
+     * 映射矩阵参照设计文档 3.2 节。
+     */
+    private Map<String, Integer> mapGutFloraToDimensions(
+            HealthReport report,
+            List<HealthGutFlora> gutFlora,
+            List<HealthIndicator> indicators,
+            List<HealthDiseaseRisk> diseaseRisks) {
+
+        Map<String, List<Integer>> dimValues = new HashMap<>();
+        for (String[] dim : DIMENSIONS) {
+            dimValues.put(dim[0], new ArrayList<>());
+        }
+
+        // === 1. 从 HealthReport 直接取评分字段 ===
+        // gutHealthScore → immunity, gut
+        if (report.getGutHealthScore() != null) {
+            dimValues.get("immunity").add(report.getGutHealthScore());
+            dimValues.get("gut").add(report.getGutHealthScore());
+        }
+        // chronicDiseaseScore → growth, nutrition(反转:分越高越健康)
+        if (report.getChronicDiseaseScore() != null) {
+            int inverted = 100 - report.getChronicDiseaseScore();
+            dimValues.get("growth").add(inverted);
+            dimValues.get("nutrition").add(inverted);
+        }
+        // nutritionScore → nutrition
+        if (report.getNutritionScore() != null) {
+            dimValues.get("nutrition").add(report.getNutritionScore());
+        }
+        // gutBalanceScore → gut
+        if (report.getGutBalanceScore() != null) {
+            dimValues.get("gut").add(report.getGutBalanceScore());
+        }
+        // gutDiversityScore → immunity, gut
+        if (report.getGutDiversityScore() != null) {
+            dimValues.get("immunity").add(report.getGutDiversityScore());
+            dimValues.get("gut").add(report.getGutDiversityScore());
+        }
+        // beneficialBacteriaScore → immunity, gut
+        if (report.getBeneficialBacteriaScore() != null) {
+            dimValues.get("immunity").add(report.getBeneficialBacteriaScore());
+            dimValues.get("gut").add(report.getBeneficialBacteriaScore());
+        }
+        // harmfulBacteriaScore → immunity, gut(反转)
+        if (report.getHarmfulBacteriaScore() != null) {
+            int inverted = 100 - report.getHarmfulBacteriaScore();
+            dimValues.get("immunity").add(inverted);
+            dimValues.get("gut").add(inverted);
+        }
+        // coreSpeciesScore → gut
+        if (report.getCoreSpeciesScore() != null) {
+            dimValues.get("gut").add(report.getCoreSpeciesScore());
+        }
+        // gutAge → sleep, gut(解析字符串,如 "35岁" → 数值映射)
+        if (report.getGutAge() != null) {
+            try {
+                int gutAgeYears = Integer.parseInt(report.getGutAge().replaceAll("[^0-9]", ""));
+                // 肠道年龄越小越好:计算与生理年龄的差值映射
+                int gutAgeScore = Math.max(0, Math.min(100, 100 - gutAgeYears * 2));
+                dimValues.get("sleep").add(gutAgeScore);
+                dimValues.get("gut").add(gutAgeScore);
+            } catch (NumberFormatException ignored) {}
+        }
+
+        // === 2. 从 HealthIndicator 取(category分类)===
+        for (HealthIndicator ind : indicators) {
+            String cat = ind.getCategory();
+            String rawVal = ind.getIndicatorValue();
+            if (cat == null || rawVal == null) continue;
+
+            int val = tryParseNumericIndicator(rawVal);
+            if (val < 0) continue; // 无法解析为数值
+
+            if ("神经递质与激素".contains(cat != null ? cat : "")) {
+                dimValues.get("sleep").add(val);
+            } else if ("短链脂肪酸".equals(cat)) {
+                dimValues.get("nutrition").add(val);
+                dimValues.get("gut").add(val);
+            } else if ("肠道屏障".equals(cat)) {
+                dimValues.get("immunity").add(val);
+                dimValues.get("gut").add(val);
+            } else if ("抗生素耐药".equals(cat)) {
+                // 耐药分越低越好 → 反转
+                dimValues.get("immunity").add(100 - Math.min(val, 100));
+            } else if (cat != null && (cat.contains("维生素") || cat.contains("矿物质") || cat.contains("微量"))) {
+                dimValues.get("nutrition").add(val);
+            }
+        }
+
+        // === 3. 从 HealthDiseaseRisk 取 ===
+        for (HealthDiseaseRisk risk : diseaseRisks) {
+            if (risk.getRiskValue() == null) continue;
+            int riskVal = tryParseNumericIndicator(risk.getRiskValue());
+            if (riskVal < 0) {
+                // 用 riskLevel 映射
+                if ("低风险".equals(risk.getRiskLevel())) riskVal = 20;
+                else if ("需注意".equals(risk.getRiskLevel())) riskVal = 50;
+                else if ("高风险".equals(risk.getRiskLevel())) riskVal = 80;
+                else continue;
+            }
+            int invertedScore = 100 - Math.min(riskVal, 100);
+            // 慢性病/代谢类风险 → growth, nutrition
+            String name = risk.getDiseaseName();
+            if (name != null && (name.contains("代谢") || name.contains("肥胖") || name.contains("超重"))) {
+                dimValues.get("growth").add(invertedScore);
+                dimValues.get("nutrition").add(invertedScore);
+            }
+            // 感染类风险 → immunity
+            if (name != null && (name.contains("感染") || name.contains("炎症"))) {
+                dimValues.get("immunity").add(invertedScore);
+            }
+            // 其他风险都影响肠胃
+            dimValues.get("gut").add(invertedScore);
+        }
+
+        // === 4. 从 HealthGutFlora 取 populationLevel(作为百分位参考,不直接用于打分)===
+        // populationLevel 如 "35%" 表示该菌种处于人群35%水平
+        // 这里暂不直接用,留给后续百分位校准
+
+        // 聚合:每个维度取平均值
+        Map<String, Integer> result = new HashMap<>();
+        for (String[] dim : DIMENSIONS) {
+            List<Integer> vals = dimValues.get(dim[0]);
+            if (vals != null && !vals.isEmpty()) {
+                int avg = (int) vals.stream().mapToInt(v -> v).average().orElse(0);
+                result.put(dim[0], Math.min(100, Math.max(0, avg)));
+            }
+        }
+        return result;
+    }
+
+    /** 尝试从字符串解析数值,失败返回 -1 */
+    private int tryParseNumericIndicator(String val) {
+        if (val == null) return -1;
+        try {
+            String cleaned = val.replaceAll("[^0-9.]", "").trim();
+            if (cleaned.isEmpty()) return -1;
+            return (int) Double.parseDouble(cleaned);
+        } catch (NumberFormatException e) {
+            return -1;
+        }
+    }
+
+    /**
+     * 百分位计算:根据原始分(0-100) + 性别年龄 → 查常模 → 返回百分位
+     *
+     * 常模表 health_norm_reference 的 percentile_X 字段统一为 0-100 评分,
+     * 与 health_dimension_scores.score 同尺度,可直接整数比较。
+     */
+    private Integer calcPercentile(Long memberId, String dimension, int score) {
+        // TODO: 需要从用户资料读取性别和年龄(月)
+        // 这里简化实现,使用默认常模
+        HealthNormReference norm = normMapper.selectNorm(dimension, "all", 120);
+        if (norm == null) return null;
+
+        // 将原始分映射到百分位(同尺度直接比较)
+        if (norm.getPercentile95() != null && score >= norm.getPercentile95()) return 95;
+        if (norm.getPercentile85() != null && score >= norm.getPercentile85()) return 85;
+        if (norm.getPercentile75() != null && score >= norm.getPercentile75()) return 75;
+        if (norm.getPercentile50() != null && score >= norm.getPercentile50()) return 50;
+        if (norm.getPercentile25() != null && score >= norm.getPercentile25()) return 25;
+        if (norm.getPercentile15() != null && score >= norm.getPercentile15()) return 15;
+        if (norm.getPercentile5() != null && score >= norm.getPercentile5()) return 5;
+        return 1;
+    }
+
+    private String calcLevel(int score) {
+        if (score >= 85) return "excellent";
+        if (score >= 60) return "good";
+        if (score >= 40) return "fair";
+        return "poor";
+    }
+}
+```
+
+- [ ] **Step 3: 编译验证**
+
+```bash
+cd /sc-data/cfc/cfc-backend && rtk mvn clean compile -q 2>&1 | tail -20
+```
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/DimensionScoreService.java
+git add cfc-backend/src/main/java/com/etotem/cfc/service/impl/DimensionScoreServiceImpl.java
+git commit -m "feat(service): refactor DimensionScoreService - new 7-dim, percentile engine, gut mapping"
+```
+
+---
+
+### Task 6: 菌群报告映射集成完善
+
+**Files:**
+- Modify: `service/PdfParseService.java` 或新增加方法
+- Modify: 在 `HealthReportController.java` 中添加报告上传后的触发调用
+
+**注意**: PdfParseService 的 `ParsedReportResult` 对象包含 `getGutHealthScore()`、`getChronicDiseaseScore()`、`getNutritionScore()`、`getBalanceScore()`、`getDiversityScore()`、`getBeneficialScore()`、`getHarmfulScore()`、`getCoreGenusScore()` 等字段。已在 `HealthReportController` 的报告上传流程中使用。
+
+需要在报告上传成功创建后,调用 `DimensionScoreServiceImpl.refreshFromGutReport()`。
+
+- [ ] **Step 1: 在 `HealthReportController` 的报告创建端点中,创建完后调用菌群映射**
+
+定位到 `HealthReportController` 中的报告创建端点(`/api/health/report/upload` 或类似),在 `healthReportService.createReport()` 调用之后添加:
+
+```java
+// 触发菌群报告→7维映射
+try {
+    dimensionScoreService.refreshFromGutReport(matchedChildId, created.getId());
+    log.info("已触发菌群报告维度映射: reportId={}, childId={}", created.getId(), matchedChildId);
+} catch (Exception e) {
+    log.warn("菌群报告维度映射失败: {}", e.getMessage());
+}
+```
+
+需要在 Controller 中注入 `DimensionScoreService`:`@Resource private DimensionScoreService dimensionScoreService;`
+
+- [ ] **Step 2: 编译验证**
+
+```bash
+cd /sc-data/cfc/cfc-backend && rtk mvn clean compile -q 2>&1 | tail -20
+```
+
+- [ ] **Step 3: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java
+git commit -m "feat(integration): trigger gut flora dimension mapping after report upload"
+```
+
+---
+
+### Task 7: 更新DTO对象
+
+**Files:**
+- Modify: `dto/HealthDimensionVO.java`
+- Modify: `dto/DimensionUploadVO.java`
+- Modify: `dto/DimensionScoreVO.java`
+
+- [ ] **Step 1: 更新 `HealthDimensionVO.java`**
+
+```java
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+@Data
+public class HealthDimensionVO {
+    private Long memberId;
+    private BigDecimal healthIndex;      // 综合健康指数
+    private List<DimensionItem> dimensions;
+    private Date updateTime;
+
+    @Data
+    public static class DimensionItem {
+        private String dimension;        // growth/sleep/vision/immunity/nutrition/gut/exercise
+        private String label;            // 中文名
+        private BigDecimal score;        // 0-100
+        private Integer percentile;      // 人群百分位
+        private String dataSource;       // REPORT/PHOTO/VOICE/MANUAL
+        private Integer tier;            // 1/2/3
+        private String level;            // excellent/good/fair/poor/none
+        private String trend;            // up/down/stable
+        private String recordDate;       // 评估日期
+        private String expireDate;       // 过期日期
+    }
+}
+```
+
+- [ ] **Step 2: 更新 `DimensionUploadVO.java`**
+
+```java
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+import java.math.BigDecimal;
+
+@Data
+public class DimensionUploadVO {
+    private Long memberId;
+    private String dimension;
+    private BigDecimal score;
+    private String sourceType;       // REPORT/PHOTO/VOICE/MANUAL
+    private BigDecimal confidence;   // AI置信度
+    private String recordDate;
+}
+```
+
+- [ ] **Step 3: 更新 `DimensionScoreVO.java`**
+
+```java
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+import java.math.BigDecimal;
+import java.util.List;
+
+@Data
+public class DimensionScoreVO {
+    private String dimension;
+    private BigDecimal score;
+    private String label;
+    private String dataSource;
+    private String recordDate;
+
+    public DimensionScoreVO() {}
+
+    public DimensionScoreVO(String dimension, String label, BigDecimal score, String dataSource, String recordDate) {
+        this.dimension = dimension;
+        this.label = label;
+        this.score = score;
+        this.dataSource = dataSource;
+        this.recordDate = recordDate;
+    }
+
+    public static List<DimensionScoreVO> DEFAULT_DIMENSIONS = java.util.Arrays.asList(
+        new DimensionScoreVO("growth", "生长发育", null, null, null),
+        new DimensionScoreVO("sleep", "睡眠质量", null, null, null),
+        new DimensionScoreVO("vision", "视力健康", null, null, null),
+        new DimensionScoreVO("immunity", "免疫力", null, null, null),
+        new DimensionScoreVO("nutrition", "营养均衡", null, null, null),
+        new DimensionScoreVO("gut", "肠胃健康", null, null, null),
+        new DimensionScoreVO("exercise", "运动活力", null, null, null)
+    );
+}
+```
+
+- [ ] **Step 4: 编译验证**
+
+```bash
+cd /sc-data/cfc/cfc-backend && rtk mvn clean compile -q 2>&1 | tail -20
+```
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/dto/
+git commit -m "feat(dto): update DTOs for new 7-dim model and percentile"
+```
+
+---
+
+### Task 8: 更新 Controller
+
+**Files:**
+- Modify: `controller/HealthDimensionController.java`
+
+- [ ] **Step 1: 更新 Controller**
+
+保留现有4个端点,`@RequestMapping("/api/dimension")` 也保持不动(API对外稳定),仅更新 DTO 引用即可。
+
+主要变更:
+1. `upload` 端点:参数验证适配新字段名
+2. 可选:新增统计端点
+
+```java
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.DimensionUploadVO;
+import com.etotem.cfc.dto.HealthDimensionQuestionnaireVO;
+import com.etotem.cfc.dto.HealthDimensionVO;
+import com.etotem.cfc.service.DimensionScoreService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "健康维度", description = "七维健康指标查询与录入(设计文档规范版)")
+@RestController
+@RequestMapping("/api/dimension")
+public class HealthDimensionController {
+
+    @Resource
+    private DimensionScoreService dimensionScoreService;
+
+    @Operation(summary = "获取健康维度概览(7维+百分位)")
+    @PostMapping("/overview")
+    public Result<HealthDimensionVO> overview(
+            @RequestBody Map<String, Object> params,
+            @RequestAttribute("userId") Long userId) {
+        Long memberId = params.get("memberId") != null
+                ? Long.valueOf(params.get("memberId").toString()) : null;
+        if (memberId == null) return Result.error("memberId不能为空");
+        HealthDimensionVO vo = dimensionScoreService.getHealthDimension(memberId);
+        return Result.success(vo);
+    }
+
+    @Operation(summary = "手动录入维度评分")
+    @PostMapping("/upload")
+    public Result<Void> upload(
+            @RequestBody DimensionUploadVO vo,
+            @RequestAttribute("userId") Long userId) {
+        if (vo.getMemberId() == null || vo.getDimension() == null || vo.getScore() == null) {
+            return Result.error("memberId, dimension, score不能为空");
+        }
+        dimensionScoreService.uploadScore(vo);
+        return Result.success(null);
+    }
+
+    @Operation(summary = "问卷提交维度评分")
+    @PostMapping("/questionnaire")
+    public Result<Void> questionnaire(
+            @RequestBody HealthDimensionQuestionnaireVO vo,
+            @RequestAttribute("userId") Long userId) {
+        if (vo.getMemberId() == null || vo.getDimension() == null || vo.getAnswers() == null) {
+            return Result.error("memberId, dimension, answers不能为空");
+        }
+        dimensionScoreService.submitQuestionnaire(vo);
+        return Result.success(null);
+    }
+
+    @Operation(summary = "获取维度历史记录")
+    @PostMapping("/history")
+    public Result<List<HealthDimensionVO.DimensionItem>> history(
+            @RequestBody Map<String, Object> params,
+            @RequestAttribute("userId") Long userId) {
+        Long memberId = params.get("memberId") != null
+                ? Long.valueOf(params.get("memberId").toString()) : null;
+        String dimension = (String) params.get("dimension");
+        int limit = params.get("limit") != null ? Integer.parseInt(params.get("limit").toString()) : 10;
+        if (memberId == null || dimension == null) {
+            return Result.error("memberId和dimension不能为空");
+        }
+        List<HealthDimensionVO.DimensionItem> items =
+            dimensionScoreService.getDimensionHistory(memberId, dimension, limit);
+        return Result.success(items);
+    }
+}
+```
+
+- [ ] **Step 2: 编译验证**
+
+```bash
+cd /sc-data/cfc/cfc-backend && rtk mvn clean compile -q 2>&1 | tail -20
+```
+
+- [ ] **Step 3: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/HealthDimensionController.java
+git commit -m "feat(controller): update to use new DTOs and consistent 7-dim API"
+```
+
+---
+
+### Task 9: 更新前端页面
+
+**Files:**
+- Modify: `cfc-frontend/pages/body/health-dimensions.vue`
+
+- [ ] **Step 1: 更新维度选项和数据源选项**
+
+```javascript
+// 将 dimensionOptions 从旧的7维替换为新的7维:
+dimensionOptions: [
+  { code: 'growth', label: '生长发育' },
+  { code: 'sleep', label: '睡眠质量' },
+  { code: 'vision', label: '视力健康' },
+  { code: 'immunity', label: '免疫力' },
+  { code: 'nutrition', label: '营养均衡' },
+  { code: 'gut', label: '肠胃健康' },
+  { code: 'exercise', label: '运动活力' }
+],
+sourceOptions: [
+  { code: 'REPORT', label: '健康报告' },
+  { code: 'PHOTO', label: '拍照识别' },
+  { code: 'VOICE', label: '语音输入' },
+  { code: 'MANUAL', label: '手动录入' }
+]
+```
+
+- [ ] **Step 2: 更新模板中的数据显示**
+
+在维度卡片的 `dim-header` 区域旁边新增百分位显示:
+```html
+<text class="dim-percentile" v-if="dim.percentile">
+  优于 {{ dim.percentile }}% 同龄人
+</text>
+```
+
+在 `dim-footer` 区域新增数据源图标显示:
+```html
+<text class="dim-source-icon" v-if="dim.dataSource">
+  {{ dim.dataSource === 'REPORT' ? '📄' : dim.dataSource === 'PHOTO' ? '📷' : dim.dataSource === 'VOICE' ? '🎤' : '✏️' }}
+  {{ dim.dataSource }}
+</text>
+```
+
+在级别标签旁新增Tier显示:
+```html
+<text class="dim-tier" v-if="dim.tier">
+  Tier{{ dim.tier }}
+</text>
+```
+
+- [ ] **Step 3: 编译验证**
+
+```bash
+cd /sc-data/cfc/cfc-frontend && rtk npx tsc --noEmit 2>&1 | tail -10 || echo "no TS check; verifying via build"
+```
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add cfc-frontend/pages/body/health-dimensions.vue
+git commit -m "feat(frontend): update 7-dim labels and add percentile+source display"
+```
+
+---
+
+## Final Verification Wave
+
+- [ ] **F1: 编译验证**
+
+```bash
+cd /sc-data/cfc/cfc-backend && rtk mvn clean compile -q 2>&1
+cd /sc-data/cfc/cfc-frontend && rtk npx vue-cli-service build --mode production 2>&1 | tail -10
+```
+
+- [ ] **F2: API端点测试(curl)**
+
+```bash
+# 启动应用(后台)
+cd /sc-data/cfc/cfc-backend && mvn spring-boot:run > /tmp/app.log 2>&1 &
+sleep 30
+# 测试overview端点
+curl -s -X POST http://localhost:9082/api/dimension/overview \
+  -H "Content-Type: application/json" \
+  -d '{"memberId": 1}' | python3 -m json.tool
+# 测试upload端点
+curl -s -X POST http://localhost:9082/api/dimension/upload \
+  -H "Content-Type: application/json" \
+  -d '{"memberId":1,"dimension":"sleep","score":85,"sourceType":"MANUAL"}' | python3 -m json.tool
+# 测试questionnaire端点
+curl -s -X POST http://localhost:9082/api/dimension/questionnaire \
+  -H "Content-Type: application/json" \
+  -d '{"memberId":1,"dimension":"nutrition","answers":[{"questionId":"q1","answer":"A","score":80},{"questionId":"q2","answer":"B","score":70}]}' | python3 -m json.tool
+# 停应用
+kill %1
+```
+
+- [ ] **F3: 设计文档合规性检查**
+
+逐项检查:
+- [ ] 7维code是否为 `growth/sleep/vision/immunity/nutrition/gut/exercise`
+- [ ] 表结构是否有 `percentile`、`tier`、`raw_data`、`assess_date`、`expire_date`
+- [ ] `health_norm_reference` 是否为百分位结构(percentile_5~95)
+- [ ] 菌群报告上传后是否触发了 `refreshFromGutReport()`
+- [ ] 常模种子数据是否包含了所有7维
+
+- [ ] **F4: 最终提交**
+
+```bash
+git push origin cfclub
+```

+ 412 - 0
docs/superpowers/specs/2026-06-26-health-dimensions-design-spec.md

@@ -0,0 +1,412 @@
+# 身体健康7维模型设计规范
+
+> 建立「身」维度下可量化的7维健康子体系,面向家长用户,以最少的用户输入获取最精准的健康画像。
+
+**版本:** v1.0  
+**日期:** 2026-06-25  
+**状态:** Draft
+
+---
+
+## 1. 背景与目标
+
+### 1.1 现有维度体系
+
+| 体系 | 维度 | 说明 |
+|------|------|------|
+| 五维能量 | 身·心·智·行·富 | 平台顶层框架,五行相生相克 |
+| 认知(DAN评估) | 注意力/专注力/记忆力/逻辑/感知/空间/加工速度 | 6+1维 |
+| 心理(EMI) | 情绪商数/心理韧性/压力应对/自我认知 | 4维 |
+| 大五人格 | 开放性/尽责性/外向性/宜人性/神经质 | 5维 |
+| 关系 | 亲密/沟通/信任 | 3维 |
+| **「身」子维度** | **生长发育/睡眠质量/视力健康/免疫力/营养均衡/肠胃健康/运动活力** | **7维(本文档)** |
+
+### 1.2 核心设计原则
+
+1. **最少用户输入**:拍照/上传 > 语音 > 点选 > 手动输入
+2. **渐进式精度**:问卷兜底(Tier 3)→ 拍照AI解析(Tier 2)→ 自动报告(Tier 1),有高精度数据自动替代低精度
+3. **常模对标**:每个维度的评分映射到同龄同性别人群百分位
+4. **菌群报告优先复用**:已有PDF解析的指标直接映射到对应维度
+
+---
+
+## 2. 7维定义与数据源
+
+### 📈 生长发育
+
+| 项目 | 内容 |
+|------|------|
+| **用户理解** | 身高体重达标吗,胖瘦合适吗 |
+| **Tier 1(拍照/自动)** | ①体脂称照片OCR → 体重/体脂率/肌肉量/骨量 ②菌群报告 → 疾病风险(易胖/超重指标)+ Firmicutes/Bacteroidetes比值 ③体检报告OCR → 身高/体重 |
+| **Tier 2(语音)** | "8岁,身高130,体重26" → NLP提取 |
+| **Tier 3(手动)** | 输入身高、体重(年龄+性别从用户资料读取) |
+| **评分算法** | BMI-for-age百分位(中国WS/T 586-2018)+ 体脂率校正 + 历史趋势(持续增长加分) |
+| **常模来源** | 中国儿童生长发育标准曲线(卫健委) |
+
+### 🛌 睡眠质量
+
+| 项目 | 内容 |
+|------|------|
+| **用户理解** | 睡够了没,睡得好不好 |
+| **Tier 1** | ①菌群报告 → 神经递质指标(血清素/GABA水平,category="神经递质") ②手环/手表睡眠截图OCR → 深睡时长/总时长/醒夜次数 |
+| **Tier 2** | "晚上十点睡早上七点起,中间醒一次" |
+| **Tier 3** | 点选睡眠时长区间(<6h / 6-8h / 8-10h / >10h)+ 点选质量(好/一般/差) |
+| **评分算法** | 时长分(年龄推荐时长±1h内满分)+ 质量调整因子 |
+| **常模来源** | 同年龄睡眠需求推荐值(美国国家睡眠基金会/AASM) |
+
+### 👁️ 视力健康
+
+| 项目 | 内容 |
+|------|------|
+| **用户理解** | 近视了没,度数涨了没 |
+| **Tier 1** | ①视力检查单/验光单拍照OCR → 左右眼裸眼视力/矫正视力/屈光度 |
+| **Tier 2** | "左眼5.0,右眼4.8" 或 "左眼100度,右眼150度" |
+| **Tier 3** | 点选是否戴镜 + 输入视力值 |
+| **评分算法** | 裸眼视力值映射分 + 屈光度变化趋势(稳定加分/加深扣分)+ 每日屏幕时间风险因子 |
+| **常模来源** | 儿童视力发育标准(各年龄正常视力下限) |
+
+### 🛡️ 免疫力
+
+| 项目 | 内容 |
+|------|------|
+| **用户理解** | 老不老实生病 |
+| **Tier 1** | ①菌群报告 → 肠道屏障指标 + 抗生素耐药指标 + 有益菌水平 ②血常规照片OCR → 白细胞/中性粒细胞/淋巴细胞/免疫球蛋白 |
+| **Tier 2** | "今年感冒了3次,没得过肺炎" |
+| **Tier 3** | 点选半年生病次数(0次/1-2次/3-5次/6次以上)+ 点选严重程度(从未住院/住院1次/住院多次) |
+| **评分算法** | 基础分=年龄常模基线 → 生病频率调整(越多越低)→ 菌群屏障加成 → 血常规校正 |
+| **常模来源** | 各年龄段年患病次数参考值 |
+
+### 🥗 营养均衡
+
+| 项目 | 内容 |
+|------|------|
+| **用户理解** | 偏不偏食,缺不缺营养 |
+| **Tier 1** | ①菌群报告 → 营养均衡分 + 微量指标(维生素D/铁/锌/钙等)+ 营养缺乏记录 ②饭菜拍照 → AI识别菜品 → 膳食多样性评分 |
+| **Tier 2** | "不爱吃青菜,肉还行,牛奶每天喝" |
+| **Tier 3** | 3题快速选择:挑食程度(不挑/挑几样/严重挑食)+ 每周食材种类数 + 补充剂使用 |
+| **评分算法** | 菌群微营养指标得分 × 0.6 + 膳食多样性得分 × 0.4(仅有问卷时=问卷分×0.8+基线×0.2) |
+| **常模来源** | 各年龄段推荐营养素摄入量(DRIs) |
+
+### 🌿 肠胃健康
+
+| 项目 | 内容 |
+|------|------|
+| **用户理解** | 肠胃舒服吗,拉得好吗 |
+| **Tier 1** | ①菌群报告 → **最完整覆盖**:健康度/平衡分/多样性/有益菌/有害菌/核心菌属/SCFA/肠型/肠道年龄 ②舌诊照片 → AI分析舌苔厚薄/舌色/齿痕 → 消化功能状态 |
+| **Tier 2** | "最近大便偏稀,一周拉4次,偶尔肚子疼" |
+| **Tier 3** | 点选消化舒适度(好/一般/差,含解释emoji辅助) |
+| **评分算法** | 菌群综合分(多指标加权)+ 舌诊校正因子 |
+| **常模来源** | 菌群报告自带 populationLevel + detectionRate(人群百分位) |
+
+### 🏃 运动活力
+
+| 项目 | 内容 |
+|------|------|
+| **用户理解** | 运动够不够,体能怎么样 |
+| **Tier 1** | ①微信运动授权 → 日均步数 ②学校体测报告拍照OCR → 跳绳/50米跑/坐位体前屈等成绩(对照国家学生体质健康标准) |
+| **Tier 2** | "每周两节体育课,周末踢一小时球" |
+| **Tier 3** | 点选运动频率(几乎不/每周1-2次/3-5次/每天)+ 点选运动强度(轻度/中等/剧烈) |
+| **评分算法** | 运动量(MET-min/周)映射分 + 国家体测标准分(如有) |
+| **常模来源** | 国家学生体质健康标准(2023版)+ 各年龄推荐运动量(WHO) |
+
+---
+
+## 3. 数据源优先级与映射规则
+
+### 3.1 全局规则
+
+```
+IF 存在 Tier 1 数据 THEN 使用 Tier 1 替代所有低层数据
+IF 仅有 Tier 2 数据 THEN 使用 NLP 提取值,置信度低于0.6时降级为 Tier 3
+IF 无 Tier 1/2 数据 THEN 使用 Tier 3(问卷/手动),标注"快速评估"标识
+```
+
+### 3.2 菌群报告 → 7维映射矩阵
+
+| 菌群报告字段 | 生长发育 | 睡眠质量 | 视力健康 | 免疫力 | 营养均衡 | 肠胃健康 | 运动活力 |
+|-------------|:-------:|:-------:|:-------:|:-----:|:-------:|:-------:|:-------:|
+| gutHealthScore | | | | ✅ | | ✅ | |
+| balanceScore | | | | | | ✅ | |
+| diversityScore | | | | ✅ | | ✅ | |
+| nutritionScore | | | | | ✅ | | |
+| chronicDiseaseScore | ✅ | | | | ✅ | | |
+| beneficialBacteriaScore | | | | ✅ | | ✅ | |
+| harmfulBacteriaScore | | | | ✅ | | ✅ | |
+| coreGenusScore | | | | | | ✅ | |
+| gutAge | | ✅ | | | | ✅ | |
+| gutBalanceScore | | | | | | ✅ | |
+| gutDiversityScore | | | | ✅ | | ✅ | |
+| 神经递质(indicators) | | ✅ | | | | | |
+| 肠道屏障(indicators) | | | | ✅ | | ✅ | |
+| 短链脂肪酸(indicators) | | | | | ✅ | ✅ | |
+| 疾病风险(diseaseRisks) | ✅ | | | ✅ | | | |
+| 菌种丰度(populationLevel) | ✅ | | | ✅ | ✅ | ✅ | |
+| 微量指标(维生素/矿物质) | | | | | ✅ | | |
+| 抗生素耐药 | | | | ✅ | | | |
+
+### 3.3 维度分数聚合
+
+```
+纬度最终分 = Σ(各数据源分 × 权重) × 年龄段常模校正因子
+
+权重说明:
+- 有菌群报告时:菌群指标权重占 0.4-0.7,其他数据源占 0.3-0.6
+- 无菌群报告时:问卷/语音权重占 0.7-1.0,基线常模占 0-0.3
+
+常模校正:
+- 原始分 → 查同性别同年龄百分位 → 输出 1-100 分(50 = 人群中位)
+- 百分位≥85 → 优秀(绿色)
+- 百分位50-84 → 良好(蓝色)
+- 百分位15-49 → 一般(黄色)
+- 百分位<15 → 关注(橙色/红色)
+```
+
+---
+
+## 4. 架构设计
+
+### 4.1 模块划分
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│                    Controller 层                              │
+│  HealthDimensionController                                    │
+│  POST /api/health/dimensions/{memberId} — 获取7维评分        │
+│  POST /api/health/dimensions/{memberId}/upload — 上传数据源  │
+│     body: { type: "report"|"photo"|"voice", file/data }     │
+└──────────────────────┬──────────────────────────────────────┘
+                       │
+┌──────────────────────▼──────────────────────────────────────┐
+│                   Service 层                                 │
+│                                                              │
+│  DimensionScoreService     ← 核心聚合器                       │
+│  ├─ getDimensions(memberId) → 7维评分 + 常模百分位            │
+│  ├─ refresh(memberId, dataSource) → 监听到新数据,重算        │
+│  └─ compareTrend(memberId, months) → 历史趋势                 │
+│                                                              │
+│  ┌────────────┬───────────┬───────────┬──────────────────┐   │
+│  │ GrowScore  │ SleepScore│ VisionScore│ ImmunityScore   │   │
+│  │ Service    │ Service   │ Service   │ Service          │   │
+│  ├────────────┼───────────┼───────────┼──────────────────┤   │
+│  │ Nutrition  │ GutScore  │ Exercise  │                  │   │
+│  │ ScoreSvc   │ Service   │ ScoreSvc  │                  │   │
+│  └────────────┴───────────┴───────────┴──────────────────┘   │
+│                                                              │
+│  外部依赖 Service:                                            │
+│  ├─ HealthReportService  (菌群报告)                          │
+│  ├─ OcrService           (照片OCR)                           │
+│  ├─ VoiceInputService    (语音→NLP)                          │
+│  ├─ TongueDiagnosisService (舌诊AI)                          │
+│  └─ WechatStepService    (微信运动)                          │
+└──────────────────────┬──────────────────────────────────────┘
+                       │
+┌──────────────────────▼──────────────────────────────────────┐
+│                  Entity / Mapper 层                           │
+│                                                              │
+│  HealthDimensionScore ← 核心实体                              │
+│  ├─ id, memberId, dimension (code), score (0-100)            │
+│  ├─ percentile (0-100), dataSource (枚举: REPORT/PHOTO/...)  │
+│  ├─ tier (1/2/3), assessDate, expireDate                     │
+│  └─ rawData (JSON) — 保留原始输入用于追溯                     │
+│                                                              │
+│  DataSourceRecord ← 数据源记录                                │
+│  ├─ id, memberId, sourceType (PDF/PHOTO/VOICE/MANUAL)        │
+│  ├─ dimensionCodes (影响的维度列表)                           │
+│  └─ parsedResult (JSON) — OCR/解析后的结构化结果              │
+└─────────────────────────────────────────────────────────────┘
+```
+
+### 4.2 核心实体设计
+
+#### HealthDimensionScore
+
+```sql
+CREATE TABLE health_dimension_scores (
+    id              BIGINT AUTO_INCREMENT PRIMARY KEY,
+    member_id       BIGINT       NOT NULL COMMENT '家庭成员ID',
+    dimension       VARCHAR(20)  NOT NULL COMMENT '维度: growth/sleep/vision/immunity/nutrition/gut/exercise',
+    score           INT          NOT NULL COMMENT '0-100分',
+    percentile      INT          COMMENT '人群百分位 0-100',
+    data_source     VARCHAR(20)  NOT NULL COMMENT '数据源: REPORT/PHOTO/VOICE/MANUAL',
+    tier            TINYINT      NOT NULL DEFAULT 3 COMMENT '数据层级: 1自动/2语音/3手动',
+    raw_data        JSON         COMMENT '原始数据快照',
+    assess_date     DATE         NOT NULL COMMENT '评估日期',
+    expire_date     DATE         COMMENT '数据过期日(Tier1长/Tier3短)',
+    created_at      DATETIME,
+    updated_at      DATETIME,
+    UNIQUE KEY uk_member_dim_date (member_id, dimension, assess_date)
+);
+```
+
+#### DataSourceRecord
+
+```sql
+CREATE TABLE health_data_sources (
+    id              BIGINT AUTO_INCREMENT PRIMARY KEY,
+    member_id       BIGINT       NOT NULL,
+    source_type     VARCHAR(20)  NOT NULL COMMENT 'PDF/PHOTO/VOICE/MANUAL/WEIXIN_STEP',
+    dimension_codes JSON         NOT NULL COMMENT '影响的维度列表',
+    parsed_result   JSON         COMMENT '解析结果全量',
+    file_url        VARCHAR(500) COMMENT '原始文件URL',
+    confidence      DECIMAL(3,2) COMMENT 'AI解析置信度 0-1',
+    created_at      DATETIME,
+    INDEX idx_member_source (member_id, source_type)
+);
+```
+
+### 4.3 维度分数生命周期
+
+```
+用户上传数据源
+     │
+     ▼
+解析层(OcrService / PdfParseService / VoiceInputService / TongueDiagnosisService)
+     │
+     ▼ 返回结构化原始数据
+DimensionScoreService.refresh(memberId, dataSource)
+     │
+     ├─ 读取该dataSource影响的所有维度
+     ├─ 对每个维度:读取所有活跃数据源
+     ├─ 按Tier优先级合并(Tier1覆盖Tier2/3)
+     ├─ 计算加权得分
+     ├─ 查常模表得到百分位
+     └─ 写入/更新 health_dimension_scores
+
+用户查看7维面板
+     │
+     ▼
+POST /api/health/dimensions/{memberId}
+     │
+     ├─ 取各维度最新 assess_date 的记录
+     ├─ 有expire_date且未过期 → 直接返回
+     ├─ 有过期 → 提示用户补充数据,同时返回过期数据(标注)
+     └─ 无数据 → 进入Tier 3(问卷流程)
+```
+
+---
+
+## 5. 常模系统
+
+### 5.1 常模数据结构
+
+```sql
+CREATE TABLE health_norm_reference (
+    id              BIGINT AUTO_INCREMENT PRIMARY KEY,
+    dimension       VARCHAR(20)  NOT NULL COMMENT '维度',
+    gender          VARCHAR(10)  NOT NULL COMMENT 'male/female/all',
+    age_min         INT          NOT NULL COMMENT '年龄下限(月)',
+    age_max         INT          NOT NULL COMMENT '年龄上限(月)',
+    percentile_5    INT,
+    percentile_15   INT,
+    percentile_25   INT,
+    percentile_50   INT  COMMENT '中位值',
+    percentile_75   INT,
+    percentile_85   INT,
+    percentile_95   INT,
+    source          VARCHAR(50)  COMMENT '常模来源: WHO/卫健委/平台自建',
+    UNIQUE KEY uk_dim_gender_age (dimension, gender, age_min, age_max)
+);
+```
+
+### 5.2 常模优先级
+
+1. **中国官方标准**(BMI-for-age WS/T 586、学生体质健康标准、血压百分位)
+2. **菌群报告人群数据**(已有 populationLevel → 平台积累后直接复用)
+3. **国际权威标准**(WHO生长曲线、睡眠时长推荐)
+4. **平台自建常模**(运营一段后基于用户数据建立自有百分位)
+
+### 5.3 菌群报告已有常模数据的利用
+
+`HealthGutFlora.populationLevel` 字段(如 "35%")和 `detectionRate`(如 "98.56%")已经是现成的常模数据。可直接用于:
+- 肠胃健康维度的评分校准
+- 免疫力维度的部分指标校准
+- 营养均衡维度的微营养指标参考
+
+策略:菌群报告解析时,populationLevel 直接作为该指标在人群中的百分位使用,无需二次查表。
+
+---
+
+## 6. 分期实现计划
+
+### Phase 1 — 核心框架 + 问卷兜底(MVP)
+
+**目标:7维全部可评估,所有用户可使用**
+
+- 创建 `HealthDimensionScore` / `DataSourceRecord` / `health_norm_reference` 表
+- `DimensionScoreService` 聚合逻辑(Tier 3 问卷兜底)
+- `HealthDimensionController` — 获取7维 / 手动输入问卷
+- 前端:7维雷达图页面 + 每个维度的简单问卷(3-5题点选)
+- 已有菌群报告数据直接映射(`PdfParseService` 解析结果 → 对应维度)
+- 部署常模种子数据(BMI/体测等国家标准)
+
+### Phase 2 — 拍照OCR
+
+**目标:替代手动输入,减少用户操作**
+
+- `OcrService` 通用引擎(支持体检报告/体测单/血常规拍照)
+- 体脂称照片OCR(显示屏幕 → 数字识别)
+- 舌诊照片AI分析(舌色/舌苔/齿痕 → 肠胃/免疫/睡眠推论)
+- `VoiceInputService`(语音→ASR→NLP指标提取)
+- 后端整合Tier1/2→覆盖Tier3
+
+### Phase 3 — 自动接入
+
+**目标:最低用户操作**
+
+- 微信运动授权接入(步数→运动活力)
+- 手环截图自动识别
+- 历史数据趋势图(各维度3/6/12个月变化)
+- 健康建议引擎(各维度低分→个性化改善建议,关联商城营养品)
+
+---
+
+## 7. 用户界面示意(文字描述)
+
+### 7.1 7维健康雷达图
+
+主页面显示一个7边形雷达图,每个顶点标注维度名称和分数(0-100),中心为常模中位线(50分)。
+
+颜色编码:
+- ≥85 分深绿 — 🟢 优秀
+- 60-84 分浅绿 — 良好
+- 40-59 分黄色 — ⚠️ 一般
+- <40 分橙色/红 — 🔴 关注
+
+### 7.2 数据源提示
+
+每个维度下方显示当前数据源等级图标:
+- 📄 菌群报告 / 🏥 体检报告(Tier 1 自动)
+- 📷 照片识别(Tier 2 拍照)
+- 🎤 语音输入(Tier 2 语音)
+- ✏️ 手动填写(Tier 3 手动)
+- 点击可查看原始数据和到期时间
+
+### 7.3 数据补充引导
+
+当某维度数据过期或从未采集时:
+- 入口文案:"拍张体检单,自动获取{维度名}评估"
+- 或语音按钮:"说句话,AI帮你记录"
+- 或点选入口:3-5题快速问卷
+
+---
+
+## 8. 待确认问题
+
+- [ ] 舌诊AI方案:自研基于颜色/纹理分类 vs 接入第三方API(如百度AI舌诊)
+- [ ] 体脂称:支持哪些品牌的屏幕布局,是否需要通用OCR适配层
+- [ ] 语音模型:用微信内置语音识别还是自建ASR
+- [ ] 常模数据:BMI百分位表是硬编码在代码中还是做成可配置表
+
+---
+
+## 9. 附录:7维与五维能量「身」的衔接
+
+```
+五维「身」能量值 = Σ(7维子维度分 × 权重)
+                    其中权重可配置(默认等权 1/7)
+
+衔接逻辑:
+- 用户首次进入 → 引导上传菌群报告/拍照 → 自动生成7维+「身」能量
+- 已有旧数据 → 历史迁移:菌群报告现有字段直接映射,缺的维度走问卷
+- 实时联动 → 任一子维度更新 → 自动重算「身」能量(已有 EnergyService)
+```

+ 10 - 0
web/index.html

@@ -15,6 +15,16 @@
   <meta name="theme-color" content="#4A9BD7">
   <title>亿图腾 · 浠艾福 — 家庭幸福健康成长平台</title>
   <link rel="stylesheet" href="css/style.css">
+
+  <!-- ========== Clarity (Microsoft) - User Behavior Analytics ========== -->
+  <script>
+    (function(c,l,a,r,i,t,y){
+      c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
+      t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
+      y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
+    })(window, document, "clarity", "script", "xbtkn3d56w");
+  </script>
+
   <script type="application/ld+json">
   {
     "@context": "https://schema.org",

+ 84 - 1
web/js/main.js

@@ -538,5 +538,88 @@ document.addEventListener('DOMContentLoaded', () => {
       ro.observe(wuxingContainer);
     }
   }
-
+  
 });
+
+// --------------------------------------------------------
+// 9. User behavior analytics — Clarity custom events
+// --------------------------------------------------------
+(function() {
+  var SCROLL_DEPTHS = [25, 50, 75, 90, 100];
+  var reportedDepths = {};
+
+  function trackScrollDepth() {
+    var docHeight = document.documentElement.scrollHeight - window.innerHeight;
+    if (docHeight <= 0) return;
+    var scrolled = Math.round((window.scrollY / docHeight) * 100);
+
+    SCROLL_DEPTHS.forEach(function(depth) {
+      if (scrolled >= depth && !reportedDepths[depth]) {
+        reportedDepths[depth] = true;
+        if (window.clarity) {
+          window.clarity('event', 'scroll_depth', { depth: depth + '%' });
+        }
+      }
+    });
+  }
+
+  var scrollTick = false;
+  window.addEventListener('scroll', function() {
+    if (!scrollTick) {
+      requestAnimationFrame(function() {
+        trackScrollDepth();
+        scrollTick = false;
+      });
+      scrollTick = true;
+    }
+  }, { passive: true });
+
+  document.addEventListener('click', function(e) {
+    var target = e.target.closest('a, button');
+    if (!target) return;
+
+    var eventData = {};
+    if (target.tagName === 'A') {
+      var href = target.getAttribute('href') || '';
+      var text = (target.textContent || '').trim().slice(0, 40);
+      if (href.startsWith('#')) {
+        eventData = { type: 'anchor', target: href, text: text };
+      } else if (href.startsWith('http') || href.startsWith('//')) {
+        eventData = { type: 'outbound', target: href, text: text };
+      } else {
+        eventData = { type: 'internal', target: href, text: text };
+      }
+      if (target.getAttribute('target') === '_blank') {
+        eventData.external = true;
+      }
+    }
+    if (target.tagName === 'BUTTON') {
+      eventData = { type: 'button', text: (target.textContent || '').trim().slice(0, 40) };
+    }
+    if (window.clarity && eventData.type) {
+      window.clarity('event', 'cta_click', eventData);
+    }
+  });
+
+  var DWELL_INTERVAL = 30000;
+  var MAX_DWELL_BEATS = 10;
+  var dwellCount = 0;
+  var dwellTimer = setInterval(function() {
+    dwellCount++;
+    if (window.clarity) {
+      window.clarity('event', 'dwell', { seconds: dwellCount * 30 });
+    }
+    if (dwellCount >= MAX_DWELL_BEATS) {
+      clearInterval(dwellTimer);
+    }
+  }, DWELL_INTERVAL);
+
+  if (window.clarity) {
+    window.clarity('event', 'session_info', {
+      width: screen.width,
+      height: screen.height,
+      referrer: document.referrer || '(direct)'
+    });
+  }
+})();
+