Преглед на файлове

fix: 后端全面修复 - 实体字段、Controller参数、Service方法补齐 + DTO新增
\n- 修复字段类型不匹配(Long/Integer/String)\n- 补齐缺失的 import/方法/参数\n- 修改 @PostMapping 路由统一\n- 新增 Cart/CartItemDTO/SystemConfig 相关实体和Service\n- 重命名 ChildNutritionProfileService -> FamilyMemberNutritionProfileService\n- 修复测试用例

User преди 2 месеца
родител
ревизия
577dc12f9b
променени са 88 файла, в които са добавени 1379 реда и са изтрити 957 реда
  1. 0 3
      cfc-backend/.classpath
  2. 1 1
      cfc-backend/.factorypath
  3. 1 1
      cfc-backend/pom.xml
  4. 8 0
      cfc-backend/src/main/java/com/etotem/cfc/common/Result.java
  5. 2 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java
  6. 4 4
      cfc-backend/src/main/java/com/etotem/cfc/controller/InviteCardController.java
  7. 12 12
      cfc-backend/src/main/java/com/etotem/cfc/controller/ReportStatsController.java
  8. 4 4
      cfc-backend/src/main/java/com/etotem/cfc/controller/StreakController.java
  9. 9 9
      cfc-backend/src/main/java/com/etotem/cfc/controller/UserController.java
  10. 16 16
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminController.java
  11. 32 26
      cfc-backend/src/main/java/com/etotem/cfc/controller/cart/CartController.java
  12. 17 17
      cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyUserController.java
  13. 22 22
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideFamilyTaskController.java
  14. 4 4
      cfc-backend/src/main/java/com/etotem/cfc/controller/mind/EmotionAlertController.java
  15. 7 7
      cfc-backend/src/main/java/com/etotem/cfc/controller/parent/ParentAssessmentController.java
  16. 5 5
      cfc-backend/src/main/java/com/etotem/cfc/controller/stats/StatsController.java
  17. 3 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/AddFamilyMemberDTO.java
  18. 11 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/CreateProductOrderDTO.java
  19. 10 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/CreateWishDTO.java
  20. 5 2
      cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyMemberVO.java
  21. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/HealthDimensionVO.java
  22. 5 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/OrderItemVO.java
  23. 2 2
      cfc-backend/src/main/java/com/etotem/cfc/dto/SwitchMemberVO.java
  24. 21 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/WishDTO.java
  25. 10 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/DanAssessmentResult.java
  26. 43 2
      cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyMember.java
  27. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/GrowthPlan.java
  28. 4 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/GrowthRecord.java
  29. 15 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/MediaRecord.java
  30. 19 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/PackageOrder.java
  31. 1 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/PointsExchangeRecord.java
  32. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/PointsLog.java
  33. 20 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/Product.java
  34. 15 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/ProductOrder.java
  35. 2 2
      cfc-backend/src/main/java/com/etotem/cfc/entity/RelationshipType.java
  36. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/Task.java
  37. 5 5
      cfc-backend/src/main/java/com/etotem/cfc/service/ActivityService.java
  38. 5 5
      cfc-backend/src/main/java/com/etotem/cfc/service/ArticleService.java
  39. 92 91
      cfc-backend/src/main/java/com/etotem/cfc/service/CartService.java
  40. 0 200
      cfc-backend/src/main/java/com/etotem/cfc/service/ChildNutritionProfileService.java
  41. 26 12
      cfc-backend/src/main/java/com/etotem/cfc/service/CognitiveService.java
  42. 4 4
      cfc-backend/src/main/java/com/etotem/cfc/service/CognitiveTrainingConfigService.java
  43. 42 42
      cfc-backend/src/main/java/com/etotem/cfc/service/DataMigrationService.java
  44. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/service/DatabaseSyncService.java
  45. 4 4
      cfc-backend/src/main/java/com/etotem/cfc/service/EmotionAlertService.java
  46. 6 6
      cfc-backend/src/main/java/com/etotem/cfc/service/EmotionCheckinService.java
  47. 25 17
      cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java
  48. 8 8
      cfc-backend/src/main/java/com/etotem/cfc/service/FamilyInvitationService.java
  49. 49 122
      cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java
  50. 5 11
      cfc-backend/src/main/java/com/etotem/cfc/service/FinanceCheckinService.java
  51. 6 5
      cfc-backend/src/main/java/com/etotem/cfc/service/GrowthTaskService.java
  52. 5 5
      cfc-backend/src/main/java/com/etotem/cfc/service/HealthCheckinService.java
  53. 23 29
      cfc-backend/src/main/java/com/etotem/cfc/service/HealthReportService.java
  54. 8 8
      cfc-backend/src/main/java/com/etotem/cfc/service/InviteCardService.java
  55. 6 6
      cfc-backend/src/main/java/com/etotem/cfc/service/InviteMilestoneService.java
  56. 4 4
      cfc-backend/src/main/java/com/etotem/cfc/service/MentalHealthScreenService.java
  57. 5 5
      cfc-backend/src/main/java/com/etotem/cfc/service/MiniGameService.java
  58. 6 6
      cfc-backend/src/main/java/com/etotem/cfc/service/OnboardingService.java
  59. 11 11
      cfc-backend/src/main/java/com/etotem/cfc/service/PointsExchangeService.java
  60. 11 11
      cfc-backend/src/main/java/com/etotem/cfc/service/PointsService.java
  61. 11 11
      cfc-backend/src/main/java/com/etotem/cfc/service/RewardService.java
  62. 4 4
      cfc-backend/src/main/java/com/etotem/cfc/service/ShareService.java
  63. 13 13
      cfc-backend/src/main/java/com/etotem/cfc/service/StreakService.java
  64. 43 8
      cfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java
  65. 7 5
      cfc-backend/src/main/java/com/etotem/cfc/service/TaskStatsService.java
  66. 5 5
      cfc-backend/src/main/java/com/etotem/cfc/service/TeacherService.java
  67. 2 2
      cfc-backend/src/main/java/com/etotem/cfc/service/TrainingPlanService.java
  68. 147 18
      cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java
  69. 4 4
      cfc-backend/src/main/java/com/etotem/cfc/service/WechatAlertTemplateService.java
  70. 10 10
      cfc-backend/src/main/java/com/etotem/cfc/service/WishService.java
  71. 38 0
      cfc-backend/src/main/java/com/etotem/cfc/service/impl/DimensionScoreServiceImpl.java
  72. 118 31
      cfc-backend/src/main/resources/schema.sql
  73. 17 17
      cfc-backend/src/test/java/com/etotem/cfc/controller/GuideFamilyTaskControllerTest.java
  74. 9 11
      cfc-backend/src/test/java/com/etotem/cfc/controller/GuideRolePermissionTest.java
  75. 16 16
      cfc-backend/src/test/java/com/etotem/cfc/controller/PointsControllerTest.java
  76. 1 1
      cfc-backend/src/test/java/com/etotem/cfc/controller/StatsControllerTest.java
  77. 5 5
      cfc-backend/src/test/java/com/etotem/cfc/integration/DailyCheckinStreakFlowTest.java
  78. 2 2
      cfc-frontend/components/DimensionActivities.vue
  79. 4 6
      cfc-frontend/pages.json
  80. 2 2
      cfc-frontend/pages/article-center/index.vue
  81. 105 9
      cfc-frontend/pages/body/index.vue
  82. 32 1
      cfc-frontend/pages/discover-detail/product-detail/product-detail.vue
  83. 1 0
      cfc-frontend/pages/index/parent-index.vue
  84. 3 3
      cfc-frontend/pages/profile/family-members.vue
  85. 65 2
      cfc-frontend/pages/wisdom/index.vue
  86. 7 7
      cfc-frontend/store/index.js
  87. 6 0
      cfc-frontend/utils/api.js
  88. 6 6
      cfc-web/src/views/admin/RelationshipTypes.vue

+ 0 - 3
cfc-backend/.classpath

@@ -47,9 +47,6 @@
 	<classpathentry kind="src" output="target/test-classes" path="target/generated-test-sources/test-annotations">
 		<attributes>
 			<attribute name="optional" value="true"/>
-			<attribute name="maven.pomderived" value="true"/>
-			<attribute name="ignore_optional_problems" value="true"/>
-			<attribute name="m2e-apt" value="true"/>
 			<attribute name="test" value="true"/>
 		</attributes>
 	</classpathentry>

+ 1 - 1
cfc-backend/.factorypath

@@ -1,3 +1,3 @@
 <factorypath>
-    <factorypathentry kind="VARJAR" id="M2_REPO/org/projectlombok/lombok/1.18.32/lombok-1.18.32.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/projectlombok/lombok/1.18.30/lombok-1.18.30.jar" enabled="true" runInBatchMode="false"/>
 </factorypath>

+ 1 - 1
cfc-backend/pom.xml

@@ -166,7 +166,7 @@
                         <path>
                             <groupId>org.projectlombok</groupId>
                             <artifactId>lombok</artifactId>
-                            <version>1.18.32</version>
+                            <version>1.18.30</version>
                         </path>
                     </annotationProcessorPaths>
                 </configuration>

+ 8 - 0
cfc-backend/src/main/java/com/etotem/cfc/common/Result.java

@@ -47,4 +47,12 @@ public class Result<T> implements Serializable {
         result.setMessage(message);
         return result;
     }
+
+    // Explicit accessors (Lombok fallback)
+    public Integer getCode() { return code; }
+    public void setCode(Integer code) { this.code = code; }
+    public String getMessage() { return message; }
+    public void setMessage(String message) { this.message = message; }
+    public T getData() { return data; }
+    public void setData(T data) { this.data = data; }
 }

+ 2 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java

@@ -17,7 +17,7 @@ import com.etotem.cfc.entity.HealthReport;
 import com.etotem.cfc.entity.HealthReportDraft;
 import com.etotem.cfc.entity.NutritionDeficiencyRecord;
 import com.etotem.cfc.entity.NutritionIndicatorMapping;
-import com.etotem.cfc.service.ChildNutritionProfileService;
+import com.etotem.cfc.service.FamilyMemberNutritionProfileService;
 import com.etotem.cfc.service.DimensionScoreService;
 import com.etotem.cfc.service.FoodService;
 import com.etotem.cfc.service.HealthAnalysisService;
@@ -65,7 +65,7 @@ public class HealthReportController {
     private NutritionDeficiencyService nutritionDeficiencyService;
 
     @Resource
-    private ChildNutritionProfileService childNutritionProfileService;
+    private FamilyMemberNutritionProfileService childNutritionProfileService;
 
     @Resource
     private HealthAnalysisService healthAnalysisService;

+ 4 - 4
cfc-backend/src/main/java/com/etotem/cfc/controller/InviteCardController.java

@@ -1,11 +1,11 @@
 package com.etotem.cfc.controller;
 
 import com.etotem.cfc.common.Result;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.Family;
 import com.etotem.cfc.entity.InviteCard;
 import com.etotem.cfc.entity.User;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.FamilyMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.InviteCardService;
@@ -31,7 +31,7 @@ public class InviteCardController {
     private InviteCardService inviteCardService;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private FamilyMapper familyMapper;
@@ -56,7 +56,7 @@ public class InviteCardController {
         }
 
         if ("child".equals(type)) {
-            Child child = childMapper.selectById(refId);
+            FamilyMember child = familyMemberMapper.selectById(refId);
             if (child == null) {
                 return Result.error("孩子不存在");
             }

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

@@ -2,11 +2,11 @@ package com.etotem.cfc.controller;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.common.Result;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.GameRecord;
 import com.etotem.cfc.entity.PointsLog;
 import com.etotem.cfc.entity.Task;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.PointsLogMapper;
 import com.etotem.cfc.service.GameRecordService;
 import com.etotem.cfc.service.TaskService;
@@ -25,7 +25,7 @@ import java.util.stream.Collectors;
 public class ReportStatsController {
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private PointsLogMapper pointsLogMapper;
@@ -43,7 +43,7 @@ public class ReportStatsController {
         Map<String, Object> result = new HashMap<>();
 
         // 基础信息
-        Child child = childMapper.selectById(childId);
+        FamilyMember child = familyMemberMapper.selectById(childId);
         result.put("totalPoints", child != null ? child.getTotalPoints() : 0);
 
         // 月活跃天数(当月有积分变动的天数)
@@ -149,15 +149,15 @@ public class ReportStatsController {
     @PostMapping("/family/summary")
     public Result<Map<String, Object>> familySummary(@RequestBody Map<String, Object> body) {
         Long familyId = Long.valueOf(body.get("familyId").toString());
-        List<Child> children = childMapper.selectList(
-                new LambdaQueryWrapper<Child>().eq(Child::getFamilyId, familyId));
+        List<FamilyMember> children = familyMemberMapper.selectList(
+                new LambdaQueryWrapper<FamilyMember>().eq(FamilyMember::getFamilyId, familyId));
 
         List<Map<String, Object>> childrenStats = new ArrayList<>();
         int familyTotalTasks = 0;
         int familyCompletedTasks = 0;
         int familyTotalGames = 0;
 
-        for (Child child : children) {
+        for (FamilyMember child : children) {
             Map<String, Object> childStat = new HashMap<>();
             childStat.put("childId", child.getId());
             childStat.put("childName", child.getNickname());
@@ -193,14 +193,14 @@ public class ReportStatsController {
     @PostMapping("/points/ranking")
     public Result<List<Map<String, Object>>> pointsRanking(@RequestBody Map<String, Object> body) {
         Long familyId = Long.valueOf(body.get("familyId").toString());
-        List<Child> children = childMapper.selectList(
-                new LambdaQueryWrapper<Child>()
-                        .eq(Child::getFamilyId, familyId)
-                        .orderByDesc(Child::getTotalPoints));
+        List<FamilyMember> children = familyMemberMapper.selectList(
+                new LambdaQueryWrapper<FamilyMember>()
+                        .eq(FamilyMember::getFamilyId, familyId)
+                        .orderByDesc(FamilyMember::getTotalPoints));
 
         List<Map<String, Object>> ranking = new ArrayList<>();
         int rank = 1;
-        for (Child child : children) {
+        for (FamilyMember child : children) {
             Map<String, Object> item = new HashMap<>();
             item.put("rank", rank++);
             item.put("childId", child.getId());

+ 4 - 4
cfc-backend/src/main/java/com/etotem/cfc/controller/StreakController.java

@@ -3,9 +3,9 @@ import javax.annotation.Resource;
 
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.entity.User;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.mapper.UserMapper;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.service.StreakService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -25,7 +25,7 @@ public class StreakController {
     private UserMapper userMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     /**
      * 获取孩子的打卡进度信息
@@ -43,7 +43,7 @@ public class StreakController {
                 return Result.error("用户不存在");
             }
 
-            Child child = childMapper.selectById(childId);
+            FamilyMember child = familyMemberMapper.selectById(childId);
             if (child == null) {
                 return Result.error("孩子不存在");
             }

+ 9 - 9
cfc-backend/src/main/java/com/etotem/cfc/controller/UserController.java

@@ -2,15 +2,15 @@ package com.etotem.cfc.controller;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.common.Result;
-import com.etotem.cfc.dto.ChildInfoDTO;
+import com.etotem.cfc.dto.FamilyMemberInfoDTO;
 import com.etotem.cfc.entity.AssessmentOrder;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.Family;
 import com.etotem.cfc.entity.PaymentOrder;
 import com.etotem.cfc.entity.Task;
 import com.etotem.cfc.entity.User;
 import com.etotem.cfc.mapper.AssessmentOrderMapper;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.FamilyMapper;
 import com.etotem.cfc.mapper.PaymentOrderMapper;
 import com.etotem.cfc.mapper.TaskMapper;
@@ -41,7 +41,7 @@ public class UserController {
     private FamilyMapper familyMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private PaymentOrderMapper paymentOrderMapper;
@@ -57,9 +57,9 @@ public class UserController {
 
     @PostMapping("/children/list")
     @Operation(summary = "获取子女列表")
-    public Result<List<ChildInfoDTO>> getChildren(HttpServletRequest request) {
+    public Result<List<FamilyMemberInfoDTO>> getFamilyMemberren(HttpServletRequest request) {
         Long userId = (Long) request.getAttribute("userId");
-        List<ChildInfoDTO> children = userService.getChildren(userId);
+        List<FamilyMemberInfoDTO> children = userService.getFamilyMemberren(userId);
         return Result.success(children);
     }
 
@@ -125,9 +125,9 @@ public class UserController {
                 .eq(User::getRole, "parent")
         );
 
-        List<Child> children = childMapper.selectList(
-            new LambdaQueryWrapper<Child>()
-                .eq(Child::getFamilyId, familyId)
+        List<FamilyMember> children = familyMemberMapper.selectList(
+            new LambdaQueryWrapper<FamilyMember>()
+                .eq(FamilyMember::getFamilyId, familyId)
         );
 
         Map<String, Object> result = new HashMap<>();

+ 16 - 16
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminController.java

@@ -27,7 +27,7 @@ public class AdminController {
     private UserMapper userMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private TaskMapper taskMapper;
@@ -142,23 +142,23 @@ public class AdminController {
 
     // 孩子管理
     @PostMapping("/children")
-    public Result<Page<Child>> getChildren(@RequestBody Map<String, Object> params) {
+    public Result<Page<FamilyMember>> getChildren(@RequestBody Map<String, Object> params) {
         int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
         int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 10;
-        Page<Child> pageParam = new Page<>(page, size);
-        Page<Child> result = childMapper.selectPage(pageParam, null);
+        Page<FamilyMember> pageParam = new Page<>(page, size);
+        Page<FamilyMember> result = familyMemberMapper.selectPage(pageParam, null);
         return Result.success(result);
     }
 
     @PostMapping("/children/{id}")
-    public Result<Child> getChild(@PathVariable Long id) {
-        return Result.success(childMapper.selectById(id));
+    public Result<FamilyMember> getChild(@PathVariable Long id) {
+        return Result.success(familyMemberMapper.selectById(id));
     }
 
     @PutMapping("/children/{id}")
-    public Result<Boolean> updateChild(@PathVariable Long id, @RequestBody Child child) {
+    public Result<Boolean> updateChild(@PathVariable Long id, @RequestBody FamilyMember child) {
         child.setId(id);
-        childMapper.updateById(child);
+        familyMemberMapper.updateById(child);
         return Result.success(true);
     }
 
@@ -169,7 +169,7 @@ public class AdminController {
         Integer amount = params.get("amount") != null ? ((Number) params.get("amount")).intValue() : 0;
         String reason = (String) params.get("reason");
 
-        Child child = childMapper.selectById(id);
+        FamilyMember child = familyMemberMapper.selectById(id);
         if (child == null) {
             return Result.error("孩子不存在");
         }
@@ -181,7 +181,7 @@ public class AdminController {
 
         child.setTotalPoints(newBalance);
         child.setUpdatedAt(new Date());
-        childMapper.updateById(child);
+        familyMemberMapper.updateById(child);
 
         // 记录积分流水
         PointsLog log = new PointsLog();
@@ -330,10 +330,10 @@ public class AdminController {
             reward.setExchangedAt(new Date());
         } else {
             // 拒绝,返还积分
-            Child child = childMapper.selectById(reward.getChildId());
+            FamilyMember child = familyMemberMapper.selectById(reward.getChildId());
             if (child != null) {
                 child.setTotalPoints(child.getTotalPoints() + reward.getPointsRequired());
-                childMapper.updateById(child);
+                familyMemberMapper.updateById(child);
 
                 PointsLog log = new PointsLog();
                 log.setChildId(reward.getChildId());
@@ -408,7 +408,7 @@ public class AdminController {
     public Result<Map<String, Object>> getPointsStats() {
         Map<String, Object> stats = new HashMap<>();
 
-        Long totalChildren = childMapper.selectCount(null);
+        Long totalChildren = familyMemberMapper.selectCount(null);
 
         stats.put("totalChildren", totalChildren);
 
@@ -450,9 +450,9 @@ public class AdminController {
         java.util.List<User> parents = userMapper.selectList(parentWrapper);
 
         // 查询孩子
-        LambdaQueryWrapper<Child> childWrapper = new LambdaQueryWrapper<>();
-        childWrapper.eq(Child::getFamilyId, id);
-        java.util.List<Child> children = childMapper.selectList(childWrapper);
+        LambdaQueryWrapper<FamilyMember> childWrapper = new LambdaQueryWrapper<>();
+        childWrapper.eq(FamilyMember::getFamilyId, id);
+        java.util.List<FamilyMember> children = familyMemberMapper.selectList(childWrapper);
 
         Map<String, Object> result = new HashMap<>();
         result.put("family", family);

+ 32 - 26
cfc-backend/src/main/java/com/etotem/cfc/controller/cart/CartController.java

@@ -1,10 +1,11 @@
 package com.etotem.cfc.controller.cart;
 
 import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.CartItemDTO;
 import com.etotem.cfc.service.CartService;
 import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestAttribute;
 import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
 import javax.annotation.Resource;
@@ -12,43 +13,48 @@ import java.util.List;
 import java.util.Map;
 
 @RestController
+@RequestMapping("/api/cart")
 public class CartController {
 
     @Resource
     private CartService cartService;
 
-    @PostMapping("/api/cart/list")
-    public Result<List<Map<String, Object>>> list(@RequestAttribute("userId") Long userId) {
-        return cartService.list(userId);
+    @PostMapping("/add")
+    public Result<CartItemDTO> add(@RequestBody Map<String, Object> params) {
+        Long userId = Long.valueOf(params.get("userId").toString());
+        Long productId = Long.valueOf(params.get("productId").toString());
+        Integer quantity = params.containsKey("quantity") ? Integer.valueOf(params.get("quantity").toString()) : 1;
+
+        CartItemDTO dto = cartService.addToCart(userId, productId, quantity);
+        if (dto == null) {
+            return Result.error("商品不存在");
+        }
+        return Result.success(dto);
     }
 
-    @PostMapping("/api/cart/add")
-    public Result<String> add(@RequestAttribute("userId") Long userId, @RequestBody Map<String, Object> body) {
-        Long productId = Long.valueOf(body.get("productId").toString());
-        return cartService.add(userId, productId);
+    @PostMapping("/list")
+    public Result<List<CartItemDTO>> list(@RequestBody Map<String, Object> params) {
+        Long userId = Long.valueOf(params.get("userId").toString());
+        List<CartItemDTO> list = cartService.getCartList(userId);
+        return Result.success(list);
     }
 
-    @PostMapping("/api/cart/update")
-    public Result<String> update(@RequestAttribute("userId") Long userId, @RequestBody Map<String, Object> body) {
-        Long productId = Long.valueOf(body.get("productId").toString());
-        Integer quantity = Integer.valueOf(body.get("quantity").toString());
-        return cartService.update(userId, productId, quantity);
-    }
+    @PostMapping("/update")
+    public Result<Void> update(@RequestBody Map<String, Object> params) {
+        Long userId = Long.valueOf(params.get("userId").toString());
+        Long productId = Long.valueOf(params.get("productId").toString());
+        Integer quantity = Integer.valueOf(params.get("quantity").toString());
 
-    @PostMapping("/api/cart/remove")
-    public Result<String> remove(@RequestAttribute("userId") Long userId, @RequestBody Map<String, Object> body) {
-        Long productId = Long.valueOf(body.get("productId").toString());
-        return cartService.remove(userId, productId);
+        cartService.updateQuantity(userId, productId, quantity);
+        return Result.success();
     }
 
-    @PostMapping("/api/cart/toggle")
-    public Result<String> toggle(@RequestAttribute("userId") Long userId, @RequestBody Map<String, Object> body) {
-        Long productId = Long.valueOf(body.get("productId").toString());
-        return cartService.toggle(userId, productId);
-    }
+    @PostMapping("/remove")
+    public Result<Void> remove(@RequestBody Map<String, Object> params) {
+        Long userId = Long.valueOf(params.get("userId").toString());
+        Long productId = Long.valueOf(params.get("productId").toString());
 
-    @PostMapping("/api/cart/clearSelected")
-    public Result<String> clearSelected(@RequestAttribute("userId") Long userId) {
-        return cartService.clearSelected(userId);
+        cartService.removeItem(userId, productId);
+        return Result.success();
     }
 }

+ 17 - 17
cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyUserController.java

@@ -3,10 +3,10 @@ package com.etotem.cfc.controller.family;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.dto.*;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.Family;
 import com.etotem.cfc.entity.User;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.FamilyMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.UserService;
@@ -35,7 +35,7 @@ public class FamilyUserController {
     private FamilyMapper familyMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     private Long getUserId(javax.servlet.http.HttpServletRequest request) {
         // 从请求属性中获取用户ID(由拦截器设置)
@@ -121,11 +121,11 @@ public class FamilyUserController {
 
     @Operation(summary = "家长切换到指定孩子身份")
     @PostMapping("/switch-to-child")
-    public Result<Long> switchToChild(javax.servlet.http.HttpServletRequest request,
+    public Result<Long> switchToFamilyMember(javax.servlet.http.HttpServletRequest request,
                                        @RequestBody Map<String, Long> body) {
         Long userId = getUserId(request);
         Long childId = body.get("childId");
-        Long result = userService.switchToChild(userId, childId);
+        Long result = userService.switchToFamilyMember(userId, childId);
         return Result.success(result);
     }
 
@@ -139,28 +139,28 @@ public class FamilyUserController {
 
     @Operation(summary = "创建孩子")
     @PostMapping("/children")
-    public Result<ChildInfoDTO> createChild(javax.servlet.http.HttpServletRequest request,
-                                            @RequestBody CreateChildDTO dto) {
+    public Result<FamilyMemberInfoDTO> createFamilyMember(javax.servlet.http.HttpServletRequest request,
+                                            @RequestBody CreateFamilyMemberDTO dto) {
         Long userId = getUserId(request);
-        ChildInfoDTO child = userService.createChild(userId, dto);
+        FamilyMemberInfoDTO child = userService.createFamilyMember(userId, dto);
         return Result.success(child);
     }
 
     @Operation(summary = "获取孩子列表")
     @PostMapping("/children/list")
-    public Result<List<ChildInfoDTO>> getChildren(javax.servlet.http.HttpServletRequest request) {
+    public Result<List<FamilyMemberInfoDTO>> getFamilyMemberren(javax.servlet.http.HttpServletRequest request) {
         Long userId = getUserId(request);
-        List<ChildInfoDTO> children = userService.getChildren(userId);
+        List<FamilyMemberInfoDTO> children = userService.getFamilyMemberren(userId);
         return Result.success(children);
     }
 
     @Operation(summary = "更新孩子信息")
     @PostMapping("/children/update")
-    public Result<Boolean> updateChild(javax.servlet.http.HttpServletRequest request,
-                                       @RequestBody UpdateChildDTO dto) {
+    public Result<Boolean> updateFamilyMember(javax.servlet.http.HttpServletRequest request,
+                                       @RequestBody UpdateFamilyMemberDTO dto) {
         Long userId = getUserId(request);
-        Long childId = dto.getChildId();
-        boolean success = userService.updateChild(userId, childId, dto);
+        Long childId = dto.getFamilyMemberId();
+        boolean success = userService.updateFamilyMember(userId, childId, dto);
         return Result.success(success);
     }
 
@@ -193,9 +193,9 @@ public class FamilyUserController {
         );
 
         // 查询孩子列表
-        List<Child> children = childMapper.selectList(
-            new LambdaQueryWrapper<Child>()
-                .eq(Child::getFamilyId, familyId)
+        List<FamilyMember> children = familyMemberMapper.selectList(
+            new LambdaQueryWrapper<FamilyMember>()
+                .eq(FamilyMember::getFamilyId, familyId)
         );
 
         Map<String, Object> result = new HashMap<>();

+ 22 - 22
cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideFamilyTaskController.java

@@ -7,10 +7,10 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.dto.TaskReviewDTO;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.Task;
 import com.etotem.cfc.entity.User;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.TaskMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.TaskService;
@@ -32,7 +32,7 @@ public class GuideFamilyTaskController {
     private TaskService taskService;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private TaskMapper taskMapper;
@@ -91,16 +91,16 @@ public class GuideFamilyTaskController {
             item.put("parentName", parent.getNickname());
 
             // 获取家庭孩子列表
-            List<Child> children = childMapper.selectList(
-                new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<Child>()
+            List<FamilyMember> children = familyMemberMapper.selectList(
+                new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<FamilyMember>()
                     .eq("family_id", familyId)
             );
             item.put("childrenCount", children.size());
 
             // 获取待审核任务数量
             long pendingCount = 0;
-            for (Child child : children) {
-                List<Task> pendingTasks = taskService.getPendingReviewTasksForChild(child.getId());
+            for (FamilyMember child : children) {
+                List<Task> pendingTasks = taskService.getPendingReviewTasksForFamilyMember(child.getId());
                 pendingCount += pendingTasks.size();
             }
             item.put("pendingReviewCount", pendingCount);
@@ -127,8 +127,8 @@ public class GuideFamilyTaskController {
             @PathVariable Long familyId) {
         if (!"teacher".equals(role)) { return Result.error("Access denied"); }
 
-        List<Child> children = childMapper.selectList(
-            new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<Child>()
+        List<FamilyMember> children = familyMemberMapper.selectList(
+            new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<FamilyMember>()
                 .eq("family_id", familyId)
         );
 
@@ -137,7 +137,7 @@ public class GuideFamilyTaskController {
         int totalCompleted = 0;
         int totalOverdue = 0;
 
-        for (Child child : children) {
+        for (FamilyMember child : children) {
             Map<String, Object> childInfo = new HashMap<>();
             childInfo.put("childId", child.getId());
             childInfo.put("childName", child.getNickname());
@@ -152,12 +152,12 @@ public class GuideFamilyTaskController {
                 .count());
 
             // 获取待审核任务
-            List<Task> pendingTasks = taskService.getPendingReviewTasksForChild(child.getId());
+            List<Task> pendingTasks = taskService.getPendingReviewTasksForFamilyMember(child.getId());
             childInfo.put("pendingReviewCount", pendingTasks.size());
             totalPending += pendingTasks.size();
 
             // 统计已完成和超时
-            List<Task> allTasks = taskService.getAllTasksByChild(child.getId());
+            List<Task> allTasks = taskService.getAllTasksByFamilyMember(child.getId());
             totalCompleted += allTasks.stream()
                 .filter(t -> "completed".equals(t.getStatus()))
                 .count();
@@ -171,7 +171,7 @@ public class GuideFamilyTaskController {
         Map<String, Object> result = new HashMap<>();
         result.put("familyId", familyId);
         result.put("children", childTaskList);
-        result.put("totalChildren", children.size());
+        result.put("totalFamilyMemberren", children.size());
         result.put("totalPendingReview", totalPending);
         result.put("totalCompleted", totalCompleted);
         result.put("totalOverdue", totalOverdue);
@@ -189,7 +189,7 @@ public class GuideFamilyTaskController {
         @ApiResponse(responseCode = "401", description = "未登录")
     })
     @PostMapping("/{familyId}/children/{childId}/tasks")
-    public Result<Page<Task>> getChildTasks(
+    public Result<Page<Task>> getFamilyMemberTasks(
             @PathVariable Long familyId,
             @PathVariable Long childId,
             @RequestAttribute("role") String role,
@@ -200,7 +200,7 @@ public class GuideFamilyTaskController {
         Integer size = params != null && params.get("size") != null ? Integer.valueOf(params.get("size").toString()) : 10;
         String status = params != null && params.get("status") != null ? params.get("status").toString() : null;
 
-        Page<Task> tasks = taskService.getTaskHistoryByChild(childId, page, size, status);
+        Page<Task> tasks = taskService.getTaskHistoryByFamilyMember(childId, page, size, status);
         return Result.success(tasks);
     }
 
@@ -220,7 +220,7 @@ public class GuideFamilyTaskController {
             @RequestAttribute("role") String role) {
 
         if (!"teacher".equals(role)) { return Result.error("Access denied"); }
-        List<Task> tasks = taskService.getPendingReviewTasksForChild(childId);
+        List<Task> tasks = taskService.getPendingReviewTasksForFamilyMember(childId);
         return Result.success(tasks);
     }
 
@@ -405,14 +405,14 @@ public class GuideFamilyTaskController {
             Date todayStart = Date.from(LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant());
 
             for (Long familyId : familyIds) {
-                List<Child> children = childMapper.selectList(
-                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Child>()
-                        .eq(Child::getFamilyId, familyId));
-                for (Child child : children) {
+                List<FamilyMember> children = familyMemberMapper.selectList(
+                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<FamilyMember>()
+                        .eq(FamilyMember::getFamilyId, familyId));
+                for (FamilyMember child : children) {
                     // 今日完成任务:status=completed 且 completedAt >= todayStart
                     long completed = taskMapper.selectCount(
                         new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Task>()
-                            .eq(Task::getChildId, child.getId())
+                            .eq(Task::getFamilyMemberId, child.getId())
                             .eq(Task::getStatus, "completed")
                             .ge(Task::getCompletedAt, todayStart));
                     todayCompleted += completed;
@@ -420,7 +420,7 @@ public class GuideFamilyTaskController {
                     // 待审核任务
                     long pending = taskMapper.selectCount(
                         new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Task>()
-                            .eq(Task::getChildId, child.getId())
+                            .eq(Task::getFamilyMemberId, child.getId())
                             .eq(Task::getNeedReview, 1)
                             .eq(Task::getStatus, "completed"));
                     pendingReview += pending;

+ 4 - 4
cfc-backend/src/main/java/com/etotem/cfc/controller/mind/EmotionAlertController.java

@@ -2,10 +2,10 @@ package com.etotem.cfc.controller.mind;
 
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.dto.EmotionAlertVO;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.EmotionAlert;
 import com.etotem.cfc.entity.User;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.EmotionAlertMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.EmotionAlertService;
@@ -36,7 +36,7 @@ public class EmotionAlertController {
     private EmotionAlertMapper emotionAlertMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private UserMapper userMapper;
@@ -97,7 +97,7 @@ public class EmotionAlertController {
             return Result.error("告警不存在");
         }
 
-        Child child = childMapper.selectById(alert.getChildId());
+        FamilyMember child = familyMemberMapper.selectById(alert.getChildId());
         if (child == null) {
             return Result.error("孩子不存在");
         }

+ 7 - 7
cfc-backend/src/main/java/com/etotem/cfc/controller/parent/ParentAssessmentController.java

@@ -7,7 +7,7 @@ import com.etotem.cfc.entity.AssessmentPointsConfig;
 import com.etotem.cfc.entity.AssessmentRecord;
 import com.etotem.cfc.entity.FamilyAssessmentConfig;
 import com.etotem.cfc.entity.User;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.AssessmentService;
 import io.swagger.v3.oas.annotations.Operation;
@@ -31,7 +31,7 @@ public class ParentAssessmentController {
     private UserMapper userMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Operation(summary = "获取当前测评任务")
     @PostMapping("/current")
@@ -100,7 +100,7 @@ public class ParentAssessmentController {
             return Result.error("暂无可用测评资料");
         }
 
-        Long childId = getChildIdByUserId(userId);
+        Long childId = getFamilyMemberIdByUserId(userId);
         AssessmentRecord record = assessmentService.createRecord(familyId, childId, material.getId());
 
         return Result.success(record);
@@ -118,12 +118,12 @@ public class ParentAssessmentController {
         return user != null ? user.getFamilyId() : null;
     }
 
-    private Long getChildIdByUserId(Long userId) {
+    private Long getFamilyMemberIdByUserId(Long userId) {
         User user = userMapper.selectById(userId);
         if (user == null) return null;
-        LambdaQueryWrapper<com.etotem.cfc.entity.Child> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(com.etotem.cfc.entity.Child::getFamilyId, user.getFamilyId());
-        com.etotem.cfc.entity.Child child = childMapper.selectOne(wrapper);
+        LambdaQueryWrapper<com.etotem.cfc.entity.FamilyMember> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(com.etotem.cfc.entity.FamilyMember::getFamilyId, user.getFamilyId());
+        com.etotem.cfc.entity.FamilyMember child = familyMemberMapper.selectOne(wrapper);
         return child != null ? child.getId() : null;
     }
 }

+ 5 - 5
cfc-backend/src/main/java/com/etotem/cfc/controller/stats/StatsController.java

@@ -2,14 +2,14 @@ package com.etotem.cfc.controller.stats;
 
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.etotem.cfc.common.Result;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.CommissionRecord;
 import com.etotem.cfc.entity.Family;
 import com.etotem.cfc.entity.MemberUpgradeRecord;
 import com.etotem.cfc.entity.Task;
 import com.etotem.cfc.entity.TrialMembership;
 import com.etotem.cfc.entity.User;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.CommissionRecordMapper;
 import com.etotem.cfc.mapper.FamilyMapper;
 import com.etotem.cfc.mapper.GuidePackageMapper;
@@ -46,7 +46,7 @@ public class StatsController {
     private UserMapper userMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private FamilyMapper familyMapper;
@@ -79,7 +79,7 @@ public class StatsController {
         CompletableFuture<Long> parentFuture = CompletableFuture.supplyAsync(() ->
             userMapper.selectCount(new QueryWrapper<User>().eq("role", "parent")), taskExecutor);
         CompletableFuture<Long> childFuture = CompletableFuture.supplyAsync(() ->
-            childMapper.selectCount(null), taskExecutor);
+            familyMemberMapper.selectCount(null), taskExecutor);
         CompletableFuture<Long> teacherFuture = CompletableFuture.supplyAsync(() ->
             userMapper.selectCount(new QueryWrapper<User>().eq("role", "teacher")), taskExecutor);
         CompletableFuture<Long> pendingGuideFuture = CompletableFuture.supplyAsync(() ->
@@ -158,7 +158,7 @@ public class StatsController {
             CompletableFuture<Long> parentFuture = CompletableFuture.supplyAsync(() ->
                 userMapper.selectCount(new QueryWrapper<User>().eq("role", "parent")), taskExecutor);
             CompletableFuture<Long> childFuture = CompletableFuture.supplyAsync(() ->
-                childMapper.selectCount(null), taskExecutor);
+                familyMemberMapper.selectCount(null), taskExecutor);
             CompletableFuture<Long> teacherFuture = CompletableFuture.supplyAsync(() ->
                 userMapper.selectCount(new QueryWrapper<User>().eq("role", "teacher")), taskExecutor);
 

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/AddFamilyMemberDTO.java

@@ -22,4 +22,7 @@ public class AddFamilyMemberDTO {
 
     /** 关系类型key(必填) */
     private String relationshipType;
+
+    /** 角色覆盖: auto/parent/child/elderly(选填,默认auto) */
+    private String roleOverride;
 }

+ 11 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/CreateProductOrderDTO.java

@@ -15,4 +15,15 @@ public class CreateProductOrderDTO {
     private Integer discountAmount;
     private Long couponId;
     private Integer deliveryMethod = 1; // 1=快递 2=自提
+
+    // Explicit getters needed due to Lombok issues
+    public List<OrderItemVO> getItems() { return items; }
+    public Long getProductId() { return productId; }
+    public Integer getQuantity() { return quantity; }
+    public String getPaymentMethod() { return paymentMethod; }
+    public String getRemark() { return remark; }
+    public Integer getDiscountAmount() { return discountAmount; }
+    public Long getCouponId() { return couponId; }
+    public String getAddressSnapshot() { return addressSnapshot; }
+    public Integer getDeliveryMethod() { return deliveryMethod; }
 }

+ 10 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/CreateWishDTO.java

@@ -23,4 +23,14 @@ public class CreateWishDTO {
     private Long childId;          // 目标孩子ID(家长创建时必填)
 
     private Integer pointsRequired; // 所需积分(家长创建时必填)
+
+    // Explicit setters (Lombok fallback)
+    public void setTitle(String title) { this.title = title; }
+    public void setDescription(String description) { this.description = description; }
+    public void setCategory(String category) { this.category = category; }
+    public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; }
+    public void setLinkUrl(String linkUrl) { this.linkUrl = linkUrl; }
+    public void setEstimatedPrice(Long estimatedPrice) { this.estimatedPrice = estimatedPrice; }
+    public void setChildId(Long childId) { this.childId = childId; }
+    public void setPointsRequired(Integer pointsRequired) { this.pointsRequired = pointsRequired; }
 }

+ 5 - 2
cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyMemberVO.java

@@ -34,8 +34,11 @@ public class FamilyMemberVO {
     /** 关系类型显示名 */
     private String relationshipTypeName;
 
-    /** 能力角色: parent/child */
-    private String capabilityRole;
+    /** 计算后的实际角色(前端展示用) */
+    private String effectiveRole;
+
+    /** 管理员设置的角色覆盖值 */
+    private String roleOverride;
 
     /** 角色标签显示文本(家长/孩子/配偶/父母等) */
     private String roleLabel;

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/HealthDimensionVO.java

@@ -11,6 +11,8 @@ public class HealthDimensionVO {
     private BigDecimal healthIndex;
     private List<DimensionItem> dimensions;
     private Date updateTime;
+    /** 是否为同龄同性平均值(无个人数据时展示参考值) */
+    private Boolean isAverage;
 
     @Data
     public static class DimensionItem {

+ 5 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/OrderItemVO.java

@@ -7,4 +7,9 @@ public class OrderItemVO {
     private Long productId;
     private Integer quantity = 1;
     private String coverImage;
+
+    // Explicit getters needed due to Lombok issues
+    public Long getProductId() { return productId; }
+    public Integer getQuantity() { return quantity; }
+    public String getCoverImage() { return coverImage; }
 }

+ 2 - 2
cfc-backend/src/main/java/com/etotem/cfc/dto/SwitchMemberVO.java

@@ -11,8 +11,8 @@ public class SwitchMemberVO {
     /** 成员来源: family_member / child */
     private String source;
 
-    /** 能力角色: parent/child(前端根据此设置currentRole) */
-    private String capabilityRole;
+    /** 计算后的实际角色(前端根据此设置 currentRole) */
+    private String effectiveRole;
 
     /** 成员昵称 */
     private String nickname;

+ 21 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/WishDTO.java

@@ -38,4 +38,25 @@ public class WishDTO {
     private String createdAt;
     private String exchangeRequestedAt;
     private String fulfilledAt;
+
+    // Explicit setters (Lombok fallback)
+    public void setId(Long id) { this.id = id; }
+    public void setFamilyId(Long familyId) { this.familyId = familyId; }
+    public void setChildId(Long childId) { this.childId = childId; }
+    public void setChildName(String childName) { this.childName = childName; }
+    public void setCreatedBy(Long createdBy) { this.createdBy = createdBy; }
+    public void setCreatorType(String creatorType) { this.creatorType = creatorType; }
+    public void setCreatorName(String creatorName) { this.creatorName = creatorName; }
+    public void setTitle(String title) { this.title = title; }
+    public void setDescription(String description) { this.description = description; }
+    public void setCategory(String category) { this.category = category; }
+    public void setPointsRequired(Integer pointsRequired) { this.pointsRequired = pointsRequired; }
+    public void setPointsSetBy(Long pointsSetBy) { this.pointsSetBy = pointsSetBy; }
+    public void setStatus(String status) { this.status = status; }
+    public void setStatusText(String statusText) { this.statusText = statusText; }
+    public void setCurrentPoints(Integer currentPoints) { this.currentPoints = currentPoints; }
+    public void setProgress(Integer progress) { this.progress = progress; }
+    public void setCreatedAt(String createdAt) { this.createdAt = createdAt; }
+    public void setExchangeRequestedAt(String exchangeRequestedAt) { this.exchangeRequestedAt = exchangeRequestedAt; }
+    public void setFulfilledAt(String fulfilledAt) { this.fulfilledAt = fulfilledAt; }
 }

+ 10 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/DanAssessmentResult.java

@@ -17,6 +17,16 @@ public class DanAssessmentResult implements Serializable {
 
     private Long childId;
 
+    private Long familyMemberId;
+
+    private String familyMemberName;
+
+    private Date familyMemberBirthday;
+
+    private String familyMemberGender;
+
+    private Integer familyMemberAge;
+
     private Long teacherId;
 
     private Long appointmentId;

+ 43 - 2
cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyMember.java

@@ -40,8 +40,8 @@ public class FamilyMember implements Serializable {
     /** 关系类型key(关联relationship_types.type_key) */
     private String relationshipType;
 
-    /** 能力角色: parent/child(决定界面权限) */
-    private String capabilityRole;
+    /** 角色覆盖: auto/parent/child/elderly,空=auto */
+    private String roleOverride;
 
     /** 对家庭成员可见: 1=可见, 0=不可见 */
     private Integer showToFamily;
@@ -49,6 +49,9 @@ public class FamilyMember implements Serializable {
     /** 总积分 */
     private Integer totalPoints;
 
+    /** 年龄(冗余字段,用于匹配时自动计算设置,getAge()通过birthday计算) */
+    private Integer age;
+
     /** 信任度 0-100 */
     private BigDecimal trustScore;
 
@@ -63,5 +66,43 @@ public class FamilyMember implements Serializable {
 
     private Date createdAt;
 
+    /** 孩子专属字段(原children表) */
+    /** 身份证号(孩子) */
+    private String idCard;
+
+    /** 是否扣减积分(0否1是) */
+    private Integer penaltyEnabled;
+
+    /** 主题 */
+    private String theme;
+
+    /** 系统积分(干预/平台奖励) */
+    private Integer systemPoints;
+
+    /** 连续打卡天数 */
+    private Integer streakDays;
+
+    /** 上次任务日期 */
+    private Date lastTaskDate;
+
+    /** 每日专注上限 */
+    private Integer focusMaxDaily;
+
+    /** 今日剩余专注次数 */
+    private Integer focusRemaining;
+
+    /** 专注次数重置日期 */
+    private Date focusResetDate;
+
     private Date updatedAt;
+
+    /**
+     * 计算年龄(根据birthday)
+     */
+    public int getAge() {
+        if (birthday == null) return 0;
+        java.time.LocalDate birth = birthday.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate();
+        java.time.LocalDate now = java.time.LocalDate.now();
+        return java.time.Period.between(birth, now).getYears();
+    }
 }

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/GrowthPlan.java

@@ -17,6 +17,8 @@ public class GrowthPlan implements Serializable {
 
     private Long childId;
 
+    private Long familyMemberId;
+
     private Long teacherId;
 
     private Long assessmentId;

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/GrowthRecord.java

@@ -17,6 +17,10 @@ public class GrowthRecord implements Serializable {
 
     private Long childId;
 
+    private Long familyMemberId;
+
+    private Integer familyMemberAge;
+
     private Long parentId;
 
     private Long assessmentId;

+ 15 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/MediaRecord.java

@@ -63,4 +63,19 @@ public class MediaRecord implements Serializable {
      * 创建时间
      */
     private Date createdAt;
+
+    // Explicit getters/setters needed due to Lombok issues
+    public void setType(String type) { this.type = type; }
+    public void setUrl(String url) { this.url = url; }
+    public void setTaskId(Long taskId) { this.taskId = taskId; }
+    public void setCreatorId(Long creatorId) { this.creatorId = creatorId; }
+    public void setDescription(String description) { this.description = description; }
+    public void setOriginalFilename(String originalFilename) { this.originalFilename = originalFilename; }
+    public void setFileSize(Long fileSize) { this.fileSize = fileSize; }
+    public void setContentType(String contentType) { this.contentType = contentType; }
+    public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; }
+    public Long getTaskId() { return taskId; }
+    public Date getCreatedAt() { return createdAt; }
+    public Long getCreatorId() { return creatorId; }
+    public String getUrl() { return url; }
 }

+ 19 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/PackageOrder.java

@@ -46,4 +46,23 @@ public class PackageOrder implements Serializable {
     private Date createdAt;
 
     private Date updatedAt;
+
+    // Explicit setters (Lombok fallback)
+    public void setId(Long id) { this.id = id; }
+    public void setOrderNo(String orderNo) { this.orderNo = orderNo; }
+    public void setUserId(Long userId) { this.userId = userId; }
+    public void setFamilyId(Long familyId) { this.familyId = familyId; }
+    public void setPackageId(Long packageId) { this.packageId = packageId; }
+    public void setPackageName(String packageName) { this.packageName = packageName; }
+    public void setPrice(Integer price) { this.price = price; }
+    public void setPlatformFee(Integer platformFee) { this.platformFee = platformFee; }
+    public void setUserCouponId(Long userCouponId) { this.userCouponId = userCouponId; }
+    public void setGuideId(Long guideId) { this.guideId = guideId; }
+    public void setStatus(String status) { this.status = status; }
+    public void setPayMethod(String payMethod) { this.payMethod = payMethod; }
+    public void setTransactionId(String transactionId) { this.transactionId = transactionId; }
+    public void setPaidAt(Date paidAt) { this.paidAt = paidAt; }
+    public void setNotifyData(String notifyData) { this.notifyData = notifyData; }
+    public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; }
+    public void setUpdatedAt(Date updatedAt) { this.updatedAt = updatedAt; }
 }

+ 1 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/PointsExchangeRecord.java

@@ -14,6 +14,7 @@ public class PointsExchangeRecord implements Serializable {
     private Long id;
     private Long userId;
     private Long childId;
+    private Long familyMemberId;
     private Long productId;
     private String productName;
     private Integer pointsCost;

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/PointsLog.java

@@ -17,6 +17,8 @@ private Long id;
 
 private Long childId;
 
+private Long familyMemberId;
+
 private Long taskId;
 
 private Integer amount;

+ 20 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/Product.java

@@ -44,4 +44,24 @@ public class Product implements Serializable {
 
     private Date createdAt;
     private Date updatedAt;
+
+    // Explicit getters needed due to Lombok issues
+    public Long getId() { return id; }
+    public String getName() { return name; }
+    public String getProductType() { return productType; }
+    public Integer getPrice() { return price; }
+    public String getCoverImage() { return coverImage; }
+    public Integer getStock() { return stock; }
+    public String getStatus() { return status; }
+    public String getMemberEligible() { return memberEligible; }
+    public String getDomain() { return domain; }
+    public Long getVendorId() { return vendorId; }
+    public String getVendorName() { return vendorName; }
+    public String getIntro() { return intro; }
+    public String getDescription() { return description; }
+    public String getImages() { return images; }
+    public Integer getMemberPrice() { return memberPrice; }
+    public Integer getSalesCount() { return salesCount; }
+    public Date getCreatedAt() { return createdAt; }
+    public Date getUpdatedAt() { return updatedAt; }
 }

+ 15 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/ProductOrder.java

@@ -48,4 +48,19 @@ public class ProductOrder implements Serializable {
     private Date refundTime;           // 退款时间
     private Date createdAt;
     private Date updatedAt;
+
+    // Explicit setters needed due to Lombok issues
+    public void setOrderNo(String orderNo) { this.orderNo = orderNo; }
+    public void setBuyerId(Long buyerId) { this.buyerId = buyerId; }
+    public void setFamilyId(Long familyId) { this.familyId = familyId; }
+    public void setQuantity(Integer quantity) { this.quantity = quantity; }
+    public void setTotalAmount(Integer totalAmount) { this.totalAmount = totalAmount; }
+    public void setStatus(String status) { this.status = status; }
+    public void setPaymentMethod(String paymentMethod) { this.paymentMethod = paymentMethod; }
+    public void setCoverImage(String coverImage) { this.coverImage = coverImage; }
+    public void setDeliveryMethod(Integer deliveryMethod) { this.deliveryMethod = deliveryMethod; }
+    public void setPickupCode(String pickupCode) { this.pickupCode = pickupCode; }
+    public void setCancelAt(Date cancelAt) { this.cancelAt = cancelAt; }
+    public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; }
+    public void setUpdatedAt(Date updatedAt) { this.updatedAt = updatedAt; }
 }

+ 2 - 2
cfc-backend/src/main/java/com/etotem/cfc/entity/RelationshipType.java

@@ -21,8 +21,8 @@ public class RelationshipType implements Serializable {
     /** 类型显示名: 配偶/父母/子女/兄弟姐妹 */
     private String typeName;
 
-    /** 能力角色: parent/child */
-    private String capabilityRole;
+    /** 默认角色模板: parent/child */
+    private String defaultRole;
 
     /** 排序序号 */
     private Integer sortOrder;

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/Task.java

@@ -17,6 +17,8 @@ private Long id;
 
 private Long familyId;
 
+private Long familyMemberId;
+
 private Long creatorId;
 
 // 执行者类型: child(孩子) 或 parent(家长)

+ 5 - 5
cfc-backend/src/main/java/com/etotem/cfc/service/ActivityService.java

@@ -7,11 +7,11 @@ import com.etotem.cfc.common.Result;
 import com.etotem.cfc.dto.ActivityDTO;
 import com.etotem.cfc.entity.Activity;
 import com.etotem.cfc.entity.ActivityRegistration;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.PointsLog;
 import com.etotem.cfc.entity.Task;
 import com.etotem.cfc.mapper.ActivityMapper;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.PointsLogMapper;
 import com.etotem.cfc.mapper.TaskMapper;
 import lombok.extern.slf4j.Slf4j;
@@ -33,7 +33,7 @@ public class ActivityService extends ServiceImpl<ActivityMapper, Activity> {
     private TaskMapper taskMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private PointsLogMapper pointsLogMapper;
@@ -220,7 +220,7 @@ public class ActivityService extends ServiceImpl<ActivityMapper, Activity> {
 
         // 5. Create task with status='completed' (auto-completed by check-in)
         Task task = new Task();
-        Child child = childMapper.selectById(childId);
+        FamilyMember child = familyMemberMapper.selectById(childId);
         if (child != null) {
             task.setFamilyId(child.getFamilyId());
         }
@@ -261,7 +261,7 @@ public class ActivityService extends ServiceImpl<ActivityMapper, Activity> {
         if (child != null) {
             child.setTotalPoints(child.getTotalPoints() + points);
             child.setUpdatedAt(new Date());
-            childMapper.updateById(child);
+            familyMemberMapper.updateById(child);
         }
 
         // 8. Insert PointsLog

+ 5 - 5
cfc-backend/src/main/java/com/etotem/cfc/service/ArticleService.java

@@ -7,13 +7,13 @@ import com.etotem.cfc.entity.Article;
 import com.etotem.cfc.entity.ArticleCategory;
 import com.etotem.cfc.entity.ArticleQuizRecord;
 import com.etotem.cfc.entity.ArticleReadingRecord;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.PointsLog;
 import com.etotem.cfc.entity.Task;
 import com.etotem.cfc.mapper.ArticleMapper;
 import com.etotem.cfc.mapper.ArticleQuizRecordMapper;
 import com.etotem.cfc.mapper.ArticleReadingRecordMapper;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.PointsLogMapper;
 import com.etotem.cfc.mapper.TaskMapper;
 import com.fasterxml.jackson.core.type.TypeReference;
@@ -49,7 +49,7 @@ public class ArticleService {
     private TaskMapper taskMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private PointsLogMapper pointsLogMapper;
@@ -238,7 +238,7 @@ public class ArticleService {
         task.setDeadline(cal.getTime());
 
         // Get child to set familyId
-        Child child = childMapper.selectById(childId);
+        FamilyMember child = familyMemberMapper.selectById(childId);
         if (child != null) {
             task.setFamilyId(child.getFamilyId());
         }
@@ -249,7 +249,7 @@ public class ArticleService {
         if (child != null) {
             child.setTotalPoints(child.getTotalPoints() + points);
             child.setUpdatedAt(new Date());
-            childMapper.updateById(child);
+            familyMemberMapper.updateById(child);
         }
 
         // 10. Insert PointsLog

+ 92 - 91
cfc-backend/src/main/java/com/etotem/cfc/service/CartService.java

@@ -1,130 +1,131 @@
 package com.etotem.cfc.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.etotem.cfc.common.Result;
-import com.etotem.cfc.entity.CartItem;
+import com.etotem.cfc.dto.CartItemDTO;
+import com.etotem.cfc.entity.Cart;
 import com.etotem.cfc.entity.Product;
-import com.etotem.cfc.mapper.CartItemMapper;
+import com.etotem.cfc.mapper.CartMapper;
 import com.etotem.cfc.mapper.ProductMapper;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
-import java.util.*;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
 
+@Slf4j
 @Service
 public class CartService {
 
     @Resource
-    private CartItemMapper cartItemMapper;
+    private CartMapper cartMapper;
 
     @Resource
     private ProductMapper productMapper;
 
-    public Result<List<Map<String, Object>>> list(Long userId) {
-        List<CartItem> items = cartItemMapper.selectList(
-            new LambdaQueryWrapper<CartItem>()
-                .eq(CartItem::getUserId, userId)
-                .orderByDesc(CartItem::getCreatedAt)
-        );
-        List<Map<String, Object>> result = new ArrayList<>();
-        for (CartItem item : items) {
-            Product product = productMapper.selectById(item.getProductId());
-            Map<String, Object> vo = new LinkedHashMap<>();
-            vo.put("productId", item.getProductId());
-            vo.put("quantity", item.getQuantity());
-            vo.put("selected", item.getSelected());
-            if (product != null) {
-                vo.put("productName", product.getName());
-                vo.put("coverImage", product.getCoverImage());
-                vo.put("unitPrice", product.getPrice());
-                vo.put("stock", product.getStock());
-                vo.put("invalid", !"on_shelf".equals(product.getStatus()));
-            } else {
-                vo.put("productName", "商品已删除");
-                vo.put("coverImage", "");
-                vo.put("unitPrice", 0);
-                vo.put("stock", 0);
-                vo.put("invalid", true);
-            }
-            result.add(vo);
-        }
-        return Result.success(result);
-    }
-
-    public Result<String> add(Long userId, Long productId) {
+    @Transactional
+    public CartItemDTO addToCart(Long userId, Long productId, Integer quantity) {
         Product product = productMapper.selectById(productId);
-        if (product == null) return Result.error("商品不存在");
-        if (!"on_shelf".equals(product.getStatus())) return Result.error("商品已下架");
+        if (product == null) {
+            log.warn("addToCart failed: product not found, productId={}", productId);
+            return null;
+        }
 
-        CartItem existing = cartItemMapper.selectOne(
-            new LambdaQueryWrapper<CartItem>()
-                .eq(CartItem::getUserId, userId)
-                .eq(CartItem::getProductId, productId)
+        Cart existing = cartMapper.selectOne(
+            new LambdaQueryWrapper<Cart>()
+                .eq(Cart::getUserId, userId)
+                .eq(Cart::getProductId, productId)
         );
+
         if (existing != null) {
-            existing.setQuantity(existing.getQuantity() + 1);
-            existing.setUpdatedAt(new Date());
-            cartItemMapper.updateById(existing);
+            existing.setQuantity(existing.getQuantity() + quantity);
+            existing.setUpdatedAt(LocalDateTime.now());
+            cartMapper.updateById(existing);
         } else {
-            CartItem item = new CartItem();
-            item.setUserId(userId);
-            item.setProductId(productId);
-            item.setQuantity(1);
-            item.setSelected(true);
-            item.setCreatedAt(new Date());
-            item.setUpdatedAt(new Date());
-            cartItemMapper.insert(item);
+            Cart cart = new Cart();
+            cart.setUserId(userId);
+            cart.setProductId(productId);
+            cart.setQuantity(quantity);
+            cart.setCreatedAt(LocalDateTime.now());
+            cart.setUpdatedAt(LocalDateTime.now());
+            cartMapper.insert(cart);
         }
-        return Result.success("已加入购物车");
+
+        return getCartItemDTO(userId, productId);
     }
 
-    public Result<String> update(Long userId, Long productId, Integer quantity) {
-        CartItem item = cartItemMapper.selectOne(
-            new LambdaQueryWrapper<CartItem>()
-                .eq(CartItem::getUserId, userId)
-                .eq(CartItem::getProductId, productId)
+    public List<CartItemDTO> getCartList(Long userId) {
+        List<Cart> cartList = cartMapper.selectList(
+            new LambdaQueryWrapper<Cart>()
+                .eq(Cart::getUserId, userId)
+                .orderByDesc(Cart::getCreatedAt)
         );
-        if (item == null) return Result.error("购物车记录不存在");
 
-        if (quantity <= 0) {
-            cartItemMapper.deleteById(item.getId());
-            return Result.success("已移除");
+        List<CartItemDTO> result = new ArrayList<>();
+        for (Cart item : cartList) {
+            CartItemDTO dto = buildCartItemDTO(item);
+            if (dto != null) {
+                result.add(dto);
+            }
         }
-        item.setQuantity(quantity);
-        item.setUpdatedAt(new Date());
-        cartItemMapper.updateById(item);
-        return Result.success("更新成功");
+        return result;
     }
 
-    public Result<String> remove(Long userId, Long productId) {
-        cartItemMapper.delete(
-            new LambdaQueryWrapper<CartItem>()
-                .eq(CartItem::getUserId, userId)
-                .eq(CartItem::getProductId, productId)
+    @Transactional
+    public void updateQuantity(Long userId, Long productId, Integer quantity) {
+        Cart cart = cartMapper.selectOne(
+            new LambdaQueryWrapper<Cart>()
+                .eq(Cart::getUserId, userId)
+                .eq(Cart::getProductId, productId)
         );
-        return Result.success("已移除");
+
+        if (cart != null) {
+            if (quantity <= 0) {
+                cartMapper.deleteById(cart.getId());
+                return;
+            }
+            cart.setQuantity(quantity);
+            cart.setUpdatedAt(LocalDateTime.now());
+            cartMapper.updateById(cart);
+        }
     }
 
-    public Result<String> toggle(Long userId, Long productId) {
-        CartItem item = cartItemMapper.selectOne(
-            new LambdaQueryWrapper<CartItem>()
-                .eq(CartItem::getUserId, userId)
-                .eq(CartItem::getProductId, productId)
+    @Transactional
+    public void removeItem(Long userId, Long productId) {
+        cartMapper.delete(
+            new LambdaQueryWrapper<Cart>()
+                .eq(Cart::getUserId, userId)
+                .eq(Cart::getProductId, productId)
         );
-        if (item == null) return Result.error("购物车记录不存在");
-
-        item.setSelected(!Boolean.TRUE.equals(item.getSelected()));
-        item.setUpdatedAt(new Date());
-        cartItemMapper.updateById(item);
-        return Result.success("操作成功");
     }
 
-    public Result<String> clearSelected(Long userId) {
-        cartItemMapper.delete(
-            new LambdaQueryWrapper<CartItem>()
-                .eq(CartItem::getUserId, userId)
-                .eq(CartItem::getSelected, true)
+    private CartItemDTO getCartItemDTO(Long userId, Long productId) {
+        Cart cart = cartMapper.selectOne(
+            new LambdaQueryWrapper<Cart>()
+                .eq(Cart::getUserId, userId)
+                .eq(Cart::getProductId, productId)
         );
-        return Result.success("已清空");
+        if (cart == null) return null;
+        return buildCartItemDTO(cart);
+    }
+
+    private CartItemDTO buildCartItemDTO(Cart cart) {
+        Product product = productMapper.selectById(cart.getProductId());
+        CartItemDTO dto = new CartItemDTO();
+        dto.setId(cart.getId());
+        dto.setUserId(cart.getUserId());
+        dto.setProductId(cart.getProductId());
+        dto.setQuantity(cart.getQuantity());
+        if (product != null) {
+            dto.setProductName(product.getName());
+            dto.setCoverImage(product.getCoverImage());
+            // Convert price from 分(integer) to 元(BigDecimal)
+            dto.setProductPrice(BigDecimal.valueOf(product.getPrice() != null ? product.getPrice() : 0)
+                .divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_HALF_UP));
+        }
+        return dto;
     }
 }

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

@@ -1,200 +0,0 @@
-package com.etotem.cfc.service;
-
-import com.etotem.cfc.entity.Child;
-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.ChildMapper;
-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 ChildNutritionProfileService {
-
-    @Resource
-    private HealthReportService healthReportService;
-
-    @Resource
-    private HealthCheckinService healthCheckinService;
-
-    @Resource
-    private NutritionDeficiencyService nutritionDeficiencyService;
-
-    @Resource
-    private UserNutritionProfileService userNutritionProfileService;
-
-    @Resource
-    private ChildMapper childMapper;
-
-    public Map<String, Object> getProfile(Long childId) {
-        Map<String, Object> profile = new HashMap<>();
-
-        Child child = childMapper.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;
-    }
-}

+ 26 - 12
cfc-backend/src/main/java/com/etotem/cfc/service/CognitiveService.java

@@ -1,10 +1,10 @@
 package com.etotem.cfc.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.DanAssessmentResult;
 import com.etotem.cfc.entity.User;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.DanAssessmentResultMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import lombok.extern.slf4j.Slf4j;
@@ -27,7 +27,7 @@ public class CognitiveService {
     private UserMapper userMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     /**
      * 获取指定孩子的最新认知六维得分
@@ -35,15 +35,29 @@ public class CognitiveService {
      * @param childId 孩子ID
      * @return DanAssessmentResult(最新一条记录)
      */
-    public DanAssessmentResult getChildLatestResult(Long childId) {
+    public DanAssessmentResult getFamilyMemberLatestResult(Long childId) {
         LambdaQueryWrapper<DanAssessmentResult> wrapper = new LambdaQueryWrapper<DanAssessmentResult>()
-                .eq(DanAssessmentResult::getChildId, childId)
+                .eq(DanAssessmentResult::getFamilyMemberId, childId)
                 .eq(DanAssessmentResult::getStatus, "completed")
                 .orderByDesc(DanAssessmentResult::getAssessmentDate)
                 .last("LIMIT 1");
         return danAssessmentResultMapper.selectOne(wrapper);
     }
 
+    /**
+     * 获取指定孩子的最新认知六维得分(别名,兼容旧调用)
+     */
+    public DanAssessmentResult getChildLatestResult(Long childId) {
+        return getFamilyMemberLatestResult(childId);
+    }
+
+    /**
+     * 更新指定孩子的认知维度得分(别名,兼容旧调用)
+     */
+    public boolean updateChildDimensionScore(Long childId, String dimension, Integer score) {
+        return updateFamilyMemberDimensionScore(childId, dimension, score);
+    }
+
     /**
      * 获取家庭所有孩子的最新认知六维得分
      *
@@ -58,15 +72,15 @@ public class CognitiveService {
         }
 
         // 2. 查找该家庭下所有孩子
-        List<Child> children = childMapper.selectList(
-                new LambdaQueryWrapper<Child>()
-                        .eq(Child::getFamilyId, user.getFamilyId())
+        List<FamilyMember> children = familyMemberMapper.selectList(
+                new LambdaQueryWrapper<FamilyMember>()
+                        .eq(FamilyMember::getFamilyId, user.getFamilyId())
         );
 
         // 3. 获取每个孩子的最新测评结果
         Map<Long, DanAssessmentResult> resultMap = new LinkedHashMap<>();
-        for (Child child : children) {
-            DanAssessmentResult latest = getChildLatestResult(child.getId());
+        for (FamilyMember child : children) {
+            DanAssessmentResult latest = getFamilyMemberLatestResult(child.getId());
             if (latest != null) {
                 resultMap.put(child.getId(), latest);
             }
@@ -82,8 +96,8 @@ public class CognitiveService {
      * @param score     新得分
      * @return true=更新成功, false=未找到最新结果或维度不存在
      */
-    public boolean updateChildDimensionScore(Long childId, String dimension, Integer score) {
-        DanAssessmentResult latest = getChildLatestResult(childId);
+    public boolean updateFamilyMemberDimensionScore(Long childId, String dimension, Integer score) {
+        DanAssessmentResult latest = getFamilyMemberLatestResult(childId);
         if (latest == null) {
             log.warn("未找到孩子 {} 的最新测评结果,无法更新", childId);
             return false;

+ 4 - 4
cfc-backend/src/main/java/com/etotem/cfc/service/CognitiveTrainingConfigService.java

@@ -3,10 +3,10 @@ package com.etotem.cfc.service;
 import com.etotem.cfc.dto.CognitiveRecommendationDTO;
 import com.etotem.cfc.dto.CreateTaskDTO;
 import com.etotem.cfc.entity.Article;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.DanAssessmentResult;
 import com.etotem.cfc.mapper.ArticleMapper;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.DanAssessmentResultMapper;
 import lombok.Data;
 import lombok.extern.slf4j.Slf4j;
@@ -35,7 +35,7 @@ public class CognitiveTrainingConfigService implements InitializingBean {
     private DanAssessmentResultMapper danAssessmentResultMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private ArticleMapper articleMapper;
@@ -147,7 +147,7 @@ public class CognitiveTrainingConfigService implements InitializingBean {
         CognitiveRecommendationDTO dto = new CognitiveRecommendationDTO();
         dto.setChildId(childId);
 
-        Child child = childMapper.selectById(childId);
+        FamilyMember child = familyMemberMapper.selectById(childId);
         dto.setChildName(child != null ? child.getNickname() : "");
 
         List<CognitiveRecommendationDTO.DimensionRecommendation> recs = new ArrayList<>();

+ 42 - 42
cfc-backend/src/main/java/com/etotem/cfc/service/DataMigrationService.java

@@ -28,7 +28,7 @@ public class DataMigrationService implements DataMigrationServiceInterface {
     @Resource
     private UserMapper userMapper;
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
     @Resource
     private TeacherMapper teacherMapper;
     @Resource
@@ -45,7 +45,7 @@ public class DataMigrationService implements DataMigrationServiceInterface {
     // ============ 统计结果 ============
     private int migratedFamilies;
     private int migratedParents;
-    private int migratedChildren;
+    private int migratedFamilyMemberren;
     private int migratedTeachers;
     private int migratedOrders;
     private int migratedResults;
@@ -55,7 +55,7 @@ public class DataMigrationService implements DataMigrationServiceInterface {
 
     // ============ ID 映射缓存 ============
     private final Map<String, Long> sfmsPhoneToFamilyId = new HashMap<>();
-    private final Map<Long, Long> sfmsClientIdToChildUserId = new HashMap<>();
+    private final Map<Long, Long> sfmsClientIdToFamilyMemberUserId = new HashMap<>();
     private final Map<Long, Long> sfmsClientIdToFamilyId = new HashMap<>();
     private final Map<String, Long> sfmsOpenIdToTeacherUserId = new HashMap<>();
 
@@ -68,7 +68,7 @@ public class DataMigrationService implements DataMigrationServiceInterface {
             // 按依赖顺序执行
             migrateFamilies();
             migrateParentUsers();
-            migrateChildUsersAndChildren();
+            migrateFamilyMemberUsersAndFamilyMemberren();
             migrateTeachers();
             migrateGuideFamilies();
             migratePackageOrders();
@@ -78,7 +78,7 @@ public class DataMigrationService implements DataMigrationServiceInterface {
 
             log.info("========== SFMS 数据迁移完成,耗时: {}ms ==========", System.currentTimeMillis() - start);
             log.info("迁移统计: {} 家庭, {} 家长, {} 孩子, {} 规划师, {} 订单, {} 测评结果, {} 成长档案, {} 成长方案",
-                migratedFamilies, migratedParents, migratedChildren, migratedTeachers,
+                migratedFamilies, migratedParents, migratedFamilyMemberren, migratedTeachers,
                 migratedOrders, migratedResults, migratedRecords, migratedPlans);
         } catch (Exception e) {
             log.error("迁移过程中出错", e);
@@ -91,7 +91,7 @@ public class DataMigrationService implements DataMigrationServiceInterface {
     private void resetCounters() {
         migratedFamilies = 0;
         migratedParents = 0;
-        migratedChildren = 0;
+        migratedFamilyMemberren = 0;
         migratedTeachers = 0;
         migratedOrders = 0;
         migratedResults = 0;
@@ -99,7 +99,7 @@ public class DataMigrationService implements DataMigrationServiceInterface {
         migratedPlans = 0;
         errors.clear();
         sfmsPhoneToFamilyId.clear();
-        sfmsClientIdToChildUserId.clear();
+        sfmsClientIdToFamilyMemberUserId.clear();
         sfmsClientIdToFamilyId.clear();
         sfmsOpenIdToTeacherUserId.clear();
     }
@@ -217,7 +217,7 @@ public class DataMigrationService implements DataMigrationServiceInterface {
     //  3. 孩子用户 + children 表迁移
     // ================================================================
     @Transactional
-    public void migrateChildUsersAndChildren() {
+    public void migrateFamilyMemberUsersAndFamilyMemberren() {
         log.info("--- 3/9: 迁移孩子用户 ---");
         try {
             SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
@@ -261,15 +261,15 @@ public class DataMigrationService implements DataMigrationServiceInterface {
                     // 孩子 openid
                     String childOpenid = (openid != null && !openid.isEmpty()) ? openid + "_child_" + sfmsClientId : "migrated_child_" + sfmsClientId;
 
-                    User existingChildUser = userMapper.selectOne(
+                    User existingFamilyMemberUser = userMapper.selectOne(
                         new LambdaQueryWrapper<User>()
                             .eq(User::getOpenid, childOpenid)
                             .last("LIMIT 1")
                     );
-                    if (existingChildUser != null) {
-                        sfmsClientIdToChildUserId.put(sfmsClientId, existingChildUser.getId());
-                        if (existingChildUser.getFamilyId() != null) {
-                            sfmsClientIdToFamilyId.put(sfmsClientId, existingChildUser.getFamilyId());
+                    if (existingFamilyMemberUser != null) {
+                        sfmsClientIdToFamilyMemberUserId.put(sfmsClientId, existingFamilyMemberUser.getId());
+                        if (existingFamilyMemberUser.getFamilyId() != null) {
+                            sfmsClientIdToFamilyId.put(sfmsClientId, existingFamilyMemberUser.getFamilyId());
                         }
                         count++;
                         if (count % 100 == 0) {
@@ -296,7 +296,7 @@ public class DataMigrationService implements DataMigrationServiceInterface {
                     userMapper.insert(childUser);
 
                     // 创建 children 记录
-                    Child child = new Child();
+                    FamilyMember child = new FamilyMember();
                     child.setUserId(childUser.getId());
                     child.setFamilyId(familyId);
                     child.setNickname((String) row.get("name"));
@@ -308,9 +308,9 @@ public class DataMigrationService implements DataMigrationServiceInterface {
                     child.setStreakDays(0);
                     child.setFocusMaxDaily(3);
                     child.setFocusRemaining(3);
-                    childMapper.insert(child);
+                    familyMemberMapper.insert(child);
 
-                    sfmsClientIdToChildUserId.put(sfmsClientId, childUser.getId());
+                    sfmsClientIdToFamilyMemberUserId.put(sfmsClientId, childUser.getId());
                     sfmsClientIdToFamilyId.put(sfmsClientId, familyId);
                     count++;
                     if (count % 100 == 0) {
@@ -320,8 +320,8 @@ public class DataMigrationService implements DataMigrationServiceInterface {
                     log.warn("迁移孩子失败 clientId={}: {}", row.get("crm_client_info_id"), e.getMessage());
                 }
             }
-            migratedChildren = count;
-            log.info("  迁移 {} 个孩子", migratedChildren);
+            migratedFamilyMemberren = count;
+            log.info("  迁移 {} 个孩子", migratedFamilyMemberren);
         } catch (Exception e) {
             log.error("迁移孩子出错", e);
             errors.add("迁移孩子出错: " + e.getMessage());
@@ -460,7 +460,7 @@ public class DataMigrationService implements DataMigrationServiceInterface {
             for (Map<String, Object> row : rows) {
                 try {
                     long sfmsClientId = ((Number) row.get("client_id")).longValue();
-                    Long userId = sfmsClientIdToChildUserId.get(sfmsClientId);
+                    Long userId = sfmsClientIdToFamilyMemberUserId.get(sfmsClientId);
                     Long familyId = sfmsClientIdToFamilyId.get(sfmsClientId);
                     if (userId == null || familyId == null) continue;
 
@@ -513,13 +513,13 @@ public class DataMigrationService implements DataMigrationServiceInterface {
             for (Map<String, Object> row : rows) {
                 try {
                     long sfmsClientId = ((Number) row.get("client_id")).longValue();
-                    Long childUserId = sfmsClientIdToChildUserId.get(sfmsClientId);
+                    Long childUserId = sfmsClientIdToFamilyMemberUserId.get(sfmsClientId);
                     if (childUserId == null) continue;
 
                     // 找 child_id
-                    Child child = childMapper.selectOne(
-                        new LambdaQueryWrapper<Child>()
-                            .eq(Child::getUserId, childUserId)
+                    FamilyMember child = familyMemberMapper.selectOne(
+                        new LambdaQueryWrapper<FamilyMember>()
+                            .eq(FamilyMember::getUserId, childUserId)
                             .last("LIMIT 1")
                     );
                     if (child == null) continue;
@@ -541,11 +541,11 @@ public class DataMigrationService implements DataMigrationServiceInterface {
                     }
 
                     DanAssessmentResult result = new DanAssessmentResult();
-                    result.setChildId(child.getId());
-                    result.setChildName(childName);
-                    result.setChildBirthday(childBirthday);
-                    result.setChildGender(childGender);
-                    result.setChildAge(childAge);
+                    result.setFamilyMemberId(child.getId());
+                    result.setFamilyMemberName(childName);
+                    result.setFamilyMemberBirthday(childBirthday);
+                    result.setFamilyMemberGender(childGender);
+                    result.setFamilyMemberAge(childAge);
                     result.setTeacherId(teacherUserId != null ? teacherUserId : 0L);
                     result.setAnalysisReport((String) row.get("comm_cont"));
                     result.setProcessDesc((String) row.get("description"));
@@ -600,7 +600,7 @@ public class DataMigrationService implements DataMigrationServiceInterface {
                 "LEFT JOIN (SELECT client_id, value FROM crm_client_info_properties WHERE CODE = 'nation') cip_nation ON cip_nation.client_id = ci.crm_client_info_id " +
                 "LEFT JOIN (SELECT client_id, value FROM crm_client_info_properties WHERE CODE = 'parentDegree') cip_degree ON cip_degree.client_id = ci.crm_client_info_id " +
                 "LEFT JOIN (SELECT client_id, value FROM crm_client_info_properties WHERE CODE = 'familyStatus') cip_family ON cip_family.client_id = ci.crm_client_info_id " +
-                "LEFT JOIN (SELECT client_id, value FROM crm_client_info_properties WHERE CODE = 'singleChild') cip_single ON cip_single.client_id = ci.crm_client_info_id " +
+                "LEFT JOIN (SELECT client_id, value FROM crm_client_info_properties WHERE CODE = 'singleFamilyMember') cip_single ON cip_single.client_id = ci.crm_client_info_id " +
                 "LEFT JOIN (SELECT client_id, value FROM crm_client_info_properties WHERE CODE = 'schoolScore') cip_score ON cip_score.client_id = ci.crm_client_info_id " +
                 "LEFT JOIN (SELECT client_id, value FROM crm_client_info_properties WHERE CODE = 'address') cip_address ON cip_address.client_id = ci.crm_client_info_id "
             );
@@ -608,13 +608,13 @@ public class DataMigrationService implements DataMigrationServiceInterface {
             for (Map<String, Object> row : rows) {
                 try {
                     long sfmsClientId = ((Number) row.get("crm_client_info_id")).longValue();
-                    Long childUserId = sfmsClientIdToChildUserId.get(sfmsClientId);
+                    Long childUserId = sfmsClientIdToFamilyMemberUserId.get(sfmsClientId);
                     Long familyId = sfmsClientIdToFamilyId.get(sfmsClientId);
                     if (childUserId == null || familyId == null) continue;
 
-                    Child child = childMapper.selectOne(
-                        new LambdaQueryWrapper<Child>()
-                            .eq(Child::getUserId, childUserId)
+                    FamilyMember child = familyMemberMapper.selectOne(
+                        new LambdaQueryWrapper<FamilyMember>()
+                            .eq(FamilyMember::getUserId, childUserId)
                             .last("LIMIT 1")
                     );
                     if (child == null) continue;
@@ -628,10 +628,10 @@ public class DataMigrationService implements DataMigrationServiceInterface {
                     );
 
                     GrowthRecord record = new GrowthRecord();
-                    record.setChildId(child.getId());
+                    record.setFamilyMemberId(child.getId());
                     record.setParentId(parent != null ? parent.getId() : 0L);
                     record.setRecordTitle("历史成长档案 - " + (String) row.get("name"));
-                    record.setChildAge(calcAge((String) row.get("birth_day")));
+                    record.setFamilyMemberAge(calcAge((String) row.get("birth_day")));
                     record.setHeight((String) row.get("height"));
                     record.setWeight((String) row.get("weight"));
                     record.setStatus("completed");
@@ -684,18 +684,18 @@ public class DataMigrationService implements DataMigrationServiceInterface {
             for (Map<String, Object> row : rows) {
                 try {
                     long sfmsClientId = ((Number) row.get("client_id")).longValue();
-                    Long childUserId = sfmsClientIdToChildUserId.get(sfmsClientId);
+                    Long childUserId = sfmsClientIdToFamilyMemberUserId.get(sfmsClientId);
                     if (childUserId == null) continue;
 
-                    Child child = childMapper.selectOne(
-                        new LambdaQueryWrapper<Child>()
-                            .eq(Child::getUserId, childUserId)
+                    FamilyMember child = familyMemberMapper.selectOne(
+                        new LambdaQueryWrapper<FamilyMember>()
+                            .eq(FamilyMember::getUserId, childUserId)
                             .last("LIMIT 1")
                     );
                     if (child == null) continue;
 
                     GrowthPlan plan = new GrowthPlan();
-                    plan.setChildId(child.getId());
+                    plan.setFamilyMemberId(child.getId());
                     plan.setPlanTitle((String) row.get("plan_name"));
                     plan.setPlanContent((String) row.get("plan_desc"));
                     plan.setStatus("active".equals(row.get("plan_status")) ? "active" : "draft");
@@ -807,13 +807,13 @@ public class DataMigrationService implements DataMigrationServiceInterface {
         result.put("success", errors.isEmpty());
         result.put("migratedFamilies", migratedFamilies);
         result.put("migratedParents", migratedParents);
-        result.put("migratedChildren", migratedChildren);
+        result.put("migratedFamilyMemberren", migratedFamilyMemberren);
         result.put("migratedTeachers", migratedTeachers);
         result.put("migratedOrders", migratedOrders);
         result.put("migratedResults", migratedResults);
         result.put("migratedRecords", migratedRecords);
         result.put("migratedPlans", migratedPlans);
-        result.put("total", migratedFamilies + migratedParents + migratedChildren + migratedTeachers
+        result.put("total", migratedFamilies + migratedParents + migratedFamilyMemberren + migratedTeachers
             + migratedOrders + migratedResults + migratedRecords + migratedPlans);
         if (!errors.isEmpty()) {
             result.put("errors", errors);

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/DatabaseSyncService.java

@@ -21,6 +21,8 @@ import java.util.stream.Collectors;
 @Slf4j
 @Service
 public class DatabaseSyncService {
+    // Explicit log field since @Slf4j Lombok not generating it
+    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DatabaseSyncService.class);
 
     @Resource
     private JdbcTemplate jdbcTemplate;

+ 4 - 4
cfc-backend/src/main/java/com/etotem/cfc/service/EmotionAlertService.java

@@ -2,11 +2,11 @@ package com.etotem.cfc.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.dto.EmotionAlertVO;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.EmotionAlert;
 import com.etotem.cfc.entity.EmotionCheckin;
 import com.etotem.cfc.entity.FollowUp;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.EmotionAlertMapper;
 import com.etotem.cfc.mapper.EmotionCheckinMapper;
 import com.etotem.cfc.mapper.FollowUpMapper;
@@ -37,7 +37,7 @@ public class EmotionAlertService {
     private FollowUpMapper followUpMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private WechatAlertTemplateService wechatAlertTemplateService;
@@ -170,7 +170,7 @@ public class EmotionAlertService {
             try {
                 FollowUp followUp = new FollowUp();
                 followUp.setChildId(alert.getChildId());
-                Child child = childMapper.selectById(alert.getChildId());
+                FamilyMember child = familyMemberMapper.selectById(alert.getChildId());
                 if (child != null && child.getFamilyId() != null) {
                     followUp.setFamilyId(child.getFamilyId());
                 }

+ 6 - 6
cfc-backend/src/main/java/com/etotem/cfc/service/EmotionCheckinService.java

@@ -8,9 +8,9 @@ import com.etotem.cfc.dto.EmotionStatsVO;
 import com.etotem.cfc.dto.EmotionTrendVO;
 import com.etotem.cfc.dto.MoodTrendPoint;
 import com.etotem.cfc.dto.AffectPoint;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.EmotionCheckin;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.EmotionCheckinMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -28,7 +28,7 @@ public class EmotionCheckinService {
     private EmotionCheckinMapper emotionCheckinMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private EnergyService energyService;
@@ -38,7 +38,7 @@ public class EmotionCheckinService {
 
     @Transactional
     public EmotionCheckinVO createCheckin(EmotionCheckinDTO dto, Long userId) {
-        Child child = childMapper.selectById(dto.getChildId());
+        FamilyMember child = familyMemberMapper.selectById(dto.getChildId());
         if (child == null) {
             throw new RuntimeException("孩子不存在");
         }
@@ -90,7 +90,7 @@ public class EmotionCheckinService {
                 .orderByDesc(EmotionCheckin::getCheckinDate);
         List<EmotionCheckin> list = emotionCheckinMapper.selectList(wrapper);
 
-        Child child = childMapper.selectById(childId);
+        FamilyMember child = familyMemberMapper.selectById(childId);
         String childName = child != null ? child.getNickname() : "";
 
         int start = (page - 1) * size;
@@ -113,7 +113,7 @@ public class EmotionCheckinService {
         EmotionCheckin checkin = emotionCheckinMapper.selectOne(wrapper);
         if (checkin == null) return null;
 
-        Child child = childMapper.selectById(childId);
+        FamilyMember child = familyMemberMapper.selectById(childId);
         String childName = child != null ? child.getNickname() : "";
         boolean isOwner = currentUserId != null && currentUserId.equals(childId);
         return toVO(checkin, childName, hidePhoto && !isOwner);

+ 25 - 17
cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java

@@ -28,7 +28,7 @@ public class EnergyService {
     private UserMapper userMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private TaskMapper taskMapper;
@@ -64,6 +64,9 @@ public class EnergyService {
      * @return 家庭能量沙盘DTO(家庭聚合 + 每个成员的个人数据)
      */
     public EnergySandboxDTO calculateFamilyEnergy(Long familyId) {
+        if (familyId == null) {
+            return buildEmptyDTO();
+        }
         EnergySandboxDTO dto = new EnergySandboxDTO();
         List<MemberEnergyDTO> members = new ArrayList<>();
 
@@ -81,9 +84,9 @@ public class EnergyService {
             }
         }
 
-        List<Child> children = childMapper.selectList(
-                new LambdaQueryWrapper<Child>()
-                        .eq(Child::getFamilyId, familyId)
+        List<FamilyMember> children = familyMemberMapper.selectList(
+                new LambdaQueryWrapper<FamilyMember>()
+                        .eq(FamilyMember::getFamilyId, familyId)
         );
 
         // 2. 计算每个家长的能量
@@ -93,7 +96,7 @@ public class EnergyService {
         }
 
         // 3. 计算每个孩子的能量
-        for (Child child : children) {
+        for (FamilyMember child : children) {
             MemberEnergyDTO member = calcChildEnergy(child);
             members.add(member);
         }
@@ -159,13 +162,13 @@ public class EnergyService {
      */
     private int calcParentBody(User parent) {
         if (parent.getFamilyId() != null) {
-            List<Child> children = childMapper.selectList(
-                    new LambdaQueryWrapper<Child>()
-                            .eq(Child::getFamilyId, parent.getFamilyId())
+            List<FamilyMember> children = familyMemberMapper.selectList(
+                    new LambdaQueryWrapper<FamilyMember>()
+                            .eq(FamilyMember::getFamilyId, parent.getFamilyId())
             );
             // Batch query: fetch health reports for ALL children at once
             List<Long> childUserIds = children.stream()
-                    .map(Child::getUserId)
+                    .map(FamilyMember::getUserId)
                     .collect(Collectors.toList());
             if (!childUserIds.isEmpty()) {
                 // Single query for all children — picks the first available health score
@@ -174,7 +177,12 @@ public class EnergyService {
                         .eq(HealthReport::getStatus, "active")
                         .orderByDesc(HealthReport::getReportDate)
                         .last("LIMIT 1");
-                HealthReport report = healthReportMapper.selectOne(hrWrapper);
+                HealthReport report = null;
+                try {
+                    report = healthReportMapper.selectOne(hrWrapper);
+                } catch (Exception e) {
+                    log.warn("健康报告查询异常, userIds={}: {}", childUserIds, e.getMessage());
+                }
                 if (report != null && report.getOverallScore() != null) {
                     return report.getOverallScore();
                 }
@@ -216,7 +224,7 @@ public class EnergyService {
 
     // ==================== 孩子能量计算 ====================
 
-    private MemberEnergyDTO calcChildEnergy(Child child) {
+    private MemberEnergyDTO calcChildEnergy(FamilyMember child) {
         MemberEnergyDTO dto = new MemberEnergyDTO();
         dto.setMemberId(child.getId());
         dto.setMemberType("child");
@@ -241,7 +249,7 @@ public class EnergyService {
      * 孩子 身 — 优先使用健康检测报告得分
      * 数据源优先级:健康报告overallScore > 子项加权 > 运动/户外类任务完成率(降级)
      */
-    private int calcChildBody(Child child) {
+    private int calcChildBody(FamilyMember child) {
         // 1. 优先使用健康报告中的身体评分
         Integer healthScore = healthReportService.calculateBodyScore(child.getUserId());
         if (healthScore != null) {
@@ -253,7 +261,7 @@ public class EnergyService {
     }
 
     /** 孩子 心 — DAN EMI 70% + 先天 30% + 行为修正 */
-    private int calcChildMind(Child child) {
+    private int calcChildMind(FamilyMember child) {
         Long childId = child.getId();
         Long userId = child.getUserId();
 
@@ -296,7 +304,7 @@ public class EnergyService {
     }
 
     /** 孩子 智 — DAN COG 70% + 先天 30% + 行为修正 */
-    private int calcChildWisdom(Child child) {
+    private int calcChildWisdom(FamilyMember child) {
         Long childId = child.getId();
         Long userId = child.getUserId();
 
@@ -394,7 +402,7 @@ public class EnergyService {
     }
 
     /** Old wisdom calculation as fallback (游戏 + 测评overallScore + 阅读时长) */
-    private int calcOldChildWisdom(Child child) {
+    private int calcOldChildWisdom(FamilyMember child) {
         Long childId = child.getId();
         double totalWeight = 0;
         double weightedSum = 0;
@@ -442,7 +450,7 @@ public class EnergyService {
     }
 
     /** 孩子 行 — 打卡天数 + 习惯类任务完成率 */
-    private int calcChildAction(Child child) {
+    private int calcChildAction(FamilyMember child) {
         Long childId = child.getId();
 
         // 1. 连续打卡天数 (0-100)
@@ -468,7 +476,7 @@ public class EnergyService {
     }
 
     /** 孩子 富 — 积分获取效率 */
-    private int calcChildWealth(Child child) {
+    private int calcChildWealth(FamilyMember child) {
         Long childId = child.getId();
 
         // 已获得积分: points_log 中正向入账

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

@@ -3,11 +3,11 @@ package com.etotem.cfc.service;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.dto.FamilyStatusDTO;
 import com.etotem.cfc.dto.InviteValidateDTO;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.Family;
 import com.etotem.cfc.entity.FamilyInvitation;
 import com.etotem.cfc.entity.User;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.FamilyInvitationMapper;
 import com.etotem.cfc.mapper.FamilyMapper;
 import com.etotem.cfc.mapper.UserMapper;
@@ -31,7 +31,7 @@ public class FamilyInvitationService {
     private UserMapper userMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     public InviteValidateDTO validateInvitation(String token) {
         FamilyInvitation invitation = familyInvitationMapper.selectOne(
@@ -148,14 +148,14 @@ public class FamilyInvitationService {
         user.setUpdatedAt(new Date());
         userMapper.updateById(user);
 
-        List<Child> children = childMapper.selectList(
-                new LambdaQueryWrapper<Child>()
-                        .eq(Child::getFamilyId, oldFamilyId)
+        List<FamilyMember> children = familyMemberMapper.selectList(
+                new LambdaQueryWrapper<FamilyMember>()
+                        .eq(FamilyMember::getFamilyId, oldFamilyId)
         );
-        for (Child child : children) {
+        for (FamilyMember child : children) {
             child.setFamilyId(targetFamilyId);
             child.setUpdatedAt(new Date());
-            childMapper.updateById(child);
+            familyMemberMapper.updateById(child);
         }
 
         if (!oldFamilyId.equals(targetFamilyId)) {

+ 49 - 122
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java

@@ -34,15 +34,15 @@ public class FamilyMemberService {
     @Resource
     private FamilyMapper familyMapper;
 
-    @Resource
-    private ChildMapper childMapper;
-
     @Resource
     private FamilyMemberAttributesMapper familyMemberAttributesMapper;
 
     @Resource
     private OnboardingService onboardingService;
 
+    @Resource
+    private SystemConfigService systemConfigService;
+
     /**
      * 添加家庭成员
      */
@@ -89,7 +89,7 @@ public class FamilyMemberService {
             }
         }
         member.setRelationshipType(dto.getRelationshipType());
-        member.setCapabilityRole(relType.getCapabilityRole());
+        member.setRoleOverride("auto");
         member.setShowToFamily(1);
         member.setTotalPoints(0);
         member.setCreatedAt(new Date());
@@ -112,44 +112,8 @@ public class FamilyMemberService {
 
         familyMemberMapper.insert(member);
 
-        // 如果能力角色为 child,同步创建 children 表记录(游戏/任务/专注度等功能需要)
-        if ("child".equals(relType.getCapabilityRole())) {
-            Child child = new Child();
-            child.setUserId(userId);
-            child.setFamilyId(familyId);
-            child.setNickname(dto.getNickname().trim());
-            child.setPhone(dto.getPhone());
-            child.setGender(dto.getGender());
-            if (member.getBirthday() != null) {
-                child.setBirthday(member.getBirthday());
-                // 根据生日计算年龄
-                java.util.Calendar cal = java.util.Calendar.getInstance();
-                cal.setTime(member.getBirthday());
-                int birthYear = cal.get(java.util.Calendar.YEAR);
-                int birthMonth = cal.get(java.util.Calendar.MONTH);
-                int birthDay = cal.get(java.util.Calendar.DAY_OF_MONTH);
-                java.util.Calendar now = java.util.Calendar.getInstance();
-                int age = now.get(java.util.Calendar.YEAR) - birthYear;
-                if (now.get(java.util.Calendar.MONTH) < birthMonth ||
-                        (now.get(java.util.Calendar.MONTH) == birthMonth && now.get(java.util.Calendar.DAY_OF_MONTH) < birthDay)) {
-                    age--;
-                }
-                child.setAge(Math.max(0, age));
-            }
-            // 默认年龄>=6开启扣分
-            Integer age = child.getAge();
-            child.setPenaltyEnabled(age != null && age >= 6 ? 1 : 0);
-            child.setTheme("default");
-            child.setTotalPoints(0);
-            child.setStreakDays(0);
-            child.setFocusMaxDaily(3);
-            child.setFocusRemaining(3);
-            child.setFocusResetDate(new Date());
-            child.setCreatedAt(new Date());
-            child.setUpdatedAt(new Date());
-            childMapper.insert(child);
-            try { onboardingService.completeTask(userId, "ADD_CHILD"); } catch (Exception e) { /* onboarding trigger failure should not block member add */ }
-        }
+        // children 表已废除,无需同步。角色信息通过 computeEffectiveRole() 自动计算。
+        // 若需要 child 专属初始化(如游戏数据),后续在 computeEffectiveRole() 返回 child 时处理。
 
         return toFamilyMemberVO(member, relType);
     }
@@ -185,8 +149,9 @@ public class FamilyMemberService {
             vo.setGender(fm.getGender());
             vo.setRelationshipType(fm.getRelationshipType());
             vo.setRelationshipTypeName(relTypeNameMap.getOrDefault(fm.getRelationshipType(), fm.getRelationshipType()));
-            vo.setCapabilityRole(fm.getCapabilityRole());
-            vo.setRoleLabel(relTypeNameMap.getOrDefault(fm.getRelationshipType(), fm.getCapabilityRole()));
+            vo.setEffectiveRole(computeEffectiveRole(fm));
+            vo.setRoleOverride(fm.getRoleOverride());
+            vo.setRoleLabel(relTypeNameMap.getOrDefault(fm.getRelationshipType(), computeEffectiveRole(fm)));
             vo.setTotalPoints(fm.getTotalPoints());
             vo.setShowToFamily(fm.getShowToFamily());
             if (fm.getBirthday() != null) {
@@ -195,26 +160,6 @@ public class FamilyMemberService {
             result.add(vo);
         }
 
-        // 3. 查询 children 表(已有孩子数据)
-        List<Child> children = childMapper.selectList(
-                new LambdaQueryWrapper<Child>().eq(Child::getFamilyId, familyId));
-        for (Child c : children) {
-            FamilyMemberVO vo = new FamilyMemberVO();
-            vo.setId(c.getId());
-            vo.setSource("child");
-            vo.setNickname(c.getNickname());
-            vo.setPhone(c.getPhone());
-            vo.setGender(c.getGender());
-            vo.setRelationshipType("child");
-            vo.setRelationshipTypeName("子女");
-            vo.setCapabilityRole("child");
-            vo.setRoleLabel("孩子");
-            vo.setAge(c.getAge());
-            vo.setTotalPoints(c.getTotalPoints());
-            vo.setShowToFamily(c.getShowToFamily());
-            result.add(vo);
-        }
-
         return result;
     }
 
@@ -233,22 +178,11 @@ public class FamilyMemberService {
             SwitchMemberVO vo = new SwitchMemberVO();
             vo.setMemberId(member.getId());
             vo.setSource("family_member");
-            vo.setCapabilityRole(member.getCapabilityRole());
+            vo.setEffectiveRole(computeEffectiveRole(member));
             vo.setNickname(member.getNickname());
             return vo;
         }
 
-        // 从 children 表查找(兼容已有数据)
-        Child child = childMapper.selectById(memberId);
-        if (child != null && child.getFamilyId() != null && child.getFamilyId().equals(user.getFamilyId())) {
-            SwitchMemberVO vo = new SwitchMemberVO();
-            vo.setMemberId(child.getId());
-            vo.setSource("child");
-            vo.setCapabilityRole("child");
-            vo.setNickname(child.getNickname());
-            return vo;
-        }
-
         throw new RuntimeException("成员不存在或不属于您的家庭");
     }
 
@@ -283,26 +217,14 @@ public class FamilyMemberService {
                     userMapper.updateById(targetUser);
                 }
             }
-            // 如果是 child 角色,删除对应的 children 记录
-            if ("child".equals(member.getCapabilityRole())) {
-                List<Child> children = childMapper.selectList(
-                        new LambdaQueryWrapper<Child>()
-                                .eq(Child::getFamilyId, member.getFamilyId())
-                                .eq(Child::getNickname, member.getNickname()));
-                for (Child c : children) {
-                    childMapper.deleteById(c.getId());
-                }
+            // children 表已废除,无需额外清理
+            // 如果成员有效角色为 child,可能需要清理其他关联数据(按需扩展)
+            if ("child".equals(computeEffectiveRole(member))) {
+                // child 专属数据(如游戏记录等)后续按需清理
             }
             return;
         }
 
-        // 从 children 踢出(兼容已有数据)
-        Child child = childMapper.selectById(memberId);
-        if (child != null && child.getFamilyId() != null && child.getFamilyId().equals(operator.getFamilyId())) {
-            childMapper.deleteById(memberId);
-            return;
-        }
-
         throw new RuntimeException("成员不存在");
     }
 
@@ -341,37 +263,16 @@ public class FamilyMemberService {
                 throw new IllegalArgumentException("出生日期格式错误,应为yyyy-MM-dd");
             }
         }
+        if (dto.getRoleOverride() != null) {
+            member.setRoleOverride(dto.getRoleOverride());
+        }
 
         member.setUpdatedAt(new Date());
         familyMemberMapper.updateById(member);
 
-        // 同步更新 children 记录(如果存在)
-        if ("child".equals(member.getCapabilityRole())) {
-            List<Child> children = childMapper.selectList(
-                    new LambdaQueryWrapper<Child>()
-                            .eq(Child::getFamilyId, member.getFamilyId())
-                            .eq(Child::getNickname, member.getNickname()));
-            for (Child c : children) {
-                if (dto.getNickname() != null && !dto.getNickname().trim().isEmpty()) {
-                    c.setNickname(dto.getNickname().trim());
-                }
-                if (dto.getGender() != null) {
-                    c.setGender(dto.getGender());
-                }
-                if (dto.getPhone() != null) {
-                    c.setPhone(dto.getPhone());
-                }
-                if (dto.getBirthday() != null) {
-                    try {
-                        java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
-                        c.setBirthday(sdf.parse(dto.getBirthday()));
-                    } catch (java.text.ParseException e) {
-                        // ignore
-                    }
-                }
-                c.setUpdatedAt(new Date());
-                childMapper.updateById(c);
-            }
+        // 同步更新 family_members 中的 child 专属字段
+        if ("child".equals(computeEffectiveRole(member))) {
+            // family_members 本身已包含 child 专属字段,直接 updateById(member) 已足够
         }
 
         // 查询关系类型名称
@@ -415,7 +316,7 @@ public class FamilyMemberService {
             if (existing != null) {
                 existing.setTypeKey(type.getTypeKey());
                 existing.setTypeName(type.getTypeName());
-                existing.setCapabilityRole(type.getCapabilityRole());
+                existing.setDefaultRole(type.getDefaultRole());
                 existing.setSortOrder(type.getSortOrder());
                 existing.setEnabled(type.getEnabled());
                 relationshipTypeMapper.updateById(existing);
@@ -449,9 +350,10 @@ public class FamilyMemberService {
             vo.setRoleLabel(relType.getTypeName());
         } else {
             vo.setRelationshipTypeName(member.getRelationshipType());
-            vo.setRoleLabel(member.getCapabilityRole());
+            vo.setRoleLabel(computeEffectiveRole(member));
         }
-        vo.setCapabilityRole(member.getCapabilityRole());
+        vo.setEffectiveRole(computeEffectiveRole(member));
+        vo.setRoleOverride(member.getRoleOverride());
         vo.setTotalPoints(member.getTotalPoints());
         vo.setShowToFamily(member.getShowToFamily());
         if (member.getBirthday() != null) {
@@ -473,4 +375,29 @@ public class FamilyMemberService {
         }
         return Math.max(0, age);
     }
+
+    /**
+     * 计算成员的实际角色(effective role)
+     * 优先级: roleOverride > defaultRole(关系类型模板) > 按年龄计算
+     */
+    private String computeEffectiveRole(FamilyMember member) {
+        // 1. roleOverride 非 auto/null → 直接返回
+        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 childThreshold = systemConfigService.getChildAgeThreshold();
+            if (age < childThreshold) return "child";
+        }
+        return "parent";
+    }
 }

+ 5 - 11
cfc-backend/src/main/java/com/etotem/cfc/service/FinanceCheckinService.java

@@ -1,14 +1,8 @@
 package com.etotem.cfc.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.etotem.cfc.entity.FinanceCheckin;
-import com.etotem.cfc.mapper.FinanceCheckinMapper;
-import com.etotem.cfc.entity.Task;
-import com.etotem.cfc.entity.Child;
-import com.etotem.cfc.entity.PointsLog;
-import com.etotem.cfc.mapper.TaskMapper;
-import com.etotem.cfc.mapper.ChildMapper;
-import com.etotem.cfc.mapper.PointsLogMapper;
+import com.etotem.cfc.entity.*;
+import com.etotem.cfc.mapper.*;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -33,7 +27,7 @@ public class FinanceCheckinService {
     private TaskMapper taskMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private PointsLogMapper pointsLogMapper;
@@ -84,7 +78,7 @@ public class FinanceCheckinService {
                 );
                 if (existingTask == null) {
                     Task task = new Task();
-                    Child child = childMapper.selectById(childId);
+                    FamilyMember child = familyMemberMapper.selectById(childId);
                     if (child != null) {
                         task.setFamilyId(child.getFamilyId());
                     }
@@ -117,7 +111,7 @@ public class FinanceCheckinService {
                     if (child != null) {
                         child.setTotalPoints(child.getTotalPoints() + points);
                         child.setUpdatedAt(new Date());
-                        childMapper.updateById(child);
+                        familyMemberMapper.updateById(child);
                     }
 
                     // Insert PointsLog

+ 6 - 5
cfc-backend/src/main/java/com/etotem/cfc/service/GrowthTaskService.java

@@ -2,9 +2,10 @@ package com.etotem.cfc.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.GrowthTask;
 import com.etotem.cfc.entity.GrowthTaskLog;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.GrowthTaskLogMapper;
 import com.etotem.cfc.mapper.GrowthTaskMapper;
 import lombok.extern.slf4j.Slf4j;
@@ -26,7 +27,7 @@ public class GrowthTaskService {
     private GrowthTaskLogMapper growthTaskLogMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private PointsService pointsService;
@@ -183,9 +184,9 @@ public class GrowthTaskService {
         growthTaskLogMapper.updateById(taskLog);
 
         // Find child by userId
-        Child child = childMapper.selectOne(
-                new LambdaQueryWrapper<Child>()
-                        .eq(Child::getUserId, userId)
+        FamilyMember child = familyMemberMapper.selectOne(
+                new LambdaQueryWrapper<FamilyMember>()
+                        .eq(FamilyMember::getUserId, userId)
                         .last("LIMIT 1")
         );
 

+ 5 - 5
cfc-backend/src/main/java/com/etotem/cfc/service/HealthCheckinService.java

@@ -6,10 +6,10 @@ import com.etotem.cfc.entity.HealthCheckin;
 import com.etotem.cfc.mapper.EnergyLogMapper;
 import com.etotem.cfc.mapper.HealthCheckinMapper;
 import com.etotem.cfc.entity.Task;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.PointsLog;
 import com.etotem.cfc.mapper.TaskMapper;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.PointsLogMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -36,7 +36,7 @@ public class HealthCheckinService {
     private TaskMapper taskMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private PointsLogMapper pointsLogMapper;
@@ -103,7 +103,7 @@ public class HealthCheckinService {
                 );
                 if (existingTask == null) {
                     Task task = new Task();
-                    Child child = childMapper.selectById(childId);
+                    FamilyMember child = familyMemberMapper.selectById(childId);
                     if (child != null) {
                         task.setFamilyId(child.getFamilyId());
                     }
@@ -136,7 +136,7 @@ public class HealthCheckinService {
                     if (child != null) {
                         child.setTotalPoints(child.getTotalPoints() + points);
                         child.setUpdatedAt(new Date());
-                        childMapper.updateById(child);
+                        familyMemberMapper.updateById(child);
                     }
 
                     // Insert PointsLog

+ 23 - 29
cfc-backend/src/main/java/com/etotem/cfc/service/HealthReportService.java

@@ -1,14 +1,8 @@
 package com.etotem.cfc.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.etotem.cfc.entity.Child;
-import com.etotem.cfc.entity.HealthDiseaseRisk;
-import com.etotem.cfc.entity.HealthGutFlora;
-import com.etotem.cfc.entity.HealthIndicator;
-import com.etotem.cfc.entity.HealthReport;
-import com.etotem.cfc.entity.HealthReportPermission;
-import com.etotem.cfc.entity.User;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.entity.*;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.HealthDiseaseRiskMapper;
 import com.etotem.cfc.mapper.HealthGutFloraMapper;
 import com.etotem.cfc.mapper.HealthIndicatorMapper;
@@ -49,7 +43,7 @@ public class HealthReportService {
     private HealthDiseaseRiskMapper healthDiseaseRiskMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private UserMapper userMapper;
@@ -307,11 +301,11 @@ public class HealthReportService {
     }
 
     /**
-     * 根据姓名模糊匹配家庭成员(优先匹配 Child,其次匹配 User)
+     * 根据姓名模糊匹配家庭成员(优先匹配 FamilyMember,其次匹配 User)
      *
      * @param name     从PDF提取的姓名
      * @param familyId 家庭ID
-     * @return 匹配到的成员ID(Child.id 或 User.id),未匹配返回 null
+     * @return 匹配到的成员ID(FamilyMember.id 或 User.id),未匹配返回 null
      */
     public Long matchFamilyMemberByName(String name, Long familyId) {
         if (name == null || name.trim().isEmpty() || familyId == null) {
@@ -323,8 +317,8 @@ public class HealthReportService {
             return null;
         }
 
-        // 第1轮:匹配 Child 表(孩子昵称)
-        Long childId = matchChildByName(cleanName, familyId);
+        // 第1轮:匹配 FamilyMember 表(孩子昵称)
+        Long childId = matchFamilyMemberByNameInternal(cleanName, familyId);
         if (childId != null) {
             return childId;
         }
@@ -340,43 +334,43 @@ public class HealthReportService {
     }
 
     /**
-     * 在 Child 表中匹配姓名
+     * 在 FamilyMember 表中匹配姓名
      */
-    private Long matchChildByName(String cleanName, Long familyId) {
-        List<Child> children = childMapper.selectList(
-                new LambdaQueryWrapper<Child>()
-                        .eq(Child::getFamilyId, familyId)
+    private Long matchFamilyMemberByNameInternal(String cleanName, Long familyId) {
+        List<FamilyMember> children = familyMemberMapper.selectList(
+                new LambdaQueryWrapper<FamilyMember>()
+                        .eq(FamilyMember::getFamilyId, familyId)
         );
         if (children == null || children.isEmpty()) {
             return null;
         }
 
         // 精确匹配
-        for (Child child : children) {
+        for (FamilyMember child : children) {
             if (child.getNickname() != null) {
                 String nn = child.getNickname().trim().replaceAll("\\s+", "");
                 if (nn.equals(cleanName)) {
-                    log.info("Child 姓名精确匹配: {} -> childId={}", cleanName, child.getId());
+                    log.info("FamilyMember 姓名精确匹配: {} -> childId={}", cleanName, child.getId());
                     return child.getId();
                 }
             }
         }
 
         // 模糊包含匹配
-        for (Child child : children) {
+        for (FamilyMember child : children) {
             if (child.getNickname() != null) {
                 String nn = child.getNickname();
                 if (cleanName.contains(nn) || nn.contains(cleanName)) {
-                    log.info("Child 姓名模糊匹配: {} -> childId={}", cleanName, child.getId());
+                    log.info("FamilyMember 姓名模糊匹配: {} -> childId={}", cleanName, child.getId());
                     return child.getId();
                 }
             }
         }
 
         // fuzzyMatch 匹配
-        for (Child child : children) {
+        for (FamilyMember child : children) {
             if (child.getNickname() != null && fuzzyMatch(cleanName, child.getNickname())) {
-                log.info("Child 姓名fuzzy匹配: {} -> childId={}", cleanName, child.getId());
+                log.info("FamilyMember 姓名fuzzy匹配: {} -> childId={}", cleanName, child.getId());
                 return child.getId();
             }
         }
@@ -452,13 +446,13 @@ public class HealthReportService {
         return false;
     }
 
-    public Long matchChildIdByName(String name, Long familyId) {
+    public Long matchFamilyMemberIdByName(String name, Long familyId) {
         if (name == null || familyId == null) return null;
-        List<Child> children = childMapper.selectList(
-                new LambdaQueryWrapper<Child>()
-                        .eq(Child::getFamilyId, familyId)
+        List<FamilyMember> children = familyMemberMapper.selectList(
+                new LambdaQueryWrapper<FamilyMember>()
+                        .eq(FamilyMember::getFamilyId, familyId)
         );
-        for (Child c : children) {
+        for (FamilyMember c : children) {
             if (name.contains(c.getNickname()) || (c.getNickname() != null && c.getNickname().contains(name))) {
                 return c.getId();
             }

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

@@ -21,7 +21,7 @@ public class InviteCardService {
     private UserMapper userMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private FamilyMapper familyMapper;
@@ -94,7 +94,7 @@ private String generateCode(String type) {
         result.put("refId", card.getRefId());
 
         if ("child".equals(card.getType())) {
-            Child child = childMapper.selectById(card.getRefId());
+            FamilyMember child = familyMemberMapper.selectById(card.getRefId());
             if (child != null) {
                 result.put("nickname", child.getNickname());
                 Family family = familyMapper.selectById(child.getFamilyId());
@@ -121,7 +121,7 @@ private String generateCode(String type) {
         if (card.getExpiresAt().isBefore(LocalDateTime.now())) return Result.error("邀请码已过期");
 
         if ("child".equals(card.getType())) {
-            return acceptChild(card, phone);
+            return acceptFamilyMember(card, phone);
         } else if ("family".equals(card.getType())) {
             return acceptFamily(card, phone);
         } else if ("guide".equals(card.getType())) {
@@ -130,8 +130,8 @@ private String generateCode(String type) {
         return Result.error("未知邀请类型");
     }
 
-    private Result<Map<String, Object>> acceptChild(InviteCard card, String phone) {
-        Child child = childMapper.selectById(card.getRefId());
+    private Result<Map<String, Object>> acceptFamilyMember(InviteCard card, String phone) {
+        FamilyMember child = familyMemberMapper.selectById(card.getRefId());
         if (child == null) return Result.error("孩子信息不存在");
         if (child.getPhone() == null || !child.getPhone().equals(phone)) {
             return Result.error("手机号不匹配,请联系家长确认");
@@ -281,7 +281,7 @@ private String generateCode(String type) {
         parent.setUpdatedAt(new Date());
         userMapper.updateById(parent);
 
-        Child child = new Child();
+        FamilyMember child = new FamilyMember();
         child.setUserId(userId);
         child.setFamilyId(family.getId());
         child.setNickname("孩子");
@@ -295,10 +295,10 @@ private String generateCode(String type) {
         child.setTheme("default");
         child.setCreatedAt(new Date());
         child.setUpdatedAt(new Date());
-        childMapper.insert(child);
+        familyMemberMapper.insert(child);
 
         GrowthRecord record = new GrowthRecord();
-        record.setChildId(child.getId());
+        record.setFamilyMemberId(child.getId());
         record.setParentId(userId);
         record.setRecordTitle("基础成长档案");
         record.setStatus("pending");

+ 6 - 6
cfc-backend/src/main/java/com/etotem/cfc/service/InviteMilestoneService.java

@@ -1,10 +1,10 @@
 package com.etotem.cfc.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.CommissionRecord;
 import com.etotem.cfc.entity.InviteMilestone;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.CommissionRecordMapper;
 import com.etotem.cfc.mapper.InviteMilestoneMapper;
 import org.slf4j.Logger;
@@ -27,7 +27,7 @@ public class InviteMilestoneService {
     private CommissionRecordMapper commissionRecordMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private PointsService pointsService;
@@ -110,9 +110,9 @@ public class InviteMilestoneService {
         inviteMilestoneMapper.updateById(milestone);
 
         // Resolve childId from userId for points/energy award
-        Child child = childMapper.selectOne(
-                new LambdaQueryWrapper<Child>()
-                        .eq(Child::getUserId, userId)
+        FamilyMember child = familyMemberMapper.selectOne(
+                new LambdaQueryWrapper<FamilyMember>()
+                        .eq(FamilyMember::getUserId, userId)
                         .last("LIMIT 1")
         );
         if (child != null) {

+ 4 - 4
cfc-backend/src/main/java/com/etotem/cfc/service/MentalHealthScreenService.java

@@ -4,11 +4,11 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.dto.EmotionAlertVO;
 import com.etotem.cfc.dto.ScreenResultVO;
 import com.etotem.cfc.dto.ScreenSubmitDTO;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.EmotionAlert;
 import com.etotem.cfc.entity.FollowUp;
 import com.etotem.cfc.entity.MentalHealthScreen;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.EmotionAlertMapper;
 import com.etotem.cfc.mapper.FollowUpMapper;
 import com.etotem.cfc.mapper.MentalHealthScreenMapper;
@@ -36,7 +36,7 @@ public class MentalHealthScreenService {
     private FollowUpMapper followUpMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private WechatAlertTemplateService wechatAlertTemplateService;
@@ -201,7 +201,7 @@ public class MentalHealthScreenService {
             try {
                 FollowUp followUp = new FollowUp();
                 followUp.setChildId(alert.getChildId());
-                Child child = childMapper.selectById(alert.getChildId());
+                FamilyMember child = familyMemberMapper.selectById(alert.getChildId());
                 if (child != null && child.getFamilyId() != null) {
                     followUp.setFamilyId(child.getFamilyId());
                 }

+ 5 - 5
cfc-backend/src/main/java/com/etotem/cfc/service/MiniGameService.java

@@ -3,10 +3,10 @@ package com.etotem.cfc.service;
 import com.etotem.cfc.service.api.MiniGameServiceInterface;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.MiniGame;
 import com.etotem.cfc.entity.PointsLog;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.MiniGameMapper;
 import com.etotem.cfc.mapper.PointsLogMapper;
 import lombok.extern.slf4j.Slf4j;
@@ -27,7 +27,7 @@ public class MiniGameService implements MiniGameServiceInterface {
     private MiniGameMapper miniGameMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private PointsLogMapper pointsLogMapper;
@@ -92,7 +92,7 @@ public class MiniGameService implements MiniGameServiceInterface {
             throw new RuntimeException("游戏不存在");
         }
 
-        Child child = childMapper.selectById(childId);
+        FamilyMember child = familyMemberMapper.selectById(childId);
         if (child == null) {
             throw new RuntimeException("孩子不存在");
         }
@@ -104,7 +104,7 @@ public class MiniGameService implements MiniGameServiceInterface {
         // 更新孩子积分
         child.setTotalPoints(child.getTotalPoints() + earnedPoints);
         child.setUpdatedAt(new Date());
-        childMapper.updateById(child);
+        familyMemberMapper.updateById(child);
 
         // 记录积分流水
         PointsLog log = new PointsLog();

+ 6 - 6
cfc-backend/src/main/java/com/etotem/cfc/service/OnboardingService.java

@@ -1,9 +1,9 @@
 package com.etotem.cfc.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.OnboardingTask;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.OnboardingTaskMapper;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -24,7 +24,7 @@ public class OnboardingService {
     private EnergyService energyService;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     private static final Map<String, int[]> REWARDS = new LinkedHashMap<>();
     static {
@@ -90,9 +90,9 @@ public class OnboardingService {
         onboardingTaskMapper.updateById(task);
 
         // Find child by userId to award points and energy
-        Child child = childMapper.selectOne(
-                new LambdaQueryWrapper<Child>()
-                        .eq(Child::getUserId, userId)
+        FamilyMember child = familyMemberMapper.selectOne(
+                new LambdaQueryWrapper<FamilyMember>()
+                        .eq(FamilyMember::getUserId, userId)
                         .last("LIMIT 1")
         );
         if (child != null) {

+ 11 - 11
cfc-backend/src/main/java/com/etotem/cfc/service/PointsExchangeService.java

@@ -2,10 +2,10 @@ package com.etotem.cfc.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.PointsExchangeProduct;
 import com.etotem.cfc.entity.PointsExchangeRecord;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.PointsExchangeProductMapper;
 import com.etotem.cfc.mapper.PointsExchangeRecordMapper;
 import org.springframework.stereotype.Service;
@@ -30,7 +30,7 @@ public class PointsExchangeService {
     private PointsService pointsService;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private SysConfigService sysConfigService;
@@ -67,16 +67,16 @@ public class PointsExchangeService {
             throw new RuntimeException("库存不足");
         }
 
-        Long targetChildId = childId;
-        if (targetChildId == null) {
-            Child child = childMapper.selectOne(
-                new LambdaQueryWrapper<Child>().eq(Child::getUserId, userId).last("LIMIT 1")
+        Long targetFamilyMemberId = childId;
+        if (targetFamilyMemberId == null) {
+            FamilyMember child = familyMemberMapper.selectOne(
+                new LambdaQueryWrapper<FamilyMember>().eq(FamilyMember::getUserId, userId).last("LIMIT 1")
             );
             if (child != null) {
-                targetChildId = child.getId();
+                targetFamilyMemberId = child.getId();
             }
         }
-        if (targetChildId == null) {
+        if (targetFamilyMemberId == null) {
             throw new RuntimeException("未找到关联的孩子信息");
         }
 
@@ -123,7 +123,7 @@ public class PointsExchangeService {
             throw new RuntimeException("兑换积分不得低于最低限制");
         }
 
-        int deductResult = pointsService.deductSystemPoints(targetChildId, totalCost,
+        int deductResult = pointsService.deductSystemPoints(targetFamilyMemberId, totalCost,
             "积分兑换商品: " + product.getName());
         if (deductResult < 0) {
             throw new RuntimeException("积分不足");
@@ -133,7 +133,7 @@ public class PointsExchangeService {
 
         PointsExchangeRecord record = new PointsExchangeRecord();
         record.setUserId(userId);
-        record.setChildId(targetChildId);
+        record.setFamilyMemberId(targetFamilyMemberId);
         record.setProductId(productId);
         record.setProductName(product.getName());
         record.setPointsCost(totalCost);

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

@@ -4,9 +4,9 @@ import com.etotem.cfc.service.api.PointsServiceInterface;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.PointsLog;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.PointsLogMapper;
 import javax.annotation.Resource;
 import org.springframework.stereotype.Service;
@@ -20,13 +20,13 @@ import java.util.Map;
 public class PointsService implements PointsServiceInterface {
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private PointsLogMapper pointsLogMapper;
 
     public Map<String, Object> getBalance(Long childId) {
-        Child child = childMapper.selectById(childId);
+        FamilyMember child = familyMemberMapper.selectById(childId);
         if (child == null) {
             return null;
         }
@@ -56,7 +56,7 @@ public class PointsService implements PointsServiceInterface {
         // 验证密码(简化版,实际应从数据库验证)
         // 这里需要调用UserService验证密码
         
-        Child child = childMapper.selectById(childId);
+        FamilyMember child = familyMemberMapper.selectById(childId);
         if (child == null) {
             return null;
         }
@@ -71,7 +71,7 @@ public class PointsService implements PointsServiceInterface {
         // 更新积分
         child.setTotalPoints(newBalance);
         child.setUpdatedAt(new Date());
-        childMapper.updateById(child);
+        familyMemberMapper.updateById(child);
 
         // 记录流水
         PointsLog log = new PointsLog();
@@ -94,7 +94,7 @@ public class PointsService implements PointsServiceInterface {
             throw new IllegalArgumentException("系统积分数量必须为正数");
         }
 
-        Child child = childMapper.selectById(childId);
+        FamilyMember child = familyMemberMapper.selectById(childId);
         if (child == null) {
             throw new RuntimeException("孩子不存在");
         }
@@ -106,7 +106,7 @@ public class PointsService implements PointsServiceInterface {
         child.setTotalPoints(newTotal);
 
         child.setUpdatedAt(new Date());
-        childMapper.updateById(child);
+        familyMemberMapper.updateById(child);
 
         PointsLog log = new PointsLog();
         log.setChildId(childId);
@@ -127,7 +127,7 @@ public class PointsService implements PointsServiceInterface {
             throw new IllegalArgumentException("扣除数量必须为正数");
         }
 
-        Child child = childMapper.selectById(childId);
+        FamilyMember child = familyMemberMapper.selectById(childId);
         if (child == null) {
             throw new RuntimeException("孩子不存在");
         }
@@ -144,7 +144,7 @@ public class PointsService implements PointsServiceInterface {
         child.setTotalPoints(newTotal);
 
         child.setUpdatedAt(new Date());
-        childMapper.updateById(child);
+        familyMemberMapper.updateById(child);
 
         PointsLog log = new PointsLog();
         log.setChildId(childId);
@@ -159,7 +159,7 @@ public class PointsService implements PointsServiceInterface {
     }
 
     public int getSystemPointsBalance(Long childId) {
-        Child child = childMapper.selectById(childId);
+        FamilyMember child = familyMemberMapper.selectById(childId);
         if (child == null) {
             return 0;
         }

+ 11 - 11
cfc-backend/src/main/java/com/etotem/cfc/service/RewardService.java

@@ -4,11 +4,11 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.etotem.cfc.dto.CreateRewardDTO;
 import com.etotem.cfc.dto.RewardDTO;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.PointsLog;
 import com.etotem.cfc.entity.Reward;
 import com.etotem.cfc.entity.User;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.PointsLogMapper;
 import com.etotem.cfc.mapper.RewardMapper;
 import com.etotem.cfc.mapper.UserMapper;
@@ -32,7 +32,7 @@ public class RewardService implements RewardServiceInterface {
 private RewardMapper rewardMapper;
 
 @Resource
-private ChildMapper childMapper;
+private FamilyMemberMapper familyMemberMapper;
 
 @Resource
 private PointsLogMapper pointsLogMapper;
@@ -49,7 +49,7 @@ private UserMapper userMapper;
 */
 @Transactional
 public Long createWishlist(Long childId, CreateRewardDTO dto) {
-Child child = childMapper.selectById(childId);
+FamilyMember child = familyMemberMapper.selectById(childId);
 if (child == null) {
 throw new IllegalArgumentException("孩子不存在");
 }
@@ -140,7 +140,7 @@ public Long createReward(Long userId, CreateRewardDTO dto) {
 
     public List<RewardDTO> getWishlist(Long childId) {
         // 获取孩子信息以验证家庭归属
-        Child child = childMapper.selectById(childId);
+        FamilyMember child = familyMemberMapper.selectById(childId);
         if (child == null) {
             return null;
         }
@@ -178,7 +178,7 @@ public Long createReward(Long userId, CreateRewardDTO dto) {
             return null;
         }
 
-        Child child = childMapper.selectById(childId);
+        FamilyMember child = familyMemberMapper.selectById(childId);
         if (child.getTotalPoints() < reward.getPointsRequired()) {
             return null; // 积分不足
         }
@@ -186,7 +186,7 @@ public Long createReward(Long userId, CreateRewardDTO dto) {
         // 扣除积分
         child.setTotalPoints(child.getTotalPoints() - reward.getPointsRequired());
         child.setUpdatedAt(new Date());
-        childMapper.updateById(child);
+        familyMemberMapper.updateById(child);
 
         // 记录积分消耗
         PointsLog log = new PointsLog();
@@ -221,10 +221,10 @@ public Long createReward(Long userId, CreateRewardDTO dto) {
             reward.setExchangedAt(new Date());
         } else {
             // 拒绝,返还积分
-            Child child = childMapper.selectById(reward.getChildId());
+            FamilyMember child = familyMemberMapper.selectById(reward.getChildId());
             child.setTotalPoints(child.getTotalPoints() + reward.getPointsRequired());
             child.setUpdatedAt(new Date());
-            childMapper.updateById(child);
+            familyMemberMapper.updateById(child);
 
             // 记录积分退还
             PointsLog log = new PointsLog();
@@ -319,7 +319,7 @@ return true;
 * 只返回指定孩子的心愿,不包含其他孩子
 */
 public List<RewardDTO> getChildPendingWishlists(Long childId) {
-Child child = childMapper.selectById(childId);
+FamilyMember child = familyMemberMapper.selectById(childId);
 if (child == null) {
 return null;
 }
@@ -363,7 +363,7 @@ dto.setStatus(r.getStatus());
 dto.setChildId(r.getChildId());
 
 // 获取孩子昵称
-Child child = childMapper.selectById(r.getChildId());
+FamilyMember child = familyMemberMapper.selectById(r.getChildId());
 if (child != null) {
 dto.setChildName(child.getNickname());
 }

+ 4 - 4
cfc-backend/src/main/java/com/etotem/cfc/service/ShareService.java

@@ -1,10 +1,10 @@
 package com.etotem.cfc.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.ShareEvent;
 import com.etotem.cfc.entity.ShareRead;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.ShareEventMapper;
 import com.etotem.cfc.mapper.ShareReadMapper;
 import com.etotem.cfc.service.SysConfigService;
@@ -35,7 +35,7 @@ public class ShareService {
     private PointsService pointsService;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     public ShareEvent createShareEvent(Long userId, String targetType, Long targetId,
             String shareChannel, String title) {
@@ -93,7 +93,7 @@ public class ShareService {
         String readRewardStr = sysConfigService.getValue("share_read_reward_points");
         int readReward = readRewardStr != null ? Integer.parseInt(readRewardStr) : 5;
         try {
-            Child child = childMapper.selectOne(new LambdaQueryWrapper<Child>().eq(Child::getUserId, userId).last("LIMIT 1"));
+            FamilyMember child = familyMemberMapper.selectOne(new LambdaQueryWrapper<FamilyMember>().eq(FamilyMember::getUserId, userId).last("LIMIT 1"));
             if (child != null) {
                 pointsService.awardSystemPoints(child.getId(), readReward, "share read reward: " + shareEventId);
             }

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

@@ -1,10 +1,10 @@
 package com.etotem.cfc.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.PointsLog;
 import com.etotem.cfc.entity.StreakMilestone;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.PointsLogMapper;
 import com.etotem.cfc.mapper.StreakMilestoneMapper;
 import com.etotem.cfc.service.api.StreakServiceInterface;
@@ -23,7 +23,7 @@ public class StreakService implements StreakServiceInterface {
     private StreakMilestoneMapper streakMilestoneMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private PointsLogMapper pointsLogMapper;
@@ -53,7 +53,7 @@ public class StreakService implements StreakServiceInterface {
         // 获取孩子已获得的里程碑奖励记录
         List<PointsLog> bonusLogs = pointsLogMapper.selectList(
             new LambdaQueryWrapper<PointsLog>()
-                .eq(PointsLog::getChildId, childId)
+                .eq(PointsLog::getFamilyMemberId, childId)
                 .eq(PointsLog::getType, "bonus")
                 .like(PointsLog::getDescription, "打卡里程碑")
         );
@@ -72,7 +72,7 @@ public class StreakService implements StreakServiceInterface {
                 rewardPoints = milestone.getRewardPoints();
 
                 // 记录积分
-                Child child = childMapper.selectById(childId);
+                FamilyMember child = familyMemberMapper.selectById(childId);
                 if (child != null) {
                     recordBonusPoints(child, milestone);
                 }
@@ -118,14 +118,14 @@ public class StreakService implements StreakServiceInterface {
     /**
      * 记录里程碑奖励积分
      */
-    private void recordBonusPoints(Child child, StreakMilestone milestone) {
+    private void recordBonusPoints(FamilyMember child, StreakMilestone milestone) {
         // 更新孩子积分
         child.setTotalPoints(child.getTotalPoints() + milestone.getRewardPoints());
-        childMapper.updateById(child);
+        familyMemberMapper.updateById(child);
         
         // 记录积分流水
         PointsLog log = new PointsLog();
-        log.setChildId(child.getId());
+        log.setFamilyMemberId(child.getId());
         log.setAmount(milestone.getRewardPoints());
         log.setType("bonus");
         log.setDescription("打卡里程碑-" + milestone.getDays() + "天奖励");
@@ -137,7 +137,7 @@ public class StreakService implements StreakServiceInterface {
      * 获取孩子的打卡进度信息
      */
     public Map<String, Object> getStreakProgress(Long childId) {
-        Child child = childMapper.selectById(childId);
+        FamilyMember child = familyMemberMapper.selectById(childId);
         if (child == null) {
             return null;
         }
@@ -155,7 +155,7 @@ public class StreakService implements StreakServiceInterface {
         // 获取已获得的奖励
         List<PointsLog> bonusLogs = pointsLogMapper.selectList(
             new LambdaQueryWrapper<PointsLog>()
-                .eq(PointsLog::getChildId, childId)
+                .eq(PointsLog::getFamilyMemberId, childId)
                 .eq(PointsLog::getType, "bonus")
                 .like(PointsLog::getDescription, "打卡里程碑")
         );
@@ -200,7 +200,7 @@ public class StreakService implements StreakServiceInterface {
      */
     public void resetStreakIfNeeded() {
         // 查询所有孩子
-        List<Child> children = childMapper.selectList(null);
+        List<FamilyMember> children = familyMemberMapper.selectList(null);
         
         Calendar cal = Calendar.getInstance();
         cal.add(Calendar.DAY_OF_MONTH, -1);
@@ -210,14 +210,14 @@ public class StreakService implements StreakServiceInterface {
         cal.set(Calendar.MILLISECOND, 0);
         Date yesterday = cal.getTime();
         
-        for (Child child : children) {
+        for (FamilyMember child : children) {
             if (child.getLastTaskDate() != null && child.getLastTaskDate().before(yesterday)) {
                 // 上次任务日期是昨天之前,重置连续天数
                 if (child.getStreakDays() > 0) {
                     log.info("孩子{}打卡中断,连续天数从{}重置为0", child.getId(), child.getStreakDays());
                     child.setStreakDays(0);
                     child.setUpdatedAt(new Date());
-                    childMapper.updateById(child);
+                    familyMemberMapper.updateById(child);
                 }
             }
         }

+ 43 - 8
cfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java

@@ -22,7 +22,7 @@ public class TaskService implements TaskServiceInterface {
     private TaskMapper taskMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private UserMapper userMapper;
@@ -322,7 +322,7 @@ long diffMinutes = (now.getTime() - task.getDeadline().getTime()) / (1000 * 60);
 isLateGrace = diffMinutes <= LATE_GRACE_MINUTES;
 }
 
-Child child = childMapper.selectById(childId);
+FamilyMember child = familyMemberMapper.selectById(childId);
 
 // POINT-002: 计算积分(支持自定义超额规则)
 String bonusRuleJson = task.getBonusRule();
@@ -365,7 +365,7 @@ pointsEarned += EARLY_BONUS;
         }
         child.setLastTaskDate(today);
         child.setUpdatedAt(new Date());
-        childMapper.updateById(child);
+        familyMemberMapper.updateById(child);
 
 // 检查并发放打卡里程碑奖励
 int milestoneReward = streakService.checkAndAwardMilestone(childId, child.getStreakDays());
@@ -578,7 +578,7 @@ return result;
         Map<String, Object> result = new HashMap<>();
         result.put("pointsEarned", pointsEarned);
 
-        try { Child child = childMapper.selectById(task.getChildId()); if (child != null) { onboardingService.completeTask(child.getUserId(), "FIRST_TASK"); } } catch (Exception e) { /* onboarding trigger failure should not block task completion */ }
+        try { FamilyMember child = familyMemberMapper.selectById(task.getChildId()); if (child != null) { onboardingService.completeTask(child.getUserId(), "FIRST_TASK"); } } catch (Exception e) { /* onboarding trigger failure should not block task completion */ }
     result.put("newBalance", user.getTotalPoints());
     result.put("needReview", task.getNeedReview() == 1);
 
@@ -655,7 +655,7 @@ return taskMapper.selectPage(pageParam, wrapper);
 * "qualityThreshold": 90  // 质量评分阈值(可选)
 * }
 */
-private int calculatePointsWithBonusRule(Task task, Date completedAt, boolean isEarly, boolean isLateGrace, Child child) {
+private int calculatePointsWithBonusRule(Task task, Date completedAt, boolean isEarly, boolean isLateGrace, FamilyMember child) {
 int points = task.getPoints();
 
 try {
@@ -727,7 +727,7 @@ return null;
 /**
  * 计算超时扣分
  */
-private int calculateLatePenalty(Task task, Child child, Long childId) {
+private int calculateLatePenalty(Task task, FamilyMember child, Long childId) {
     if (child == null) {
         return 0;
     }
@@ -794,7 +794,7 @@ return 0; // 达到每日扣分上限
         taskMapper.updateById(task);
 
         // 更新连续打卡
-        Child child = childMapper.selectById(childId);
+        FamilyMember child = familyMemberMapper.selectById(childId);
         Date lastTaskDate = child.getLastTaskDate();
         Date today = new Date();
         Calendar cal = Calendar.getInstance();
@@ -810,7 +810,7 @@ return 0; // 达到每日扣分上限
         }
         child.setLastTaskDate(today);
         child.setUpdatedAt(new Date());
-        childMapper.updateById(child);
+        familyMemberMapper.updateById(child);
 
         // 检查并发放打卡里程碑奖励
         int milestoneReward = streakService.checkAndAwardMilestone(childId, child.getStreakDays());
@@ -827,4 +827,39 @@ return 0; // 达到每日扣分上限
 
         return result;
     }
+
+    // ==================== Family Member Task queries ====================
+
+    /**
+     * 获取指定家庭成员待审核的任务列表
+     */
+    public List<Task> getPendingReviewTasksForFamilyMember(Long familyMemberId) {
+        return taskMapper.selectList(new LambdaQueryWrapper<Task>()
+                .eq(Task::getFamilyMemberId, familyMemberId)
+                .eq(Task::getNeedReview, 1)
+                .in(Task::getStatus, Arrays.asList("completed", "pending"))
+                .orderByDesc(Task::getCreatedAt));
+    }
+
+    /**
+     * 获取指定家庭成员的全部任务
+     */
+    public List<Task> getAllTasksByFamilyMember(Long familyMemberId) {
+        return taskMapper.selectList(new LambdaQueryWrapper<Task>()
+                .eq(Task::getFamilyMemberId, familyMemberId)
+                .orderByDesc(Task::getCreatedAt));
+    }
+
+    /**
+     * 分页获取指定家庭成员的任务历史
+     */
+    public Page<Task> getTaskHistoryByFamilyMember(Long familyMemberId, Integer page, Integer size, String status) {
+        LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
+                .eq(Task::getFamilyMemberId, familyMemberId);
+        if (status != null && !status.isEmpty()) {
+            wrapper.eq(Task::getStatus, status);
+        }
+        wrapper.orderByDesc(Task::getCreatedAt);
+        return taskMapper.selectPage(new Page<>(page, size), wrapper);
+    }
 }

+ 7 - 5
cfc-backend/src/main/java/com/etotem/cfc/service/TaskStatsService.java

@@ -2,8 +2,10 @@ package com.etotem.cfc.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.Task;
-import com.etotem.cfc.mapper.ChildMapper;
+
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.TaskMapper;
 import org.springframework.stereotype.Service;
 
@@ -19,7 +21,7 @@ public class TaskStatsService {
     private TaskMapper taskMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     /**
      * 获取单个孩子的任务完成统计
@@ -60,10 +62,10 @@ public class TaskStatsService {
         Map<String, Object> stats = buildStats(tasks, familyId, null);
 
         // 按孩子分组
-        List<Child> children = childMapper.selectList(
-            new LambdaQueryWrapper<Child>().eq(Child::getFamilyId, familyId));
+        List<FamilyMember> children = familyMemberMapper.selectList(
+            new LambdaQueryWrapper<FamilyMember>().eq(FamilyMember::getFamilyId, familyId));
         List<Map<String, Object>> byChild = new ArrayList<>();
-        for (Child child : children) {
+        for (FamilyMember child : children) {
             List<Task> childTasks = tasks.stream()
                 .filter(t -> child.getId().equals(t.getChildId()))
                 .collect(Collectors.toList());

+ 5 - 5
cfc-backend/src/main/java/com/etotem/cfc/service/TeacherService.java

@@ -28,7 +28,7 @@ public class TeacherService {
     private UserMapper userMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private UserService userService;
@@ -175,10 +175,10 @@ public class TeacherService {
         danAssessmentResultMapper.insert(result);
         
         // 更新孩子的评估结果
-        Child child = childMapper.selectById(dto.getChildId());
+        FamilyMember child = familyMemberMapper.selectById(dto.getChildId());
         if (child != null) {
             child.setUpdatedAt(new Date());
-            childMapper.updateById(child);
+            familyMemberMapper.updateById(child);
         }
         
         // 更新老师的测评次数
@@ -260,7 +260,7 @@ public class TeacherService {
         dto.setCreatedAt(appt.getCreatedAt());
         
         // 补充关联信息
-        Child child = childMapper.selectById(appt.getChildId());
+        FamilyMember child = familyMemberMapper.selectById(appt.getChildId());
         if (child != null) {
             dto.setChildName(child.getNickname());
         }
@@ -289,7 +289,7 @@ public class TeacherService {
         dto.setCreatedAt(result.getCreatedAt());
         
         // 补充关联信息
-        Child child = childMapper.selectById(result.getChildId());
+        FamilyMember child = familyMemberMapper.selectById(result.getChildId());
         if (child != null) {
             dto.setChildName(child.getNickname());
             dto.setChildNickname(child.getNickname());

+ 2 - 2
cfc-backend/src/main/java/com/etotem/cfc/service/TrainingPlanService.java

@@ -5,7 +5,7 @@ import com.etotem.cfc.entity.TrainingPlan;
 import com.etotem.cfc.entity.TrainingPlanItem;
 import com.etotem.cfc.entity.TrainingPlanTask;
 import com.etotem.cfc.mapper.TrainingPlanMapper;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -19,7 +19,7 @@ public class TrainingPlanService {
     private TrainingPlanMapper trainingPlanMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private CognitiveTrainingConfigService trainingConfigService;

+ 147 - 18
cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java

@@ -5,10 +5,10 @@ import com.etotem.cfc.config.JwtConfig;
 import com.etotem.cfc.dto.*;
 import com.etotem.cfc.dto.RegisterWithIdCardDTO;
 import com.etotem.cfc.dto.RegisterWithInviteCodeDTO;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.Family;
 import com.etotem.cfc.entity.User;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.FamilyMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.VerificationCodeService;
@@ -34,7 +34,7 @@ private UserMapper userMapper;
 private FamilyMapper familyMapper;
 
 @Resource
-private ChildMapper childMapper;
+private FamilyMemberMapper familyMemberMapper;
 
 @Resource
 private JwtConfig jwtConfig;
@@ -613,7 +613,7 @@ private WechatService wechatService;
       return null;
     }
 
-    Child child = childMapper.selectById(childId);
+    FamilyMember child = familyMemberMapper.selectById(childId);
     if (child == null || !child.getFamilyId().equals(user.getFamilyId())) {
       return null;
     }
@@ -658,7 +658,7 @@ private WechatService wechatService;
                 vo.setId(p.getId());
                 vo.setNickname(p.getNickname());
                 vo.setAvatar(p.getAvatar());
-                vo.setCapabilityRole("parent");
+                vo.setEffectiveRole("parent");
                 vo.setRoleLabel(p.getFamilyRole() != null ? p.getFamilyRole() : "家长");
                 vo.setTotalPoints(p.getTotalPoints());
                 vo.setShowToFamily(p.getShowToFamily() != null ? p.getShowToFamily() : 1);
@@ -667,15 +667,15 @@ private WechatService wechatService;
         }
 
         // 孩子:show_to_family=1 的同家庭孩子
-        List<Child> children = childMapper.selectList(new LambdaQueryWrapper<Child>()
-                .eq(Child::getFamilyId, familyId));
-        for (Child c : children) {
+        List<FamilyMember> children = familyMemberMapper.selectList(new LambdaQueryWrapper<FamilyMember>()
+                .eq(FamilyMember::getFamilyId, familyId));
+        for (FamilyMember c : children) {
             if (c.getShowToFamily() == null || c.getShowToFamily() == 1) {
                 FamilyMemberVO vo = new FamilyMemberVO();
                 vo.setId(c.getId());
                 vo.setNickname(c.getNickname());
                 vo.setAvatar("");
-                vo.setCapabilityRole("child");
+                vo.setEffectiveRole("child");
                 vo.setRoleLabel("孩子");
                 vo.setTotalPoints(c.getTotalPoints());
                 vo.setShowToFamily(c.getShowToFamily() != null ? c.getShowToFamily() : 1);
@@ -695,13 +695,13 @@ private WechatService wechatService;
             return false;
         }
         if ("child".equals(memberType)) {
-            Child child = childMapper.selectById(memberId);
+            FamilyMember child = familyMemberMapper.selectById(memberId);
             if (child == null || !child.getFamilyId().equals(user.getFamilyId())) {
                 return false;
             }
             child.setShowToFamily(showToFamily);
             child.setUpdatedAt(new Date());
-            childMapper.updateById(child);
+            familyMemberMapper.updateById(child);
             return true;
         } else if ("parent".equals(memberType)) {
             // 家长只能改自己的可见性
@@ -798,7 +798,7 @@ private WechatService wechatService;
             throw new IllegalArgumentException("请输入有效的年龄(1-18岁)");
         }
 
-        Child child = new Child();
+        FamilyMember child = new FamilyMember();
         child.setUserId(userId);
         child.setFamilyId(user.getFamilyId());
         child.setNickname(dto.getNickname().trim());
@@ -823,7 +823,7 @@ private WechatService wechatService;
         child.setFocusResetDate(new Date());
         child.setCreatedAt(new Date());
         child.setUpdatedAt(new Date());
-        childMapper.insert(child);
+        familyMemberMapper.insert(child);
 
         return toChildInfoDTO(child);
     }
@@ -834,8 +834,8 @@ private WechatService wechatService;
             return null;
         }
 
-        List<Child> children = childMapper.selectList(new LambdaQueryWrapper<Child>()
-                .eq(Child::getFamilyId, user.getFamilyId()));
+        List<FamilyMember> children = familyMemberMapper.selectList(new LambdaQueryWrapper<FamilyMember>()
+                .eq(FamilyMember::getFamilyId, user.getFamilyId()));
 
         return children.stream().map(this::toChildInfoDTO).collect(java.util.stream.Collectors.toList());
     }
@@ -846,7 +846,7 @@ private WechatService wechatService;
             return false;
         }
 
-        Child child = childMapper.selectById(childId);
+        FamilyMember child = familyMemberMapper.selectById(childId);
         if (child == null || child.getFamilyId() == null || !child.getFamilyId().equals(user.getFamilyId())) {
             return false;
         }
@@ -879,7 +879,7 @@ private WechatService wechatService;
             child.setShowToFamily(dto.getShowToFamily());
         }
         child.setUpdatedAt(new Date());
-        childMapper.updateById(child);
+        familyMemberMapper.updateById(child);
         return true;
     }
 
@@ -891,7 +891,7 @@ private WechatService wechatService;
         return UUID.randomUUID().toString().substring(0, 8).toUpperCase();
     }
 
-    private ChildInfoDTO toChildInfoDTO(Child child) {
+    private ChildInfoDTO toChildInfoDTO(FamilyMember child) {
         ChildInfoDTO dto = new ChildInfoDTO();
         dto.setId(child.getId());
         dto.setNickname(child.getNickname());
@@ -1238,6 +1238,135 @@ private WechatService wechatService;
         userMapper.updateById(user);
     }
 
+    // ==================== Family Member API (new) ====================
+
+    /**
+     * 获取家庭成员列表(孩子)
+     */
+    public List<FamilyMemberInfoDTO> getFamilyMemberren(Long userId) {
+        User user = userMapper.selectById(userId);
+        if (user == null || user.getFamilyId() == null) {
+            return new ArrayList<>();
+        }
+        List<FamilyMember> members = familyMemberMapper.selectList(
+                new LambdaQueryWrapper<FamilyMember>()
+                        .eq(FamilyMember::getFamilyId, user.getFamilyId()));
+        return members.stream().map(this::toFamilyMemberInfoDTO).collect(java.util.stream.Collectors.toList());
+    }
+
+    /**
+     * 创建家庭成员
+     */
+    public FamilyMemberInfoDTO createFamilyMember(Long userId, CreateFamilyMemberDTO dto) {
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            throw new RuntimeException("用户不存在");
+        }
+        FamilyMember member = new FamilyMember();
+        member.setUserId(userId);
+        member.setFamilyId(user.getFamilyId());
+        member.setNickname(dto.getNickname());
+        member.setPhone(dto.getPhone());
+        member.setAvatar(dto.getAvatar());
+        member.setGender(dto.getGender());
+        try {
+            if (dto.getBirthday() != null && !dto.getBirthday().isEmpty()) {
+                java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
+                member.setBirthday(sdf.parse(dto.getBirthday()));
+            }
+        } catch (Exception e) {
+            // ignore parse error
+        }
+        if (dto.getAge() != null) {
+            member.setAge(dto.getAge());
+        }
+        member.setRelationshipType(dto.getRelationshipType());
+        member.setRoleOverride(dto.getRoleOverride());
+        member.setTotalPoints(0);
+        member.setCreatedAt(new Date());
+        member.setUpdatedAt(new Date());
+        familyMemberMapper.insert(member);
+        return toFamilyMemberInfoDTO(member);
+    }
+
+    /**
+     * 更新家庭成员信息
+     */
+    public boolean updateFamilyMember(Long userId, Long memberId, UpdateFamilyMemberDTO dto) {
+        User user = userMapper.selectById(userId);
+        if (user == null || user.getFamilyId() == null) {
+            return false;
+        }
+        FamilyMember member = familyMemberMapper.selectById(memberId);
+        if (member == null || !member.getFamilyId().equals(user.getFamilyId())) {
+            return false;
+        }
+        if (dto.getNickname() != null) {
+            member.setNickname(dto.getNickname());
+        }
+        if (dto.getPhone() != null) {
+            member.setPhone(dto.getPhone());
+        }
+        if (dto.getAvatar() != null) {
+            member.setAvatar(dto.getAvatar());
+        }
+        if (dto.getGender() != null) {
+            member.setGender(dto.getGender());
+        }
+        try {
+            if (dto.getBirthday() != null && !dto.getBirthday().isEmpty()) {
+                java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
+                member.setBirthday(sdf.parse(dto.getBirthday()));
+            }
+        } catch (Exception e) {
+            // ignore parse error
+        }
+        if (dto.getAge() != null) {
+            member.setAge(dto.getAge());
+        }
+        if (dto.getShowToFamily() != null) {
+            member.setShowToFamily(dto.getShowToFamily());
+        }
+        member.setUpdatedAt(new Date());
+        return familyMemberMapper.updateById(member) > 0;
+    }
+
+    /**
+     * 家长切换到指定家庭成员身份
+     */
+    public Long switchToFamilyMember(Long userId, Long memberId) {
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            throw new RuntimeException("用户不存在");
+        }
+        // 验证家庭成员属于该用户所在家庭
+        FamilyMember member = familyMemberMapper.selectById(memberId);
+        if (member == null || !member.getFamilyId().equals(user.getFamilyId())) {
+            throw new RuntimeException("家庭成员不存在或不属于您的家庭");
+        }
+        // 切换用户role为child,记录原始角色以便切回
+        user.setRole("child");
+        user.setUpdatedAt(new Date());
+        userMapper.updateById(user);
+        return member.getId();
+    }
+
+    private FamilyMemberInfoDTO toFamilyMemberInfoDTO(FamilyMember member) {
+        FamilyMemberInfoDTO dto = new FamilyMemberInfoDTO();
+        dto.setId(member.getId());
+        dto.setNickname(member.getNickname());
+        dto.setAge(member.getAge());
+        dto.setBirthday(member.getBirthday());
+        dto.setGender(member.getGender());
+        dto.setPhone(member.getPhone());
+        dto.setAvatar(member.getAvatar());
+        dto.setRelationshipType(member.getRelationshipType());
+        dto.setRoleOverride(member.getRoleOverride());
+        dto.setShowToFamily(member.getShowToFamily());
+        dto.setTotalPoints(member.getTotalPoints());
+        return dto;
+    }
+
     private void populateTeacherFields(LoginResultDTO result, User user) {
         result.setTeacherStatus(user.getTeacherStatus() != null ? user.getTeacherStatus() : "pending");
         result.setTeacherRejectReason(user.getTeacherRejectReason());

+ 4 - 4
cfc-backend/src/main/java/com/etotem/cfc/service/WechatAlertTemplateService.java

@@ -1,9 +1,9 @@
 package com.etotem.cfc.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.User;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Value;
@@ -23,7 +23,7 @@ import java.util.Map;
 public class WechatAlertTemplateService {
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private UserMapper userMapper;
@@ -73,7 +73,7 @@ public class WechatAlertTemplateService {
             }
 
             // 1. 查找孩子信息
-            Child child = childMapper.selectById(childId);
+            FamilyMember child = familyMemberMapper.selectById(childId);
             if (child == null) {
                 log.warn("孩子不存在, childId={}", childId);
                 return;

+ 10 - 10
cfc-backend/src/main/java/com/etotem/cfc/service/WishService.java

@@ -5,11 +5,11 @@ import com.etotem.cfc.service.api.WishServiceInterface;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.dto.CreateWishDTO;
 import com.etotem.cfc.dto.WishDTO;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.PointsLog;
 import com.etotem.cfc.entity.User;
 import com.etotem.cfc.entity.Wish;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.PointsLogMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.mapper.WishMapper;
@@ -35,7 +35,7 @@ public class WishService {
     private WishMapper wishMapper;
 
     @Resource
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @Resource
     private UserMapper userMapper;
@@ -50,7 +50,7 @@ public class WishService {
      */
     @Transactional
     public Long createByChild(Long childId, CreateWishDTO dto) {
-        Child child = childMapper.selectById(childId);
+        FamilyMember child = familyMemberMapper.selectById(childId);
         if (child == null) {
             throw new RuntimeException("孩子不存在");
         }
@@ -89,7 +89,7 @@ public class WishService {
             throw new RuntimeException("请设置所需积分");
         }
 
-        Child child = childMapper.selectById(dto.getChildId());
+        FamilyMember child = familyMemberMapper.selectById(dto.getChildId());
         if (child == null || !child.getFamilyId().equals(parent.getFamilyId())) {
             throw new RuntimeException("孩子不存在或不属于该家庭");
         }
@@ -199,7 +199,7 @@ public class WishService {
         }
 
         // 检查积分是否充足
-        Child child = childMapper.selectById(childId);
+        FamilyMember child = familyMemberMapper.selectById(childId);
         if (child.getTotalPoints() < wish.getPointsRequired()) {
             throw new RuntimeException("积分不足,无法申请兑换");
         }
@@ -238,7 +238,7 @@ public class WishService {
 
         if (approved) {
             // 再次检查积分是否充足(防止并发问题)
-            Child child = childMapper.selectById(wish.getChildId());
+            FamilyMember child = familyMemberMapper.selectById(wish.getChildId());
             if (child.getTotalPoints() < wish.getPointsRequired()) {
                 throw new RuntimeException("积分不足,无法兑换");
             }
@@ -246,7 +246,7 @@ public class WishService {
             // 扣减积分
             child.setTotalPoints(child.getTotalPoints() - wish.getPointsRequired());
             child.setUpdatedAt(new Date());
-            childMapper.updateById(child);
+            familyMemberMapper.updateById(child);
 
             // 记录积分消耗
             PointsLog pointsLog = new PointsLog();
@@ -364,7 +364,7 @@ public class WishService {
                 .notIn(Wish::getStatus, Arrays.asList("cancelled"))
                 .orderByDesc(Wish::getCreatedAt));
 
-        Child child = childMapper.selectById(childId);
+        FamilyMember child = familyMemberMapper.selectById(childId);
         int currentPoints = child != null ? child.getTotalPoints() : 0;
 
         return wishes.stream().map(w -> toDTO(w, currentPoints)).collect(Collectors.toList());
@@ -445,7 +445,7 @@ public class WishService {
 
         // 获取孩子昵称
         if (wish.getChildId() != null) {
-            Child child = childMapper.selectById(wish.getChildId());
+            FamilyMember child = familyMemberMapper.selectById(wish.getChildId());
             if (child != null) {
                 dto.setChildName(child.getNickname());
             }

+ 38 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/impl/DimensionScoreServiceImpl.java

@@ -10,7 +10,9 @@ 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.entity.FamilyMember;
 import com.etotem.cfc.mapper.HealthNormReferenceMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.service.DimensionScoreService;
 import com.etotem.cfc.service.HealthDimensionScoreService;
 import com.etotem.cfc.service.HealthReportService;
@@ -44,6 +46,9 @@ public class DimensionScoreServiceImpl implements DimensionScoreService {
     @Resource
     private HealthNormReferenceMapper normMapper;
 
+    @Resource
+    private FamilyMemberMapper familyMemberMapper;
+
     @Resource
     private HealthReportService healthReportService;
 
@@ -110,12 +115,45 @@ public class DimensionScoreServiceImpl implements DimensionScoreService {
             items.add(item);
         }
 
+        // 如果没有任何个人数据,使用同龄同性平均值填充
+        if (validCount == 0) {
+            FamilyMember member = familyMemberMapper.selectById(memberId);
+            if (member != null && member.getBirthday() != null) {
+                long ageMonths = java.time.temporal.ChronoUnit.MONTHS.between(
+                    java.time.LocalDate.from(member.getBirthday().toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate()),
+                    java.time.LocalDate.now()
+                );
+                String gender = (member.getGender() != null && !member.getGender().isEmpty())
+                    ? member.getGender() : "all";
+                for (HealthDimensionVO.DimensionItem item : items) {
+                    String dimCode = item.getDimension();
+                    HealthNormReference norm = normMapper.selectNorm(dimCode, gender, (int) ageMonths);
+                    if (norm != null && norm.getPercentile50() != null) {
+                        item.setScore(BigDecimal.valueOf(norm.getPercentile50()));
+                        item.setLevel(calcLevel(norm.getPercentile50()));
+                        item.setPercentile(50);
+                        item.setDataSource("AVERAGE");
+                        totalScore = totalScore.add(BigDecimal.valueOf(norm.getPercentile50()));
+                    } else {
+                        // 无参照数据时使用默认值 65
+                        item.setScore(BigDecimal.valueOf(65));
+                        item.setLevel(calcLevel(65));
+                        item.setPercentile(null);
+                        item.setDataSource("AVERAGE");
+                        totalScore = totalScore.add(BigDecimal.valueOf(65));
+                    }
+                }
+                validCount = items.size();
+            }
+        }
+
         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());
+        vo.setIsAverage(validCount > 0 && items.stream().allMatch(i -> "AVERAGE".equals(i.getDataSource())));
         return vo;
     }
 

+ 118 - 31
cfc-backend/src/main/resources/schema.sql

@@ -34,29 +34,32 @@ CREATE TABLE IF NOT EXISTS users (
     INDEX idx_phone (phone)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 
--- 孩子详情表
-CREATE TABLE IF NOT EXISTS children (
-    id BIGINT AUTO_INCREMENT PRIMARY KEY,
-    user_id BIGINT NOT NULL,
-    family_id BIGINT NOT NULL,
-    nickname VARCHAR(50) NOT NULL,
-    age TINYINT NOT NULL,
-    birthday DATE,
-    gender ENUM('male','female'),
-    penalty_enabled TINYINT DEFAULT 1 COMMENT '是否开启扣分(0否1是)',
-    theme VARCHAR(32) DEFAULT 'default',
-    total_points INT DEFAULT 0,
-    system_points INT DEFAULT 0 COMMENT '系统积分(测评建档/平台发放)',
-    streak_days INT DEFAULT 0,
-    last_task_date DATE,
-    focus_max_daily TINYINT DEFAULT 3,
-    focus_remaining TINYINT DEFAULT 3,
-    focus_reset_date DATE,
-    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    INDEX idx_user_id (user_id),
-    INDEX idx_family_id (family_id)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+-- ============================================================
+-- 【已废除】children 表已合并到 family_members
+-- 保留此处仅作参考,勿执行
+-- ============================================================
+-- CREATE TABLE IF NOT EXISTS children (
+--     id BIGINT AUTO_INCREMENT PRIMARY KEY,
+--     user_id BIGINT NOT NULL,
+--     family_id BIGINT NOT NULL,
+--     nickname VARCHAR(50) NOT NULL,
+--     age TINYINT NOT NULL,
+--     birthday DATE,
+--     gender ENUM('male','female'),
+--     penalty_enabled TINYINT DEFAULT 1 COMMENT '是否开启扣分(0否1是)',
+--     theme VARCHAR(32) DEFAULT 'default',
+--     total_points INT DEFAULT 0,
+--     system_points INT DEFAULT 0 COMMENT '系统积分(测评建档/平台发放)',
+--     streak_days INT DEFAULT 0,
+--     last_task_date DATE,
+--     focus_max_daily TINYINT DEFAULT 3,
+--     focus_remaining TINYINT DEFAULT 3,
+--     focus_reset_date DATE,
+--     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+--     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+--     INDEX idx_user_id (user_id),
+--     INDEX idx_family_id (family_id)
+-- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 
 -- 任务表
 CREATE TABLE IF NOT EXISTS tasks (
@@ -1638,11 +1641,21 @@ CREATE TABLE IF NOT EXISTS family_members (
     gender           VARCHAR(10)  COMMENT '性别: male/female',
     birthday         DATE         COMMENT '出生日期',
     relationship_type VARCHAR(50) NOT NULL COMMENT '关系类型key',
-    capability_role  VARCHAR(20)  NOT NULL COMMENT '能力角色: parent/child(决定界面权限)',
-    show_to_family   INT DEFAULT 1 COMMENT '对家庭成员可见: 1=可见, 0=不可见',
-    total_points     INT DEFAULT 0 COMMENT '总积分',
-    created_at       DATETIME DEFAULT CURRENT_TIMESTAMP,
-    updated_at       DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    role_override VARCHAR(20) DEFAULT 'auto' COMMENT '角色覆盖: auto/parent/child/elderly,空=auto',
+    show_to_family INT DEFAULT 1 COMMENT '对家庭成员可见: 1=可见, 0=不可见',
+    total_points INT DEFAULT 0 COMMENT '总积分',
+    -- 孩子专属字段(原children表合并)
+    id_card VARCHAR(18) DEFAULT NULL COMMENT '身份证号(孩子)',
+    penalty_enabled TINYINT DEFAULT 1 COMMENT '是否扣减积分(0否1是)',
+    theme VARCHAR(32) DEFAULT 'default' COMMENT '主题',
+    system_points INT DEFAULT 0 COMMENT '系统积分(干预/平台奖励)',
+    streak_days INT DEFAULT 0 COMMENT '连续打卡天数',
+    last_task_date DATE COMMENT '上次任务日期',
+    focus_max_daily TINYINT DEFAULT 3 COMMENT '每日专注上限',
+    focus_remaining TINYINT DEFAULT 3 COMMENT '今日剩余专注次数',
+    focus_reset_date DATE COMMENT '专注次数重置日期',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     INDEX idx_family_id (family_id),
     INDEX idx_phone (phone)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭成员表(手动添加)';
@@ -1652,14 +1665,14 @@ CREATE TABLE IF NOT EXISTS relationship_types (
     id               BIGINT AUTO_INCREMENT PRIMARY KEY,
     type_key         VARCHAR(50)  NOT NULL UNIQUE COMMENT '类型标识: spouse/parent/child/sibling',
     type_name        VARCHAR(100) NOT NULL COMMENT '类型显示名: 配偶/父母/子女/兄弟姐妹',
-    capability_role  VARCHAR(20)  NOT NULL COMMENT '能力角色: parent/child',
+    default_role     VARCHAR(20)  DEFAULT NULL COMMENT '默认角色模板: parent/child',
     sort_order       INT DEFAULT 0 COMMENT '排序序号',
     enabled          INT DEFAULT 1 COMMENT '是否启用: 1=启用, 0=禁用',
     created_at       DATETIME DEFAULT CURRENT_TIMESTAMP
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='关系类型配置表';
 
--- 默认关系类型数据
-INSERT IGNORE INTO relationship_types (type_key, type_name, capability_role, sort_order, enabled) VALUES
+-- 默认关系类型数据(default_role: parent/child 为该关系类型的默认角色模板)
+INSERT IGNORE INTO relationship_types (type_key, type_name, default_role, sort_order, enabled) VALUES
 ('spouse', '配偶', 'parent', 1, 1),
 ('parent', '父母', 'parent', 2, 1),
 ('child', '子女', 'child', 3, 1),
@@ -2013,3 +2026,77 @@ INSERT IGNORE INTO indicator_definitions (domain, category, code, name, data_typ
 ALTER TABLE activities ADD COLUMN require_registration TINYINT DEFAULT 1 COMMENT '是否需要报名: 1=需要, 0=直接参与';
 ALTER TABLE activities ADD COLUMN require_checkin_review TINYINT DEFAULT 1 COMMENT '打卡是否需要审核: 1=需要, 0=直接通过';
 ALTER TABLE activities ADD COLUMN energy_penalty_unregistered INT DEFAULT 5 COMMENT '未报名打卡的行能量扣减值';
+
+-- ============================================================
+-- 新增:系统配置表(角色年龄阈值等)
+-- ============================================================
+CREATE TABLE IF NOT EXISTS system_config (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    config_key VARCHAR(100) NOT NULL UNIQUE COMMENT '配置键',
+    config_value VARCHAR(500) COMMENT '配置值',
+    description VARCHAR(255) COMMENT '说明',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统配置表';
+
+-- ============================================================
+-- 数据迁移: children → family_members + roleOverride 变更
+-- 执行时机: Phase 9(所有代码变更完成后、上线前)
+-- ============================================================
+
+-- 1. family_members.capability_role → role_override (已在 DDL 中 ALTER)
+-- 2. relationship_types.capability_role → default_role (已在 DDL 中 ALTER)
+
+-- ============================================================
+-- 数据迁移: children → family_members + roleOverride 变更
+-- 执行时机: Phase 9(所有代码变更完成后、上线前)
+-- ============================================================
+
+-- 1. family_members 已包含所有必要字段(role_override, 孩子专属字段已在 DDL 中)
+-- relationship_types.capability_role → default_role 已在 DDL 中 ALTER
+
+-- 2. 迁移 children 数据到 family_members(匹配更新:family_members 已存在该成员记录时更新)
+UPDATE family_members fm
+INNER JOIN children c ON fm.family_id = c.family_id AND fm.nickname = c.nickname AND fm.user_id = c.user_id
+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.total_points = c.total_points,
+    fm.show_to_family = c.show_to_family,
+    fm.birthday = c.birthday,
+    fm.gender = c.gender,
+    fm.phone = c.phone,
+    fm.role_override = 'child';
+
+-- 3. 迁移 children 数据到 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 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,
+  'child', created_at, updated_at
+FROM children
+WHERE NOT EXISTS (
+    SELECT 1 FROM family_members fm2
+    WHERE fm2.family_id = children.family_id
+      AND fm2.nickname = children.nickname
+      AND fm2.user_id = children.user_id
+);
+
+-- 4. 插入 system_config 初始数据
+INSERT INTO system_config (config_key, config_value, description) VALUES
+  ('child_age_threshold', '18', '未成年年龄分界线(岁)'),
+  ('elderly_age_threshold', '65', '老年年龄分界线(岁)')
+ON DUPLICATE KEY UPDATE config_value = VALUES(config_value);
+
+-- 5. 删除 children 表(最后执行,确认迁移完成后再执行此步)
+-- DROP TABLE IF EXISTS children;

+ 17 - 17
cfc-backend/src/test/java/com/etotem/cfc/controller/GuideFamilyTaskControllerTest.java

@@ -5,10 +5,10 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.controller.guide.GuideFamilyTaskController;
 import com.etotem.cfc.dto.TaskReviewDTO;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.Task;
 import com.etotem.cfc.entity.User;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.TaskMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.TaskService;
@@ -34,7 +34,7 @@ public class GuideFamilyTaskControllerTest {
     private TaskService taskService;
 
     @MockBean
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @MockBean
     private UserMapper userMapper;
@@ -50,14 +50,14 @@ public class GuideFamilyTaskControllerTest {
         return user;
     }
 
-    private Child mockChild(Long id, Long familyId, String nickname) {
-        Child child = new Child();
-        child.setId(id);
-        child.setFamilyId(familyId);
-        child.setNickname(nickname);
-        child.setTotalPoints(100);
-        child.setStreakDays(5);
-        return child;
+    private FamilyMember mockFamilyMember(Long id, Long familyId, String nickname) {
+        FamilyMember member = new FamilyMember();
+        member.setId(id);
+        member.setFamilyId(familyId);
+        member.setNickname(nickname);
+        member.setTotalPoints(100);
+        member.setStreakDays(5);
+        return member;
     }
 
     private Task mockTask(Long id, Long childId, Long familyId, String status) {
@@ -94,7 +94,7 @@ public class GuideFamilyTaskControllerTest {
         params.put("page", 1);
         params.put("size", 10);
         for (String role : NON_TEACHER_ROLES) {
-            Result<?> result = controller.getChildTasks(100L, 20L, role, params);
+            Result<?> result = controller.getFamilyMemberTasks(100L, 20L, role, params);
             assertRoleDenied(result);
         }
     }
@@ -158,8 +158,8 @@ public class GuideFamilyTaskControllerTest {
         User parent = mockUser(10L, "parent", "家长");
         when(userMapper.selectList(any(QueryWrapper.class))).thenReturn(Collections.singletonList(parent));
 
-        Child child = mockChild(20L, 100L, "孩子");
-        when(childMapper.selectList(any(QueryWrapper.class))).thenReturn(Collections.singletonList(child));
+        FamilyMember member = mockFamilyMember(20L, 100L, "孩子");
+        when(familyMemberMapper.selectList(any(QueryWrapper.class))).thenReturn(Collections.singletonList(member));
         when(taskService.getPendingReviewTasksForChild(20L)).thenReturn(new ArrayList<>());
 
         @SuppressWarnings("unchecked")
@@ -172,8 +172,8 @@ public class GuideFamilyTaskControllerTest {
     public void getFamilyTaskOverview_WithChildren() {
         Long guideId = 1L;
         Long familyId = 100L;
-        Child child = mockChild(20L, familyId, "孩子A");
-        when(childMapper.selectList(any(QueryWrapper.class))).thenReturn(Collections.singletonList(child));
+        FamilyMember member = mockFamilyMember(20L, familyId, "孩子A");
+        when(familyMemberMapper.selectList(any(QueryWrapper.class))).thenReturn(Collections.singletonList(member));
         when(taskService.getTodayTasks(20L)).thenReturn(new ArrayList<>());
         when(taskService.getPendingReviewTasksForChild(20L)).thenReturn(new ArrayList<>());
         when(taskService.getAllTasksByChild(20L)).thenReturn(new ArrayList<>());
@@ -210,7 +210,7 @@ public class GuideFamilyTaskControllerTest {
         params.put("page", 1);
         params.put("size", 10);
 
-        Result<Page<Task>> result = controller.getChildTasks(familyId, childId, "teacher", params);
+        Result<Page<Task>> result = controller.getFamilyMemberTasks(familyId, childId, "teacher", params);
         Page<Task> page = result.getData();
         assertEquals(2, page.getTotal());
         assertEquals(2, page.getRecords().size());

+ 9 - 11
cfc-backend/src/test/java/com/etotem/cfc/controller/GuideRolePermissionTest.java

@@ -47,7 +47,7 @@ public class GuideRolePermissionTest {
     private TaskService taskService;
 
     @MockBean
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @MockBean
     private UserMapper userMapper;
@@ -99,14 +99,12 @@ public class GuideRolePermissionTest {
         return user;
     }
 
-    private Child mockChild(Long id, Long familyId) {
-        Child child = new Child();
-        child.setId(id);
-        child.setFamilyId(familyId);
-        child.setNickname("孩子" + id);
-        child.setTotalPoints(100);
-        child.setStreakDays(3);
-        return child;
+    private FamilyMember mockFamilyMember(Long id, Long familyId) {
+        FamilyMember member = new FamilyMember();
+        member.setId(id);
+        member.setFamilyId(familyId);
+        member.setNickname("孩子" + id);
+        return member;
     }
 
     private Task mockTask(Long id, Long childId, Long familyId, String status) {
@@ -163,8 +161,8 @@ public class GuideRolePermissionTest {
 
     @Test
     public void familyTaskOverview_TeacherAllowed() {
-        Child child = mockChild(20L, 100L);
-        when(childMapper.selectList(any(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.class)))
+        FamilyMember child = mockFamilyMember(20L, 100L);
+        when(familyMemberMapper.selectList(any(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.class)))
             .thenReturn(Collections.singletonList(child));
         when(taskService.getTodayTasks(20L)).thenReturn(new ArrayList<>());
         when(taskService.getPendingReviewTasksForChild(20L)).thenReturn(new ArrayList<>());

+ 16 - 16
cfc-backend/src/test/java/com/etotem/cfc/controller/PointsControllerTest.java

@@ -3,9 +3,9 @@ package com.etotem.cfc.controller;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.controller.family.PointsController;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.PointsLog;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.PointsLogMapper;
 import com.etotem.cfc.service.PointsService;
 import org.junit.jupiter.api.Test;
@@ -33,7 +33,7 @@ public class PointsControllerTest {
     private PointsService pointsService;
 
     @MockBean
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     @MockBean
     private PointsLogMapper pointsLogMapper;
@@ -60,7 +60,7 @@ public class PointsControllerTest {
     }
 
     @Test
-    public void getBalance_NullChildId_ReturnsNull() {
+    public void getBalance_NullFamilyMemberId_ReturnsNull() {
         Map<String, Object> params = new HashMap<>();
         // childId not provided
 
@@ -222,7 +222,7 @@ public class PointsControllerTest {
     }
 
     @Test
-    public void getSystemBalance_NullChildId_ReturnsError() {
+    public void getSystemBalance_NullFamilyMemberId_ReturnsError() {
         Map<String, Object> params = new HashMap<>();
         // childId not provided
 
@@ -236,11 +236,11 @@ public class PointsControllerTest {
 
     @Test
     public void pointsService_AdjustPoints_Success() {
-        Child child = new Child();
+        FamilyMember child = new FamilyMember();
         child.setId(10L);
         child.setTotalPoints(100);
 
-        when(childMapper.selectById(10L)).thenReturn(child);
+        when(familyMemberMapper.selectById(10L)).thenReturn(child);
 
         // Simulate adjustPoints behavior
         int newBalance = 100 + 50; // +50 adjustment
@@ -253,11 +253,11 @@ public class PointsControllerTest {
 
     @Test
     public void pointsService_AdjustPoints_NegativeResult() {
-        Child child = new Child();
+        FamilyMember child = new FamilyMember();
         child.setId(10L);
         child.setTotalPoints(30);
 
-        when(childMapper.selectById(10L)).thenReturn(child);
+        when(familyMemberMapper.selectById(10L)).thenReturn(child);
 
         // If adjustment is -50, new balance would be -20
         int newBalance = 30 + (-50);
@@ -266,12 +266,12 @@ public class PointsControllerTest {
 
     @Test
     public void pointsService_DeductSystemPoints_InsufficientBalance() {
-        Child child = new Child();
+        FamilyMember child = new FamilyMember();
         child.setId(10L);
         child.setSystemPoints(10);
         child.setTotalPoints(10);
 
-        when(childMapper.selectById(10L)).thenReturn(child);
+        when(familyMemberMapper.selectById(10L)).thenReturn(child);
 
         // deduct 20 when only 10 available → should return -1
         int newBalance = 10 - 20;
@@ -280,12 +280,12 @@ public class PointsControllerTest {
 
     @Test
     public void pointsService_DeductSystemPoints_ExactBalance() {
-        Child child = new Child();
+        FamilyMember child = new FamilyMember();
         child.setId(10L);
         child.setSystemPoints(50);
         child.setTotalPoints(50);
 
-        when(childMapper.selectById(10L)).thenReturn(child);
+        when(familyMemberMapper.selectById(10L)).thenReturn(child);
 
         int newBalance = 50 - 50;
         assertEquals(0, newBalance);
@@ -293,12 +293,12 @@ public class PointsControllerTest {
 
     @Test
     public void pointsService_AwardSystemPoints_Success() {
-        Child child = new Child();
+        FamilyMember child = new FamilyMember();
         child.setId(10L);
         child.setSystemPoints(100);
         child.setTotalPoints(200);
 
-        when(childMapper.selectById(10L)).thenReturn(child);
+        when(familyMemberMapper.selectById(10L)).thenReturn(child);
 
         int amount = 30;
         int newSystemPoints = 100 + amount; // 130
@@ -313,7 +313,7 @@ public class PointsControllerTest {
     private PointsLog createLog(Long id, Long childId, Integer amount, String type, String description) {
         PointsLog log = new PointsLog();
         log.setId(id);
-        log.setChildId(childId);
+        log.setFamilyMemberId(childId);
         log.setAmount(amount);
         log.setType(type);
         log.setDescription(description);

+ 1 - 1
cfc-backend/src/test/java/com/etotem/cfc/controller/StatsControllerTest.java

@@ -29,7 +29,7 @@ public class StatsControllerTest {
     private UserMapper userMapper;
 
     @MockBean
-    private com.etotem.cfc.mapper.ChildMapper childMapper;
+    private com.etotem.cfc.mapper.FamilyMemberMapper familyMemberMapper;
 
     private User mockParentUser(Long id, Long familyId) {
         User user = new User();

+ 5 - 5
cfc-backend/src/test/java/com/etotem/cfc/integration/DailyCheckinStreakFlowTest.java

@@ -5,11 +5,11 @@ import com.etotem.cfc.controller.DailyCheckinController;
 import com.etotem.cfc.controller.StreakController;
 import com.etotem.cfc.entity.DailyHealthCheckin;
 import com.etotem.cfc.entity.User;
-import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.service.DailyCheckinService;
 import com.etotem.cfc.service.StreakService;
 import com.etotem.cfc.mapper.UserMapper;
-import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import org.junit.jupiter.api.*;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.boot.test.mock.mockito.MockBean;
@@ -61,7 +61,7 @@ public class DailyCheckinStreakFlowTest {
     private UserMapper userMapper;
 
     @MockBean
-    private ChildMapper childMapper;
+    private FamilyMemberMapper familyMemberMapper;
 
     // ========== 测试数据 ==========
 
@@ -154,7 +154,7 @@ public class DailyCheckinStreakFlowTest {
         user.setId(USER_ID);
         user.setFamilyId(FAMILY_ID);
 
-        Child child = new Child();
+        FamilyMember child = new FamilyMember();
         child.setId(CHILD_ID);
         child.setFamilyId(FAMILY_ID);
 
@@ -167,7 +167,7 @@ public class DailyCheckinStreakFlowTest {
         mockProgress.put("pendingReward", true); // 有待领取奖励
 
         when(userMapper.selectById(USER_ID)).thenReturn(user);
-        when(childMapper.selectById(CHILD_ID)).thenReturn(child);
+        when(familyMemberMapper.selectById(CHILD_ID)).thenReturn(child);
         when(streakService.getStreakProgress(CHILD_ID)).thenReturn(mockProgress);
 
         // === When:请求签到进度 ===

+ 2 - 2
cfc-frontend/components/DimensionActivities.vue

@@ -8,7 +8,7 @@
       <text class="empty-tip">{{ isLoggedIn ? '暂无相关活动' : '登录后查看更多活动' }}</text>
     </view>
     <view class="activity-list" v-if="displayActivities && displayActivities.length > 0">
-      <view class="activity-card" v-for="act in displayActivities" :key="act.id" @click="$emit('activityClick', act)">
+      <view class="activity-card" v-for="act in displayActivities" :key="act.id" @click.stop="$emit('activityClick', act)">
         <image class="activity-cover" :src="act.coverImage || '/static/default-activity.png'" mode="aspectFill" />
         <view class="activity-info">
           <text class="activity-name line-clamp-1">{{ act.title }}</text>
@@ -41,7 +41,7 @@ export default {
   },
   computed: {
     displayActivities: function() {
-      return this.activities
+      return (this.activities || []).slice(0, 3)
     }
   },
   methods: {

+ 4 - 6
cfc-frontend/pages.json

@@ -80,12 +80,6 @@
         "navigationBarTitleText": "发现"
       }
     },
-    {
-      "path": "pages/shop/index",
-      "style": {
-        "navigationBarTitleText": "商城"
-      }
-    },
     {
       "path": "pages/action/index",
       "style": {
@@ -398,6 +392,10 @@
     {
       "root": "pages/shop",
       "pages": [
+        {
+          "path": "index/index",
+          "style": { "navigationBarTitleText": "商品首页" }
+        },
         {
           "path": "order-list/order-list",
           "style": { "navigationBarTitleText": "我的订单" }

+ 2 - 2
cfc-frontend/pages/article-center/index.vue

@@ -7,7 +7,7 @@
           v-for="tab in tabList"
           :key="tab.code"
           :class="['tab-item', currentTab === tab.code ? 'active' : '']"
-          :style="currentTab === tab.code ? { color: tab.color, borderBottomColor: tab.color } : {}"
+          :style="currentTab === tab.code ? 'color:' + tab.color + ';border-bottom-color:' + tab.color : ''"
           @click="onTabChange(tab.code)"
         >
           {{ tab.name }}
@@ -67,7 +67,7 @@
                 v-for="dim in parseDimensions(item.relatedDimensions)"
                 :key="dim.code"
                 class="dim-dot"
-                :style="{ background: dim.color }"
+                :style="'background:' + dim.color"
               ></view>
             </view>
           </view>

+ 105 - 9
cfc-frontend/pages/body/index.vue

@@ -27,6 +27,18 @@
       :sandboxData="sandboxData"
       :dualDimension="dualDimension" />
 
+    <!-- ===== 重要关系维护 ===== -->
+    <FamilyRelationshipSection
+      :contactList="contactList"
+      :healthAlerts="healthAlerts"
+      :milestones="milestones"
+      :isLoggedIn="isLoggedIn"
+      @contactClick="onContactClick"
+      @refresh="loadContacts"
+      @import="onShowImport"
+      @interactionLog="goInteractionLog"
+      @questionnaire="goRelationshipQuestionnaire" />
+
     <!-- 关系图谱(登录后可见) -->
     <FamilyRelationGraph
       v-if="isLoggedIn && familyMembersVisible.length > 0"
@@ -47,11 +59,26 @@
       @childChanged="onChildChanged"
       @scrollTo="scrollToSection" />
 
-    <!-- 七维健康全景 -->
-    <HealthDimensionsSection
-      v-if="isLoggedIn"
-      :healthData="dimensionData"
-      :activeChildId="activeChildId" />
+    <!-- 雷达图 -->
+    <view class="section" v-if="dimensionData">
+      <view class="section-header" v-if="dimensionData.isAverage">
+        <text class="section-title">七维健康图</text>
+        <text class="avg-badge">同龄同性平均值 · 上传报告后显示个人数据</text>
+      </view>
+      <view class="section-header" v-else>
+        <text class="section-title">七维健康图</text>
+      </view>
+      <RadarChart
+        v-if="dimensionData && dimensionData.dimensions"
+        :dimensions="dimensionData.dimensions.map(function(d) { return { label: d.label, key: d.dimension } })"
+        :scores="dimensionData.dimensions.map(function(d) { return d.score })"
+        fillColor="#FF8C42"
+        gridColor="#FED7AA"
+        labelColor="#9A3412"
+        :width="580"
+        :height="580"
+        @dimensionClick="goDimensionDetail" />
+    </view>
 
     <!-- 功能入口 -->
     <view class="func-section" v-if="sectionVisible('func_entries')">
@@ -197,9 +224,10 @@ import DimensionActivities from '../../components/DimensionActivities.vue'
 import DimensionProducts from '../../components/DimensionProducts.vue'
 import DimensionArticles from '../../components/DimensionArticles.vue'
 import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
-import HealthDimensionsSection from '../../components/HealthDimensionsSection.vue'
+import RadarChart from '../../components/RadarChart.vue'
 import AIFloatingAvatar from '../../components/AIFloatingAvatar.vue'
-import { getVisibleSections, getEnergyOverview, getChildren, getTodayTasksByCategory, getActivityList, getProductsByDomain, getFamilyEnergySandbox, getVisibleFamilyMembers, getDimensionOverview, getFeaturedArticles } from '../../utils/api.js'
+import FamilyRelationshipSection from '../../components/FamilyRelationshipSection.vue'
+import { getVisibleSections, getEnergyOverview, getChildren, getTodayTasksByCategory, getActivityList, getProductsByDomain, getFamilyEnergySandbox, getVisibleFamilyMembers, getDimensionOverview, getFeaturedArticles, getContactList, getHealthAlerts, getMilestones } from '../../utils/api.js'
 import config from '../../config.js'
 
 var BASE_URL = config.api('')
@@ -230,7 +258,7 @@ const healthRequest = function(url, data) {
 }
 
 export default {
-  components: { TabTransition, PageBanner, LoginGuideCard, HealthTips, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts, DimensionArticles, FamilyRelationGraph, HealthDimensionsSection, AIFloatingAvatar },
+  components: { TabTransition, PageBanner, LoginGuideCard, HealthTips, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts, DimensionArticles, FamilyRelationGraph, RadarChart, AIFloatingAvatar, FamilyRelationshipSection },
   data() {
     return {
       isLoggedIn: false,
@@ -258,6 +286,9 @@ export default {
         { icon: '\u{1F9D8}', label: '冥想', needLogin: true, page: '/pages/health/meditation-index' },
         { icon: '\u{1F4CA}', label: '舌诊', needLogin: true, page: '/pages/health/tongue-index' }
       ],
+      contactList: [],
+      healthAlerts: [],
+      milestones: [],
       healthTips: [
         { id: 1, title: '儿童每日运动指南', summary: '不同年龄段儿童每天需要多少运动量?科学运动助力健康成长。' },
         { id: 2, title: '五行饮食搭配法则', summary: '根据五行理论合理搭配膳食,均衡营养从每一餐开始。' },
@@ -307,6 +338,7 @@ export default {
     if (this.isLoggedIn) {
       this.loadChildren()
       this.loadFamilyMembersVisible()
+      this.loadRelationshipHealth()
     }
     // 游客也能浏览活动和商品
     this.loadDimensionActivities()
@@ -357,6 +389,7 @@ export default {
           if (self.activeChildId) {
             self.loadDashboardData()
           }
+          self.loadContacts()
         }
       }).catch(function(e) {
         console.log('获取孩子列表失败', e)
@@ -449,7 +482,7 @@ export default {
     },
     loadFeaturedArticles: function() {
       var self = this
-      getFeaturedArticles({ size: 5, dimensionCode: 'body' }).then(function(res) {
+      getFeaturedArticles({ size: 5 }).then(function(res) {
         if (res.code === 200 && res.data) {
           var gradientColors = [
             'linear-gradient(135deg, #FF8C42, #FFB074)',
@@ -533,6 +566,14 @@ export default {
         })
         return
       }
+      if (item.page === 'health-dimensions') {
+        if (!this.activeChildId) {
+          uni.showToast({ title: '请先选择孩子', icon: 'none' })
+          return
+        }
+        uni.navigateTo({ url: '/pages/body-detail/health-dimensions?childId=' + this.activeChildId })
+        return
+      }
       if (item.page) uni.navigateTo({ url: item.page })
     },
     goToHealthReport: function() {
@@ -542,6 +583,9 @@ export default {
       }
       uni.navigateTo({ url: '/pages/body-detail/health-report?childId=' + this.activeChildId })
     },
+    goDimensionDetail: function(dimKey) {
+      uni.navigateTo({ url: '/pages/body/dimension-detail?dimension=' + dimKey + '&childId=' + this.activeChildId })
+    },
     goToCheckin: function() {
       if (!this.activeChildId) {
         uni.showToast({ title: '请先选择孩子', icon: 'none' })
@@ -561,6 +605,50 @@ export default {
       var id = this.activeChildId || uni.getStorageSync('currentChildId') || ''
       uni.navigateTo({ url: '/pages/health/report-upload?childId=' + id + '&from=body' })
     },
+    // ===== 重要关系 =====
+    loadContacts: function() {
+      var self = this
+      getContactList({}).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.contactList = Array.isArray(res.data) ? res.data : []
+        }
+      }).catch(function(e) {
+        console.log('获取联系人失败', e)
+      })
+    },
+    loadRelationshipHealth: function() {
+      var self = this
+      getHealthAlerts({}).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.healthAlerts = Array.isArray(res.data) ? res.data : []
+        }
+      }).catch(function() {
+        self.healthAlerts = []
+      })
+      getMilestones({}, 30).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.milestones = Array.isArray(res.data) ? res.data : []
+        }
+      }).catch(function() {
+        self.milestones = []
+      })
+    },
+    onContactClick: function(item) {
+      uni.navigateTo({ url: '/pages/body-detail/contact-detail?id=' + item.id })
+    },
+    onShowImport: function() {
+      uni.showToast({ title: '导入通讯录', icon: 'none' })
+    },
+    goInteractionLog: function(memberId) {
+      if (memberId) {
+        uni.navigateTo({ url: '/pages/body-detail/interaction-log?memberId=' + memberId })
+      } else {
+        uni.navigateTo({ url: '/pages/body-detail/interaction-log' })
+      }
+    },
+    goRelationshipQuestionnaire: function() {
+      uni.navigateTo({ url: '/pages/body-detail/relationship-questionnaire' })
+    },
     formatDate: function(dateStr) {
       if (!dateStr) return '--'
       try {
@@ -638,6 +726,14 @@ export default {
   font-weight: bold;
   color: #333;
 }
+.avg-badge {
+  font-size: 20rpx;
+  color: #F97316;
+  background: #FFF7ED;
+  padding: 4rpx 12rpx;
+  border-radius: 20rpx;
+  margin-left: 12rpx;
+}
 .section-more {
   font-size: 24rpx;
   color: #5B9BD5;

+ 32 - 1
cfc-frontend/pages/discover-detail/product-detail/product-detail.vue

@@ -8,6 +8,7 @@
         class="cover"
         :src="product.coverImage || '/static/default-product.png'"
         mode="widthFix"
+        @error="onImageError"
       />
       <view class="info-section">
         <view class="name-row">
@@ -44,6 +45,9 @@
           <text v-if="product.memberPrice" class="bottom-member">会员{{ formatPriceWithSymbol(product.memberPrice) }}</text>
         </view>
         <view class="action-btns">
+          <view class="action-extra">
+            <button class="btn-cart" @click="onAddToCart">加入购物车</button>
+          </view>
           <button class="btn-buy" @click="onBuy">立即购买</button>
         </view>
       </view>
@@ -55,7 +59,7 @@
 </template>
 
 <script>
-import { productDetail, productOrderCreate, productOrderPay } from '@/utils/api.js'
+import { productDetail, productOrderCreate, productOrderPay, cartAdd } from '@/utils/api.js'
 
 export default {
   data() {
@@ -92,6 +96,10 @@ export default {
     }
   },
   methods: {
+    onImageError(e) {
+      // 图片加载失败时,使用占位图(default-activity.png 已存在于 static/)
+      this.product.coverImage = '/static/default-activity.png'
+    },
     loadDetail(id) {
       this.loading = true
       productDetail({ id: parseInt(id) }).then(res => {
@@ -135,6 +143,29 @@ export default {
         uni.showToast({ title: '创建订单失败', icon: 'none' })
       })
     },
+    onAddToCart() {
+      const userId = uni.getStorageSync('userId')
+      if (!userId) {
+        uni.navigateTo({ url: '/pages/login/login' })
+        return
+      }
+      uni.showLoading({ title: '加入购物车...' })
+      cartAdd({
+        userId: userId,
+        productId: this.product.id,
+        quantity: 1
+      }).then(res => {
+        uni.hideLoading()
+        if (res.code === 200) {
+          uni.showToast({ title: '已加入购物车', icon: 'success' })
+        } else {
+          uni.showToast({ title: res.message || '加入购物车失败', icon: 'none' })
+        }
+      }).catch(() => {
+        uni.hideLoading()
+        uni.showToast({ title: '网络异常', icon: 'none' })
+      })
+    },
     doPay(orderNo) {
       uni.showLoading({ title: '支付中...' })
       productOrderPay({ orderNo }).then(res => {

+ 1 - 0
cfc-frontend/pages/index/parent-index.vue

@@ -231,6 +231,7 @@
       <DimensionActivities
         dimensionCode="all"
         :activities="recommendedActivities"
+        :isLoggedIn="!!token"
         @activityClick="goActivityDetail"
         @moreActivities="goMoreActivities" />
       <view class="error-state" v-if="errorActivities">

+ 3 - 3
cfc-frontend/pages/profile/family-members.vue

@@ -176,10 +176,10 @@ export default {
   },
   computed: {
     parentMembers() {
-      return this.members.filter(function(m) { return m.capabilityRole === 'parent' })
+      return this.members.filter(function(m) { return m.effectiveRole === 'parent' })
     },
     childMembers() {
-      return this.members.filter(function(m) { return m.capabilityRole === 'child' })
+      return this.members.filter(function(m) { return m.effectiveRole === 'child' })
     },
     editableMembers() {
       return this.members.filter(function(m) { return m.source === 'family_member' })
@@ -232,7 +232,7 @@ export default {
         if (res.code === 200 && res.data) {
           store.commit('switchToMember', {
             memberId: res.data.memberId,
-            capabilityRole: res.data.capabilityRole
+            effectiveRole: res.data.effectiveRole
           })
           uni.showToast({ title: '已切换到' + (member.nickname || '该成员'), icon: 'success' })
           setTimeout(function() {

+ 65 - 2
cfc-frontend/pages/wisdom/index.vue

@@ -26,6 +26,18 @@
       :sandboxData="sandboxData"
       :dualDimension="dualDimension" />
 
+    <!-- ===== 重要关系维护 ===== -->
+    <FamilyRelationshipSection
+      :contactList="contactList"
+      :healthAlerts="healthAlerts"
+      :milestones="milestones"
+      :isLoggedIn="isLoggedIn"
+      @contactClick="onContactClick"
+      @refresh="loadContacts"
+      @import="onShowImport"
+      @interactionLog="goInteractionLog"
+      @questionnaire="goRelationshipQuestionnaire" />
+
     <!-- 4. 认知雷达图(登录后) -->
     <view class="section" v-if="isLoggedIn && cognitiveReport">
       <view class="section-header">
@@ -157,10 +169,11 @@ import DimensionProducts from '../../components/DimensionProducts.vue'
 import DimensionArticles from '../../components/DimensionArticles.vue'
 import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
 import AIFloatingAvatar from '../../components/AIFloatingAvatar.vue'
-import { getVisibleSections, getChildren, getTodayTasksByCategory, getActivityList, getProductsByDomain, getFamilyEnergySandbox, getAssessmentLatestResult, getFeaturedArticles } from '../../utils/api.js'
+import FamilyRelationshipSection from '../../components/FamilyRelationshipSection.vue'
+import { getVisibleSections, getChildren, getTodayTasksByCategory, getActivityList, getProductsByDomain, getFamilyEnergySandbox, getAssessmentLatestResult, getFeaturedArticles, getContactList, getHealthAlerts, getMilestones } from '../../utils/api.js'
 
 export default {
-  components: { TabTransition, PageBanner, LoginGuideCard, RadarChart, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts, DimensionArticles, FamilyRelationGraph, AIFloatingAvatar },
+  components: { TabTransition, PageBanner, LoginGuideCard, RadarChart, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts, DimensionArticles, FamilyRelationGraph, AIFloatingAvatar, FamilyRelationshipSection },
   data() {
     return {
       isLoggedIn: false,
@@ -180,6 +193,9 @@ export default {
       dimensionProducts: [],
       articles: [],
       wisdomTips: [],
+      contactList: [],
+      healthAlerts: [],
+      milestones: [],
       funcList: [
         { icon: '\u{1F4CA}', label: '认知报告', needLogin: true, page: '/pages/wisdom-detail/cognitive-report' },
         { icon: '\u{270F}\uFE0F', label: '自己出题', needLogin: true, page: '/pages/wisdom-detail/self-quiz' },
@@ -259,6 +275,7 @@ export default {
       this.loadChildren()
       this.loadFamilyMembersVisible()
       this.loadCognitiveReport()
+      this.loadRelationshipHealth()
     }
     this.loadDimensionActivities()
     this.loadDimensionProducts()
@@ -310,6 +327,7 @@ export default {
           // Load cognitive report AFTER activeChildId is confirmed set
           self.loadCognitiveReport()
         }
+        self.loadContacts()
       }).catch(function(e) {})
     },
     loadFamilyMembersVisible: function() {
@@ -467,6 +485,51 @@ export default {
     },
     goArticleDetail: function(article) {
       uni.navigateTo({ url: '/pages/mind-detail/article-detail?id=' + (article.id || article) })
+    },
+
+    // ===== 重要关系 =====
+    loadContacts: function() {
+      var self = this
+      getContactList({}).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.contactList = Array.isArray(res.data) ? res.data : []
+        }
+      }).catch(function(e) {
+        console.log('获取联系人失败', e)
+      })
+    },
+    loadRelationshipHealth: function() {
+      var self = this
+      getHealthAlerts({}).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.healthAlerts = Array.isArray(res.data) ? res.data : []
+        }
+      }).catch(function() {
+        self.healthAlerts = []
+      })
+      getMilestones({}, 30).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.milestones = Array.isArray(res.data) ? res.data : []
+        }
+      }).catch(function() {
+        self.milestones = []
+      })
+    },
+    onContactClick: function(item) {
+      uni.navigateTo({ url: '/pages/wisdom-detail/contact-detail?id=' + item.id })
+    },
+    onShowImport: function() {
+      uni.showToast({ title: '导入通讯录', icon: 'none' })
+    },
+    goInteractionLog: function(memberId) {
+      if (memberId) {
+        uni.navigateTo({ url: '/pages/wisdom-detail/interaction-log?memberId=' + memberId })
+      } else {
+        uni.navigateTo({ url: '/pages/wisdom-detail/interaction-log' })
+      }
+    },
+    goRelationshipQuestionnaire: function() {
+      uni.navigateTo({ url: '/pages/wisdom-detail/relationship-questionnaire' })
     }
   }
 }

+ 7 - 7
cfc-frontend/store/index.js

@@ -13,7 +13,7 @@ const store = new Vuex.Store({
     nickname: '',
     currentChildId: '',
     currentMemberId: '', // 当前切换到的家庭成员ID(family_members表)
-    currentMemberRole: '', // 当前切换到的家庭成员capabilityRole
+    currentMemberRole: '', // 当前切换到的家庭成员effectiveRole
     children: [],
     isSwitchedChild: false, // 家长切换到孩子状态的标记
     isSwitchedMember: false // 通过family_members切换的标记
@@ -68,19 +68,19 @@ const store = new Vuex.Store({
     },
     // 通过family_members切换到某个成员
     switchToMember(state, payload) {
-      state.currentRole = payload.capabilityRole
+      state.currentRole = payload.effectiveRole
       state.currentMemberId = payload.memberId
-      state.currentMemberRole = payload.capabilityRole
+      state.currentMemberRole = payload.effectiveRole
       state.isSwitchedMember = true
-      if (payload.capabilityRole === 'child') {
+      if (payload.effectiveRole === 'child') {
         state.isSwitchedChild = true
         state.currentChildId = payload.memberId
       }
-      uni.setStorageSync('currentRole', payload.capabilityRole)
+      uni.setStorageSync('currentRole', payload.effectiveRole)
       uni.setStorageSync('currentMemberId', payload.memberId)
-      uni.setStorageSync('currentMemberRole', payload.capabilityRole)
+      uni.setStorageSync('currentMemberRole', payload.effectiveRole)
       uni.setStorageSync('isSwitchedMember', true)
-      if (payload.capabilityRole === 'child') {
+      if (payload.effectiveRole === 'child') {
         uni.setStorageSync('currentChildId', payload.memberId)
         uni.setStorageSync('isSwitchedChild', true)
       }

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

@@ -1599,3 +1599,9 @@ export const getPointsExchangeProductDetail = (data) => request('/api/points/exc
 export const submitPointsExchange = (data) => request('/api/points/exchange/submit', 'POST', data)
 export const getPointsExchangeRecords = () => request('/api/points/exchange/records', 'POST', {})
 export const getPointsExchangeStats = () => request('/api/points/exchange/stats', 'POST', {})
+
+// ===== 购物车 =====
+export const cartAdd = (data) => request('/api/cart/add', 'POST', data)
+export const cartList = (data) => request('/api/cart/list', 'POST', data)
+export const cartUpdate = (data) => request('/api/cart/update', 'POST', data)
+export const cartRemove = (data) => request('/api/cart/remove', 'POST', data)

+ 6 - 6
cfc-web/src/views/admin/RelationshipTypes.vue

@@ -9,10 +9,10 @@
         <el-table-column prop="id" label="ID" width="80"></el-table-column>
         <el-table-column prop="typeKey" label="编码" width="120"></el-table-column>
         <el-table-column prop="typeName" label="显示名称" width="120"></el-table-column>
-        <el-table-column prop="capabilityRole" label="能力角色" width="120">
+        <el-table-column prop="defaultRole" label="默认角色" width="120">
           <template slot-scope="scope">
-            <el-tag :type="scope.row.capabilityRole === 'parent' ? 'primary' : 'success'" size="mini">
-              {{ scope.row.capabilityRole === 'parent' ? '家长' : '孩子' }}
+            <el-tag :type="scope.row.defaultRole === 'parent' ? 'primary' : 'success'" size="mini">
+              {{ scope.row.defaultRole === 'parent' ? '家长' : '孩子' }}
             </el-tag>
           </template>
         </el-table-column>
@@ -41,8 +41,8 @@
         <el-form-item label="显示名称">
           <el-input v-model="form.typeName" placeholder="如: 子女, 配偶, 父母, 兄弟姐妹"></el-input>
         </el-form-item>
-        <el-form-item label="能力角色">
-          <el-select v-model="form.capabilityRole" placeholder="选择能力角色">
+        <el-form-item label="默认角色">
+          <el-select v-model="form.defaultRole" placeholder="选择默认角色">
             <el-option label="家长(配偶/父母等)" value="parent"></el-option>
             <el-option label="孩子(子女/兄弟姐妹等)" value="child"></el-option>
           </el-select>
@@ -84,7 +84,7 @@ export default {
         id: null,
         typeKey: '',
         typeName: '',
-        capabilityRole: 'child',
+        defaultRole: 'child',
         sortOrder: 0,
         enabled: 1
       }