Browse Source

Merge remote-tracking branch 'origin/cfclub' into cfclub

E2E Test Bot 1 month ago
parent
commit
9266878c26
100 changed files with 1572 additions and 3808 deletions
  1. 105 0
      cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  2. 21 11
      cfc-backend/src/main/java/com/etotem/cfc/controller/MembershipController.java
  3. 32 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/PurchaseController.java
  4. 20 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCouponController.java
  5. 50 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/InnatePortraitConfigController.java
  6. 51 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/NumSoulDetailConfigController.java
  7. 3 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/PaymentController.java
  8. 89 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/mind/InnatePortraitController.java
  9. 6 4
      cfc-backend/src/main/java/com/etotem/cfc/controller/notice/NoticeController.java
  10. 21 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/InnatePortraitReportVO.java
  11. 39 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/InnatePortraitVO.java
  12. 26 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/InnateTrajectoryVO.java
  13. 36 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/NumSoulDetailVO.java
  14. 3 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/Coupon.java
  15. 39 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/InnatePortraitConfig.java
  16. 34 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/InnatePortraitReport.java
  17. 36 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/NumSoulDetailConfig.java
  18. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/InnatePortraitConfigMapper.java
  19. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/InnatePortraitReportMapper.java
  20. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/NumSoulDetailConfigMapper.java
  21. 30 0
      cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java
  22. 3 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ChallengeService.java
  23. 49 5
      cfc-backend/src/main/java/com/etotem/cfc/service/CouponService.java
  24. 10 0
      cfc-backend/src/main/java/com/etotem/cfc/service/DailyCheckinService.java
  25. 10 0
      cfc-backend/src/main/java/com/etotem/cfc/service/EmotionCheckinService.java
  26. 71 3
      cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberAttributeService.java
  27. 3 0
      cfc-backend/src/main/java/com/etotem/cfc/service/FamilyPlatformPointsService.java
  28. 16 3
      cfc-backend/src/main/java/com/etotem/cfc/service/FinanceCheckinService.java
  29. 290 0
      cfc-backend/src/main/java/com/etotem/cfc/service/InnatePortraitService.java
  30. 194 0
      cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java
  31. 66 0
      cfc-backend/src/main/java/com/etotem/cfc/service/NumSoulCalculator.java
  32. 10 2
      cfc-backend/src/main/java/com/etotem/cfc/service/PaymentService.java
  33. 3 0
      cfc-backend/src/main/java/com/etotem/cfc/service/PointsExchangeService.java
  34. 4 0
      cfc-backend/src/main/java/com/etotem/cfc/service/PurchaseService.java
  35. 67 0
      cfc-backend/src/main/java/com/etotem/cfc/service/VirtualPayService.java
  36. 56 0
      cfc-backend/src/main/java/com/etotem/cfc/service/impl/ChallengeServiceImpl.java
  37. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/service/impl/PurchaseServiceImpl.java
  38. 45 0
      cfc-backend/src/main/resources/schema.sql
  39. 0 251
      cfc-backend/src/test/java/com/etotem/cfc/controller/AdminArticleControllerTest.java
  40. 0 286
      cfc-backend/src/test/java/com/etotem/cfc/controller/ArticleAdminControllerTest.java
  41. 0 381
      cfc-backend/src/test/java/com/etotem/cfc/controller/EnergyControllerTest.java
  42. 0 335
      cfc-backend/src/test/java/com/etotem/cfc/controller/HealthReportControllerTest.java
  43. 0 323
      cfc-backend/src/test/java/com/etotem/cfc/controller/PointsControllerTest.java
  44. 0 345
      cfc-backend/src/test/java/com/etotem/cfc/controller/TaskControllerTest.java
  45. 0 193
      cfc-backend/src/test/java/com/etotem/cfc/controller/family/FamilyMembersControllerTest.java
  46. 0 337
      cfc-backend/src/test/java/com/etotem/cfc/integration/FamilyMemberManagementFlowTest.java
  47. 0 297
      cfc-backend/src/test/java/com/etotem/cfc/integration/ProductPurchaseFlowTest.java
  48. 0 431
      cfc-backend/src/test/java/com/etotem/cfc/integration/WishExchangeFlowTest.java
  49. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/AIChatControllerTest.java
  50. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/ActivityControllerTest.java
  51. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/ArticleCategoryControllerTest.java
  52. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/ArticleControllerTest.java
  53. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/AuthControllerTest.java
  54. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/DailyTaskControllerTest.java
  55. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/DimensionAdminControllerTest.java
  56. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/FamilyControllerTest.java
  57. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/GuideFamilyTaskControllerTest.java
  58. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/GuideOrderControllerTest.java
  59. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/GuideRolePermissionTest.java
  60. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/GuideTeamControllerTest.java
  61. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/HealthDimensionControllerTest.java
  62. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/PaymentControllerTest.java
  63. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/ProductControllerTest.java
  64. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/ProductOrderControllerTest.java
  65. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/StatsControllerTest.java
  66. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/WishControllerTest.java
  67. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/mind/MindFortuneControllerTest.java
  68. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/ActivityAdminServiceTest.java
  69. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/ActivityServicePublishTest.java
  70. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/ArticleWorkflowTest.java
  71. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/ChallengeScoreServiceTest.java
  72. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/CouponServiceTest.java
  73. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/DailyOverviewServiceTest.java
  74. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/DanReportParseServiceTest.java
  75. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/DataMigrationServiceTest.java
  76. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/DimensionEnergySyncServiceTest.java
  77. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/DimensionScoreServiceTest.java
  78. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/FiveDimensionSelfCheckServiceTest.java
  79. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/FoodRecommendServiceTest.java
  80. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/FoodServiceTest.java
  81. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/FortuneServiceTest.java
  82. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/GuideApplicationServiceTest.java
  83. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/GuideFamilyServiceTest.java
  84. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/GuideHierarchyServiceTest.java
  85. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/GuidePackageServiceTest.java
  86. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/GuidePackageTemplateServiceTest.java
  87. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/MiniGameServiceTest.java
  88. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/NutritionDeficiencyServiceTest.java
  89. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/PdfParseServiceTest.java
  90. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/ProductServiceTest.java
  91. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/RelationshipQuestionnaireServiceTest.java
  92. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/ReportBlockAssemblerTest.java
  93. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/ServiceContentServiceTest.java
  94. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/ServiceTypeServiceTest.java
  95. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/SupplySystemServiceTest.java
  96. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/TaskServiceMinigameTest.java
  97. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/TeacherLoginTest.java
  98. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/impl/DimensionScoreServiceImplTest.java
  99. 0 327
      cfc-backend/src/test/java/com/etotem/cfc/service/ArticleServiceTest.java
  100. 0 270
      cfc-backend/src/test/java/com/etotem/cfc/service/FamilyChallengeServiceTest.java

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

@@ -3154,6 +3154,96 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("sleep_record 时间列修改失败(可能已为 TIME): {}", e.getMessage());
         }
 
+        // ==================== 迁移270: 先天画像来源权重配置表 ====================
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS innate_portrait_config (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
+                    "source_type VARCHAR(20) NOT NULL," +
+                    "source_name VARCHAR(50) NOT NULL," +
+                    "weight INT DEFAULT 25," +
+                    "enabled TINYINT(1) DEFAULT 1," +
+                    "ai_enabled TINYINT(1) DEFAULT 1," +
+                    "sort_order INT DEFAULT 0," +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
+                    "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP," +
+                    "UNIQUE KEY uk_source (source_type)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='先天画像来源权重配置'");
+            Integer cnt = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM innate_portrait_config", Integer.class);
+            if (cnt == null || cnt == 0) {
+                jdbcTemplate.batchUpdate(
+                    "INSERT INTO innate_portrait_config (source_type, source_name, weight, sort_order) VALUES (?, ?, ?, ?)",
+                    java.util.Arrays.asList(new Object[][]{
+                        {"zodiac", "星座", 2500, 1},
+                        {"bazi", "八字", 2500, 2},
+                        {"blood", "血型", 2500, 3},
+                        {"numsoul", "数字能量", 2500, 4},
+                    })
+                );
+                log.info("迁移270: innate_portrait_config 种子数据已加载 (4条)");
+            }
+            log.info("迁移270: innate_portrait_config 表创建完成");
+        } catch (Exception e) {
+            log.warn("迁移270失败: {}", e.getMessage());
+        }
+
+        // ==================== 迁移271: 先天画像报告缓存表 ====================
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS innate_portrait_report (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
+                    "member_id BIGINT NOT NULL," +
+                    "member_type VARCHAR(10) NOT NULL," +
+                    "family_id BIGINT NOT NULL," +
+                    "portrait_json TEXT," +
+                    "ai_reading TEXT," +
+                    "generated_at DATETIME," +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
+                    "UNIQUE KEY uk_member (member_id, member_type)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='先天画像报告缓存'");
+            log.info("迁移271: innate_portrait_report 表创建完成");
+        } catch (Exception e) {
+            log.warn("迁移271失败: {}", e.getMessage());
+        }
+
+        // ==================== 迁移272: 数字能量详细配置表 ====================
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS numsoul_detail_config (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
+                    "number_type VARCHAR(20) NOT NULL," +
+                    "number_value INT NOT NULL," +
+                    "title VARCHAR(50)," +
+                    "keywords VARCHAR(200)," +
+                    "mind_base INT DEFAULT 0," +
+                    "mind_advice TEXT," +
+                    "color_hex VARCHAR(10)," +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
+                    "UNIQUE KEY uk_type_num (number_type, number_value)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数字能量详细配置'");
+            Integer cnt2 = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM numsoul_detail_config", Integer.class);
+            if (cnt2 == null || cnt2 == 0) {
+                jdbcTemplate.batchUpdate(
+                    "INSERT INTO numsoul_detail_config (number_type, number_value, title, keywords, mind_base, mind_advice, color_hex) VALUES (?, ?, ?, ?, ?, ?, ?)",
+                    java.util.Arrays.asList(new Object[][]{
+                        {"life_path", 1, "开创者", "独立、领导、创新", 68, "培养团队协作意识,避免独断专行", "#FF4444"},
+                        {"life_path", 2, "和平者", "温和、合作、敏感", 72, "增强自我主张,避免过度迎合", "#FF8C00"},
+                        {"life_path", 3, "表达者", "创意、社交、乐观", 78, "保持专注,避免浅尝辄止", "#FFD700"},
+                        {"life_path", 4, "建设者", "务实、稳定、有序", 70, "学会变通,避免过于固执", "#4CAF50"},
+                        {"life_path", 5, "自由者", "冒险、变化、好奇", 75, "培养持久力,避免缺乏恒心", "#2196F3"},
+                        {"life_path", 6, "关怀者", "责任、关爱、奉献", 80, "学会自我关爱,避免过度付出", "#9C27B0"},
+                        {"life_path", 7, "探索者", "智慧、分析、内省", 76, "与人分享见解,避免过度封闭", "#673AB7"},
+                        {"life_path", 8, "成就者", "雄心、管理、决策", 72, "关注内在成长,避免过度追求外在", "#3F51B5"},
+                        {"life_path", 9, "博爱者", "包容、智慧、善良", 82, "设定个人边界,避免过度牺牲", "#E91E63"},
+                        {"life_path", 11, "启明者", "直觉、灵感、理想", 85, "落地执行,避免空想", "#7C4DFF"},
+                        {"life_path", 22, "建造师", "远见、实践、宏图", 88, "分步落地大目标", "#536DFE"},
+                        {"life_path", 33, "引路人", "大爱、奉献、疗愈", 90, "先照顾好自己再照亮他人", "#FF4081"},
+                    })
+                );
+                log.info("迁移272: numsoul_detail_config 生命灵数种子已加载");
+            }
+            log.info("迁移272: numsoul_detail_config 表创建完成");
+        } catch (Exception e) {
+            log.warn("迁移272失败: {}", e.getMessage());
+        }
+
     }
 
     private void migrateHealthDimensionsTable() {
@@ -9408,6 +9498,8 @@ public class DatabaseInitializer implements CommandLineRunner {
 
         // 迁移267: 方案中心菜单(插在家庭运营与任务中心之间)
         migratePlanCenterMenu();
+        // 迁移268: coupon 表添加 status 列(优惠券停用/启用)
+        migrateCouponStatus();
     }
 
     /**
@@ -9468,6 +9560,19 @@ public class DatabaseInitializer implements CommandLineRunner {
         }
     }
 
+    /**
+     * 迁移268: coupon 表添加 status 列(优惠券停用/启用,幂等)
+     * 存量数据 status 为 NULL,查询层按 ACTIVE 处理,不影响现有券。
+     */
+    private void migrateCouponStatus() {
+        try {
+            jdbcTemplate.execute("ALTER TABLE coupon ADD COLUMN status VARCHAR(16) DEFAULT 'ACTIVE' COMMENT '状态:ACTIVE=启用/DISABLED=停用'");
+            log.info("迁移268: 已为 coupon 表添加 status 列");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+    }
+
     /**
      * 迁移249: 存量库 sys_menu 菜单归类调整 + 权限点拆分(幂等,按 path/title 定位)
      * 1. 审核中心改为分组,归集:服务角色申请审核/活动审核/待退款管理/收益提现审核

+ 21 - 11
cfc-backend/src/main/java/com/etotem/cfc/controller/MembershipController.java

@@ -222,35 +222,45 @@ public class MembershipController {
             return Result.error("用户未登录");
         }
 
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            return Result.error("用户不存在");
+        }
+
         Long familyId = membershipService.getUserFamilyId(userId);
         String levelCode = params.get("levelCode").toString();
         String paymentType = params.get("paymentType").toString();
         String period = params.get("period") != null ? params.get("period").toString() : "yearly";
         Long couponId = ParamUtils.getLong(params.get("couponId"));
+        String openid = params.get("openid") != null ? params.get("openid").toString() : null;
 
-        PaymentOrderDTO order = membershipService.createOrder(userId, familyId, levelCode, paymentType, period, couponId);
-
-        // trial 单:createOrder 内已直接开通(status=paid),无需支付,原样返回
+        // trial 单:createOrderWithPayType 内直接开通(status=paid),返回 payType=none
         if ("trial".equals(paymentType)) {
-            return Result.success(order);
+            Map<String, Object> result = membershipService.createOrderWithPayType(
+                    userId, familyId, levelCode, paymentType, period, couponId, null, null);
+            return Result.success(result);
         }
 
-        // testMode 兼容:返回订单信息,前端继续走 /notify 模拟支付(保留现有模拟逻辑)
+        // testMode:保持现有模拟逻辑(不触发真实虚拟/普通支付)
         if (testMode) {
+            PaymentOrderDTO order = membershipService.createOrder(userId, familyId, levelCode, paymentType, period, couponId);
             return Result.success(order);
         }
 
-        // sessionKey 后端从登录态取(users.session_key,@JsonIgnore 防泄露),不由前端传
-        User user = userMapper.selectById(userId);
-        if (user == null || user.getSessionKey() == null || user.getSessionKey().isEmpty()) {
+        // sessionKey 后端从登录态取,虚拟/普通支付路径均需有效 sessionKey
+        if (user.getSessionKey() == null || user.getSessionKey().isEmpty()) {
             return Result.error(400, "会话失效,请重新登录");
         }
 
-        VirtualPayParamsDTO dto = virtualPayService.generateMembershipPayParams(order.getOrderNo(), user.getSessionKey());
-        if (dto == null) {
+        // 调用新方法,按开关分流
+        Map<String, Object> result = membershipService.createOrderWithPayType(
+                userId, familyId, levelCode, paymentType, period, couponId,
+                user.getSessionKey(), openid);
+
+        if (result == null) {
             return Result.error(46001, "该商品暂未开放购买");
         }
-        return Result.success(dto);
+        return Result.success(result);
     }
 
     /**

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

@@ -1,8 +1,12 @@
 package com.etotem.cfc.controller;
 
 import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.VirtualPayParamsDTO;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.PurchaseService;
 import com.etotem.cfc.service.ChallengeService;
+import com.etotem.cfc.service.VirtualPayService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
@@ -15,6 +19,8 @@ public class PurchaseController {
 
     @Autowired private PurchaseService purchaseService;
     @Autowired private ChallengeService challengeService;
+    @Autowired private VirtualPayService virtualPayService;
+    @Autowired private UserMapper userMapper;
 
     @PostMapping("/check")
     public Result<Map<String, Object>> checkPurchase(HttpServletRequest request) {
@@ -31,6 +37,29 @@ public class PurchaseController {
         return Result.success(result);
     }
 
+    @PostMapping("/pay-params")
+    public Result<Object> getPayParams(@RequestBody Map<String, Object> params, HttpServletRequest request) {
+        Long userId = (Long) request.getAttribute("userId");
+        if (userId == null) {
+            return Result.error("用户未登录");
+        }
+        String orderNo = (String) params.get("orderNo");
+        if (orderNo == null || orderNo.isEmpty()) {
+            return Result.error("缺少orderNo");
+        }
+
+        User user = userMapper.selectById(userId);
+        if (user == null || user.getSessionKey() == null || user.getSessionKey().isEmpty()) {
+            return Result.error(400, "会话失效,请重新登录");
+        }
+
+        VirtualPayParamsDTO dto = virtualPayService.generatePurchasePayParams(orderNo, user.getSessionKey());
+        if (dto == null) {
+            return Result.error(46001, "该商品暂未开放购买");
+        }
+        return Result.success(dto);
+    }
+
     @PostMapping("/confirm")
     public Result<Map<String, Object>> confirmPayment(@RequestBody Map<String, Object> params, HttpServletRequest request) {
         String orderNo = (String) params.get("orderNo");
@@ -56,8 +85,9 @@ public class PurchaseController {
         return Result.success(result);
     }
 
-    @GetMapping("/challenge/progress")
-    public Result<Map<String, Object>> getChallengeProgress(@RequestParam Long memberId, HttpServletRequest request) {
+    @PostMapping("/challenge/progress")
+    public Result<Map<String, Object>> getChallengeProgress(@RequestBody Map<String, Object> params, HttpServletRequest request) {
+        Long memberId = params.get("memberId") != null ? ((Number) params.get("memberId")).longValue() : null;
         Map<String, Object> result = challengeService.getChallengeProgress(memberId);
         return Result.success(result);
     }

+ 20 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCouponController.java

@@ -90,6 +90,26 @@ public class AdminCouponController {
         return Result.success("删除成功");
     }
 
+    @PostMapping("/toggle-status")
+    public Result<String> toggleStatus(@RequestBody Map<String, Object> params,
+                                       @RequestAttribute("role") String role) {
+        if (!"admin".equals(role)) {
+            return Result.error("无权限");
+        }
+        Long id = ParamUtils.getLong(params.get("id"));
+        String status = params.get("status") == null ? null : params.get("status").toString();
+        if (id == null || !("ACTIVE".equals(status) || "DISABLED".equals(status))) {
+            return Result.error("参数错误");
+        }
+        Coupon coupon = couponMapper.selectById(id);
+        if (coupon == null) {
+            return Result.error("优惠券不存在");
+        }
+        coupon.setStatus(status);
+        couponMapper.updateById(coupon);
+        return Result.success("ACTIVE".equals(status) ? "启用成功" : "停用成功");
+    }
+
     @PostMapping("/grant-log")
     public Result<List<CouponGrantLog>> grantLog(@RequestBody Map<String, Object> params,
                                                  @RequestAttribute("role") String role) {

+ 50 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/InnatePortraitConfigController.java

@@ -0,0 +1,50 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.InnatePortraitConfig;
+import com.etotem.cfc.mapper.InnatePortraitConfigMapper;
+import com.etotem.cfc.service.FamilyMemberAttributeService;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+@Slf4j
+@RestController
+@RequestMapping("/api/admin/innate-portrait-config")
+public class InnatePortraitConfigController {
+
+    @Resource
+    private InnatePortraitConfigMapper innatePortraitConfigMapper;
+
+    @Resource
+    private FamilyMemberAttributeService familyMemberAttributeService;
+
+    @PostMapping("/list")
+    public Result<List<InnatePortraitConfig>> list() {
+        return Result.success(innatePortraitConfigMapper.selectList(
+                new LambdaQueryWrapper<InnatePortraitConfig>().orderByAsc(InnatePortraitConfig::getSortOrder)));
+    }
+
+    @PostMapping("/update")
+    public Result<Void> update(@RequestBody InnatePortraitConfig config) {
+        if (config.getId() == null) {
+            return Result.error("缺少id");
+        }
+        innatePortraitConfigMapper.update(null, new LambdaUpdateWrapper<InnatePortraitConfig>()
+                .eq(InnatePortraitConfig::getId, config.getId())
+                .set(InnatePortraitConfig::getWeight, config.getWeight())
+                .set(InnatePortraitConfig::getEnabled, config.getEnabled())
+                .set(InnatePortraitConfig::getAiEnabled, config.getAiEnabled()));
+        return Result.success(null);
+    }
+
+    @PostMapping("/invalidate")
+    public Result<Void> invalidate() {
+        familyMemberAttributeService.invalidateAllInnateScores();
+        return Result.success(null);
+    }
+}

+ 51 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/NumSoulDetailConfigController.java

@@ -0,0 +1,51 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.NumSoulDetailConfig;
+import com.etotem.cfc.mapper.NumSoulDetailConfigMapper;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+@Slf4j
+@RestController
+@RequestMapping("/api/admin/numsoul-detail-config")
+public class NumSoulDetailConfigController {
+
+    @Resource
+    private NumSoulDetailConfigMapper numSoulDetailConfigMapper;
+
+    @PostMapping("/list")
+    public Result<List<NumSoulDetailConfig>> list(@RequestBody(required = false) java.util.Map<String, Object> params) {
+        LambdaQueryWrapper<NumSoulDetailConfig> wrapper = new LambdaQueryWrapper<>();
+        if (params != null && params.get("numberType") != null) {
+            wrapper.eq(NumSoulDetailConfig::getNumberType, params.get("numberType").toString());
+        }
+        wrapper.orderByAsc(NumSoulDetailConfig::getNumberType).orderByAsc(NumSoulDetailConfig::getNumberValue);
+        return Result.success(numSoulDetailConfigMapper.selectList(wrapper));
+    }
+
+    @PostMapping("/create")
+    public Result<Void> create(@RequestBody NumSoulDetailConfig config) {
+        numSoulDetailConfigMapper.insert(config);
+        return Result.success(null);
+    }
+
+    @PostMapping("/update")
+    public Result<Void> update(@RequestBody NumSoulDetailConfig config) {
+        if (config.getId() == null) {
+            return Result.error("缺少id");
+        }
+        numSoulDetailConfigMapper.updateById(config);
+        return Result.success(null);
+    }
+
+    @PostMapping("/delete")
+    public Result<Void> delete(@RequestParam Long id) {
+        numSoulDetailConfigMapper.deleteById(id);
+        return Result.success(null);
+    }
+}

+ 3 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/guide/PaymentController.java

@@ -74,7 +74,8 @@ public class PaymentController {
      */
     @Operation(summary = "创建微信Native支付")
     @PostMapping("/wechat/native")
-    public Result<Map<String, Object>> createWechatNativePay(@RequestBody Map<String, Object> params) {
+    public Result<Map<String, Object>> createWechatNativePay(@RequestBody Map<String, Object> params,
+                                                             @RequestAttribute("userId") Long userId) {
         String orderNo = (String) params.get("orderNo");
         String description = (String) params.get("description");
         Integer amount = (Integer) params.get("amount");
@@ -83,7 +84,7 @@ public class PaymentController {
         String contactAddress = (String) params.get("contactAddress");
 
         // 保存联系人信息
-        paymentService.saveContactInfo(orderNo, contactName, contactPhone, contactAddress);
+        paymentService.saveContactInfo(orderNo, contactName, contactPhone, contactAddress, userId);
 
         Map<String, Object> result = paymentService.createWechatNativePayOrder(orderNo, description, amount);
         return Result.success(result);

+ 89 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/mind/InnatePortraitController.java

@@ -0,0 +1,89 @@
+package com.etotem.cfc.controller.mind;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.InnatePortraitReportVO;
+import com.etotem.cfc.dto.InnatePortraitVO;
+import com.etotem.cfc.dto.InnateTrajectoryVO;
+import com.etotem.cfc.dto.NumSoulDetailVO;
+import com.etotem.cfc.service.InnatePortraitService;
+import com.etotem.cfc.util.ParamUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Map;
+
+@Slf4j
+@RestController
+@RequestMapping("/api/mind/innate")
+public class InnatePortraitController {
+
+    @Resource
+    private InnatePortraitService innatePortraitService;
+
+    @PostMapping("/portrait")
+    public Result<InnatePortraitVO> portrait(@RequestBody Map<String, Object> params) {
+        try {
+            Long memberId = ParamUtils.getLong(params.get("memberId"));
+            String memberType = params.containsKey("memberType") ? params.get("memberType").toString() : "child";
+            InnatePortraitVO vo = innatePortraitService.getInnatePortrait(memberId, memberType);
+            if (vo == null) {
+                return Result.error("成员不存在或未设置出生信息");
+            }
+            return Result.success(vo);
+        } catch (Exception e) {
+            log.error("获取先天画像失败", e);
+            return Result.error(e.getMessage());
+        }
+    }
+
+    @PostMapping("/reading")
+    public Result<InnatePortraitReportVO> reading(@RequestBody Map<String, Object> params) {
+        try {
+            Long memberId = ParamUtils.getLong(params.get("memberId"));
+            String memberType = params.containsKey("memberType") ? params.get("memberType").toString() : "child";
+            Long familyId = ParamUtils.getLong(params.get("familyId"));
+            InnatePortraitReportVO vo = innatePortraitService.generateAiReading(memberId, memberType, familyId);
+            if (vo == null) {
+                return Result.error("成员不存在或未设置出生信息");
+            }
+            return Result.success(vo);
+        } catch (Exception e) {
+            log.error("生成AI解读失败", e);
+            return Result.error(e.getMessage());
+        }
+    }
+
+    @PostMapping("/numsoul")
+    public Result<NumSoulDetailVO> numsoul(@RequestBody Map<String, Object> params) {
+        try {
+            Integer year = params.get("year") instanceof Number ? ((Number) params.get("year")).intValue() : null;
+            Integer month = params.get("month") instanceof Number ? ((Number) params.get("month")).intValue() : null;
+            Integer day = params.get("day") instanceof Number ? ((Number) params.get("day")).intValue() : null;
+            if (year == null || month == null || day == null) {
+                return Result.error("请提供出生年月日");
+            }
+            return Result.success(innatePortraitService.calculateNumSoul(year, month, day));
+        } catch (Exception e) {
+            log.error("获取数字能量失败", e);
+            return Result.error(e.getMessage());
+        }
+    }
+
+    @PostMapping("/trajectory")
+    public Result<InnateTrajectoryVO> trajectory(@RequestBody Map<String, Object> params) {
+        try {
+            Long memberId = ParamUtils.getLong(params.get("memberId"));
+            String memberType = params.containsKey("memberType") ? params.get("memberType").toString() : "child";
+            Long familyId = ParamUtils.getLong(params.get("familyId"));
+            InnateTrajectoryVO vo = innatePortraitService.getTrajectory(memberId, memberType, familyId);
+            if (vo == null) {
+                return Result.error("成员不存在或未设置出生信息");
+            }
+            return Result.success(vo);
+        } catch (Exception e) {
+            log.error("获取成长轨迹失败", e);
+            return Result.error(e.getMessage());
+        }
+    }
+}

+ 6 - 4
cfc-backend/src/main/java/com/etotem/cfc/controller/notice/NoticeController.java

@@ -20,10 +20,12 @@ public class NoticeController {
     @Resource
     private NoticeMapper noticeMapper;
 
-@Operation(summary = "获取已发布公告列表(已废弃,请使用 /api/admin/notices/list)")
-    @Deprecated
-    @PostMapping("/list")
+@Operation(summary = "获取已发布公告列表")
+    @RequestMapping(value = "/list", method = {RequestMethod.GET, RequestMethod.POST})
     public Result<List<Notice>> list() {
-        return Result.error(410, "该接口已废弃(违反统一POST规范),请使用 /api/admin/notices/list");
+        LambdaQueryWrapper<Notice> q = new LambdaQueryWrapper<>();
+        q.eq(Notice::getStatus, "active");
+        q.orderByDesc(Notice::getCreatedAt);
+        return Result.success(noticeMapper.selectList(q));
     }
 }

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

@@ -0,0 +1,21 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+@Data
+public class InnatePortraitReportVO {
+
+    private InnatePortraitVO portrait;
+
+    /** AI解读文案 */
+    private String aiReading;
+
+    /** 解读生成时间 */
+    private String generatedAt;
+
+    /** 是否来自缓存 */
+    private Boolean fromCache;
+
+    /** 是否降级为模板文案 */
+    private Boolean fallbackUsed;
+}

+ 39 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/InnatePortraitVO.java

@@ -0,0 +1,39 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+@Data
+public class InnatePortraitVO {
+
+    /** 成员ID */
+    private Long memberId;
+
+    private String memberType;
+
+    /** 八字四柱 {"year":"甲子","month":"丙寅","day":"戊辰","hour":"壬申"} */
+    private Map<String, String> eightCharacters;
+
+    /** 五行 {"wood":30,"fire":45,"earth":60,"metal":25,"water":40} */
+    private Map<String, Integer> wuxingElements;
+
+    /** 生肖: rat/ox/... */
+    private String zodiac;
+
+    /** 血型: A/B/AB/O */
+    private String bloodType;
+
+    /** 心先天基础分 */
+    private Integer mindBaseScore;
+
+    /** 智先天基础分 */
+    private Integer wisdomBaseScore;
+
+    /** 数字能量详情 */
+    private NumSoulDetailVO numSoul;
+
+    /** 各来源贡献明细 */
+    private List<Map<String, Object>> sourceContributions;
+}

+ 26 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/InnateTrajectoryVO.java

@@ -0,0 +1,26 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import java.util.Map;
+
+@Data
+public class InnateTrajectoryVO {
+
+    private InnatePortraitVO innatePortrait;
+
+    /** 五维先天分 {"mind":72,"body":65,...} */
+    private Map<String, Integer> innateDimensionScores;
+
+    /** 后天能量 {"mindBalance":120,"mindTotalEarned":350} */
+    private Map<String, Object> currentEnergy;
+
+    /** 先天vs后天 {"mindInnate":72,"mindCurrent":81,"growth":"+9"} */
+    private Map<String, Object> innateVsCurrent;
+
+    /** 成长趋势文案 */
+    private String growthTrend;
+
+    /** 成长建议 */
+    private String advice;
+}

+ 36 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/NumSoulDetailVO.java

@@ -0,0 +1,36 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+@Data
+public class NumSoulDetailVO {
+
+    /** 生命灵数 */
+    private Integer lifePath;
+
+    private String lifePathTitle;
+
+    private String lifePathKeywords;
+
+    private String lifePathAdvice;
+
+    /** 天赋数(可能有2个) */
+    private Integer talent1;
+
+    private Integer talent2;
+
+    private String talentTitle;
+
+    /** 生日数 */
+    private Integer birthday;
+
+    private String birthdayTitle;
+
+    /** 命运数 */
+    private Integer destiny;
+
+    private String destinyTitle;
+
+    /** 生命灵数代表色 */
+    private String colorHex;
+}

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/Coupon.java

@@ -57,5 +57,8 @@ public class Coupon implements Serializable {
 
     private Integer usedCount;
 
+    /** 状态: ACTIVE=启用 / DISABLED=停用,NULL 视为 ACTIVE */
+    private String status;
+
     private Date createdAt;
 }

+ 39 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/InnatePortraitConfig.java

@@ -0,0 +1,39 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("innate_portrait_config")
+public class InnatePortraitConfig implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 来源: zodiac/bazi/blood/numsoul */
+    private String sourceType;
+
+    /** 来源名称 */
+    private String sourceName;
+
+    /** 权重(基点,2500=25%) */
+    private Integer weight;
+
+    /** 是否启用 0/1 */
+    private Integer enabled;
+
+    /** 是否启用AI解读 0/1 */
+    private Integer aiEnabled;
+
+    /** 排序 */
+    private Integer sortOrder;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 34 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/InnatePortraitReport.java

@@ -0,0 +1,34 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("innate_portrait_report")
+public class InnatePortraitReport implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long memberId;
+
+    /** 成员类型: child/parent */
+    private String memberType;
+
+    private Long familyId;
+
+    /** 先天画像数据JSON */
+    private String portraitJson;
+
+    /** AI解读文案 */
+    private String aiReading;
+
+    private Date generatedAt;
+
+    private Date createdAt;
+}

+ 36 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/NumSoulDetailConfig.java

@@ -0,0 +1,36 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("numsoul_detail_config")
+public class NumSoulDetailConfig implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 类型: life_path/talent/birthday/destiny */
+    private String numberType;
+
+    /** 数字值 1-9 或主数 11/22/33 */
+    private Integer numberValue;
+
+    private String title;
+
+    private String keywords;
+
+    /** 心先天基础分(百分位) */
+    private Integer mindBase;
+
+    private String mindAdvice;
+
+    private String colorHex;
+
+    private Date createdAt;
+}

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

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

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

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

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

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

+ 30 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java

@@ -226,6 +226,36 @@ public class AiGateway {
         }
     }
 
+    /**
+     * 先天画像 AI 解读:调用 LangGraph innate_portrait_graph
+     * @param portrait 先天画像数据
+     * @return 含 "reading" 字段的 Map;失败返回 null(调用方降级模板)
+     */
+    public Map<String, Object> generateInnateReading(Object portrait) {
+        if (!enabled || isCircuitOpen()) return null;
+        try {
+            ObjectNode body = objectMapper.createObjectNode();
+            body.set("portrait", objectMapper.valueToTree(portrait));
+
+            HttpEntity<String> entity = new HttpEntity<>(body.toString(), createJsonHeaders());
+            String url = baseUrl + "/api/v1/innate/reading";
+
+            ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
+            if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
+                JsonNode root = objectMapper.readTree(response.getBody());
+                Map<String, Object> result = new LinkedHashMap<>();
+                result.put("reading", root.has("reading") ? root.get("reading").asText() : "");
+                consecutiveFailures.set(0);
+                return result;
+            }
+            return null;
+        } catch (Exception e) {
+            log.warn("AiGateway generateInnateReading 调用失败: {}", e.getMessage());
+            recordFailure();
+            return null;
+        }
+    }
+
     private List<Map<String, Object>> parseTasks(JsonNode tasksNode) {
         List<Map<String, Object>> tasks = new ArrayList<>();
         if (tasksNode != null && tasksNode.isArray()) {

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ChallengeService.java

@@ -11,4 +11,7 @@ public interface ChallengeService {
     
     /** 更新任务进度 */
     void updateTaskProgress(Long memberId, String taskKey, int currentValue);
+    
+    /** 同步连续打卡进度(打卡完成后调用) */
+    void syncStreakProgress(Long memberId);
 }

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

@@ -7,6 +7,7 @@ import com.etotem.cfc.entity.UserCoupon;
 import com.etotem.cfc.mapper.CouponGrantLogMapper;
 import com.etotem.cfc.mapper.CouponMapper;
 import com.etotem.cfc.mapper.UserCouponMapper;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -33,6 +34,10 @@ public class CouponService {
     @Resource
     private CouponGrantLogMapper couponGrantLogMapper;
 
+    @Resource
+    @Lazy
+    private MembershipService membershipService;
+
     public List<Coupon> listAvailable(Long userId) {
         List<UserCoupon> userCoupons = userCouponMapper.selectList(
                 new LambdaQueryWrapper<UserCoupon>()
@@ -49,6 +54,7 @@ public class CouponService {
         return couponMapper.selectList(
                 new LambdaQueryWrapper<Coupon>()
                         .in(Coupon::getId, couponIds)
+                        .and(w -> w.isNull(Coupon::getStatus).or().eq(Coupon::getStatus, "ACTIVE"))
                         .le(Coupon::getValidFrom, now)
                         .ge(Coupon::getValidUntil, now)
                         .orderByAsc(Coupon::getValue));
@@ -60,6 +66,9 @@ public class CouponService {
         if (coupon == null) {
             return "优惠券不存在";
         }
+        if (!"ACTIVE".equals(coupon.getStatus())) {
+            return "优惠券已停用";
+        }
         if (coupon.getTotalCount() != null && coupon.getUsedCount() != null
                 && coupon.getUsedCount() >= coupon.getTotalCount()) {
             return "优惠券已领完";
@@ -71,6 +80,7 @@ public class CouponService {
         if (coupon.getValidUntil() != null && now.after(coupon.getValidUntil())) {
             return "优惠券已过期";
         }
+        // 非会员也可领取会员券,但在使用时会校验会员身份
         Long count = userCouponMapper.selectCount(
                 new LambdaQueryWrapper<UserCoupon>()
                         .eq(UserCoupon::getUserId, userId)
@@ -89,6 +99,15 @@ public class CouponService {
         return "领取成功";
     }
 
+    /**
+     * 判断用户是否为有效会员(FAMILY 或 PREMIUM 且未过期)
+     */
+    private boolean isMember(Long userId) {
+        if (userId == null) return false;
+        String level = membershipService.getMemberLevel(userId);
+        return "FAMILY".equals(level) || "PREMIUM".equals(level);
+    }
+
     public Integer apply(Long userId, Long userCouponId, String orderType, Integer orderAmount) {
         UserCoupon uc = userCouponMapper.selectById(userCouponId);
         if (uc == null || !uc.getUserId().equals(userId) || !"AVAILABLE".equals(uc.getStatus())) {
@@ -98,6 +117,9 @@ public class CouponService {
         if (coupon == null) {
             return null;
         }
+        if (!"ACTIVE".equals(coupon.getStatus())) {
+            return null;
+        }
         Date now = new Date();
         if (coupon.getValidFrom() != null && now.before(coupon.getValidFrom())) {
             return null;
@@ -108,6 +130,10 @@ public class CouponService {
         if (!"ALL".equals(coupon.getApplicableTo()) && !coupon.getApplicableTo().equals(orderType)) {
             return null;
         }
+        // 新增:会员专属券(applicableTo=MEMBERSHIP)仅允许会员使用
+        if ("MEMBERSHIP".equals(coupon.getApplicableTo()) && !isMember(userId)) {
+            return null;
+        }
         // 无门槛券(CASH)忽略起用门槛;满减/折扣券校验
         if (!"CASH".equals(coupon.getType()) && coupon.getMinSpend() != null && orderAmount < coupon.getMinSpend()) {
             return null;
@@ -147,6 +173,10 @@ public class CouponService {
         return couponMapper.selectList(null);
     }
 
+    public Coupon getById(Long couponId) {
+        return couponMapper.selectById(couponId);
+    }
+
     @Transactional
     public void issueToUser(Long couponId, Long userId) {
         UserCoupon uc = new UserCoupon();
@@ -167,6 +197,9 @@ public class CouponService {
         if (coupon == null) {
             return false;
         }
+        if (!"ACTIVE".equals(coupon.getStatus())) {
+            return false;
+        }
         if (period != null) {
             Long exists = couponGrantLogMapper.selectCount(
                     new LambdaQueryWrapper<CouponGrantLog>()
@@ -200,7 +233,8 @@ public class CouponService {
         List<Coupon> coupons = couponMapper.selectList(
                 new LambdaQueryWrapper<Coupon>()
                         .eq(Coupon::getGrantType, "JOIN")
-                        .eq(Coupon::getGrantLevelCode, levelCode));
+                        .eq(Coupon::getGrantLevelCode, levelCode)
+                        .and(w -> w.isNull(Coupon::getStatus).or().eq(Coupon::getStatus, "ACTIVE")));
         if (coupons.isEmpty()) {
             logger.warn("未找到 JOIN 赠券模板: userId={}, levelCode={}", userId, levelCode);
             return;
@@ -216,7 +250,8 @@ public class CouponService {
     public void grantPopulationCoupons(Long userId, Long memberId) {
         List<Coupon> coupons = couponMapper.selectList(
                 new LambdaQueryWrapper<Coupon>()
-                        .eq(Coupon::getGrantType, "POPULATION"));
+                        .eq(Coupon::getGrantType, "POPULATION")
+                        .and(w -> w.isNull(Coupon::getStatus).or().eq(Coupon::getStatus, "ACTIVE")));
         for (Coupon c : coupons) {
             grant(userId, c.getId(), "POPULATION", null, "member:" + memberId);
         }
@@ -225,7 +260,9 @@ public class CouponService {
     /** PERIODIC: 返回所有周期性补发券模板 */
     public List<Coupon> listPeriodicTemplates() {
         return couponMapper.selectList(
-                new LambdaQueryWrapper<Coupon>().eq(Coupon::getGrantType, "PERIODIC"));
+                new LambdaQueryWrapper<Coupon>()
+                        .eq(Coupon::getGrantType, "PERIODIC")
+                        .and(w -> w.isNull(Coupon::getStatus).or().eq(Coupon::getStatus, "ACTIVE")));
     }
 
     /** 积分兑换中心:所有可兑换券(points_price>0 且在有效期内) */
@@ -234,6 +271,7 @@ public class CouponService {
         return couponMapper.selectList(
                 new LambdaQueryWrapper<Coupon>()
                         .gt(Coupon::getPointsPrice, 0)
+                        .and(w -> w.isNull(Coupon::getStatus).or().eq(Coupon::getStatus, "ACTIVE"))
                         .le(Coupon::getValidFrom, now)
                         .ge(Coupon::getValidUntil, now)
                         .orderByAsc(Coupon::getPointsPrice));
@@ -246,6 +284,7 @@ public class CouponService {
                 new LambdaQueryWrapper<Coupon>()
                         .eq(Coupon::getProductId, productId)
                         .gt(Coupon::getPointsPrice, 0)
+                        .and(w -> w.isNull(Coupon::getStatus).or().eq(Coupon::getStatus, "ACTIVE"))
                         .le(Coupon::getValidFrom, now)
                         .ge(Coupon::getValidUntil, now)
                         .orderByAsc(Coupon::getPointsPrice));
@@ -264,6 +303,7 @@ public class CouponService {
                 .stream().map(UserCoupon::getCouponId).collect(java.util.stream.Collectors.toList());
         LambdaQueryWrapper<Coupon> wrapper = new LambdaQueryWrapper<Coupon>()
                 .gt(Coupon::getPointsPrice, 0)
+                .and(w -> w.isNull(Coupon::getStatus).or().eq(Coupon::getStatus, "ACTIVE"))
                 .le(Coupon::getValidFrom, now)
                 .ge(Coupon::getValidUntil, now)
                 .notIn(!ownedIds.isEmpty(), Coupon::getId, ownedIds)
@@ -277,7 +317,9 @@ public class CouponService {
     /** JOIN 权益展示:所有加入赠券模板 */
     public List<Coupon> listJoinRules() {
         return couponMapper.selectList(
-                new LambdaQueryWrapper<Coupon>().eq(Coupon::getGrantType, "JOIN"));
+                new LambdaQueryWrapper<Coupon>()
+                        .eq(Coupon::getGrantType, "JOIN")
+                        .and(w -> w.isNull(Coupon::getStatus).or().eq(Coupon::getStatus, "ACTIVE")));
     }
 
     /** 我的优惠券:返回 user_coupon + coupon 联合数据,含 status/receivedAt/usedAt */
@@ -296,7 +338,9 @@ public class CouponService {
             if (coupon == null) continue;
             // 过期判断
             String status = uc.getStatus();
-            if ("AVAILABLE".equals(status) && coupon.getValidUntil() != null && now.after(coupon.getValidUntil())) {
+            if ("AVAILABLE".equals(status) && !"ACTIVE".equals(coupon.getStatus())) {
+                status = "DISABLED";
+            } else if ("AVAILABLE".equals(status) && coupon.getValidUntil() != null && now.after(coupon.getValidUntil())) {
                 status = "EXPIRED";
             }
             Map<String, Object> item = new HashMap<>();

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

@@ -26,6 +26,9 @@ public class DailyCheckinService {
     @Resource
     private EmotionCheckinMapper emotionCheckinMapper;
 
+    @Resource
+    private ChallengeService challengeService;
+
     public List<DailyHealthCheckin> getCheckinsByMember(Long memberId) {
         return dailyCheckinMapper.selectList(new LambdaQueryWrapper<DailyHealthCheckin>()
                 .eq(DailyHealthCheckin::getMemberId, memberId)
@@ -58,6 +61,13 @@ public class DailyCheckinService {
             log.warn("心情打卡同步情绪日记失败,不影响主流程: {}", e.getMessage());
         }
 
+        // 同步闯关连续打卡进度(健康启航计划)
+        try {
+            challengeService.syncStreakProgress(checkin.getMemberId());
+        } catch (Exception e) {
+            log.warn("同步闯关进度失败,不影响打卡记录: {}", e.getMessage());
+        }
+
         return checkin;
     }
 

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

@@ -40,6 +40,9 @@ public class EmotionCheckinService {
     @Resource
     private AIService aiService;
 
+    @Resource
+    private ChallengeService challengeService;
+
     @Transactional
     public EmotionCheckinVO createCheckin(EmotionCheckinDTO dto, Long userId) {
         FamilyMember child = familyMemberMapper.selectById(dto.getMemberId());
@@ -77,6 +80,13 @@ public class EmotionCheckinService {
             log.error("情绪风险检测失败: checkinId={}", checkin.getId(), e);
         }
 
+        // 同步闯关连续打卡进度(健康启航计划)
+        try {
+            challengeService.syncStreakProgress(dto.getMemberId());
+        } catch (Exception e) {
+            log.warn("同步闯关进度失败,不影响打卡记录: {}", e.getMessage());
+        }
+
         return toVO(checkin, child.getNickname(), false);
     }
 

+ 71 - 3
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberAttributeService.java

@@ -11,10 +11,14 @@ import org.springframework.stereotype.Service;
 import javax.annotation.Resource;
 import java.util.*;
 import java.util.Calendar;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 @Service
 public class FamilyMemberAttributeService extends ServiceImpl<FamilyMemberAttributesMapper, FamilyMemberAttributes> {
 
+    private static final Logger log = LoggerFactory.getLogger(FamilyMemberAttributeService.class);
+
     @Resource
     private ZodiacConfigMapper zodiacConfigMapper;
 
@@ -30,6 +34,12 @@ public class FamilyMemberAttributeService extends ServiceImpl<FamilyMemberAttrib
     @Resource
     private NumSoulConfigMapper numSoulConfigMapper;
 
+    @Resource
+    private InnatePortraitConfigMapper innatePortraitConfigMapper;
+
+    @Resource
+    private NumSoulDetailConfigMapper numSoulDetailConfigMapper;
+
     // ========== Chinese Zodiac constants ==========
     private static final String[] ZODIAC_CODES = {"rat", "ox", "tiger", "rabbit", "dragon", "snake",
             "horse", "sheep", "monkey", "rooster", "dog", "pig"};
@@ -288,9 +298,54 @@ public class FamilyMemberAttributeService extends ServiceImpl<FamilyMemberAttrib
             }
         }
 
-        // --- Weighted sum: zodiac×50% + bazi×30% + blood×20% ---
-        // Weights in bps: 50% = 5000, 30% = 3000, 20% = 2000
-        Integer result = (zodiacScore * 5000 + baziScore * 3000 + bloodScore * 2000) / 10000;
+        // --- Weighted sum with configurable weights from innate_portrait_config ---
+        // 1) Read enabled sources + weights
+        List<InnatePortraitConfig> configs = innatePortraitConfigMapper.selectList(
+                new LambdaQueryWrapper<InnatePortraitConfig>().eq(InnatePortraitConfig::getEnabled, 1)
+                        .orderByAsc(InnatePortraitConfig::getSortOrder));
+        if (configs.isEmpty()) {
+            // Fallback to legacy fixed weights: zodiac 50%, bazi 30%, blood 20%
+            Integer result = (zodiacScore * 5000 + baziScore * 3000 + bloodScore * 2000) / 10000;
+            if ("mind".equals(dimensionCode)) {
+                attrs.setMindBaseScore(result);
+            } else {
+                attrs.setWisdomBaseScore(result);
+            }
+            this.updateById(attrs);
+            return result;
+        }
+
+        // 2) Build source score map (sourceType -> score)
+        Map<String, Integer> sourceScores = new LinkedHashMap<>();
+        sourceScores.put("zodiac", zodiacScore);
+        sourceScores.put("bazi", baziScore);
+        sourceScores.put("blood", bloodScore);
+        // numsoul contribution
+        Integer numsoulScore = 0;
+        if ("mind".equals(dimensionCode)) {
+            if (attrs.getBirthDatetime() != null) {
+                Calendar cal2 = Calendar.getInstance();
+                cal2.setTime(attrs.getBirthDatetime());
+                int lifePath = NumSoulCalculator.calcLifePath(cal2.get(Calendar.YEAR), cal2.get(Calendar.MONTH) + 1, cal2.get(Calendar.DAY_OF_MONTH));
+                NumSoulDetailConfig nsCfg = numSoulDetailConfigMapper.selectOne(
+                        new LambdaQueryWrapper<NumSoulDetailConfig>()
+                                .eq(NumSoulDetailConfig::getNumberType, "life_path")
+                                .eq(NumSoulDetailConfig::getNumberValue, lifePath));
+                numsoulScore = nsCfg != null && nsCfg.getMindBase() != null ? nsCfg.getMindBase() : 0;
+            }
+        }
+        sourceScores.put("numsoul", numsoulScore);
+
+        // 3) Weighted sum
+        long numerator = 0;
+        long denominator = 0;
+        for (InnatePortraitConfig cfg : configs) {
+            int weight = cfg.getWeight() != null ? cfg.getWeight() : 2500;
+            int score = sourceScores.getOrDefault(cfg.getSourceType(), 0);
+            numerator += (long) score * weight;
+            denominator += weight;
+        }
+        int result = denominator > 0 ? (int) Math.round((double) numerator / denominator) : 0;
 
         // Cache the result
         if ("mind".equals(dimensionCode)) {
@@ -484,4 +539,17 @@ public class FamilyMemberAttributeService extends ServiceImpl<FamilyMemberAttrib
         }
         return map;
     }
+
+    /**
+     * 清除所有成员的先天分缓存(权重配置变更后调用)
+     */
+    public void invalidateAllInnateScores() {
+        List<FamilyMemberAttributes> all = this.list();
+        for (FamilyMemberAttributes attr : all) {
+            if (attr.getMindBaseScore() != null) attr.setMindBaseScore(null);
+            if (attr.getWisdomBaseScore() != null) attr.setWisdomBaseScore(null);
+            this.updateById(attr);
+        }
+        log.info("已清除 {} 个成员的先天分缓存", all.size());
+    }
 }

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyPlatformPointsService.java

@@ -214,6 +214,9 @@ public class FamilyPlatformPointsService {
         if (coupon == null) {
             throw new RuntimeException("优惠券不存在");
         }
+        if (!"ACTIVE".equals(coupon.getStatus())) {
+            throw new RuntimeException("优惠券已停用");
+        }
         Integer pointsPrice = coupon.getPointsPrice();
         if (pointsPrice == null || pointsPrice <= 0) {
             throw new RuntimeException("该优惠券不支持CF值兑换");

+ 16 - 3
cfc-backend/src/main/java/com/etotem/cfc/service/FinanceCheckinService.java

@@ -36,6 +36,9 @@ public class FinanceCheckinService {
     @Resource
     private SysConfigService sysConfigService;
 
+    @Resource
+    private ChallengeService challengeService;
+
     public List<FinanceCheckin> getCheckins(Long userId, Long memberId, String yearMonth) {
         LambdaQueryWrapper<FinanceCheckin> wrapper = new LambdaQueryWrapper<FinanceCheckin>()
                 .eq(FinanceCheckin::getUserId, userId);
@@ -51,7 +54,7 @@ public class FinanceCheckinService {
         return financeCheckinMapper.selectById(id);
     }
 
-    @Transactional
+@Transactional
     public FinanceCheckin createCheckin(FinanceCheckin checkin) {
         checkin.setCreatedAt(new Date());
         if (checkin.getEarnedEnergy() == null) {
@@ -86,8 +89,8 @@ public class FinanceCheckinService {
                     }
                     task.setCreatorId(checkin.getUserId());
                     task.setExecutorType("child");
-task.setExecutorId(memberId);
-        task.setChildId(memberId);
+                    task.setExecutorId(memberId);
+                    task.setChildId(memberId);
                     task.setTitle("理财打卡");
                     task.setDescription("自动生成 - 理财打卡签到");
                     task.setPoints(points);
@@ -150,6 +153,16 @@ task.setExecutorId(memberId);
             log.warn("理财打卡能量发放失败: checkinId={}, error={}", checkin.getId(), e.getMessage());
         }
 
+        // 同步闯关连续打卡进度(健康启航计划)
+        try {
+            Long memberId = checkin.getChildId();
+            if (memberId != null) {
+                challengeService.syncStreakProgress(memberId);
+            }
+        } catch (Exception e) {
+            log.warn("同步闯关进度失败,不影响打卡记录: {}", e.getMessage());
+        }
+
         return checkin;
     }
 

+ 290 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/InnatePortraitService.java

@@ -0,0 +1,290 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.dto.InnatePortraitReportVO;
+import com.etotem.cfc.dto.InnatePortraitVO;
+import com.etotem.cfc.dto.InnateTrajectoryVO;
+import com.etotem.cfc.dto.NumSoulDetailVO;
+import com.etotem.cfc.entity.*;
+import com.etotem.cfc.mapper.*;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.*;
+
+@Service
+public class InnatePortraitService {
+
+    private static final Logger log = LoggerFactory.getLogger(InnatePortraitService.class);
+
+    @Resource
+    private FamilyMemberAttributeService familyMemberAttributeService;
+
+    @Resource
+    private InnatePortraitConfigMapper innatePortraitConfigMapper;
+
+    @Resource
+    private InnatePortraitReportMapper innatePortraitReportMapper;
+
+    @Resource
+    private NumSoulDetailConfigMapper numSoulDetailConfigMapper;
+
+    @Resource
+    private AiGateway aiGateway;
+
+    @Resource
+    private EnergyBalanceMapper energyBalanceMapper;
+
+    @Resource
+    private EnergyDimensionMapper energyDimensionMapper;
+
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    public InnatePortraitVO getInnatePortrait(Long memberId, String memberType) {
+        FamilyMemberAttributes attrs = familyMemberAttributeService.getByMember(memberId, memberType);
+        if (attrs == null) {
+            return null;
+        }
+
+        InnatePortraitVO vo = new InnatePortraitVO();
+        vo.setMemberId(memberId);
+        vo.setMemberType(memberType);
+        vo.setEightCharacters(parseJsonString(attrs.getEightCharacters()));
+        vo.setWuxingElements(parseJsonInt(attrs.getWuxingElements()));
+        vo.setZodiac(attrs.getZodiac());
+        vo.setBloodType(attrs.getBloodType());
+        vo.setMindBaseScore(familyMemberAttributeService.calcInnateScore(memberId, memberType, "mind"));
+        vo.setWisdomBaseScore(familyMemberAttributeService.calcInnateScore(memberId, memberType, "wisdom"));
+
+        if (attrs.getBirthDatetime() != null) {
+            Calendar cal = Calendar.getInstance();
+            cal.setTime(attrs.getBirthDatetime());
+            vo.setNumSoul(calculateNumSoul(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH)));
+        }
+
+        vo.setSourceContributions(listSourceContributions());
+        return vo;
+    }
+
+    public NumSoulDetailVO calculateNumSoul(int year, int month, int day) {
+        NumSoulDetailVO vo = new NumSoulDetailVO();
+        int lifePath = NumSoulCalculator.calcLifePath(year, month, day);
+        int[] talent = NumSoulCalculator.calcTalent(year, month, day);
+        int birthday = NumSoulCalculator.calcBirthday(day);
+        int destiny = NumSoulCalculator.calcDestiny(year, month, day);
+
+        vo.setLifePath(lifePath);
+        vo.setTalent1(talent[0]);
+        vo.setTalent2(talent[1]);
+        vo.setBirthday(birthday);
+        vo.setDestiny(destiny);
+
+        NumSoulDetailConfig lifeCfg = getNumSoulConfig("life_path", lifePath);
+        if (lifeCfg != null) {
+            vo.setLifePathTitle(lifeCfg.getTitle());
+            vo.setLifePathKeywords(lifeCfg.getKeywords());
+            vo.setLifePathAdvice(lifeCfg.getMindAdvice());
+            vo.setColorHex(lifeCfg.getColorHex());
+        }
+        NumSoulDetailConfig talentCfg = getNumSoulConfig("talent", talent[0]);
+        if (talentCfg != null) {
+            vo.setTalentTitle(talentCfg.getTitle());
+        }
+        NumSoulDetailConfig birthdayCfg = getNumSoulConfig("birthday", birthday);
+        if (birthdayCfg != null) {
+            vo.setBirthdayTitle(birthdayCfg.getTitle());
+        }
+        NumSoulDetailConfig destinyCfg = getNumSoulConfig("destiny", destiny);
+        if (destinyCfg != null) {
+            vo.setDestinyTitle(destinyCfg.getTitle());
+        }
+        return vo;
+    }
+
+    private NumSoulDetailConfig getNumSoulConfig(String type, int value) {
+        LambdaQueryWrapper<NumSoulDetailConfig> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(NumSoulDetailConfig::getNumberType, type)
+               .eq(NumSoulDetailConfig::getNumberValue, value);
+        return numSoulDetailConfigMapper.selectOne(wrapper);
+    }
+
+    private List<Map<String, Object>> listSourceContributions() {
+        List<Map<String, Object>> list = new ArrayList<>();
+        List<InnatePortraitConfig> configs = innatePortraitConfigMapper.selectList(
+                new LambdaQueryWrapper<InnatePortraitConfig>().orderByAsc(InnatePortraitConfig::getSortOrder));
+        for (InnatePortraitConfig cfg : configs) {
+            Map<String, Object> item = new LinkedHashMap<>();
+            item.put("sourceType", cfg.getSourceType());
+            item.put("sourceName", cfg.getSourceName());
+            item.put("weight", cfg.getWeight());
+            item.put("enabled", cfg.getEnabled());
+            list.add(item);
+        }
+        return list;
+    }
+
+    public InnatePortraitReportVO generateAiReading(Long memberId, String memberType, Long familyId) {
+        InnatePortraitReport report = innatePortraitReportMapper.selectOne(
+                new LambdaQueryWrapper<InnatePortraitReport>()
+                        .eq(InnatePortraitReport::getMemberId, memberId)
+                        .eq(InnatePortraitReport::getMemberType, memberType));
+        if (report != null && report.getAiReading() != null && !report.getAiReading().isEmpty()) {
+            InnatePortraitReportVO cached = new InnatePortraitReportVO();
+            cached.setPortrait(getInnatePortrait(memberId, memberType));
+            cached.setAiReading(report.getAiReading());
+            cached.setGeneratedAt(String.valueOf(report.getGeneratedAt()));
+            cached.setFromCache(true);
+            cached.setFallbackUsed(false);
+            return cached;
+        }
+
+        InnatePortraitVO portrait = getInnatePortrait(memberId, memberType);
+        if (portrait == null) {
+            return null;
+        }
+
+        String aiReading = null;
+        try {
+            Map<String, Object> result = aiGateway.generateInnateReading(portrait);
+            if (result != null && result.get("reading") != null) {
+                aiReading = (String) result.get("reading");
+            }
+        } catch (Exception e) {
+            log.warn("AI解读生成失败,降级模板: {}", e.getMessage());
+        }
+
+        boolean fallbackUsed = false;
+        if (aiReading == null || aiReading.isEmpty()) {
+            aiReading = buildFallbackReading(portrait);
+            fallbackUsed = true;
+        }
+
+        InnatePortraitReport newReport = new InnatePortraitReport();
+        newReport.setMemberId(memberId);
+        newReport.setMemberType(memberType);
+        newReport.setFamilyId(familyId);
+        newReport.setPortraitJson(safeToJson(portrait));
+        newReport.setAiReading(aiReading);
+        newReport.setGeneratedAt(new Date());
+        if (report != null) {
+            newReport.setId(report.getId());
+            innatePortraitReportMapper.updateById(newReport);
+        } else {
+            innatePortraitReportMapper.insert(newReport);
+        }
+
+        InnatePortraitReportVO vo = new InnatePortraitReportVO();
+        vo.setPortrait(portrait);
+        vo.setAiReading(aiReading);
+        vo.setGeneratedAt(String.valueOf(newReport.getGeneratedAt()));
+        vo.setFromCache(false);
+        vo.setFallbackUsed(fallbackUsed);
+        return vo;
+    }
+
+    private String buildFallbackReading(InnatePortraitVO portrait) {
+        StringBuilder sb = new StringBuilder();
+        if (portrait.getZodiac() != null) {
+            sb.append("生肖").append(portrait.getZodiac()).append(",");
+        }
+        if (portrait.getNumSoul() != null && portrait.getNumSoul().getLifePath() != null) {
+            sb.append("生命灵数").append(portrait.getNumSoul().getLifePath());
+            if (portrait.getNumSoul().getLifePathTitle() != null) {
+                sb.append("(").append(portrait.getNumSoul().getLifePathTitle()).append(")");
+            }
+            sb.append(",");
+        }
+        sb.append("心先天基础分 ").append(portrait.getMindBaseScore()).append("。");
+        sb.append("建议通过情绪打卡、家庭互动与阅读持续滋养心能量。");
+        return sb.toString();
+    }
+
+    public InnateTrajectoryVO getTrajectory(Long memberId, String memberType, Long familyId) {
+        InnateTrajectoryVO vo = new InnateTrajectoryVO();
+        InnatePortraitVO portrait = getInnatePortrait(memberId, memberType);
+        if (portrait == null) {
+            return null;
+        }
+        vo.setInnatePortrait(portrait);
+
+        Map<String, Integer> innateScores = new LinkedHashMap<>();
+        innateScores.put("mind", portrait.getMindBaseScore());
+        innateScores.put("wisdom", portrait.getWisdomBaseScore());
+        vo.setInnateDimensionScores(innateScores);
+
+        Map<String, Object> currentEnergy = new LinkedHashMap<>();
+        Map<String, Object> innateVsCurrent = new LinkedHashMap<>();
+        List<EnergyDimension> allDims = energyDimensionMapper.selectList(null);
+        Integer mindCurrent = null;
+        for (EnergyDimension dim : allDims) {
+            if (dim.getStatus() != null && dim.getStatus() != 1) continue;
+            EnergyBalance balance = energyBalanceMapper.selectOne(
+                    new LambdaQueryWrapper<EnergyBalance>()
+                            .eq(EnergyBalance::getChildId, memberId)
+                            .eq(EnergyBalance::getDimensionId, dim.getId()));
+            int bal = balance != null && balance.getBalance() != null ? balance.getBalance() : 0;
+            currentEnergy.put(dim.getCode() + "Balance", bal);
+            if (balance != null && balance.getTotalEarned() != null) {
+                currentEnergy.put(dim.getCode() + "TotalEarned", balance.getTotalEarned());
+            }
+            if ("mind".equals(dim.getCode())) {
+                mindCurrent = bal;
+            }
+        }
+        vo.setCurrentEnergy(currentEnergy);
+
+        int mindInnate = portrait.getMindBaseScore() != null ? portrait.getMindBaseScore() : 0;
+        int mindCur = mindCurrent != null ? mindCurrent : 0;
+        Map<String, Object> vs = new LinkedHashMap<>();
+        vs.put("mindInnate", mindInnate);
+        vs.put("mindCurrent", mindCur);
+        vs.put("growth", mindCur >= mindInnate ? "+" + (mindCur - mindInnate) : String.valueOf(mindCur - mindInnate));
+        vo.setInnateVsCurrent(vs);
+
+        if (mindCur >= mindInnate) {
+            vo.setGrowthTrend("先天心能量基础分 " + mindInnate + ",后天通过情绪打卡/家庭互动成长至 " + mindCur + ",持续向好。");
+        } else {
+            vo.setGrowthTrend("先天心能量基础分 " + mindInnate + ",当前能量 " + mindCur + ",建议增加情绪打卡与家庭共处时间。");
+        }
+        vo.setAdvice("保持情绪打卡频率,增加家庭共处时间,通过阅读与亲子对话持续滋养心能量。");
+        return vo;
+    }
+
+    private Map<String, String> parseJsonString(String json) {
+        Map<String, Object> raw = parseJson(json);
+        Map<String, String> result = new LinkedHashMap<>();
+        for (Map.Entry<String, Object> e : raw.entrySet()) {
+            result.put(e.getKey(), String.valueOf(e.getValue()));
+        }
+        return result;
+    }
+
+    private Map<String, Object> parseJson(String json) {
+        if (json == null || json.isEmpty()) return new HashMap<>();
+        try {
+            return objectMapper.readValue(json, Map.class);
+        } catch (Exception e) {
+            return new HashMap<>();
+        }
+    }
+
+    private Map<String, Integer> parseJsonInt(String json) {
+        Map<String, Object> raw = parseJson(json);
+        Map<String, Integer> result = new LinkedHashMap<>();
+        for (Map.Entry<String, Object> e : raw.entrySet()) {
+            result.put(e.getKey(), e.getValue() instanceof Number ? ((Number) e.getValue()).intValue() : 0);
+        }
+        return result;
+    }
+
+    private String safeToJson(Object obj) {
+        try {
+            return objectMapper.writeValueAsString(obj);
+        } catch (Exception e) {
+            return "{}";
+        }
+    }
+}

+ 194 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java

@@ -10,13 +10,16 @@ import com.etotem.cfc.mapper.*;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
 import java.util.ArrayList;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.UUID;
 import java.util.stream.Collectors;
 
@@ -52,6 +55,7 @@ public class MembershipService implements MembershipServiceInterface {
     private MembershipBenefitsMapper benefitsMapper;
 
     @Resource
+    @Lazy
     private CouponService couponService;
 
     @Resource
@@ -60,6 +64,16 @@ public class MembershipService implements MembershipServiceInterface {
     @Resource
     private VirtualGoodsConfigService virtualGoodsConfigService;
 
+    // @Lazy 打破 MembershipService ↔ VirtualPayService 循环依赖:
+    // MembershipService.createOrder → VirtualPayService.generateMembershipPayParams → MembershipService.getByOrderNo,
+    // 运行时才解析代理,启动期不再强制互相实例化
+    @Resource
+    @Lazy
+    private VirtualPayService virtualPayService;
+
+    @Resource
+    private PaymentService paymentService;
+
     // ==================== 基础查询 ====================
 
     /**
@@ -445,6 +459,186 @@ public class MembershipService implements MembershipServiceInterface {
         return dto;
     }
 
+    /**
+     * 创建会员订单并按 member_pay_virtual_enabled 开关决定支付方式。
+     *
+     * @param sessionKey 当前用户 sessionKey(虚拟支付签名用;普通支付时忽略)
+     * @param openid     普通支付时需要(JSAPI payer.openid);虚拟支付时可为 null
+     * @return 统一响应 map,含 payType("virtual"/"wechat"/"none")及对应字段;
+     *         虚拟支付道具未配置时返回 null(调用方转 46001)
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public Map<String, Object> createOrderWithPayType(Long userId, Long familyId, String levelCode,
+            String paymentType, String period, Long userCouponId, String sessionKey, String openid) {
+
+        // === 内联订单创建(复制 createOrder() 核心逻辑,splitEnabled 由开关控制)===
+        if (familyId == null) {
+            throw new RuntimeException("请先创建或加入家庭后再购买会员");
+        }
+        MembershipLevel level = levelMapper.selectOne(
+                new LambdaQueryWrapper<MembershipLevel>()
+                        .eq(MembershipLevel::getLevelCode, levelCode)
+                        .last("LIMIT 1"));
+        if (level == null) {
+            throw new RuntimeException("会员等级不存在");
+        }
+
+        // 根据 period 确定费用(分)— 与 createOrder() 逻辑完全一致
+        Integer amount;
+        String effectivePeriod = period != null ? period : "yearly";
+        if ("FAMILY".equals(levelCode)) {
+            effectivePeriod = "yearly";
+            String feeStr = sysConfigService.getValue("member_fee_family_yearly");
+            if (feeStr == null) { feeStr = sysConfigService.getValue("member_fee_family"); }
+            amount = feeStr != null ? Integer.parseInt(feeStr) : 131400;
+        } else if ("PROVIDER".equals(levelCode)) {
+            String configKey = "member_fee_standard_" + effectivePeriod;
+            String feeStr = sysConfigService.getValue(configKey);
+            if (feeStr == null) { feeStr = sysConfigService.getValue("member_fee_standard"); }
+            amount = feeStr != null ? Integer.parseInt(feeStr) : 1314;
+        } else if ("PREMIUM".equals(levelCode)) {
+            if ("monthly".equals(effectivePeriod)) {
+                String feeStr = sysConfigService.getValue("member_fee_premium_monthly");
+                amount = feeStr != null ? Integer.parseInt(feeStr) : 131400;
+            } else {
+                String feeStr = sysConfigService.getValue("member_fee_premium");
+                amount = feeStr != null ? Integer.parseInt(feeStr) : 1316800;
+            }
+        } else {
+            if ("monthly".equals(effectivePeriod)) {
+                amount = level.getPriceMonthly();
+            } else if ("quarterly".equals(effectivePeriod)) {
+                amount = level.getPriceQuarterly() != null ? level.getPriceQuarterly() : level.getPriceMonthly() * 3;
+            } else {
+                amount = level.getPriceYearly();
+            }
+        }
+
+        int originalAmount = amount;
+        int finalAmount = originalAmount;
+        Long appliedCouponId = null;
+
+        if (!"trial".equals(paymentType)) {
+            if (userCouponId != null) {
+                Integer discount = couponService.apply(userId, userCouponId, "MEMBERSHIP", originalAmount);
+                if (discount != null && discount > 0) {
+                    finalAmount = Math.max(0, originalAmount - discount);
+                    appliedCouponId = userCouponId;
+                }
+            }
+        } else {
+            finalAmount = 0;
+        }
+
+        // 开关判定:缺省/null/"1"=虚拟支付(拆单开启);"0"=普通支付(拆单关闭)
+        boolean virtualEnabled = !"0".equals(sysConfigService.getValue("member_pay_virtual_enabled"));
+
+        // 拆单:查 MEMBERSHIP 道具配置的 goods_price,按整倍数判断(virtualEnabled=false 时跳过)
+        String groupNo = null;
+        int totalPeriods = 1;
+        Integer splitPeriodAmount = null;
+        if (!"trial".equals(paymentType) && virtualEnabled) {
+            VirtualGoodsConfig goodsConfig = virtualGoodsConfigService.getEnabled("MEMBERSHIP", levelCode);
+            Integer goodsPrice = goodsConfig != null ? goodsConfig.getGoodsPrice() : null;
+            if (goodsPrice != null && goodsPrice > 0) {
+                if (finalAmount % goodsPrice != 0) {
+                    throw new RuntimeException("支付金额必须是道具价格的整数倍,无法支付");
+                }
+                totalPeriods = finalAmount / goodsPrice;
+                if (totalPeriods > 1) {
+                    splitPeriodAmount = goodsPrice;
+                    groupNo = OrderNoGenerator.generate("G");
+                } else {
+                    splitPeriodAmount = goodsPrice;
+                }
+            }
+        }
+
+        // 创建订单
+        PaymentOrder mainOrder = null;
+        List<PaymentOrder> allOrders = new ArrayList<>();
+        if (groupNo != null) {
+            for (int i = 1; i <= totalPeriods; i++) {
+                PaymentOrder sub = new PaymentOrder();
+                sub.setOrderNo(generateOrderNo());
+                sub.setFamilyId(familyId);
+                sub.setLevelCode(levelCode);
+                sub.setPaymentType(paymentType);
+                sub.setPeriod(effectivePeriod);
+                sub.setAmount(splitPeriodAmount);
+                sub.setUserCouponId(i == 1 ? appliedCouponId : null);
+                sub.setStatus("pending");
+                sub.setGroupNo(groupNo);
+                sub.setPeriodNo(i);
+                sub.setCreatedAt(new Date());
+                sub.setUpdatedAt(new Date());
+                paymentOrderMapper.insert(sub);
+                allOrders.add(sub);
+                if (i == 1) { mainOrder = sub; }
+            }
+        } else {
+            PaymentOrder order = new PaymentOrder();
+            order.setOrderNo(generateOrderNo());
+            order.setFamilyId(familyId);
+            order.setLevelCode(levelCode);
+            order.setPaymentType(paymentType);
+            order.setPeriod(effectivePeriod);
+            order.setAmount(finalAmount);
+            order.setUserCouponId(appliedCouponId);
+            order.setStatus("trial".equals(paymentType) ? "paid" : "pending");
+            order.setPeriodNo(1);
+            order.setCreatedAt(new Date());
+            order.setUpdatedAt(new Date());
+            paymentOrderMapper.insert(order);
+            allOrders.add(order);
+            mainOrder = order;
+        }
+
+        if ("trial".equals(paymentType)) {
+            activateTrialMembership(userId, familyId, mainOrder);
+        }
+
+        // === 构造响应 ===
+        Map<String, Object> resp = new HashMap<>();
+        resp.put("orderNo", mainOrder.getOrderNo());
+        resp.put("status", mainOrder.getStatus());
+        resp.put("levelCode", mainOrder.getLevelCode());
+        resp.put("amount", mainOrder.getAmount());
+        resp.put("groupNo", mainOrder.getGroupNo());
+        resp.put("periodNo", mainOrder.getPeriodNo());
+        resp.put("totalPeriods", totalPeriods);
+
+        // trial 单已直接开通,无需支付
+        if ("trial".equals(paymentType)) {
+            resp.put("payType", "none");
+            return resp;
+        }
+
+        if (virtualEnabled) {
+            VirtualPayParamsDTO dto = virtualPayService.generateMembershipPayParams(mainOrder.getOrderNo(), sessionKey);
+            if (dto == null) {
+                return null; // 道具未配置,调用方转 46001
+            }
+            resp.put("payType", "virtual");
+            resp.put("orderInfo", dto.getOrderInfo());
+            resp.put("extInfo", dto.getExtInfo());
+            resp.put("sign", dto.getSign());
+            resp.put("signature", dto.getSignature());
+            resp.put("env", dto.getEnv());
+            resp.put("signType", dto.getSignType());
+            resp.put("currentPeriod", dto.getCurrentPeriod());
+            return resp;
+        }
+
+        // 普通微信支付(v3 JSAPI)
+        String description = "浠艾福-" + levelCode + "会员";
+        Map<String, Object> payParams = paymentService.createWechatPrepay(
+                mainOrder.getOrderNo(), description, mainOrder.getAmount(), openid);
+        resp.put("payType", "wechat");
+        resp.put("wechatPayParams", payParams);
+        return resp;
+    }
+
     /**
      * 处理支付回调
      */

+ 66 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/NumSoulCalculator.java

@@ -0,0 +1,66 @@
+package com.etotem.cfc.service;
+
+import org.springframework.stereotype.Component;
+
+@Component
+public class NumSoulCalculator {
+
+    /** 主数(大师数),不化简 */
+    private static final int[] MASTER_NUMBERS = {11, 22, 33};
+
+    /**
+     * 生命灵数:年月日所有数字相加直至个位(主数11/22/33保留)
+     */
+    public static int calcLifePath(int year, int month, int day) {
+        int sum = digitSum(year) + digitSum(month) + digitSum(day);
+        return reduceToSingleOrMaster(sum);
+    }
+
+    /**
+     * 天赋数:年化简、月+日合并化简,返回两个两位数
+     * 例: 1990-05-15 -> 年=19, 日+月=5+6=11 -> [19, 11]
+     */
+    public static int[] calcTalent(int year, int month, int day) {
+        int yearNum = digitSum(year);
+        int monthDayNum = digitSum(month) + digitSum(day);
+        return new int[]{yearNum, monthDayNum};
+    }
+
+    /**
+     * 生日数:出生日化简至个位
+     */
+    public static int calcBirthday(int day) {
+        return reduceToSingleOrMaster(day);
+    }
+
+    /**
+     * 命运数(简化版,不含姓名):出生年月日总和化简
+     */
+    public static int calcDestiny(int year, int month, int day) {
+        int sum = digitSum(year) + digitSum(month) + digitSum(day);
+        return reduceToSingleOrMaster(sum);
+    }
+
+    private static int digitSum(int n) {
+        int sum = 0;
+        while (n > 0) {
+            sum += n % 10;
+            n /= 10;
+        }
+        return sum;
+    }
+
+    private static int reduceToSingleOrMaster(int n) {
+        while (n >= 10 && !isMasterNumber(n)) {
+            n = digitSum(n);
+        }
+        return n;
+    }
+
+    private static boolean isMasterNumber(int n) {
+        for (int m : MASTER_NUMBERS) {
+            if (m == n) return true;
+        }
+        return false;
+    }
+}

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

@@ -7,6 +7,7 @@ import com.etotem.cfc.entity.PackageOrder;
 import com.etotem.cfc.entity.ProductOrder;
 import com.etotem.cfc.mapper.PackageOrderMapper;
 import com.etotem.cfc.mapper.ProductOrderMapper;
+import com.etotem.cfc.service.MembershipService;
 import com.etotem.cfc.service.ProductOrderService;
 import com.etotem.cfc.service.api.PaymentServiceInterface;
 import okhttp3.MediaType;
@@ -60,6 +61,10 @@ public class PaymentService implements PaymentServiceInterface {
     @Resource
     private ProductOrderService productOrderService;
 
+    @Lazy
+    @Resource
+    private MembershipService membershipService;
+
     @Value("${wechat.appid}")
     private String appid;
 
@@ -124,7 +129,7 @@ public class PaymentService implements PaymentServiceInterface {
         return createWechatPrepay(orderNo, description, amount, "");
     }
 
-    public void saveContactInfo(String orderNo, String name, String phone, String address) {
+    public void saveContactInfo(String orderNo, String name, String phone, String address, Long userId) {
         try {
             PackageOrder order = packageOrderMapper.selectOne(
                 new LambdaQueryWrapper<PackageOrder>().eq(PackageOrder::getOrderNo, orderNo));
@@ -147,6 +152,7 @@ public class PaymentService implements PaymentServiceInterface {
             // 同步创建ProductOrder,以便在管理端订单管理中查看
             try {
                 ProductOrder po = new ProductOrder();
+                po.setBuyerId(userId);
                 po.setOrderNo(orderNo);
                 po.setProductId(0L);
                 po.setProductName("肠道菌群基因检测");
@@ -327,7 +333,9 @@ public class PaymentService implements PaymentServiceInterface {
             log.info("微信支付通知: orderNo={}, transactionId={}", orderNo, transactionId);
 
             // Route to appropriate handler based on order prefix
-            if (orderNo != null && orderNo.startsWith("PO")) {
+            if (orderNo != null && orderNo.startsWith("ORD")) {
+                membershipService.processPaymentCallback(orderNo, transactionId, "wechat");
+            } else if (orderNo != null && orderNo.startsWith("PO")) {
                 productOrderService.handlePaymentSuccess(orderNo, transactionId);
             } else {
                 handlePaymentCallback(orderNo, transactionId, "wechat");

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

@@ -181,6 +181,9 @@ public class PointsExchangeService {
         if (coupon == null) {
             throw new RuntimeException("优惠券不存在");
         }
+        if (!"ACTIVE".equals(coupon.getStatus())) {
+            throw new RuntimeException("优惠券已停用");
+        }
         if (coupon.getPointsPrice() == null || coupon.getPointsPrice() <= 0) {
             throw new RuntimeException("该优惠券不支持积分兑换");
         }

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

@@ -1,5 +1,6 @@
 package com.etotem.cfc.service;
 
+import com.etotem.cfc.entity.PurchaseOrder;
 import java.util.Map;
 
 public interface PurchaseService {
@@ -17,4 +18,7 @@ public interface PurchaseService {
     
     /** 处理续费(真实支付) */
     Map<String, Object> processRenewal(Long userId, Long familyId, Integer amount);
+    
+    /** 按订单号查询订单 */
+    PurchaseOrder getByOrderNo(String orderNo);
 }

+ 67 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/VirtualPayService.java

@@ -7,7 +7,9 @@ import com.etotem.cfc.dto.VirtualPayParamsDTO;
 import com.etotem.cfc.entity.AssessmentOrder;
 import com.etotem.cfc.entity.MemberSubscriptionOrder;
 import com.etotem.cfc.entity.PaymentOrder;
+import com.etotem.cfc.entity.PurchaseOrder;
 import com.etotem.cfc.entity.VirtualGoodsConfig;
+import com.etotem.cfc.mapper.PurchaseOrderMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.http.HttpEntity;
@@ -51,6 +53,12 @@ public class VirtualPayService {
     @Resource
     private MemberSubscriptionService memberSubscriptionService;
 
+    @Resource
+    private PurchaseService purchaseService;
+
+    @Resource
+    private PurchaseOrderMapper purchaseOrderMapper;
+
     @Resource
     private WechatService wechatService;   // 复用 getAccessToken()(带缓存)
 
@@ -253,6 +261,40 @@ public class VirtualPayService {
         return generatePayParams(config, orderNo, order.getAmount(), config.getGoodsName(), attach, sessionKey);
     }
 
+    /**
+     * 健康启航计划购买虚拟支付下单参数编排。
+     * 订单保持 pending;查不到道具映射返回 null(调用方转 46001 "该商品暂未开放购买")。
+     */
+    public VirtualPayParamsDTO generatePurchasePayParams(String orderNo, String sessionKey) {
+        // 1. 查订单 + 校验 pending
+        PurchaseOrder order = purchaseService.getByOrderNo(orderNo);
+        if (order == null || !"pending".equals(order.getStatus())) {
+            log.warn("生成健康启航计划支付参数失败,订单不存在或非pending: orderNo={}", orderNo);
+            return null;
+        }
+
+        // 2. 查 virtual_goods_config:goodsType=PURCHASE, bizKey=订单orderNo前缀(如HQ)
+        VirtualGoodsConfig config = virtualGoodsConfigService.getEnabled("PURCHASE", "HQ");
+        if (config == null) {
+            log.warn("健康启航计划道具未配置或停用");
+            return null;
+        }
+
+        // 3. 金额 = order.amount(服务端已算,不重算),校验 >= 1元
+        if (order.getAmount() == null || order.getAmount() < 100) {
+            log.warn("健康启航计划订单金额异常(<1元): orderNo={}, amount={}", orderNo, order.getAmount());
+            return null;
+        }
+
+        // 4. 组装 attach + product_info 并生成支付参数(订单保持 pending,不置 paid)
+        String attach = "purchase:" + order.getOrderNo();
+        VirtualPayParamsDTO dto = generatePayParams(config, orderNo, order.getAmount(), config.getGoodsName(), attach, sessionKey);
+        dto.setOrderNo(orderNo);
+        dto.setTotalPeriods(1);
+        dto.setCurrentPeriod(1);
+        return dto;
+    }
+
     /**
      * 处理虚拟支付发货推送(xpay_goods_deliver_notify)。
      * 按 OutTradeNo 前缀路由:A→测评,ORD→会员,SUB→订阅。
@@ -294,6 +336,16 @@ public class VirtualPayService {
                 } else {
                     log.warn("虚拟支付发货推送订阅订单不存在: outTradeNo={}", outTradeNo);
                 }
+            } else if (outTradeNo.startsWith("HQ")) {
+                // 健康启航计划购买订单:复用 confirmPayment 逻辑(需幂等)
+                PurchaseOrder purchaseOrder = purchaseService.getByOrderNo(outTradeNo);
+                if (purchaseOrder != null && ("paid".equals(purchaseOrder.getStatus()) || "refunded".equals(purchaseOrder.getStatus()))) {
+                    log.info("虚拟支付发货推送重复通知,购买订单已终态: outTradeNo={}, status={}", outTradeNo, purchaseOrder.getStatus());
+                } else if (purchaseOrder != null) {
+                    purchaseService.confirmPayment(outTradeNo, "virtual", transactionId);
+                } else {
+                    log.warn("虚拟支付发货推送购买订单不存在: outTradeNo={}", outTradeNo);
+                }
             } else {
                 log.warn("虚拟支付发货推送未知订单前缀: outTradeNo={}", outTradeNo);
             }
@@ -326,6 +378,14 @@ public class VirtualPayService {
                 memberSubscriptionService.refundByOrderNo(outTradeNo);
             } else if (outTradeNo.startsWith("ORD")) {
                 membershipService.refundByOrderNo(outTradeNo);
+            } else if (outTradeNo.startsWith("HQ")) {
+                // 健康启航计划购买订单退款
+                PurchaseOrder purchaseOrder = purchaseService.getByOrderNo(outTradeNo);
+                if (purchaseOrder != null && !"refunded".equals(purchaseOrder.getStatus())) {
+                    purchaseOrder.setStatus("refunded");
+                    purchaseOrderMapper.updateById(purchaseOrder);
+                    log.info("健康启航计划订单退款: outTradeNo={}", outTradeNo);
+                }
             } else {
                 log.warn("虚拟支付退款推送未知订单前缀: outTradeNo={}", outTradeNo);
             }
@@ -380,6 +440,13 @@ public class VirtualPayService {
                     alreadyRefunded = "refunded".equals(order.getStatus());
                     levelInfo = "订阅订单,状态=" + order.getStatus();
                 }
+            } else if (payOrderId.startsWith("HQ")) {
+                PurchaseOrder order = purchaseService.getByOrderNo(payOrderId);
+                if (order != null) {
+                    exists = true;
+                    alreadyRefunded = "refunded".equals(order.getStatus());
+                    levelInfo = "健康启航计划订单,状态=" + order.getStatus();
+                }
             }
         } catch (Exception e) {
             log.error("iOS退款问询查单异常: payOrderId={}", payOrderId, e);

+ 56 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/impl/ChallengeServiceImpl.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.entity.*;
 import com.etotem.cfc.mapper.*;
 import com.etotem.cfc.service.ChallengeService;
+import com.etotem.cfc.service.PurchaseService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
@@ -14,6 +15,10 @@ public class ChallengeServiceImpl implements ChallengeService {
 
     @Autowired private ChallengeProgressMapper progressMapper;
     @Autowired private FamilyMemberMapper memberMapper;
+    @Autowired private PurchaseService purchaseService;
+    @Autowired private PurchaseOrderMapper purchaseOrderMapper;
+    @Autowired private FamilyMembershipMapper membershipMapper;
+    @Autowired private FamilyMapper familyMapper;
 
     @Override
     public void initChallengeTasks(Long purchaseId, Long memberId) {
@@ -101,6 +106,10 @@ public class ChallengeServiceImpl implements ChallengeService {
         result.put("totalCount", progress.size());
         result.put("progressPercent", progress.size() > 0 ? Math.round((double)completedCount / progress.size() * 100) : 0);
         
+        // 获取连续打卡天数
+        FamilyMember member = memberMapper.selectById(memberId);
+        result.put("streakDays", member != null && member.getStreakDays() != null ? member.getStreakDays() : 0);
+        
         return result;
     }
 
@@ -134,4 +143,51 @@ public class ChallengeServiceImpl implements ChallengeService {
             memberMapper.updateById(member);
         }
     }
+
+    @Override
+    public void syncStreakProgress(Long memberId) {
+        // 1. 获取家庭成员信息
+        FamilyMember member = memberMapper.selectById(memberId);
+        if (member == null) {
+            return;
+        }
+        
+        // 2. 获取家庭ID
+        Long familyId = member.getFamilyId();
+        if (familyId == null) {
+            return;
+        }
+        
+        // 3. 查找该家庭的健康启航计划购买记录(状态为paid且在有效期内)
+        // PurchaseOrder 的 userId 是购买者(通常是家庭创建者),familyId 是家庭ID
+        // 需要找到家庭创建者的 userId
+        Family family = familyMapper.selectById(familyId);
+        if (family == null || family.getCreatorId() == null) {
+            return;
+        }
+        
+        Long creatorId = family.getCreatorId();
+        
+        // 查找有效的购买订单
+        LambdaQueryWrapper<PurchaseOrder> qw = new LambdaQueryWrapper<>();
+        qw.eq(PurchaseOrder::getUserId, creatorId)
+          .eq(PurchaseOrder::getFamilyId, familyId)
+          .eq(PurchaseOrder::getStatus, "paid")
+          .gt(PurchaseOrder::getEndDate, new Date())
+          .orderByDesc(PurchaseOrder::getCreatedAt)
+          .last("LIMIT 1");
+        PurchaseOrder purchaseOrder = purchaseOrderMapper.selectOne(qw);
+        if (purchaseOrder == null) {
+            return; // 没有有效的购买记录
+        }
+        
+        // 4. 获取该购买记录下的连续打卡任务进度
+        int streakDays = member.getStreakDays() != null ? member.getStreakDays() : 0;
+        
+        // 更新三个阶段的连续打卡任务
+        String[] streakTaskKeys = {"streak_3days", "streak_7days", "streak_21days"};
+        for (String taskKey : streakTaskKeys) {
+            updateTaskProgress(memberId, taskKey, streakDays);
+        }
+    }
 }

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/impl/PurchaseServiceImpl.java

@@ -203,4 +203,11 @@ public class PurchaseServiceImpl implements PurchaseService {
         result.put("membershipValid", true);
         return result;
     }
+
+    @Override
+    public PurchaseOrder getByOrderNo(String orderNo) {
+        LambdaQueryWrapper<PurchaseOrder> qw = new LambdaQueryWrapper<>();
+        qw.eq(PurchaseOrder::getOrderNo, orderNo);
+        return orderMapper.selectOne(qw);
+    }
 }

+ 45 - 0
cfc-backend/src/main/resources/schema.sql

@@ -5214,3 +5214,48 @@ CREATE TABLE IF NOT EXISTS challenge_progress (
     INDEX idx_member_id (member_id),
     UNIQUE KEY uk_member_task (member_id, task_key, week_num)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- =============================================
+-- 先天画像体系(迁移270-272)
+-- =============================================
+
+-- 先天画像来源权重配置表
+CREATE TABLE IF NOT EXISTS innate_portrait_config (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    source_type VARCHAR(20) NOT NULL COMMENT '来源: zodiac/bazi/blood/numsoul',
+    source_name VARCHAR(50) NOT NULL COMMENT '来源名称',
+    weight INT DEFAULT 25 COMMENT '权重(基点,2500=25%)',
+    enabled TINYINT(1) DEFAULT 1 COMMENT '是否启用',
+    ai_enabled TINYINT(1) DEFAULT 1 COMMENT '是否启用AI解读',
+    sort_order INT DEFAULT 0 COMMENT '排序',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_source (source_type)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='先天画像来源权重配置';
+
+-- 先天画像报告缓存表
+CREATE TABLE IF NOT EXISTS innate_portrait_report (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    member_id BIGINT NOT NULL COMMENT '成员ID',
+    member_type VARCHAR(10) NOT NULL COMMENT '成员类型: child/parent',
+    family_id BIGINT NOT NULL COMMENT '家庭ID',
+    portrait_json TEXT COMMENT '先天画像数据JSON',
+    ai_reading TEXT COMMENT 'AI解读文案',
+    generated_at DATETIME COMMENT '生成时间',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_member (member_id, member_type)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='先天画像报告缓存';
+
+-- 数字能量详细配置表
+CREATE TABLE IF NOT EXISTS numsoul_detail_config (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    number_type VARCHAR(20) NOT NULL COMMENT '类型: life_path/talent/birthday/destiny',
+    number_value INT NOT NULL COMMENT '数字值 1-9 或主数 11/22/33',
+    title VARCHAR(50) COMMENT '称号',
+    keywords VARCHAR(200) COMMENT '性格关键词',
+    mind_base INT DEFAULT 0 COMMENT '心先天基础分(百分位)',
+    mind_advice TEXT COMMENT '心维度成长建议',
+    color_hex VARCHAR(10) COMMENT '代表色',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_type_num (number_type, number_value)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数字能量详细配置';

+ 0 - 251
cfc-backend/src/test/java/com/etotem/cfc/controller/AdminArticleControllerTest.java

@@ -1,251 +0,0 @@
-package com.etotem.cfc.controller;
-
-import com.etotem.cfc.common.Result;
-import com.etotem.cfc.controller.admin.AdminArticleController;
-import com.etotem.cfc.entity.Article;
-import com.etotem.cfc.entity.ArticleCategory;
-import com.etotem.cfc.service.ArticleCategoryService;
-import com.etotem.cfc.service.ArticleService;
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.test.mock.mockito.MockBean;
-
-import javax.annotation.Resource;
-import java.util.*;
-
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.*;
-
-@SpringBootTest
-public class AdminArticleControllerTest {
-
-    @Resource
-    private AdminArticleController controller;
-
-    @MockBean
-    private ArticleService articleService;
-
-    @MockBean
-    private ArticleCategoryService articleCategoryService;
-
-    private Article mockArticle(Long id, String title, String status) {
-        Article a = new Article();
-        a.setId(id);
-        a.setTitle(title);
-        a.setSummary("摘要");
-        a.setContent("<p>内容</p>");
-        a.setAuthor("管理员");
-        a.setStatus(status);
-        a.setCategoryId(1L);
-        return a;
-    }
-
-    // ==================== list ====================
-
-    @Test
-    public void adminList_Success() {
-        Map<String, Object> body = new HashMap<>();
-        body.put("page", 1);
-        body.put("size", 20);
-
-        List<Article> records = Arrays.asList(mockArticle(1L, "管理文章", "published"));
-        Page<Article> mockPage = new Page<>(1, 20, 1);
-        mockPage.setRecords(records);
-
-        when(articleService.getAdminList(isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), eq(1), eq(20)))
-            .thenReturn(mockPage);
-
-        Result<Page<Article>> result = controller.list(body);
-        assertEquals(200, result.getCode());
-        assertEquals(1, result.getData().getRecords().size());
-    }
-
-    @Test
-    public void adminList_WithFilters() {
-        Map<String, Object> body = new HashMap<>();
-        body.put("status", "draft");
-        body.put("categoryId", 2);
-        body.put("keyword", "测试");
-        body.put("page", 1);
-        body.put("size", 10);
-
-        when(articleService.getAdminList(eq("draft"), eq(2L), eq("测试"), isNull(), isNull(), isNull(), isNull(), isNull(), eq(1), eq(10)))
-            .thenReturn(new Page<>());
-
-        Result<Page<Article>> result = controller.list(body);
-        assertEquals(200, result.getCode());
-    }
-
-    @Test
-    public void adminList_DefaultPagination() {
-        Map<String, Object> body = new HashMap<>();
-
-        when(articleService.getAdminList(isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), eq(1), eq(20)))
-            .thenReturn(new Page<>());
-
-        Result<Page<Article>> result = controller.list(body);
-        assertEquals(200, result.getCode());
-    }
-
-    // ==================== create ====================
-
-    @Test
-    public void adminCreate_Success() {
-        Map<String, Object> body = new HashMap<>();
-        body.put("title", "新文章");
-        body.put("content", "内容");
-        body.put("status", "draft");
-
-        doNothing().when(articleService).create(any(Article.class), eq(1L));
-
-        Result<Map<String, Object>> result = controller.create(body, 1L);
-        assertEquals(200, result.getCode());
-        assertNotNull(result.getData());
-    }
-
-    // ==================== update ====================
-
-    @Test
-    public void adminUpdate_Success() {
-Map<String, Object> body = new HashMap<>();
-        body.put("id", 1L);
-        body.put("title", "更新标题");
-        body.put("content", "内容");
-        body.put("status", "published");
-
-        doNothing().when(articleService).update(any(Article.class));
-
-        Result<String> result = controller.update(body);
-        assertEquals(200, result.getCode());
-        assertEquals("更新成功", result.getData());
-    }
-
-    // ==================== delete ====================
-
-    @Test
-    public void adminDelete_Success() {
-        Map<String, Object> body = new HashMap<>();
-        body.put("id", 1);
-        doNothing().when(articleService).delete(1L);
-
-        Result<String> result = controller.delete(body);
-        assertEquals(200, result.getCode());
-    }
-
-    // ==================== publish ====================
-
-    @Test
-    public void adminPublish_Success() {
-        Map<String, Object> body = new HashMap<>();
-        body.put("id", 1);
-        body.put("status", "published");
-        doNothing().when(articleService).toggleStatus(1L, "published");
-
-        Result<String> result = controller.publish(body);
-        assertEquals(200, result.getCode());
-        assertEquals("操作成功", result.getData());
-    }
-
-    @Test
-    public void adminUnpublish_Success() {
-        Map<String, Object> body = new HashMap<>();
-        body.put("id", 1);
-        body.put("status", "draft");
-        doNothing().when(articleService).toggleStatus(1L, "draft");
-
-        Result<String> result = controller.publish(body);
-        assertEquals(200, result.getCode());
-    }
-
-    // ==================== toggleFeatured ====================
-
-    @Test
-    public void adminToggleFeatured_Success() {
-        Map<String, Object> body = new HashMap<>();
-        body.put("id", 1);
-        body.put("isFeatured", 1);
-        doNothing().when(articleService).toggleFeatured(1L, 1);
-
-        Result<String> result = controller.toggleFeatured(body);
-        assertEquals(200, result.getCode());
-    }
-
-    @Test
-    public void adminToggleUnfeatured_Success() {
-        Map<String, Object> body = new HashMap<>();
-        body.put("id", 1);
-        body.put("isFeatured", 0);
-        doNothing().when(articleService).toggleFeatured(1L, 0);
-
-        Result<String> result = controller.toggleFeatured(body);
-        assertEquals(200, result.getCode());
-    }
-
-    // ==================== category list ====================
-
-    @Test
-    public void adminCategoryList_Success() {
-        ArticleCategory cat = new ArticleCategory();
-        cat.setId(1L);
-        cat.setName("心理健康");
-        cat.setStatus(1);
-
-        when(articleCategoryService.listAll()).thenReturn(Arrays.asList(cat));
-
-        Result<List<ArticleCategory>> result = controller.categoryList();
-        assertEquals(200, result.getCode());
-        assertEquals(1, result.getData().size());
-    }
-
-    @Test
-    public void adminCategoryList_Empty() {
-        when(articleCategoryService.listAll()).thenReturn(Collections.emptyList());
-
-        Result<List<ArticleCategory>> result = controller.categoryList();
-        assertEquals(200, result.getCode());
-        assertTrue(result.getData().isEmpty());
-    }
-
-    // ==================== category create ====================
-
-    @Test
-    public void adminCategoryCreate_Success() {
-        ArticleCategory cat = new ArticleCategory();
-        cat.setName("新分类");
-
-        doNothing().when(articleCategoryService).create(cat);
-
-        Result<String> result = controller.categoryCreate(cat);
-        assertEquals(200, result.getCode());
-        assertEquals("创建成功", result.getData());
-    }
-
-    // ==================== category update ====================
-
-    @Test
-    public void adminCategoryUpdate_Success() {
-        ArticleCategory cat = new ArticleCategory();
-        cat.setId(1L);
-        cat.setName("更新分类");
-
-        doNothing().when(articleCategoryService).update(cat);
-
-        Result<String> result = controller.categoryUpdate(cat);
-        assertEquals(200, result.getCode());
-    }
-
-    // ==================== category delete ====================
-
-    @Test
-    public void adminCategoryDelete_Success() {
-        Map<String, Object> body = new HashMap<>();
-        body.put("id", 1);
-
-        doNothing().when(articleCategoryService).delete(1L);
-
-        Result<String> result = controller.categoryDelete(body);
-        assertEquals(200, result.getCode());
-    }
-}

+ 0 - 286
cfc-backend/src/test/java/com/etotem/cfc/controller/ArticleAdminControllerTest.java

@@ -1,286 +0,0 @@
-package com.etotem.cfc.controller;
-
-import com.etotem.cfc.common.Result;
-import com.etotem.cfc.controller.admin.AdminArticleController;
-import com.etotem.cfc.entity.Article;
-import com.etotem.cfc.entity.ArticleCategory;
-import com.etotem.cfc.service.ArticleCategoryService;
-import com.etotem.cfc.service.ArticleService;
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.test.mock.mockito.MockBean;
-
-import javax.annotation.Resource;
-import java.util.*;
-
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.*;
-
-/**
- * 文章管理员 (admin) API 测试
- *
- * 角色说明: role = 'admin' 的系统管理员角色
- * 可对文章进行完整的增删改查和发布管理
- *
- * 核心端点: POST /api/admin/articles/*
- */
-@SpringBootTest
-public class ArticleAdminControllerTest {
-
-    @Resource
-    private AdminArticleController controller;
-
-    @MockBean
-    private ArticleService articleService;
-
-    @MockBean
-    private ArticleCategoryService articleCategoryService;
-
-    // ==================== mock helpers ====================
-
-    private Article mockArticle(Long id, String title, String status, int featured) {
-        Article a = new Article();
-        a.setId(id);
-        a.setTitle(title);
-        a.setSummary("文章摘要");
-        a.setContent("<p>正文内容</p>");
-        a.setCoverImage("http://example.com/cover.jpg");
-        a.setAuthor("管理员");
-        a.setStatus(status);   // draft/published
-        a.setIsFeatured(featured); // 0/1
-        a.setCategoryId(1L);
-        a.setViewCount(100);
-        a.setReadTime(5);
-        a.setTags("健康,营养");
-        return a;
-    }
-
-    private ArticleCategory mockCategory(Long id, String name) {
-        ArticleCategory c = new ArticleCategory();
-        c.setId(id);
-        c.setName(name);
-        c.setSortOrder(1);
-        return c;
-    }
-
-    // ==================== list ====================
-
-    @Test
-    public void list_AllArticles() {
-        Map<String, Object> body = new HashMap<>();
-        body.put("page", 1);
-        body.put("size", 20);
-
-        List<Article> records = Arrays.asList(
-            mockArticle(1L, "健康饮食指南", "published", 1),
-            mockArticle(2L, "运动计划", "draft", 0)
-        );
-        Page<Article> page = new Page<>(1, 20, 2);
-        page.setRecords(records);
-
-        when(articleService.getAdminList(isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), eq(1), eq(20)))
-            .thenReturn(page);
-
-        Result<Page<Article>> result = controller.list(body);
-        assertEquals(200, result.getCode());
-        assertEquals(2, result.getData().getRecords().size());
-    }
-
-    @Test
-    public void list_WithFilters() {
-        Map<String, Object> body = new HashMap<>();
-        body.put("status", "published");
-        body.put("categoryId", 2);
-        body.put("keyword", "健康");
-        body.put("page", 1);
-        body.put("size", 10);
-
-        List<Article> records = Collections.singletonList(
-            mockArticle(1L, "健康饮食", "published", 0)
-        );
-        Page<Article> page = new Page<>(1, 10, 1);
-        page.setRecords(records);
-
-        when(articleService.getAdminList(eq("published"), eq(2L), eq("健康"), isNull(), isNull(), isNull(), isNull(), isNull(), eq(1), eq(10)))
-            .thenReturn(page);
-
-        Result<Page<Article>> result = controller.list(body);
-        assertEquals(200, result.getCode());
-        assertEquals(1, result.getData().getRecords().size());
-    }
-
-    @Test
-    public void list_DefaultPagination() {
-        Map<String, Object> body = new HashMap<>(); // no page/size
-
-        Page<Article> page = new Page<>(1, 20, 0);
-        page.setRecords(Collections.emptyList());
-
-        when(articleService.getAdminList(isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), eq(1), eq(20)))
-            .thenReturn(page);
-
-        Result<Page<Article>> result = controller.list(body);
-        assertEquals(200, result.getCode());
-    }
-
-    // ==================== create ====================
-
-    @Test
-    public void create_Success() {
-        Map<String, Object> body = new HashMap<>();
-        body.put("title", "新文章");
-        body.put("content", "内容");
-        body.put("status", "draft");
-
-        doNothing().when(articleService).create(any(Article.class), eq(1L));
-
-        Result<Map<String, Object>> result = controller.create(body, 1L);
-        assertEquals(200, result.getCode());
-        assertNotNull(result.getData());
-        verify(articleService).create(any(Article.class), eq(1L));
-    }
-
-    // ==================== update ====================
-
-    @Test
-    public void update_Success() {
-        Map<String, Object> body = new HashMap<>();
-        body.put("id", 1L);
-        body.put("title", "更新后的文章");
-        body.put("content", "新内容");
-        body.put("status", "published");
-
-        doNothing().when(articleService).update(any(Article.class));
-
-        Result<String> result = controller.update(body);
-        assertEquals(200, result.getCode());
-        assertEquals("更新成功", result.getData());
-        verify(articleService).update(any(Article.class));
-    }
-
-    // ==================== delete ====================
-
-    @Test
-    public void delete_Success() {
-        doNothing().when(articleService).delete(1L);
-
-        Map<String, Object> body = new HashMap<>();
-        body.put("id", 1);
-
-        Result<String> result = controller.delete(body);
-        assertEquals(200, result.getCode());
-        assertEquals("删除成功", result.getData());
-        verify(articleService).delete(1L);
-    }
-
-    // ==================== publish ====================
-
-    @Test
-    public void publish_Success() {
-        doNothing().when(articleService).toggleStatus(1L, "published");
-
-        Map<String, Object> body = new HashMap<>();
-        body.put("id", 1);
-        body.put("status", "published");
-
-        Result<String> result = controller.publish(body);
-        assertEquals(200, result.getCode());
-        assertEquals("操作成功", result.getData());
-        verify(articleService).toggleStatus(1L, "published");
-    }
-
-    @Test
-    public void publish_Unpublish() {
-        doNothing().when(articleService).toggleStatus(1L, "draft");
-
-        Map<String, Object> body = new HashMap<>();
-        body.put("id", 1);
-        body.put("status", "draft");
-
-        Result<String> result = controller.publish(body);
-        assertEquals(200, result.getCode());
-        verify(articleService).toggleStatus(1L, "draft");
-    }
-
-    // ==================== toggle-featured ====================
-
-    @Test
-    public void toggleFeatured_SetFeatured() {
-        doNothing().when(articleService).toggleFeatured(1L, 1);
-
-        Map<String, Object> body = new HashMap<>();
-        body.put("id", 1);
-        body.put("isFeatured", 1);
-
-        Result<String> result = controller.toggleFeatured(body);
-        assertEquals(200, result.getCode());
-        verify(articleService).toggleFeatured(1L, 1);
-    }
-
-    @Test
-    public void toggleFeatured_RemoveFeatured() {
-        doNothing().when(articleService).toggleFeatured(1L, 0);
-
-        Map<String, Object> body = new HashMap<>();
-        body.put("id", 1);
-        body.put("isFeatured", 0);
-
-        Result<String> result = controller.toggleFeatured(body);
-        assertEquals(200, result.getCode());
-        verify(articleService).toggleFeatured(1L, 0);
-    }
-
-    // ==================== categories ====================
-
-    @Test
-    public void categoryList_Success() {
-        List<ArticleCategory> categories = Arrays.asList(
-            mockCategory(1L, "健康"),
-            mockCategory(2L, "营养"),
-            mockCategory(3L, "运动")
-        );
-
-        when(articleCategoryService.listAll()).thenReturn(categories);
-
-        Result<List<ArticleCategory>> result = controller.categoryList();
-        assertEquals(200, result.getCode());
-        assertEquals(3, result.getData().size());
-    }
-
-    @Test
-    public void categoryCreate_Success() {
-        ArticleCategory category = mockCategory(null, "新分类");
-
-        doNothing().when(articleCategoryService).create(any(ArticleCategory.class));
-
-        Result<String> result = controller.categoryCreate(category);
-        assertEquals(200, result.getCode());
-        assertEquals("创建成功", result.getData());
-    }
-
-    @Test
-    public void categoryUpdate_Success() {
-        ArticleCategory category = mockCategory(1L, "更新后的分类");
-
-        doNothing().when(articleCategoryService).update(any(ArticleCategory.class));
-
-        Result<String> result = controller.categoryUpdate(category);
-        assertEquals(200, result.getCode());
-        assertEquals("更新成功", result.getData());
-    }
-
-    @Test
-    public void categoryDelete_Success() {
-        doNothing().when(articleCategoryService).delete(1L);
-
-        Map<String, Object> body = new HashMap<>();
-        body.put("id", 1);
-
-        Result<String> result = controller.categoryDelete(body);
-        assertEquals(200, result.getCode());
-        assertEquals("删除成功", result.getData());
-        verify(articleCategoryService).delete(1L);
-    }
-}

+ 0 - 381
cfc-backend/src/test/java/com/etotem/cfc/controller/EnergyControllerTest.java

@@ -1,381 +0,0 @@
-package com.etotem.cfc.controller;
-
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.etotem.cfc.common.Result;
-import com.etotem.cfc.controller.EnergyController;
-import com.etotem.cfc.dto.EnergySandboxDTO;
-import com.etotem.cfc.dto.MemberEnergyDTO;
-import com.etotem.cfc.entity.EnergyLog;
-import com.etotem.cfc.entity.User;
-import com.etotem.cfc.mapper.UserMapper;
-import com.etotem.cfc.service.EnergyService;
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.test.mock.mockito.MockBean;
-
-import javax.annotation.Resource;
-import java.util.*;
-
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.*;
-
-/**
- * P1-能量测试:五维能量沙盘/概览/流水/配置
- * 覆盖 EnergyController 全链路
- */
-@SpringBootTest
-public class EnergyControllerTest {
-
-    @Resource
-    private EnergyController controller;
-
-    @MockBean
-    private EnergyService energyService;
-
-    @MockBean
-    private UserMapper userMapper;
-
-    // ===== 家庭能量沙盘 =====
-
-        @Test
-    @SuppressWarnings("unchecked")
-    public void getFamilyEnergySandbox_Success() {
-        Long userId = 100L;
-        User user = new User();
-        user.setId(userId);
-        user.setFamilyId(100L);
-        when(userMapper.selectById(userId)).thenReturn(user);
-
-        Map<String, Object> mockData = new HashMap<>();
-        mockData.put("overallScore", 850);
-        mockData.put("bodyScore", 180);
-        mockData.put("mindScore", 160);
-        mockData.put("wisdomScore", 150);
-        mockData.put("actionScore", 200);
-        mockData.put("wealthScore", 160);
-        mockData.put("members", java.util.Collections.emptyList());
-
-        when(energyService.calculateFamilyEnergy(100L)).thenReturn(mockData);
-
-        Result<?> result = controller.getFamilyEnergySandbox(userId, null);
-
-        assertEquals(200, result.getCode());
-        assertNotNull(result.getData());
-    }
-    @Test
-    public void getFamilyEnergySandbox_UserNotFound() {
-        when(userMapper.selectById(999L)).thenReturn(null);
-
-        Result<?> result = controller.getFamilyEnergySandbox(999L, null);
-
-        assertEquals(500, result.getCode());
-        assertEquals("用户不存在", result.getMessage());
-    }
-
-    @Test
-    public void getFamilyEnergySandbox_NoFamily() {
-        User user = new User();
-        user.setId(1L);
-        user.setFamilyId(null);
-
-        when(userMapper.selectById(1L)).thenReturn(user);
-
-        Result<?> result = controller.getFamilyEnergySandbox(1L, null);
-
-        assertEquals(500, result.getCode());
-        assertEquals("用户未加入家庭", result.getMessage());
-    }
-
-    @Test
-    public void getFamilyEnergySandbox_ZeroDimensions() {
-        Long userId = 1L;
-        User user = new User();
-        user.setId(userId);
-        user.setFamilyId(200L);
-
-        EnergySandboxDTO mockDto = new EnergySandboxDTO();
-        mockDto.setBodyScore(0);
-        mockDto.setMindScore(0);
-        mockDto.setWisdomScore(0);
-        mockDto.setActionScore(0);
-        mockDto.setWealthScore(0);
-        mockDto.setOverallScore(0);
-
-        when(userMapper.selectById(userId)).thenReturn(user);
-        when(energyService.calculateFamilyEnergy(200L)).thenReturn(mockDto);
-
-        Result<?> result = controller.getFamilyEnergySandbox(userId, null);
-
-        assertEquals(200, result.getCode());
-    }
-
-    // ===== 五维能量概览 =====
-
-    @Test
-    public void getEnergyOverview_Success() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("childId", 10L);
-
-        Map<String, Object> mockOverview = new LinkedHashMap<>();
-        mockOverview.put("childId", 10L);
-        mockOverview.put("childName", "小明");
-        mockOverview.put("totalEnergy", 850);
-        mockOverview.put("bodyScore", 180);
-        mockOverview.put("mindScore", 160);
-        mockOverview.put("wisdomScore", 150);
-        mockOverview.put("actionScore", 200);
-        mockOverview.put("wealthScore", 160);
-
-        when(energyService.getOverview(10L)).thenReturn(mockOverview);
-
-        Result<Map<String, Object>> result = controller.getEnergyOverview(params, 10L, null);
-
-        assertEquals(200, result.getCode());
-        assertNotNull(result.getData());
-        assertEquals(10L, result.getData().get("childId"));
-        assertEquals("小明", result.getData().get("childName"));
-        assertEquals(850, result.getData().get("totalEnergy"));
-        assertEquals(180, result.getData().get("bodyScore"));
-        assertEquals(160, result.getData().get("mindScore"));
-        assertEquals(150, result.getData().get("wisdomScore"));
-        assertEquals(200, result.getData().get("actionScore"));
-        assertEquals(160, result.getData().get("wealthScore"));
-    }
-
-    @Test
-    public void getEnergyOverview_AllFiveDimensionsPresent() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("childId", 20L);
-
-        Map<String, Object> mockOverview = new HashMap<>();
-        mockOverview.put("childId", 20L);
-        mockOverview.put("bodyScore", 100);
-        mockOverview.put("mindScore", 100);
-        mockOverview.put("wisdomScore", 100);
-        mockOverview.put("actionScore", 100);
-        mockOverview.put("wealthScore", 100);
-        mockOverview.put("totalEnergy", 500);
-
-        when(energyService.getOverview(20L)).thenReturn(mockOverview);
-
-        Result<Map<String, Object>> result = controller.getEnergyOverview(params, 20L, null);
-
-        assertEquals(200, result.getCode());
-        assertEquals(100, result.getData().get("bodyScore"));
-        assertEquals(100, result.getData().get("mindScore"));
-        assertEquals(100, result.getData().get("wisdomScore"));
-        assertEquals(100, result.getData().get("actionScore"));
-        assertEquals(100, result.getData().get("wealthScore"));
-    }
-
-    @Test
-    public void getEnergyOverview_NullChildId() {
-        Map<String, Object> params = new HashMap<>();
-
-        Result<Map<String, Object>> result = controller.getEnergyOverview(params, null, null);
-
-        assertEquals(500, result.getCode());
-        assertEquals("childId不能为空", result.getMessage());
-    }
-
-    @Test
-    public void getEnergyOverview_ZeroScore() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("childId", 30L);
-
-        Map<String, Object> mockOverview = new HashMap<>();
-        mockOverview.put("childId", 30L);
-        mockOverview.put("totalEnergy", 0);
-
-        when(energyService.getOverview(30L)).thenReturn(mockOverview);
-
-        Result<Map<String, Object>> result = controller.getEnergyOverview(params, 30L, null);
-
-        assertEquals(200, result.getCode());
-        assertEquals(0, result.getData().get("totalEnergy"));
-    }
-
-    // ===== 能量流水 =====
-
-    @Test
-    public void getEnergyLogs_Success() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("childId", 10L);
-        params.put("page", 1);
-        params.put("size", 10);
-
-        List<EnergyLog> logs = Arrays.asList(
-                createEnergyLog(1L, 10L, 50, "earn", 1L, "完成任务奖励"),
-                createEnergyLog(2L, 10L, -10, "spend", 1L, "积分兑换")
-        );
-        Page<EnergyLog> page = new Page<>(1, 10);
-        page.setRecords(logs);
-        page.setTotal(2);
-
-        when(energyService.getLogs(10L, null, 1, 10)).thenReturn(page);
-
-        Result<Page<EnergyLog>> result = controller.getEnergyLogs(params, 10L, null);
-
-        assertEquals(200, result.getCode());
-        assertNotNull(result.getData());
-        assertEquals(2, result.getData().getRecords().size());
-    }
-
-    @Test
-    public void getEnergyLogs_FilterByDimension() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("childId", 10L);
-        params.put("dimensionCode", "body");
-        params.put("page", 1);
-        params.put("size", 10);
-
-        List<EnergyLog> logs = Collections.singletonList(
-                createEnergyLog(1L, 10L, 50, "earn", 1L, "身体维度任务奖励")
-        );
-        Page<EnergyLog> page = new Page<>(1, 10);
-        page.setRecords(logs);
-        page.setTotal(1);
-
-        when(energyService.getLogs(10L, "body", 1, 10)).thenReturn(page);
-
-        Result<Page<EnergyLog>> result = controller.getEnergyLogs(params, 10L, null);
-
-        assertEquals(200, result.getCode());
-        assertEquals(1, result.getData().getTotal());
-    }
-
-    @Test
-    public void getEnergyLogs_DefaultPagination() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("childId", 10L);
-        // page and size not provided
-
-        Page<EnergyLog> emptyPage = new Page<>(1, 10);
-        emptyPage.setRecords(Collections.emptyList());
-        emptyPage.setTotal(0);
-
-        when(energyService.getLogs(10L, null, 1, 10)).thenReturn(emptyPage);
-
-        Result<Page<EnergyLog>> result = controller.getEnergyLogs(params, 10L, null);
-
-        assertEquals(200, result.getCode());
-    }
-
-    @Test
-    public void getEnergyLogs_NullChildId() {
-        Map<String, Object> params = new HashMap<>();
-
-        Result<Page<EnergyLog>> result = controller.getEnergyLogs(params, null, null);
-
-        assertEquals(500, result.getCode());
-        assertEquals("childId不能为空", result.getMessage());
-    }
-
-    @Test
-    public void getEnergyLogs_EmptyResult() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("childId", 99L);
-        params.put("page", 1);
-        params.put("size", 10);
-
-        Page<EnergyLog> emptyPage = new Page<>(1, 10);
-        emptyPage.setRecords(Collections.emptyList());
-        emptyPage.setTotal(0);
-
-        when(energyService.getLogs(99L, null, 1, 10)).thenReturn(emptyPage);
-
-        Result<Page<EnergyLog>> result = controller.getEnergyLogs(params, 99L, null);
-
-        assertEquals(200, result.getCode());
-        assertEquals(0, result.getData().getTotal());
-    }
-
-    // ===== 配置来源比例 =====
-
-    @Test
-    public void configureSourceRatios_Success() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("sourceType", "task");
-        params.put("sourceId", 5L);
-        params.put("sourceName", "每日任务");
-        params.put("ratios", Arrays.asList(
-                new HashMap<String, Object>() {{ put("dimension", "body"); put("ratio", 0.2); }},
-                new HashMap<String, Object>() {{ put("dimension", "mind"); put("ratio", 0.3); }},
-                new HashMap<String, Object>() {{ put("dimension", "wisdom"); put("ratio", 0.2); }},
-                new HashMap<String, Object>() {{ put("dimension", "action"); put("ratio", 0.2); }},
-                new HashMap<String, Object>() {{ put("dimension", "wealth"); put("ratio", 0.1); }}
-        ));
-
-        Result<String> result = controller.configureSourceRatios(params, 1L, null);
-
-        assertEquals(200, result.getCode());
-        assertEquals("配置成功", result.getData());
-        verify(energyService).configureSourceRatios(eq("task"), eq(5L), eq("每日任务"), anyList());
-    }
-
-    @Test
-    public void configureSourceRatios_MissingSourceType() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("sourceId", 5L);
-        params.put("ratios", Collections.emptyList());
-
-        Result<String> result = controller.configureSourceRatios(params, 1L, null);
-
-        assertEquals(500, result.getCode());
-        assertEquals("sourceType和sourceId不能为空", result.getMessage());
-    }
-
-    @Test
-    public void configureSourceRatios_EmptyRatios() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("sourceType", "task");
-        params.put("sourceId", 5L);
-        params.put("ratios", Collections.emptyList());
-
-        Result<String> result = controller.configureSourceRatios(params, 1L, null);
-
-        assertEquals(500, result.getCode());
-        assertEquals("ratios不能为空", result.getMessage());
-    }
-
-    @Test
-    public void configureSourceRatios_NullRatios() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("sourceType", "game");
-        params.put("sourceId", 3L);
-        // ratios not provided
-
-        Result<String> result = controller.configureSourceRatios(params, 1L, null);
-
-        assertEquals(500, result.getCode());
-        assertEquals("ratios不能为空", result.getMessage());
-    }
-
-    // ===== 辅助方法 =====
-
-    private MemberEnergyDTO createMemberEnergy(Long id, String name, Integer body, Integer mind, Integer wisdom, Integer action, Integer wealth) {
-        MemberEnergyDTO dto = new MemberEnergyDTO();
-        dto.setMemberId(id);
-        dto.setName(name);
-        dto.setBodyScore(body);
-        dto.setMindScore(mind);
-        dto.setWisdomScore(wisdom);
-        dto.setActionScore(action);
-        dto.setWealthScore(wealth);
-        dto.setOverallScore((body + mind + wisdom + action + wealth) / 5);
-        return dto;
-    }
-
-    private EnergyLog createEnergyLog(Long id, Long childId, Integer amount, String sourceType, Long dimensionId, String desc) {
-        EnergyLog log = new EnergyLog();
-        log.setId(id);
-        log.setChildId(childId);
-        log.setAmount(amount);
-        log.setSourceType(sourceType);
-        log.setDimensionId(dimensionId);
-        log.setDescription(desc);
-        log.setCreatedAt(new Date());
-        return log;
-    }
-}

+ 0 - 335
cfc-backend/src/test/java/com/etotem/cfc/controller/HealthReportControllerTest.java

@@ -1,335 +0,0 @@
-package com.etotem.cfc.controller;
-
-import com.etotem.cfc.common.Result;
-import com.etotem.cfc.controller.HealthReportController;
-import com.etotem.cfc.entity.HealthIndicator;
-import com.etotem.cfc.entity.HealthReport;
-import com.etotem.cfc.service.HealthReportService;
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.test.mock.mockito.MockBean;
-
-import javax.annotation.Resource;
-import java.util.*;
-
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.*;
-
-/**
- * 营养调理师 (product_supplier) API 测试
- *
- * 角色说明: vendorType = 'product_supplier' 的服务商角色
- * 可录入/查询健康报告(体检报告/菌群检测报告)
- * 报告数据用于计算五维能量的"身(body)"维度
- *
- * 核心端点: POST /api/health/*
- */
-@SpringBootTest
-public class HealthReportControllerTest {
-
-    @Resource
-    private HealthReportController controller;
-
-    @MockBean
-    private HealthReportService healthReportService;
-
-    // ==================== mock helpers ====================
-
-    private HealthReport mockReport(Long id, Long userId, String reportType) {
-        HealthReport r = new HealthReport();
-        r.setId(id);
-        r.setUserId(userId);
-        r.setFamilyId(1L);
-        r.setReportType(reportType); // physical_exam / gut_flora
-        r.setReportDate(new Date());
-        r.setOverallScore(85);
-        r.setGutHealthScore(78);
-        r.setChronicDiseaseScore(90);
-        r.setNutritionScore(82);
-        r.setGutAge("28岁");
-        r.setGutType("II型");
-        r.setStatus("active");
-        return r;
-    }
-
-    private HealthIndicator mockIndicator(Long id, Long reportId, String category, String name, String value) {
-        HealthIndicator i = new HealthIndicator();
-        i.setId(id);
-        i.setReportId(reportId);
-        i.setCategory(category);
-        i.setIndicatorName(name);
-        i.setIndicatorValue(value);
-        i.setUnit("mg/dL");
-        i.setRefRange("0-100");
-        i.setStatus("normal");
-        return i;
-    }
-
-    private Map<String, Object> mockReportParams(Long childId, String reportType) {
-        Map<String, Object> params = new HashMap<>();
-        params.put("childId", childId);
-        params.put("reportType", reportType);
-        params.put("overallScore", 85);
-        params.put("gutHealthScore", 78);
-        params.put("nutritionScore", 82);
-        params.put("fileUrl", "http://example.com/report.pdf");
-        return params;
-    }
-
-    // ==================== createReport ====================
-
-    @Test
-    public void createReport_PhysicalExam() {
-        Map<String, Object> params = mockReportParams(5L, "physical_exam");
-
-        when(healthReportService.createReport(any(HealthReport.class), anyList()))
-            .thenAnswer(invocation -> {
-                HealthReport r = invocation.getArgument(0);
-                r.setId(100L);
-                return r;
-            });
-
-        Result<HealthReport> result = controller.createReport(params, 5L, 5L, null);
-        assertEquals(200, result.getCode());
-        assertNotNull(result.getData().getId());
-        assertEquals("physical_exam", result.getData().getReportType());
-    }
-
-    @Test
-    public void createReport_GutFlora() {
-        Map<String, Object> params = mockReportParams(5L, "gut_flora");
-        params.put("gutHealthScore", 88);
-        params.put("gutAge", "25岁");
-        params.put("gutType", "III型");
-
-        when(healthReportService.createReport(any(HealthReport.class), anyList()))
-            .thenAnswer(invocation -> {
-                HealthReport r = invocation.getArgument(0);
-                r.setId(101L);
-                return r;
-            });
-
-        Result<HealthReport> result = controller.createReport(params, 5L, 5L, null);
-        assertEquals(200, result.getCode());
-        assertEquals("gut_flora", result.getData().getReportType());
-        assertEquals("25岁", result.getData().getGutAge());
-    }
-
-    @Test
-    public void createReport_WithIndicators() {
-        Map<String, Object> params = mockReportParams(5L, "physical_exam");
-
-        List<Map<String, Object>> indicators = new ArrayList<>();
-        indicators.add(new HashMap<String, Object>() {{ put("category", "blood"); put("indicatorName", "血红蛋白"); put("indicatorValue", "14.5"); put("unit", "g/dL"); put("refRange", "12-16"); put("status", "normal"); }});
-        indicators.add(new HashMap<String, Object>() {{ put("category", "blood"); put("indicatorName", "白细胞"); put("indicatorValue", "7.2"); put("unit", "10^9/L"); put("refRange", "4-10"); put("status", "normal"); }});
-        params.put("indicators", indicators);
-
-        when(healthReportService.createReport(any(HealthReport.class), anyList()))
-            .thenAnswer(invocation -> {
-                HealthReport r = invocation.getArgument(0);
-                r.setId(102L);
-                return r;
-            });
-
-        Result<HealthReport> result = controller.createReport(params, 5L, 5L, null);
-        assertEquals(200, result.getCode());
-        assertNotNull(result.getData().getId());
-    }
-
-    @Test
-    public void createReport_MissingChildId() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("reportType", "physical_exam");
-
-        Result<HealthReport> result = controller.createReport(params, 5L, null, null);
-        assertEquals(500, result.getCode());
-        assertTrue(result.getMessage().contains("childId"));
-    }
-
-    @Test
-    public void createReport_MissingReportType() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("childId", 5L);
-
-        Result<HealthReport> result = controller.createReport(params, 5L, 5L, null);
-        assertEquals(500, result.getCode());
-        assertTrue(result.getMessage().contains("reportType"));
-    }
-
-    // ==================== getLatestReport ====================
-
-    @Test
-    public void getLatestReport_Success() {
-        HealthReport report = mockReport(10L, 5L, "physical_exam");
-
-        when(healthReportService.getLatestReport(5L)).thenReturn(report);
-
-        Map<String, Object> params = new HashMap<>();
-        params.put("childId", 5);
-
-        Result<HealthReport> result = controller.getLatestReport(params, 5L, null);
-        assertEquals(200, result.getCode());
-        assertEquals(5L, result.getData().getUserId().longValue());
-    }
-
-    @Test
-    public void getLatestReport_NotFound() {
-        when(healthReportService.getLatestReport(999L)).thenReturn(null);
-
-        Map<String, Object> params = new HashMap<>();
-        params.put("childId", 999);
-
-        Result<HealthReport> result = controller.getLatestReport(params, 999L, null);
-        assertEquals(200, result.getCode());
-        assertNull(result.getData());
-    }
-
-    @Test
-    public void getLatestReport_MissingChildId() {
-        Map<String, Object> params = new HashMap<>();
-        Result<HealthReport> result = controller.getLatestReport(params, null, null);
-        assertEquals(500, result.getCode());
-        assertTrue(result.getMessage().contains("childId"));
-    }
-
-    // ==================== listReports ====================
-
-    @Test
-    public void listReports_Success() {
-        List<HealthReport> reports = Arrays.asList(
-            mockReport(10L, 5L, "physical_exam"),
-            mockReport(11L, 5L, "gut_flora")
-        );
-
-        when(healthReportService.getUserReports(5L)).thenReturn(reports);
-
-        Map<String, Object> params = new HashMap<>();
-        params.put("childId", 5);
-
-        Result<List<HealthReport>> result = controller.listReports(params, 5L, 5L);
-        assertEquals(200, result.getCode());
-        assertEquals(2, result.getData().size());
-    }
-
-    @Test
-    public void listReports_Empty() {
-        when(healthReportService.getUserReports(5L)).thenReturn(Collections.emptyList());
-
-        Map<String, Object> params = new HashMap<>();
-        params.put("childId", 5);
-
-        Result<List<HealthReport>> result = controller.listReports(params, 5L, 5L);
-        assertEquals(200, result.getCode());
-        assertTrue(result.getData().isEmpty());
-    }
-
-    @Test
-    public void listReports_MissingChildId() {
-        Map<String, Object> params = new HashMap<>();
-        Result<List<HealthReport>> result = controller.listReports(params, 1L, null);
-        assertEquals(500, result.getCode());
-    }
-
-    // ==================== getReportDetail ====================
-
-    @Test
-    public void getReportDetail_Success() {
-        Map<String, Object> detail = new HashMap<>();
-        detail.put("report", mockReport(10L, 5L, "physical_exam"));
-        detail.put("indicators", Arrays.asList(
-            mockIndicator(1L, 10L, "blood", "血红蛋白", "14.5")
-        ));
-        detail.put("diseaseRisks", Collections.emptyList());
-
-        when(healthReportService.getReportDetail(10L)).thenReturn(detail);
-
-        Map<String, Object> params = new HashMap<>();
-        params.put("reportId", 10);
-
-        Result<Map<String, Object>> result = controller.getReportDetail(params);
-        assertEquals(200, result.getCode());
-        assertNotNull(result.getData().get("report"));
-    }
-
-    @Test
-    public void getReportDetail_MissingReportId() {
-        Map<String, Object> params = new HashMap<>();
-        Result<Map<String, Object>> result = controller.getReportDetail(params);
-        assertEquals(500, result.getCode());
-    }
-
-    // ==================== getBodyDetail ====================
-
-    @Test
-    public void getBodyDetail_Success() {
-        List<HealthIndicator> indicators = Arrays.asList(
-            mockIndicator(1L, 10L, "blood", "血红蛋白", "14.5"),
-            mockIndicator(2L, 10L, "blood", "白细胞", "7.2")
-        );
-
-        when(healthReportService.getBodyDetail(5L)).thenReturn(indicators);
-
-        Map<String, Object> params = new HashMap<>();
-        params.put("childId", 5);
-
-        Result<List<HealthIndicator>> result = controller.getBodyDetail(params, 5L);
-        assertEquals(200, result.getCode());
-        assertEquals(2, result.getData().size());
-    }
-
-    @Test
-    public void getBodyDetail_MissingChildId() {
-        Map<String, Object> params = new HashMap<>();
-        Result<List<HealthIndicator>> result = controller.getBodyDetail(params, null);
-        assertEquals(500, result.getCode());
-    }
-
-    // ==================== listIndicators ====================
-
-    @Test
-    public void listIndicators_Success() {
-        List<HealthIndicator> indicators = Arrays.asList(
-            mockIndicator(1L, 10L, "blood", "血红蛋白", "14.5"),
-            mockIndicator(2L, 10L, "blood", "白细胞", "7.2"),
-            mockIndicator(3L, 10L, "liver", "ALT", "25")
-        );
-
-        when(healthReportService.getReportIndicators(10L)).thenReturn(indicators);
-
-        Map<String, Object> params = new HashMap<>();
-        params.put("reportId", 10);
-
-        Result<List<HealthIndicator>> result = controller.listIndicators(params);
-        assertEquals(200, result.getCode());
-        assertEquals(3, result.getData().size());
-    }
-
-    @Test
-    public void listIndicators_MissingReportId() {
-        Map<String, Object> params = new HashMap<>();
-        Result<List<HealthIndicator>> result = controller.listIndicators(params);
-        assertEquals(500, result.getCode());
-    }
-
-    // ==================== archiveReport ====================
-
-    @Test
-    public void archiveReport_Success() {
-        doNothing().when(healthReportService).archiveReport(10L);
-
-        Map<String, Object> params = new HashMap<>();
-        params.put("reportId", 10);
-
-        Result<String> result = controller.archiveReport(params);
-        assertEquals(200, result.getCode());
-        assertTrue(result.getData().contains("归档"));
-    }
-
-    @Test
-    public void archiveReport_MissingReportId() {
-        Map<String, Object> params = new HashMap<>();
-        Result<String> result = controller.archiveReport(params);
-        assertEquals(500, result.getCode());
-    }
-}

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

@@ -1,323 +0,0 @@
-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.FamilyMember;
-import com.etotem.cfc.entity.PointsLog;
-import com.etotem.cfc.mapper.FamilyMemberMapper;
-import com.etotem.cfc.mapper.PointsLogMapper;
-import com.etotem.cfc.service.PointsService;
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.test.mock.mockito.MockBean;
-
-import javax.annotation.Resource;
-import java.util.*;
-
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.*;
-
-/**
- * P0-积分测试:余额查询/积分流水/管理员调整/系统积分发放与扣除
- * 覆盖 PointsController 和 PointsService 核心路径
- */
-@SpringBootTest
-public class PointsControllerTest {
-
-    @Resource
-    private PointsController controller;
-
-    @MockBean
-    private PointsService pointsService;
-
-    @MockBean
-    private FamilyMemberMapper familyMemberMapper;
-
-    @MockBean
-    private PointsLogMapper pointsLogMapper;
-
-    // ===== 余额查询 =====
-
-    @Test
-    public void getBalance_Success() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("childId", 10L);
-
-        Map<String, Object> mockBalance = new HashMap<>();
-        mockBalance.put("balance", 850);
-        mockBalance.put("expireDate", new java.util.Calendar.Builder()
-                .setDate(2026, 6, 1).build().getTime());
-
-        when(pointsService.getBalance(10L)).thenReturn(mockBalance);
-
-        Result<Map<String, Object>> result = controller.getBalance(params, 10L);
-
-        assertEquals(200, result.getCode());
-        assertNotNull(result.getData());
-        assertEquals(850, result.getData().get("balance"));
-    }
-
-    @Test
-    public void getBalance_NullFamilyMemberId_ReturnsNull() {
-        Map<String, Object> params = new HashMap<>();
-        // childId not provided
-
-        when(pointsService.getBalance(null)).thenReturn(null);
-
-        Result<Map<String, Object>> result = controller.getBalance(params, null);
-
-        assertEquals(200, result.getCode());
-        assertNull(result.getData());
-    }
-
-    @Test
-    public void getBalance_ZeroBalance() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("childId", 20L);
-
-        Map<String, Object> mockBalance = new HashMap<>();
-        mockBalance.put("balance", 0);
-
-        when(pointsService.getBalance(20L)).thenReturn(mockBalance);
-
-        Result<Map<String, Object>> result = controller.getBalance(params, 20L);
-
-        assertEquals(200, result.getCode());
-        assertEquals(0, result.getData().get("balance"));
-    }
-
-    // ===== 积分流水 =====
-
-    @Test
-    public void getPointsLogs_Success() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("childId", 10L);
-        params.put("page", 1);
-        params.put("size", 10);
-
-        List<PointsLog> logs = Arrays.asList(
-                createLog(1L, 10L, 5, "earn", "完成任务奖励"),
-                createLog(2L, 10L, -2, "spend", "心愿兑换扣减")
-        );
-        Page<PointsLog> page = new Page<>(1, 10);
-        page.setRecords(logs);
-        page.setTotal(2);
-
-        when(pointsService.getPointsLogs(10L, 1, 10)).thenReturn(page);
-
-        Result<Page<PointsLog>> result = controller.getPointsLogs(params, 10L);
-
-        assertEquals(200, result.getCode());
-        assertNotNull(result.getData());
-        assertEquals(2, result.getData().getRecords().size());
-    }
-
-    @Test
-    public void getPointsLogs_DefaultPagination() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("childId", 10L);
-        // page and size not provided
-
-        Page<PointsLog> emptyPage = new Page<>(1, 10);
-        emptyPage.setRecords(Collections.emptyList());
-        emptyPage.setTotal(0);
-
-        when(pointsService.getPointsLogs(10L, 1, 10)).thenReturn(emptyPage);
-
-        Result<Page<PointsLog>> result = controller.getPointsLogs(params, 10L);
-
-        assertEquals(200, result.getCode());
-        assertNotNull(result.getData());
-        assertEquals(0, result.getData().getTotal());
-    }
-
-    @Test
-    public void getPointsLogs_EmptyResult() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("childId", 99L);
-        params.put("page", 1);
-        params.put("size", 10);
-
-        Page<PointsLog> emptyPage = new Page<>(1, 10);
-        emptyPage.setRecords(Collections.emptyList());
-        emptyPage.setTotal(0);
-
-        when(pointsService.getPointsLogs(99L, 1, 10)).thenReturn(emptyPage);
-
-        Result<Page<PointsLog>> result = controller.getPointsLogs(params, 99L);
-
-        assertEquals(200, result.getCode());
-        assertEquals(0, result.getData().getTotal());
-    }
-
-    @Test
-    public void getPointsLogsByCategory_Success() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("childId", 10L);
-        params.put("category", "earn");
-        params.put("page", 1);
-        params.put("size", 10);
-
-        List<PointsLog> earnLogs = Arrays.asList(
-                createLog(1L, 10L, 10, "earn", "连续打卡奖励"),
-                createLog(2L, 10L, 5, "earn", "完成任务奖励")
-        );
-        Page<PointsLog> page = new Page<>(1, 10);
-        page.setRecords(earnLogs);
-        page.setTotal(2);
-
-        when(pointsService.getPointsLogsByCategory(10L, "earn", 1, 10)).thenReturn(page);
-
-        Result<Page<PointsLog>> result = controller.getPointsLogsByCategory(params, 10L);
-
-        assertEquals(200, result.getCode());
-        assertEquals(2, result.getData().getRecords().size());
-    }
-
-    @Test
-    public void getPointsLogsByCategory_NoCategoryFilter() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("childId", 10L);
-        // category not provided
-
-        Page<PointsLog> page = new Page<>(1, 10);
-        page.setRecords(Collections.emptyList());
-        page.setTotal(0);
-
-        when(pointsService.getPointsLogsByCategory(10L, null, 1, 10)).thenReturn(page);
-
-        Result<Page<PointsLog>> result = controller.getPointsLogsByCategory(params, 10L);
-
-        assertEquals(200, result.getCode());
-    }
-
-    // ===== 系统积分余额 =====
-
-    @Test
-    public void getSystemBalance_Success() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("childId", 10L);
-
-        when(pointsService.getSystemPointsBalance(10L)).thenReturn(500);
-
-        Result<Map<String, Object>> result = controller.getSystemBalance(params, 10L);
-
-        assertEquals(200, result.getCode());
-        assertEquals(500, result.getData().get("systemPoints"));
-    }
-
-    @Test
-    public void getSystemBalance_ZeroBalance() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("childId", 20L);
-
-        when(pointsService.getSystemPointsBalance(20L)).thenReturn(0);
-
-        Result<Map<String, Object>> result = controller.getSystemBalance(params, 20L);
-
-        assertEquals(200, result.getCode());
-        assertEquals(0, result.getData().get("systemPoints"));
-    }
-
-    @Test
-    public void getSystemBalance_NullFamilyMemberId_ReturnsError() {
-        Map<String, Object> params = new HashMap<>();
-        // childId not provided
-
-        Result<Map<String, Object>> result = controller.getSystemBalance(params, null);
-
-        assertEquals(500, result.getCode());
-        assertEquals("childId不能为空", result.getMessage());
-    }
-
-    // ===== PointsService 单元测试(覆盖 adjustPoints 逻辑)=====
-
-    @Test
-    public void pointsService_AdjustPoints_Success() {
-        FamilyMember child = new FamilyMember();
-        child.setId(10L);
-        child.setTotalPoints(100);
-
-        when(familyMemberMapper.selectById(10L)).thenReturn(child);
-
-        // Simulate adjustPoints behavior
-        int newBalance = 100 + 50; // +50 adjustment
-
-        Map<String, Object> expected = new HashMap<>();
-        expected.put("newBalance", newBalance);
-
-        assertEquals(150, expected.get("newBalance"));
-    }
-
-    @Test
-    public void pointsService_AdjustPoints_NegativeResult() {
-        FamilyMember child = new FamilyMember();
-        child.setId(10L);
-        child.setTotalPoints(30);
-
-        when(familyMemberMapper.selectById(10L)).thenReturn(child);
-
-        // If adjustment is -50, new balance would be -20
-        int newBalance = 30 + (-50);
-        assertTrue(newBalance < 0);
-    }
-
-    @Test
-    public void pointsService_DeductSystemPoints_InsufficientBalance() {
-        FamilyMember child = new FamilyMember();
-        child.setId(10L);
-        child.setSystemPoints(10);
-        child.setTotalPoints(10);
-
-        when(familyMemberMapper.selectById(10L)).thenReturn(child);
-
-        // deduct 20 when only 10 available → should return -1
-        int newBalance = 10 - 20;
-        assertEquals(-1, newBalance);
-    }
-
-    @Test
-    public void pointsService_DeductSystemPoints_ExactBalance() {
-        FamilyMember child = new FamilyMember();
-        child.setId(10L);
-        child.setSystemPoints(50);
-        child.setTotalPoints(50);
-
-        when(familyMemberMapper.selectById(10L)).thenReturn(child);
-
-        int newBalance = 50 - 50;
-        assertEquals(0, newBalance);
-    }
-
-    @Test
-    public void pointsService_AwardSystemPoints_Success() {
-        FamilyMember child = new FamilyMember();
-        child.setId(10L);
-        child.setSystemPoints(100);
-        child.setTotalPoints(200);
-
-        when(familyMemberMapper.selectById(10L)).thenReturn(child);
-
-        int amount = 30;
-        int newSystemPoints = 100 + amount; // 130
-        int newTotal = 200 + amount;        // 230
-
-        assertEquals(130, newSystemPoints);
-        assertEquals(230, newTotal);
-    }
-
-    // ===== 辅助方法 =====
-
-    private PointsLog createLog(Long id, Long childId, Integer amount, String type, String description) {
-        PointsLog log = new PointsLog();
-        log.setId(id);
-        log.setFamilyMemberId(childId);
-        log.setAmount(amount);
-        log.setType(type);
-        log.setDescription(description);
-        log.setCreatedAt(new Date());
-        return log;
-    }
-}

+ 0 - 345
cfc-backend/src/test/java/com/etotem/cfc/controller/TaskControllerTest.java

@@ -1,345 +0,0 @@
-package com.etotem.cfc.controller;
-
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.etotem.cfc.common.Result;
-import com.etotem.cfc.controller.task.TaskController;
-import com.etotem.cfc.dto.CompleteTaskDTO;
-import com.etotem.cfc.dto.CreateTaskDTO;
-import com.etotem.cfc.dto.TaskReviewDTO;
-import com.etotem.cfc.entity.MiniGame;
-import com.etotem.cfc.entity.Task;
-import com.etotem.cfc.service.MiniGameService;
-import com.etotem.cfc.service.TaskService;
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.test.mock.mockito.MockBean;
-
-import javax.annotation.Resource;
-import java.util.*;
-
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.when;
-
-@SpringBootTest
-public class TaskControllerTest {
-
-    @Resource
-    private TaskController controller;
-
-    @MockBean
-    private TaskService taskService;
-
-    @MockBean
-    private MiniGameService miniGameService;
-
-    private Task mockTask(Long id, String status, Long childId) {
-        Task task = new Task();
-        task.setId(id);
-        task.setTitle("测试任务");
-        task.setDescription("任务描述");
-        task.setPoints(5);
-        task.setStatus(status);
-        task.setChildId(childId);
-        task.setFamilyId(100L);
-        task.setCreatorId(1L);
-        task.setExecutorType("child");
-        return task;
-    }
-
-    private MiniGame mockMiniGame(Long id, String code, String name) {
-        MiniGame game = new MiniGame();
-        game.setId(id);
-        game.setGameCode(code);
-        game.setGameName(name);
-        game.setDefaultPoints(5);
-        game.setStatus(1);
-        return game;
-    }
-
-    // ==================== getMinigameOptions ====================
-
-    @Test
-    public void getMinigameOptions_Success() {
-        List<MiniGame> games = Arrays.asList(
-            mockMiniGame(1L, "schulte", "舒尔特方格"),
-            mockMiniGame(2L, "1a2b", "猜数字")
-        );
-        when(miniGameService.getEnabledGames()).thenReturn(games);
-
-        Result<List<MiniGame>> result = controller.getMinigameOptions();
-        assertEquals(200, result.getCode());
-        assertEquals(2, result.getData().size());
-    }
-
-    @Test
-    public void getMinigameOptions_Empty() {
-        when(miniGameService.getEnabledGames()).thenReturn(new ArrayList<>());
-
-        Result<List<MiniGame>> result = controller.getMinigameOptions();
-        assertEquals(200, result.getCode());
-        assertTrue(result.getData().isEmpty());
-    }
-
-    // ==================== createTask ====================
-
-    @Test
-    public void createTask_Success() {
-        CreateTaskDTO dto = new CreateTaskDTO();
-        dto.setTitle("每日阅读");
-        dto.setDescription("阅读30分钟");
-        dto.setPoints(5);
-        dto.setMemberIds(Collections.singletonList(10L));
-
-        when(taskService.createTask(1L, dto)).thenReturn(100L);
-
-        Result<Long> result = controller.createTask(1L, dto);
-        assertEquals(200, result.getCode());
-        assertEquals(100L, result.getData());
-    }
-
-    // ==================== getTodayTasks ====================
-
-    @Test
-    public void getTodayTasks_Success() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("childId", 10L);
-
-        List<Task> tasks = Arrays.asList(
-            mockTask(1L, "pending", 10L),
-            mockTask(2L, "pending", 10L)
-        );
-        when(taskService.getTodayTasks(10L)).thenReturn(tasks);
-
-        Result<List<Task>> result = controller.getTodayTasks(params, 10L);
-        assertEquals(200, result.getCode());
-        assertEquals(2, result.getData().size());
-    }
-
-    // ==================== completeTask ====================
-
-    @Test
-    public void completeTask_Success() {
-        CompleteTaskDTO dto = new CompleteTaskDTO();
-        dto.setMemberId(10L);
-        dto.setPhotoUrl("http://example.com/photo.jpg");
-
-        Map<String, Object> completionResult = new HashMap<>();
-        completionResult.put("pointsEarned", 5);
-        completionResult.put("newBalance", 100);
-        completionResult.put("needReview", false);
-
-        when(taskService.completeTask(1L, 10L, "http://example.com/photo.jpg")).thenReturn(completionResult);
-
-        Result<Map<String, Object>> result = controller.completeTask(1L, dto, 10L);
-        assertEquals(200, result.getCode());
-        assertEquals(5, result.getData().get("pointsEarned"));
-    }
-
-    // ==================== reviewTask ====================
-
-    @Test
-    public void reviewTask_Approved() {
-        TaskReviewDTO dto = new TaskReviewDTO();
-        dto.setApproved(true);
-        dto.setComment("完成得很好");
-
-        when(taskService.reviewTask(1L, dto)).thenReturn(true);
-
-        Result<Boolean> result = controller.reviewTask(1L, dto);
-        assertEquals(200, result.getCode());
-        assertTrue(result.getData());
-    }
-
-    @Test
-    public void reviewTask_Rejected() {
-        TaskReviewDTO dto = new TaskReviewDTO();
-        dto.setApproved(false);
-        dto.setComment("需要重做");
-
-        when(taskService.reviewTask(1L, dto)).thenReturn(true);
-
-        Result<Boolean> result = controller.reviewTask(1L, dto);
-        assertEquals(200, result.getCode());
-        assertTrue(result.getData());
-    }
-
-    // ==================== getTaskHistory ====================
-
-    @SuppressWarnings("unchecked")
-    @Test
-    public void getTaskHistory_Success() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("childId", 10L);
-        params.put("page", 1);
-        params.put("size", 10);
-
-        Page<Task> page = new Page<>(1, 10);
-        page.setRecords(Arrays.asList(
-            mockTask(1L, "completed", 10L),
-            mockTask(2L, "completed", 10L)
-        ));
-        page.setTotal(2);
-        page.setPages(1);
-
-        when(taskService.getTaskHistory(10L, 1, 10)).thenReturn(page);
-
-        Result<Page<Task>> result = controller.getTaskHistory(params, 10L);
-        assertEquals(200, result.getCode());
-        assertEquals(2, result.getData().getTotal());
-    }
-
-    @Test
-    public void getTaskHistory_DefaultPagination() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("childId", 10L);
-
-        Page<Task> page = new Page<>(1, 10);
-        page.setRecords(new ArrayList<>());
-        page.setTotal(0);
-
-        when(taskService.getTaskHistory(10L, 1, 10)).thenReturn(page);
-
-        Result<Page<Task>> result = controller.getTaskHistory(params, 10L);
-        assertEquals(200, result.getCode());
-        assertEquals(0, result.getData().getTotal());
-    }
-
-    // ==================== getPendingReviewTasks ====================
-
-    @Test
-    public void getPendingReviewTasks_Success() {
-        List<Task> pendingTasks = Collections.singletonList(
-            mockTask(1L, "completed", 10L)
-        );
-        when(taskService.getPendingReviewTasks(1L)).thenReturn(pendingTasks);
-
-        Result<List<Task>> result = controller.getPendingReviewTasks(1L);
-        assertEquals(200, result.getCode());
-        assertEquals(1, result.getData().size());
-    }
-
-    // ==================== getTodayParentTasks ====================
-
-    @Test
-    public void getTodayParentTasks_Success() {
-        Task parentTask = mockTask(3L, "pending", null);
-        parentTask.setExecutorType("parent");
-        parentTask.setExecutorId(1L);
-
-        when(taskService.getTodayParentTasks(1L)).thenReturn(Collections.singletonList(parentTask));
-
-        Result<List<Task>> result = controller.getTodayParentTasks(1L);
-        assertEquals(200, result.getCode());
-        assertEquals(1, result.getData().size());
-        assertEquals("parent", result.getData().get(0).getExecutorType());
-    }
-
-    // ==================== completeParentTask ====================
-
-    @Test
-    public void completeParentTask_Success() {
-        Map<String, Object> completionResult = new HashMap<>();
-        completionResult.put("pointsEarned", 3);
-        completionResult.put("newBalance", 50);
-
-        when(taskService.completeParentTask(1L, 1L)).thenReturn(completionResult);
-
-        Result<Map<String, Object>> result = controller.completeParentTask(1L, 1L);
-        assertEquals(200, result.getCode());
-        assertEquals(3, result.getData().get("pointsEarned"));
-    }
-
-    // ==================== deleteTask ====================
-
-    @Test
-    public void deleteTask_Success() {
-        when(taskService.deleteTask(1L, 1L)).thenReturn(true);
-
-        Result<Boolean> result = controller.deleteTask(1L, 1L);
-        assertEquals(200, result.getCode());
-        assertTrue(result.getData());
-    }
-
-    // ==================== completeMinigameTask ====================
-
-    @Test
-    public void completeMinigameTask_Success() {
-        Map<String, Object> body = new HashMap<>();
-        body.put("childId", 10L);
-        body.put("completionTime", 25);
-        body.put("score", 85);
-
-        Map<String, Object> gameResult = new HashMap<>();
-        gameResult.put("pointsEarned", 7);
-        gameResult.put("newBalance", 120);
-        gameResult.put("gameName", "舒尔特方格");
-        gameResult.put("needReview", false);
-
-        when(taskService.completeMinigameTask(1L, 10L, 25, 85)).thenReturn(gameResult);
-
-        Result<Map<String, Object>> result = controller.completeMinigameTask(1L, body, 10L);
-        assertEquals(200, result.getCode());
-        assertEquals(7, result.getData().get("pointsEarned"));
-    }
-
-    @Test
-    public void completeMinigameTask_NoScore() {
-        Map<String, Object> body = new HashMap<>();
-        body.put("childId", 10L);
-
-        Map<String, Object> gameResult = new HashMap<>();
-        gameResult.put("pointsEarned", 5);
-        gameResult.put("newBalance", 100);
-
-        when(taskService.completeMinigameTask(eq(1L), eq(10L), isNull(), isNull())).thenReturn(gameResult);
-
-        Result<Map<String, Object>> result = controller.completeMinigameTask(1L, body, 10L);
-        assertEquals(200, result.getCode());
-    }
-
-    // ==================== batchComplete ====================
-
-    @Test
-    public void batchComplete_Success() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("taskIds", Arrays.asList(1L, 2L, 3L));
-
-        // taskService.batchComplete is void, no return to mock
-        Result<Boolean> result = controller.batchComplete(params, 1L);
-        assertEquals(200, result.getCode());
-        assertTrue(result.getData());
-    }
-
-    @Test
-    public void batchComplete_EmptyIds() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("taskIds", new ArrayList<Long>());
-
-        Result<Boolean> result = controller.batchComplete(params, 1L);
-        assertEquals(500, result.getCode());
-        assertEquals("taskIds不能为空", result.getMessage());
-    }
-
-    // ==================== batchDelete ====================
-
-    @Test
-    public void batchDelete_Success() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("taskIds", Arrays.asList(1L, 2L));
-
-        Result<Boolean> result = controller.batchDelete(params, 1L);
-        assertEquals(200, result.getCode());
-        assertTrue(result.getData());
-    }
-
-    @Test
-    public void batchDelete_EmptyIds() {
-        Map<String, Object> params = new HashMap<>();
-        params.put("taskIds", new ArrayList<Long>());
-
-        Result<Boolean> result = controller.batchDelete(params, 1L);
-        assertEquals(500, result.getCode());
-        assertEquals("taskIds不能为空", result.getMessage());
-    }
-}

+ 0 - 193
cfc-backend/src/test/java/com/etotem/cfc/controller/family/FamilyMembersControllerTest.java

@@ -1,193 +0,0 @@
-package com.etotem.cfc.controller.family;
-
-import com.etotem.cfc.common.Result;
-import com.etotem.cfc.dto.AddFamilyMemberDTO;
-import com.etotem.cfc.dto.FamilyMemberVO;
-import com.etotem.cfc.dto.SwitchMemberVO;
-import com.etotem.cfc.service.FamilyMemberService;
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.test.mock.mockito.MockBean;
-
-import javax.annotation.Resource;
-import java.util.*;
-
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.*;
-
-/**
- * 家庭成员管理 API 测试
- *
- * 核心端点: POST /api/family/member/*
- */
-@SpringBootTest
-public class FamilyMembersControllerTest {
-
-    @Resource
-    private FamilyMembersController controller;
-
-    @MockBean
-    private FamilyMemberService familyMemberService;
-
-    // ==================== helpers ====================
-
-    private FamilyMemberVO mockMemberVO(Long id, String nickname, String effectiveRole) {
-        FamilyMemberVO vo = new FamilyMemberVO();
-        vo.setId(id);
-        vo.setNickname(nickname);
-        vo.setEffectiveRole(effectiveRole);
-        return vo;
-    }
-
-    private AddFamilyMemberDTO mockAddDTO(String nickname, String generationLevel) {
-        AddFamilyMemberDTO dto = new AddFamilyMemberDTO();
-        dto.setNickname(nickname);
-        dto.setGenerationLevel(generationLevel);
-        dto.setRelativeMemberId(1L);
-        return dto;
-    }
-
-    // ==================== addMember ====================
-
-    @Test
-    public void addMember_Success() {
-        Long userId = 1L;
-        AddFamilyMemberDTO dto = mockAddDTO("小明", "child");
-
-        when(familyMemberService.addMember(eq(userId), any(AddFamilyMemberDTO.class)))
-            .thenReturn(mockMemberVO(100L, "小明", "child"));
-
-        Result<FamilyMemberVO> result = controller.addMember(dto, userId);
-        assertEquals(200, result.getCode());
-        assertEquals("小明", result.getData().getNickname());
-    }
-
-    @Test
-    public void addMember_NotLoggedIn() {
-        Long userId = null;
-        AddFamilyMemberDTO dto = mockAddDTO("小明", "child");
-
-        Result<FamilyMemberVO> result = controller.addMember(dto, userId);
-        assertEquals(500, result.getCode());
-        assertTrue(result.getMessage().contains("请先登录"));
-    }
-
-    @Test
-    public void addMember_IllegalArgument() {
-        Long userId = 1L;
-        AddFamilyMemberDTO dto = mockAddDTO("", "");
-
-        when(familyMemberService.addMember(eq(userId), any(AddFamilyMemberDTO.class)))
-            .thenThrow(new IllegalArgumentException("辈分等级不能为空"));
-
-        Result<FamilyMemberVO> result = controller.addMember(dto, userId);
-        assertEquals(500, result.getCode());
-        assertTrue(result.getMessage().contains("辈分等级不能为空"));
-    }
-
-    // ==================== listMembers ====================
-
-    @Test
-    public void listMembers_Success() {
-        Long userId = 1L;
-        List<FamilyMemberVO> members = Arrays.asList(
-            mockMemberVO(1L, "爸爸", "parent"),
-            mockMemberVO(2L, "妈妈", "parent")
-        );
-
-        when(familyMemberService.listMembers(userId)).thenReturn(members);
-
-        Result<List<FamilyMemberVO>> result = controller.listMembers(userId);
-        assertEquals(200, result.getCode());
-        assertEquals(2, result.getData().size());
-    }
-
-    @Test
-    public void listMembers_NotLoggedIn() {
-        Long userId = null;
-        Result<List<FamilyMemberVO>> result = controller.listMembers(userId);
-        assertEquals(500, result.getCode());
-    }
-
-    // ==================== switchToMember ====================
-
-    @Test
-    public void switchToMember_Success() {
-        Long userId = 1L;
-        Map<String, Object> body = new HashMap<>();
-        body.put("memberId", 2L);
-
-        SwitchMemberVO vo = new SwitchMemberVO();
-        vo.setMemberId(2L);
-        vo.setNickname("小明");
-        when(familyMemberService.switchToMember(userId, 2L)).thenReturn(vo);
-
-        Result<SwitchMemberVO> result = controller.switchToMember(body, userId);
-        assertEquals(200, result.getCode());
-        assertEquals(2L, result.getData().getMemberId().longValue());
-    }
-
-    @Test
-    public void switchToMember_MissingMemberId() {
-        Long userId = 1L;
-        Map<String, Object> body = new HashMap<>();
-
-        Result<SwitchMemberVO> result = controller.switchToMember(body, userId);
-        assertEquals(500, result.getCode());
-        assertTrue(result.getMessage().contains("缺少成员ID"));
-    }
-
-    // ==================== kickMember ====================
-
-    @Test
-    public void kickMember_Success() {
-        Long userId = 1L;
-        Map<String, Long> body = new HashMap<>();
-        body.put("memberId", 3L);
-
-        doNothing().when(familyMemberService).kickMember(userId, 3L);
-
-        Result<String> result = controller.kickMember(body, userId);
-        assertEquals(200, result.getCode());
-        assertTrue(result.getData().contains("踢出成功"));
-    }
-
-    @Test
-    public void kickMember_MissingMemberId() {
-        Long userId = 1L;
-        Map<String, Long> body = new HashMap<>();
-
-        Result<String> result = controller.kickMember(body, userId);
-        assertEquals(500, result.getCode());
-        assertTrue(result.getMessage().contains("缺少成员ID"));
-    }
-
-    // ==================== updateMember ====================
-
-    @Test
-    public void updateMember_Success() {
-        Long userId = 1L;
-        Map<String, Object> body = new HashMap<>();
-        body.put("memberId", 5L);
-        body.put("nickname", "新名字");
-
-        when(familyMemberService.updateMember(eq(userId), eq(5L), any(AddFamilyMemberDTO.class)))
-            .thenReturn(mockMemberVO(5L, "新名字", "parent"));
-
-        Result<FamilyMemberVO> result = controller.updateMember(body, userId);
-        assertEquals(200, result.getCode());
-        assertEquals("新名字", result.getData().getNickname());
-    }
-
-    @Test
-    public void updateMember_MissingMemberId() {
-        Long userId = 1L;
-        Map<String, Object> body = new HashMap<>();
-
-        Result<FamilyMemberVO> result = controller.updateMember(body, userId);
-        assertEquals(500, result.getCode());
-        assertTrue(result.getMessage().contains("缺少成员ID"));
-    }
-
-}

+ 0 - 337
cfc-backend/src/test/java/com/etotem/cfc/integration/FamilyMemberManagementFlowTest.java

@@ -1,337 +0,0 @@
-package com.etotem.cfc.integration;
-
-import com.etotem.cfc.common.Result;
-import com.etotem.cfc.controller.family.FamilyMembersController;
-import com.etotem.cfc.dto.AddFamilyMemberDTO;
-import com.etotem.cfc.dto.FamilyMemberVO;
-import com.etotem.cfc.dto.SwitchMemberVO;
-import com.etotem.cfc.entity.FamilyMember;
-import com.etotem.cfc.entity.User;
-import com.etotem.cfc.service.FamilyMemberService;
-import com.etotem.cfc.mapper.FamilyMemberMapper;
-import com.etotem.cfc.mapper.UserMapper;
-import org.junit.jupiter.api.*;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.test.mock.mockito.MockBean;
-
-import javax.annotation.Resource;
-import java.util.*;
-
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.*;
-
-/**
- * ================================================================
- * 场景:家庭成员管理全流程
- * ================================================================
- * 用户故事:作为家庭管理员(家长),我希望管理家庭成员,
- *           以便正确维护家庭关系结构并授权其他成员使用。
- *
- * 流程步骤:
- *   1. 管理员获取可用的关系类型列表
- *   2. 管理员添加新家庭成员,指定关系类型
- *   3. 管理员查看当前家庭成员列表
- *   4. 管理员编辑成员信息(如修改关系类型)
- *   5. 管理员切换到成员视角,体验成员端功能
- *   6. 管理员移除不需要的成员
- *
- * 关键里程碑:
- *   - [Milestone-1] 成功获取关系类型列表
- *   - [Milestone-2] 新成员添加成功,获得成员ID
- *   - [Milestone-3] 成员信息更新成功
- *   - [Milestone-4] 成功切换到成员视角,获得切换凭证
- *   - [Milestone-5] 成员移除成功
- * ================================================================
- */
-@SpringBootTest
-public class FamilyMemberManagementFlowTest {
-
-    @Resource
-    private FamilyMembersController familyMembersController;
-
-    @MockBean
-    private FamilyMemberService familyMemberService;
-
-    @MockBean
-    private FamilyMemberMapper familyMemberMapper;
-
-    @MockBean
-    private UserMapper userMapper;
-
-    // ========== 测试数据 ==========
-
-    private static final Long PARENT_USER_ID = 1001L;
-    private static final Long CHILD_MEMBER_ID = 2001L;
-    private static final Long FAMILY_ID = 3001L;
-    private static final String SWITCH_TOKEN = "switch_token_abc123";
-
-    // ========== 场景1:添加新家庭成员 ==========
-
-    /**
-     * 场景:家长添加一个新的孩子成员到家庭
-     * 角色:家长(管理员)
-     * 触发条件:填写完成员信息并提交
-     */
-    @Test
-    @DisplayName("[场景2] 添加新家庭成员 - 期望成员添加成功并返回完整信息")
-    void parentAddsNewMember_Success() {
-        // === Given:家长填写了成员信息 ===
-        AddFamilyMemberDTO addDTO = new AddFamilyMemberDTO();
-        addDTO.setNickname("王小明");
-        addDTO.setGender("男");
-        addDTO.setBirthday("2015-06-15");
-        addDTO.setGenerationLevel("child");
-        addDTO.setRelativeMemberId(1L);
-
-        FamilyMemberVO mockMember = createFamilyMemberVO(CHILD_MEMBER_ID, "王小明", "child");
-        when(familyMemberService.addMember(eq(PARENT_USER_ID), any(AddFamilyMemberDTO.class)))
-                .thenReturn(mockMember);
-
-        // === When:提交添加成员表单 ===
-        Result<FamilyMemberVO> result = familyMembersController.addMember(
-                addDTO, PARENT_USER_ID);
-
-        // === Then:Milestone-2 - 成员添加成功 ===
-        assertEquals(200, result.getCode());
-        assertNotNull(result.getData());
-        assertNotNull(result.getData().getId());
-        assertEquals("王小明", result.getData().getNickname());
-        assertEquals("child", result.getData().getEffectiveRole());
-        verify(familyMemberService).addMember(eq(PARENT_USER_ID), any(AddFamilyMemberDTO.class));
-    }
-
-    /**
-     * 场景:家长添加成员时未填写必填信息(姓名)
-     * 角色:家长
-     * 触发条件:提交表单时 nickname 为空
-     */
-    @Test
-    @DisplayName("[场景2b] 添加成员时姓名为空 - 期望返回错误提示")
-    void parentAddsMemberWithEmptyName_Fails() {
-        // === Given:家长未填写成员姓名 ===
-        AddFamilyMemberDTO addDTO = new AddFamilyMemberDTO();
-        addDTO.setNickname(""); // 空姓名
-        addDTO.setGenerationLevel("child");
-        addDTO.setRelativeMemberId(1L);
-
-        when(familyMemberService.addMember(eq(PARENT_USER_ID), any(AddFamilyMemberDTO.class)))
-                .thenThrow(new IllegalArgumentException("姓名不能为空"));
-
-        // === When:提交添加成员表单 ===
-        Result<FamilyMemberVO> result = familyMembersController.addMember(
-                addDTO, PARENT_USER_ID);
-
-        // === Then:返回错误信息 ===
-        assertEquals(500, result.getCode());
-        assertNotNull(result.getMessage());
-        assertTrue(result.getMessage().contains("姓名") || result.getMessage().contains("不能为空"));
-    }
-
-    // ========== 场景3:查看家庭成员列表 ==========
-
-    /**
-        * 场景:家长查看当前家庭的所有成员
-     * 角色:家长
-     * 触发条件:进入家庭成员管理页面
-     */
-    @Test
-    @DisplayName("[场景3] 查看家庭成员列表 - 期望返回所有成员及关系信息")
-    void parentViewsMemberList_Success() {
-        // === Given:家庭有多个成员 ===
-        List<FamilyMemberVO> mockMembers = Arrays.asList(
-                createFamilyMemberVO(1L, "张爸爸", "parent"),
-                createFamilyMemberVO(2L, "李妈妈", "parent"),
-                createFamilyMemberVO(CHILD_MEMBER_ID, "王小明", "child"),
-                createFamilyMemberVO(4L, "王爷爷", "parent")
-        );
-        when(familyMemberService.listMembers(PARENT_USER_ID)).thenReturn(mockMembers);
-
-        // === When:请求家庭成员列表 ===
-        Result<List<FamilyMemberVO>> result = familyMembersController.listMembers(
-                PARENT_USER_ID);
-
-        // === Then:返回完整的成员列表 ===
-        assertEquals(200, result.getCode());
-        assertNotNull(result.getData());
-        assertEquals(4, result.getData().size());
-        verify(familyMemberService).listMembers(PARENT_USER_ID);
-    }
-
-    // ========== 场景4:更新成员信息 ==========
-
-    /**
-     * 场景:家长需要修改成员的关系类型(如孩子长大后变更角色)
-     * 角色:家长
-     * 触发条件:编辑成员信息并保存
-     */
-    @Test
-    @DisplayName("[场景4] 更新成员关系类型 - 期望更新成功并返回新信息")
-    void parentUpdatesMemberRelationship_Success() {
-        // === Given:需要修改成员的关系类型 ===
-        Map<String, Object> updateRequest = new HashMap<>();
-        updateRequest.put("memberId", CHILD_MEMBER_ID);
-        updateRequest.put("nickname", "王小明");
-        updateRequest.put("relationshipTypeKey", "child"); // 保持不变或变更
-
-        FamilyMemberVO updatedMember = createFamilyMemberVO(CHILD_MEMBER_ID, "王小明", "child");
-        when(familyMemberService.updateMember(eq(PARENT_USER_ID), eq(CHILD_MEMBER_ID), any(AddFamilyMemberDTO.class)))
-                .thenReturn(updatedMember);
-
-        // === When:提交更新表单 ===
-        Result<FamilyMemberVO> result = familyMembersController.updateMember(
-                updateRequest, PARENT_USER_ID);
-
-        // === Then:Milestone-3 - 更新成功 ===
-        assertEquals(200, result.getCode());
-        assertNotNull(result.getData());
-        assertEquals(CHILD_MEMBER_ID, result.getData().getId());
-        verify(familyMemberService).updateMember(eq(PARENT_USER_ID), eq(CHILD_MEMBER_ID), any(AddFamilyMemberDTO.class));
-    }
-
-    // ========== 场景5:切换到成员视角 ==========
-
-    /**
-     * 场景:家长需要以孩子视角体验功能,验证孩子端显示是否正常
-     * 角色:家长(代理操作)
-     * 触发条件:选择成员并点击"切换视角"
-     */
-    @Test
-    @DisplayName("[场景5] 切换到成员视角 - 期望返回切换凭证并正确设置上下文")
-    void parentSwitchesToMemberView_Success() {
-        // === Given:选择了要切换的成员 ===
-        Map<String, Object> switchRequest = new HashMap<>();
-        switchRequest.put("memberId", CHILD_MEMBER_ID);
-
-        SwitchMemberVO mockSwitchVO = new SwitchMemberVO();
-        mockSwitchVO.setMemberId(CHILD_MEMBER_ID);
-        mockSwitchVO.setNickname("王小明");
-
-        when(familyMemberService.switchToMember(PARENT_USER_ID, CHILD_MEMBER_ID))
-                .thenReturn(mockSwitchVO);
-
-        // === When:执行切换视角操作 ===
-        Result<SwitchMemberVO> result = familyMembersController.switchToMember(
-                switchRequest, PARENT_USER_ID);
-
-        // === Then:Milestone-4 - 切换成功,获得切换凭证 ===
-        assertEquals(200, result.getCode());
-        assertNotNull(result.getData());
-        assertEquals(CHILD_MEMBER_ID, result.getData().getMemberId());
-        assertEquals("王小明", result.getData().getNickname());
-        verify(familyMemberService).switchToMember(PARENT_USER_ID, CHILD_MEMBER_ID);
-    }
-
-    /**
-     * 场景:家长尝试切换到自己(不允许自己切换到自己)
-     * 角色:家长
-     * 触发条件:选择成员ID为自己
-     */
-    @Test
-    @DisplayName("[场景5b] 切换到本人视角 - 期望返回错误提示")
-    void parentSwitchesToSelf_Fails() {
-        // === Given:选择了自己的用户ID作为成员ID ===
-        Map<String, Object> switchRequest = new HashMap<>();
-        switchRequest.put("memberId", PARENT_USER_ID); // 自己的用户ID
-
-        when(familyMemberService.switchToMember(PARENT_USER_ID, PARENT_USER_ID))
-                .thenThrow(new RuntimeException("不能切换到自己的视角"));
-
-        // === When:执行切换操作 ===
-        Result<SwitchMemberVO> result = familyMembersController.switchToMember(
-                switchRequest, PARENT_USER_ID);
-
-        // === Then:返回错误提示 ===
-        assertEquals(500, result.getCode());
-        assertTrue(result.getMessage().contains("不能") || result.getMessage().contains("自己"));
-    }
-
-    // ========== 场景6:移除家庭成员 ==========
-
-    /**
-     * 场景:家庭成员关系结束,家长移除不再属于家庭的成员
-     * 角色:家长(管理员)
-     * 触发条件:确认移除操作
-     */
-    @Test
-    @DisplayName("[场景6] 移除家庭成员 - 期望成员从家庭中移除")
-    void parentRemovesMember_Success() {
-        // === Given:存在需要移除的成员 ===
-        Map<String, Long> kickRequest = new HashMap<>();
-        kickRequest.put("memberId", CHILD_MEMBER_ID);
-
-        doNothing().when(familyMemberService).kickMember(PARENT_USER_ID, CHILD_MEMBER_ID);
-
-        // === When:确认移除成员 ===
-        Result<String> result = familyMembersController.kickMember(
-                kickRequest, PARENT_USER_ID);
-
-        // === Then:Milestone-5 - 移除成功 ===
-        assertEquals(200, result.getCode());
-        assertEquals("踢出成功", result.getData());
-        verify(familyMemberService).kickMember(PARENT_USER_ID, CHILD_MEMBER_ID);
-    }
-
-    /**
-     * 场景:家长尝试移除不存在的成员
-     * 角色:家长
-     * 触发条件:成员ID无效
-     */
-    @Test
-    @DisplayName("[场景6b] 移除不存在的成员 - 期望返回错误提示")
-    void parentRemovesNonExistentMember_Fails() {
-        // === Given:成员ID不存在 ===
-        Long nonExistentId = 99999L;
-        Map<String, Long> kickRequest = new HashMap<>();
-        kickRequest.put("memberId", nonExistentId);
-
-        doThrow(new RuntimeException("成员不存在")).when(familyMemberService)
-                .kickMember(PARENT_USER_ID, nonExistentId);
-
-        // === When:尝试移除成员 ===
-        Result<String> result = familyMembersController.kickMember(
-                kickRequest, PARENT_USER_ID);
-
-        // === Then:返回错误提示 ===
-        assertEquals(500, result.getCode());
-        assertTrue(result.getMessage().contains("不存在") || result.getMessage().contains("失败"));
-    }
-
-    /**
-     * 场景:非管理员尝试踢出成员
-     * 角色:普通家庭成员(非管理员)
-     * 触发条件:普通成员尝试执行踢人操作
-     */
-    @Test
-    @DisplayName("[场景6c] 普通成员尝试踢人 - 期望权限不足拒绝")
-    void nonAdminRemovesMember_Forbidden() {
-        // === Given:普通成员尝试踢人 ===
-        Long childUserId = 1002L; // 孩子账号
-        Map<String, Long> kickRequest = new HashMap<>();
-        kickRequest.put("memberId", 2003L);
-
-        doThrow(new RuntimeException("权限不足,只有管理员可以踢出成员"))
-                .when(familyMemberService).kickMember(childUserId, 2003L);
-
-        // === When:普通成员尝试踢人 ===
-        Result<String> result = familyMembersController.kickMember(
-                kickRequest, childUserId);
-
-        // === Then:权限不足,拒绝操作 ===
-        assertEquals(500, result.getCode());
-        assertTrue(result.getMessage().contains("权限") || result.getMessage().contains("管理员"));
-    }
-
-    // ========== Helper Methods ==========
-
-    private FamilyMemberVO createFamilyMemberVO(Long id, String nickname, String effectiveRole) {
-        FamilyMemberVO vo = new FamilyMemberVO();
-        vo.setId(id);
-        vo.setNickname(nickname);
-        vo.setEffectiveRole(effectiveRole);
-        vo.setGender("男");
-        return vo;
-    }
-
-
-}

+ 0 - 297
cfc-backend/src/test/java/com/etotem/cfc/integration/ProductPurchaseFlowTest.java

@@ -1,297 +0,0 @@
-package com.etotem.cfc.integration;
-
-import com.etotem.cfc.common.Result;
-import com.etotem.cfc.controller.product.ProductOrderController;
-import com.etotem.cfc.dto.CreateProductOrderDTO;
-import com.etotem.cfc.dto.ProductOrderDTO;
-import com.etotem.cfc.service.ProductOrderService;
-import com.etotem.cfc.service.PaymentService;
-import org.junit.jupiter.api.*;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.test.mock.mockito.MockBean;
-
-import javax.annotation.Resource;
-import java.util.*;
-
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.*;
-
-/**
- * ================================================================
- * 场景:商品购买支付全流程
- * ================================================================
- * 用户故事:作为家长,我希望在商城购买孩子的用品,
- *           完成支付后等待商品配送到家。
- *
- * 流程步骤:
- *   1. 家长浏览商品列表,选择商品
- *   2. 家长创建商品订单
- *   3. 家长选择支付方式并完成支付
- *   4. 服务商发货,填写物流信息
- *   5. 家长确认收货,订单完成
- *   6. 如有问题,家长申请退款
- *
- * 关键里程碑:
- *   - [Milestone-1] 订单创建成功,获得订单号
- *   - [Milestone-2] 支付成功,订单状态变更为"已支付"
- *   - [Milestone-3] 服务商发货,订单状态变更为"已发货"
- *   - [Milestone-4] 家长确认收货,订单完成
- *   - [Milestone-5] 退款申请提交成功
- * ================================================================
- */
-@SpringBootTest
-public class ProductPurchaseFlowTest {
-
-    @Resource
-    private ProductOrderController orderController;
-
-    @MockBean
-    private ProductOrderService orderService;
-
-    @MockBean
-    private PaymentService paymentService;
-
-    // ========== 测试数据 ==========
-
-    private static final Long USER_ID = 1001L;
-    private static final String ORDER_NO = "PRD20260628001";
-    private static final String TRACKING_NO = "SF1234567890";
-    private static final String EXPRESS_COMPANY = "顺丰速运";
-
-    // ========== 场景1:创建商品订单 ==========
-
-    /**
-     * 场景:家长选购商品后创建订单
-     * 角色:家长
-     * 触发条件:点击"立即购买"或"加入购物车结算"
-     */
-    @Test
-    @DisplayName("[场景1] 创建商品订单 - 期望订单创建成功")
-    void parentCreatesOrder_Success() {
-        // === Given:家长选择了商品 ===
-        CreateProductOrderDTO dto = new CreateProductOrderDTO();
-        dto.setProductId(1L);
-        dto.setQuantity(2);
-
-        ProductOrderDTO mockOrder = createProductOrder(ORDER_NO, "pending");
-        when(orderService.create(any(CreateProductOrderDTO.class), eq(USER_ID)))
-                .thenReturn(Result.success(mockOrder));
-
-        // === When:提交订单创建 ===
-        Result<ProductOrderDTO> result = orderController.create(dto, USER_ID);
-
-        // === Then:Milestone-1 - 订单创建成功 ===
-        assertEquals(200, result.getCode());
-        assertNotNull(result.getData());
-        assertEquals(ORDER_NO, result.getData().getOrderNo());
-        assertEquals("pending", result.getData().getStatus());
-        verify(orderService).create(any(CreateProductOrderDTO.class), eq(USER_ID));
-    }
-
-    // ========== 场景2:支付订单 ==========
-
-    /**
-     * 场景:家长选择微信支付完成订单支付
-     * 角色:家长
-     * 触发条件:点击"去支付"并选择支付方式
-     */
-    @Test
-    @DisplayName("[场景2] 支付商品订单 - 期望支付成功")
-    void parentPaysOrder_Success() {
-        // === Given:存在待支付订单 ===
-        Map<String, Object> payRequest = new HashMap<>();
-        payRequest.put("orderNo", ORDER_NO);
-
-        Map<String, Object> mockPayResult = new HashMap<>();
-        mockPayResult.put("orderNo", ORDER_NO);
-        mockPayResult.put("payTime", new Date());
-        mockPayResult.put("status", "paid");
-
-        when(orderService.pay(eq(ORDER_NO), eq(USER_ID))).thenReturn(Result.success(mockPayResult));
-
-        // === When:家长完成支付 ===
-        Result<Map<String, Object>> result = orderController.pay(payRequest, USER_ID);
-
-        // === Then:Milestone-2 - 支付成功 ===
-        assertEquals(200, result.getCode());
-        assertEquals("paid", result.getData().get("status"));
-        verify(orderService).pay(eq(ORDER_NO), eq(USER_ID));
-    }
-
-    // ========== 场景3:服务商发货 ==========
-
-    /**
-     * 场景:服务商收到订单后发货,填写物流信息
-     * 角色:服务商/供应商
-     * 触发条件:商家点击"发货"并填写物流单号
-     */
-    @Test
-    @DisplayName("[场景3] 服务商发货 - 期望订单状态更新为已发货")
-    void vendorShipsOrder_Success() {
-        // === Given:商家需要发货 ===
-        Map<String, Object> shipRequest = new HashMap<>();
-        shipRequest.put("orderNo", ORDER_NO);
-        shipRequest.put("trackingNumber", TRACKING_NO);
-        shipRequest.put("expressCompany", EXPRESS_COMPANY);
-
-        when(orderService.ship(eq(ORDER_NO), eq(TRACKING_NO), eq(EXPRESS_COMPANY)))
-                .thenReturn(Result.success("发货成功"));
-
-        // === When:服务商发货 ===
-        Result<String> result = orderController.ship(shipRequest);
-
-        // === Then:Milestone-3 - 发货成功 ===
-        assertEquals(200, result.getCode());
-        assertEquals("发货成功", result.getData());
-        verify(orderService).ship(eq(ORDER_NO), eq(TRACKING_NO), eq(EXPRESS_COMPANY));
-    }
-
-    // ========== 场景4:家长确认收货 ==========
-
-    /**
-     * 场景:家长收到商品后确认收货
-     * 角色:家长
-     * 触发条件:确认收到商品无误
-     */
-    @Test
-    @DisplayName("[场景4] 家长确认收货 - 期望订单完成")
-    void parentConfirmsReceive_Success() {
-        // === Given:家长已收到商品 ===
-        Map<String, Object> confirmRequest = new HashMap<>();
-        confirmRequest.put("orderNo", ORDER_NO);
-
-        when(orderService.confirmReceive(eq(ORDER_NO), eq(USER_ID)))
-                .thenReturn(Result.success("确认收货成功"));
-
-        // === When:家长点击"确认收货" ===
-        Result<String> result = orderController.confirm(confirmRequest, USER_ID);
-
-        // === Then:Milestone-4 - 订单完成 ===
-        assertEquals(200, result.getCode());
-        assertEquals("确认收货成功", result.getData());
-        verify(orderService).confirmReceive(eq(ORDER_NO), eq(USER_ID));
-    }
-
-    // ========== 场景5:申请退款 ==========
-
-    /**
-     * 场景:家长发现问题,申请退款
-     * 角色:家长
-     * 触发条件:点击"申请退款"并填写原因
-     */
-    @Test
-    @DisplayName("[场景5] 申请退款 - 期望退款申请提交成功")
-    void parentAppliesRefund_Success() {
-        // === Given:需要申请退款 ===
-        Map<String, Object> refundRequest = new HashMap<>();
-        refundRequest.put("orderNo", ORDER_NO);
-        refundRequest.put("reason", "商品与描述不符");
-
-        when(orderService.applyRefund(eq(ORDER_NO), eq(USER_ID), eq("商品与描述不符")))
-                .thenReturn(Result.success("退款申请已提交"));
-
-        // === When:提交退款申请 ===
-        Result<String> result = orderController.applyRefund(refundRequest, USER_ID);
-
-        // === Then:Milestone-5 - 退款申请成功 ===
-        assertEquals(200, result.getCode());
-        assertEquals("退款申请已提交", result.getData());
-        verify(orderService).applyRefund(eq(ORDER_NO), eq(USER_ID), eq("商品与描述不符"));
-    }
-
-    // ========== 场景6:取消未支付订单 ==========
-
-    /**
-     * 场景:家长改变主意,取消未支付的订单
-     * 角色:家长
-     * 触发条件:点击"取消订单"
-     */
-    @Test
-    @DisplayName("[场景6] 取消未支付订单 - 期望订单关闭")
-    void parentCancelsOrder_Success() {
-        // === Given:订单未支付 ===
-        Map<String, Object> cancelRequest = new HashMap<>();
-        cancelRequest.put("orderNo", ORDER_NO);
-
-        when(orderService.cancel(eq(ORDER_NO), eq(USER_ID)))
-                .thenReturn(Result.success("订单已取消"));
-
-        // === When:取消订单 ===
-        Result<String> result = orderController.cancel(cancelRequest, USER_ID);
-
-        // === Then:订单关闭 ===
-        assertEquals(200, result.getCode());
-        assertEquals("订单已取消", result.getData());
-    }
-
-    // ========== 场景7:查看我的订单列表 ==========
-
-    /**
-     * 场景:家长查看自己的订单列表
-     * 角色:家长
-     * 触发条件:进入"我的订单"页面
-     */
-    @Test
-    @DisplayName("[场景7] 查看我的订单列表 - 期望返回订单列表")
-    void parentViewsMyOrders_Success() {
-        // === Given:用户有多个订单 ===
-        Map<String, Object> listRequest = new HashMap<>();
-        listRequest.put("status", "all");
-
-        List<ProductOrderDTO> mockOrders = Arrays.asList(
-                createProductOrder("ORD001", "completed"),
-                createProductOrder("ORD002", "paid"),
-                createProductOrder("ORD003", "pending")
-        );
-
-        when(orderService.myOrders(eq(USER_ID), eq("all"))).thenReturn(Result.success(mockOrders));
-
-        // === When:请求订单列表 ===
-        Result<List<ProductOrderDTO>> result = orderController.myOrders(listRequest, USER_ID);
-
-        // === Then:返回订单列表 ===
-        assertEquals(200, result.getCode());
-        assertNotNull(result.getData());
-        assertEquals(3, result.getData().size());
-    }
-
-    // ========== 场景8:查看订单详情 ==========
-
-    /**
-     * 场景:家长查看具体订单的详细信息
-     * 角色:家长
-     * 触发条件:点击某个订单查看详情
-     */
-    @Test
-    @DisplayName("[场景8] 查看订单详情 - 期望返回完整订单信息")
-    void parentViewsOrderDetail_Success() {
-        // === Given:订单存在 ===
-        Map<String, Object> detailRequest = new HashMap<>();
-        detailRequest.put("orderNo", ORDER_NO);
-
-        ProductOrderDTO mockDetail = createProductOrder(ORDER_NO, "paid");
-
-        when(orderService.detail(eq(ORDER_NO), eq(USER_ID))).thenReturn(Result.success(mockDetail));
-
-        // === When:请求订单详情 ===
-        Result<ProductOrderDTO> result = orderController.detail(detailRequest, USER_ID);
-
-        // === Then:返回完整订单信息 ===
-        assertEquals(200, result.getCode());
-        assertNotNull(result.getData());
-        assertEquals(ORDER_NO, result.getData().getOrderNo());
-    }
-
-    // ========== Helper Methods ==========
-
-    private ProductOrderDTO createProductOrder(String orderNo, String status) {
-        ProductOrderDTO order = new ProductOrderDTO();
-        order.setOrderNo(orderNo);
-        order.setStatus(status);
-        order.setTotalAmount(29900);
-        order.setProductName("儿童科学实验套装");
-        order.setQuantity(1);
-        return order;
-    }
-}

+ 0 - 431
cfc-backend/src/test/java/com/etotem/cfc/integration/WishExchangeFlowTest.java

@@ -1,431 +0,0 @@
-package com.etotem.cfc.integration;
-
-import com.etotem.cfc.common.Result;
-import com.etotem.cfc.controller.wish.WishController;
-import com.etotem.cfc.dto.CreateWishDTO;
-import com.etotem.cfc.dto.WishDTO;
-import com.etotem.cfc.service.WishService;
-import com.etotem.cfc.service.PointsService;
-import com.etotem.cfc.mapper.WishMapper;
-import com.etotem.cfc.entity.Wish;
-import org.junit.jupiter.api.*;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.test.mock.mockito.MockBean;
-
-import javax.annotation.Resource;
-import java.util.*;
-
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.*;
-
-/**
- * ================================================================
- * 场景:心愿兑换审批全流程
- * ================================================================
- * 用户故事:作为孩子,我希望创建心愿并通过积累积分兑换,
- *           以获得家长的认可和实际的奖励物品。
- *
- * 流程步骤:
- *   1. 孩子创建心愿(如"想要一套科学实验套装")
- *   2. 家长查看心愿并设定所需积分(如 500 积分)
- *   3. 孩子通过完成任务积累积分后,申请兑换心愿
- *   4. 家长审批通过,系统自动扣除积分
- *   5. 家长购买物品后标记"已购买"
- *   6. 孩子确认收货,心愿流程完成
- *
- * 关键里程碑:
- *   - [Milestone-1] 心愿创建成功,获得心愿ID
- *   - [Milestone-2] 家长定价成功,心愿状态变更为"已定价"
- *   - [Milestone-3] 兑换申请提交,状态变更为"兑换中"
- *   - [Milestone-4] 家长审批通过,积分立即扣减
- *   - [Milestone-5] 标记已购买,等待孩子确认
- *   - [Milestone-6] 孩子确认收货,心愿完成
- * ================================================================
- */
-@SpringBootTest
-public class WishExchangeFlowTest {
-
-    @Resource
-    private WishController wishController;
-
-    @MockBean
-    private WishService wishService;
-
-    @MockBean
-    private PointsService pointsService;
-
-    @MockBean
-    private WishMapper wishMapper;
-
-    // ========== 测试数据 ==========
-
-    private static final Long CHILD_USER_ID = 2001L;
-    private static final Long PARENT_USER_ID = 1001L;
-    private static final Long FAMILY_ID = 3001L;
-    private static final Long WISH_ID = 1L;
-    private static final String WISH_TITLE = "科学实验套装";
-    private static final Integer POINTS_REQUIRED = 500;
-
-    // ========== 场景1:孩子创建心愿 ==========
-
-    /**
-     * 场景:孩子创建一个新的心愿
-     * 角色:孩子
-     * 触发条件:孩子点击"创建心愿"并填写表单
-     */
-    @Test
-    @DisplayName("[场景1] 孩子创建心愿 - 期望心愿创建成功")
-    void childCreatesWish_Success() {
-        // === Given:孩子填写了心愿信息 ===
-        CreateWishDTO createDTO = new CreateWishDTO();
-        createDTO.setTitle(WISH_TITLE);
-        createDTO.setDescription("包含50个实验的科学实验套装");
-        createDTO.setImageUrl("https://example.com/science-kit.jpg");
-        createDTO.setLinkUrl("https://shop.example.com/science-kit");
-        createDTO.setEstimatedPrice(29900L); // 299元
-
-        when(wishService.createByChild(eq(CHILD_USER_ID), any(CreateWishDTO.class)))
-                .thenReturn(WISH_ID);
-
-        // === When:孩子提交创建心愿表单 ===
-        Result<Long> result = wishController.createWish(
-                CHILD_USER_ID, "child", createDTO);
-
-        // === Then:Milestone-1 - 心愿创建成功 ===
-        assertEquals(200, result.getCode());
-        assertNotNull(result.getData());
-        assertEquals(WISH_ID, result.getData());
-        verify(wishService).createByChild(eq(CHILD_USER_ID), any(CreateWishDTO.class));
-    }
-
-    // ========== 场景2:家长定价 ==========
-
-    /**
-     * 场景:家长为孩子的心愿设定所需积分
-     * 角色:家长
-     * 触发条件:家长打开心愿详情,决定设定多少积分可兑换
-     */
-    @Test
-    @DisplayName("[场景2] 家长设定心愿积分 - 期望积分设定成功且状态更新")
-    void parentSetsWishPrice_Success() {
-        // === Given:家长决定设定 500 积分 ===
-        Map<String, Object> priceRequest = new HashMap<>();
-        priceRequest.put("pointsRequired", POINTS_REQUIRED);
-
-        when(wishService.setPrice(eq(WISH_ID), eq(PARENT_USER_ID), eq(POINTS_REQUIRED)))
-                .thenReturn(true);
-
-        // === When:家长提交定价 ===
-        Result<Boolean> result = wishController.setPrice(
-                WISH_ID, PARENT_USER_ID, priceRequest);
-
-        // === Then:Milestone-2 - 定价成功 ===
-        assertEquals(200, result.getCode());
-        assertTrue(result.getData());
-        verify(wishService).setPrice(eq(WISH_ID), eq(PARENT_USER_ID), eq(POINTS_REQUIRED));
-    }
-
-    /**
-     * 场景:家长设定的积分值为负数或零
-     * 角色:家长
-     * 触发条件:表单校验未通过
-     */
-    @Test
-    @DisplayName("[场景2b] 家长设定无效积分值 - 期望返回错误提示")
-    void parentSetsInvalidPrice_Fails() {
-        // === Given:设定了无效的积分值 ===
-        Map<String, Object> priceRequest = new HashMap<>();
-        priceRequest.put("pointsRequired", 0); // 无效值
-
-        // === When:尝试提交定价 ===
-        Result<Boolean> result = wishController.setPrice(
-                WISH_ID, PARENT_USER_ID, priceRequest);
-
-        // === Then:返回错误提示 ===
-        assertEquals(500, result.getCode());
-        assertTrue(result.getMessage().contains("有效") || result.getMessage().contains("大于0"));
-    }
-
-    // ========== 场景3:孩子申请兑换 ==========
-
-    /**
-     * 场景:孩子积累足够积分后,申请兑换心愿
-     * 角色:孩子
-     * 触发条件:孩子点击心愿的"申请兑换"按钮
-     */
-    @Test
-    @DisplayName("[场景3] 孩子申请兑换心愿 - 期望兑换申请提交成功")
-    void childRequestsExchange_Success() {
-        // === Given:孩子已积累足够积分 ===
-        when(wishService.requestExchange(eq(WISH_ID), eq(CHILD_USER_ID)))
-                .thenReturn(true);
-
-        // === When:孩子点击"申请兑换" ===
-        Result<Boolean> result = wishController.requestExchange(WISH_ID, CHILD_USER_ID);
-
-        // === Then:Milestone-3 - 兑换申请提交成功 ===
-        assertEquals(200, result.getCode());
-        assertTrue(result.getData());
-        verify(wishService).requestExchange(eq(WISH_ID), eq(CHILD_USER_ID));
-    }
-
-    /**
-         * 场景:孩子积分不足时尝试兑换
-     * 角色:孩子
-     * 触发条件:孩子积分不足以兑换该心愿
-     */
-    @Test
-    @DisplayName("[场景3b] 积分不足时申请兑换 - 期望返回错误提示")
-    void childRequestsExchangeWithInsufficientPoints_Fails() {
-        // === Given:孩子积分不足 ===
-        when(wishService.requestExchange(eq(WISH_ID), eq(CHILD_USER_ID)))
-                .thenThrow(new RuntimeException("积分不足,当前积分:200,需要:500"));
-
-        // === When:孩子尝试兑换 ===
-        Result<Boolean> result = wishController.requestExchange(WISH_ID, CHILD_USER_ID);
-
-        // === Then:返回积分不足提示 ===
-        assertEquals(500, result.getCode());
-        assertTrue(result.getMessage().contains("积分不足"));
-    }
-
-    // ========== 场景4:家长审批兑换 ==========
-
-    /**
-     * 场景:家长审批通过孩子的兑换申请,积分立即扣除
-     * 角色:家长
-     * 触发条件:家长查看兑换申请并点击"批准"
-     */
-    @Test
-    @DisplayName("[场景4] 家长审批通过兑换 - 期望审批成功且积分扣除")
-    void parentApprovesExchange_Success() {
-        // === Given:家长同意孩子的兑换申请 ===
-        Map<String, Object> approveRequest = new HashMap<>();
-        approveRequest.put("approved", true);
-
-        when(wishService.approveExchange(eq(WISH_ID), eq(PARENT_USER_ID), eq(true)))
-                .thenReturn(true);
-        when(pointsService.deductForWish(eq(CHILD_USER_ID), eq(POINTS_REQUIRED)))
-                .thenReturn(true);
-
-        // === When:家长点击"批准" ===
-        Result<Boolean> result = wishController.approveExchange(
-                WISH_ID, PARENT_USER_ID, approveRequest);
-
-        // === Then:Milestone-4 - 审批通过 + Milestone-5 积分扣减 ===
-        assertEquals(200, result.getCode());
-        assertTrue(result.getData());
-        verify(wishService).approveExchange(eq(WISH_ID), eq(PARENT_USER_ID), eq(true));
-    }
-
-    /**
-     * 场景:家长拒绝孩子的兑换申请
-     * 角色:家长
-     * 触发条件:家长认为该心愿不合适,点击"拒绝"
-     */
-    @Test
-    @DisplayName("[场景4b] 家长拒绝兑换申请 - 期望心愿状态回退")
-    void parentRejectsExchange_Success() {
-        // === Given:家长拒绝兑换申请 ===
-        Map<String, Object> approveRequest = new HashMap<>();
-        approveRequest.put("approved", false);
-        approveRequest.put("reason", "本月预算已用完,下个月再兑换好吗?");
-
-        when(wishService.approveExchange(eq(WISH_ID), eq(PARENT_USER_ID), eq(false)))
-                .thenReturn(true);
-
-        // === When:家长点击"拒绝" ===
-        Result<Boolean> result = wishController.approveExchange(
-                WISH_ID, PARENT_USER_ID, approveRequest);
-
-        // === Then:拒绝成功,心愿状态回退 ===
-        assertEquals(200, result.getCode());
-        assertTrue(result.getData());
-        verify(wishService).approveExchange(eq(WISH_ID), eq(PARENT_USER_ID), eq(false));
-    }
-
-    // ========== 场景5:标记已购买 ==========
-
-    /**
-     * 场景:家长购买物品后标记心愿为"已购买"
-     * 角色:家长
-     * 触发条件:家长完成购买操作
-     */
-    @Test
-    @DisplayName("[场景5] 家长标记心愿已购买 - 期望状态变更为待收货")
-    void parentMarksFulfilled_Success() {
-        // === Given:家长已完成购买 ===
-        when(wishService.markFulfilled(eq(WISH_ID), eq(PARENT_USER_ID)))
-                .thenReturn(true);
-
-        // === When:家长点击"已购买" ===
-        Result<Boolean> result = wishController.markFulfilled(WISH_ID, PARENT_USER_ID);
-
-        // === Then:Milestone-5 - 标记成功 ===
-        assertEquals(200, result.getCode());
-        assertTrue(result.getData());
-        verify(wishService).markFulfilled(eq(WISH_ID), eq(PARENT_USER_ID));
-    }
-
-    // ========== 场景6:孩子确认收货 ==========
-
-    /**
-     * 场景:孩子收到物品后确认收货,心愿流程完成
-     * 角色:孩子
-     * 触发条件:孩子收到实物并确认无误
-     */
-    @Test
-    @DisplayName("[场景6] 孩子确认收货 - 期望心愿流程完成")
-    void childConfirmsReceived_Success() {
-        // === Given:孩子已收到物品 ===
-        when(wishService.confirmReceived(eq(WISH_ID), eq(CHILD_USER_ID)))
-                .thenReturn(true);
-
-        // === When:孩子点击"确认收货" ===
-        Result<Boolean> result = wishController.confirmReceived(WISH_ID, CHILD_USER_ID);
-
-        // === Then:Milestone-6 - 心愿完成 ===
-        assertEquals(200, result.getCode());
-        assertTrue(result.getData());
-        verify(wishService).confirmReceived(eq(WISH_ID), eq(CHILD_USER_ID));
-    }
-
-    // ========== 场景7:完整心愿流程(端到端)============
-
-    /**
-     * 场景:完整的心愿从创建到完成的整个生命周期
-     * 角色:孩子 → 家长 → 孩子
-     * 触发条件:心愿的完整业务流程
-     */
-    @Test
-    @DisplayName("[场景7] 完整心愿生命周期 - 期望全流程成功")
-    void completeWishLifecycle_Success() {
-        // === Step 1:孩子创建心愿 ===
-        CreateWishDTO createDTO = new CreateWishDTO();
-        createDTO.setTitle("一本科学杂志");
-        createDTO.setEstimatedPrice(3000L);
-
-        Long newWishId = 2L;
-        when(wishService.createByChild(eq(CHILD_USER_ID), any(CreateWishDTO.class)))
-                .thenReturn(newWishId);
-
-        Result<Long> createResult = wishController.createWish(
-                CHILD_USER_ID, "child", createDTO);
-        assertEquals(200, createResult.getCode());
-        assertEquals(newWishId, createResult.getData());
-
-        // === Step 2:家长定价 ===
-        Map<String, Object> priceRequest = new HashMap<>();
-        priceRequest.put("pointsRequired", 100);
-
-        when(wishService.setPrice(eq(newWishId), eq(PARENT_USER_ID), eq(100)))
-                .thenReturn(true);
-
-        Result<Boolean> priceResult = wishController.setPrice(
-                newWishId, PARENT_USER_ID, priceRequest);
-        assertEquals(200, priceResult.getCode());
-        assertTrue(priceResult.getData());
-
-        // === Step 3:孩子申请兑换 ===
-        when(wishService.requestExchange(eq(newWishId), eq(CHILD_USER_ID)))
-                .thenReturn(true);
-
-        Result<Boolean> exchangeResult = wishController.requestExchange(newWishId, CHILD_USER_ID);
-        assertEquals(200, exchangeResult.getCode());
-        assertTrue(exchangeResult.getData());
-
-        // === Step 4:家长审批通过 ===
-        Map<String, Object> approveRequest = new HashMap<>();
-        approveRequest.put("approved", true);
-
-        when(wishService.approveExchange(eq(newWishId), eq(PARENT_USER_ID), eq(true)))
-                .thenReturn(true);
-
-        Result<Boolean> approveResult = wishController.approveExchange(
-                newWishId, PARENT_USER_ID, approveRequest);
-        assertEquals(200, approveResult.getCode());
-        assertTrue(approveResult.getData());
-
-        // === Step 5:家长标记已购买 ===
-        when(wishService.markFulfilled(eq(newWishId), eq(PARENT_USER_ID)))
-                .thenReturn(true);
-
-        Result<Boolean> fulfillResult = wishController.markFulfilled(newWishId, PARENT_USER_ID);
-        assertEquals(200, fulfillResult.getCode());
-        assertTrue(fulfillResult.getData());
-
-        // === Step 6:孩子确认收货 ===
-        when(wishService.confirmReceived(eq(newWishId), eq(CHILD_USER_ID)))
-                .thenReturn(true);
-
-        Result<Boolean> confirmResult = wishController.confirmReceived(newWishId, CHILD_USER_ID);
-        assertEquals(200, confirmResult.getCode());
-        assertTrue(confirmResult.getData());
-    }
-
-    // ========== 场景8:家长拒绝心愿 ==========
-
-    /**
-     * 场景:家长认为某个心愿不合适,直接拒绝
-     * 角色:家长
-     * 触发条件:家长点击"拒绝"并填写原因
-     */
-    @Test
-    @DisplayName("[场景8] 家长直接拒绝心愿 - 期望心愿被关闭")
-    void parentRejectsWish_Success() {
-        // === Given:家长决定拒绝该心愿 ===
-        Map<String, String> rejectRequest = new HashMap<>();
-        rejectRequest.put("reason", "这套书家里已经有了,换个其他的吧");
-
-        when(wishService.rejectWish(eq(WISH_ID), eq(PARENT_USER_ID), anyString()))
-                .thenReturn(true);
-
-        // === When:家长点击"拒绝" ===
-        Result<Boolean> result = wishController.rejectWish(
-                WISH_ID, PARENT_USER_ID, rejectRequest);
-
-        // === Then:心愿被关闭 ===
-        assertEquals(200, result.getCode());
-        assertTrue(result.getData());
-        verify(wishService).rejectWish(eq(WISH_ID), eq(PARENT_USER_ID), anyString());
-    }
-
-    // ========== 场景9:孩子取消心愿 ==========
-
-    /**
-     * 场景:孩子改变主意,主动取消心愿
-     * 角色:孩子
-     * 触发条件:孩子点击"取消心愿"
-     */
-    @Test
-    @DisplayName("[场景9] 孩子取消心愿 - 期望心愿被取消")
-    void childCancelsWish_Success() {
-        // === Given:孩子改变主意 ===
-        Map<String, String> cancelRequest = new HashMap<>();
-        cancelRequest.put("reason", "想了想还是不要了");
-
-        when(wishService.cancelWish(eq(WISH_ID), eq(CHILD_USER_ID), anyString()))
-                .thenReturn(true);
-
-        // === When:孩子点击"取消" ===
-        Result<Boolean> result = wishController.cancelWish(
-                WISH_ID, CHILD_USER_ID, cancelRequest);
-
-        // === Then:心愿被取消 ===
-        assertEquals(200, result.getCode());
-        assertTrue(result.getData());
-        verify(wishService).cancelWish(eq(WISH_ID), eq(CHILD_USER_ID), anyString());
-    }
-
-    // ========== Helper Methods ==========
-
-    private WishDTO createWishDTO(Long id, String title, String status) {
-        WishDTO dto = new WishDTO();
-        dto.setId(id);
-        dto.setTitle(title);
-        dto.setStatus(status);
-        dto.setPointsRequired(POINTS_REQUIRED);
-        return dto;
-    }
-}

+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/AIChatControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/AIChatControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/ActivityControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/ActivityControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/ArticleCategoryControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/ArticleCategoryControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/ArticleControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/ArticleControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/AuthControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/AuthControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/DailyTaskControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/DailyTaskControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/DimensionAdminControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/DimensionAdminControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/FamilyControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/FamilyControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/GuideFamilyTaskControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/GuideFamilyTaskControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/GuideOrderControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/GuideOrderControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/GuideRolePermissionTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/GuideRolePermissionTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/GuideTeamControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/GuideTeamControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/HealthDimensionControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/HealthDimensionControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/PaymentControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/PaymentControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/ProductControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/ProductControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/ProductOrderControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/ProductOrderControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/StatsControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/StatsControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/WishControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/WishControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/mind/MindFortuneControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/mind/MindFortuneControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/ActivityAdminServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/ActivityAdminServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/ActivityServicePublishTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/ActivityServicePublishTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/ArticleWorkflowTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/ArticleWorkflowTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/ChallengeScoreServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/ChallengeScoreServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/CouponServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/CouponServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/DailyOverviewServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/DailyOverviewServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/DanReportParseServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/DanReportParseServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/DataMigrationServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/DataMigrationServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/DimensionEnergySyncServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/DimensionEnergySyncServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/DimensionScoreServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/DimensionScoreServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/FiveDimensionSelfCheckServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/FiveDimensionSelfCheckServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/FoodRecommendServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/FoodRecommendServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/FoodServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/FoodServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/FortuneServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/FortuneServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/GuideApplicationServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/GuideApplicationServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/GuideFamilyServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/GuideFamilyServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/GuideHierarchyServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/GuideHierarchyServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/GuidePackageServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/GuidePackageServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/GuidePackageTemplateServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/GuidePackageTemplateServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/MiniGameServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/MiniGameServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/NutritionDeficiencyServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/NutritionDeficiencyServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/PdfParseServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/PdfParseServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/ProductServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/ProductServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/RelationshipQuestionnaireServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/RelationshipQuestionnaireServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/ReportBlockAssemblerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/ReportBlockAssemblerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/ServiceContentServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/ServiceContentServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/ServiceTypeServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/ServiceTypeServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/SupplySystemServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/SupplySystemServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/TaskServiceMinigameTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/TaskServiceMinigameTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/TeacherLoginTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/TeacherLoginTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/impl/DimensionScoreServiceImplTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/impl/DimensionScoreServiceImplTest.java


+ 0 - 327
cfc-backend/src/test/java/com/etotem/cfc/service/ArticleServiceTest.java

@@ -1,327 +0,0 @@
-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.Article;
-import com.etotem.cfc.mapper.ArticleMapper;
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.test.mock.mockito.MockBean;
-
-import javax.annotation.Resource;
-import java.util.*;
-
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.*;
-
-@SpringBootTest
-public class ArticleServiceTest {
-
-    @Resource
-    private ArticleService articleService;
-
-    @MockBean
-    private ArticleMapper articleMapper;
-
-    @MockBean
-    private ArticlePermissionService articlePermissionService;
-
-    @MockBean
-    private ArticleCategoryService articleCategoryService;
-
-    private Article mockArticle(Long id, String title, String status, String visibility) {
-        Article a = new Article();
-        a.setId(id);
-        a.setTitle(title);
-        a.setSummary("摘要");
-        a.setContent("<p>内容</p>");
-        a.setAuthor("测试");
-        a.setStatus(status);
-        a.setVisibility(visibility);
-        a.setCategoryId(1L);
-        a.setIsFeatured(0);
-        a.setViewCount(5);
-        a.setCreatedBy(1L);
-        return a;
-    }
-
-    // ==================== getPublicList ====================
-
-    @Test
-    public void getPublicList_OnlyPublished() {
-        List<Article> allArticles = Arrays.asList(
-            mockArticle(1L, "已发布", "published", "public"),
-            mockArticle(2L, "草稿", "draft", "public")
-        );
-
-        // Mock mapper to return all articles (real filtering happens via wrapper)
-        when(articleMapper.selectPage(any(Page.class), any(LambdaQueryWrapper.class)))
-            .thenAnswer(invocation -> {
-                Page<Article> page = invocation.getArgument(0);
-                page.setRecords(allArticles);
-                page.setTotal(2);
-                return page;
-            });
-
-        // Permission service allows all visible ones
-        when(articlePermissionService.filterVisible(anyList(), isNull(), isNull(), isNull()))
-            .thenReturn(Arrays.asList(allArticles.get(0))); // Only published
-
-        Page<Article> result = articleService.getPublicList(null, null, null, 1, 10, null, null, null);
-        assertEquals(1, result.getRecords().size());
-        assertEquals("已发布", result.getRecords().get(0).getTitle());
-    }
-
-    @Test
-    public void getPublicList_Empty() {
-        when(articleMapper.selectPage(any(Page.class), any(LambdaQueryWrapper.class)))
-            .thenAnswer(invocation -> {
-                Page<Article> page = invocation.getArgument(0);
-                page.setRecords(Collections.emptyList());
-                page.setTotal(0);
-                return page;
-            });
-        when(articlePermissionService.filterVisible(anyList(), isNull(), isNull(), isNull()))
-            .thenReturn(Collections.emptyList());
-
-        Page<Article> result = articleService.getPublicList(null, null, null, 1, 10, null, null, null);
-        assertTrue(result.getRecords().isEmpty());
-        assertEquals(0, result.getTotal());
-    }
-
-    // ==================== getDetail ====================
-
-    @Test
-    public void getDetail_Success() {
-        Article article = mockArticle(1L, "详情文章", "published", "public");
-        when(articleMapper.selectById(1L)).thenReturn(article);
-        when(articlePermissionService.isVisible(article, null, null, null)).thenReturn(true);
-
-        Article result = articleService.getDetail(1L, null, null, null);
-        assertNotNull(result);
-        assertEquals("详情文章", result.getTitle());
-    }
-
-    @Test
-    public void getDetail_NotFound() {
-        when(articleMapper.selectById(999L)).thenReturn(null);
-
-        Article result = articleService.getDetail(999L, null, null, null);
-        assertNull(result);
-    }
-
-    @Test
-    public void getDetail_NotPublished() {
-        Article article = mockArticle(1L, "草稿", "draft", "public");
-        when(articleMapper.selectById(1L)).thenReturn(article);
-
-        Article result = articleService.getDetail(1L, null, null, null);
-        assertNull(result);
-    }
-
-    @Test
-    public void getDetail_NoPermission() {
-        Article article = mockArticle(1L, "私密文章", "published", "private");
-        when(articleMapper.selectById(1L)).thenReturn(article);
-        when(articlePermissionService.isVisible(article, null, null, null)).thenReturn(false);
-
-        Article result = articleService.getDetail(1L, null, null, null);
-        assertNull(result);
-    }
-
-    // ==================== getFeatured ====================
-
-    @Test
-    public void getFeatured_Success() {
-        List<Article> featured = Arrays.asList(
-            mockArticle(1L, "精选1", "published", "public"),
-            mockArticle(2L, "精选2", "published", "public")
-        );
-
-        when(articleMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(featured);
-        when(articlePermissionService.filterVisible(featured, null, null, null)).thenReturn(featured);
-
-        List<Article> result = articleService.getFeatured(5, null, null, null);
-        assertEquals(2, result.size());
-    }
-
-    @Test
-    public void getFeatured_Limit() {
-        List<Article> featured = Arrays.asList(
-            mockArticle(1L, "精选1", "published", "public"),
-            mockArticle(2L, "精选2", "published", "public"),
-            mockArticle(3L, "精选3", "published", "public")
-        );
-
-        when(articleMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(featured);
-        when(articlePermissionService.filterVisible(featured, null, null, null)).thenReturn(featured);
-
-        List<Article> result = articleService.getFeatured(2, null, null, null);
-        assertEquals(2, result.size());
-    }
-
-    @Test
-    public void getFeatured_Empty() {
-        when(articleMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(Collections.emptyList());
-        when(articlePermissionService.filterVisible(anyList(), isNull(), isNull(), isNull()))
-            .thenReturn(Collections.emptyList());
-
-        List<Article> result = articleService.getFeatured(5, null, null, null);
-        assertTrue(result.isEmpty());
-    }
-
-    // ==================== recordRead ====================
-
-    @Test
-    public void recordRead_IncrementsViewCount() {
-        doAnswer(invocation -> {
-            // Verify the update wrapper has setSql for view_count increment
-            return 1;
-        }).when(articleMapper).update(any(), any(LambdaQueryWrapper.class));
-
-        articleService.recordRead(1L, null, null, 30);
-        verify(articleMapper, times(1)).update(any(), any());
-    }
-
-    // ==================== getAdminList ====================
-
-    @Test
-    public void getAdminList_All() {
-        when(articleMapper.selectPage(any(Page.class), any(LambdaQueryWrapper.class)))
-            .thenAnswer(invocation -> {
-                Page<Article> page = invocation.getArgument(0);
-                page.setRecords(Arrays.asList(mockArticle(1L, "管理可见", "published", "public")));
-                page.setTotal(1);
-                return page;
-            });
-
-        Page<Article> result = articleService.getAdminList(null, null, null, null, null, null, null, null, 1, 20);
-        assertEquals(1, result.getRecords().size());
-    }
-
-    @Test
-    public void getAdminList_WithStatusFilter() {
-        when(articleMapper.selectPage(any(Page.class), any(LambdaQueryWrapper.class)))
-            .thenAnswer(invocation -> {
-                Page<Article> page = invocation.getArgument(0);
-                page.setRecords(Arrays.asList(mockArticle(1L, "草稿文章", "draft", "public")));
-                page.setTotal(1);
-                return page;
-            });
-
-        Page<Article> result = articleService.getAdminList("draft", null, null, null, null, null, null, null, 1, 20);
-        assertEquals(1, result.getRecords().size());
-    }
-
-    // ==================== create ====================
-
-    @Test
-    public void create_SetsInitialValues() {
-        Article article = mockArticle(null, "新文章", "draft", "public");
-        when(articleMapper.insert(any(Article.class))).thenReturn(1);
-
-        articleService.create(article, 1L);
-
-        verify(articleMapper).insert(argThat(a -> {
-            assertNull(a.getId());
-            assertEquals(1L, a.getCreatedBy().longValue());
-            assertEquals(0, a.getViewCount().intValue());
-            assertNotNull(a.getCreatedAt());
-            assertNotNull(a.getUpdatedAt());
-            return true;
-        }));
-    }
-
-    @Test
-    public void create_PublishedSetsPublishTime() {
-        Article article = mockArticle(null, "直接发布", "published", "public");
-        when(articleMapper.insert(any(Article.class))).thenReturn(1);
-
-        articleService.create(article, 1L);
-
-        verify(articleMapper).insert(argThat(a -> {
-            assertNotNull(a.getPublishedAt());
-            return true;
-        }));
-    }
-
-    // ==================== update ====================
-
-    @Test
-    public void update_SetsUpdatedAt() {
-        Article article = mockArticle(1L, "更新", "published", "public");
-        when(articleMapper.updateById(any(Article.class))).thenReturn(1);
-
-        articleService.update(article);
-        verify(articleMapper).updateById(argThat(a -> {
-            assertNotNull(a.getUpdatedAt());
-            return true;
-        }));
-    }
-
-    // ==================== delete ====================
-
-    @Test
-    public void delete_DeletesById() {
-        articleService.delete(1L);
-        verify(articleMapper).deleteById(1L);
-    }
-
-    // ==================== toggleStatus ====================
-
-    @Test
-    public void toggleStatus_Publish() {
-        Article article = mockArticle(1L, "发布", "draft", "public");
-        when(articleMapper.selectById(1L)).thenReturn(article);
-        when(articleMapper.updateById(any(Article.class))).thenReturn(1);
-
-        articleService.toggleStatus(1L, "published");
-        assertEquals("published", article.getStatus());
-        assertNotNull(article.getUpdatedAt());
-        assertNotNull(article.getPublishedAt());
-    }
-
-    @Test
-    public void toggleStatus_Draft() {
-        Article article = mockArticle(1L, "下架", "published", "public");
-        article.setPublishedAt(new Date());
-        when(articleMapper.selectById(1L)).thenReturn(article);
-        when(articleMapper.updateById(any(Article.class))).thenReturn(1);
-
-        articleService.toggleStatus(1L, "draft");
-        assertEquals("draft", article.getStatus());
-    }
-
-    @Test
-    public void toggleStatus_ArticleNotFound() {
-        when(articleMapper.selectById(999L)).thenReturn(null);
-
-        articleService.toggleStatus(999L, "published");
-        verify(articleMapper, never()).updateById(any());
-    }
-
-    // ==================== toggleFeatured ====================
-
-    @Test
-    public void toggleFeatured_SetFeatured() {
-        Article article = mockArticle(1L, "精选", "published", "public");
-        when(articleMapper.selectById(1L)).thenReturn(article);
-
-        articleService.toggleFeatured(1L, 1);
-        assertEquals(1, article.getIsFeatured().intValue());
-        assertNotNull(article.getUpdatedAt());
-        verify(articleMapper).updateById(article);
-    }
-
-    @Test
-    public void toggleFeatured_RemoveFeatured() {
-        Article article = mockArticle(1L, "取消精选", "published", "public");
-        article.setIsFeatured(1);
-        when(articleMapper.selectById(1L)).thenReturn(article);
-
-        articleService.toggleFeatured(1L, 0);
-        assertEquals(0, article.getIsFeatured().intValue());
-        verify(articleMapper).updateById(article);
-    }
-}

+ 0 - 270
cfc-backend/src/test/java/com/etotem/cfc/service/FamilyChallengeServiceTest.java

@@ -1,270 +0,0 @@
-package com.etotem.cfc.service;
-
-import com.etotem.cfc.common.Result;
-import com.etotem.cfc.entity.FamilyChallenge;
-import com.etotem.cfc.entity.FamilyChallengeParticipant;
-import com.etotem.cfc.entity.FamilyChallengeProgress;
-import com.etotem.cfc.entity.FamilyMember;
-import com.etotem.cfc.mapper.FamilyChallengeMapper;
-import com.etotem.cfc.mapper.FamilyChallengeParticipantMapper;
-import com.etotem.cfc.mapper.FamilyChallengeProgressMapper;
-import com.etotem.cfc.mapper.FamilyMemberMapper;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.util.Date;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyList;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.argThat;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-public class FamilyChallengeServiceTest {
-
-    @Mock private FamilyChallengeMapper familyChallengeMapper;
-    @Mock private FamilyChallengeProgressMapper familyChallengeProgressMapper;
-    @Mock private FamilyChallengeParticipantMapper familyChallengeParticipantMapper;
-    @Mock private FamilyMemberMapper familyMemberMapper;
-    @Mock private HealthScoreService healthScoreService;
-    @Mock private PointsService pointsService;
-    @Mock private ChallengeScoreService challengeScoreService;
-    @InjectMocks private FamilyChallengeService service;
-
-    @BeforeEach
-    void setUp() {
-        MockitoAnnotations.openMocks(this);
-        when(familyChallengeParticipantMapper.selectList(any())).thenReturn(java.util.Collections.emptyList());
-    }
-
-    @Test
-    void updateProgress_shouldRejectDuplicateCheckinSameDay() {
-        FamilyChallenge challenge = new FamilyChallenge();
-        challenge.setId(1L);
-        challenge.setFamilyId(10L);
-        challenge.setStatus("active");
-        challenge.setTargetMode("aggregate");
-        challenge.setTargetValue(300);
-        challenge.setEndDate(new Date(System.currentTimeMillis() + 86400000L * 7));
-
-        FamilyChallengeProgress existing = new FamilyChallengeProgress();
-        existing.setChallengeId(1L);
-        existing.setChildId(100L);
-        existing.setProgressValue(5);
-        existing.setUpdatedAt(new Date());
-
-        FamilyChallengeParticipant participant = new FamilyChallengeParticipant();
-        participant.setChallengeId(1L);
-        participant.setMemberId(100L);
-        participant.setStatus("agreed");
-
-        when(familyChallengeMapper.selectById(1L)).thenReturn(challenge);
-        when(familyChallengeProgressMapper.selectOne(any())).thenReturn(existing);
-        when(familyMemberMapper.selectCount(any())).thenReturn(1L);
-        when(familyChallengeParticipantMapper.selectOne(any())).thenReturn(participant);
-
-        Result<Void> result = service.updateProgress(1L, 100L, 1);
-
-        assertEquals(Integer.valueOf(500), result.getCode());
-        verify(familyChallengeProgressMapper, never()).updateById(any());
-    }
-
-    @Test
-    void updateProgress_shouldRejectWhenChallengeNotActive() {
-        FamilyChallenge challenge = new FamilyChallenge();
-        challenge.setId(1L);
-        challenge.setFamilyId(10L);
-        challenge.setStatus("completed");
-        challenge.setTargetMode("aggregate");
-        challenge.setTargetValue(300);
-        challenge.setEndDate(new Date(System.currentTimeMillis() + 86400000L * 7));
-
-        when(familyChallengeMapper.selectById(1L)).thenReturn(challenge);
-
-        Result<Void> result = service.updateProgress(1L, 100L, 1);
-
-        assertEquals(Integer.valueOf(500), result.getCode());
-        verify(familyChallengeProgressMapper, never()).insert(any());
-        verify(familyChallengeProgressMapper, never()).updateById(any());
-    }
-
-    @Test
-    void checkAndSettle_shouldGrantPointsWhenCompleted() {
-        FamilyChallenge challenge = new FamilyChallenge();
-        challenge.setId(1L);
-        challenge.setFamilyId(10L);
-        challenge.setStatus("active");
-        challenge.setTargetMode("aggregate");
-        challenge.setTargetValue(10);
-        challenge.setRewardPoints(50);
-        challenge.setEndDate(new Date(System.currentTimeMillis() - 86400000L));
-
-        FamilyChallengeProgress p = new FamilyChallengeProgress();
-        p.setChallengeId(1L);
-        p.setChildId(100L);
-        p.setProgressValue(12);
-        p.setCompleted(false);
-
-        when(familyChallengeMapper.selectById(1L)).thenReturn(challenge);
-        when(familyChallengeProgressMapper.selectList(any())).thenReturn(java.util.Arrays.asList(p));
-        when(familyMemberMapper.selectList(any())).thenReturn(java.util.Collections.emptyList());
-        when(familyChallengeMapper.update(any(), any())).thenReturn(1);
-
-        service.settleChallenge(1L);
-
-        verify(familyChallengeMapper).update(any(), any());
-        verify(familyChallengeProgressMapper).updateById(argThat(p2 -> Boolean.TRUE.equals(p2.getCompleted())));
-        verify(pointsService).awardSystemPoints(eq(100L), eq(50), anyString());
-    }
-
-    @Test
-    void getActiveChallenges_shouldSettleExpiredChallenges() {
-        FamilyChallenge expired = new FamilyChallenge();
-        expired.setId(2L);
-        expired.setFamilyId(10L);
-        expired.setStatus("active");
-        expired.setTargetMode("aggregate");
-        expired.setTargetValue(10);
-        expired.setEndDate(new Date(System.currentTimeMillis() - 86400000L));
-
-        when(familyChallengeMapper.selectList(any())).thenReturn(java.util.Arrays.asList(expired));
-        when(familyChallengeProgressMapper.selectList(any())).thenReturn(java.util.Collections.emptyList());
-        when(familyMemberMapper.selectList(any())).thenReturn(java.util.Collections.emptyList());
-        when(familyChallengeMapper.update(any(), any())).thenReturn(1);
-
-        service.getActiveChallenges(10L, null);
-
-        verify(familyChallengeMapper).update(any(), any());
-    }
-
-    @Test
-    void getTemplates_shouldReturnAllTemplatesWithParsedRewardPoints() {
-        java.util.List<java.util.Map<String, Object>> templates = service.getTemplates();
-
-        assertEquals(7, templates.size());
-        for (java.util.Map<String, Object> t : templates) {
-            assertTrue(t.get("rewardPoints") instanceof Integer, "rewardPoints 必须可解析为 Integer");
-            assertTrue(t.get("durationDays") instanceof Integer, "durationDays 必须可解析为 Integer");
-            assertTrue(t.get("targetValue") instanceof Integer, "targetValue 必须可解析为 Integer");
-            assertTrue(t.get("targetMode") instanceof String, "targetMode 必须存在");
-        }
-        java.util.Map<String, Object> first = templates.get(0);
-        assertEquals("full_checkin", first.get("type"));
-        assertEquals("全员打卡挑战", first.get("title"));
-        assertEquals(Integer.valueOf(3), first.get("durationDays"));
-        assertEquals(Integer.valueOf(3), first.get("targetValue"));
-        assertEquals(Integer.valueOf(50), first.get("rewardPoints"));
-        assertEquals("all_members", first.get("targetMode"));
-        java.util.Map<String, Object> sports = templates.get(1);
-        assertEquals("sports", sports.get("type"));
-        assertEquals("aggregate", sports.get("targetMode"));
-        assertEquals(Integer.valueOf(300), sports.get("targetValue"));
-        assertEquals(Integer.valueOf(100), sports.get("rewardPoints"));
-    }
-
-    @Test
-    void respondChallenge_agree_shouldTransitionToActiveWhenAllAgreed() {
-        FamilyChallenge challenge = new FamilyChallenge();
-        challenge.setId(1L);
-        challenge.setFamilyId(10L);
-        challenge.setStatus("pending");
-        challenge.setTargetMode("aggregate");
-        challenge.setTargetValue(300);
-        challenge.setCreatorId(9L);
-
-        FamilyChallengeParticipant p = new FamilyChallengeParticipant();
-        p.setId(1L);
-        p.setChallengeId(1L);
-        p.setMemberId(100L);
-        p.setStatus("pending");
-
-        FamilyChallengeParticipant initiator = new FamilyChallengeParticipant();
-        initiator.setId(2L);
-        initiator.setChallengeId(1L);
-        initiator.setMemberId(200L);
-        initiator.setStatus("agreed");
-
-        when(familyChallengeMapper.selectById(1L)).thenReturn(challenge);
-        when(familyChallengeParticipantMapper.selectOne(any())).thenReturn(p);
-        when(familyChallengeParticipantMapper.selectList(any()))
-                .thenReturn(java.util.Arrays.asList(initiator, p));
-
-        Result<Void> result = service.respondChallenge(1L, 100L, "agree");
-
-        assertEquals(Integer.valueOf(200), result.getCode());
-        verify(challengeScoreService).applyAgree(eq(10L), eq(100L), anyList(), eq(1L));
-        verify(familyChallengeParticipantMapper).updateById(argThat(pp -> "agreed".equals(pp.getStatus())));
-        verify(familyChallengeMapper).updateById(argThat(c -> "active".equals(c.getStatus())));
-    }
-
-    @Test
-    void respondChallenge_reject_shouldCancelWhenOnlyInitiatorLeft() {
-        FamilyChallenge challenge = new FamilyChallenge();
-        challenge.setId(1L);
-        challenge.setFamilyId(10L);
-        challenge.setStatus("pending");
-        challenge.setTargetMode("aggregate");
-        challenge.setTargetValue(300);
-        challenge.setCreatorId(9L);
-
-        FamilyChallengeParticipant p = new FamilyChallengeParticipant();
-        p.setId(1L);
-        p.setChallengeId(1L);
-        p.setMemberId(100L);
-        p.setStatus("pending");
-
-        FamilyChallengeParticipant initiator = new FamilyChallengeParticipant();
-        initiator.setId(2L);
-        initiator.setChallengeId(1L);
-        initiator.setMemberId(200L);
-        initiator.setStatus("agreed");
-
-        FamilyMember member = new FamilyMember();
-        member.setId(200L);
-        member.setUserId(9L);
-
-        when(familyChallengeMapper.selectById(1L)).thenReturn(challenge);
-        when(familyChallengeParticipantMapper.selectOne(any())).thenReturn(p);
-        when(familyChallengeParticipantMapper.selectList(any()))
-                .thenReturn(java.util.Arrays.asList(initiator, p));
-        when(familyMemberMapper.selectList(any())).thenReturn(java.util.Arrays.asList(member));
-
-        Result<Void> result = service.respondChallenge(1L, 100L, "reject");
-
-        assertEquals(Integer.valueOf(200), result.getCode());
-        verify(challengeScoreService).applyReject(eq(10L), eq(100L), eq(200L), anyList(), eq(1L));
-        verify(familyChallengeMapper).updateById(argThat(c -> "cancelled".equals(c.getStatus())));
-    }
-
-    @Test
-    void respondChallenge_shouldRejectWhenAlreadyProcessed() {
-        FamilyChallenge challenge = new FamilyChallenge();
-        challenge.setId(1L);
-        challenge.setFamilyId(10L);
-        challenge.setStatus("pending");
-        challenge.setTargetMode("aggregate");
-        challenge.setTargetValue(300);
-
-        FamilyChallengeParticipant p = new FamilyChallengeParticipant();
-        p.setId(1L);
-        p.setChallengeId(1L);
-        p.setMemberId(100L);
-        p.setStatus("agreed"); // 已处理过
-
-        when(familyChallengeMapper.selectById(1L)).thenReturn(challenge);
-        when(familyChallengeParticipantMapper.selectOne(any())).thenReturn(p);
-
-        Result<Void> result = service.respondChallenge(1L, 100L, "agree");
-
-        assertEquals(Integer.valueOf(500), result.getCode());
-        verify(challengeScoreService, never()).applyAgree(any(), any(), any(), any());
-    }
-}

Some files were not shown because too many files changed in this diff