Преглед изворни кода

feat: 新增Cart/SystemConfig模块 + 家庭角色DTO/组件 + 设计文档
- 新增购物车实体/CartItemDTO/CartMapper/CartService
- 新增系统配置实体/SystemConfigMapper/SystemConfigService
- 新增家庭角色相关DTO(Create/Info/Update) + NutritionProfileService
- 新增 FamilyRelationshipSection 前端组件
- 新增四维度首页及家庭成员角色系统重构方案文档

User пре 2 месеци
родитељ
комит
22d7b6019f

+ 16 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/CartItemDTO.java

@@ -0,0 +1,16 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class CartItemDTO {
+    private Long id;
+    private Long userId;
+    private Long productId;
+    private Integer quantity;
+    private String productName;
+    private BigDecimal productPrice;
+    private String coverImage;
+}

+ 15 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/CreateFamilyMemberDTO.java

@@ -0,0 +1,15 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+@Data
+public class CreateFamilyMemberDTO {
+    private String nickname;
+    private String phone;
+    private String avatar;
+    private String gender;
+    private String birthday;
+    private Integer age;
+    private String relationshipType;
+    private String roleOverride;
+}

+ 22 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyMemberInfoDTO.java

@@ -0,0 +1,22 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class FamilyMemberInfoDTO {
+    private Long id;
+    private String nickname;
+    private Integer age;
+    private Date birthday;
+    private String gender;
+    private String phone;
+    private String avatar;
+    private String relationshipType;
+    private String relationshipTypeName;
+    private String roleOverride;
+    private String effectiveRole;
+    private Integer totalPoints;
+    private Integer showToFamily;
+}

+ 17 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/UpdateFamilyMemberDTO.java

@@ -0,0 +1,17 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+@Data
+public class UpdateFamilyMemberDTO {
+    private Long familyMemberId;
+    private String nickname;
+    private String phone;
+    private String avatar;
+    private String gender;
+    private String birthday;
+    private Integer age;
+    private String relationshipType;
+    private String roleOverride;
+    private Integer showToFamily;
+}

+ 21 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/Cart.java

@@ -0,0 +1,21 @@
+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.time.LocalDateTime;
+
+@Data
+@TableName("cart")
+public class Cart implements Serializable {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long userId;
+    private Long productId;
+    private Integer quantity;
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
+}

+ 29 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/SystemConfig.java

@@ -0,0 +1,29 @@
+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("system_config")
+public class SystemConfig implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 配置键 */
+    private String configKey;
+
+    /** 配置值 */
+    private String configValue;
+
+    /** 说明 */
+    private String description;
+
+    private Date createdAt;
+    private Date updatedAt;
+}

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/CartMapper.java

@@ -0,0 +1,7 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.Cart;
+
+public interface CartMapper extends BaseMapper<Cart> {
+}

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/SystemConfigMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.SystemConfig;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface SystemConfigMapper extends BaseMapper<SystemConfig> {
+}

+ 200 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberNutritionProfileService.java

@@ -0,0 +1,200 @@
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.entity.FamilyMember;
+import com.etotem.cfc.entity.HealthCheckin;
+import com.etotem.cfc.entity.HealthReport;
+import com.etotem.cfc.entity.NutritionDeficiencyRecord;
+import com.etotem.cfc.entity.UserNutritionProfile;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+public class FamilyMemberNutritionProfileService {
+
+    @Resource
+    private HealthReportService healthReportService;
+
+    @Resource
+    private HealthCheckinService healthCheckinService;
+
+    @Resource
+    private NutritionDeficiencyService nutritionDeficiencyService;
+
+    @Resource
+    private UserNutritionProfileService userNutritionProfileService;
+
+    @Resource
+    private FamilyMemberMapper familyMemberMapper;
+
+    public Map<String, Object> getProfile(Long childId) {
+        Map<String, Object> profile = new HashMap<>();
+
+        FamilyMember child = familyMemberMapper.selectById(childId);
+        if (child == null) {
+            return profile;
+        }
+        profile.put("childId", childId);
+        profile.put("nickname", child.getNickname());
+        profile.put("age", child.getAge());
+        profile.put("gender", child.getGender());
+
+        HealthReport latestReport = healthReportService.getLatestReport(childId);
+        profile.put("latestReport", latestReport);
+        if (latestReport != null) {
+            profile.put("indicators", healthReportService.getReportIndicators(latestReport.getId()));
+            List<NutritionDeficiencyRecord> records = nutritionDeficiencyService.getDeficiencyRecords(childId, latestReport.getId());
+            if (records == null || records.isEmpty()) {
+                records = nutritionDeficiencyService.analyzeDeficiency(childId, latestReport.getId());
+            }
+            profile.put("deficiencyRecords", records);
+        } else {
+            profile.put("indicators", Collections.emptyList());
+            profile.put("deficiencyRecords", Collections.emptyList());
+        }
+
+        List<HealthReport> reports = healthReportService.getUserReports(childId);
+        profile.put("reportCount", reports != null ? reports.size() : 0);
+        profile.put("reportTimeline", reports);
+
+        Map<String, Object> checkinSummary = buildCheckinSummary(childId);
+        profile.put("checkinSummary", checkinSummary);
+
+        UserNutritionProfile nutritionProfile = userNutritionProfileService.getByUserId(childId);
+        profile.put("nutritionProfile", nutritionProfile);
+
+        return profile;
+    }
+
+    public Map<String, Object> getDeficiencySummary(Long childId) {
+        Map<String, Object> summary = new HashMap<>();
+
+        List<NutritionDeficiencyRecord> allRecords = nutritionDeficiencyService.getDeficiencyRecords(childId, null);
+        summary.put("total", allRecords.size());
+
+        Map<String, List<NutritionDeficiencyRecord>> byType = new HashMap<>();
+        byType.put("deficiency", new ArrayList<>());
+        byType.put("excess", new ArrayList<>());
+        byType.put("borderline", new ArrayList<>());
+
+        for (NutritionDeficiencyRecord r : allRecords) {
+            String type = r.getDeficiencyType();
+            if (byType.containsKey(type)) {
+                byType.get(type).add(r);
+            }
+        }
+
+        summary.put("deficiencyCount", byType.get("deficiency").size());
+        summary.put("excessCount", byType.get("excess").size());
+        summary.put("borderlineCount", byType.get("borderline").size());
+
+        Map<String, List<Map<String, Object>>> nutrientsByType = new HashMap<>();
+        for (Map.Entry<String, List<NutritionDeficiencyRecord>> entry : byType.entrySet()) {
+            List<Map<String, Object>> nutrientList = new ArrayList<>();
+            for (NutritionDeficiencyRecord r : entry.getValue()) {
+                Map<String, Object> item = new HashMap<>();
+                item.put("nutrient", r.getNutrient());
+                item.put("severity", r.getSeverity());
+                item.put("symptoms", r.getSymptoms());
+                item.put("foodSuggestions", r.getFoodSuggestions());
+                item.put("indicatorName", r.getIndicatorName());
+                item.put("indicatorValue", r.getIndicatorValue());
+                item.put("refRange", r.getRefRange());
+                nutrientList.add(item);
+            }
+            nutrientsByType.put(entry.getKey(), nutrientList);
+        }
+        summary.put("nutrientsByType", nutrientsByType);
+
+        Set<String> allFoodSuggestions = new LinkedHashSet<>();
+        for (NutritionDeficiencyRecord r : allRecords) {
+            if (r.getFoodSuggestions() != null) {
+                for (String food : r.getFoodSuggestions().split("[,,]")) {
+                    String trimmed = food.trim();
+                    if (!trimmed.isEmpty()) {
+                        allFoodSuggestions.add(trimmed);
+                    }
+                }
+            }
+        }
+        summary.put("allFoodSuggestions", new ArrayList<>(allFoodSuggestions));
+
+        return summary;
+    }
+
+    public Map<String, Object> getReportTrendData(Long childId) {
+        Map<String, Object> trend = new HashMap<>();
+
+        List<HealthReport> reports = healthReportService.getUserReports(childId);
+        if (reports == null || reports.isEmpty()) {
+            trend.put("dates", Collections.emptyList());
+            trend.put("scores", Collections.emptyList());
+            return trend;
+        }
+
+        List<HealthReport> sorted = new ArrayList<>(reports);
+        sorted.sort((a, b) -> {
+            if (a.getReportDate() == null) return 1;
+            if (b.getReportDate() == null) return -1;
+            return a.getReportDate().compareTo(b.getReportDate());
+        });
+
+        List<String> dates = new ArrayList<>();
+        List<Integer> scores = new ArrayList<>();
+        java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
+
+        for (HealthReport r : sorted) {
+            dates.add(r.getReportDate() != null ? sdf.format(r.getReportDate()) : "");
+            scores.add(r.getOverallScore() != null ? r.getOverallScore() : 0);
+        }
+
+        trend.put("dates", dates);
+        trend.put("scores", scores);
+
+        Map<String, List<Integer>> subScores = new HashMap<>();
+        List<Integer> nutritionScores = new ArrayList<>();
+        List<Integer> gutScores = new ArrayList<>();
+        for (HealthReport r : sorted) {
+            nutritionScores.add(r.getNutritionScore() != null ? r.getNutritionScore() : 0);
+            gutScores.add(r.getGutHealthScore() != null ? r.getGutHealthScore() : 0);
+        }
+        subScores.put("nutrition", nutritionScores);
+        subScores.put("gutHealth", gutScores);
+        trend.put("subScores", subScores);
+
+        return trend;
+    }
+
+    private Map<String, Object> buildCheckinSummary(Long childId) {
+        Map<String, Object> summary = new HashMap<>();
+        List<HealthCheckin> checkins = healthCheckinService.getCheckins(childId, childId, null);
+        long totalCheckins = checkins != null ? checkins.size() : 0;
+        summary.put("totalCheckins", totalCheckins);
+
+        long recent30Days = 0;
+        Calendar cal = Calendar.getInstance();
+        cal.add(Calendar.DAY_OF_MONTH, -30);
+        Date thirtyDaysAgo = cal.getTime();
+        if (checkins != null) {
+            for (HealthCheckin c : checkins) {
+                if (c.getCheckinDate() != null && c.getCheckinDate().after(thirtyDaysAgo)) {
+                    recent30Days++;
+                }
+            }
+        }
+        summary.put("recent30Days", recent30Days);
+
+        if (totalCheckins > 0) {
+            summary.put("streakDays", Math.min((int) totalCheckins, 30));
+        } else {
+            summary.put("streakDays", 0);
+        }
+
+        return summary;
+    }
+}

+ 40 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/SystemConfigService.java

@@ -0,0 +1,40 @@
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.entity.SystemConfig;
+import com.etotem.cfc.mapper.SystemConfigMapper;
+import org.springframework.stereotype.Service;
+import javax.annotation.Resource;
+
+@Service
+public class SystemConfigService {
+
+    private static final int DEFAULT_CHILD_AGE = 18;
+    private static final int DEFAULT_ELDERLY_AGE = 65;
+
+    @Resource
+    private SystemConfigMapper systemConfigMapper;
+
+    /**
+     * 获取未成年年龄阈值,默认18岁
+     */
+    public int getChildAgeThreshold() {
+        SystemConfig cfg = systemConfigMapper.selectOne(
+            new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SystemConfig>()
+                .eq(SystemConfig::getConfigKey, "child_age_threshold"));
+        return cfg != null && cfg.getConfigValue() != null
+            ? Integer.parseInt(cfg.getConfigValue())
+            : DEFAULT_CHILD_AGE;
+    }
+
+    /**
+     * 获取老年年龄阈值,默认65岁
+     */
+    public int getElderlyAgeThreshold() {
+        SystemConfig cfg = systemConfigMapper.selectOne(
+            new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SystemConfig>()
+                .eq(SystemConfig::getConfigKey, "elderly_age_threshold"));
+        return cfg != null && cfg.getConfigValue() != null
+            ? Integer.parseInt(cfg.getConfigValue())
+            : DEFAULT_ELDERLY_AGE;
+    }
+}

+ 316 - 0
cfc-frontend/components/FamilyRelationshipSection.vue

@@ -0,0 +1,316 @@
+<template>
+  <view class="family-relationship-wrapper">
+    <!-- ===== 重要关系维护 ===== -->
+    <view class="contact-section" v-if="isLoggedIn">
+    <view class="contact-header">
+      <text class="contact-section-title">重要关系</text>
+      <view class="contact-header-right">
+        <text class="contact-more" @click="$emit('refresh')">刷新</text>
+        <text class="contact-import-btn" @click="$emit('import')">+ 导入</text>
+      </view>
+    </view>
+    <scroll-view class="contact-scroll" scroll-x enable-flex>
+      <view class="contact-scroll-inner">
+        <view class="contact-card-wrap" v-for="item in contactList" :key="item.id" @click="$emit('contactClick', item)">
+          <ContactCard :contact="item" />
+        </view>
+        <view class="contact-card-wrap contact-add-card" @click="$emit('import')">
+          <view class="add-card-inner">
+            <text class="add-card-icon">+</text>
+            <text class="add-card-label">导入通讯录</text>
+          </view>
+        </view>
+      </view>
+    </scroll-view>
+    <view class="contact-empty" v-if="contactList.length === 0">
+      <text class="contact-empty-text">暂无重要联系人,点击导入添加</text>
+    </view>
+  </view>
+
+  <!-- ===== 关系健康概览 ===== -->
+  <view class="rq-section" v-if="isLoggedIn && (healthAlerts.length > 0 || milestones.length > 0)">
+    <view class="rq-header">
+      <text class="rq-title">关系健康</text>
+      <text class="rq-more" @click="$emit('questionnaire')">去做问卷</text>
+    </view>
+    <!-- 健康预警列表 -->
+    <view class="rq-alerts" v-if="healthAlerts.length > 0">
+      <view class="rq-alert-card" v-for="alert in healthAlerts" :key="alert.memberId" @click="$emit('interactionLog', alert.memberId)">
+        <view class="rq-alert-icon">
+          <text class="rq-alert-emoji">⚠️</text>
+        </view>
+        <view class="rq-alert-info">
+          <text class="rq-alert-name">{{ alert.memberName }}</text>
+          <text class="rq-alert-desc">已{{ alert.daysSinceInteraction }}天无互动</text>
+        </view>
+        <view class="rq-alert-action">
+          <text class="rq-alert-btn">去互动</text>
+        </view>
+      </view>
+    </view>
+    <!-- 里程碑列表 -->
+    <view class="rq-milestones" v-if="milestones.length > 0">
+      <view class="rq-milestone-card" v-for="m in milestones" :key="m.memberId">
+        <view class="rq-milestone-icon">
+          <text class="rq-milestone-emoji">🎂</text>
+        </view>
+        <view class="rq-milestone-info">
+          <text class="rq-milestone-name">{{ m.memberName }}</text>
+          <text class="rq-milestone-desc">{{ m.daysUntil > 0 ? m.daysUntil + '天后生日' : '今天生日' }}</text>
+        </view>
+      </view>
+    </view>
+    <!-- 快捷操作 -->
+    <view class="rq-quick-actions">
+      <view class="rq-action-btn" @click="$emit('interactionLog')">
+        <text class="rq-action-icon">📝</text>
+        <text class="rq-action-label">记录互动</text>
+      </view>
+      <view class="rq-action-btn" @click="$emit('questionnaire')">
+        <text class="rq-action-icon">🤝</text>
+        <text class="rq-action-label">做关系问卷</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  props: {
+    contactList: { type: Array, default: function() { return [] } },
+    healthAlerts: { type: Array, default: function() { return [] } },
+    milestones: { type: Array, default: function() { return [] } },
+    isLoggedIn: { type: Boolean, default: false }
+  }
+}
+</script>
+
+<style scoped>
+/* ===== 重要关系 ===== */
+.contact-section {
+  margin: 20rpx 20rpx 0;
+}
+.contact-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 16rpx;
+}
+.contact-section-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+}
+.contact-header-right {
+  display: flex;
+  gap: 16rpx;
+  align-items: center;
+}
+.contact-more {
+  font-size: 24rpx;
+  color: #999;
+}
+.contact-import-btn {
+  font-size: 24rpx;
+  color: #F97316;
+  font-weight: 500;
+}
+.contact-scroll {
+  white-space: nowrap;
+  overflow: hidden;
+}
+.contact-scroll-inner {
+  display: flex;
+  flex-direction: row;
+  gap: 16rpx;
+  padding: 8rpx 0 16rpx;
+}
+.contact-card-wrap {
+  flex-shrink: 0;
+  width: 220rpx;
+}
+.contact-card-wrap:active {
+  opacity: 0.8;
+}
+.contact-add-card {
+  width: 160rpx;
+}
+.add-card-inner {
+  width: 160rpx;
+  height: 280rpx;
+  background: #fff;
+  border-radius: 16rpx;
+  border: 2rpx dashed #ddd;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 12rpx;
+}
+.add-card-icon {
+  font-size: 48rpx;
+  color: #ccc;
+}
+.add-card-label {
+  font-size: 22rpx;
+  color: #999;
+}
+.contact-empty {
+  padding: 40rpx 0;
+  text-align: center;
+}
+.contact-empty-text {
+  font-size: 26rpx;
+  color: #ccc;
+}
+
+/* ===== 关系健康概览 ===== */
+.rq-section {
+  margin: 20rpx 20rpx 0;
+}
+.rq-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 16rpx;
+}
+.rq-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+}
+.rq-more {
+  font-size: 24rpx;
+  color: #F97316;
+  font-weight: 500;
+}
+/* 健康预警卡片 */
+.rq-alerts {
+  display: flex;
+  flex-direction: column;
+  gap: 12rpx;
+  margin-bottom: 16rpx;
+}
+.rq-alert-card {
+  background: #FFF7ED;
+  border-radius: 16rpx;
+  padding: 20rpx;
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+  border: 1rpx solid #FED7AA;
+}
+.rq-alert-card:active {
+  opacity: 0.8;
+}
+.rq-alert-icon {
+  width: 64rpx;
+  height: 64rpx;
+  border-radius: 50%;
+  background: #fff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+}
+.rq-alert-emoji {
+  font-size: 32rpx;
+}
+.rq-alert-info {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  gap: 4rpx;
+}
+.rq-alert-name {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #333;
+}
+.rq-alert-desc {
+  font-size: 24rpx;
+  color: #F97316;
+}
+.rq-alert-action {
+  flex-shrink: 0;
+}
+.rq-alert-btn {
+  font-size: 24rpx;
+  color: #fff;
+  background: #F97316;
+  padding: 8rpx 20rpx;
+  border-radius: 20rpx;
+}
+/* 里程碑卡片 */
+.rq-milestones {
+  display: flex;
+  flex-direction: column;
+  gap: 12rpx;
+  margin-bottom: 16rpx;
+}
+.rq-milestone-card {
+  background: #F0F9FF;
+  border-radius: 16rpx;
+  padding: 20rpx;
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+  border: 1rpx solid #BAE6FD;
+}
+.rq-milestone-icon {
+  width: 64rpx;
+  height: 64rpx;
+  border-radius: 50%;
+  background: #fff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+}
+.rq-milestone-emoji {
+  font-size: 32rpx;
+}
+.rq-milestone-info {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  gap: 4rpx;
+}
+.rq-milestone-name {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #333;
+}
+.rq-milestone-desc {
+  font-size: 24rpx;
+  color: #0EA5E9;
+}
+/* 快捷操作 */
+.rq-quick-actions {
+  display: flex;
+  gap: 16rpx;
+  margin-top: 8rpx;
+}
+.rq-action-btn {
+  flex: 1;
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 20rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 10rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+}
+.rq-action-btn:active {
+  opacity: 0.8;
+}
+.rq-action-icon {
+  font-size: 36rpx;
+}
+.rq-action-label {
+  font-size: 26rpx;
+  color: #333;
+  font-weight: 500;
+}
+</style>

+ 163 - 0
docs/四维度首页及商品详情页修改方案.md

@@ -0,0 +1,163 @@
+# 四维度首页及商品详情页修改方案
+
+**日期:** 2026-07-04  
+**状态:** 待实施
+
+---
+
+## 问题 1:行首页添加推荐阅读组件
+
+### 现状
+
+行首页 `cfc-frontend/pages/action/index.vue` **已有** `<DimensionArticles>` 组件(第72-77行),在 `onShow` 中调用 `loadFeaturedArticles()`(第279行)。
+
+但存在以下问题:
+- 模板使用 `:articles="articles"` 而非计算属性 `actionArticles`(第239-250行过滤逻辑未被使用)
+- `loadFeaturedArticles()` 调用时传递 `dimensionCode: 'action'` 参数(第478行),若后端 API 不支持此参数过滤则返回空数据
+
+### 方案
+
+| 步骤 | 操作 | 涉及文件 |
+|------|------|---------|
+| 1.1 | 用微信开发者工具观察网络请求,确认 `getFeaturedArticles` 接口是否支持 `dimensionCode` 过滤 | 前端 devtools |
+| 1.2 | 若不支持,移除 `dimensionCode: 'action'` 参数,让接口返回通用推荐列表 | `cfc-frontend/pages/action/index.vue:478` |
+| 1.3 | 将 `:articles="articles"` 改为 `:articles="actionArticles"` 以使用已过滤的计算属性 | `cfc-frontend/pages/action/index.vue:75` |
+| 1.4 | 确认 `DimensionArticles` 组件的 props 与传入数据结构匹配 | `cfc-frontend/components/DimensionArticles.vue` |
+
+---
+
+## 问题 2:四维度首页统一用户关系卡片
+
+### 现状
+
+四个维度首页的「关系」模块实现不一致:
+
+| 页面 | FamilyRelationGraph | 联系人卡片(ContactCard) | 关系健康模块(健康预警/里程碑) |
+|------|:------------------:|:----------:|:-----------:|
+| **行** (action) | ✅ | ✅ 含刷新/导入 | ✅ 健康预警/里程碑/互动记录/问卷 |
+| **身** (body) | ✅ | ❌ | ❌ |
+| **心** (mind) | ✅ 带 compatibilityMap | ❌ | ❌ |
+| **智** (wisdom) | ✅ | ❌ | ❌ |
+
+行首页的「重要关系」+「关系健康」模块代码在第80-169行(contact-section)和第106-150行(rq-section),其他三个页面缺少这部分。
+
+### 方案
+
+提取行首页的联系人+关系健康模块为可复用组件,再添加到其他三个页面。
+
+| 步骤 | 操作 | 涉及文件 |
+|------|------|---------|
+| 2.1 | 新建组件 `components/FamilyRelationshipSection.vue`,从 action/index.vue 提取 contact-section 和 rq-section 的 template + 样式 | `cfc-frontend/components/FamilyRelationshipSection.vue` |
+| 2.2 | 组件 props:`contactList`, `healthAlerts`, `milestones`, `isLoggedIn`, `dimensionCode` | 同上 |
+| 2.3 | 组件 events:`@contactClick`, `@refresh`, `@import`, `@interactionLog`, `@questionnaire` | 同上 |
+| 2.4 | 行首页替换原有内联代码为 `<FamilyRelationshipSection>` | `cfc-frontend/pages/action/index.vue` |
+| 2.5 | 在身首页添加 `<FamilyRelationshipSection>`(需添加 `contactList`, `healthAlerts`, `milestones` 数据加载方法) | `cfc-frontend/pages/body/index.vue` |
+| 2.6 | 在心首页添加 `<FamilyRelationshipSection>`(需添加 `contactList`, `healthAlerts`, `milestones` 数据加载方法) | `cfc-frontend/pages/mind/index.vue` |
+| 2.7 | 在智首页添加 `<FamilyRelationshipSection>`(需添加 `contactList`, `healthAlerts`, `milestones` 数据加载方法) | `cfc-frontend/pages/wisdom/index.vue` |
+| 2.8 | 各页面添加缺失的 API 引入:`getContactList`, `getHealthAlerts`, `getMilestones` | 各 index.vue 的 import 语句 |
+| 2.9 | 各页面添加 `loadContacts`, `loadRelationshipHealth` 等数据加载方法 | 各 index.vue 的 methods |
+
+**注意:** 心首页的 `FamilyRelationGraph` 带了 `:compatibilityMap="compatibilityMapForGraph"` props,这部分逻辑保留不变。
+
+---
+
+## 问题 3:商品详情页添加购物车
+
+### 现状
+
+- `cfc-frontend/pages/discover-detail/product-detail/product-detail.vue` 底部栏只有「立即购买」按钮
+- CSS 已预定义 `.btn-cart` 和 `.action-extra` 样式(第304-326行),但模板中未使用
+- 后端无购物车功能(`api.js` 无 cart 相关函数,无 `CartController`)
+
+### 方案
+
+开发完整购物车功能(后端 + 前端 + 页面集成)。
+
+#### 后端新增
+
+| 步骤 | 操作 | 涉及文件 |
+|------|------|---------|
+| 3.1 | 新建 entity `Cart.java` → 表 `cart`(字段:`id`, `user_id`, `product_id`, `quantity`, `created_at`, `updated_at`) | `cfc-backend/.../entity/Cart.java` |
+| 3.2 | 新建 mapper `CartMapper.java` | `cfc-backend/.../mapper/CartMapper.java` |
+| 3.3 | 新建 service `CartService.java`,实现:addToCart / getCartList / updateQuantity / removeItem | `cfc-backend/.../service/CartService.java` |
+| 3.4 | 新建 controller `CartController.java`,接口:<br>POST `/api/cart/add` — 添加到购物车<br>POST `/api/cart/list` — 获取购物车列表<br>POST `/api/cart/update` — 更新数量<br>POST `/api/cart/remove` — 移除商品 | `cfc-backend/.../controller/CartController.java` |
+| 3.5 | `mvn clean compile` 编译验证 | |
+
+#### 前端修改
+
+| 步骤 | 操作 | 涉及文件 |
+|------|------|---------|
+| 3.6 | api.js 新增 `cartAdd`, `cartList`, `cartUpdate`, `cartRemove` 函数 | `cfc-frontend/utils/api.js` |
+| 3.7 | 商品详情页底部栏添加「加入购物车」按钮(`.btn-cart` 样式已存在,模板中放开并绑定事件) | `cfc-frontend/pages/discover-detail/product-detail/product-detail.vue` |
+| 3.8 | 实现 `onAddToCart()` 方法:调用 `cartAdd` API,失败时 toast 提示 | 同上 methods |
+| 3.9 | 可选:购物车角标 badge 显示数量(通过 Vuex 或 globalData 管理) | 后续迭代 |
+
+**布局:** 底部栏左侧为价格信息,中间/右侧并排「加入购物车」(白底橙色边框,描边样式) + 「立即购买」(橙色渐变填充)。
+
+---
+
+## 问题 4:POST /api/energy/sandbox 500 错误
+
+### 现状
+
+- 端点:`POST /api/energy/sandbox`,由 `EnergyController.getFamilyEnergySandbox()` 处理
+- 调用链:`EnergyController` → `EnergyService.calculateFamilyEnergy(user.getFamilyId())`
+- 500 是运行时异常,非业务错误返回
+
+### 可能根因
+
+| 可能性 | 原因 | 概率 |
+|--------|------|------|
+| A | `user.getFamilyId()` 为 null 时,`calculateFamilyEnergy(null)` 传入 null,后续 `childMapper.selectList` 或内部方法查询 null 条件抛异常 | **高** |
+| B | `healthReportMapper.selectOne()` 查询时表字段不一致或连接失败 | 中 |
+| C | `calcChildMind` / `calcChildBody` 等方法中某个 mapper 查询异常 | 低 |
+| D | `@RequestAttribute("userId")` 解析失败导致 userId 为 null | 低 |
+
+### 诊断方法
+
+查看后端控制台日志,500 错误必有完整 stack trace。根据堆栈定位具体抛异常的行。
+
+### 推荐修复方案
+
+| 步骤 | 操作 | 涉及文件 |
+|------|------|---------|
+| 4.1 | 在 `EnergyService.calculateFamilyEnergy` 方法入口添加 null 检查:若 `familyId == null` 则直接返回 `buildEmptyDTO()` | `cfc-backend/.../service/EnergyService.java:66` |
+| 4.2 | 在 `calcParentBody` 的 `healthReportMapper.selectOne()` 查询外层加 try-catch,避免表不存在时全量异常 | `EnergyService.java` 相关方法 |
+| 4.3 | 若后端日志显示具体 mapper 查询异常(如表字段不匹配),需修正 entity 字段或数据库表结构 | 对应 entity / mapper |
+
+---
+
+## 问题 5:/static/default-product.png 图片缺失
+
+### 现状
+
+- 文件 `cfc-frontend/static/default-product.png` 不存在
+- `product-detail.vue` 第9行引用:`:src="product.coverImage || '/static/default-product.png'"`
+- 微信开发者工具控制台报 `Failed to load local image resource`
+
+### 方案
+
+| 步骤 | 操作 | 涉及文件 |
+|------|------|---------|
+| 5.1 | 制作或放置一张 400x400px 的默认商品占位图(PNG格式,透明底或灰色底配商品图标)到 `cfc-frontend/static/default-product.png` | `cfc-frontend/static/default-product.png` |
+| 5.2 | 或在 `product-detail.vue` 的 `<image>` 组件上添加 `@error="onImageError"` 事件,用 JS 兜底处理 | `cfc-frontend/pages/discover-detail/product-detail/product-detail.vue:7` |
+
+---
+
+## 实施优先级
+
+| 优先级 | 问题 | 依赖 | 预估工时 |
+|--------|------|------|---------|
+| **P0** | 问题4:sandbox 500(所有登录用户均受影响,能量数据无法加载) | 需先查看后端日志定位异常 | 1-2h |
+| **P0** | 问题5:默认图片缺失(控制台报错,影响调试体验) | 无 | 0.2h |
+| **P1** | 问题1:推荐阅读(组件已存在,可能只需调整参数或确认接口) | 无 | 0.5h |
+| **P1** | 问题3:购物车(需后端新建 entity/mapper/service/controller + 前端多处修改) | 无 | 3-4h |
+| **P2** | 问题2:统一关系卡片(需新建组件 + 修改四个页面,量大) | 无 | 4-6h |
+
+---
+
+## 修改记录
+
+| 日期 | 修改内容 | 修改人 |
+|------|---------|--------|
+| 2026-07-04 | 创建本文档 |  |

+ 695 - 0
docs/家庭成员角色系统重构方案.md

@@ -0,0 +1,695 @@
+# 家庭成员角色系统重构方案
+
+> 目标:废除 children 表,合并到 family_members;用 roleOverride 替代 capabilityRole,支持管理员手动设置 + 年龄自动判断。
+
+---
+
+## 一、核心变更目标
+
+| 目标 | 说明 |
+|------|------|
+| 废除 children 表 | 合并到 family_members,删除 Child.java / ChildMapper |
+| capabilityRole → roleOverride | family_members 表字段,管理员可手动覆盖 |
+| capabilityRole → defaultRole | relationship_types 表字段,关系类型的默认模板值 |
+| 可配置年龄阈值 | system_config 表存储 child/elderly 年龄分界 |
+| effectiveRole 计算 | 优先 roleOverride → 模板值 → 年龄计算 |
+
+---
+
+## 二、数据库变更
+
+### 2.1 family_members 表扩字段 + capability_role → role_override
+
+```sql
+ALTER TABLE family_members 
+  CHANGE COLUMN capability_role role_override VARCHAR(20) DEFAULT 'auto' 
+  COMMENT '角色覆盖: auto/parent/child/elderly,空=auto';
+
+ALTER TABLE family_members ADD COLUMN id_card VARCHAR(18) DEFAULT NULL COMMENT '身份证号(孩子)';
+ALTER TABLE family_members ADD COLUMN penalty_enabled TINYINT DEFAULT 1 COMMENT '是否扣减积分(0否1是)';
+ALTER TABLE family_members ADD COLUMN theme VARCHAR(32) DEFAULT 'default' COMMENT '主题';
+ALTER TABLE family_members ADD COLUMN system_points INT DEFAULT 0 COMMENT '系统积分(干预/平台奖励)';
+ALTER TABLE family_members ADD COLUMN streak_days INT DEFAULT 0 COMMENT '连续打卡天数';
+ALTER TABLE family_members ADD COLUMN last_task_date DATE COMMENT '上次任务日期';
+ALTER TABLE family_members ADD COLUMN focus_max_daily TINYINT DEFAULT 3 COMMENT '每日专注上限';
+ALTER TABLE family_members ADD COLUMN focus_remaining TINYINT DEFAULT 3 COMMENT '今日剩余专注次数';
+ALTER TABLE family_members ADD COLUMN focus_reset_date DATE COMMENT '专注次数重置日期';
+```
+
+### 2.2 relationship_types 表重命名字段
+
+```sql
+ALTER TABLE relationship_types
+  CHANGE COLUMN capability_role default_role VARCHAR(20) DEFAULT NULL 
+  COMMENT '默认角色模板: parent/child';
+```
+
+### 2.3 新建 system_config 表
+
+```sql
+CREATE TABLE IF NOT EXISTS system_config (
+  id BIGINT AUTO_INCREMENT PRIMARY KEY,
+  config_key VARCHAR(100) NOT NULL UNIQUE,
+  config_value VARCHAR(500),
+  description VARCHAR(255),
+  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+INSERT INTO system_config (config_key, config_value, description) VALUES
+  ('child_age_threshold', '18', '未成年年龄分界线(岁)'),
+  ('elderly_age_threshold', '65', '老年年龄分界线(岁)');
+```
+
+### 2.4 数据迁移脚本
+
+```sql
+-- ============================================================
+-- 迁移 children 数据到 family_members
+-- ============================================================
+
+-- Step 1: 匹配更新 — family_members 中已存在 (family_id, nickname) 相同的记录
+UPDATE family_members fm
+INNER JOIN children c ON fm.family_id = c.family_id AND fm.nickname = c.nickname
+SET 
+  fm.id_card = c.id_card,
+  fm.penalty_enabled = c.penalty_enabled,
+  fm.theme = c.theme,
+  fm.system_points = c.system_points,
+  fm.streak_days = c.streak_days,
+  fm.last_task_date = c.last_task_date,
+  fm.focus_max_daily = c.focus_max_daily,
+  fm.focus_remaining = c.focus_remaining,
+  fm.focus_reset_date = c.focus_reset_date,
+  fm.role_override = 'child';
+
+-- Step 2: 不匹配 — 插入为新的 family_members 记录
+INSERT INTO family_members (
+  family_id, user_id, nickname, birthday, gender, phone,
+  id_card, penalty_enabled, theme, system_points, streak_days, last_task_date,
+  focus_max_daily, focus_remaining, focus_reset_date, show_to_family, total_points,
+  role_override, created_at, updated_at
+)
+SELECT 
+  c.family_id, c.user_id, c.nickname, c.birthday, c.gender, c.phone,
+  c.id_card, c.penalty_enabled, c.theme, c.system_points, c.streak_days, c.last_task_date,
+  c.focus_max_daily, c.focus_remaining, c.focus_reset_date, c.show_to_family, c.total_points,
+  'child', c.created_at, c.updated_at
+FROM children c
+WHERE NOT EXISTS (
+  SELECT 1 FROM family_members fm2 
+  WHERE fm2.family_id = c.family_id AND fm2.nickname = c.nickname
+);
+
+-- Step 3: tasks.child_id 处理
+--    旧方案:tasks.child_id → family_members.id 映射(需创建 mapping 表或重建关联逻辑)
+--    本次方案:保留 tasks.child_id,但业务层通过 (family_id, nickname) 匹配到新的 family_members.id
+--    后续迭代中重构为 family_member_id 统一关联
+
+-- Step 4: 删除 children 表
+DROP TABLE IF EXISTS children;
+```
+
+---
+
+## 三、后端 Java 变更
+
+### 3.1 文件变更总览
+
+| 分类 | 新建 | 修改 | 删除 |
+|------|:----:|:----:|:----:|
+| Entity | 1 | 2 | 1 |
+| Mapper | 1 | 0 | 1 |
+| Service | 2 | ~25 | 0 |
+| Controller | 0 | ~10 | 0 |
+| DTO/VO | 0 | 4 | 0 |
+| 测试类 | 0 | ~8 | 0 |
+| 配置类 | 0 | 2 | 0 |
+
+### 3.2 新建文件
+
+**SystemConfig.java**
+```java
+@Data
+@TableName("system_config")
+public class SystemConfig {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String configKey;
+    private String configValue;
+    private String description;
+    private Date createdAt;
+    private Date updatedAt;
+}
+```
+
+**SystemConfigMapper.java**
+```java
+public interface SystemConfigMapper extends BaseMapper<SystemConfig> {}
+```
+
+**SystemConfigService.java**
+```java
+@Service
+public class SystemConfigService {
+    @Resource
+    private SystemConfigMapper systemConfigMapper;
+
+    private static final int DEFAULT_CHILD_AGE = 18;
+    private static final int DEFAULT_ELDERLY_AGE = 65;
+
+    public int getChildAgeThreshold() {
+        SystemConfig cfg = systemConfigMapper.selectOne(
+            new LambdaQueryWrapper<SystemConfig>()
+                .eq(SystemConfig::getConfigKey, "child_age_threshold"));
+        return cfg != null ? Integer.parseInt(cfg.getConfigValue()) : DEFAULT_CHILD_AGE;
+    }
+
+    public int getElderlyAgeThreshold() {
+        SystemConfig cfg = systemConfigMapper.selectOne(
+            new LambdaQueryWrapper<SystemConfig>()
+                .eq(SystemConfig::getConfigKey, "elderly_age_threshold"));
+        return cfg != null ? Integer.parseInt(cfg.getConfigValue()) : DEFAULT_ELDERLY_AGE;
+    }
+}
+```
+
+### 3.3 删除文件
+
+- `cfc-backend/src/main/java/com/etotem/cfc/entity/Child.java`
+- `cfc-backend/src/main/java/com/etotem/cfc/mapper/ChildMapper.java`
+
+### 3.4 FamilyMember.java 字段变更
+
+```java
+// 删除
+- private String capabilityRole;
+
+// 新增
++ private String roleOverride;  // auto/parent/child/elderly,默认 auto
++ private String idCard;
++ private Integer penaltyEnabled;
++ private String theme;
++ private Integer systemPoints;
++ private Integer streakDays;
++ private Date lastTaskDate;
++ private Integer focusMaxDaily;
++ private Integer focusRemaining;
++ private Date focusResetDate;
+```
+
+### 3.5 RelationshipType.java 字段变更
+
+```java
+// 删除
+- private String capabilityRole;
+
+// 新增
++ private String defaultRole;  // 默认角色模板: parent/child
+```
+
+### 3.6 FamilyMemberVO.java 字段变更
+
+```java
+// 删除
+- private String capabilityRole;
+
+// 新增
++ private String effectiveRole;   // 计算后的实际角色(前端展示用)
++ private String roleOverride;     // 管理员设置的覆盖值
+```
+
+### 3.7 SwitchMemberVO.java 字段变更
+
+```java
+// 删除
+- private String capabilityRole;
+
+// 新增
++ private String effectiveRole;
+```
+
+### 3.8 AddFamilyMemberDTO.java 新增字段
+
+```java
++ private String roleOverride;  // auto/parent/child/elderly
+```
+
+### 3.9 FamilyMemberService.java 核心改动
+
+#### 3.9.1 新增 computeEffectiveRole 方法
+
+```java
+public String computeEffectiveRole(FamilyMember member) {
+    // 1. roleOverride 非 auto → 直接返回
+    if (member.getRoleOverride() != null && !"auto".equals(member.getRoleOverride())) {
+        return member.getRoleOverride();
+    }
+    // 2. 取关系类型模板值
+    RelationshipType relType = relationshipTypeMapper.selectOne(
+        new LambdaQueryWrapper<RelationshipType>()
+            .eq(RelationshipType::getTypeKey, member.getRelationshipType()));
+    if (relType != null && relType.getDefaultRole() != null) {
+        return relType.getDefaultRole();
+    }
+    // 3. 按年龄计算
+    if (member.getBirthday() != null) {
+        int age = calculateAge(member.getBirthday());
+        int threshold = systemConfigService.getChildAgeThreshold();
+        if (age < threshold) return "child";
+        if (threshold >= 65 && age >= threshold) return "elderly";
+    }
+    return "parent";
+}
+```
+
+#### 3.9.2 addMember() 方法(约 line 92)
+
+```java
+// 改前
+member.setCapabilityRole(relType.getCapabilityRole());
+
+// 改后
+member.setRoleOverride("auto");
+```
+
+#### 3.9.3 children 表同步逻辑(约 line 116)
+
+```java
+// 改前
+if ("child".equals(relType.getCapabilityRole())) {
+    Child child = new Child();
+    // ... childMapper.insert(child)
+}
+
+// 改后
+// 无需同步到 children 表,数据已存在 family_members 中
+// 如需 child 专属逻辑(游戏/任务),通过 computeEffectiveRole 判断即可
+```
+
+#### 3.9.4 listMembers() 方法(约 line 188)
+
+```java
+// 改前
+vo.setCapabilityRole(fm.getCapabilityRole());
+
+// 改后
+vo.setEffectiveRole(computeEffectiveRole(fm));
+vo.setRoleOverride(fm.getRoleOverride());
+```
+
+#### 3.9.5 switchToMember() 方法(约 line 236)
+
+```java
+// 改前
+vo.setCapabilityRole(member.getCapabilityRole());
+
+// 改后
+vo.setEffectiveRole(computeEffectiveRole(member));
+```
+
+#### 3.9.6 kickMember() 方法(约 line 287)
+
+```java
+// 改前
+if ("child".equals(member.getCapabilityRole())) {
+    List<Child> children = childMapper.selectList(...);
+    for (Child c : children) { childMapper.deleteById(c.getId()); }
+}
+
+// 改后
+// children 表已废除,删除 family_members 记录即可
+// 若 roleOverride 为 child,需清理 family_members 中对应行
+if ("child".equals(computeEffectiveRole(member))) {
+    familyMemberMapper.deleteById(memberId);
+}
+```
+
+#### 3.9.7 updateMember() 方法(约 line 349)
+
+```java
+// 改前
+if ("child".equals(member.getCapabilityRole())) {
+    List<Child> children = childMapper.selectList(...);
+    for (Child c : children) { /* update child */ }
+}
+
+// 改后
+if ("child".equals(computeEffectiveRole(member))) {
+    // 直接更新 family_members 中的 child 专属字段即可
+    FamilyMember toUpdate = familyMemberMapper.selectById(member.getId());
+    // ... update fields
+}
+
+// 另:支持 roleOverride 更新
+if (dto.getRoleOverride() != null) {
+    member.setRoleOverride(dto.getRoleOverride());
+}
+```
+
+#### 3.9.8 saveRelationshipType() 方法(约 line 418)
+
+```java
+// 改前
+existing.setCapabilityRole(type.getCapabilityRole());
+
+// 改后
+existing.setDefaultRole(type.getDefaultRole());
+```
+
+#### 3.9.9 toFamilyMemberVO() 方法(约 line 454)
+
+```java
+// 改前
+vo.setCapabilityRole(member.getCapabilityRole());
+
+// 改后
+vo.setEffectiveRole(computeEffectiveRole(member));
+vo.setRoleOverride(member.getRoleOverride());
+```
+
+### 3.10 所有引用 childMapper 的文件替换清单
+
+#### Service 层(约 25 个)
+
+| 文件 | 替换方式 |
+|------|---------|
+| `UserService.java` | `childMapper.selectList` → `familyMemberMapper.selectList` + computeEffectiveRole 过滤 |
+| `StreakService.java` | 同上,streak_days 在 family_members 中 |
+| `MiniGameService.java` | 同上 |
+| `TaskService.java` | 同上 |
+| `PointsService.java` | 同上 |
+| `WishService.java` | 同上 |
+| `RewardService.java` | 同上 |
+| `EnergyService.java` | 同上 |
+| `OnboardingService.java` | 同上 |
+| `TaskStatsService.java` | 同上 |
+| `GrowthTaskService.java` | 同上 |
+| `InviteCardService.java` | 同上 |
+| `InviteMilestoneService.java` | 同上 |
+| `DataMigrationService.java` | 同上 |
+| `EmotionAlertService.java` | 同上 |
+| `EmotionCheckinService.java` | 同上 |
+| `FinanceCheckinService.java` | 同上 |
+| `HealthCheckinService.java` | 同上 |
+| `HealthReportService.java` | 同上 |
+| `ArticleService.java` | 同上 |
+| `ActivityService.java` | 同上 |
+| `CompatibilityService.java` | 同上 |
+| `AIService.java` | 同上 |
+| `ShareService.java` | 同上 |
+| `PointsExchangeService.java` | 同上 |
+| `TeacherService.java` | 同上 |
+| `MentalHealthScreenService.java` | 同上 |
+| `CognitiveService.java` | 同上 |
+| `TrainingPlanService.java` | 同上 |
+| `ChildNutritionProfileService.java` | 同上 |
+| `CognitiveTrainingConfigService.java` | 同上 |
+| `ProductCategoryService.java` | 同上 |
+| `ProductOrderService.java` | 同上 |
+| `ProductDimensionConfigService.java` | 同上 |
+
+**通用替换模式**:
+
+```java
+// 改前
+List<Child> children = childMapper.selectList(
+    new LambdaQueryWrapper<Child>()
+        .eq(Child::getFamilyId, familyId));
+
+// 改后
+List<FamilyMember> members = familyMemberMapper.selectList(
+    new LambdaQueryWrapper<FamilyMember>()
+        .eq(FamilyMember::getFamilyId, familyId));
+List<FamilyMember> children = members.stream()
+    .filter(m -> "child".equals(computeEffectiveRole(m)))
+    .collect(Collectors.toList());
+
+// 或通过方法(性能更优,由 FamilyMemberService 提供工具方法)
+List<FamilyMember> children = familyMemberService.listChildrenByFamilyId(familyId);
+```
+
+#### Controller 层(约 10 个)
+
+| 文件 | 改动 |
+|------|------|
+| `StreakController.java` | childMapper → familyMemberMapper |
+| `UserController.java` | 同上 |
+| `AdminController.java` | 同上 |
+| `FamilyUserController.java` | 同上 |
+| `InviteCardController.java` | 同上 |
+| `ReportStatsController.java` | 同上 |
+| `ParentAssessmentController.java` | 同上 |
+| `GuideFamilyTaskController.java` | 同上 |
+| `EmotionAlertController.java` | 同上 |
+| `StatsController.java` | 同上 |
+
+#### 配置/初始化类
+
+| 文件 | 改动 |
+|------|------|
+| `DatabaseInitializer.java` | 移除 child 初始化逻辑 |
+| `SchemaSyncHandler.java` | 移除 children 表同步逻辑 |
+
+#### 测试类(约 8 个)
+
+| 文件 | 改动 |
+|------|------|
+| `GuideFamilyTaskControllerTest.java` | mock Child 相关数据 → FamilyMember |
+| `GuideRolePermissionTest.java` | 同上 |
+| `PointsControllerTest.java` | 同上 |
+| `StatsControllerTest.java` | 同上 |
+| `DailyCheckinStreakFlowTest.java` | 同上 |
+| `WishExchangeFlowTest.java` | 同上 |
+| `TaskServiceMinigameTest.java` | 同上 |
+| `GuideHierarchyServiceTest.java` | 同上 |
+
+### 3.11 其他 DTO/Entity 改动
+
+| 文件 | 改动 |
+|------|------|
+| `CreateTaskDTO.java` | 移除 childId 相关,改用 familyMemberId |
+| `DirectRegisterDTO.java` | 移除 child 相关字段 |
+| `MemberEnergyDTO.java` | capabilityRole → effectiveRole |
+| `PhoneLoginDTO.java` | 移除 child 相关 |
+| `RegisterWithInviteCodeDTO.java` | 移除 child 相关 |
+| `Task.java` entity | childId 字段保留(待后续重构),关联逻辑改为 familyMemberId |
+| `User.java` entity | 暂无 children 引用 |
+| `Wish.java` entity | 无 |
+| `Article.java` entity | 无 |
+
+---
+
+## 四、前端变更
+
+### 4.1 cfc-web(Web管理端)
+
+#### RelationshipTypes.vue
+- 字段标签:`能力角色` → `默认角色模板`
+- 字段名:`capabilityRole` → `defaultRole`
+- 下拉选项说明:`不指定则按年龄自动判断`
+
+#### 新增家庭成员 roleOverride 下拉
+- 位置:家庭成员管理页(新建或修改成员时)
+- 字段:`roleOverride` 下拉(auto / parent / child / elderly)
+- 权限:仅家庭管理员可见
+
+### 4.2 cfc-小程序
+
+| 文件 | 改动 |
+|------|------|
+| `store/modules/family.js` | `capabilityRole` → `effectiveRole` |
+| 家庭切换组件 | 使用 `effectiveRole` 决定家长端/孩子端界面 |
+| 相关 API 响应处理 | `capabilityRole` → `effectiveRole` |
+
+---
+
+## 五、schema.sql 同步更新
+
+需同步更新以下内容:
+1. `family_members` 表:`role_override` 字段替换 `capability_role`,新增 10 个 child 字段
+2. `relationship_types` 表:`default_role` 字段替换 `capability_role`
+3. 新增 `system_config` 表定义
+4. 删除 `children` 表定义
+5. 附注迁移 SQL(2.4 节)
+
+---
+
+## 六、实施顺序
+
+```
+Phase 1 — 数据库层
+  [ ] 1.1 修改 schema.sql(DDL)
+  [ ] 1.2 执行 ALTER TABLE
+  [ ] 1.3 验证表结构
+
+Phase 2 — 基础设施(SystemConfig)
+  [ ] 2.1 新建 SystemConfig.java + SystemConfigMapper.java
+  [ ] 2.2 新建 SystemConfigService.java
+  [ ] 2.3 mvn compile 验证
+
+Phase 3 — Entity 核心
+  [ ] 3.1 FamilyMember.java:字段变更 + 10 个新字段
+  [ ] 3.2 RelationshipType.java:字段重命名
+  [ ] 3.3 删除 Child.java + ChildMapper.java
+  [ ] 3.4 FamilyMemberVO.java:字段变更
+  [ ] 3.5 SwitchMemberVO.java:字段变更
+  [ ] 3.6 AddFamilyMemberDTO.java:新增 roleOverride
+  [ ] 3.7 mvn compile 验证
+
+Phase 4 — Service 层(FamilyMemberService 核心逻辑)
+  [ ] 4.1 实现 computeEffectiveRole() 方法
+  [ ] 4.2 重构 addMember()
+  [ ] 4.3 重构 listMembers()
+  [ ] 4.4 重构 switchToMember()
+  [ ] 4.5 重构 kickMember()
+  [ ] 4.6 重构 updateMember()
+  [ ] 4.7 重构 saveRelationshipType()
+  [ ] 4.8 重构 toFamilyMemberVO()
+  [ ] 4.9 mvn compile 验证
+
+Phase 5 — Service 层(批量替换 childMapper)
+  [ ] 5.1 UserService.java
+  [ ] 5.2 StreakService.java
+  [ ] 5.3 MiniGameService.java
+  [ ] 5.4 TaskService.java
+  [ ] 5.5 PointsService.java
+  [ ] 5.6 WishService.java
+  [ ] 5.7 RewardService.java
+  [ ] 5.8 EnergyService.java
+  [ ] 5.9 其余 20+ 个 Service
+  [ ] 5.10 mvn compile 验证
+
+Phase 6 — Controller 层
+  [ ] 6.1 FamilyMembersController.java
+  [ ] 6.2 RelationshipTypeAdminController.java
+  [ ] 6.3 其余 ~10 个 Controller
+  [ ] 6.4 mvn compile 验证
+
+Phase 7 — 测试类
+  [ ] 7.1 更新 ~8 个测试类
+  [ ] 7.2 mvn test 验证
+
+Phase 8 — 前端
+  [ ] 8.1 cfc-web RelationshipTypes.vue 字段更新
+  [ ] 8.2 cfc-web 家庭成员管理页新增 roleOverride 下拉
+  [ ] 8.3 cfc-小程序 family store + 组件适配
+  [ ] 8.4 npm run build 验证
+
+Phase 9 — 数据迁移
+  [ ] 9.1 备份数据库
+  [ ] 9.2 执行迁移 SQL(2.4 节)
+  [ ] 9.3 验证数据完整性
+  [ ] 9.4 删除 children 表
+
+Phase 10 — 最终验证
+  [ ] 10.1 mvn clean compile
+  [ ] 10.2 mvn test
+  [ ] 10.3 后端启动验证
+  [ ] 10.4 前端启动验证
+```
+
+---
+
+## 七、涉及文件清单(共约 93 个)
+
+### 后端 Java
+
+| 文件 | 操作 |
+|------|------|
+| `entity/Child.java` | **删除** |
+| `mapper/ChildMapper.java` | **删除** |
+| `entity/FamilyMember.java` | 修改 |
+| `entity/RelationshipType.java` | 修改 |
+| `entity/SystemConfig.java` | **新建** |
+| `mapper/SystemConfigMapper.java` | **新建** |
+| `service/SystemConfigService.java` | **新建** |
+| `service/FamilyMemberService.java` | 修改(核心) |
+| `service/UserService.java` | 修改 |
+| `service/StreakService.java` | 修改 |
+| `service/MiniGameService.java` | 修改 |
+| `service/TaskService.java` | 修改 |
+| `service/PointsService.java` | 修改 |
+| `service/WishService.java` | 修改 |
+| `service/RewardService.java` | 修改 |
+| `service/EnergyService.java` | 修改 |
+| `service/OnboardingService.java` | 修改 |
+| `service/TaskStatsService.java` | 修改 |
+| `service/GrowthTaskService.java` | 修改 |
+| `service/InviteCardService.java` | 修改 |
+| `service/InviteMilestoneService.java` | 修改 |
+| `service/DataMigrationService.java` | 修改 |
+| `service/EmotionAlertService.java` | 修改 |
+| `service/EmotionCheckinService.java` | 修改 |
+| `service/FinanceCheckinService.java` | 修改 |
+| `service/HealthCheckinService.java` | 修改 |
+| `service/HealthReportService.java` | 修改 |
+| `service/ArticleService.java` | 修改 |
+| `service/ActivityService.java` | 修改 |
+| `service/CompatibilityService.java` | 修改 |
+| `service/AIService.java` | 修改 |
+| `service/ShareService.java` | 修改 |
+| `service/PointsExchangeService.java` | 修改 |
+| `service/TeacherService.java` | 修改 |
+| `service/MentalHealthScreenService.java` | 修改 |
+| `service/CognitiveService.java` | 修改 |
+| `service/TrainingPlanService.java` | 修改 |
+| `service/ChildNutritionProfileService.java` | 修改 |
+| `service/CognitiveTrainingConfigService.java` | 修改 |
+| `service/ProductCategoryService.java` | 修改 |
+| `service/ProductOrderService.java` | 修改 |
+| `service/ProductDimensionConfigService.java` | 修改 |
+| `controller/StreakController.java` | 修改 |
+| `controller/UserController.java` | 修改 |
+| `controller/AdminController.java` | 修改 |
+| `controller/FamilyUserController.java` | 修改 |
+| `controller/InviteCardController.java` | 修改 |
+| `controller/ReportStatsController.java` | 修改 |
+| `controller/ParentAssessmentController.java` | 修改 |
+| `controller/GuideFamilyTaskController.java` | 修改 |
+| `controller/EmotionAlertController.java` | 修改 |
+| `controller/StatsController.java` | 修改 |
+| `controller/FamilyMembersController.java` | 修改 |
+| `controller/RelationshipTypeAdminController.java` | 修改 |
+| `config/DatabaseInitializer.java` | 修改 |
+| `config/SchemaSyncHandler.java` | 修改 |
+| `dto/FamilyMemberVO.java` | 修改 |
+| `dto/SwitchMemberVO.java` | 修改 |
+| `dto/AddFamilyMemberDTO.java` | 修改 |
+| `dto/CreateTaskDTO.java` | 修改 |
+| `dto/DirectRegisterDTO.java` | 修改 |
+| `dto/MemberEnergyDTO.java` | 修改 |
+| `dto/PhoneLoginDTO.java` | 修改 |
+| `dto/RegisterWithInviteCodeDTO.java` | 修改 |
+| `entity/Task.java` | 修改 |
+| `test/GuideFamilyTaskControllerTest.java` | 修改 |
+| `test/GuideRolePermissionTest.java` | 修改 |
+| `test/PointsControllerTest.java` | 修改 |
+| `test/StatsControllerTest.java` | 修改 |
+| `test/DailyCheckinStreakFlowTest.java` | 修改 |
+| `test/WishExchangeFlowTest.java` | 修改 |
+| `test/TaskServiceMinigameTest.java` | 修改 |
+| `test/GuideHierarchyServiceTest.java` | 修改 |
+
+### 前端
+
+| 文件 | 操作 |
+|------|------|
+| `cfc-web/views/admin/RelationshipTypes.vue` | 修改 |
+| `cfc-web/家庭成员管理页`(路径待确认) | 修改/新建 |
+| `cfc-frontend/store/modules/family.js` | 修改 |
+| `cfc-frontend/家庭切换组件`(路径待确认) | 修改 |
+
+---
+
+## 八、风险提示
+
+1. **tasks.child_id 外键**:`tasks` 表的 `child_id` 字段原指向 `children.id`,合并后该字段需要业务确认处理方式。建议保留字段但通过 (family_id, nickname) 匹配到新的 family_members.id。
+
+2. **ID 空间冲突**:children 表和 family_members 表的 ID 自增序列独立,迁移后不会冲突(匹配行复用 family_members 的 ID,不匹配行 INSERT 时会分配新的 ID)。
+
+3. **40+ 文件改动**:全部改完后建议用 `mvn clean compile` 全量验证,逐个 Service 测试太慢。
+
+4. **测试覆盖**:测试类中的 mock 数据结构需要同步更新为 FamilyMember 结构。
+
+5. **数据一致性**:迁移过程中对 children 和 family_members 的并发写入需要加事务保护。