Pārlūkot izejas kodu

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

# Conflicts:
#	cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
#	cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java
#	cfc-backend/src/main/java/com/etotem/cfc/controller/notice/NoticeController.java
#	cfc-frontend/pages/health/gut-flora-foods-detail.vue
Xiaogang Liao 2 mēneši atpakaļ
vecāks
revīzija
25309bf693
100 mainītis faili ar 5520 papildinājumiem un 387 dzēšanām
  1. 138 0
      cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  2. 10 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthPlanController.java
  3. 24 11
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java
  4. 73 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/PurchaseController.java
  5. 45 31
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminEnergyConfigController.java
  6. 22 22
      cfc-backend/src/main/java/com/etotem/cfc/controller/butler/ButlerController.java
  7. 46 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/butler/FamilyButlerSelectController.java
  8. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyController.java
  9. 12 20
      cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyUserController.java
  10. 0 5
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideController.java
  11. 0 151
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuidePackageController.java
  12. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/PaymentController.java
  13. 4 7
      cfc-backend/src/main/java/com/etotem/cfc/controller/notice/NoticeController.java
  14. 43 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/ButlerAssignment.java
  15. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/ButlerProfile.java
  16. 29 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/ChallengeProgress.java
  17. 27 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/DimensionDailyLimit.java
  18. 29 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/PurchaseOrder.java
  19. 12 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/ButlerAssignmentMapper.java
  20. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/ChallengeProgressMapper.java
  21. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/DimensionDailyLimitMapper.java
  22. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/PurchaseOrderMapper.java
  23. 169 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ButlerService.java
  24. 14 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ChallengeService.java
  25. 38 0
      cfc-backend/src/main/java/com/etotem/cfc/service/HealthReportService.java
  26. 20 0
      cfc-backend/src/main/java/com/etotem/cfc/service/PurchaseService.java
  27. 15 14
      cfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java
  28. 137 0
      cfc-backend/src/main/java/com/etotem/cfc/service/impl/ChallengeServiceImpl.java
  29. 206 0
      cfc-backend/src/main/java/com/etotem/cfc/service/impl/PurchaseServiceImpl.java
  30. 41 0
      cfc-backend/src/main/resources/db/migration/V252__create_purchase_tables.sql
  31. 43 0
      cfc-backend/src/main/resources/schema.sql
  32. 16 8
      cfc-frontend/pages.json
  33. 10 3
      cfc-frontend/pages/action-detail/index.vue
  34. 0 1
      cfc-frontend/pages/action-detail/member-action-detail.vue
  35. 0 1
      cfc-frontend/pages/activity/index.vue
  36. 0 1
      cfc-frontend/pages/body-detail/checkin.vue
  37. 0 1
      cfc-frontend/pages/body-detail/health-report.vue
  38. 0 1
      cfc-frontend/pages/body-detail/member-body-detail.vue
  39. 0 1
      cfc-frontend/pages/body/member-body-detail.vue
  40. 1 1
      cfc-frontend/pages/diet/food-query.vue
  41. 1 1
      cfc-frontend/pages/diet/meal-config.vue
  42. 1 1
      cfc-frontend/pages/diet/preferences.vue
  43. 1 1
      cfc-frontend/pages/diet/recommendation.vue
  44. 1 1
      cfc-frontend/pages/diet/record-detail.vue
  45. 1 1
      cfc-frontend/pages/diet/records.vue
  46. 1 6
      cfc-frontend/pages/games/list.vue
  47. 1 1
      cfc-frontend/pages/growth/caution-detail.vue
  48. 143 0
      cfc-frontend/pages/growth/challenge-progress.vue
  49. 0 1
      cfc-frontend/pages/growth/diet-checkin.vue
  50. 0 1
      cfc-frontend/pages/growth/exercise-checkin.vue
  51. 0 1
      cfc-frontend/pages/growth/growth-emotion-checkin.vue
  52. 0 1
      cfc-frontend/pages/growth/profile/index.vue
  53. 0 1
      cfc-frontend/pages/growth/sleep-checkin.vue
  54. 7 9
      cfc-frontend/pages/health-main/index.vue
  55. 0 1
      cfc-frontend/pages/health/bodyfat-confirm.vue
  56. 0 1
      cfc-frontend/pages/health/daily-checkin.vue
  57. 0 1
      cfc-frontend/pages/health/diet-index.vue
  58. 0 1
      cfc-frontend/pages/health/exercise-index.vue
  59. 1 0
      cfc-frontend/pages/health/gut-flora-detail.vue
  60. 0 1
      cfc-frontend/pages/health/gut-flora-risks-detail.vue
  61. 0 1
      cfc-frontend/pages/health/gut-flora-species-detail.vue
  62. 0 1
      cfc-frontend/pages/health/gut-index.vue
  63. 25 9
      cfc-frontend/pages/health/health-plan-summary.vue
  64. 0 1
      cfc-frontend/pages/health/nutrition-profile.vue
  65. 0 1
      cfc-frontend/pages/health/physical-exam-detail.vue
  66. 0 1
      cfc-frontend/pages/health/report-confirm.vue
  67. 93 4
      cfc-frontend/pages/health/report-list.vue
  68. 0 1
      cfc-frontend/pages/health/sleep-index.vue
  69. 0 1
      cfc-frontend/pages/health/teacher-select.vue
  70. 0 1
      cfc-frontend/pages/health/water-index.vue
  71. 0 1
      cfc-frontend/pages/home-pages/member-home-detail.vue
  72. 32 1
      cfc-frontend/pages/index-home/index.vue
  73. 1 0
      cfc-frontend/pages/meal/log.vue
  74. 1 0
      cfc-frontend/pages/meal/preference.vue
  75. 1 0
      cfc-frontend/pages/meal/recipe-detail.vue
  76. 1 0
      cfc-frontend/pages/meal/recommend.vue
  77. 1 1
      cfc-frontend/pages/member-detail/report-detail.vue
  78. 1 1
      cfc-frontend/pages/member-detail/service-role-apply.vue
  79. 0 1
      cfc-frontend/pages/mind-detail/member-mind-detail.vue
  80. 0 1
      cfc-frontend/pages/mind/member-mind-detail.vue
  81. 208 0
      cfc-frontend/pages/purchase/index.vue
  82. 167 0
      cfc-frontend/pages/purchase/renewal.vue
  83. 0 1
      cfc-frontend/pages/rewards/rewards.vue
  84. 0 1
      cfc-frontend/pages/tianpan/annual-energy.vue
  85. 0 1
      cfc-frontend/pages/tianpan/daily-fortune.vue
  86. 0 1
      cfc-frontend/pages/tianpan/related-items.vue
  87. 0 1
      cfc-frontend/pages/wisdom-detail/member-wisdom-detail.vue
  88. 0 1
      cfc-frontend/pages/wisdom/member-wisdom-detail.vue
  89. 131 26
      cfc-frontend/utils/api.js
  90. 1 1
      cfc-web/.last_build_commit
  91. 2 2
      cfc-web/package-lock.json
  92. 1 1
      cfc-web/package.json
  93. 142 0
      cfc-web/public/CHANGELOG-v1.0.md
  94. 143 1
      cfc-web/public/CHANGELOG.md
  95. 218 0
      cfc-web/src/views/admin/PurchaseManagement.vue
  96. 6 3
      docs/superpowers/PROJECT-OVERVIEW.md
  97. 166 9
      docs/superpowers/api/API_REFERENCE.md
  98. 315 0
      docs/superpowers/plans/2026-08-16-wealth-features-restructure.md
  99. 1125 0
      docs/superpowers/plans/2026-08-22-health-start-plan.md
  100. 1325 0
      docs/superpowers/plans/2026-08-23-coach-butler.md

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

@@ -3006,6 +3006,24 @@ log.info("已添加template_id列到tasks表");
 		} catch (Exception e) {
 			log.warn("finance_checkins.type 添加失败(可能已存在): " + e.getMessage());
 		}
+		// 迁移255: 创建energy_daily_limit表(每日能量上限配置持久化)
+		try {
+			jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS energy_daily_limit (" +
+					"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+					"dimension_code VARCHAR(20) NOT NULL COMMENT '维度代码: body/mind/wisdom/action/wealth', " +
+					"daily_limit INT NOT NULL DEFAULT 50 COMMENT '每日能量上限', " +
+					"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+					"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+					"UNIQUE KEY uk_dimension_code (dimension_code)" +
+					") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='每日能量上限配置'");
+			log.info("已创建energy_daily_limit表");
+			// 插入默认数据(幂等)
+			jdbcTemplate.execute("INSERT IGNORE INTO energy_daily_limit (dimension_code, daily_limit) VALUES " +
+					"('body', 50), ('mind', 50), ('wisdom', 80), ('action', 60), ('wealth', 50)");
+			log.info("已插入energy_daily_limit默认数据");
+		} catch (Exception ex) {
+			log.warn("energy_daily_limit 表创建失败(可能已存在): " + ex.getMessage());
+		}
         ensureColumn("file_record", "file_type", "VARCHAR(50) DEFAULT NULL COMMENT '文件类型: image/pdf/video/audio/other'");
         ensureColumn("file_record", "description", "TEXT COMMENT '文件描述'");
 
@@ -9045,6 +9063,126 @@ private void runMigration100() {
 		// 迁移251: 健康饮食组挂载 报告管理/报告审核/应季食材(《角色权限与菜单归类整合方案》第二部分,幂等)
 		migrateHealthMenuMount();
 
+		// 迁移252: 创建用户画像表
+		try {
+			jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS profile_snapshot (" +
+					"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+					"member_id BIGINT NOT NULL UNIQUE, " +
+					"dimension_scores JSON DEFAULT NULL, " +
+					"body_metrics JSON DEFAULT NULL, " +
+					"mind_metrics JSON DEFAULT NULL, " +
+					"wisdom_metrics JSON DEFAULT NULL, " +
+					"action_metrics JSON DEFAULT NULL, " +
+					"wealth_metrics JSON DEFAULT NULL, " +
+					"problem_domains VARCHAR(500) DEFAULT NULL, " +
+					"computed_at DATETIME DEFAULT NULL, " +
+					"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+					"INDEX idx_member_id (member_id)" +
+					") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
+			log.info("已创建profile_snapshot表");
+		} catch (Exception e) {
+			log.warn("创建profile_snapshot表可能已存在: {}", e.getMessage());
+		}
+
+		try {
+			jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS profile_history (" +
+					"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+					"member_id BIGINT NOT NULL, " +
+					"snapshot_date DATE NOT NULL, " +
+					"all_metrics JSON NOT NULL, " +
+					"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+					"INDEX idx_member_date (member_id, snapshot_date)" +
+					") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
+			log.info("已创建profile_history表");
+		} catch (Exception e) {
+			log.warn("创建profile_history表可能已存在: {}", e.getMessage());
+		}
+
+		// 迁移254: 创建 report_template 表(报告展示模板配置)
+		try {
+			jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS report_template (" +
+				"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+				"name VARCHAR(100) NOT NULL COMMENT '模板名称', " +
+				"report_type VARCHAR(50) NOT NULL COMMENT '报告类型: gut_flora/dan/physical_exam/tongue', " +
+				"config JSON NOT NULL COMMENT '模板配置JSON: fixedBlocks+freeDisplay', " +
+				"is_active TINYINT DEFAULT 1 COMMENT '0=停用 1=启用', " +
+				"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+				"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+				"UNIQUE KEY uk_report_type (report_type)" +
+				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报告展示模板'");
+			log.info("已创建report_template表");
+		} catch (Exception e) {
+			log.warn("report_template表可能已存在: {}", e.getMessage());
+		}
+
+		// 迁移255: report_unknown_upload 添加 annotation 列(管理员标注训练数据)
+		ensureColumn("report_unknown_upload", "annotation", "TEXT COMMENT '管理员标注: 手动指定类型+确认提取结果JSON'");
+
+		// 迁移256: social_circle 添加圈子管理字段
+		ensureColumn("social_circle", "description", "VARCHAR(500) NULL COMMENT '圈子描述'");
+		ensureColumn("social_circle", "cover_image", "VARCHAR(500) NULL COMMENT '封面图URL'");
+		ensureColumn("social_circle", "join_condition", "TINYINT(1) DEFAULT 0 COMMENT '加入条件: 0=公开/1=审核/2=邀请'");
+		ensureColumn("social_circle", "review_required", "TINYINT(1) DEFAULT 0 COMMENT '是否需要审核: 0=否/1=是'");
+		ensureColumn("social_circle", "is_public", "TINYINT(1) DEFAULT 1 COMMENT '是否公开可见: 0=否/1=是'");
+
+		// 迁移257: social_circle_member 添加审核状态和角色字段
+		ensureColumn("social_circle_member", "status", "VARCHAR(20) DEFAULT 'approved' COMMENT '申请状态: pending/approved/rejected'");
+		ensureColumn("social_circle_member", "role", "VARCHAR(20) DEFAULT 'member' COMMENT '角色: member/admin'");
+
+		// 迁移258: 创建圈子动态帖子表
+		try {
+			jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS social_circle_post (" +
+					"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+					"circle_id BIGINT NOT NULL COMMENT '圈子ID', " +
+					"member_id BIGINT NOT NULL COMMENT '发布者成员ID', " +
+					"member_type VARCHAR(20) NOT NULL COMMENT 'parent/child', " +
+					"type VARCHAR(50) NOT NULL COMMENT '帖子类型: checkin/diet/exercise/sleep/water/mood/text', " +
+					"content TEXT COMMENT '文字内容', " +
+					"checkin_id BIGINT COMMENT '关联打卡记录ID', " +
+					"image_urls TEXT COMMENT '图片URL列表JSON', " +
+					"like_count INT DEFAULT 0 COMMENT '点赞数', " +
+					"comment_count INT DEFAULT 0 COMMENT '评论数', " +
+					"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+					"deleted TINYINT(1) DEFAULT 0, " +
+					"INDEX idx_circle_time (circle_id, created_at), " +
+					"INDEX idx_member_time (member_id, created_at)" +
+					") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='圈子动态帖子'");
+			log.info("已创建social_circle_post表");
+		} catch (Exception e) { log.warn("social_circle_post表可能已存在"); }
+
+		// 迁移259: 创建圈子点赞表
+		try {
+			jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS social_circle_like (" +
+					"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+					"circle_id BIGINT NOT NULL, " +
+					"post_id BIGINT NOT NULL COMMENT '被点赞的帖子ID', " +
+					"member_id BIGINT NOT NULL COMMENT '点赞成员ID', " +
+					"member_type VARCHAR(20) NOT NULL, " +
+					"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+					"UNIQUE KEY uk_like (circle_id, post_id, member_id)" +
+					") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='圈子点赞记录'");
+			log.info("已创建social_circle_like表");
+		} catch (Exception e) { log.warn("social_circle_like表可能已存在"); }
+
+		// 迁移260: 创建圈子评论表
+		try {
+			jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS social_circle_comment (" +
+					"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+					"circle_id BIGINT NOT NULL, " +
+					"post_id BIGINT NOT NULL COMMENT '所属帖子ID', " +
+					"parent_id BIGINT DEFAULT NULL COMMENT '回复的评论ID(NULL=一级评论)', " +
+					"member_id BIGINT NOT NULL COMMENT '评论者成员ID', " +
+					"member_type VARCHAR(20) NOT NULL, " +
+					"content TEXT NOT NULL COMMENT '评论内容', " +
+					"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+					"INDEX idx_post_time (post_id, created_at)" +
+					") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='圈子评论记录'");
+			log.info("已创建social_circle_comment表");
+		} catch (Exception e) { log.warn("social_circle_comment表可能已存在"); }
+
+		// 迁移261: butler_profiles 添加停接新单开关(管家自助选择功能)
+		ensureColumn("butler_profiles", "accepting",
+				"TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否接受新家庭绑定: 0=停接/1=接单'");
 	}
 
 	/**

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

@@ -1,6 +1,7 @@
 package com.etotem.cfc.controller;
 
 import com.etotem.cfc.common.Result;
+import com.etotem.cfc.service.PurchaseService;
 import com.etotem.cfc.entity.HealthPlan;
 import com.etotem.cfc.service.HealthPlanService;
 import io.swagger.v3.oas.annotations.Operation;
@@ -20,11 +21,20 @@ public class HealthPlanController {
     @Resource
     private HealthPlanService healthPlanService;
 
+    @Resource
+    private com.etotem.cfc.service.PurchaseService purchaseService;
+
     @Operation(summary = "生成健康方案(文本)")
     @PostMapping("/generate")
     public Result<String> generate(@RequestBody Map<String, Object> params,
                                    @RequestAttribute("userId") Long userId,
                                    @RequestAttribute(value = "familyId", required = false) Long familyId) {
+        // 付费墙拦截
+        java.util.Map<String, Object> purchaseCheck = purchaseService.checkPurchase(userId);
+        if (!Boolean.TRUE.equals(purchaseCheck.get("isActive"))) {
+            return Result.error("请先购买健康启航计划体验包");
+        }
+        
         String memberIds = (String) params.get("memberIds");
         String dimensions = (String) params.get("dimensions");
         String goal = (String) params.get("goal");

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

@@ -1,6 +1,7 @@
 package com.etotem.cfc.controller;
 
 import com.etotem.cfc.common.Result;
+import com.etotem.cfc.service.PurchaseService;
 import com.etotem.cfc.dto.HealthAnalysisResult;
 import com.etotem.cfc.dto.MemberMatchResult;
 import com.etotem.cfc.dto.ParsedDiseaseRisk;
@@ -155,6 +156,9 @@ public class HealthReportController {
     @Resource
     private ReportBlockAssembler reportBlockAssembler;
 
+    @Resource
+    private com.etotem.cfc.service.PurchaseService purchaseService;
+
     /**
      * 创建健康报告(含指标明细)
      */
@@ -166,6 +170,12 @@ public class HealthReportController {
             @RequestAttribute(value = "familyId", required = false) Long familyId,
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
 
+        // 付费墙拦截:检查是否购买健康启航计划
+        java.util.Map<String, Object> purchaseCheck = purchaseService.checkPurchase(userId);
+        if (!Boolean.TRUE.equals(purchaseCheck.get("isActive"))) {
+            return Result.error("请先购买健康启航计划体验包");
+        }
+
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }
@@ -260,26 +270,29 @@ public class HealthReportController {
 
     /**
      * 获取孩子的所有健康报告列表
+     * 未传 memberId 时返回当前家庭的全部报告;传 memberId 时按成员过滤(兼容 users.id / family_members.id)
      */
     @Operation(summary = "获取健康报告列表")
     @PostMapping("/report/list")
     public Result<List<HealthReport>> listReports(
             @RequestBody Map<String, Object> params,
-            @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId,
             @RequestAttribute(value = "familyId", required = false) Long familyId) {
-        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
+        // 返回条数:显式传 size 则截取前 size 条;不传则返回全部分页列表
+        int size = params.get("size") != null ? Integer.parseInt(params.get("size").toString()) : 0;
+        Long memberId = ParamUtils.getLong(params.get("memberId"));
+        List<HealthReport> reports;
         if (memberId != null) {
-            List<HealthReport> reports = healthReportService.getUserReports(memberId);
-            // 小程序健康页仅展示最近 3 条,后台只返回 3 条,避免前端截断与多余数据传输
-            return Result.success(limitLatest(reports, 3));
+            reports = healthReportService.getMemberReports(memberId);
+        } else if (familyId != null) {
+            // 未指定 memberId 时,返回当前家庭的全部报告
+            reports = healthReportService.getFamilyReports(familyId);
+        } else {
+            return Result.error("无法获取家庭信息,请先创建或加入家庭");
         }
-        // 未指定 memberId 时,回退到家庭报告列表
-        if (familyId != null) {
-            List<HealthReport> reports = healthReportService.getFamilyReports(familyId);
-            // 同上,仅返回最近 3 条
-            return Result.success(limitLatest(reports, 3));
+        if (size > 0) {
+            return Result.success(limitLatest(reports, size));
         }
-        return Result.error("memberId不能为空");
+        return Result.success(reports);
     }
 
     /**

+ 73 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/PurchaseController.java

@@ -0,0 +1,73 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.service.PurchaseService;
+import com.etotem.cfc.service.ChallengeService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/purchase")
+public class PurchaseController {
+
+    @Autowired private PurchaseService purchaseService;
+    @Autowired private ChallengeService challengeService;
+
+    @PostMapping("/check")
+    public Result<Map<String, Object>> checkPurchase(HttpServletRequest request) {
+        Long userId = (Long) request.getAttribute("userId");
+        Map<String, Object> result = purchaseService.checkPurchase(userId);
+        return Result.success(result);
+    }
+
+    @PostMapping("/create")
+    public Result<Map<String, Object>> createOrder(HttpServletRequest request) {
+        Long userId = (Long) request.getAttribute("userId");
+        Long familyId = (Long) request.getAttribute("familyId");
+        Map<String, Object> result = purchaseService.createOrder(userId, familyId);
+        return Result.success(result);
+    }
+
+    @PostMapping("/confirm")
+    public Result<Map<String, Object>> confirmPayment(@RequestBody Map<String, Object> params, HttpServletRequest request) {
+        String orderNo = (String) params.get("orderNo");
+        String paymentMethod = (String) params.get("paymentMethod");
+        String transactionId = (String) params.get("transactionId");
+        Map<String, Object> result = purchaseService.confirmPayment(orderNo, paymentMethod, transactionId);
+        return Result.success(result);
+    }
+
+    @PostMapping("/renewal-info")
+    public Result<Map<String, Object>> getRenewalInfo(HttpServletRequest request) {
+        Long userId = (Long) request.getAttribute("userId");
+        Map<String, Object> result = purchaseService.getRenewalInfo(userId);
+        return Result.success(result);
+    }
+
+    @PostMapping("/renew")
+    public Result<Map<String, Object>> renew(@RequestBody Map<String, Object> params, HttpServletRequest request) {
+        Long userId = (Long) request.getAttribute("userId");
+        Long familyId = (Long) request.getAttribute("familyId");
+        Integer amount = params.get("amount") != null ? ((Number) params.get("amount")).intValue() : null;
+        Map<String, Object> result = purchaseService.processRenewal(userId, familyId, amount);
+        return Result.success(result);
+    }
+
+    @GetMapping("/challenge/progress")
+    public Result<Map<String, Object>> getChallengeProgress(@RequestParam Long memberId, HttpServletRequest request) {
+        Map<String, Object> result = challengeService.getChallengeProgress(memberId);
+        return Result.success(result);
+    }
+
+    @PostMapping("/challenge/update")
+    public Result<Void> updateChallengeProgress(@RequestBody Map<String, Object> params, HttpServletRequest request) {
+        Long memberId = (Long) params.get("memberId");
+        String taskKey = (String) params.get("taskKey");
+        int currentValue = ((Number) params.get("currentValue")).intValue();
+        challengeService.updateTaskProgress(memberId, taskKey, currentValue);
+        return Result.success(null);
+    }
+}

+ 45 - 31
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminEnergyConfigController.java

@@ -2,7 +2,9 @@ package com.etotem.cfc.controller.admin;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.DimensionDailyLimit;
 import com.etotem.cfc.entity.EnergySourceConfig;
+import com.etotem.cfc.mapper.DimensionDailyLimitMapper;
 import com.etotem.cfc.mapper.EnergySourceConfigMapper;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -23,6 +25,9 @@ public class AdminEnergyConfigController {
     @Resource
     private EnergySourceConfigMapper energySourceConfigMapper;
 
+    @Resource
+    private DimensionDailyLimitMapper dimensionDailyLimitMapper;
+
     /**
      * 能量来源配置列表
      */
@@ -95,27 +100,49 @@ public class AdminEnergyConfigController {
     }
 
     /**
-     * 获取每日能量限制(V1 返回硬编码值)
+     * 获取每日能量限制(数据库持久化)
      */
     @Operation(summary = "获取每日能量限制")
     @PostMapping("/daily-limits")
     public Result<List<DimensionDailyLimit>> dailyLimits(@RequestBody Map<String, Object> params) {
-        List<DimensionDailyLimit> limits = new ArrayList<>();
-        limits.add(new DimensionDailyLimit("body", 50));
-        limits.add(new DimensionDailyLimit("mind", 50));
-        limits.add(new DimensionDailyLimit("wisdom", 80));
-        limits.add(new DimensionDailyLimit("action", 60));
-        limits.add(new DimensionDailyLimit("wealth", 50));
+        List<DimensionDailyLimit> limits = dimensionDailyLimitMapper.selectList(
+            new LambdaQueryWrapper<DimensionDailyLimit>()
+                .orderByAsc(DimensionDailyLimit::getDimensionCode)
+        );
         return Result.success(limits);
     }
 
     /**
-     * 保存每日能量限制(V1 仅返回成功,实际存储待后续迁移)
+     * 保存每日能量限制(数据库持久化)
      */
     @Operation(summary = "保存每日能量限制")
     @PostMapping("/daily-limit-save")
     public Result<String> dailyLimitSave(@RequestBody DailyLimitSaveRequest request) {
-        // V1: 仅返回成功,实际存储待后续迁移到数据库
+        if (request.getLimits() == null || request.getLimits().isEmpty()) {
+            return Result.error("limits不能为空");
+        }
+        Date now = new Date();
+        for (DimensionDailyLimit dto : request.getLimits()) {
+            String code = dto.getDimensionCode();
+            Integer limit = dto.getDailyLimit();
+            if (code == null || limit == null) {
+                return Result.error("dimensionCode和dailyLimit不能为空");
+            }
+            DimensionDailyLimit existing = dimensionDailyLimitMapper.selectOne(
+                new LambdaQueryWrapper<DimensionDailyLimit>().eq(DimensionDailyLimit::getDimensionCode, code));
+            if (existing != null) {
+                existing.setDailyLimit(limit);
+                existing.setUpdatedAt(now);
+                dimensionDailyLimitMapper.updateById(existing);
+            } else {
+                DimensionDailyLimit entity = new DimensionDailyLimit();
+                entity.setDimensionCode(code);
+                entity.setDailyLimit(limit);
+                entity.setCreatedAt(now);
+                entity.setUpdatedAt(now);
+                dimensionDailyLimitMapper.insert(entity);
+            }
+        }
         return Result.success("保存成功");
     }
 
@@ -162,13 +189,16 @@ public class AdminEnergyConfigController {
         Map<String, Object> data = new HashMap<>();
         data.put("sources", sources);
 
-        // 每日限制
+        // 每日限制(从数据库读取)
         List<Map<String, Object>> dailyLimits = new ArrayList<>();
-        dailyLimits.add(limitMap("body", 50));
-        dailyLimits.add(limitMap("mind", 50));
-        dailyLimits.add(limitMap("wisdom", 80));
-        dailyLimits.add(limitMap("action", 60));
-        dailyLimits.add(limitMap("wealth", 50));
+        List<DimensionDailyLimit> limitsFromDb = dimensionDailyLimitMapper.selectList(
+            new LambdaQueryWrapper<DimensionDailyLimit>().orderByAsc(DimensionDailyLimit::getDimensionCode));
+        for (DimensionDailyLimit limit : limitsFromDb) {
+            Map<String, Object> m = new HashMap<>();
+            m.put("dimensionCode", limit.getDimensionCode());
+            m.put("dailyLimit", limit.getDailyLimit());
+            dailyLimits.add(m);
+        }
         data.put("dailyLimits", dailyLimits);
 
         return Result.success(data);
@@ -212,22 +242,6 @@ public class AdminEnergyConfigController {
         public void setRatio(Integer ratio) { this.ratio = ratio; }
     }
 
-    public static class DimensionDailyLimit {
-        private String dimensionCode;
-        private Integer dailyLimit;
-
-        public DimensionDailyLimit() {}
-        public DimensionDailyLimit(String dimensionCode, Integer dailyLimit) {
-            this.dimensionCode = dimensionCode;
-            this.dailyLimit = dailyLimit;
-        }
-
-        public String getDimensionCode() { return dimensionCode; }
-        public void setDimensionCode(String dimensionCode) { this.dimensionCode = dimensionCode; }
-        public Integer getDailyLimit() { return dailyLimit; }
-        public void setDailyLimit(Integer dailyLimit) { this.dailyLimit = dailyLimit; }
-    }
-
     public static class DailyLimitSaveRequest {
         private List<DimensionDailyLimit> limits;
 

+ 22 - 22
cfc-backend/src/main/java/com/etotem/cfc/controller/butler/ButlerController.java

@@ -83,27 +83,27 @@ public class ButlerController {
         return Result.success(data);
     }
 
-/**
- * 获取服务记录
- * POST /api/butler/service-records
- */
-@Deprecated
-@Operation(summary = "获取服务记录", deprecated = true)
-@PostMapping("/service-records")
-public Result<Map<String, Object>> getServiceRecords(@RequestBody Map<String, Object> params,
-@RequestAttribute("userId") Long userId) {
-throw new UnsupportedOperationException("管家服务记录功能暂未开放");
-}
+    /**
+     * 获取服务记录(已废弃)
+     * POST /api/butler/service-records
+     */
+    @Deprecated
+    @Operation(summary = "获取服务记录", deprecated = true)
+    @PostMapping("/service-records")
+    public Result<Map<String, Object>> getServiceRecords(@RequestBody Map<String, Object> params,
+    @RequestAttribute("userId") Long userId) {
+    return Result.error(410, "该接口已废弃,无替代方案");
+    }
 
-/**
- * 获取佣金记录
- * POST /api/butler/commissions
- */
-@Deprecated
-@Operation(summary = "获取佣金记录", deprecated = true)
-@PostMapping("/commissions")
-public Result<Map<String, Object>> getCommissions(@RequestBody Map<String, Object> params,
-@RequestAttribute("userId") Long userId) {
-throw new UnsupportedOperationException("管家佣金功能暂未开放");
-}
+    /**
+     * 获取佣金记录(已废弃)
+     * POST /api/butler/commissions
+     */
+    @Deprecated
+    @Operation(summary = "获取佣金记录", deprecated = true)
+    @PostMapping("/commissions")
+    public Result<Map<String, Object>> getCommissions(@RequestBody Map<String, Object> params,
+    @RequestAttribute("userId") Long userId) {
+    return Result.error(410, "该接口已废弃,无替代方案");
+    }
 }

+ 46 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/butler/FamilyButlerSelectController.java

@@ -0,0 +1,46 @@
+package com.etotem.cfc.controller.butler;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.service.ButlerService;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 家庭端管家自助选择(仅 L2 绑定受限,浏览开放)
+ */
+@RestController
+@RequestMapping("/api/butler")
+public class FamilyButlerSelectController {
+
+    @Resource
+    private ButlerService butlerService;
+
+    @PostMapping("/available-list")
+    public Result<List<Map<String, Object>>> availableList() {
+        return butlerService.getAvailableButlers();
+    }
+
+    @PostMapping("/my-butler")
+    public Result<Map<String, Object>> myButler(@RequestAttribute("userId") Long userId) {
+        return butlerService.getMyButler(userId);
+    }
+
+    @PostMapping("/select")
+    public Result<Map<String, Object>> select(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> params) {
+        Object butlerUserIdObj = params.get("butlerUserId");
+        if (butlerUserIdObj == null || butlerUserIdObj.toString().trim().isEmpty()) {
+            return Result.error("缺少 butlerUserId 参数");
+        }
+        Long butlerUserId = Long.valueOf(butlerUserIdObj.toString());
+        return butlerService.selectButler(userId, butlerUserId);
+    }
+}

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyController.java

@@ -36,6 +36,7 @@ public class FamilyController {
      * 获取当前家庭的邀请码(已废弃,请使用邀请令牌系统)
      */
     @Operation(summary = "获取邀请码(已废弃)")
+    @Deprecated
     @PostMapping("/invite-code")
     public Result<String> getInviteCode() {
         return Result.error(410, "该接口已废弃,请使用 /api/family/invite/generate 生成邀请令牌");
@@ -45,6 +46,7 @@ public class FamilyController {
      * 生成/重置邀请码(已废弃,请使用邀请令牌系统)
      */
     @Operation(summary = "生成邀请码(已废弃)")
+    @Deprecated
     @PostMapping("/invite-code/generate")
     public Result<String> generateInviteCode() {
         return Result.error(410, "该接口已废弃,请使用 /api/family/invite/generate 生成邀请令牌");

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

@@ -90,11 +90,12 @@ public class FamilyUserController {
     }
 
     @Operation(summary = "切换家长/孩子模式(已废弃,使用 /api/family/member/switch)")
+    @Deprecated
     @PostMapping("/switch-mode")
     public Result<String> switchMode(@RequestAttribute("userId") Long userId,
-        @RequestBody Map<String, String> body) {
+        @RequestBody Map<String, String> body) {
         return Result.error(410, "该接口已废弃,请使用 /api/family/member/switch");
-    }
+    }
 
     @Operation(summary = "切换用户角色 (支持 parent/child/teacher)")
     @PostMapping("/switch-role")
@@ -116,36 +117,27 @@ public class FamilyUserController {
     }
 
     @Operation(summary = "家长切换到指定孩子身份(已废弃,使用 /api/family/member/switch)")
+    @Deprecated
     @PostMapping("/switch-to-child")
     public Result<Long> switchToFamilyMember(@RequestAttribute("userId") Long userId,
         @RequestBody Map<String, Long> body) {
         return Result.error(410, "该接口已废弃,请使用 /api/family/member/switch");
     }
 
-    @Operation(summary = "孩子切换回家长身份")
+    @Operation(summary = "孩子切换回家长身份(已废弃,使用 /api/family/member/switch)")
+    @Deprecated
     @PostMapping("/switch-back-to-parent")
     public Result<Boolean> switchBackToParent(@RequestAttribute("userId") Long userId) {
-        boolean result = userService.switchBackToParent(userId);
-        return Result.success(result);
+        return Result.error(410, "该接口已废弃,请使用 /api/family/member/switch");
     }
 
-    @Operation(summary = "带密码校验切换回家长视角")
+    @Operation(summary = "带密码校验切换回家长视角(已废弃,使用 /api/family/member/switch)")
+    @Deprecated
     @PostMapping("/switch-back-verify")
     public Result<Boolean> switchBackVerify(@RequestAttribute("userId") Long userId,
-        @RequestBody Map<String, String> body) {
-        if (userId == null) {
-            return Result.error("请先登录");
-        }
-        String password = body.get("password");
-        if (password == null || password.isEmpty()) {
-            return Result.error("请输入密码");
-        }
-        boolean verified = userService.verifySwitchBack(userId, password);
-        if (!verified) {
-            return Result.error("密码错误");
-        }
-        return Result.success(true);
-    }
+        @RequestBody Map<String, String> body) {
+        return Result.error(410, "该接口已废弃,请使用 /api/family/member/switch");
+    }
 
     @Operation(summary = "创建孩子")
     @PostMapping("/children")

+ 0 - 5
cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideController.java

@@ -103,11 +103,6 @@ public class GuideController {
         return Result.success(guidePackageService.getPackagesByLevel(level));
     }
 
-    @PostMapping("/package/templates")
-    public Result<List<GuidePackage>> getPackageTemplates() {
-        return Result.success(guidePackageService.getActivePackages());
-    }
-
     @PostMapping("/parent-invite/generate")
     public Result<Map<String, Object>> generateParentInvite(@RequestBody Map<String, Object> params,
         @RequestAttribute("userId") Long userId,

+ 0 - 151
cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuidePackageController.java

@@ -1,151 +0,0 @@
-package com.etotem.cfc.controller.guide;
-
-import com.etotem.cfc.common.Result;
-import com.etotem.cfc.dto.GuidePackageDTO;
-import com.etotem.cfc.entity.GuidePackage;
-import com.etotem.cfc.entity.GuidePackageTemplate;
-import com.etotem.cfc.service.GuidePackageService;
-import com.etotem.cfc.service.GuidePackageTemplateService;
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import org.springframework.web.bind.annotation.*;
-
-import javax.annotation.Resource;
-import java.util.List;
-import java.util.Map;
-import com.etotem.cfc.util.ParamUtils;
-
-@Tag(name = "成长规划师任务模板", description = "成长规划师任务模板管理接口")
-@RestController
-@RequestMapping("/api/guide/packages")
-public class GuidePackageController {
-
-    @Resource
-    private GuidePackageService guidePackageService;
-
-    @Resource
-    private GuidePackageTemplateService guidePackageTemplateService;
-
-    @PostMapping("/my")
-    @Operation(summary = "获取我的任务模板")
-    public Result<List<GuidePackage>> getMyPackagesAlias(@RequestAttribute("userId") Long guideId) {
-        List<GuidePackage> packages = guidePackageService.getGuidePackages(guideId);
-        return Result.success(packages);
-    }
-
-    @PostMapping("/list")
-    @Operation(summary = "获取我的任务模板列表")
-    public Result<List<GuidePackage>> getMyPackages(@RequestAttribute("userId") Long guideId) {
-        List<GuidePackage> packages = guidePackageService.getGuidePackages(guideId);
-        return Result.success(packages);
-    }
-
-    @PostMapping("/detail")
-    @Operation(summary = "获取任务模板详情")
-    public Result<GuidePackage> getPackageDetail(@RequestBody Map<String, Object> params) {
-        Long id = ParamUtils.getLong(params.get("id"));
-        GuidePackage pkg = guidePackageService.getById(id);
-        if (pkg == null) {
-            return Result.error("任务模板不存在");
-        }
-        return Result.success(pkg);
-    }
-
-    @PostMapping("/create")
-    @Operation(summary = "创建任务模板")
-    public Result<GuidePackage> createPackage(@RequestAttribute("userId") Long guideId,
-                                               @RequestBody GuidePackageDTO dto) {
-        GuidePackage pkg = new GuidePackage();
-        pkg.setGuideId(guideId);
-        pkg.setPackageId(dto.getTemplateId());
-        pkg.setName(dto.getName());
-        pkg.setPrice(dto.getPrice());
-        pkg.setDescription(dto.getDescription());
-        pkg.setValidityDays(dto.getValidityMonths());
-        pkg.setCommissionRate(dto.getCommissionRate());
-        pkg.setSortOrder(0);
-        pkg.setStatus("DRAFT");
-        boolean success = guidePackageService.save(pkg);
-        if (success) {
-            return Result.success(pkg);
-        }
-        return Result.error("创建失败");
-    }
-
-    @PostMapping("/update")
-    @Operation(summary = "更新任务模板")
-    public Result<GuidePackage> updatePackage(@RequestAttribute("userId") Long guideId,
-                                               @RequestBody Map<String, Object> params) {
-        Long id = ParamUtils.getLong(params.get("id"));
-        GuidePackage existing = guidePackageService.getById(id);
-        if (existing == null) {
-            return Result.error("任务模板不存在");
-        }
-        if (params.get("name") != null) {
-            existing.setName(params.get("name").toString());
-        }
-        if (params.get("price") != null) {
-            existing.setPrice(Integer.valueOf(params.get("price").toString()));
-        }
-        guidePackageService.updateById(existing);
-        return Result.success(existing);
-    }
-
-    @PostMapping("/update-status")
-    @Operation(summary = "更新任务模板状态")
-    public Result<Boolean> updateStatus(@RequestAttribute("userId") Long guideId,
-                                        @RequestBody Map<String, Object> params) {
-        Long id = ParamUtils.getLong(params.get("id"));
-        String status = params.get("status").toString();
-        GuidePackage existing = guidePackageService.getById(id);
-        if (existing == null) {
-            return Result.error("任务模板不存在");
-        }
-        if (!"DRAFT".equals(status) && !"PUBLISHED".equals(status) && !"PAUSED".equals(status)) {
-            return Result.error("无效状态");
-        }
-        existing.setStatus(status);
-        guidePackageService.updateById(existing);
-        return Result.success(true);
-    }
-
-    @PostMapping("/delete")
-    @Operation(summary = "删除任务模板")
-    public Result<Boolean> deletePackage(@RequestAttribute("userId") Long guideId,
-                                         @RequestBody Map<String, Object> params) {
-        Long id = ParamUtils.getLong(params.get("id"));
-        GuidePackage existing = guidePackageService.getById(id);
-        if (existing == null) {
-            return Result.error("任务模板不存在");
-        }
-        existing.setStatus("DELETED");
-        guidePackageService.updateById(existing);
-        return Result.success(true);
-    }
-
-    @PostMapping("/templates")
-    @Operation(summary = "获取任务模板模板列表")
-    public Result<List<GuidePackageTemplate>> getTemplates() {
-        List<GuidePackageTemplate> templates = guidePackageTemplateService.list();
-        return Result.success(templates);
-    }
-
-    @PostMapping("/template-detail")
-    @Operation(summary = "获取模板详情")
-    public Result<GuidePackageTemplate> getTemplateDetail(@RequestBody Map<String, Object> params) {
-        Long id = ParamUtils.getLong(params.get("id"));
-        GuidePackageTemplate template = guidePackageTemplateService.getById(id);
-        if (template == null) {
-            return Result.error("模板不存在");
-        }
-        return Result.success(template);
-    }
-
-    @PostMapping("/increment-sales")
-    @Operation(summary = "增加任务模板销量")
-    public Result<Boolean> incrementSales(@RequestBody Map<String, Object> params) {
-        Long id = ParamUtils.getLong(params.get("id"));
-        boolean success = guidePackageService.incrementSalesCount(id);
-        return Result.success(success);
-    }
-}

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

@@ -190,7 +190,7 @@ public class PaymentController {
      * 生成支付二维码图片
      */
     @Operation(summary = "生成支付二维码")
-    @GetMapping("/qrcode")
+    @PostMapping("/qrcode")
     public void generateQrCode(String text, HttpServletResponse response) throws Exception {
         int size = 280;
         Hashtable<EncodeHintType, Object> hints = new Hashtable<>();

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

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

+ 43 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/ButlerAssignment.java

@@ -0,0 +1,43 @@
+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;
+
+/**
+ * 会员-管家分配记录(butler_assignments 表唯一 Java 写入方;
+ * 订阅激活自动分配只增 member_count 不建行,本表行仅由家庭自助选择/换绑与管理端解绑维护)
+ */
+@Data
+@TableName("butler_assignments")
+public class ButlerAssignment implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 家庭 ID(唯一键 uk_family_assignment)
+     */
+    private Long familyId;
+
+    /**
+     * 管家用户 ID
+     */
+    private Long butlerUserId;
+
+    /**
+     * 关联订阅 ID
+     */
+    private Long subscriptionId;
+
+    /**
+     * 订阅级别 L1/L2
+     */
+    private String level;
+
+    private Date assignedAt;
+}

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

@@ -39,6 +39,8 @@ public class ButlerProfile implements Serializable {
 
     private Integer commissionRateL2;
 
+    private Integer accepting;
+
     private Date createdAt;
 
     private Date updatedAt;

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

@@ -0,0 +1,29 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("challenge_progress")
+public class ChallengeProgress implements Serializable {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long purchaseId;
+    private Long memberId;
+    private Integer weekNum;
+    private String taskType;
+    private String taskKey;
+    private String taskTitle;
+    private Integer targetValue;
+    private Integer currentValue;
+    private Integer rewardCf;
+    private String status;
+    private Date completedAt;
+    private Date createdAt;
+    private Date updatedAt;
+}

+ 27 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/DimensionDailyLimit.java

@@ -0,0 +1,27 @@
+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("energy_daily_limit")
+public class DimensionDailyLimit implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 维度代码: body/mind/wisdom/action/wealth */
+    private String dimensionCode;
+
+    /** 每日能量上限 */
+    private Integer dailyLimit;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

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

@@ -0,0 +1,29 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("purchase_orders")
+public class PurchaseOrder implements Serializable {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String orderNo;
+    private Long userId;
+    private Long familyId;
+    private Integer amount;
+    private String status;
+    private String paymentMethod;
+    private String transactionId;
+    private Date startDate;
+    private Date endDate;
+    private Integer isEarlyRenewal;
+    private Integer cfRewardGiven;
+    private Date createdAt;
+    private Date paidAt;
+}

+ 12 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/ButlerAssignmentMapper.java

@@ -0,0 +1,12 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.ButlerAssignment;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 会员-管家分配记录 Mapper(风格与 ButlerProfileMapper 一致)
+ */
+@Mapper
+public interface ButlerAssignmentMapper extends BaseMapper<ButlerAssignment> {
+}

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

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

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

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

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

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

+ 169 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ButlerService.java

@@ -18,6 +18,11 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import com.etotem.cfc.util.SortUtil;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.etotem.cfc.entity.ButlerAssignment;
+import com.etotem.cfc.mapper.ButlerAssignmentMapper;
+import com.etotem.cfc.service.MemberSubscriptionService;
+import com.etotem.cfc.entity.MemberSubscription;
 
 @Service
 public class ButlerService {
@@ -33,6 +38,12 @@ public class ButlerService {
     @Resource
     private UserService userService;
 
+    @Resource
+    private ButlerAssignmentMapper butlerAssignmentMapper;
+
+    @Resource
+    private MemberSubscriptionService memberSubscriptionService;
+
     /**
      * 申请成为管家
      * @param userId 申请人用户 ID
@@ -325,4 +336,162 @@ public class ButlerService {
 
         return butler.getUserId();
     }
+
+    // ==================== 家庭端自助选择 ====================
+
+    /**
+     * 可选管家列表(浏览开放,不校验会员等级)
+     * 条件:approved + tier=L2 + accepting=1 + 有余量;返回脱敏档案
+     */
+    public Result<List<Map<String, Object>>> getAvailableButlers() {
+        LambdaQueryWrapper<ButlerProfile> query = new LambdaQueryWrapper<>();
+        query.eq(ButlerProfile::getStatus, "approved")
+             .eq(ButlerProfile::getTier, "L2")
+             .eq(ButlerProfile::getAccepting, 1)
+             .apply("member_count < max_members")
+             .orderByAsc(ButlerProfile::getCreatedAt);
+        SortUtil.applySort(query);
+        List<ButlerProfile> profiles = butlerProfileMapper.selectList(query);
+
+        List<Long> userIds = new java.util.ArrayList<>();
+        for (ButlerProfile p : profiles) {
+            userIds.add(p.getUserId());
+        }
+        Map<Long, User> userMap = new HashMap<>();
+        if (!userIds.isEmpty()) {
+            for (User u : userMapper.selectBatchIds(userIds)) {
+                userMap.put(u.getId(), u);
+            }
+        }
+
+        List<Map<String, Object>> result = new java.util.ArrayList<>();
+        for (ButlerProfile p : profiles) {
+            User u = userMap.get(p.getUserId());
+            Map<String, Object> item = new HashMap<>();
+            item.put("butlerUserId", p.getUserId());
+            item.put("nickname", u != null ? u.getNickname() : "管家");
+            item.put("avatar", u != null ? u.getAvatar() : "");
+            item.put("description", p.getDescription());
+            item.put("certificates", p.getCertificates());
+            item.put("remainingSlots", (p.getMaxMembers() == null ? 0 : p.getMaxMembers())
+                    - (p.getMemberCount() == null ? 0 : p.getMemberCount()));
+            result.add(item);
+        }
+        return Result.success(result);
+    }
+
+    /**
+     * 当前家庭的管家绑定信息(无绑定时 data 为 null)
+     */
+    public Result<Map<String, Object>> getMyButler(Long userId) {
+        Long familyId = requireFamilyId(userId);
+        if (familyId == null) {
+            return Result.error("未找到家庭信息");
+        }
+        ButlerAssignment assignment = butlerAssignmentMapper.selectOne(
+                new LambdaQueryWrapper<ButlerAssignment>()
+                        .eq(ButlerAssignment::getFamilyId, familyId)
+                        .last("LIMIT 1"));
+        if (assignment == null) {
+            return Result.success(null);
+        }
+        return Result.success(buildButlerSummary(assignment));
+    }
+
+    /**
+     * 自助绑定/更换管家:L2 校验 + 条件扣容 + INSERT/UPDATE 双路径
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public Result<Map<String, Object>> selectButler(Long userId, Long butlerUserId) {
+        Long familyId = requireFamilyId(userId);
+        if (familyId == null) {
+            return Result.error("未找到家庭信息");
+        }
+        // ① L2 有效订阅校验
+        MemberSubscription sub = memberSubscriptionService.getActiveSubscription(familyId);
+        boolean active = sub != null && sub.getExpireTime() != null
+                && sub.getExpireTime().after(new Date());
+        if (!active || !"L2".equals(sub.getLevel())) {
+            return Result.error("该功能面向久久一生(L2)会员家庭");
+        }
+        // ② 目标管家校验
+        ButlerProfile butler = butlerProfileMapper.selectOne(
+                new LambdaQueryWrapper<ButlerProfile>()
+                        .eq(ButlerProfile::getUserId, butlerUserId)
+                        .eq(ButlerProfile::getStatus, "approved")
+                        .eq(ButlerProfile::getTier, "L2")
+                        .eq(ButlerProfile::getAccepting, 1)
+                        .last("LIMIT 1"));
+        if (butler == null) {
+            return Result.error("该管家不可选择");
+        }
+        // ③ 条件原子扣容(防并发超卖)
+        LambdaUpdateWrapper<ButlerProfile> incr = new LambdaUpdateWrapper<>();
+        incr.eq(ButlerProfile::getId, butler.getId())
+            .apply("member_count < max_members")
+            .setSql("member_count = member_count + 1");
+        int affected = butlerProfileMapper.update(null, incr);
+        if (affected == 0) {
+            return Result.error("该管家名额已满");
+        }
+        // ④ assignments INSERT/UPDATE 双路径(激活自动分配只加计数不建行)
+        ButlerAssignment existing = butlerAssignmentMapper.selectOne(
+                new LambdaQueryWrapper<ButlerAssignment>()
+                        .eq(ButlerAssignment::getFamilyId, familyId)
+                        .last("LIMIT 1"));
+        if (existing != null) {
+            // 旧管家计数回退
+            LambdaUpdateWrapper<ButlerProfile> decr = new LambdaUpdateWrapper<>();
+            decr.eq(ButlerProfile::getUserId, existing.getButlerUserId())
+                .setSql("member_count = GREATEST(member_count - 1, 0)");
+            butlerProfileMapper.update(null, decr);
+
+            existing.setButlerUserId(butlerUserId);
+            existing.setLevel(sub.getLevel());
+            existing.setAssignedAt(new Date());
+            butlerAssignmentMapper.updateById(existing);
+        } else {
+            ButlerAssignment na = new ButlerAssignment();
+            na.setFamilyId(familyId);
+            na.setButlerUserId(butlerUserId);
+            na.setSubscriptionId(sub.getId());
+            na.setLevel(sub.getLevel());
+            na.setAssignedAt(new Date());
+            butlerAssignmentMapper.insert(na);
+        }
+        log.info("Family {} selected butler {}, level={}", familyId, butlerUserId, sub.getLevel());
+
+        ButlerAssignment saved = butlerAssignmentMapper.selectOne(
+                new LambdaQueryWrapper<ButlerAssignment>()
+                        .eq(ButlerAssignment::getFamilyId, familyId)
+                        .last("LIMIT 1"));
+        return Result.success(buildButlerSummary(saved));
+    }
+
+    /**
+     * userId -> familyId(与 MembershipController 同源:users.family_id)
+     */
+    private Long requireFamilyId(Long userId) {
+        User user = userMapper.selectById(userId);
+        return user != null ? user.getFamilyId() : null;
+    }
+
+    /**
+     * 组装绑定摘要(脱敏:不含联系方式)
+     */
+    private Map<String, Object> buildButlerSummary(ButlerAssignment assignment) {
+        Map<String, Object> summary = new HashMap<>();
+        summary.put("butlerUserId", assignment.getButlerUserId());
+        summary.put("level", assignment.getLevel());
+        summary.put("assignedAt", assignment.getAssignedAt());
+        User bu = userMapper.selectById(assignment.getButlerUserId());
+        summary.put("nickname", bu != null ? bu.getNickname() : "管家");
+        summary.put("avatar", bu != null ? bu.getAvatar() : "");
+        ButlerProfile bp = butlerProfileMapper.selectOne(
+                new LambdaQueryWrapper<ButlerProfile>()
+                        .eq(ButlerProfile::getUserId, assignment.getButlerUserId())
+                        .last("LIMIT 1"));
+        summary.put("description", bp != null ? bp.getDescription() : "");
+        return summary;
+    }
 }

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

@@ -0,0 +1,14 @@
+package com.etotem.cfc.service;
+
+import java.util.Map;
+
+public interface ChallengeService {
+    /** 初始化闯关任务 */
+    void initChallengeTasks(Long purchaseId, Long memberId);
+    
+    /** 获取用户闯关进度 */
+    Map<String, Object> getChallengeProgress(Long memberId);
+    
+    /** 更新任务进度 */
+    void updateTaskProgress(Long memberId, String taskKey, int currentValue);
+}

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

@@ -206,6 +206,44 @@ public class HealthReportService {
         );
     }
 
+    /**
+     * 获取某家庭成员的所有健康报告
+     * 兼容 subject_id 的两种历史语义(users.id / family_members.id):
+     * 先通过 family_members 表归一该成员的两种 id,再用 IN 匹配 subject_id/user_id
+     */
+    public List<HealthReport> getMemberReports(Long memberId) {
+        if (memberId == null) {
+            return Collections.emptyList();
+        }
+        Set<Long> ids = new HashSet<>();
+        ids.add(memberId);
+        try {
+            FamilyMember fm = familyMemberMapper.selectById(memberId);
+            if (fm != null) {
+                ids.add(fm.getId());
+                if (fm.getUserId() != null) {
+                    ids.add(fm.getUserId());
+                }
+            }
+            // memberId 可能是 users.id,反查其关联的 family_members 行
+            List<FamilyMember> linked = familyMemberMapper.selectList(
+                    new LambdaQueryWrapper<FamilyMember>().eq(FamilyMember::getUserId, memberId));
+            for (FamilyMember item : linked) {
+                ids.add(item.getId());
+            }
+        } catch (Exception e) {
+            log.warn("归一家庭成员ID失败 memberId={}: {}", memberId, e.getMessage());
+        }
+        return healthReportMapper.selectList(
+                new LambdaQueryWrapper<HealthReport>()
+                        .and(w -> w.in(HealthReport::getSubjectId, ids)
+                                .or(w2 -> w2.isNull(HealthReport::getSubjectId)
+                                        .in(HealthReport::getUserId, ids)))
+                        .eq(HealthReport::getStatus, "active")
+                        .orderByDesc(HealthReport::getReportDate)
+        );
+    }
+
     /**
      * 获取某份报告的所有指标
      */

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

@@ -0,0 +1,20 @@
+package com.etotem.cfc.service;
+
+import java.util.Map;
+
+public interface PurchaseService {
+    /** 检查用户是否有有效购买 */
+    Map<String, Object> checkPurchase(Long userId);
+    
+    /** 创建购买订单 */
+    Map<String, Object> createOrder(Long userId, Long familyId);
+    
+    /** 确认支付 */
+    Map<String, Object> confirmPayment(String orderNo, String paymentMethod, String transactionId);
+    
+    /** 获取续费信息 */
+    Map<String, Object> getRenewalInfo(Long userId);
+    
+    /** 处理续费(真实支付) */
+    Map<String, Object> processRenewal(Long userId, Long familyId, Integer amount);
+}

+ 15 - 14
cfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java

@@ -172,6 +172,7 @@ public class TaskService implements TaskServiceInterface {
         
         if ("child".equals(executorType)) {
             task.setExecutorId(memberId);
+            task.setFamilyMemberId(memberId);
             task.setChildId(memberId);
         } else if ("member".equals(executorType)) {
             task.setExecutorId(memberId);
@@ -317,7 +318,7 @@ public List<Task> getTodayTasks(Long memberId, String dimensionCode) {
         Date endOfDay = calendar.getTime();
 
         LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
-            .eq(Task::getChildId, memberId)
+            .eq(Task::getFamilyMemberId, memberId)
             .ge(Task::getDeadline, startOfDay)
             .lt(Task::getDeadline, endOfDay)
             .in(Task::getStatus, Arrays.asList("pending", "completed", "locked"))
@@ -381,7 +382,7 @@ public List<Task> getTodayTasks(Long memberId, String dimensionCode) {
         Date endOfDay = calendar.getTime();
 
         return filterLockedChildren(taskMapper.selectList(new LambdaQueryWrapper<Task>()
-                .eq(Task::getChildId, memberId)
+                .eq(Task::getFamilyMemberId, memberId)
                 .eq(Task::getCategory, category)
                 .ge(Task::getDeadline, startOfDay)
                 .lt(Task::getDeadline, endOfDay)
@@ -413,7 +414,7 @@ public List<Task> getTodayTasks(Long memberId, String dimensionCode) {
 
         List<Task> allTasks = taskMapper.selectList(
             new LambdaQueryWrapper<Task>()
-                .eq(Task::getChildId, memberId)
+                .eq(Task::getFamilyMemberId, memberId)
                 .between(Task::getDeadline, todayStart, todayEnd)
         );
 
@@ -425,7 +426,7 @@ public List<Task> getTodayTasks(Long memberId, String dimensionCode) {
 
         List<Task> pendingReview = taskMapper.selectList(
             new LambdaQueryWrapper<Task>()
-                .eq(Task::getChildId, memberId)
+                .eq(Task::getFamilyMemberId, memberId)
                 .eq(Task::getNeedReview, 1)
                 .eq(Task::getStatus, "pending")
         );
@@ -450,7 +451,7 @@ public List<Task> getTodayTasks(Long memberId, String dimensionCode) {
     @Transactional
     public Map<String, Object> startTask(Long taskId, Long memberId) {
         Task task = taskMapper.selectById(taskId);
-        if (task == null || !task.getChildId().equals(memberId)) {
+        if (task == null || !task.getFamilyMemberId().equals(memberId)) {
             throw new IllegalArgumentException("任务不存在或不属于该成员");
         }
         if ("completed".equals(task.getStatus())) {
@@ -500,7 +501,7 @@ public List<Task> getTodayTasks(Long memberId, String dimensionCode) {
     @Transactional
     public Map<String, Object> completeTask(Long taskId, Long memberId, String photoUrl, String content, String contentType) {
         Task task = taskMapper.selectById(taskId);
-        if (task == null || !task.getChildId().equals(memberId)) {
+        if (task == null || !task.getFamilyMemberId().equals(memberId)) {
             return null;
         }
 
@@ -754,14 +755,14 @@ return result;
     public Page<Task> getTaskHistory(Long memberId, Integer page, Integer size) {
         Page<Task> pageParam = new Page<>(page, size);
         return taskMapper.selectPage(pageParam, new LambdaQueryWrapper<Task>()
-                .eq(Task::getChildId, memberId)
+                .eq(Task::getFamilyMemberId, memberId)
                 .orderByDesc(Task::getCreatedAt));
     }
 
     public Page<Task> getTaskHistoryByCategory(Long memberId, Integer page, Integer size, String category) {
         Page<Task> pageParam = new Page<>(page, size);
         return taskMapper.selectPage(pageParam, new LambdaQueryWrapper<Task>()
-                .eq(Task::getChildId, memberId)
+                .eq(Task::getFamilyMemberId, memberId)
                 .eq(Task::getCategory, category)
                 .orderByDesc(Task::getCreatedAt));
     }
@@ -870,7 +871,7 @@ return result;
 
         // 记录积分流水
         PointsLog log = new PointsLog();
-        log.setChildId(task.getChildId());
+        log.setChildId(task.getFamilyMemberId());
         log.setAmount(pointsEarned);
         log.setType(isEarly ? "earn" : "earn");
         log.setDescription("完成任务: " + task.getTitle());
@@ -880,7 +881,7 @@ return result;
         Map<String, Object> result = new HashMap<>();
         result.put("pointsEarned", pointsEarned);
 
-        try { FamilyMember child = familyMemberMapper.selectById(task.getChildId()); if (child != null) { onboardingService.completeTask(child.getUserId(), "FIRST_TASK"); } } catch (Exception e) { /* onboarding trigger failure should not block task completion */ }
+        try { FamilyMember child = familyMemberMapper.selectById(task.getFamilyMemberId()); if (child != null) { onboardingService.completeTask(child.getUserId(), "FIRST_TASK"); } } catch (Exception e) { /* onboarding trigger failure should not block task completion */ }
     result.put("newBalance", user.getTotalPoints());
     result.put("needReview", task.getNeedReview() == 1);
 
@@ -917,7 +918,7 @@ return result;
    */
   public List<Task> getPendingReviewTasksForChild(Long memberId) {
     return taskMapper.selectList(new LambdaQueryWrapper<Task>()
-        .eq(Task::getChildId, memberId)
+        .eq(Task::getFamilyMemberId, memberId)
         .eq(Task::getNeedReview, 1)
         .eq(Task::getStatus, "completed")
         .orderByDesc(Task::getCompletedAt));
@@ -928,7 +929,7 @@ return result;
    */
   public List<Task> getAllTasksByChild(Long memberId) {
     return taskMapper.selectList(new LambdaQueryWrapper<Task>()
-        .eq(Task::getChildId, memberId)
+        .eq(Task::getFamilyMemberId, memberId)
         .orderByDesc(Task::getCreatedAt));
   }
 
@@ -938,7 +939,7 @@ return result;
   public Page<Task> getTaskHistoryByChild(Long memberId, Integer page, Integer size, String status) {
     Page<Task> pageParam = new Page<>(page, size);
     LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
-        .eq(Task::getChildId, memberId)
+        .eq(Task::getFamilyMemberId, memberId)
         .orderByDesc(Task::getCreatedAt);
     
 if (status != null && !status.isEmpty()) {
@@ -1074,7 +1075,7 @@ return 0; // 达到每日扣分上限
     @Transactional
     public Map<String, Object> completeMinigameTask(Long taskId, Long memberId, Integer completionTime, Integer score) {
         Task task = taskMapper.selectById(taskId);
-        if (task == null || !task.getChildId().equals(memberId)) {
+        if (task == null || !task.getFamilyMemberId().equals(memberId)) {
             throw new RuntimeException("任务不存在或不属于该孩子");
         }
 

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

@@ -0,0 +1,137 @@
+package com.etotem.cfc.service.impl;
+
+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 org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+
+@Service
+public class ChallengeServiceImpl implements ChallengeService {
+
+    @Autowired private ChallengeProgressMapper progressMapper;
+    @Autowired private FamilyMemberMapper memberMapper;
+
+    @Override
+    public void initChallengeTasks(Long purchaseId, Long memberId) {
+        List<Map<String, Object>> week1Tasks = Arrays.asList(
+            CreateMap("first_report_upload", "首次报告上传", 1, 100),
+            CreateMap("streak_3days", "连续3天打卡", 3, 100),
+            CreateMap("upload_1_report", "上传1份报告", 1, 60)
+        );
+        
+        List<Map<String, Object>> week2Tasks = Arrays.asList(
+            CreateMap("upload_3_reports", "上传3份报告", 3, 100),
+            CreateMap("streak_7days", "连续7天打卡", 7, 100),
+            CreateMap("upload_3_reports_total", "累计上传3份报告", 3, 60)
+        );
+        
+        List<Map<String, Object>> week3Tasks = Arrays.asList(
+            CreateMap("complete_3_challenges", "完成3个挑战", 3, 100),
+            CreateMap("streak_21days", "连续21天打卡", 21, 100),
+            CreateMap("upload_5_reports", "上传5份报告", 5, 60)
+        );
+        
+        insertTasks(purchaseId, memberId, 1, week1Tasks);
+        insertTasks(purchaseId, memberId, 2, week2Tasks);
+        insertTasks(purchaseId, memberId, 3, week3Tasks);
+    }
+    
+    private Map<String, Object> CreateMap(String key, String title, int target, int reward) {
+        Map<String, Object> map = new HashMap<>();
+        map.put("taskKey", key);
+        map.put("taskTitle", title);
+        map.put("targetValue", target);
+        map.put("rewardCf", reward);
+        return map;
+    }
+    
+    private void insertTasks(Long purchaseId, Long memberId, int weekNum, List<Map<String, Object>> tasks) {
+        for (Map<String, Object> task : tasks) {
+            ChallengeProgress progress = new ChallengeProgress();
+            progress.setPurchaseId(purchaseId);
+            progress.setMemberId(memberId);
+            progress.setWeekNum(weekNum);
+            progress.setTaskType("fixed");
+            progress.setTaskKey((String) task.get("taskKey"));
+            progress.setTaskTitle((String) task.get("taskTitle"));
+            progress.setTargetValue((Integer) task.get("targetValue"));
+            progress.setCurrentValue(0);
+            progress.setRewardCf((Integer) task.get("rewardCf"));
+            progress.setStatus("pending");
+            progressMapper.insert(progress);
+        }
+    }
+
+    @Override
+    public Map<String, Object> getChallengeProgress(Long memberId) {
+        Map<String, Object> result = new HashMap<>();
+        
+        LambdaQueryWrapper<ChallengeProgress> qw = new LambdaQueryWrapper<>();
+        qw.eq(ChallengeProgress::getMemberId, memberId)
+           .orderByAsc(ChallengeProgress::getWeekNum);
+        List<ChallengeProgress> progress = progressMapper.selectList(qw);
+        
+        Map<Integer, List<ChallengeProgress>> byWeek = new LinkedHashMap<>();
+        for (ChallengeProgress p : progress) {
+            byWeek.computeIfAbsent(p.getWeekNum(), k -> new ArrayList<>()).add(p);
+        }
+        
+        int totalReward = 0;
+        int completedReward = 0;
+        int completedCount = 0;
+        
+        for (ChallengeProgress p : progress) {
+            totalReward += p.getRewardCf();
+            if ("completed".equals(p.getStatus())) {
+                completedReward += p.getRewardCf();
+                completedCount++;
+            }
+        }
+        
+        result.put("success", true);
+        result.put("tasks", progress);
+        result.put("byWeek", byWeek);
+        result.put("totalReward", totalReward);
+        result.put("completedReward", completedReward);
+        result.put("completedCount", completedCount);
+        result.put("totalCount", progress.size());
+        result.put("progressPercent", progress.size() > 0 ? Math.round((double)completedCount / progress.size() * 100) : 0);
+        
+        return result;
+    }
+
+    @Override
+    public void updateTaskProgress(Long memberId, String taskKey, int currentValue) {
+        LambdaQueryWrapper<ChallengeProgress> qw = new LambdaQueryWrapper<>();
+        qw.eq(ChallengeProgress::getMemberId, memberId)
+           .eq(ChallengeProgress::getTaskKey, taskKey)
+           .eq(ChallengeProgress::getStatus, "pending");
+        
+        ChallengeProgress progress = progressMapper.selectOne(qw);
+        if (progress != null) {
+            progress.setCurrentValue(currentValue);
+            progressMapper.updateById(progress);
+            
+            if (currentValue >= progress.getTargetValue()) {
+                completeTask(progress);
+            }
+        }
+    }
+    
+    private void completeTask(ChallengeProgress progress) {
+        progress.setStatus("completed");
+        progress.setCompletedAt(new java.util.Date());
+        progressMapper.updateById(progress);
+        
+        FamilyMember member = memberMapper.selectById(progress.getMemberId());
+        if (member != null) {
+            int currentPoints = member.getSystemPoints() != null ? member.getSystemPoints() : 0;
+            member.setSystemPoints(currentPoints + progress.getRewardCf());
+            memberMapper.updateById(member);
+        }
+    }
+}

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

@@ -0,0 +1,206 @@
+package com.etotem.cfc.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.*;
+import com.etotem.cfc.mapper.*;
+import com.etotem.cfc.service.PurchaseService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+
+@Service
+public class PurchaseServiceImpl implements PurchaseService {
+
+    @Autowired private PurchaseOrderMapper orderMapper;
+    @Autowired private FamilyMembershipMapper membershipMapper;
+    @Autowired private FamilyMemberMapper memberMapper;
+
+    @Override
+    public Map<String, Object> checkPurchase(Long userId) {
+        Map<String, Object> result = new HashMap<>();
+        
+        LambdaQueryWrapper<PurchaseOrder> qw = new LambdaQueryWrapper<>();
+        qw.eq(PurchaseOrder::getUserId, userId)
+           .eq(PurchaseOrder::getStatus, "paid")
+           .orderByDesc(PurchaseOrder::getCreatedAt)
+           .last("LIMIT 1");
+        PurchaseOrder order = orderMapper.selectOne(qw);
+        
+        if (order == null) {
+            result.put("hasPurchased", false);
+            result.put("daysRemaining", 0);
+            return result;
+        }
+        
+        boolean isActive = order.getEndDate().after(new Date());
+        
+        result.put("hasPurchased", true);
+        result.put("isActive", isActive);
+        result.put("orderId", order.getId());
+        result.put("orderNo", order.getOrderNo());
+        
+        if (isActive) {
+            long daysRemaining = java.time.temporal.ChronoUnit.DAYS.between(
+                java.time.LocalDate.now(), order.getEndDate().toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate());
+            result.put("daysRemaining", Math.max(0, daysRemaining));
+            result.put("startDate", order.getStartDate());
+            result.put("endDate", order.getEndDate());
+            result.put("isEarlyRenewal", order.getIsEarlyRenewal());
+        } else {
+            result.put("daysRemaining", 0);
+        }
+        
+        return result;
+    }
+
+    @Override
+    public Map<String, Object> createOrder(Long userId, Long familyId) {
+        Map<String, Object> result = new HashMap<>();
+        
+        String orderNo = "HQ" + System.currentTimeMillis() + UUID.randomUUID().toString().substring(0, 6).toUpperCase();
+        
+        PurchaseOrder order = new PurchaseOrder();
+        order.setOrderNo(orderNo);
+        order.setUserId(userId);
+        order.setFamilyId(familyId);
+        order.setAmount(19900);
+        order.setStatus("pending");
+        order.setStartDate(new Date());
+        
+        java.time.LocalDateTime endDate = java.time.LocalDateTime.now().plusDays(7);
+        order.setEndDate(java.sql.Timestamp.valueOf(endDate));
+        
+        orderMapper.insert(order);
+        
+        result.put("success", true);
+        result.put("orderNo", orderNo);
+        result.put("amount", 19900);
+        result.put("daysRemaining", 7);
+        return result;
+    }
+
+    @Override
+    public Map<String, Object> confirmPayment(String orderNo, String paymentMethod, String transactionId) {
+        Map<String, Object> result = new HashMap<>();
+        
+        LambdaQueryWrapper<PurchaseOrder> qw = new LambdaQueryWrapper<>();
+        qw.eq(PurchaseOrder::getOrderNo, orderNo);
+        PurchaseOrder order = orderMapper.selectOne(qw);
+        
+        if (order == null) {
+            result.put("success", false);
+            result.put("message", "订单不存在");
+            return result;
+        }
+        
+        if (!"pending".equals(order.getStatus())) {
+            result.put("success", false);
+            result.put("message", "订单状态异常");
+            return result;
+        }
+        
+        order.setStatus("paid");
+        order.setPaymentMethod(paymentMethod);
+        order.setTransactionId(transactionId);
+        order.setPaidAt(new Date());
+        orderMapper.updateById(order);
+        
+        int cfReward = 300;
+        order.setCfRewardGiven(cfReward);
+        
+        LambdaQueryWrapper<FamilyMember> mqw = new LambdaQueryWrapper<>();
+        mqw.eq(FamilyMember::getFamilyId, order.getFamilyId())
+           .eq(FamilyMember::getUserId, order.getUserId())
+           .last("LIMIT 1");
+        FamilyMember member = memberMapper.selectOne(mqw);
+        
+        if (member != null) {
+            int currentPoints = member.getSystemPoints() != null ? member.getSystemPoints() : 0;
+            member.setSystemPoints(currentPoints + cfReward);
+            memberMapper.updateById(member);
+        }
+        
+        result.put("success", true);
+        result.put("orderId", order.getId());
+        result.put("cfReward", cfReward);
+        result.put("daysRemaining", 7);
+        return result;
+    }
+
+    @Override
+    public Map<String, Object> getRenewalInfo(Long userId) {
+        Map<String, Object> result = new HashMap<>();
+        
+        LambdaQueryWrapper<FamilyMember> qw = new LambdaQueryWrapper<>();
+        qw.select(FamilyMember::getSystemPoints, FamilyMember::getFamilyId)
+           .eq(FamilyMember::getUserId, userId)
+           .last("LIMIT 1");
+        FamilyMember member = memberMapper.selectOne(qw);
+        
+        int cfBalance = member != null && member.getSystemPoints() != null ? member.getSystemPoints() : 0;
+        int discount = Math.min(cfBalance, 19900);
+        
+        result.put("success", true);
+        result.put("cfBalance", cfBalance);
+        result.put("discount", discount);
+        result.put("payAmount", 199900 - discount);
+        result.put("discountPercent", cfBalance >= 19900 ? 100 : Math.round((double)discount / 19900 * 100));
+        
+        return result;
+    }
+
+    @Override
+    public Map<String, Object> processRenewal(Long userId, Long familyId, Integer amount) {
+        Map<String, Object> result = new HashMap<>();
+        
+        LambdaQueryWrapper<PurchaseOrder> qw = new LambdaQueryWrapper<>();
+        qw.eq(PurchaseOrder::getUserId, userId)
+           .eq(PurchaseOrder::getStatus, "paid")
+           .last("LIMIT 1");
+        PurchaseOrder existingOrder = orderMapper.selectOne(qw);
+        
+        if (existingOrder == null) {
+            result.put("success", false);
+            result.put("message", "无有效购买记录");
+            return result;
+        }
+        
+        existingOrder.setIsEarlyRenewal(1);
+        orderMapper.updateById(existingOrder);
+        
+        String orderNo = "HR" + System.currentTimeMillis() + UUID.randomUUID().toString().substring(0, 6).toUpperCase();
+        PurchaseOrder renewalOrder = new PurchaseOrder();
+        renewalOrder.setOrderNo(orderNo);
+        renewalOrder.setUserId(userId);
+        renewalOrder.setFamilyId(familyId);
+        renewalOrder.setAmount(amount);
+        renewalOrder.setStatus("paid");
+        renewalOrder.setPaymentMethod("external");
+        renewalOrder.setStartDate(new Date());
+        java.time.LocalDateTime endDate = java.time.LocalDateTime.now().plusYears(1);
+        renewalOrder.setEndDate(java.sql.Timestamp.valueOf(endDate));
+        renewalOrder.setIsEarlyRenewal(1);
+        orderMapper.insert(renewalOrder);
+        
+        LambdaQueryWrapper<FamilyMembership> memQw = new LambdaQueryWrapper<>();
+        memQw.eq(FamilyMembership::getFamilyId, familyId);
+        FamilyMembership membership = membershipMapper.selectOne(memQw);
+        
+        if (membership == null) {
+            membership = new FamilyMembership();
+            membership.setFamilyId(familyId);
+            membership.setLevelCode("FAMILY");
+            membership.setStartDate(new Date());
+        }
+        membership.setEndDate(java.sql.Timestamp.valueOf(endDate));
+        membership.setPaymentStatus("paid");
+        membership.setOrderNo(orderNo);
+        membershipMapper.insert(membership);
+        
+        result.put("success", true);
+        result.put("orderNo", orderNo);
+        result.put("membershipValid", true);
+        return result;
+    }
+}

+ 41 - 0
cfc-backend/src/main/resources/db/migration/V252__create_purchase_tables.sql

@@ -0,0 +1,41 @@
+-- 健康启航计划购买订单表
+CREATE TABLE IF NOT EXISTS purchase_orders (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    order_no VARCHAR(64) NOT NULL UNIQUE COMMENT '订单号',
+    user_id BIGINT NOT NULL COMMENT '用户ID',
+    family_id BIGINT NOT NULL COMMENT '家庭ID',
+    amount INT NOT NULL COMMENT '支付金额(分)',
+    status VARCHAR(20) DEFAULT 'pending' COMMENT '状态: pending/paid/cancelled/refunded',
+    payment_method VARCHAR(20) DEFAULT NULL COMMENT '支付方式: wechat/alipay',
+    transaction_id VARCHAR(64) DEFAULT NULL COMMENT '支付流水号',
+    start_date DATETIME NOT NULL COMMENT '体验开始时间',
+    end_date DATETIME NOT NULL COMMENT '体验结束时间',
+    is_early_renewal TINYINT DEFAULT 0 COMMENT '是否早期续费(7天内)',
+    cf_reward_given INT DEFAULT 0 COMMENT '已发放CF值',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    paid_at DATETIME DEFAULT NULL,
+    INDEX idx_user_id (user_id),
+    INDEX idx_family_id (family_id),
+    INDEX idx_order_no (order_no)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康启航计划购买订单';
+
+-- 闯关任务进度表
+CREATE TABLE IF NOT EXISTS challenge_progress (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    purchase_id BIGINT NOT NULL COMMENT '购买订单ID',
+    member_id BIGINT NOT NULL COMMENT '家庭成员ID',
+    week_num INT NOT NULL COMMENT '周次: 1/2/3',
+    task_type VARCHAR(50) NOT NULL COMMENT '任务类型: fixed/personalized/streak',
+    task_key VARCHAR(100) NOT NULL COMMENT '任务标识',
+    task_title VARCHAR(255) NOT NULL COMMENT '任务标题',
+    target_value INT DEFAULT 0 COMMENT '目标值',
+    current_value INT DEFAULT 0 COMMENT '当前值',
+    reward_cf INT DEFAULT 0 COMMENT '奖励CF值',
+    status VARCHAR(20) DEFAULT 'pending' COMMENT '状态: pending/in_progress/completed',
+    completed_at DATETIME DEFAULT NULL,
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_purchase_id (purchase_id),
+    INDEX idx_member_id (member_id),
+    UNIQUE KEY uk_member_task (member_id, task_key, week_num)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='闯关任务进度';

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

@@ -2637,6 +2637,7 @@ CREATE TABLE IF NOT EXISTS butler_profiles (
     reject_reason VARCHAR(500),
     commission_rate_l1 INT DEFAULT 1000,
     commission_rate_l2 INT DEFAULT 5000,
+    accepting TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否接受新家庭绑定: 0=停接/1=接单',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     approved_at DATETIME,
@@ -5115,3 +5116,45 @@ CREATE TABLE IF NOT EXISTS profile_history (
     INDEX idx_member_date (member_id, snapshot_date),
     INDEX idx_date (snapshot_date)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户画像历史快照';
+
+-- 健康启航计划购买订单表
+CREATE TABLE IF NOT EXISTS purchase_orders (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    order_no VARCHAR(64) NOT NULL UNIQUE,
+    user_id BIGINT NOT NULL,
+    family_id BIGINT NOT NULL,
+    amount INT NOT NULL,
+    status VARCHAR(20) DEFAULT 'pending',
+    payment_method VARCHAR(20),
+    transaction_id VARCHAR(64),
+    start_date DATETIME NOT NULL,
+    end_date DATETIME NOT NULL,
+    is_early_renewal TINYINT DEFAULT 0,
+    cf_reward_given INT DEFAULT 0,
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    paid_at DATETIME,
+    INDEX idx_user_id (user_id),
+    INDEX idx_family_id (family_id),
+    INDEX idx_order_no (order_no)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- 闯关任务进度表
+CREATE TABLE IF NOT EXISTS challenge_progress (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    purchase_id BIGINT NOT NULL,
+    member_id BIGINT NOT NULL,
+    week_num INT NOT NULL,
+    task_type VARCHAR(50) NOT NULL,
+    task_key VARCHAR(100) NOT NULL,
+    task_title VARCHAR(255) NOT NULL,
+    target_value INT DEFAULT 0,
+    current_value INT DEFAULT 0,
+    reward_cf INT DEFAULT 0,
+    status VARCHAR(20) DEFAULT 'pending',
+    completed_at DATETIME,
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_purchase_id (purchase_id),
+    INDEX idx_member_id (member_id),
+    UNIQUE KEY uk_member_task (member_id, task_key, week_num)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

+ 16 - 8
cfc-frontend/pages.json

@@ -445,29 +445,25 @@
         {
           "path": "diet-checkin",
           "style": {
-            "navigationBarTitleText": "饮食打卡",
-            "navigationStyle": "custom"
+            "navigationBarTitleText": "饮食打卡"
           }
         },
         {
           "path": "exercise-checkin",
           "style": {
-            "navigationBarTitleText": "运动打卡",
-            "navigationStyle": "custom"
+            "navigationBarTitleText": "运动打卡"
           }
         },
         {
           "path": "sleep-checkin",
           "style": {
-            "navigationBarTitleText": "睡眠打卡",
-            "navigationStyle": "custom"
+            "navigationBarTitleText": "睡眠打卡"
           }
         },
         {
           "path": "growth-emotion-checkin",
           "style": {
-            "navigationBarTitleText": "心情打卡 (成长)",
-            "navigationStyle": "custom"
+            "navigationBarTitleText": "心情打卡 (成长)"
           }
         }
       ]
@@ -1359,6 +1355,18 @@
             "navigationBarTitleText": "发现圈子",
             "navigationStyle": "custom"
           }
+        },
+        {
+          "path": "circle-feed",
+          "style": {
+            "navigationBarTitleText": "圈子动态"
+          }
+        },
+        {
+          "path": "create-circle",
+          "style": {
+            "navigationBarTitleText": "创建圈子"
+          }
         }
       ]
     },

+ 10 - 3
cfc-frontend/pages/action-detail/index.vue

@@ -31,6 +31,9 @@
     <FamilyEnergyBar
       v-if="isLoggedIn"
       dimensionCode="action"
+      :sandboxData="sandboxData"
+      :dualDimension="dualDimension" />
+
     <!-- 关系解读 -->
     <view class="section score-explain-card" v-if="isLoggedIn && currentView === 'self' && actionScore != null">
       <view class="explain-header">
@@ -39,8 +42,6 @@
       </view>
       <text class="explain-text">{{ actionScore >= 80 ? '家庭关系和谐,家人沟通顺畅。' : actionScore >= 60 ? '家庭关系一般,建议多花时间陪伴家人。' : '家庭关系需要改善,建议进行家庭沟通活动。' }}</text>
     </view>
-      :sandboxData="sandboxData"
-      :dualDimension="dualDimension" />
     <!-- 关系六维 -->
     <dimension-sub-dims
       v-if="isLoggedIn && actionSubDims.length > 0"
@@ -253,6 +254,12 @@ export default {
       }
     },
 
+    /** 当前选中成员的行维度分数 */
+    actionScore: function() {
+      var member = this.currentMemberEnergies
+      return member ? (member.actionScore || 0) : null
+    },
+
     /** 行6维分值(从当前成员中提取,与 dimensions 顺序对应) */
     actionScores: function() {
       var member = this.currentMemberEnergies
@@ -361,7 +368,6 @@ export default {
         this.loadSandboxData()
       }
     },
-    },
     loadEnergyData: function() {
       var self = this
       getEnergyOverview(this.activeChildId).then(function(res) {
@@ -443,6 +449,7 @@ export default {
       })
     }
   }
+}
 </script>
 
 <style scoped>

+ 0 - 1
cfc-frontend/pages/action-detail/member-action-detail.vue

@@ -1,7 +1,6 @@
 <template>
   <view class="detail-container">
     <!-- 顶部导航 -->
-</view>
 <!-- 品牌定位语(登录前显示) -->
     <view class="brand-strap" v-if="!isLoggedIn">
       <text class="brand-strap-text">改善全家关系与沟通能力</text>

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

@@ -1,7 +1,6 @@
 <template>
   <view class="activity-container">
     <!-- Nav bar with title "活动中心" and admin "+" button -->
-</view>
 
     <!-- Tab bar (4 tabs) -->
     <view class="tab-bar">

+ 0 - 1
cfc-frontend/pages/body-detail/checkin.vue

@@ -1,7 +1,6 @@
 <template>
   <view class="checkin-container">
     <!-- 顶部导航 -->
-</view>
 
     <!-- 能量奖励提示 -->
     <view class="energy-tip">

+ 0 - 1
cfc-frontend/pages/body-detail/health-report.vue

@@ -1,7 +1,6 @@
 <template>
   <view class="report-container">
     <!-- 顶部导航 -->
-</view>
 
     <!-- 报告头部 -->
     <view class="report-header" v-if="report">

+ 0 - 1
cfc-frontend/pages/body-detail/member-body-detail.vue

@@ -1,7 +1,6 @@
 <template>
   <view class="detail-container">
     <!-- 顶部导航 -->
-</view>
 
     <!-- 五维能量条 -->
     <FamilyEnergyBar

+ 0 - 1
cfc-frontend/pages/body/member-body-detail.vue

@@ -1,7 +1,6 @@
 <template>
   <view class="detail-container">
     <!-- 顶部导航 -->
-</view>
 
     <!-- 五维能量条 -->
     <FamilyEnergyBar

+ 1 - 1
cfc-frontend/pages/diet/food-query.vue

@@ -1,5 +1,5 @@
 <template>
-  <view class="food-query-page"></view>
+  <view class="food-query-page">
 
     <view class="search-bar">
       <input class="search-input" placeholder="搜索食材..." v-model="searchQuery" @confirm="searchFood" />

+ 1 - 1
cfc-frontend/pages/diet/meal-config.vue

@@ -1,5 +1,5 @@
 <template>
-  <view class="meal-config-page"></view>
+  <view class="meal-config-page">
 
     <scroll-view class="content" scroll-y>
       <!-- 日期类型切换 -->

+ 1 - 1
cfc-frontend/pages/diet/preferences.vue

@@ -1,5 +1,5 @@
 <template>
-  <view class="preferences-page"></view>
+  <view class="preferences-page">
 
     <scroll-view class="content" scroll-y>
       <!-- 硬性要求 -->

+ 1 - 1
cfc-frontend/pages/diet/recommendation.vue

@@ -1,5 +1,5 @@
 <template>
-  <view class="recommendation-page"></view>
+  <view class="recommendation-page">
 
     <scroll-view class="content" scroll-y>
       <!-- 无推荐状态 -->

+ 1 - 1
cfc-frontend/pages/diet/record-detail.vue

@@ -1,5 +1,5 @@
 <template>
-  <view class="record-detail-page"></view>
+  <view class="record-detail-page">
 
     <scroll-view class="content" scroll-y v-if="record">
       <!-- 照片 -->

+ 1 - 1
cfc-frontend/pages/diet/records.vue

@@ -1,5 +1,5 @@
 <template>
-  <view class="records-page"></view>
+  <view class="records-page">
 
     <!-- 日期选择 -->
     <view class="date-picker">

+ 1 - 6
cfc-frontend/pages/games/list.vue

@@ -4,13 +4,8 @@
       <text class="title">🎮 益智小游戏</text>
       <text class="subtitle">完成游戏获得积分奖励</text>
     </view>
-<view class="nav-item" @click="goStats">
-        <text class="nav-icon">📊</text>
-        <text class="nav-text">训练统计</text>
-      </view>
-    </view>
 
-    <view class="game-list">
+  <view class="game-list">
       <view 
         class="game-card" 
         v-for="game in gameList" 

+ 1 - 1
cfc-frontend/pages/growth/caution-detail.vue

@@ -1,5 +1,5 @@
 <template>
-  <view class="page"></view>
+  <view class="page">
 
     <view class="warning-banner">
       <text class="banner-icon">⚠️</text>

+ 143 - 0
cfc-frontend/pages/growth/challenge-progress.vue

@@ -0,0 +1,143 @@
+<template>
+  <view class="page">
+    <view class="nav-bar">
+      <view class="nav-back" @tap="goBack">
+        <text class="back-text">‹ 返回</text>
+      </view>
+      <text class="nav-title">闯关进度</text>
+    </view>
+
+    <scroll-view class="content" scroll-y>
+      <!-- 进度概览 -->
+      <view class="progress-overview">
+        <view class="progress-circle" :style="{ background: 'conic-gradient(#F97316 ' + progressPercent + '%, #f0f0f0 ' + progressPercent + '%)' }">
+          <text class="progress-percent">{{ progressPercent }}%</text>
+        </view>
+        <text class="progress-label">已完成 {{ completedCount }}/{{ totalCount }} 项任务</text>
+        <text class="progress-cf">已获得 {{ completedReward }} CF值</text>
+      </view>
+
+      <!-- 各周任务 -->
+      <view class="card" v-for="(tasks, week) in tasksByWeek" :key="week">
+        <view class="card-header">
+          <text class="card-title">第{{ week }}周</text>
+          <text class="card-reward">+{{ weekReward(week) }} CF</text>
+        </view>
+        <view class="task-list">
+          <view class="task-item" v-for="task in tasks" :key="task.taskKey">
+            <view class="task-left">
+              <text class="task-icon" :class="{ 'completed': task.status === 'completed' }">{{ task.status === 'completed' ? '✓' : '○' }}</text>
+              <view class="task-info">
+                <text class="task-title">{{ task.taskTitle }}</text>
+                <text class="task-progress">{{ task.currentValue }}/{{ task.targetValue }}</text>
+              </view>
+            </view>
+            <text class="task-reward">+{{ task.rewardCf }} CF</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 连续打卡 -->
+      <view class="card">
+        <view class="card-header">
+          <text class="card-title">连续打卡</text>
+          <text class="card-streak">{{ streakDays }} 天</text>
+        </view>
+        <view class="streak-grid">
+          <view class="streak-day" v-for="n in 21" :key="n" :class="{ 'done': n <= streakDays }">
+            <text>{{ n }}</text>
+          </view>
+        </view>
+        <text class="streak-hint">连续打卡可得额外CF加成</text>
+      </view>
+    </scroll-view>
+  </view>
+</template>
+
+<script>
+import { getChallengeProgress } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      loading: true,
+      tasksByWeek: {},
+      totalReward: 0,
+      completedReward: 0,
+      completedCount: 0,
+      totalCount: 0,
+      streakDays: 0
+    }
+  },
+  computed: {
+    progressPercent() {
+      return this.totalCount > 0 ? Math.round(this.completedCount / this.totalCount * 100) : 0
+    }
+  },
+  onLoad() {
+    this.loadProgress()
+  },
+  methods: {
+    goBack() { uni.navigateBack() },
+    async loadProgress() {
+      this.loading = true
+      try {
+        var res = await getChallengeProgress(uni.getStorageSync('currentMemberId') || uni.getStorageSync('userId'))
+        if (res.data && res.data.success) {
+          this.tasksByWeek = res.data.byWeek || {}
+          this.totalReward = res.data.totalReward || 0
+          this.completedReward = res.data.completedReward || 0
+          this.completedCount = res.data.completedCount || 0
+          this.totalCount = res.data.totalCount || 0
+          this.streakDays = res.data.streakDays || 0
+        }
+      } catch (e) {
+        console.log('加载进度失败', e)
+      } finally {
+        this.loading = false
+      }
+    },
+    weekReward(week) {
+      var tasks = this.tasksByWeek[week] || []
+      return tasks.reduce(function(sum, t) { return sum + (t.rewardCf || 0); }, 0)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page { min-height: 100vh; background: #FFF7ED; }
+.nav-bar { display: flex; align-items: center; height: 88rpx; padding-top: env(safe-area-inset-top); background: #fff; border-bottom: 1rpx solid #f0f0f0; position: relative; }
+.nav-back { position: absolute; left: 24rpx; top: 0; bottom: 0; display: flex; align-items: center; }
+.back-text { font-size: 28rpx; color: #F97316; }
+.nav-title { flex: 1; text-align: center; font-size: 32rpx; font-weight: bold; color: #333; }
+.content { height: calc(100vh - 88rpx); }
+
+.progress-overview { background: linear-gradient(135deg, #FF8C42, #F97316); padding: 40rpx 32rpx; text-align: center; }
+.progress-circle { width: 160rpx; height: 160rpx; border-radius: 50%; margin: 0 auto 16rpx; display: flex; align-items: center; justify-content: center; box-shadow: 0 4rpx 20rpx rgba(249,115,22,0.3); }
+.progress-percent { font-size: 36rpx; font-weight: bold; color: #fff; }
+.progress-label { font-size: 24rpx; color: rgba(255,255,255,0.9); display: block; }
+.progress-cf { font-size: 28rpx; color: #FFD700; font-weight: bold; margin-top: 8rpx; display: block; }
+
+.card { margin: 16rpx 24rpx; background: #fff; border-radius: 20rpx; overflow: hidden; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
+.card-header { display: flex; justify-content: space-between; align-items: center; padding: 20rpx 24rpx; border-bottom: 1rpx solid #f5f5f5; }
+.card-title { font-size: 28rpx; font-weight: bold; color: #333; }
+.card-reward { font-size: 22rpx; color: #F97316; }
+.card-streak { font-size: 28rpx; font-weight: bold; color: #F97316; }
+
+.task-list { padding: 16rpx 24rpx; }
+.task-item { display: flex; justify-content: space-between; align-items: center; padding: 16rpx 0; border-bottom: 1rpx solid #f5f5f5; }
+.task-item:last-child { border-bottom: none; }
+.task-left { display: flex; align-items: center; gap: 16rpx; flex: 1; min-width: 0; }
+.task-icon { font-size: 32rpx; color: #ccc; width: 40rpx; text-align: center; }
+.task-icon.completed { color: #4CAF50; }
+.task-info { flex: 1; min-width: 0; }
+.task-title { font-size: 26rpx; color: #333; display: block; }
+.task-progress { font-size: 22rpx; color: #999; margin-top: 4rpx; display: block; }
+.task-reward { font-size: 22rpx; color: #F97316; flex-shrink: 0; margin-left: 16rpx; }
+
+.streak-grid { display: flex; flex-wrap: wrap; gap: 8rpx; padding: 16rpx 24rpx; }
+.streak-day { width: 48rpx; height: 48rpx; border-radius: 50%; background: #f0f0f0; display: flex; align-items: center; justify-content: center; font-size: 20rpx; color: #999; }
+.streak-day.done { background: #F97316; color: #fff; }
+.streak-hint { font-size: 22rpx; color: #999; text-align: center; padding: 0 24rpx 16rpx; display: block; }
+</style>

+ 0 - 1
cfc-frontend/pages/growth/diet-checkin.vue

@@ -93,7 +93,6 @@
         </view>
       </view>
     </scroll-view>
-  </view>
 </template>
 
 <script>

+ 0 - 1
cfc-frontend/pages/growth/exercise-checkin.vue

@@ -105,7 +105,6 @@
         </view>
       </view>
     </scroll-view>
-  </view>
 </template>
 
 <script>

+ 0 - 1
cfc-frontend/pages/growth/growth-emotion-checkin.vue

@@ -103,7 +103,6 @@
         </view>
       </view>
     </scroll-view>
-  </view>
 </template>
 
 <script>

+ 0 - 1
cfc-frontend/pages/growth/profile/index.vue

@@ -197,7 +197,6 @@
         <text class="empty-hint">完成打卡后自动生成</text>
       </view>
     </scroll-view>
-  </view>
 </template>
 
 <script>

+ 0 - 1
cfc-frontend/pages/growth/sleep-checkin.vue

@@ -56,7 +56,6 @@
         </view>
       </view>
     </scroll-view>
-  </view>
 </template>
 
 <script>

+ 7 - 9
cfc-frontend/pages/health-main/index.vue

@@ -558,15 +558,13 @@ export default {
       try {
         var api = require('../../utils/api.js')
         var memberId = uni.getStorageSync('currentChildId') || uni.getStorageSync('currentMemberId') || uni.getStorageSync('userId') || ''
-        // 健康报告(memberId 为空时不调用,避免后端 400)
-        if (memberId) {
-          api.getReportList(memberId).then(function(res) {
-            if (res.code === 200 && res.data) {
-              var list = Array.isArray(res.data) ? res.data : (res.data.list || [])
-              self.reports = list
-            }
-          }).catch(function() {})
-        }
+        // 健康报告:不传 memberId,由后端返回当前家庭的全部报告
+        api.getReportList().then(function(res) {
+          if (res.code === 200 && res.data) {
+            var list = Array.isArray(res.data) ? res.data : (res.data.list || [])
+            self.reports = list
+          }
+        }).catch(function() {})
         // 身体七维概览
       if (memberId) {
         api.getDimensionOverview({ memberId: memberId }).then(function(res) {

+ 0 - 1
cfc-frontend/pages/health/bodyfat-confirm.vue

@@ -39,7 +39,6 @@
         <button class="save-btn" :disabled="saving" @click="save">{{ saving ? '保存中...' : '保存指标' }}</button>
       </view>
     </scroll-view>
-  </view>
 </template>
 
 <script>

+ 0 - 1
cfc-frontend/pages/health/daily-checkin.vue

@@ -88,7 +88,6 @@
         </view>
       </view>
     </scroll-view>
-  </view>
 </template>
 
 <script>

+ 0 - 1
cfc-frontend/pages/health/diet-index.vue

@@ -126,7 +126,6 @@
         </view>
       </view>
     </scroll-view>
-  </view>
 </template>
 
 <script>

+ 0 - 1
cfc-frontend/pages/health/exercise-index.vue

@@ -128,7 +128,6 @@
         </view>
       </view>
     </scroll-view>
-  </view>
 </template>
 
 <script>

+ 1 - 0
cfc-frontend/pages/health/gut-flora-detail.vue

@@ -1,4 +1,5 @@
 <template>
+  <view class="page">
   <scroll-view class="content" scroll-y>
       <!-- 报告头部 -->
       <view class="report-header">

+ 0 - 1
cfc-frontend/pages/health/gut-flora-risks-detail.vue

@@ -1,7 +1,6 @@
 <template>
   <view class="page">
 <text class="nav-edit" @tap="toggleEdit">{{ isEditing ? '完成' : '编辑' }}</text>
-    </view>
 
     <scroll-view class="content" scroll-y>
       <view class="section-label">重要风险</view>

+ 0 - 1
cfc-frontend/pages/health/gut-flora-species-detail.vue

@@ -1,7 +1,6 @@
 <template>
   <view class="page">
 <text class="nav-edit" @tap="toggleEdit">{{ isEditing ? '完成' : '编辑' }}</text>
-    </view>
 
     <scroll-view class="tabs" scroll-x>
       <view class="tab-item" v-for="(tab, idx) in tabs" :key="idx"

+ 0 - 1
cfc-frontend/pages/health/gut-index.vue

@@ -81,7 +81,6 @@
         </view>
       </view>
     </scroll-view>
-  </view>
 </template>
 
 <script>

+ 25 - 9
cfc-frontend/pages/health/health-plan-summary.vue

@@ -37,13 +37,13 @@
       <view class="hint-text">选择方案针对的家庭成员</view>
       <view class="member-list">
         <view v-for="(m, idx) in executors" :key="getMemberKey(m, idx)"
-              class="member-card" :class="{ active: selectedMemberIds.indexOf(m.memberId) >= 0 }"
+              class="member-card" :class="{ active: isMemberSelected(m.memberId) }"
               @click="toggleMember(m.memberId)">
           <view class="member-card-left">
             <view class="member-avatar">{{ m.name.charAt(0) }}</view>
             <text class="member-card-name">{{ m.name }}</text>
           </view>
-          <view class="member-check">{{ selectedMemberIds.indexOf(m.memberId) >= 0 ? '✓' : '' }}</view>
+          <view class="member-check">{{ isMemberSelected(m.memberId) ? '✓' : '' }}</view>
         </view>
         <view v-if="executors.length === 0" class="empty-state">
           <text class="empty-icon">👨‍👩‍👧</text>
@@ -217,13 +217,15 @@ export default {
   },
   onLoad: function(options) {
     if (options.initialMemberIds) {
+      var self = this
       this.selectedMemberIds = options.initialMemberIds.split(',').filter(function(id) { return id })
     }
     this.planId = options.planId ? parseInt(options.planId) : null
-    this.loadExecutors()
     if (this.planId) {
       this.viewMode = true
       this.loadPlanDetail()
+    } else {
+      this.loadExecutors()
     }
     if (options.autoExecute === '1') {
       var self = this
@@ -231,7 +233,9 @@ export default {
     }
   },
   onShow: function() {
-    this.loadExecutors()
+    if (!this._executorsLoaded) {
+      this.loadExecutors()
+    }
   },
   methods: {
     getMemberKey: function(m, idx) { return m.memberId || idx },
@@ -241,18 +245,25 @@ export default {
         if (res && res.code === 200 && res.data) {
           var list = res.data instanceof Array ? res.data : []
           self.executors = list.filter(function(m) { return m.memberId || m.id }).map(function(m) {
-            return { memberId: m.memberId || m.id, name: m.nickname || m.name || '成员' }
+            return { memberId: String(m.memberId || m.id), name: m.nickname || m.name || '成员' }
           })
           if (self.selectedMemberIds.length === 0 && self.executors.length > 0) {
             self.selectedMemberIds = [self.executors[0].memberId]
+          } else {
+            self.selectedMemberIds = self.selectedMemberIds.map(function(id) { return String(id) })
           }
         }
-      }).catch(function() { self.executors = [] })
+        self._executorsLoaded = true
+      }).catch(function() { self.executors = []; self._executorsLoaded = true })
+    },
+    isMemberSelected: function(memberId) {
+      return this.selectedMemberIds.indexOf(String(memberId)) >= 0
     },
     toggleMember: function(memberId) {
-      var idx = this.selectedMemberIds.indexOf(memberId)
+      var id = String(memberId)
+      var idx = this.selectedMemberIds.indexOf(id)
       if (idx >= 0) this.selectedMemberIds.splice(idx, 1)
-      else this.selectedMemberIds.push(memberId)
+      else this.selectedMemberIds.push(id)
     },
     formatTime: function(iso) {
       if (!iso) return ''
@@ -275,7 +286,10 @@ export default {
         self.loading = false
         if (res && res.code === 200 && res.data) {
           var data = res.data
-          if (data.overview) {
+          if (typeof data === 'string') {
+            self.planData = { overview: data }
+            self.step = 2
+          } else if (data.overview) {
             self.planData = data
             if (data.sections && data.sections instanceof Array) {
               data.sections.forEach(function(sec) {
@@ -453,6 +467,8 @@ export default {
       this.taskSubmitted = false
       this.editing = false
       this.sectionFeedback = ''
+      this.selectedMemberIds = []
+      this.userGoal = ''
       this.planData = {}
       this.sections = {
         nutrition: { content: '', confirmed: false },

+ 0 - 1
cfc-frontend/pages/health/nutrition-profile.vue

@@ -1,6 +1,5 @@
 <template>
   <view class="nutrition-profile">
-</view>
 
     <view class="profile-header" v-if="childName">
       <view class="child-info">

+ 0 - 1
cfc-frontend/pages/health/physical-exam-detail.vue

@@ -1,7 +1,6 @@
 <template>
   <view class="detail-container">
     <!-- 导航栏 -->
-</view>
 
     <!-- 加载状态 -->
     <view class="loading-wrap" v-if="loading">

+ 0 - 1
cfc-frontend/pages/health/report-confirm.vue

@@ -1,6 +1,5 @@
 <template>
   <view class="confirm-container">
-</view>
 
     <view class="tip-banner">
       <text class="tip-icon">✅</text>

+ 93 - 4
cfc-frontend/pages/health/report-list.vue

@@ -1,5 +1,25 @@
 <template>
   <view class="list-container">
+    <!-- 成员过滤 -->
+    <view class="member-filter">
+      <scroll-view class="filter-scroll" scroll-x="true" :show-scrollbar="false">
+        <view class="filter-chips">
+          <view class="chip" :class="{ 'chip-active': !selectedMemberId }" @click="selectAllMembers">
+            <text class="chip-text">全部</text>
+          </view>
+          <view
+            class="chip"
+            v-for="item in members"
+            :key="item.id"
+            :class="{ 'chip-active': selectedMemberId === item._filterId }"
+            @click="selectMember(item)"
+          >
+            <text class="chip-text">{{ item.nickname || '成员' }}</text>
+          </view>
+        </view>
+      </scroll-view>
+    </view>
+
     <!-- 加载状态 -->
     <view class="loading-wrap" v-if="loading">
       <text class="loading-text">加载中...</text>
@@ -9,7 +29,7 @@
     <view class="report-list" v-else-if="reports.length > 0">
       <view
         class="report-card"
-        v-for="item in reports.slice(0, 3)"
+        v-for="item in reports"
         :key="item.id"
         @click="goToDetail(item.id, item.reportType)"
       >
@@ -43,27 +63,63 @@
 </template>
 
 <script>
-import { getReportList, deleteReport } from '../../utils/api.js'
+import { getReportList, deleteReport, getFamilyMemberList } from '../../utils/api.js'
 import { parseDate } from '../../utils/format.js'
 
 export default {
   data() {
     return {
       reports: [],
+      members: [],
+      selectedMemberId: null,
       loading: false,
       memberId: null
     }
   },
   onLoad(options) {
-    // 优先用导航参数传入的 memberId;未传时后端回退到家庭报告列表
+    // 导航参数可带初始 memberId(预选对应成员);未传时默认"全部"
     this.memberId = options.memberId ? parseInt(options.memberId) : null
+    this.selectedMemberId = this.memberId
+    this.loadMembers()
     this.loadData()
   },
   methods: {
+    loadMembers() {
+      var self = this
+      getFamilyMemberList({}).then(function(res) {
+        if (res.code === 200 && Array.isArray(res.data)) {
+          // _filterId 预计算,供 :class 内联比较使用(小程序 :class 禁止方法调用)
+          var list = []
+          for (var i = 0; i < res.data.length; i++) {
+            var m = res.data[i]
+            list.push({
+              id: m.id,
+              nickname: m.nickname,
+              avatar: m.avatar,
+              _filterId: m.id || m.userId
+            })
+          }
+          self.members = list
+        }
+      }).catch(function(e) {
+        console.log('获取家庭成员失败', e)
+      })
+    },
+    selectAllMembers() {
+      if (this.selectedMemberId === null) return
+      this.selectedMemberId = null
+      this.loadData()
+    },
+    selectMember(item) {
+      var filterId = item._filterId
+      if (this.selectedMemberId === filterId) return
+      this.selectedMemberId = filterId
+      this.loadData()
+    },
     loadData() {
       var self = this
       self.loading = true
-      getReportList(self.memberId).then(function(res) {
+      getReportList(self.selectedMemberId).then(function(res) {
         self.reports = res.data || []
       }).catch(function(e) {
         console.log('获取报告列表失败', e)
@@ -135,6 +191,39 @@ export default {
   background: #F5F7FA;
 }
 
+/* ===== 成员过滤 ===== */
+.member-filter {
+  background: #F5F7FA;
+  padding: 20rpx 30rpx 4rpx;
+}
+.filter-scroll {
+  width: 100%;
+  white-space: nowrap;
+}
+.filter-chips {
+  display: flex;
+  flex-direction: row;
+}
+.chip {
+  flex-shrink: 0;
+  background: #fff;
+  border: 1rpx solid #E2E8F0;
+  border-radius: 28rpx;
+  padding: 10rpx 30rpx;
+  margin-right: 16rpx;
+}
+.chip-active {
+  background: #5B9BD5;
+  border-color: #5B9BD5;
+}
+.chip-text {
+  font-size: 26rpx;
+  color: #64748B;
+}
+.chip-active .chip-text {
+  color: #fff;
+}
+
 /* ===== 加载状态 ===== */
 .loading-wrap {
   display: flex;

+ 0 - 1
cfc-frontend/pages/health/sleep-index.vue

@@ -128,7 +128,6 @@
         </view>
       </view>
     </scroll-view>
-  </view>
 </template>
 
 <script>

+ 0 - 1
cfc-frontend/pages/health/teacher-select.vue

@@ -1,7 +1,6 @@
 <template>
   <view class="page">
     <!-- 导航栏 -->
-</view>
 
     <!-- 提示 -->
     <view class="tip-banner">

+ 0 - 1
cfc-frontend/pages/health/water-index.vue

@@ -97,7 +97,6 @@
         </view>
       </view>
     </scroll-view>
-  </view>
 </template>
 
 <script>

+ 0 - 1
cfc-frontend/pages/home-pages/member-home-detail.vue

@@ -1,7 +1,6 @@
 <template>
   <view class="detail-container">
     <!-- 顶部导航 -->
-</view>
 
     <!-- 加载中 -->
     <view class="loading" v-if="loading">加载中...</view>

+ 32 - 1
cfc-frontend/pages/index-home/index.vue

@@ -397,6 +397,7 @@ export default {
       splashOverlayFading: false,
       _splashReady: false,
       _watchPageReadyDone: false,
+      pendingSurveys: [],
 
       typeList: [
         { label: '全部', value: '' },
@@ -530,6 +531,8 @@ export default {
 
     // 始终执行初始化,确保登录态变更后及时切换视图
     this._doInit()
+    // 定期调研待办角标(内部自检登录态,未登录静默清空)
+    this._loadSurveyBadge()
   },
   methods: {
     _checkSplashReady() {
@@ -886,8 +889,35 @@ export default {
       var id = this.currentChildId || ''
       uni.navigateTo({ url: '/pages/health/report-upload?memberId=' + id + '&from=index' })
     },
+    // 定期调研待办角标(内部自检登录态,未登录静默清空)
+    _loadSurveyBadge: function() {
+      var self = this
+      if (!this.isLoggedIn()) { self.pendingSurveys = []; return }
+      getSurveyStatus({}).then(function(res) {
+        var list = (res && res.code === 200 && res.data && res.data instanceof Array) ? res.data : []
+        var pending = []
+        for (var i = 0; i < list.length; i++) {
+          if (list[i] && list[i].status === 'pending') pending.push(list[i])
+        }
+        self.pendingSurveys = pending
+      }).catch(function() { self.pendingSurveys = [] })
+    },
     goSelfTest: function() {
-      uni.navigateTo({ url: '/pages/health/survey-questionnaire?dim=all' })
+      var firstPending = null
+      for (var i = 0; i < this.pendingSurveys.length; i++) {
+        var s = this.pendingSurveys[i]
+        if (s && s.templateId) { firstPending = s; break }
+      }
+      if (firstPending) {
+        var url = '/pages/health/survey-questionnaire?templateId=' + firstPending.templateId +
+          '&templateTitle=' + encodeURIComponent(firstPending.title || '定期调研') +
+          '&templateDimension=' + encodeURIComponent(firstPending.dimension || '') +
+          '&periodStart=' + (firstPending.periodStart || '') +
+          '&periodEnd=' + (firstPending.periodEnd || '')
+        uni.navigateTo({ url: url })
+      } else {
+        uni.navigateTo({ url: '/pages/health/survey-questionnaire?dim=all' })
+      }
     },
     _doInit(options) {
       options = options || {}
@@ -1808,5 +1838,6 @@ export default {
 .challenge-add-btn::after {
   border: none;
 }
+.self-test-badge { display: inline-block; align-self: flex-start; margin-top: 8rpx; font-size: 20rpx; color: #F97316; background: rgba(249,115,22,0.1); padding: 4rpx 14rpx; border-radius: 999rpx; }
 
 </style>

+ 1 - 0
cfc-frontend/pages/meal/log.vue

@@ -1,3 +1,4 @@
+<!-- DEPRECATED: 请使用 pages/diet/records 替代(功能已合并) -->
 <template>
   <view class="meal-log">
     <!-- 日期导航 -->

+ 1 - 0
cfc-frontend/pages/meal/preference.vue

@@ -1,3 +1,4 @@
+<!-- DEPRECATED: 请使用 pages/diet/preferences 替代(功能已合并) -->
 <template>
   <view class="preference-page">
     <!-- 月食品预算 -->

+ 1 - 0
cfc-frontend/pages/meal/recipe-detail.vue

@@ -1,3 +1,4 @@
+<!-- DEPRECATED: 请使用 pages/growth/foods-detail 替代(功能已迁移) -->
 <template>
   <view class="recipe-detail">
     <view v-if="recipe" class="detail-content">

+ 1 - 0
cfc-frontend/pages/meal/recommend.vue

@@ -1,3 +1,4 @@
+<!-- DEPRECATED: 请使用 pages/diet/ 替代(功能已合并至 pages/diet/recommendation 和 pages/diet/index) -->
 <template>
   <view class="meal-recommend">
     <!-- 顶部配置 -->

+ 1 - 1
cfc-frontend/pages/member-detail/report-detail.vue

@@ -1,6 +1,6 @@
 <template>
   <view class="container">
-    <!-- 顶部导航 --></view>
+    <!-- 顶部导航 -->
 
     <!-- 加载状态 -->
     <view class="loading" v-if="loading">加载中...</view>

+ 1 - 1
cfc-frontend/pages/member-detail/service-role-apply.vue

@@ -1,6 +1,6 @@
 <template>
   <view class="container">
-    <!-- 顶部导航 --></view>
+    <!-- 顶部导航 -->
 
     <!-- 加载状态 -->
     <view class="loading" v-if="loading">加载中...</view>

+ 0 - 1
cfc-frontend/pages/mind-detail/member-mind-detail.vue

@@ -1,7 +1,6 @@
 <template>
   <view class="detail-container">
     <!-- 顶部导航 -->
-</view>
 
     <!-- 五维能量条 -->
     <FamilyEnergyBar

+ 0 - 1
cfc-frontend/pages/mind/member-mind-detail.vue

@@ -1,7 +1,6 @@
 <template>
   <view class="detail-container">
     <!-- 顶部导航 -->
-</view>
 
     <!-- 五维能量条 -->
     <FamilyEnergyBar

+ 208 - 0
cfc-frontend/pages/purchase/index.vue

@@ -0,0 +1,208 @@
+<template>
+  <view class="page">
+    <view class="nav-bar">
+      <view class="nav-back" @tap="goBack">
+        <text class="back-text">‹ 返回</text>
+      </view>
+      <text class="nav-title">健康启航计划</text>
+    </view>
+
+    <scroll-view class="content" scroll-y>
+      <!-- 头部横幅 -->
+      <view class="hero">
+        <text class="hero-title">健康启航计划</text>
+        <text class="hero-desc">199元解锁全年健康管理</text>
+        <view class="hero-price">
+          <text class="price-original">¥199</text>
+          <text class="price-note">7天全功能体验</text>
+        </view>
+      </view>
+
+      <!-- 权益对比 -->
+      <view class="card">
+        <view class="card-header">
+          <text class="card-title">您的权益</text>
+        </view>
+        <view class="benefit-list">
+          <view class="benefit-item" v-for="(item, idx) in benefits" :key="idx">
+            <text class="benefit-icon">{{ item.icon }}</text>
+            <text class="benefit-text">{{ item.text }}</text>
+            <text class="benefit-status" :class="item.active ? 'active' : 'inactive'">{{ item.active ? '✓ 已包含' : '✗ 需升级' }}</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- CF奖励计算器 -->
+      <view class="card">
+        <view class="card-header">
+          <text class="card-title">打卡返CF值</text>
+          <text class="card-subtitle">完成任务最高可得19,900 CF = 199元</text>
+        </view>
+        <view class="cf-calculator">
+          <view class="cf-row">
+            <text class="cf-label">基础奖励</text>
+            <text class="cf-value">300 CF</text>
+          </view>
+          <view class="cf-row">
+            <text class="cf-label">连续打卡加成</text>
+            <text class="cf-value">+420 CF</text>
+          </view>
+          <view class="cf-row">
+            <text class="cf-label">个性化任务</text>
+            <text class="cf-value">+180 CF</text>
+          </view>
+          <view class="cf-divider"></view>
+          <view class="cf-total">
+            <text class="cf-total-label">累计可得</text>
+            <text class="cf-total-value">19,900 CF</text>
+          </view>
+          <text class="cf-equivalent">= ¥199 全额返还</text>
+        </view>
+      </view>
+
+      <!-- 闯关任务预览 -->
+      <view class="card">
+        <view class="card-header">
+          <text class="card-title">3周闯关挑战</text>
+        </view>
+        <view class="challenge-preview">
+          <view class="week-item" v-for="(week, idx) in weeks" :key="idx">
+            <view class="week-header">
+              <text class="week-num">第{{ idx + 1 }}周</text>
+              <text class="week-reward">{{ week.reward }} CF</text>
+            </view>
+            <view class="week-tasks">
+              <text class="task-tag" v-for="(task, tidx) in week.tasks" :key="tidx">{{ task }}</text>
+            </view>
+          </view>
+        </view>
+      </view>
+
+      <!-- 购买按钮 -->
+      <view class="purchase-wrap">
+        <view class="purchase-btn" :class="{ 'loading': submitting }" @tap="handlePurchase">
+          <text class="btn-text">{{ submitting ? '处理中...' : '立即开启体验' }}</text>
+        </view>
+        <text class="purchase-tip">7天内可随时取消,首周打卡可获199元CF返还</text>
+      </view>
+    </scroll-view>
+  </view>
+</template>
+
+<script>
+import { checkPurchase, createPurchaseOrder } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      submitting: false,
+      benefits: [
+        { icon: '📋', text: '无限报告上传', active: true },
+        { icon: '🤖', text: 'AI报告解析', active: true },
+        { icon: '📝', text: '每日方案生成', active: true },
+        { icon: '🏆', text: '3周闯关挑战', active: true },
+        { icon: '💰', text: 'CF值奖励', active: true },
+        { icon: '👨‍👩‍👧', text: '家庭会员权益', active: false }
+      ],
+      weeks: [
+        { reward: '300 CF', tasks: ['首次报告上传', '连续3天打卡', '上传1份报告'] },
+        { reward: '300 CF', tasks: ['上传3份报告', '连续7天打卡', '累计3份报告'] },
+        { reward: '300 CF', tasks: ['完成3个挑战', '连续21天打卡', '上传5份报告'] }
+      ]
+    }
+  },
+  onLoad() {
+    this.checkStatus()
+  },
+  methods: {
+    goBack() { uni.navigateBack() },
+    async checkStatus() {
+      try {
+        var res = await checkPurchase()
+        if (res.data && res.data.hasPurchased && res.data.isActive) {
+          // 已有有效购买,跳转闯关进度
+          uni.redirectTo({ url: '/pages/growth/challenge-progress' })
+        }
+      } catch (e) {
+        console.log('检查购买状态失败', e)
+      }
+    },
+    handlePurchase() {
+      var self = this
+      self.submitting = true
+      createPurchaseOrder().then(function(res) {
+        self.submitting = false
+        if (res.data && res.data.success) {
+          // 跳转到支付页面(这里先提示,实际需对接支付)
+          uni.showModal({
+            title: '确认支付',
+            content: '支付199元开启7天全功能体验,完成挑战可返还199元CF值',
+            success: function(modalRes) {
+              if (modalRes.confirm) {
+                uni.showToast({ title: '支付功能开发中', icon: 'none' })
+              }
+            }
+          })
+        }
+      }).catch(function(err) {
+        self.submitting = false
+        uni.showToast({ title: '创建订单失败', icon: 'none' })
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page { min-height: 100vh; background: #FFF7ED; }
+.nav-bar { display: flex; align-items: center; height: 88rpx; padding-top: env(safe-area-inset-top); background: #fff; border-bottom: 1rpx solid #f0f0f0; position: relative; }
+.nav-back { position: absolute; left: 24rpx; top: 0; bottom: 0; display: flex; align-items: center; }
+.back-text { font-size: 28rpx; color: #F97316; }
+.nav-title { flex: 1; text-align: center; font-size: 32rpx; font-weight: bold; color: #333; }
+.content { height: calc(100vh - 88rpx); }
+
+.hero { background: linear-gradient(135deg, #FF8C42, #F97316); padding: 40rpx 32rpx; text-align: center; }
+.hero-title { font-size: 40rpx; font-weight: bold; color: #fff; display: block; }
+.hero-desc { font-size: 26rpx; color: rgba(255,255,255,0.9); margin-top: 8rpx; display: block; }
+.hero-price { margin-top: 24rpx; }
+.price-original { font-size: 56rpx; font-weight: bold; color: #fff; }
+.price-note { font-size: 22rpx; color: rgba(255,255,255,0.8); margin-left: 12rpx; }
+
+.card { margin: 16rpx 24rpx; background: #fff; border-radius: 20rpx; overflow: hidden; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
+.card-header { padding: 24rpx 24rpx 16rpx; border-bottom: 1rpx solid #f5f5f5; }
+.card-title { font-size: 28rpx; font-weight: bold; color: #333; }
+.card-subtitle { font-size: 22rpx; color: #999; margin-left: 12rpx; }
+
+.benefit-list { padding: 16rpx 24rpx; }
+.benefit-item { display: flex; align-items: center; gap: 16rpx; padding: 16rpx 0; border-bottom: 1rpx solid #f5f5f5; }
+.benefit-item:last-child { border-bottom: none; }
+.benefit-icon { font-size: 36rpx; }
+.benefit-text { flex: 1; font-size: 26rpx; color: #333; }
+.benefit-status { font-size: 22rpx; padding: 4rpx 12rpx; border-radius: 12rpx; }
+.benefit-status.active { background: #E8F5E9; color: #4CAF50; }
+.benefit-status.inactive { background: #F5F5F5; color: #999; }
+
+.cf-calculator { padding: 16rpx 24rpx; }
+.cf-row { display: flex; justify-content: space-between; padding: 8rpx 0; }
+.cf-label { font-size: 24rpx; color: #666; }
+.cf-value { font-size: 24rpx; color: #F97316; font-weight: bold; }
+.cf-divider { height: 1rpx; background: #e0e0e0; margin: 12rpx 0; }
+.cf-total { display: flex; justify-content: space-between; padding: 8rpx 0; }
+.cf-total-label { font-size: 26rpx; color: #333; font-weight: bold; }
+.cf-total-value { font-size: 32rpx; color: #F97316; font-weight: bold; }
+.cf-equivalent { font-size: 22rpx; color: #999; text-align: center; margin-top: 8rpx; display: block; }
+
+.challenge-preview { padding: 16rpx 24rpx; }
+.week-item { margin-bottom: 20rpx; }
+.week-item:last-child { margin-bottom: 0; }
+.week-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12rpx; }
+.week-num { font-size: 26rpx; font-weight: bold; color: #333; }
+.week-reward { font-size: 22rpx; color: #F97316; }
+.week-tasks { display: flex; flex-wrap: wrap; gap: 8rpx; }
+.task-tag { font-size: 22rpx; color: #666; background: #FFF3E0; padding: 6rpx 12rpx; border-radius: 12rpx; }
+
+.purchase-wrap { padding: 32rpx 24rpx 40rpx; text-align: center; }
+.purchase-btn { background: linear-gradient(135deg, #FF8C42, #F97316); color: #fff; text-align: center; padding: 28rpx 0; border-radius: 16rpx; font-size: 32rpx; font-weight: bold; }
+.purchase-btn.loading { opacity: 0.6; }
+.purchase-tip { font-size: 22rpx; color: #999; margin-top: 16rpx; display: block; }
+</style>

+ 167 - 0
cfc-frontend/pages/purchase/renewal.vue

@@ -0,0 +1,167 @@
+<template>
+  <view class="page">
+    <view class="nav-bar">
+      <view class="nav-back" @tap="goBack">
+        <text class="back-text">‹ 返回</text>
+      </view>
+      <text class="nav-title">续费升级</text>
+    </view>
+
+    <scroll-view class="content" scroll-y>
+      <!-- CF值余额 -->
+      <view class="cf-balance-card">
+        <text class="balance-label">您的CF值余额</text>
+        <text class="balance-value">{{ cfBalance }}</text>
+        <text class="balance-equivalent">{{ cfBalance }} CF = ¥{{ (cfBalance * 0.01).toFixed(2) }}</text>
+      </view>
+
+      <!-- 续费信息 -->
+      <view class="card">
+        <view class="card-header">
+          <text class="card-title">续费详情</text>
+        </view>
+        <view class="price-row">
+          <text class="price-label">家庭会员年费</text>
+          <text class="price-original">¥1,999</text>
+        </view>
+        <view class="price-row discount">
+          <text class="price-label">CF值抵扣</text>
+          <text class="price-discount">-¥{{ discountAmount }}</text>
+        </view>
+        <view class="price-divider"></view>
+        <view class="price-row total">
+          <text class="price-label">实付金额</text>
+          <text class="price-total">¥{{ payAmount }}</text>
+        </view>
+        <text class="price-note" v-if="discountPercent > 0">已抵扣 {{ discountPercent }}%({{ cfBalance }} CF值)</text>
+      </view>
+
+      <!-- 会员权益 -->
+      <view class="card">
+        <view class="card-header">
+          <text class="card-title">年费会员权益</text>
+        </view>
+        <view class="benefit-grid">
+          <view class="benefit-cell" v-for="(item, idx) in benefits" :key="idx">
+            <text class="benefit-cell-icon">{{ item.icon }}</text>
+            <text class="benefit-cell-text">{{ item.text }}</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 支付按钮 -->
+      <view class="pay-wrap">
+        <view class="pay-btn" :class="{ 'loading': submitting }" @tap="handlePay">
+          <text class="btn-text">{{ submitting ? '处理中...' : '立即续费' }}</text>
+        </view>
+        <text class="pay-tip">支持微信支付/支付宝,续费后即时生效</text>
+      </view>
+    </scroll-view>
+  </view>
+</template>
+
+<script>
+import { getRenewalInfo } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      submitting: false,
+      cfBalance: 0,
+      discount: 0,
+      payAmount: 199900
+    }
+  },
+  computed: {
+    discountAmount() {
+      return (this.discount * 0.01).toFixed(2)
+    },
+    discountPercent() {
+      return this.cfBalance >= 19900 ? 100 : Math.round(this.discount / 19900 * 100)
+    }
+  },
+  benefits: [
+    { icon: '📋', text: '无限报告上传' },
+    { icon: '🤖', text: 'AI报告解析' },
+    { icon: '📝', text: '无限方案生成' },
+    { icon: '🏆', text: '全量闯关奖励' },
+    { icon: '👨‍👩‍👧', text: '最多5名成员' },
+    { icon: '🎯', text: '专属规划师' }
+  ],
+  onLoad() {
+    this.loadInfo()
+  },
+  methods: {
+    goBack() { uni.navigateBack() },
+    async loadInfo() {
+      try {
+        var res = await getRenewalInfo()
+        if (res.data && res.data.success) {
+          this.cfBalance = res.data.cfBalance
+          this.discount = res.data.discount
+          this.payAmount = res.data.payAmount
+        }
+      } catch (e) {
+        console.log('加载续费信息失败', e)
+      }
+    },
+    handlePay() {
+      // TODO: 对接真实支付
+      uni.showModal({
+        title: '续费确认',
+        content: '确认支付 ¥' + (this.payAmount / 100).toFixed(2) + ' 续费家庭会员年费?',
+        success: (res) => {
+          if (res.confirm) {
+            this.submitting = true
+            setTimeout(() => {
+              this.submitting = false
+              uni.showToast({ title: '续费成功', icon: 'success' })
+              setTimeout(() => {
+                uni.navigateBack()
+              }, 1500)
+            }, 1000)
+          }
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page { min-height: 100vh; background: #FFF7ED; }
+.nav-bar { display: flex; align-items: center; height: 88rpx; padding-top: env(safe-area-inset-top); background: #fff; border-bottom: 1rpx solid #f0f0f0; position: relative; }
+.nav-back { position: absolute; left: 24rpx; top: 0; bottom: 0; display: flex; align-items: center; }
+.back-text { font-size: 28rpx; color: #F97316; }
+.nav-title { flex: 1; text-align: center; font-size: 32rpx; font-weight: bold; color: #333; }
+.content { height: calc(100vh - 88rpx); }
+
+.cf-balance-card { background: linear-gradient(135deg, #667eea, #764ba2); padding: 40rpx 32rpx; text-align: center; }
+.balance-label { font-size: 24rpx; color: rgba(255,255,255,0.8); display: block; }
+.balance-value { font-size: 64rpx; font-weight: bold; color: #fff; display: block; margin: 8rpx 0; }
+.balance-equivalent { font-size: 24rpx; color: rgba(255,255,255,0.9); display: block; }
+
+.card { margin: 16rpx 24rpx; background: #fff; border-radius: 20rpx; overflow: hidden; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
+.card-header { padding: 24rpx; border-bottom: 1rpx solid #f5f5f5; }
+.card-title { font-size: 28rpx; font-weight: bold; color: #333; }
+
+.price-row { display: flex; justify-content: space-between; align-items: center; padding: 16rpx 24rpx; }
+.price-label { font-size: 26rpx; color: #666; }
+.price-original { font-size: 26rpx; color: #999; text-decoration: line-through; }
+.price-row.discount { background: #FFF3E0; }
+.price-discount { font-size: 26rpx; color: #F97316; font-weight: bold; }
+.price-divider { height: 1rpx; background: #e0e0e0; margin: 0 24rpx; }
+.price-row.total { background: #FFF8E1; }
+.price-total { font-size: 36rpx; color: #F97316; font-weight: bold; }
+.price-note { font-size: 22rpx; color: #999; text-align: center; padding: 12rpx 24rpx 24rpx; display: block; }
+
+.benefit-grid { display: flex; flex-wrap: wrap; padding: 16rpx 24rpx; }
+.benefit-cell { width: calc(50% - 12rpx); display: flex; align-items: center; gap: 12rpx; padding: 16rpx 0; border-bottom: 1rpx solid #f5f5f5; }
+.benefit-cell-icon { font-size: 32rpx; }
+.benefit-cell-text { font-size: 24rpx; color: #333; }
+
+.pay-wrap { padding: 32rpx 24rpx 40rpx; text-align: center; }
+.pay-btn { background: linear-gradient(135deg, #667eea, #764ba2); color: #fff; text-align: center; padding: 28rpx 0; border-radius: 16rpx; font-size: 32rpx; font-weight: bold; }
+.pay-btn.loading { opacity: 0.6; }
+.pay-tip { font-size: 22rpx; color: #999; margin-top: 16rpx; display: block; }
+</style>

+ 0 - 1
cfc-frontend/pages/rewards/rewards.vue

@@ -1,6 +1,5 @@
 <template>
   <view class="container">
-</view>
     <view class="reward-list" v-if="rewards.length > 0">
       <view v-for="reward in rewards" :key="reward.id" class="reward-item">
         <view class="reward-info">

+ 0 - 1
cfc-frontend/pages/tianpan/annual-energy.vue

@@ -1,6 +1,5 @@
 <template>
 <view class="annual-energy-page">
-</view>
   <view v-if="loading" class="loading-state"><text>加载中...</text></view>
   <view v-else-if="error" class="error-state"><text class="error-text">{{ error }}</text></view>
   <view v-else-if="energy" class="content">

+ 0 - 1
cfc-frontend/pages/tianpan/daily-fortune.vue

@@ -1,6 +1,5 @@
 <template>
 <view class="daily-fortune-page">
-</view>
   <view v-if="loading" class="loading-state"><text>加载中...</text></view>
   <view v-else-if="error" class="error-state"><text class="error-text">{{ error }}</text></view>
   <view v-else-if="fortune" class="content">

+ 0 - 1
cfc-frontend/pages/tianpan/related-items.vue

@@ -1,6 +1,5 @@
 <template>
 <view class="related-items-page">
-</view>
   <view class="tab-bar">
     <view v-for="(tab, idx) in tabs" :key="idx" :class="'tab-item ' + (activeTab === idx ? 'active' : '')" @click="switchTab(idx)">
       <text class="tab-text">{{ tab.label }}</text>

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

@@ -1,7 +1,6 @@
 <template>
   <view class="detail-container">
     <!-- 顶部导航 -->
-</view>
 
     <!-- 五维能量条 -->
     <FamilyEnergyBar

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

@@ -1,7 +1,6 @@
 <template>
   <view class="detail-container">
     <!-- 顶部导航 -->
-</view>
 
     <!-- 五维能量条 -->
     <FamilyEnergyBar

+ 131 - 26
cfc-frontend/utils/api.js

@@ -186,7 +186,7 @@ export const wechatPhoneLogin = (data) => {
 
 // 获取平台公告
 export const getNotices = () => {
-  return request('/api/notices/list', 'GET')
+  return request('/api/notices/list', 'POST', {})
 }
 
 // 微信静默登录
@@ -439,7 +439,7 @@ export const getTaskHistory = (memberId, page = 1, size = 10) => {
 }
 
 export const deleteTask = (taskId) => {
-  return request(`/api/tasks/${taskId}`, 'DELETE')
+  return request(`/api/tasks/${taskId}`, 'POST', { _method: 'DELETE' })
 }
 
 export const batchCompleteTasks = (taskIds) => {
@@ -574,11 +574,11 @@ export const requestWishExchange = (id) => {
 }
 
 export const confirmWishReceived = (id) => {
-  return request(`/api/wishes/${id}/confirm`, 'PUT', {})
+  return request(`/api/wishes/${id}/confirm`, 'POST', {})
 }
 
 export const cancelWish = (id, reason) => {
-  return request(`/api/wishes/${id}`, 'DELETE', { reason })
+  return request(`/api/wishes/${id}`, 'POST', { _method: 'DELETE', reason })
 }
 
 // ===
@@ -601,7 +601,7 @@ export const confirmBind = (data) => {
 
 // 解除绑定
 export const unbindFamily = (id) => {
-  return request(`/api/guide/bind/${id}`, 'DELETE')
+  return request(`/api/guide/bind/${id}`, 'POST', { _method: 'DELETE' })
 }
 
 // 创建任务模板
@@ -611,7 +611,7 @@ export const createPackage = (data) => {
 
 // 更新任务模板
 export const updatePackage = (id, data) => {
-  return request(`/api/packages/${id}`, 'PUT', data)
+  return request('/api/packages/' + id, 'POST', Object.assign({ _method: 'PUT' }, data))
 }
 
 // 获取任务模板详情
@@ -641,12 +641,12 @@ export const addPackageItem = (packageId, data) => {
 
 // 更新任务项
 export const updatePackageItem = (itemId, data) => {
-  return request(`/api/packages/items/${itemId}`, 'PUT', data)
+  return request('/api/packages/items/' + itemId, 'POST', { _method: 'PUT' }, data)
 }
 
 // 删除任务项
 export const deletePackageItem = (itemId) => {
-  return request(`/api/packages/items/${itemId}`, 'DELETE')
+  return request('/api/packages/items/' + itemId, 'POST', { _method: 'DELETE' })
 }
 
 // 获取任务模板任务项
@@ -706,12 +706,12 @@ export const generateInviteCode = () => {
 
 // 获取邀请码列表
 export const getInviteCodes = () => {
-  return request('/api/guide/invite-codes', 'GET')
+  return request('/api/guide/invite-codes', 'POST', {})
 }
 
 // 验证邀请码
 export const validateInviteCode = (code) => {
-  return request('/api/guide/invite-code/validate', 'GET', null, { code })
+  return request('/api/guide/invite-code/validate', 'POST', { code })
 }
 
 export const checkTeamInviteStatus = (inviteCode) => {
@@ -728,17 +728,17 @@ export const leaveCurrentTeam = () => {
 
 // 获取下级成长规划师列表
 export const getChildGuides = () => {
-  return request('/api/guide/child-guides', 'GET')
+  return request('/api/guide/child-guides', 'POST', {})
 }
 
 // 获取所有下级成长规划师
 export const getAllChildGuides = () => {
-  return request('/api/guide/all-child-guides', 'GET')
+  return request('/api/guide/all-child-guides', 'POST', {})
 }
 
 // 获取上级成长规划师
 export const getParentGuide = () => {
-  return request('/api/guide/parent-guide', 'GET')
+  return request('/api/guide/parent-guide', 'POST', {})
 }
 
 // ===== 训练方案管理 =====
@@ -815,32 +815,32 @@ export const addGuidePackage = (guideId, packageId, price, commissionRate) => {
 
 // 获取成长规划师套餐列表
 export const getGuidePackages = (guideId) => {
-  return request('/api/guide/packages', 'GET', null, { guideId })
+  return request('/api/guide/packages/my', 'POST', { guideId: guideId })
 }
 
 // 获取成长规划师套餐定价
 export const getGuidePackage = (guideId, packageId) => {
-  return request('/api/guide/package/' + packageId, 'GET', null, { guideId })
+  return request('/api/guide/package/' + packageId, 'POST', { guideId: guideId })
 }
 
 // 更新成长规划师套餐价格
 export const updateGuidePackagePrice = (guideId, packageId, price) => {
-  return request('/api/guide/package/' + packageId, 'PUT', { guideId, price })
+  return request('/api/guide/package/' + packageId + '/update', 'POST', { guideId: guideId, price: price })
 }
 
 // 删除成长规划师套餐
 export const deleteGuidePackage = (guideId, packageId) => {
-  return request('/api/guide/package/' + packageId, 'DELETE', null, { guideId })
+  return request('/api/guide/package/' + packageId + '/delete', 'POST', { guideId: guideId })
 }
 
 // 获取套餐价格
 export const getPackagePrice = (packageId, guideId, inviteCode) => {
-  return request('/api/package/price', 'GET', null, { packageId, guideId, inviteCode })
+  return request('/api/package/price', 'POST', { packageId, guideId, inviteCode })
 }
 
 // 获取可用的成长规划师列表
 export const getAvailableGuides = (packageId) => {
-  return request('/api/package/available-guides', 'GET', null, { packageId })
+  return request('/api/package/available-guides', 'POST', { packageId })
 }
 
 // 创建套餐订单
@@ -850,7 +850,7 @@ export const createPackageOrder = (data) => {
 
 // 获取订单状态
 export const getOrderStatus = (orderNo) => {
-  return request('/api/payment/order/status/' + orderNo, 'GET')
+  return request('/api/payment/order/status/' + orderNo, 'POST', {})
 }
 
 // 创建微信支付订单
@@ -1045,7 +1045,7 @@ export const getGameLeaderboard = (params) => {
 
 // 邀请码相关
 export const getInviteCode = () => {
-  return request('/api/family/invite-code', 'GET')
+  return request('/api/family/invite-code', 'POST', {})
 }
 
 export const generateFamilyInviteCode = () => {
@@ -1855,7 +1855,7 @@ export const joinGeneralCircle = (circleId, memberId, memberType) => {
 export const leaveGeneralCircle = (circleId, memberId, memberType) => {
   return request('/api/circle/leave', 'POST', { circleId: circleId, memberId: memberId, memberType: memberType || 'child' })
 }
-export const createCircle = (data) => request('/api/circle/create', 'POST', data)
+export const createGeneralCircle = (data) => request('/api/circle/create', 'POST', data)
 export const updateCircle = (data) => request('/api/circle/update', 'POST', data)
 export const getCircleFeed = (data) => request('/api/circle/feed', 'POST', data)
 export const getMultiCircleFeed = (data) => request('/api/circle/feed/multi', 'POST', data)
@@ -1944,7 +1944,12 @@ export const getGutFloraIndicators = (data) => request('/api/health/gut-flora',
 
 
 export const createHealthReport = (data) => request('/api/health/report/create', 'POST', data)
-export const getReportList = (memberId) => request('/api/health/report/list', 'POST', { memberId })
+// 健康报告列表:memberId 可选,不传时后端返回当前家庭的全部报告
+export const getReportList = (memberId) => {
+  var data = {}
+  if (memberId) { data.memberId = memberId }
+  return request('/api/health/report/list', 'POST', data)
+}
 export const getFamilyReports = () => request('/api/health/reports/family', 'POST', {})
 export const getReportDetail = (reportId) => request('/api/health/report/detail', 'POST', { reportId })
 // DAN 报告详情(含 blocks)
@@ -2201,7 +2206,7 @@ export const listInteractions = (data) => {
 }
 
 export const getInteractionTypes = () => {
-  return request('/api/family/interaction/types', 'GET', {})
+  return request('/api/family/interaction/types', 'POST', {})
 }
 
 // ===== 鍏崇郴闂嵎 =====
@@ -2214,11 +2219,11 @@ export const submitQuestionnaire = (data) => {
 }
 
 export const getQuestionnaireSnapshot = (memberId) => {
-  return request('/api/family/questionnaire/snapshot/' + memberId, 'GET', {})
+  return request('/api/family/questionnaire/snapshot/' + memberId, 'POST', {})
 }
 
 export const getQuestionnaireHistory = (memberId) => {
-  return request('/api/family/questionnaire/history/' + memberId, 'GET', {})
+  return request('/api/family/questionnaire/history/' + memberId, 'POST', {})
 }
 
 // ===== 五维家庭自检(P0-1)=====
@@ -2605,3 +2610,103 @@ export const sttTranscribe = (filePath) => {
     })
   })
 }
+
+// 健康启航计划
+export function checkPurchase() {
+  return new Promise((resolve, reject) => {
+    const token = uni.getStorageSync('token')
+    uni.request({
+      url: config.API_BASE_URL + '/api/purchase/check',
+      method: 'POST',
+      data: {},
+      header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
+      success: (res) => {
+        if (res.data && res.data.code === 200) {
+          resolve(res.data)
+        } else {
+          reject(res.data || { code: -1, message: '检查购买状态失败' })
+        }
+      },
+      fail: (err) => reject(err)
+    })
+  })
+}
+
+export function createPurchaseOrder() {
+  return new Promise((resolve, reject) => {
+    const token = uni.getStorageSync('token')
+    uni.request({
+      url: config.API_BASE_URL + '/api/purchase/create',
+      method: 'POST',
+      data: {},
+      header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
+      success: (res) => {
+        if (res.data && res.data.code === 200) {
+          resolve(res.data)
+        } else {
+          reject(res.data || { code: -1, message: '创建订单失败' })
+        }
+      },
+      fail: (err) => reject(err)
+    })
+  })
+}
+
+export function confirmPurchase(data) {
+  return new Promise((resolve, reject) => {
+    const token = uni.getStorageSync('token')
+    uni.request({
+      url: config.API_BASE_URL + '/api/purchase/confirm',
+      method: 'POST',
+      data: data,
+      header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
+      success: (res) => {
+        if (res.data && res.data.code === 200) {
+          resolve(res.data)
+        } else {
+          reject(res.data || { code: -1, message: '确认支付失败' })
+        }
+      },
+      fail: (err) => reject(err)
+    })
+  })
+}
+
+export function getRenewalInfo() {
+  return new Promise((resolve, reject) => {
+    const token = uni.getStorageSync('token')
+    uni.request({
+      url: config.API_BASE_URL + '/api/purchase/renewal-info',
+      method: 'POST',
+      data: {},
+      header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
+      success: (res) => {
+        if (res.data && res.data.code === 200) {
+          resolve(res.data)
+        } else {
+          reject(res.data || { code: -1, message: '获取续费信息失败' })
+        }
+      },
+      fail: (err) => reject(err)
+    })
+  })
+}
+
+export function getChallengeProgress(memberId) {
+  return new Promise((resolve, reject) => {
+    const token = uni.getStorageSync('token')
+    uni.request({
+      url: config.API_BASE_URL + '/api/purchase/challenge/progress?memberId=' + memberId,
+      method: 'GET',
+      header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
+      success: (res) => {
+        if (res.data && res.data.code === 200) {
+          resolve(res.data)
+        } else {
+          reject(res.data || { code: -1, message: '获取闯关进度失败' })
+        }
+      },
+      fail: (err) => reject(err)
+    })
+  })
+}

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-1e9da2cfb9d5095aa8c44fcf4233cf79d216b2d7
+8521180d9ceebdefc7e1ec36e373f54f55d6d055

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1194",
+  "version": "1.0.1206",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.1194",
+      "version": "1.0.1206",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1195",
+  "version": "1.0.1207",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

+ 142 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,148 @@
 
 ---
 
+## v1.0.1207 (2026-08-23)
+
+### 新功能
+- 家庭端管家自助选择接口
+- ButlerService新增管家浏览/查询/自助绑定方法
+
+### Bug 修复
+- member toggle + race condition + generate string data
+- 移除打卡页navigationStyle:custom,恢复原生导航栏标题和返回按钮
+
+### 其他
+- - Prevent onLoad + onShow double-load race with _executorsLoaded flag
+- - Only loadExecutors in onLoad when not in planId/viewMode path
+- - Handle generateHealthPlan API returning data as markdown string (typeof check)
+- - Reset selectedMemberIds and userGoal in reCreate()
+- Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+
+
+## v1.0.1206 (2026-08-23)
+
+### 新功能
+- 新增butler_assignments实体与Mapper
+- butler_profiles 新增accepting停接开关列
+- Web管理端购买记录管理
+
+### 文档
+- 总览登记AI健康教练人格分化与管家自助选择条目
+
+
+## v1.0.1205 (2026-08-23)
+
+### 新功能
+- 报告列表支持家庭维度查询+成员过滤,健康页不再传memberId
+
+
+## v1.0.1204 (2026-08-23)
+
+### 新功能
+- 添加续费页和闯关进度页
+
+
+## v1.0.1203 (2026-08-23)
+
+### 文档
+- 新增AI健康教练人格分化与管家自助选择设计规格
+- 健康启航计划实现计划
+
+### 其他
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+
+### 新功能
+- 添加购买页和API封装
+- 付费墙拦截+前端API+页面注册
+- 健康启航计划后端核心
+
+
+## v1.0.1202 (2026-08-22)
+
+### 其他
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+
+### 文档
+- API参考文档补全13个Controller共65+接口及废弃状态更新
+
+### Bug 修复
+- 废弃接口返回410及补充@Deprecated注解(Family/Butler/Notice)
+- childId→familyMemberId 引用统一(19处TaskService)
+
+### 新功能
+- energy daily limit 数据库持久化 - 新增DimensionDailyLimit表及Controller读写
+
+
+## v1.0.1201 (2026-08-22)
+
+### 文档
+- 新增财富功能重构计划与设计方案
+
+### Bug 修复
+- games/list.vue 移除错误编辑导致的孤儿view标签
+
+
+## v1.0.1200 (2026-08-22)
+
+### 重构
+- 合并重复接口-删除死代码GuidePackageController,统一所有接口为POST
+
+
+## v1.0.1199 (2026-08-22)
+
+### Bug 修复
+- 修复多根元素及语法错误(games/list, gut-flora-detail, action-detail, api.js)
+
+
+## v1.0.1198 (2026-08-22)
+
+### Bug 修复
+- 修复 f048448e3 移除nav-bar导致的45个页面模板view标签不平衡
+
+### 其他
+- - 删除了根<view class='page'>但保留了对应的</view>
+- - 删除了nav-bar的<view>打开标签但保留了</view>
+- 导致模板出现多根元素或孤立</view>,HBuilderX编译报错:
+- 'Element is missing end tag' / 'Multi-root element found' / 'Root element expected'
+- 修复方式: 删除所有孤立的</view>标签(共45个文件)
+- 
+
+
+## v1.0.1197 (2026-08-22)
+
+### 文档
+- 更新健康启航计划——体验期内升级享199元抵扣
+
+
+## v1.0.1196 (2026-08-22)
+
+### 文档
+- 更新健康启航计划——CF值1CF=0.01元,全勤19900CF抵199元
+
+### Bug 修复
+- use beam_size=1 + vad_filter for faster STT inference
+- delay first kb_sync by 120s, increase gunicorn timeout to 600s
+
+### 新功能
+- add voice input button to AI chat page
+- add AiGateway.transcribe + /api/ai/transcribe endpoint
+- integrate STT into diet/exercise check-in pages
+- add sttTranscribe API utility for speech-to-text
+- add STT endpoint with faster-whisper base model
+
+
 ## v1.0.1195 (2026-08-22)
 
 ### 新功能

+ 143 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.1195
+> 当前版本: v1.0.1207
 
 ## 历史版本
 
@@ -8,6 +8,148 @@
 
 ---
 
+## v1.0.1207 (2026-08-23)
+
+### 新功能
+- 家庭端管家自助选择接口
+- ButlerService新增管家浏览/查询/自助绑定方法
+
+### Bug 修复
+- member toggle + race condition + generate string data
+- 移除打卡页navigationStyle:custom,恢复原生导航栏标题和返回按钮
+
+### 其他
+- - Prevent onLoad + onShow double-load race with _executorsLoaded flag
+- - Only loadExecutors in onLoad when not in planId/viewMode path
+- - Handle generateHealthPlan API returning data as markdown string (typeof check)
+- - Reset selectedMemberIds and userGoal in reCreate()
+- Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+
+
+## v1.0.1206 (2026-08-23)
+
+### 新功能
+- 新增butler_assignments实体与Mapper
+- butler_profiles 新增accepting停接开关列
+- Web管理端购买记录管理
+
+### 文档
+- 总览登记AI健康教练人格分化与管家自助选择条目
+
+
+## v1.0.1205 (2026-08-23)
+
+### 新功能
+- 报告列表支持家庭维度查询+成员过滤,健康页不再传memberId
+
+
+## v1.0.1204 (2026-08-23)
+
+### 新功能
+- 添加续费页和闯关进度页
+
+
+## v1.0.1203 (2026-08-23)
+
+### 文档
+- 新增AI健康教练人格分化与管家自助选择设计规格
+- 健康启航计划实现计划
+
+### 其他
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+
+### 新功能
+- 添加购买页和API封装
+- 付费墙拦截+前端API+页面注册
+- 健康启航计划后端核心
+
+
+## v1.0.1202 (2026-08-22)
+
+### 其他
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+
+### 文档
+- API参考文档补全13个Controller共65+接口及废弃状态更新
+
+### Bug 修复
+- 废弃接口返回410及补充@Deprecated注解(Family/Butler/Notice)
+- childId→familyMemberId 引用统一(19处TaskService)
+
+### 新功能
+- energy daily limit 数据库持久化 - 新增DimensionDailyLimit表及Controller读写
+
+
+## v1.0.1201 (2026-08-22)
+
+### 文档
+- 新增财富功能重构计划与设计方案
+
+### Bug 修复
+- games/list.vue 移除错误编辑导致的孤儿view标签
+
+
+## v1.0.1200 (2026-08-22)
+
+### 重构
+- 合并重复接口-删除死代码GuidePackageController,统一所有接口为POST
+
+
+## v1.0.1199 (2026-08-22)
+
+### Bug 修复
+- 修复多根元素及语法错误(games/list, gut-flora-detail, action-detail, api.js)
+
+
+## v1.0.1198 (2026-08-22)
+
+### Bug 修复
+- 修复 f048448e3 移除nav-bar导致的45个页面模板view标签不平衡
+
+### 其他
+- - 删除了根<view class='page'>但保留了对应的</view>
+- - 删除了nav-bar的<view>打开标签但保留了</view>
+- 导致模板出现多根元素或孤立</view>,HBuilderX编译报错:
+- 'Element is missing end tag' / 'Multi-root element found' / 'Root element expected'
+- 修复方式: 删除所有孤立的</view>标签(共45个文件)
+- 
+
+
+## v1.0.1197 (2026-08-22)
+
+### 文档
+- 更新健康启航计划——体验期内升级享199元抵扣
+
+
+## v1.0.1196 (2026-08-22)
+
+### 文档
+- 更新健康启航计划——CF值1CF=0.01元,全勤19900CF抵199元
+
+### Bug 修复
+- use beam_size=1 + vad_filter for faster STT inference
+- delay first kb_sync by 120s, increase gunicorn timeout to 600s
+
+### 新功能
+- add voice input button to AI chat page
+- add AiGateway.transcribe + /api/ai/transcribe endpoint
+- integrate STT into diet/exercise check-in pages
+- add sttTranscribe API utility for speech-to-text
+- add STT endpoint with faster-whisper base model
+
+
 ## v1.0.1195 (2026-08-22)
 
 ### 新功能

+ 218 - 0
cfc-web/src/views/admin/PurchaseManagement.vue

@@ -0,0 +1,218 @@
+<template>
+  <div class="purchase-mgmt admin-page">
+    <el-card>
+      <div slot="header" class="admin-page-header">
+        <span class="admin-page-title">健康启航计划管理</span>
+        <el-button type="primary" size="mini" @click="loadList" :loading="loading">刷新</el-button>
+      </div>
+
+      <!-- 统计卡片 -->
+      <el-row :gutter="16" style="margin-bottom: 16px;">
+        <el-col :span="6">
+          <el-card shadow="hover" class="stat-card">
+            <div class="stat-value">{{ stats.totalOrders }}</div>
+            <div class="stat-label">总订单数</div>
+          </el-card>
+        </el-col>
+        <el-col :span="6">
+          <el-card shadow="hover" class="stat-card">
+            <div class="stat-value">{{ stats.paidOrders }}</div>
+            <div class="stat-label">已支付</div>
+          </el-card>
+        </el-col>
+        <el-col :span="6">
+          <el-card shadow="hover" class="stat-card">
+            <div class="stat-value">{{ stats.activeUsers }}</div>
+            <div class="stat-label">活跃体验用户</div>
+          </el-card>
+        </el-col>
+        <el-col :span="6">
+          <el-card shadow="hover" class="stat-card">
+            <div class="stat-value">{{ stats.revenue }}</div>
+            <div class="stat-label">总收入(元)</div>
+          </el-card>
+        </el-col>
+      </el-row>
+
+      <!-- 筛选 -->
+      <el-form :inline="true" size="small" style="margin-bottom: 16px;">
+        <el-form-item label="状态">
+          <el-select v-model="filter.status" placeholder="全部" clearable style="width: 120px;">
+            <el-option label="待支付" value="pending" />
+            <el-option label="已支付" value="paid" />
+            <el-option label="已取消" value="cancelled" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="搜索">
+          <el-input v-model="filter.keyword" placeholder="用户ID/订单号" clearable style="width: 200px;" @keyup.enter.native="loadList" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" size="small" @click="loadList">查询</el-button>
+        </el-form-item>
+      </el-form>
+
+      <!-- 列表 -->
+      <el-table :data="list" v-loading="loading" border stripe style="width: 100%;">
+        <el-table-column prop="orderNo" label="订单号" width="180" />
+        <el-table-column prop="userId" label="用户ID" width="100" />
+        <el-table-column prop="familyId" label="家庭ID" width="100" />
+        <el-table-column prop="amount" label="金额(分)" width="100">
+          <template slot-scope="{ row }">{{ row.amount / 100 }}元</template>
+        </el-table-column>
+        <el-table-column prop="status" label="状态" width="100">
+          <template slot-scope="{ row }">
+            <el-tag :type="statusType(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="startDate" label="开始时间" width="160">
+          <template slot-scope="{ row }">{{ formatDateTime(row.startDate) }}</template>
+        </el-table-column>
+        <el-table-column prop="endDate" label="结束时间" width="160">
+          <template slot-scope="{ row }">{{ formatDateTime(row.endDate) }}</template>
+        </el-table-column>
+        <el-table-column prop="isEarlyRenewal" label="早期续费" width="90">
+          <template slot-scope="{ row }">
+            <el-tag :type="row.isEarlyRenewal ? 'success' : 'info'" size="small">{{ row.isEarlyRenewal ? '是' : '否' }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="createdAt" label="创建时间" width="160">
+          <template slot-scope="{ row }">{{ formatDateTime(row.createdAt) }}</template>
+        </el-table-column>
+        <el-table-column label="操作" width="120" fixed="right">
+          <template slot-scope="{ row }">
+            <el-button type="text" size="small" @click="viewDetail(row)">详情</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页 -->
+      <el-pagination
+        style="margin-top: 16px; text-align: right;"
+        layout="total, prev, pager, next"
+        :total="total"
+        :current-page="page"
+        :page-size="size"
+        @current-change="handlePageChange"
+      />
+    </el-card>
+
+    <!-- 详情弹窗 -->
+    <el-dialog title="订单详情" :visible.sync="dialogVisible" width="600px">
+      <el-descriptions :column="2" border size="small" v-if="currentOrder">
+        <el-descriptions-item label="订单号">{{ currentOrder.orderNo }}</el-descriptions-item>
+        <el-descriptions-item label="用户ID">{{ currentOrder.userId }}</el-descriptions-item>
+        <el-descriptions-item label="家庭ID">{{ currentOrder.familyId }}</el-descriptions-item>
+        <el-descriptions-item label="金额">{{ currentOrder.amount / 100 }}元</el-descriptions-item>
+        <el-descriptions-item label="状态">
+          <el-tag :type="statusType(currentOrder.status)" size="small">{{ statusText(currentOrder.status) }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="支付方式">{{ currentOrder.paymentMethod || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="开始时间">{{ formatDateTime(currentOrder.startDate) }}</el-descriptions-item>
+        <el-descriptions-item label="结束时间">{{ formatDateTime(currentOrder.endDate) }}</el-descriptions-item>
+        <el-descriptions-item label="是否早期续费">{{ currentOrder.isEarlyRenewal ? '是' : '否' }}</el-descriptions-item>
+        <el-descriptions-item label="已发放CF值">{{ currentOrder.cfRewardGiven || 0 }} CF</el-descriptions-item>
+        <el-descriptions-item label="创建时间">{{ formatDateTime(currentOrder.createdAt) }}</el-descriptions-item>
+        <el-descriptions-item label="支付时间">{{ formatDateTime(currentOrder.paidAt) }}</el-descriptions-item>
+      </el-descriptions>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import request from '@/utils/request'
+
+export default {
+  name: 'PurchaseManagement',
+  data() {
+    return {
+      loading: false,
+      list: [],
+      total: 0,
+      page: 1,
+      size: 20,
+      filter: { status: '', keyword: '' },
+      dialogVisible: false,
+      currentOrder: null,
+      stats: {
+        totalOrders: 0,
+        paidOrders: 0,
+        activeUsers: 0,
+        revenue: '0.00'
+      }
+    }
+  },
+  created() {
+    this.loadList()
+    this.loadStats()
+  },
+  methods: {
+    async loadList() {
+      this.loading = true
+      try {
+        const res = await request({
+          url: '/api/admin/purchase/list',
+          method: 'post',
+          data: { page: this.page, size: this.size, ...this.filter }
+        })
+        if (res.data && res.data.code === 200) {
+          this.list = res.data.data.records || []
+          this.total = res.data.data.total || 0
+        }
+      } catch (e) {
+        console.error('加载列表失败', e)
+      } finally {
+        this.loading = false
+      }
+    },
+    async loadStats() {
+      try {
+        const res = await request({
+          url: '/api/admin/purchase/stats',
+          method: 'post'
+        })
+        if (res.data && res.data.code === 200) {
+          this.stats = res.data.data
+        }
+      } catch (e) {
+        console.error('加载统计失败', e)
+      }
+    },
+    viewDetail(row) {
+      this.currentOrder = row
+      this.dialogVisible = true
+    },
+    handlePageChange(page) {
+      this.page = page
+      this.loadList()
+    },
+    statusType(status) {
+      const map = { pending: 'warning', paid: 'success', cancelled: 'info' }
+      return map[status] || ''
+    },
+    statusText(status) {
+      const map = { pending: '待支付', paid: '已支付', cancelled: '已取消' }
+      return map[status] || status
+    },
+    formatDateTime(dateStr) {
+      if (!dateStr) return '-'
+      const d = new Date(dateStr)
+      if (isNaN(d)) return dateStr.toString().substring(0, 19)
+      return d.getFullYear() + '-' +
+        String(d.getMonth() + 1).padStart(2, '0') + '-' +
+        String(d.getDate()).padStart(2, '0') + ' ' +
+        String(d.getHours()).padStart(2, '0') + ':' +
+        String(d.getMinutes()).padStart(2, '0') + ':' +
+        String(d.getSeconds()).padStart(2, '0')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.admin-page { padding: 16px; }
+.admin-page-header { display: flex; justify-content: space-between; align-items: center; }
+.admin-page-title { font-size: 16px; font-weight: bold; }
+.stat-card { text-align: center; }
+.stat-value { font-size: 28rpx; font-weight: bold; color: #F97316; }
+.stat-label { font-size: 12px; color: #999; margin-top: 4px; }
+</style>

+ 6 - 3
docs/superpowers/PROJECT-OVERVIEW.md

@@ -1,8 +1,8 @@
 # 浠艾福 项目全景 — 阶段性需求与设计汇总
 
-**文档版本:** v2.8
-**日期:** 2026-08-15
-**状态:** 已确认(v2.1 Phase 2-4 全栈完成)+ 虚拟支付改造(Tasks 1-12 已完成,退款闭环实施中)+ 新用户注册引导(12 Tasks 全栈完成)+ 家庭成员关系条增强(✅ 已实施)+ TabBar 重构(✅ 4Tab + 中间五维启动器,已提交)+ **LIFETIME 终身会员(✅ 11 Tasks 全栈完成)** + **健康数据中心(✅ 全栈完成)** + **SKU 价格单位统一分 + 规格选择响应式修复(✅ 7 Tasks 全栈完成)**
+**文档版本:** v2.9
+**日期:** 2026-08-23
+**状态:** 已确认(v2.1 Phase 2-4 全栈完成)+ 虚拟支付改造(Tasks 1-12 已完成,退款闭环实施中)+ 新用户注册引导(12 Tasks 全栈完成)+ 家庭成员关系条增强(✅ 已实施)+ TabBar 重构(✅ 4Tab + 中间五维启动器,已提交)+ **LIFETIME 终身会员(✅ 11 Tasks 全栈完成)** + **健康数据中心(✅ 全栈完成)** + **SKU 价格单位统一分 + 规格选择响应式修复(✅ 7 Tasks 全栈完成)** + **AI健康教练人格分化与管家自助选择(🔄 实施中,11 Tasks)**
 **维护:** 所有需求变更需更新本文档
 
 ---
@@ -210,6 +210,7 @@
 | 订阅+推广核心实施(P0) | `plans/2026-07-09-family-membership-core.md` |
 | AI健康管家+推荐引擎(P1) | `plans/2026-07-22-comprehensive-implementation.md`(Phase 2 Wave 3) |
 | **LIFETIME 终身会员(方案A:等级扩展+实时聚合+管理员审核)** | `specs/2026-08-07-xaf-partner-ppt-redesign-design.md` + `plans/2026-08-09-lifetime-membership.md` |
+| **AI健康教练人格分化与管家自助选择**(L2 自助绑管/换绑 + 浠宝/福宝人格话术 + 管理端分配管理与停接开关) 🔄 实施中 | `specs/2026-08-23-coach-butler-design.md` + `plans/2026-08-23-coach-butler.md` |
 
 ---
 
@@ -357,6 +358,7 @@
 | `2026-08-18-homepage-health-funnel-design.md` | ✅ 已实施(四段式漏斗:了解区自测卡/发现区沙盘+上传报告/行动区聚合/内容区降级;未登录态零改动) | 首页重构 — 主动健康漏斗(了解→发现→改变) |
 | `2026-08-13-health-status-survey-design.md` | 🔄 v2 设计已定稿(2026-08-18;v1 已实施;v2 疾病史两源预置清单+确诊时间+服药时长) | 当前状态调研 — 健康现状档案 |
 | `2026-08-18-health-status-survey-v2.md` | 🟢 已实施(单文件改 health-status-form.vue;8 组 54 项预置清单分类多选+确诊时间;用药年月+时长自动计算;后端零变更;v1 旧数据兼容) | 当前状态调研 — 健康现状档案 v2 |
+| `2026-08-23-coach-butler-design.md` | 🔄 实施中 | AI健康教练人格分化(浠宝/福宝话术路由)与 L2 家庭管家自助选择设计 |
 | `api/API_REFERENCE.md` | 🟢 已建立(2026-08-18;200+ 接口清单;废弃接口标注;新增接口检查流程) | 后台接口参考文档 |
 
 ### 实施计划(plans/)
@@ -428,6 +430,7 @@
 | `2026-08-10-unlock-gates-implementation.md` | 🟡 待实施 | 通关解锁功能实施计划(三关递进:自检→个人沙盘/邀请→全家沙盘/创建→微行动;沙盘个人全家切换;8种判定+奖励发放;增值关卡购物/会员/报告;管理端关卡配置) |
 | `2026-08-15-daily-task-overview.md` | 🟡 待实施 | 今日任务首页卡片实施计划(仅小程序接入 + 1个只读聚合接口 `/api/daily-task/overview`,聚合打卡/任务/阅读活动/测评报告四类,点击跳转+返回自动刷新;设计稿:2026-08-15-daily-task-overview-design.md) |
 | `2026-08-15-sku-price-cents-and-spec-selector.md` | ✅ 已实施(7 Tasks:实体/DTO/订单/购物车/迁移/管理端/小程序,5 commits) | SKU 规格价格统一分单位 + 详情页规格按钮修复实施计划 |
+| `2026-08-23-coach-butler.md` | 🔄 实施中(11 Tasks:DB迁移/实体Mapper/Service/控制器×2/AI注入/LangGraph路由/小程序/Web管理端/文档验收) | AI健康教练人格分化与管家自助选择实施计划(设计稿:2026-08-23-coach-butler-design.md) |
 
 ### 计划与设计文档(specs/)
 

+ 166 - 9
docs/superpowers/api/API_REFERENCE.md

@@ -398,8 +398,14 @@ find cfc-backend/src/main/java -name "*XxxService.java" -o -name "*XxxController
 | `/api/admin/dimension/*` | 维度配置 |
 | `/api/admin/dimension-config/*` | 维度权重 |
 | `/api/admin/energy/*` | 能量配置 |
+| `/api/admin/energy-config/*` | 五行能量来源/每日限制配置 |
+| `/api/admin/ecom-supplier/*` | 电商供应商管理 |
+| `/api/admin/guide/*` | 成长规划师管理(申请/套餐) |
+| `/api/admin/growth-archive-product/*` | 成长档案关联商品 |
 | `/api/admin/knowledge-base/*` | 知识库管理 |
+| `/api/admin/notices/*` | 公告管理 |
 | `/api/admin/package-templates/*` | 套餐模板 |
+| `/api/admin/ppoint-config/*` | P点配置(品类级比例+商品级固定值) |
 | `/api/admin/product/*` | 商品管理 |
 | `/api/admin/report-parser/*` | 报告解析配置 |
 | `/api/admin/report-template/*` | 报告展示模板管理 |
@@ -458,6 +464,156 @@ find cfc-backend/src/main/java -name "*XxxService.java" -o -name "*XxxController
 | `POST /api/wealth/checkin/*` | 财富打卡 |
 | `POST /api/insurance/*` | 保险规划 |
 
+### 4.17 日常任务聚合(`/api/daily-task/*`)
+
+| 路径 | 说明 |
+|------|------|
+| `POST /api/daily-task/overview` | 今日任务总览(打卡/任务/阅读活动/测评报告四类聚合) |
+
+### 4.18 健康维度评分(`/api/dimension/*`)
+
+| 路径 | 说明 |
+|------|------|
+| `POST /api/dimension/overview` | 获取健康维度概览(七维评分+百分位) |
+| `POST /api/dimension/upload` | 手动录入维度评分 |
+| `POST /api/dimension/questionnaire` | 问卷提交维度评分 |
+| `POST /api/dimension/history` | 获取维度历史记录 |
+
+### 4.19 家庭成员关系质量(`/api/family/relationship/*`)
+
+| 路径 | 说明 |
+|------|------|
+| `POST /api/family/relationship/scores` | 获取家庭成员关系质量评分 |
+| `POST /api/family/relationship/calculate` | 计算并保存关系质量评分 |
+| `POST /api/family/relationship/health-alerts` | 获取健康预警列表 |
+| `POST /api/family/relationship/milestones` | 获取即将到来的里程碑 |
+| `POST /api/family/relationship/pairwise` | 获取以指定成员为中心的所有成员关系 |
+
+### 4.20 佣金管理(`/api/commission/*`)
+
+| 路径 | 说明 |
+|------|------|
+| `POST /api/commission/summary` | 获取佣金汇总 |
+| `POST /api/commission/list` | 佣金流水列表 |
+| `POST /api/commission/team` | 团队统计(L1/L2) |
+| `POST /api/commission/stats` | 佣金统计面板 |
+| `POST /api/commission/balance` | 可提现余额详情 |
+| `POST /api/commission/team/list` | 团队成员列表 |
+| `POST /api/commission/funnel` | 佣金转化漏斗 |
+
+### 4.21 管理端-能量配置(`/api/admin/energy-config/*`)
+
+| 路径 | 说明 |
+|------|------|
+| `POST /api/admin/energy-config/source-list` | 能量来源配置列表 |
+| `POST /api/admin/energy-config/source-save` | 保存能量来源配置(新增/更新) |
+| `POST /api/admin/energy-config/source-delete` | 删除能量来源配置 |
+| `POST /api/admin/energy-config/daily-limits` | 获取每日能量限制 |
+| `POST /api/admin/energy-config/daily-limit-save` | 保存每日能量限制 |
+| `POST /api/admin/energy-config/overview` | 能量配置总览(来源+限制) |
+
+### 4.22 管理端-公告(`/api/admin/notices/*`)
+
+| 路径 | 说明 |
+|------|------|
+| `POST /api/admin/notices/list` | 公告列表(分页) |
+| `POST /api/admin/notices/all` | 全量公告列表 |
+| `POST /api/admin/notices/create` | 创建公告 |
+| `POST /api/admin/notices/update` | 更新公告 |
+| `POST /api/admin/notices/delete` | 删除公告(软删) |
+| `POST /api/admin/notices/pin` | 置顶公告 |
+
+### 4.23 管理端-成长规划师(`/api/admin/guide/*`)
+
+| 路径 | 说明 |
+|------|------|
+| `POST /api/admin/guide/applications/pending` | 待审批规划师申请 |
+| `POST /api/admin/guide/applications/{id}/approve` | 审批通过 |
+| `POST /api/admin/guide/applications/{id}/reject` | 驳回申请 |
+| `POST /api/admin/guide/guides` | 获取所有成长规划师列表 |
+| `POST /api/admin/guide/guides/level/{level}` | 按级别获取规划师 |
+| `POST /api/admin/guide/guides/approved` | 获取已入驻(approved)的服务商列表 |
+| `POST /api/admin/guide/packages` | 获取所有套餐模板 |
+| `POST /api/admin/guide/packages/create` | 创建套餐 |
+| `POST /api/admin/guide/packages/{id}/update` | 更新套餐 |
+| `POST /api/admin/guide/packages/{id}/status` | 更新套餐状态 |
+
+### 4.24 管理端-商品(`/api/admin/product/*`)
+
+| 路径 | 说明 |
+|------|------|
+| `POST /api/admin/product/list` | 商品列表(分页/筛选/搜索) |
+| `POST /api/admin/product/review` | 审核商品(approve/reject) |
+| `POST /api/admin/product/shelve` | 商品上下架 |
+| `POST /api/admin/product/detail` | 商品详情 |
+| `POST /api/admin/product/create` | 创建商品 |
+| `POST /api/admin/product/update` | 更新商品 |
+| `POST /api/admin/product/copy` | 复制商品 |
+| `POST /api/admin/product/delete` | 删除商品 |
+
+### 4.25 管理端-成长档案关联商品(`/api/admin/growth-archive-product/*`)
+
+| 路径 | 说明 |
+|------|------|
+| `POST /api/admin/growth-archive-product/save` | 保存关联商品规则 |
+| `POST /api/admin/growth-archive-product/detail` | 关联商品规则详情 |
+| `POST /api/admin/growth-archive-product/delete` | 删除关联商品规则 |
+
+### 4.26 管理端-电商供应商(`/api/admin/ecom-supplier/*`)
+
+| 路径 | 说明 |
+|------|------|
+| `POST /api/admin/ecom-supplier/list` | 供应商列表(分页/搜索) |
+| `POST /api/admin/ecom-supplier/all` | 所有供应商(下拉选择) |
+| `POST /api/admin/ecom-supplier/save` | 创建供应商 |
+| `POST /api/admin/ecom-supplier/update` | 更新供应商 |
+| `POST /api/admin/ecom-supplier/delete` | 删除供应商 |
+
+### 4.27 管理端-测评材料(`/api/admin/assessment/*`)
+
+| 路径 | 说明 |
+|------|------|
+| `POST /api/admin/assessment/materials` | 测评资料列表 |
+| `POST /api/admin/assessment/materials/{id}` | 测评资料详情 |
+| `POST /api/admin/assessment/materials/create` | 创建测评资料 |
+| `POST /api/admin/assessment/materials/{id}/update` | 更新测评资料 |
+| `POST /api/admin/assessment/materials/{id}/delete` | 删除测评资料(软删) |
+| `POST /api/admin/assessment/config` | 获取积分配置 |
+| `POST /api/admin/assessment/config/update` | 更新积分配置 |
+
+### 4.28 管理端-P点配置(`/api/admin/ppoint-config/*`)
+
+| 路径 | 说明 |
+|------|------|
+| `POST /api/admin/ppoint-config/category/list` | 品类级P点比例列表 |
+| `POST /api/admin/ppoint-config/category/save` | 保存品类级P点比例 |
+| `POST /api/admin/ppoint-config/category/delete` | 删除品类级P点比例 |
+| `POST /api/admin/ppoint-config/category/by-category` | 按品类ID获取P点比例 |
+| `POST /api/admin/ppoint-config/product/list` | 商品级P点列表 |
+| `POST /api/admin/ppoint-config/product/save` | 保存商品级P点 |
+| `POST /api/admin/ppoint-config/product/delete` | 删除商品级P点 |
+| `POST /api/admin/ppoint-config/product/by-product` | 按商品ID获取P点 |
+| `POST /api/admin/ppoint-config/effective` | 获取商品综合有效P点值 |
+
+### 4.29 管理端-报告解析(`/api/admin/report-parser/*`)
+
+| 路径 | 说明 |
+|------|------|
+| `POST /api/admin/report-parser/types` | 报告类型列表 |
+| `POST /api/admin/report-parser/types/save` | 保存/更新报告类型 |
+| `POST /api/admin/report-parser/types/toggle` | 启用/停用报告类型 |
+| `POST /api/admin/report-parser/types/delete` | 删除报告类型 |
+| `POST /api/admin/report-parser/fingerprints/list` | 指纹规则列表 |
+| `POST /api/admin/report-parser/fingerprints/save` | 保存指纹规则 |
+| `POST /api/admin/report-parser/fingerprints/delete` | 删除指纹规则 |
+| `POST /api/admin/report-parser/imports` | 导入记录列表 |
+| `POST /api/admin/report-parser/imports/import` | 上传导入包 |
+| `POST /api/admin/report-parser/imports/approve` | 审批导入 |
+| `POST /api/admin/report-parser/imports/reject` | 驳回导入 |
+| `POST /api/admin/report-parser/unknown-clusters` | 未知报告聚类列表 |
+| `POST /api/admin/report-parser/unknown-uploads` | 聚类上传记录 |
+| `POST /api/admin/report-parser/generate-type` | 自学习生成报告类型 |
+
 ---
 
 ## 五、新增接口流程
@@ -481,15 +637,16 @@ find cfc-backend/src/main/java -name "*XxxService.java" -o -name "*XxxController
 
 ## 六、待清理的废弃接口
 
-| Controller | 废弃接口 | 替代方案 | 清理条件 |
-|------------|----------|----------|----------|
-| `FamilyController` | `/invite-code`, `/invite-code/generate` | `/api/family/invite/generate` | 确认前端无调用 |
-| `FamilyUserController` | `/switch-mode`, `/switch-to-child`, `/switch-back-to-parent`, `/switch-back-verify` | `/api/family/member/switch` | 确认前端无调用 |
-| `ButlerController` | `/service-records`, `/commissions` | — | 无替代,可删除 |
-| `TeacherController` | 全部 `/api/teacher/*` 方法 | `/api/guide/families/*` | 确认规划师端无调用 |
-| `DanAssessmentController` | `/current`(获取当前测评) | — | 无前端调用方 |
-| `NoticeController` | `GET /list`(违反 POST 规范) | — | 重构为 POST |
+| Controller | 废弃接口 | 替代方案 | 当前状态 | 清理条件 |
+|------------|----------|----------|----------|----------|
+| `FamilyController` | `/invite-code`, `/invite-code/generate` | `/api/family/invite/generate` | @Deprecated + 410 ✓ | 确认前端无调用 |
+| `FamilyUserController` | `/switch-mode`, `/switch-to-child` | `/api/family/member/switch` | @Deprecated + 410 ✓ | 确认前端无调用 |
+| `FamilyUserController` | `/switch-back-to-parent`, `/switch-back-verify` | `/api/family/member/switch` | 已修复:@Deprecated + 410 | 确认前端无调用 |
+| `ButlerController` | `/service-records`, `/commissions` | — | @Deprecated + 410 ✓ | 可删除 |
+| `TeacherController` | 全部 `/api/teacher/*` 方法 | `/api/guide/families/*` | @Deprecated + UnsupportedOperationException | 确认规划师端无调用 |
+| `DanAssessmentController` | `/current` | — | 需验证 | 无前端调用方 |
+| `NoticeController` | `GET /list`(违反 POST 规范) | `/api/admin/notices/list` | 已修复:@Deprecated + 410 | 可删除 |
 
 ---
 
-*文档最后更新:2026-08-22*
+*文档最后更新:2026-08-23*

+ 315 - 0
docs/superpowers/plans/2026-08-16-wealth-features-restructure.md

@@ -0,0 +1,315 @@
+# 财富功能入口重构 实施计划
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 将财富维度(富页面)的「财富服务」9宫格拆解,功能迁移到我的页面(成就区)、成长页(今日增长),并在富页面直接展示保单列表。
+
+**Architecture:** 纯前端改动,3个 `.vue` 文件独立修改,不涉及后端、数据库或新增页面。每个文件可独立并行修改。
+
+**Tech Stack:** uni-app Vue 2 (Options API),微信小程序
+
+**涉及文件:**
+- `cfc-frontend/pages/profile-main/profile.vue` — 成就区增加「推广收益」图标
+- `cfc-frontend/pages/wealth/index.vue` — 删除服务网格 + 新增保单列表
+- `cfc-frontend/pages/growth-main/index.vue` — 今日成长增加记账打卡项
+
+**API 依赖:**
+- `getReferralSummary()` → `POST /api/invite/summary` — 返回 `{ totalEarnings, totalCount }`
+- `getInsuranceList(data)` → `POST /api/wealth/insurance/list` — 返回保单列表
+- `getCheckinList(data)` → `POST /api/wealth/checkin/list` — 返回打卡记录列表
+
+## Global Constraints
+
+- Vue 2 Options API,禁止 Composition API
+- 禁止可选链 `?.`(用 `&&` 替代)
+- 禁止 CSS Grid(用 flexbox)
+- 禁止 `:key` 表达式(用方法调用代替)
+- 禁止直接 `new Date(string)`(用 `parseDate()`)
+- 统一 `@PostMapping` 风格(前端仅涉及页面组件,不涉及后端接口)
+
+---
+
+### Task 1: 我的页面 — 成就区增加「推广收益」图标
+
+**Files:**
+- Modify: `cfc-frontend/pages/profile-main/profile.vue` — 成就网格 + data + loadAllData + 导航方法
+
+**Interfaces:**
+- Consumes: `getReferralSummary` (already imported at line 253)
+- Produces: 成就网格第9项「推广收益」,点击跳转 `/pages/promotion/index`
+
+- [ ] **Step 1: 在 data 中增加 referralEarnings 字段**
+
+在 `data()` 中 `totalPoints: 0` 行附近增加:
+```javascript
+referralEarnings: 0,
+```
+
+- [ ] **Step 2: 在 loadAllData 中加载推广收益**
+
+在 `loadAllData()` 方法末尾增加:
+```javascript
+this.loadReferralEarnings()
+```
+
+- [ ] **Step 3: 实现 loadReferralEarnings 方法**
+
+在 `loadProfileStats()` 方法之后增加:
+```javascript
+loadReferralEarnings() {
+  var self = this
+  getReferralSummary().then(function(res) {
+    if (res && res.code === 200 && res.data) {
+      self.referralEarnings = res.data.totalEarnings || 0
+    }
+  }).catch(function() {})
+},
+```
+
+- [ ] **Step 4: 在成就网格中增加第9个图标**
+
+在 `ach-item` 列表末尾("上传报告"之后)增加:
+```html
+<view class="ach-item" @click="goToWealthCenter">
+  <view class="ach-icon-wrap" style="background:rgba(245,158,11,0.12)">
+    <text class="ach-icon">💰</text>
+  </view>
+  <text class="ach-num">{{ referralEarnings }}</text>
+  <text class="ach-label">推广收益</text>
+</view>
+```
+
+- [ ] **Step 5: 实现 goToWealthCenter 导航方法**
+
+在 methods 中增加:
+```javascript
+goToWealthCenter: function() {
+  uni.navigateTo({ url: '/pages/promotion/index' })
+},
+```
+
+- [ ] **Step 6: 验证 — lsp_diagnostics 检查**
+
+Run: `lsp_diagnostics` on `cfc-frontend/pages/profile-main/profile.vue`
+Expected: 无错误
+
+---
+
+### Task 2: 富页面 — 去掉财富服务卡片 + 新增保单列表
+
+**Files:**
+- Modify: `cfc-frontend/pages/wealth/index.vue` — 删除 service-grid 区块,新增保单列表区块,data 增加字段,加载保单数据
+
+**Interfaces:**
+- Consumes: `getInsuranceList` (已导入,见 api.js 1961)
+- Produces: 保单摘要列表,最多3条,底部「查看更多」跳转
+
+- [ ] **Step 1: 在 data 中增加保单相关字段**
+
+在 `data()` 中 `guestProducts: []` 附近增加:
+```javascript
+// 保单列表
+insuranceList: [],
+insuranceLoaded: false,
+```
+
+- [ ] **Step 2: 在 onShow 中加载保单数据**
+
+在 `onShow` 方法中 `this.loadFamilyMembersVisible()` 之后增加:
+```javascript
+this.loadInsuranceList()
+```
+
+- [ ] **Step 3: 实现 loadInsuranceList 方法**
+
+在 methods 中增加:
+```javascript
+loadInsuranceList() {
+  var self = this
+  this.insuranceLoaded = false
+  getInsuranceList({}).then(function(res) {
+    if (res && res.code === 200) {
+      self.insuranceList = (res.data || []).slice(0, 3)
+    }
+    self.insuranceLoaded = true
+  }).catch(function() {
+    self.insuranceLoaded = true
+  })
+},
+```
+
+- [ ] **Step 4: 实现 getStatusLabel 方法(保单状态中文)**
+
+在 methods 中增加:
+```javascript
+getPolicyStatusLabel: function(status) {
+  var map = { active: '有效', expired: '已过期', cancelled: '已取消' }
+  return map[status] || status || '未知'
+},
+```
+
+- [ ] **Step 5: 删除「财富服务」9宫格区块**
+
+删除模板中 `service-grid` 整个区块(第 177-189 行):
+```html
+<!-- 财富服务 8 宫格 -->
+<view class="section">
+  <view class="section-header">
+    <text class="section-title">🚀 财富服务</text>
+  </view>
+  <view class="service-grid">
+    ...
+  </view>
+</view>
+```
+
+- [ ] **Step 6: 在财富数据卡下方新增保单管理区块**
+
+在财富数据卡(孩子/家长)和 `DimensionIntroCard` 之间增加:
+```html
+<!-- 保单管理 -->
+<view class="section" v-if="insuranceLoaded">
+  <view class="section-header">
+    <text class="section-title">🛡️ 保单管理</text>
+    <text class="section-more" @click="goToInsuranceList">查看更多 ›</text>
+  </view>
+  <view class="insurance-card" v-if="insuranceList.length === 0">
+    <view class="insurance-empty">
+      <text class="insurance-empty-icon">📋</text>
+      <text class="insurance-empty-text">暂无保单</text>
+    </view>
+  </view>
+  <view class="insurance-card" v-else>
+    <view class="insurance-item" v-for="(item, idx) in insuranceList" :key="idx" @click="goToInsuranceList">
+      <view class="insurance-item-header">
+        <text class="insurance-item-name">{{ item.policyName }}</text>
+        <text class="insurance-item-status" :class="'status-' + item.status">{{ getPolicyStatusLabel(item.status) }}</text>
+      </view>
+      <view class="insurance-item-row">
+        <text class="insurance-item-label">保险公司</text>
+        <text class="insurance-item-value">{{ item.insuranceCompany || '未知' }}</text>
+      </view>
+      <view class="insurance-item-row">
+        <text class="insurance-item-label">被保人</text>
+        <text class="insurance-item-value">{{ item.insuredPerson }}</text>
+      </view>
+    </view>
+  </view>
+</view>
+```
+
+- [ ] **Step 7: 实现 goToInsuranceList 导航方法**
+
+在 methods 中增加:
+```javascript
+goToInsuranceList: function() {
+  uni.navigateTo({ url: '/pages/wealth-sub/insurance-list' })
+},
+```
+
+- [ ] **Step 8: 新增保单卡片样式**
+
+在 `<style scoped>` 中增加:
+```css
+/* 保单管理 */
+.insurance-card {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 24rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.08);
+}
+.insurance-empty {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 40rpx 0;
+}
+.insurance-empty-icon { font-size: 60rpx; }
+.insurance-empty-text { font-size: 24rpx; color: #999; margin-top: 12rpx; }
+.insurance-item {
+  padding: 16rpx 0;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+.insurance-item:last-child { border-bottom: none; }
+.insurance-item-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 8rpx;
+}
+.insurance-item-name {
+  font-size: 26rpx;
+  font-weight: 600;
+  color: #333;
+}
+.insurance-item-status {
+  font-size: 20rpx;
+  padding: 4rpx 12rpx;
+  border-radius: 8rpx;
+}
+.insurance-item-status.status-active { background: #DCFCE7; color: #16A34A; }
+.insurance-item-status.status-expired { background: #FEF2F2; color: #DC2626; }
+.insurance-item-status.status-cancelled { background: #F5F5F5; color: #999; }
+.insurance-item-row {
+  display: flex;
+  justify-content: space-between;
+  padding: 4rpx 0;
+}
+.insurance-item-label { font-size: 22rpx; color: #999; }
+.insurance-item-value { font-size: 22rpx; color: #666; }
+```
+
+- [ ] **Step 9: 验证 — lsp_diagnostics 检查**
+
+Run: `lsp_diagnostics` on `cfc-frontend/pages/wealth/index.vue`
+Expected: 无错误
+
+---
+
+### Task 3: 成长页 — 今日成长增加「记账打卡」跟踪项
+
+**Files:**
+- Modify: `cfc-frontend/pages/growth-main/index.vue` — progressItems 增加第5项,loadProgressItems 增加打卡判断
+
+**Interfaces:**
+- Consumes: `getCheckinList` (需在 import 中增加)
+- Produces: 今日成长第5项「记账打卡」显示今日打卡状态
+
+- [ ] **Step 1: 在 import 中增加 getCheckinList**
+
+在 `import { ... } from '../../utils/api.js'` 中增加 `getCheckinList`:
+```javascript
+import { healthCheckinList, getHealthMealList, getSurveyStatus, getSurveyHistory, getFoodCautionList, getGrowthRecommendations, getExerciseRecords, getSleepRecords, getEmotionCheckinList, getCheckinList } from '../../utils/api.js'
+```
+
+- [ ] **Step 2: 在 loadProgressItems 中增加记账打卡判断**
+
+在 `items.push({ key: 'emotion', ... })` 之后增加:
+```javascript
+// 记账打卡
+var checkinDone = false
+try {
+  var cParams = self.memberId ? { memberId: self.memberId } : {}
+  var cRes = await getCheckinList(cParams)
+  var cList = (cRes.code === 200 && cRes.data) ? cRes.data : []
+  for (var ci = 0; ci < cList.length; ci++) {
+    if (cList[ci] && cList[ci].checkinDate && cList[ci].checkinDate.indexOf(self.todayDate) === 0) {
+      checkinDone = true
+      break
+    }
+  }
+} catch(e) {}
+items.push({ key: 'checkin', icon: '💰', label: '记账打卡', done: checkinDone, actionText: '去打卡' })
+```
+
+- [ ] **Step 3: 在 handleProgressClick 中增加记账打卡跳转**
+
+在 `handleProgressClick` 方法的跳转映射中增加 `checkin` 路由:
+```javascript
+var m = { checkin: '/pages/wealth-sub/checkin', diet: '/pages/health/diet-index', exercise: '/pages/health/exercise-index', sleep: '/pages/health/sleep-index', emotion: '/pages/mind/index' }
+```
+
+- [ ] **Step 4: 验证 — lsp_diagnostics 检查**
+
+Run: `lsp_diagnostics` on `cfc-frontend/pages/growth-main/index.vue`
+Expected: 无错误

+ 1125 - 0
docs/superpowers/plans/2026-08-22-health-start-plan.md

@@ -0,0 +1,1125 @@
+# 健康启航计划 实现计划
+
+> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
+
+**目标:** 实现新用户付费转化产品"健康启航计划"——199元体验套餐,7天全功能体验+3周闯关任务,CF值激励,续费引导至家庭会员年费。
+
+**架构:** 新增购买订单表+挑战赛进度表;付费墙拦截在报告上传/方案生成接口;前端购买弹窗+续费页+闯关进度页;复用现有会员体系和CF值系统。
+
+**技术栈:** Java 8 + MyBatis-Plus、LangGraph Python (FastAPI)、uni-app Vue 2、Element UI
+
+---
+
+## 文件结构
+
+### 新建文件
+- `cfc-backend/src/main/resources/db/migration/V252__create_purchase_tables.sql`
+- `cfc-backend/src/main/java/com/etotem/cfc/entity/PurchaseOrder.java`
+- `cfc-backend/src/main/java/com/etotem/cfc/entity/ChallengeProgress.java`
+- `cfc-backend/src/main/java/com/etotem/cfc/mapper/PurchaseOrderMapper.java`
+- `cfc-backend/src/main/java/com/etotem/cfc/mapper/ChallengeProgressMapper.java`
+- `cfc-backend/src/main/java/com/etotem/cfc/service/PurchaseService.java`
+- `cfc-backend/src/main/java/com/etotem/cfc/service/impl/PurchaseServiceImpl.java`
+- `cfc-backend/src/main/java/com/etotem/cfc/service/ChallengeService.java`
+- `cfc-backend/src/main/java/com/etotem/cfc/service/impl/ChallengeServiceImpl.java`
+- `cfc-backend/src/main/java/com/etotem/cfc/controller/PurchaseController.java`
+- `cfc-langgraph/app/api/purchase.py`
+- `cfc-frontend/pages/purchase/index.vue` (购买页)
+- `cfc-frontend/pages/purchase/renewal.vue` (续费页)
+- `cfc-frontend/pages/growth/challenge-progress.vue` (闯关进度页)
+- `cfc-web/src/views/admin/PurchaseManagement.vue` (管理端)
+
+### 修改文件
+- `cfc-backend/src/main/resources/schema.sql` — 添加新表定义
+- `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` — 添加迁移
+- `cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java` — 添加付费墙拦截
+- `cfc-backend/src/main/java/com/etotem/cfc/controller/HealthPlanController.java` — 添加付费墙拦截
+- `cfc-frontend/utils/api.js` — 新增购买相关API
+- `cfc-frontend/pages.json` — 注册新页面
+
+---
+
+## 任务分解
+
+### 任务1:数据库迁移 — 创建购买和闯关表
+
+**文件:**
+- 新建:`cfc-backend/src/main/resources/db/migration/V252__create_purchase_tables.sql`
+- 修改:`cfc-backend/src/main/resources/schema.sql`
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`
+
+- [ ] **步骤 1:编写迁移SQL**
+
+```sql
+-- 健康启航计划购买订单表
+CREATE TABLE IF NOT EXISTS purchase_orders (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    order_no VARCHAR(64) NOT NULL UNIQUE COMMENT '订单号',
+    user_id BIGINT NOT NULL COMMENT '用户ID',
+    family_id BIGINT NOT NULL COMMENT '家庭ID',
+    amount INT NOT NULL COMMENT '支付金额(分)',
+    status VARCHAR(20) DEFAULT 'pending' COMMENT '状态: pending/paid/cancelled/refunded',
+    payment_method VARCHAR(20) DEFAULT NULL COMMENT '支付方式: wechat/alipay',
+    transaction_id VARCHAR(64) DEFAULT NULL COMMENT '支付流水号',
+    start_date DATETIME NOT NULL COMMENT '体验开始时间',
+    end_date DATETIME NOT NULL COMMENT '体验结束时间',
+    is_early_renewal TINYINT DEFAULT 0 COMMENT '是否早期续费(7天内)',
+    cf_reward_given INT DEFAULT 0 COMMENT '已发放CF值',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    paid_at DATETIME DEFAULT NULL,
+    INDEX idx_user_id (user_id),
+    INDEX idx_family_id (family_id),
+    INDEX idx_order_no (order_no)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康启航计划购买订单';
+
+-- 闯关任务进度表
+CREATE TABLE IF NOT EXISTS challenge_progress (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    purchase_id BIGINT NOT NULL COMMENT '购买订单ID',
+    member_id BIGINT NOT NULL COMMENT '家庭成员ID',
+    week_num INT NOT NULL COMMENT '周次: 1/2/3',
+    task_type VARCHAR(50) NOT NULL COMMENT '任务类型: fixed/personalized/streak',
+    task_key VARCHAR(100) NOT NULL COMMENT '任务标识',
+    task_title VARCHAR(255) NOT NULL COMMENT '任务标题',
+    target_value INT DEFAULT 0 COMMENT '目标值',
+    current_value INT DEFAULT 0 COMMENT '当前值',
+    reward_cf INT DEFAULT 0 COMMENT '奖励CF值',
+    status VARCHAR(20) DEFAULT 'pending' COMMENT '状态: pending/in_progress/completed',
+    completed_at DATETIME DEFAULT NULL,
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_purchase_id (purchase_id),
+    INDEX idx_member_id (member_id),
+    UNIQUE KEY uk_member_task (member_id, task_key, week_num)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='闯关任务进度';
+```
+
+- [ ] **步骤 2:同步到 schema.sql**
+
+在 `schema.sql` 末尾追加相同的建表语句。
+
+- [ ] **步骤 3:添加迁移到 DatabaseInitializer**
+
+```java
+// 迁移252: 创建健康启航计划购买和闯关表
+try {
+    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS purchase_orders (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "order_no VARCHAR(64) NOT NULL UNIQUE, " +
+            "user_id BIGINT NOT NULL, " +
+            "family_id BIGINT NOT NULL, " +
+            "amount INT NOT NULL, " +
+            "status VARCHAR(20) DEFAULT 'pending', " +
+            "payment_method VARCHAR(20), " +
+            "transaction_id VARCHAR(64), " +
+            "start_date DATETIME NOT NULL, " +
+            "end_date DATETIME NOT NULL, " +
+            "is_early_renewal TINYINT DEFAULT 0, " +
+            "cf_reward_given INT DEFAULT 0, " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "paid_at DATETIME, " +
+            "INDEX idx_user_id (user_id), " +
+            "INDEX idx_family_id (family_id), " +
+            "INDEX idx_order_no (order_no)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
+    log.info("已创建purchase_orders表");
+} catch (Exception e) {
+    log.warn("创建purchase_orders表可能已存在: {}", e.getMessage());
+}
+
+try {
+    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS challenge_progress (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "purchase_id BIGINT NOT NULL, " +
+            "member_id BIGINT NOT NULL, " +
+            "week_num INT NOT NULL, " +
+            "task_type VARCHAR(50) NOT NULL, " +
+            "task_key VARCHAR(100) NOT NULL, " +
+            "task_title VARCHAR(255) NOT NULL, " +
+            "target_value INT DEFAULT 0, " +
+            "current_value INT DEFAULT 0, " +
+            "reward_cf INT DEFAULT 0, " +
+            "status VARCHAR(20) DEFAULT 'pending', " +
+            "completed_at DATETIME, " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+            "INDEX idx_purchase_id (purchase_id), " +
+            "INDEX idx_member_id (member_id), " +
+            "UNIQUE KEY uk_member_task (member_id, task_key, week_num)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
+    log.info("已创建challenge_progress表");
+} catch (Exception e) {
+    log.warn("创建challenge_progress表可能已存在: {}", e.getMessage());
+}
+```
+
+- [ ] **步骤 4:编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile -q
+```
+
+预期:BUILD SUCCESS
+
+- [ ] **步骤 5:Commit**
+
+```bash
+git add cfc-backend/src/main/resources/db/migration/V252__create_purchase_tables.sql \
+       cfc-backend/src/main/resources/schema.sql \
+       cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
+git commit -m "feat(purchase): 创建健康启航计划购买订单表和闯关进度表"
+```
+
+---
+
+### 任务2:Java实体类和Mapper
+
+**文件:**
+- 新建:`cfc-backend/src/main/java/com/etotem/cfc/entity/PurchaseOrder.java`
+- 新建:`cfc-backend/src/main/java/com/etotem/cfc/entity/ChallengeProgress.java`
+- 新建:`cfc-backend/src/main/java/com/etotem/cfc/mapper/PurchaseOrderMapper.java`
+- 新建:`cfc-backend/src/main/java/com/etotem/cfc/mapper/ChallengeProgressMapper.java`
+
+- [ ] **步骤 1:创建 PurchaseOrder 实体**
+
+```java
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("purchase_orders")
+public class PurchaseOrder implements Serializable {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String orderNo;
+    private Long userId;
+    private Long familyId;
+    private Integer amount;
+    private String status;
+    private String paymentMethod;
+    private String transactionId;
+    private Date startDate;
+    private Date endDate;
+    private Integer isEarlyRenewal;
+    private Integer cfRewardGiven;
+    private Date createdAt;
+    private Date paidAt;
+}
+```
+
+- [ ] **步骤 2:创建 ChallengeProgress 实体**
+
+```java
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("challenge_progress")
+public class ChallengeProgress implements Serializable {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long purchaseId;
+    private Long memberId;
+    private Integer weekNum;
+    private String taskType;
+    private String taskKey;
+    private String taskTitle;
+    private Integer targetValue;
+    private Integer currentValue;
+    private Integer rewardCf;
+    private String status;
+    private Date completedAt;
+    private Date createdAt;
+    private Date updatedAt;
+}
+```
+
+- [ ] **步骤 3:创建 Mapper 接口**
+
+```java
+// PurchaseOrderMapper.java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.PurchaseOrder;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface PurchaseOrderMapper extends BaseMapper<PurchaseOrder> {
+}
+
+// ChallengeProgressMapper.java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.ChallengeProgress;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface ChallengeProgressMapper extends BaseMapper<ChallengeProgress> {
+}
+```
+
+- [ ] **步骤 4:编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile -q
+```
+
+- [ ] **步骤 5:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/entity/PurchaseOrder.java \
+       cfc-backend/src/main/java/com/etotem/cfc/entity/ChallengeProgress.java \
+       cfc-backend/src/main/java/com/etotem/cfc/mapper/PurchaseOrderMapper.java \
+       cfc-backend/src/main/java/com/etotem/cfc/mapper/ChallengeProgressMapper.java
+git commit -m "feat(purchase): 创建购买订单和闯关进度实体类及Mapper"
+```
+
+---
+
+### 任务3:购买服务 — 核心业务逻辑
+
+**文件:**
+- 新建:`cfc-backend/src/main/java/com/etotem/cfc/service/PurchaseService.java`
+- 新建:`cfc-backend/src/main/java/com/etotem/cfc/service/impl/PurchaseServiceImpl.java`
+
+- [ ] **步骤 1:创建 Service 接口**
+
+```java
+package com.etotem.cfc.service;
+
+import java.util.Map;
+
+public interface PurchaseService {
+    /** 检查用户是否有有效购买 */
+    Map<String, Object> checkPurchase(Long userId);
+    
+    /** 创建购买订单 */
+    Map<String, Object> createOrder(Long userId, Long familyId);
+    
+    /** 确认支付 */
+    Map<String, Object> confirmPayment(String orderNo, String paymentMethod, String transactionId);
+    
+    /** 获取续费信息 */
+    Map<String, Object> getRenewalInfo(Long userId);
+    
+    /** 处理续费(真实支付) */
+    Map<String, Object> processRenewal(Long userId, Long familyId, Integer amount);
+}
+```
+
+- [ ] **步骤 2:创建实现类**
+
+```java
+package com.etotem.cfc.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.*;
+import com.etotem.cfc.mapper.*;
+import com.etotem.cfc.service.PurchaseService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+@Service
+public class PurchaseServiceImpl implements PurchaseService {
+
+    @Autowired private PurchaseOrderMapper orderMapper;
+    @Autowired private FamilyMembershipMapper membershipMapper;
+    @Autowired private FamilyMemberMapper memberMapper;
+    @Autowired private PointsLogMapper pointsLogMapper;
+
+    @Override
+    public Map<String, Object> checkPurchase(Long userId) {
+        Map<String, Object> result = new HashMap<>();
+        
+        // 查询最新购买记录
+        LambdaQueryWrapper<PurchaseOrder> qw = new LambdaQueryWrapper<>();
+        qw.eq(PurchaseOrder::getUserId, userId)
+           .orderByDesc(PurchaseOrder::getCreatedAt)
+           .last("LIMIT 1");
+        PurchaseOrder order = orderMapper.selectOne(qw);
+        
+        if (order == null) {
+            result.put("hasPurchased", false);
+            result.put("daysRemaining", 0);
+            return result;
+        }
+        
+        boolean isActive = order.getStatus().equals("paid") 
+            && order.getEndDate().after(new Date());
+        
+        result.put("hasPurchased", true);
+        result.put("isActive", isActive);
+        result.put("orderId", order.getId());
+        result.put("orderNo", order.getOrderNo());
+        
+        if (isActive) {
+            long daysRemaining = java.time.temporal.ChronoUnit.DAYS.between(
+                LocalDateTime.now(), order.getEndDate().toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDateTime());
+            result.put("daysRemaining", Math.max(0, daysRemaining));
+            result.put("startDate", order.getStartDate());
+            result.put("endDate", order.getEndDate());
+            result.put("isEarlyRenewal", order.getIsEarlyRenewal());
+        } else {
+            result.put("daysRemaining", 0);
+        }
+        
+        return result;
+    }
+
+    @Override
+    public Map<String, Object> createOrder(Long userId, Long familyId) {
+        Map<String, Object> result = new HashMap<>();
+        
+        // 生成订单号
+        String orderNo = "HQ" + System.currentTimeMillis() + UUID.randomUUID().toString().substring(0, 6).toUpperCase();
+        
+        // 创建订单
+        PurchaseOrder order = new PurchaseOrder();
+        order.setOrderNo(orderNo);
+        order.setUserId(userId);
+        order.setFamilyId(familyId);
+        order.setAmount(19900); // 199元
+        order.setStatus("pending");
+        order.setStartDate(new Date());
+        
+        // 7天体验期
+        LocalDateTime endDate = LocalDateTime.now().plusDays(7);
+        order.setEndDate(Date.from(endDate.atZone(java.time.ZoneId.systemDefault()).toInstant()));
+        
+        orderMapper.insert(order);
+        
+        result.put("success", true);
+        result.put("orderNo", orderNo);
+        result.put("amount", 19900);
+        result.put("daysRemaining", 7);
+        return result;
+    }
+
+    @Override
+    public Map<String, Object> confirmPayment(String orderNo, String paymentMethod, String transactionId) {
+        Map<String, Object> result = new HashMap<>();
+        
+        LambdaQueryWrapper<PurchaseOrder> qw = new LambdaQueryWrapper<>();
+        qw.eq(PurchaseOrder::getOrderNo, orderNo);
+        PurchaseOrder order = orderMapper.selectOne(qw);
+        
+        if (order == null) {
+            result.put("success", false);
+            result.put("message", "订单不存在");
+            return result;
+        }
+        
+        if (!"pending".equals(order.getStatus())) {
+            result.put("success", false);
+            result.put("message", "订单状态异常");
+            return result;
+        }
+        
+        // 更新订单状态
+        order.setStatus("paid");
+        order.setPaymentMethod(paymentMethod);
+        order.setTransactionId(transactionId);
+        order.setPaidAt(new Date());
+        orderMapper.updateById(order);
+        
+        // 发放CF值奖励(基础奖励)
+        int cfReward = 300; // 基础CF值
+        order.setCfRewardGiven(cfReward);
+        
+        // 发放到家庭成员
+        FamilyMember member = memberMapper.selectById(order.getFamilyId());
+        if (member != null) {
+            member.setSystemPoints((member.getSystemPoints() != null ? member.getSystemPoints() : 0) + cfReward);
+            memberMapper.updateById(member);
+        }
+        
+        result.put("success", true);
+        result.put("orderId", order.getId());
+        result.put("cfReward", cfReward);
+        result.put("daysRemaining", 7);
+        return result;
+    }
+
+    @Override
+    public Map<String, Object> getRenewalInfo(Long userId) {
+        Map<String, Object> result = new HashMap<>();
+        
+        // 获取用户CF值余额
+        LambdaQueryWrapper<FamilyMember> qw = new LambdaQueryWrapper<>();
+        qw.select(FamilyMember::getSystemPoints)
+           .eq(FamilyMember::getUserId, userId)
+           .last("LIMIT 1");
+        FamilyMember member = memberMapper.selectOne(qw);
+        
+        int cfBalance = member != null && member.getSystemPoints() != null ? member.getSystemPoints() : 0;
+        
+        // 计算可抵扣金额(1 CF = 0.01元 = 1分)
+        int discount = Math.min(cfBalance, 19900); // 最多抵扣199元
+        
+        result.put("success", true);
+        result.put("cfBalance", cfBalance);
+        result.put("discount", discount);
+        result.put("payAmount", 199900 - discount); // 年费1999元 - 抵扣
+        result.put("discountPercent", cfBalance >= 19900 ? 100 : Math.round((double)discount / 19900 * 100));
+        
+        return result;
+    }
+
+    @Override
+    public Map<String, Object> processRenewal(Long userId, Long familyId, Integer amount) {
+        Map<String, Object> result = new HashMap<>();
+        
+        // 检查是否已有购买记录
+        LambdaQueryWrapper<PurchaseOrder> qw = new LambdaQueryWrapper<>();
+        qw.eq(PurchaseOrder::getUserId, userId)
+           .eq(PurchaseOrder::getStatus, "paid")
+           .last("LIMIT 1");
+        PurchaseOrder existingOrder = orderMapper.selectOne(qw);
+        
+        if (existingOrder == null) {
+            result.put("success", false);
+            result.put("message", "无有效购买记录");
+            return result;
+        }
+        
+        // 标记为早期续费
+        existingOrder.setIsEarlyRenewal(1);
+        orderMapper.updateById(existingOrder);
+        
+        // 创建续费订单
+        String orderNo = "HR" + System.currentTimeMillis() + UUID.randomUUID().toString().substring(0, 6).toUpperCase();
+        PurchaseOrder renewalOrder = new PurchaseOrder();
+        renewalOrder.setOrderNo(orderNo);
+        renewalOrder.setUserId(userId);
+        renewalOrder.setFamilyId(familyId);
+        renewalOrder.setAmount(amount);
+        renewalOrder.setStatus("paid");
+        renewalOrder.setPaymentMethod("external");
+        renewalOrder.setStartDate(new Date());
+        LocalDateTime endDate = LocalDateTime.now().plusYears(1);
+        renewalOrder.setEndDate(Date.from(endDate.atZone(java.time.ZoneId.systemDefault()).toInstant()));
+        renewalOrder.setIsEarlyRenewal(1);
+        orderMapper.insert(renewalOrder);
+        
+        // 激活家庭会员年费
+        LambdaQueryWrapper<FamilyMembership> memQw = new LambdaQueryWrapper<>();
+        memQw.eq(FamilyMembership::getFamilyId, familyId);
+        FamilyMembership membership = membershipMapper.selectOne(memQw);
+        
+        if (membership == null) {
+            membership = new FamilyMembership();
+            membership.setFamilyId(familyId);
+            membership.setLevelCode("FAMILY");
+            membership.setStartDate(new Date());
+        }
+        membership.setEndDate(Date.from(endDate.atZone(java.time.ZoneId.systemDefault()).toInstant()));
+        membership.setPaymentStatus("paid");
+        membership.setOrderNo(orderNo);
+        membershipMapper.insert(membership);
+        
+        result.put("success", true);
+        result.put("orderNo", orderNo);
+        result.put("membershipValid", true);
+        return result;
+    }
+}
+```
+
+- [ ] **步骤 3:编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile -q
+```
+
+- [ ] **步骤 4:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/PurchaseService.java \
+       cfc-backend/src/main/java/com/etotem/cfc/service/impl/PurchaseServiceImpl.java
+git commit -m "feat(purchase): 实现购买服务 — 订单创建、支付确认、续费处理"
+```
+
+---
+
+### 任务4:闯关服务 — 任务管理和进度追踪
+
+**文件:**
+- 新建:`cfc-backend/src/main/java/com/etotem/cfc/service/ChallengeService.java`
+- 新建:`cfc-backend/src/main/java/com/etotem/cfc/service/impl/ChallengeServiceImpl.java`
+
+- [ ] **步骤 1:创建 Service 接口**
+
+```java
+package com.etotem.cfc.service;
+
+import java.util.List;
+import java.util.Map;
+
+public interface ChallengeService {
+    /** 初始化闯关任务 */
+    void initChallengeTasks(Long purchaseId, Long memberId);
+    
+    /** 获取用户闯关进度 */
+    Map<String, Object> getChallengeProgress(Long memberId);
+    
+    /** 更新任务进度 */
+    void updateTaskProgress(Long memberId, String taskKey, int currentValue);
+    
+    /** 检查并完成任务 */
+    Map<String, Object> checkAndCompleteTask(Long memberId, String taskKey);
+}
+```
+
+- [ ] **步骤 2:创建实现类**
+
+```java
+package com.etotem.cfc.service.impl;
+
+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 org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+
+@Service
+public class ChallengeServiceImpl implements ChallengeService {
+
+    @Autowired private ChallengeProgressMapper progressMapper;
+    @Autowired private FamilyMemberMapper memberMapper;
+    @Autowired private HealthSleepRecordMapper sleepMapper;
+    @Autowired private HealthExerciseRecordMapper exerciseMapper;
+    @Autowired private HealthMealRecordMapper mealMapper;
+    @Autowired private EmotionCheckinMapper emotionMapper;
+
+    @Override
+    public void initChallengeTasks(Long purchaseId, Long memberId) {
+        // 第1周任务
+        List<Map<String, Object>> week1Tasks = Arrays.asList(
+            CreateMap("first_report_upload", "首次报告上传", 1, 100),
+            CreateMap("streak_3days", "连续3天打卡", 3, 100),
+            CreateMap("upload_1_report", "上传1份报告", 1, 60)
+        );
+        
+        // 第2周任务
+        List<Map<String, Object>> week2Tasks = Arrays.asList(
+            CreateMap("upload_3_reports", "上传3份报告", 3, 100),
+            CreateMap("streak_7days", "连续7天打卡", 7, 100),
+            CreateMap("upload_3_reports_total", "累计上传3份报告", 3, 60)
+        );
+        
+        // 第3周任务
+        List<Map<String, Object>> week3Tasks = Arrays.asList(
+            CreateMap("complete_3_challenges", "完成3个挑战", 3, 100),
+            CreateMap("streak_21days", "连续21天打卡", 21, 100),
+            CreateMap("upload_5_reports", "上传5份报告", 5, 60)
+        );
+        
+        insertTasks(purchaseId, memberId, 1, week1Tasks);
+        insertTasks(purchaseId, memberId, 2, week2Tasks);
+        insertTasks(purchaseId, memberId, 3, week3Tasks);
+    }
+    
+    private Map<String, Object> CreateMap(String key, String title, int target, int reward) {
+        Map<String, Object> map = new HashMap<>();
+        map.put("taskKey", key);
+        map.put("taskTitle", title);
+        map.put("targetValue", target);
+        map.put("rewardCf", reward);
+        return map;
+    }
+    
+    private void insertTasks(Long purchaseId, Long memberId, int weekNum, List<Map<String, Object>> tasks) {
+        for (Map<String, Object> task : tasks) {
+            ChallengeProgress progress = new ChallengeProgress();
+            progress.setPurchaseId(purchaseId);
+            progress.setMemberId(memberId);
+            progress.setWeekNum(weekNum);
+            progress.setTaskType("fixed");
+            progress.setTaskKey((String) task.get("taskKey"));
+            progress.setTaskTitle((String) task.get("taskTitle"));
+            progress.setTargetValue((Integer) task.get("targetValue"));
+            progress.setCurrentValue(0);
+            progress.setRewardCf((Integer) task.get("rewardCf"));
+            progress.setStatus("pending");
+            progressMapper.insert(progress);
+        }
+    }
+
+    @Override
+    public Map<String, Object> getChallengeProgress(Long memberId) {
+        Map<String, Object> result = new HashMap<>();
+        
+        // 查询所有任务进度
+        LambdaQueryWrapper<ChallengeProgress> qw = new LambdaQueryWrapper<>();
+        qw.eq(ChallengeProgress::getMemberId, memberId)
+           .orderByAsc(ChallengeProgress::getWeekNum);
+        List<ChallengeProgress> progress = progressMapper.selectList(qw);
+        
+        // 按周分组
+        Map<Integer, List<ChallengeProgress>> byWeek = new LinkedHashMap<>();
+        for (ChallengeProgress p : progress) {
+            byWeek.computeIfAbsent(p.getWeekNum(), k -> new ArrayList<>()).add(p);
+        }
+        
+        // 计算总CF奖励和已完成CF
+        int totalReward = 0;
+        int completedReward = 0;
+        int completedCount = 0;
+        
+        for (ChallengeProgress p : progress) {
+            totalReward += p.getRewardCf();
+            if ("completed".equals(p.getStatus())) {
+                completedReward += p.getRewardCf();
+                completedCount++;
+            }
+        }
+        
+        result.put("success", true);
+        result.put("tasks", progress);
+        result.put("byWeek", byWeek);
+        result.put("totalReward", totalReward);
+        result.put("completedReward", completedReward);
+        result.put("completedCount", completedCount);
+        result.put("totalCount", progress.size());
+        result.put("progressPercent", progress.size() > 0 ? Math.round((double)completedCount / progress.size() * 100) : 0);
+        
+        return result;
+    }
+
+    @Override
+    public void updateTaskProgress(Long memberId, String taskKey, int currentValue) {
+        LambdaQueryWrapper<ChallengeProgress> qw = new LambdaQueryWrapper<>();
+        qw.eq(ChallengeProgress::getMemberId, memberId)
+           .eq(ChallengeProgress::getTaskKey, taskKey)
+           .eq(ChallengeProgress::getStatus, "pending");
+        
+        ChallengeProgress progress = progressMapper.selectOne(qw);
+        if (progress != null) {
+            progress.setCurrentValue(currentValue);
+            progressMapper.updateById(progress);
+            
+            // 检查是否完成
+            if (currentValue >= progress.getTargetValue()) {
+                completeTask(progress);
+            }
+        }
+    }
+    
+    private void completeTask(ChallengeProgress progress) {
+        progress.setStatus("completed");
+        progress.setCompletedAt(new java.util.Date());
+        progressMapper.updateById(progress);
+        
+        // 发放CF值
+        FamilyMember member = memberMapper.selectById(progress.getMemberId());
+        if (member != null) {
+            member.setSystemPoints((member.getSystemPoints() != null ? member.getSystemPoints() : 0) + progress.getRewardCf());
+            memberMapper.updateById(member);
+        }
+    }
+
+    @Override
+    public Map<String, Object> checkAndCompleteTask(Long memberId, String taskKey) {
+        // 查询任务
+        LambdaQueryWrapper<ChallengeProgress> qw = new LambdaQueryWrapper<>();
+        qw.eq(ChallengeProgress::getMemberId, memberId)
+           .eq(ChallengeProgress::getTaskKey, taskKey);
+        ChallengeProgress progress = progressMapper.selectOne(qw);
+        
+        if (progress == null) {
+            return Map.of("success", false, "message", "任务不存在");
+        }
+        
+        if ("completed".equals(progress.getStatus())) {
+            return Map.of("success", true, "alreadyCompleted", true);
+        }
+        
+        // 检查当前进度
+        int currentValue = calculateCurrentValue(memberId, taskKey);
+        progress.setCurrentValue(currentValue);
+        
+        Map<String, Object> result = new HashMap<>();
+        result.put("success", true);
+        result.put("currentValue", currentValue);
+        result.put("targetValue", progress.getTargetValue());
+        result.put("isCompleted", currentValue >= progress.getTargetValue());
+        
+        if (currentValue >= progress.getTargetValue()) {
+            completeTask(progress);
+            result.put("rewardCf", progress.getRewardCf());
+            result.put("message", "任务完成!获得 " + progress.getRewardCf() + " CF值");
+        }
+        
+        return result;
+    }
+    
+    private int calculateCurrentValue(Long memberId, String taskKey) {
+        // 根据任务类型计算当前值
+        switch (taskKey) {
+            case "first_report_upload":
+                // 查询是否有报告上传记录
+                return 1; // 简化:假设已上传
+            case "streak_3days":
+            case "streak_7days":
+            case "streak_21days":
+                // 查询连续打卡天数
+                return getStreakDays(memberId);
+            case "upload_1_report":
+            case "upload_3_reports":
+            case "upload_5_reports":
+                // 查询报告上传数量
+                return getReportCount(memberId);
+            default:
+                return 0;
+        }
+    }
+    
+    private int getStreakDays(Long memberId) {
+        // 简化实现:返回实际连续打卡天数
+        return 0; // 需要从打卡记录计算
+    }
+    
+    private int getReportCount(Long memberId) {
+        // 简化实现:返回报告数量
+        return 0; // 需要从报告记录计算
+    }
+}
+```
+
+- [ ] **步骤 3:编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile -q
+```
+
+- [ ] **步骤 4:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/ChallengeService.java \
+       cfc-backend/src/main/java/com/etotem/cfc/service/impl/ChallengeServiceImpl.java
+git commit -m "feat(purchase): 实现闯关服务 — 任务初始化、进度追踪、CF奖励发放"
+```
+
+---
+
+### 任务5:控制器 — API接口
+
+**文件:**
+- 新建:`cfc-backend/src/main/java/com/etotem/cfc/controller/PurchaseController.java`
+
+- [ ] **步骤 1:创建控制器**
+
+```java
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.service.PurchaseService;
+import com.etotem.cfc.service.ChallengeService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/purchase")
+public class PurchaseController {
+
+    @Autowired private PurchaseService purchaseService;
+    @Autowired private ChallengeService challengeService;
+
+    @PostMapping("/check")
+    public Result<Map<String, Object>> checkPurchase(HttpServletRequest request) {
+        Long userId = (Long) request.getAttribute("userId");
+        Map<String, Object> result = purchaseService.checkPurchase(userId);
+        return Result.success(result);
+    }
+
+    @PostMapping("/create")
+    public Result<Map<String, Object>> createOrder(HttpServletRequest request) {
+        Long userId = (Long) request.getAttribute("userId");
+        Long familyId = (Long) request.getAttribute("familyId");
+        Map<String, Object> result = purchaseService.createOrder(userId, familyId);
+        return Result.success(result);
+    }
+
+    @PostMapping("/confirm")
+    public Result<Map<String, Object>> confirmPayment(@RequestBody Map<String, Object> params, HttpServletRequest request) {
+        String orderNo = (String) params.get("orderNo");
+        String paymentMethod = (String) params.get("paymentMethod");
+        String transactionId = (String) params.get("transactionId");
+        Map<String, Object> result = purchaseService.confirmPayment(orderNo, paymentMethod, transactionId);
+        return Result.success(result);
+    }
+
+    @PostMapping("/renewal-info")
+    public Result<Map<String, Object>> getRenewalInfo(HttpServletRequest request) {
+        Long userId = (Long) request.getAttribute("userId");
+        Map<String, Object> result = purchaseService.getRenewalInfo(userId);
+        return Result.success(result);
+    }
+
+    @PostMapping("/renew")
+    public Result<Map<String, Object>> renew(@RequestBody Map<String, Object> params, HttpServletRequest request) {
+        Long userId = (Long) request.getAttribute("userId");
+        Long familyId = (Long) request.getAttribute("familyId");
+        Integer amount = (Integer) params.get("amount");
+        Map<String, Object> result = purchaseService.processRenewal(userId, familyId, amount);
+        return Result.success(result);
+    }
+
+    @GetMapping("/challenge/progress")
+    public Result<Map<String, Object>> getChallengeProgress(@RequestParam Long memberId, HttpServletRequest request) {
+        Map<String, Object> result = challengeService.getChallengeProgress(memberId);
+        return Result.success(result);
+    }
+
+    @PostMapping("/challenge/update")
+    public Result<Void> updateChallengeProgress(@RequestBody Map<String, Object> params, HttpServletRequest request) {
+        Long memberId = (Long) params.get("memberId");
+        String taskKey = (String) params.get("taskKey");
+        int currentValue = ((Number) params.get("currentValue")).intValue();
+        challengeService.updateTaskProgress(memberId, taskKey, currentValue);
+        return Result.success(null);
+    }
+}
+```
+
+- [ ] **步骤 2:编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile -q
+```
+
+- [ ] **步骤 3:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/PurchaseController.java
+git commit -m "feat(purchase): 添加购买API控制器"
+```
+
+---
+
+### 任务6:付费墙拦截
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java`
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/HealthPlanController.java`
+
+- [ ] **步骤 1:在 HealthReportController 中添加拦截**
+
+找到报告上传方法,在业务逻辑前添加:
+
+```java
+// 付费墙拦截
+@Autowired
+private com.etotem.cfc.service.PurchaseService purchaseService;
+
+// 在 upload 方法开头添加
+Long userId = (Long) request.getAttribute("userId");
+Map<String, Object> purchaseCheck = purchaseService.checkPurchase(userId);
+if (!Boolean.TRUE.equals(purchaseCheck.get("isActive"))) {
+    return Result.error("请先购买健康启航计划体验包");
+}
+```
+
+- [ ] **步骤 2:在 HealthPlanController 中添加拦截**
+
+找到方案生成方法,在业务逻辑前添加相同拦截逻辑。
+
+- [ ] **步骤 3:编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile -q
+```
+
+- [ ] **步骤 4:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java \
+       cfc-backend/src/main/java/com/etotem/cfc/controller/HealthPlanController.java
+git commit -m "feat(purchase): 在报告和方案接口添加付费墙拦截"
+```
+
+---
+
+### 任务7:前端 — API封装和购买页
+
+**文件:**
+- 修改:`cfc-frontend/utils/api.js`
+- 新建:`cfc-frontend/pages/purchase/index.vue`
+
+- [ ] **步骤 1:新增 API 函数**
+
+在 `cfc-frontend/utils/api.js` 末尾追加:
+
+```javascript
+// 健康启航计划
+export function checkPurchase() {
+  return request('/api/purchase/check', 'POST', {})
+}
+
+export function createPurchaseOrder() {
+  return request('/api/purchase/create', 'POST', {})
+}
+
+export function confirmPurchase(data) {
+  return request('/api/purchase/confirm', 'POST', data)
+}
+
+export function getRenewalInfo() {
+  return request('/api/purchase/renewal-info', 'POST', {})
+}
+
+export function renewPurchase(data) {
+  return request('/api/purchase/renew', 'POST', data)
+}
+
+export function getChallengeProgress(memberId) {
+  return request('/api/purchase/challenge/progress?memberId=' + memberId, 'GET', {})
+}
+```
+
+- [ ] **步骤 2:创建购买页**
+
+创建 `cfc-frontend/pages/purchase/index.vue`,包含:
+- 权益对比展示
+- CF奖励计算器
+- 购买按钮(跳转支付)
+- 已有购买的进度展示
+
+- [ ] **步骤 3:Commit**
+
+```bash
+git add cfc-frontend/utils/api.js cfc-frontend/pages/purchase/index.vue
+git commit -m "feat(purchase): 前端购买页和API封装"
+```
+
+---
+
+### 任务8:前端 — 续费页和闯关进度页
+
+**文件:**
+- 新建:`cfc-frontend/pages/purchase/renewal.vue`
+- 新建:`cfc-frontend/pages/growth/challenge-progress.vue`
+
+- [ ] **步骤 1:创建续费页**
+
+展示:
+- 当前CF值余额
+- 可抵扣金额
+- 实付金额
+- 支付按钮(跳转外部支付)
+
+- [ ] **步骤 2:创建闯关进度页**
+
+展示:
+- 3周任务列表
+- 每个任务的完成进度
+- 已获得CF值
+- 连续打卡天数
+
+- [ ] **步骤 3:Commit**
+
+```bash
+git add cfc-frontend/pages/purchase/renewal.vue \
+       cfc-frontend/pages/growth/challenge-progress.vue
+git commit -m "feat(purchase): 续费页和闯关进度页"
+```
+
+---
+
+### 任务9:管理端 — 购买记录管理
+
+**文件:**
+- 新建:`cfc-web/src/views/admin/PurchaseManagement.vue`
+
+- [ ] **步骤 1:创建管理页面**
+
+功能:
+- 购买记录列表
+- 续费记录统计
+- CF值发放管理
+- 用户查询
+
+- [ ] **步骤 2:Commit**
+
+```bash
+git add cfc-web/src/views/admin/PurchaseManagement.vue
+git commit -m "feat(purchase): Web管理端购买记录管理"
+```
+
+---
+
+### 任务10:联调测试与部署
+
+- [ ] **步骤 1:数据库迁移执行**
+
+```bash
+mysql -h 192.168.16.251 -u zxyj -p'zxyj@123' zxyj < cfc-backend/src/main/resources/db/migration/V252__create_purchase_tables.sql
+```
+
+- [ ] **步骤 2:启动后端测试API**
+
+```bash
+cd cfc-backend && mvn spring-boot:run
+# 测试购买、续费、进度查询等接口
+```
+
+- [ ] **步骤 3:小程序联调**
+
+在微信开发者工具中测试:
+- 购买流程
+- 付费墙拦截
+- 续费流程
+- 闯关进度显示
+
+- [ ] **步骤 4:修复bug并提交**
+
+```bash
+git add -A
+git commit -m "fix(purchase): 联调测试修复"
+git push origin cfclub
+```
+
+---
+
+## 注意事项
+
+1. **支付安全**:真实支付需要对接微信支付/支付宝,此处先实现框架,实际支付回调需对接公司支付系统
+2. **并发控制**:购买订单创建需要加分布式锁,防止重复购买
+3. **数据一致性**:CF值发放需要事务保证,避免发放失败
+4. **测试环境**:先在内网测试环境验证完整流程,再上线生产
+
+## 验收标准
+
+- [ ] 新用户首次上传报告时弹出购买提示
+- [ ] 购买成功后7天内可无限使用报告上传和方案生成
+- [ ] 第8天开始功能受限,提示续费
+- [ ] 续费页面正确显示CF值抵扣金额
+- [ ] 闯关任务正确初始化并追踪进度
+- [ ] 完成任务后CF值正确发放
+- [ ] 管理端可查看购买记录和统计数据

+ 1325 - 0
docs/superpowers/plans/2026-08-23-coach-butler.md

@@ -0,0 +1,1325 @@
+# AI健康教练人格分化与管家自助选择 实现计划
+
+> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
+
+**目标:** 复用吉祥物管线为健康教练提供浠宝/福宝双人格话术;L2 家庭可自助浏览并绑定/更换真人管家;管理端补齐分配管理与停接开关。
+
+**架构:** 后端在现有 butler 体系上增量扩展(1 列迁移 + 1 新实体 + ButlerService 方法 + 2 个控制器);AI 人格经 `/health-coach/send` 补注入 `coach_id`,经 AiGateway context 透传至 LangGraph 按 prompt_key 三级回退路由;小程序新增选择页三态视图;Web 管理端新增分配管理视图与开关列。
+
+**技术栈:** Spring Boot 2.7.18 + MyBatis-Plus(Java 8)/ FastAPI + LangGraph(Python)/ uni-app Vue 2 Options API / Vue 2 + Element UI
+
+**规格:** `docs/superpowers/specs/2026-08-23-coach-butler-design.md`(已批准,commit 8cc0a758)
+
+**⚠️ 本仓库验证约定(覆盖技能默认 TDD 流程):**
+- 后端唯一验证方式:`cd cfc-backend && mvn clean compile`(EXIT 0 即通过,无测试基建)
+- 小程序前端验证:提取改动 `.vue` 的 `<script>` 块存临时文件后 `node --check`;**禁止**运行整包 build
+- 所有 git 命令带 `GIT_MASTER=1` 前缀;commit message 用中文 SEMANTIC 风格(feat:/fix:/docs:)
+- 每个任务结束必须 commit(单文件或强耦合文件组一个 commit)
+
+---
+
+## 文件结构总览
+
+| # | 文件 | 动作 | 职责 |
+|---|------|------|------|
+| 1 | `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` | 修改 | 迁移261:butler_profiles.accepting |
+| 2 | `cfc-backend/src/main/resources/schema.sql` | 修改 | 同步 accepting 列定义 |
+| 3 | `cfc-backend/src/main/java/com/etotem/cfc/entity/ButlerProfile.java` | 修改 | 加 accepting 字段 |
+| 4 | `cfc-backend/src/main/java/com/etotem/cfc/entity/ButlerAssignment.java` | 创建 | butler_assignments 表实体(当前全库无引用,本功能为首写入方) |
+| 5 | `cfc-backend/src/main/java/com/etotem/cfc/mapper/ButlerAssignmentMapper.java` | 创建 | BaseMapper |
+| 6 | `cfc-backend/src/main/java/com/etotem/cfc/service/ButlerService.java` | 修改 | 家庭端三方法 + 管理端三方法 |
+| 7 | `cfc-backend/src/main/java/com/etotem/cfc/controller/butler/FamilyButlerSelectController.java` | 创建 | 家庭端 3 端点 |
+| 8 | `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminButlerController.java` | 修改 | 管理端 3 端点 |
+| 9 | `cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java` | 修改 | /health-coach/send 注入 coach_id |
+| 10 | `cfc-langgraph/app/graphs/health_coach_graph.py` | 修改 | generate_answer prompt 路由 |
+| 11 | `cfc-frontend/utils/api.js` | 修改 | 3 个接口封装 |
+| 12 | `cfc-frontend/pages/butler/select.vue` | 创建 | 管家选择页(三态) |
+| 13 | `cfc-frontend/pages.json` | 修改 | 注册 select 页 |
+| 14 | `cfc-frontend/pages/ai/chat.vue` | 修改 | 欢迎语带教练名 |
+| 15 | `cfc-frontend/pages/membership/index.vue` | 修改 | 我的管家入口卡片 |
+| 16 | `cfc-web/src/api/butler.js` | 修改 | 3 个封装 |
+| 17 | `cfc-web/src/views/admin/ButlerAssignments.vue` | 创建 | 分配管理视图 |
+| 18 | `cfc-web/src/views/admin/review/ButlerAudit.vue` | 修改 | 停接开关列 |
+| 19 | `docs/superpowers/api/API_REFERENCE.md` | 修改 | 登记 6 个新接口 |
+
+---
+
+### 任务 1:DB 迁移 —— butler_profiles.accepting 停接开关列
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`(迁移260 块之后,约 :9143 起)
+- 修改:`cfc-backend/src/main/resources/schema.sql`(butler_profiles CREATE TABLE,约 :2646 commission_rate_l2 行后)
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/entity/ButlerProfile.java`(commissionRateL2 字段后,约 :40)
+
+- [ ] **步骤 1.1:DatabaseInitializer 添加迁移 261**
+
+在 `// 迁移260: 创建圈子评论表` 整块结束后、下一个方法或空行处插入:
+
+```java
+		// 迁移261: butler_profiles 添加停接新单开关(管家自助选择功能)
+		ensureColumn("butler_profiles", "accepting",
+				"TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否接受新家庭绑定: 0=停接/1=接单'");
+```
+
+注意保持与相邻代码相同的缩进风格(该文件使用 Tab)。
+
+- [ ] **步骤 1.2:schema.sql 同步**
+
+在 `CREATE TABLE IF NOT EXISTS butler_profiles (` 块内,`commission_rate_l2 INT DEFAULT 5000,` 行之后插入一行:
+
+```sql
+    accepting TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否接受新家庭绑定: 0=停接/1=接单',
+```
+
+- [ ] **步骤 1.3:实体加字段**
+
+在 `ButlerProfile.java` 的 `private Integer commissionRateL2;` 之后加:
+
+```java
+    /**
+     * 是否接受新家庭绑定: 0=停接/1=接单
+     */
+    private Integer accepting;
+```
+
+- [ ] **步骤 1.4:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 1.5:Commit**
+
+```bash
+GIT_MASTER=1 git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java \
+  cfc-backend/src/main/resources/schema.sql \
+  cfc-backend/src/main/java/com/etotem/cfc/entity/ButlerProfile.java
+GIT_MASTER=1 git commit -m "feat: butler_profiles 新增accepting停接开关列"
+```
+
+---
+
+### 任务 2:新建 ButlerAssignment 实体与 Mapper
+
+**文件:**
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/entity/ButlerAssignment.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/mapper/ButlerAssignmentMapper.java`
+
+表结构参照(schema.sql 已有,勿改):`id, family_id(UNIQUE uk_family_assignment), butler_user_id, subscription_id, level, assigned_at`
+
+- [ ] **步骤 2.1:确认 Mapper 风格**
+
+运行:`cat cfc-backend/src/main/java/com/etom/cfc/mapper/ButlerProfileMapper.java 2>/dev/null || cat cfc-backend/src/main/java/com/etotem/cfc/mapper/ButlerProfileMapper.java`
+观察:是否有 `@Mapper` 注解、是否继承 `BaseMapper<T>`。新 Mapper 与其保持完全一致的风格。
+
+- [ ] **步骤 2.2:创建实体**
+
+`entity/ButlerAssignment.java`:
+
+```java
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 会员-管家分配记录(家庭自助选择/换绑写入)
+ */
+@Data
+@TableName("butler_assignments")
+public class ButlerAssignment {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 家庭 ID(uk_family_assignment 唯一)
+     */
+    private Long familyId;
+
+    /**
+     * 管家用户 ID
+     */
+    private Long butlerUserId;
+
+    /**
+     * 关联订阅 ID
+     */
+    private Long subscriptionId;
+
+    /**
+     * 订阅级别 L1/L2
+     */
+    private String level;
+
+    private Date assignedAt;
+}
+```
+
+若项目实体不使用 Lombok `@Data`(对照 ButlerProfile.java 是否有 getter/setter 手写),则改为手写各字段的 getter/setter,去掉 `@Data`。
+
+- [ ] **步骤 2.3:创建 Mapper**
+
+`mapper/ButlerAssignmentMapper.java`(按步骤 2.1 观察到的风格调整注解):
+
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.ButlerAssignment;
+
+public interface ButlerAssignmentMapper extends BaseMapper<ButlerAssignment> {
+}
+```
+
+- [ ] **步骤 2.4:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 2.5:Commit**
+
+```bash
+GIT_MASTER=1 git add cfc-backend/src/main/java/com/etotem/cfc/entity/ButlerAssignment.java \
+  cfc-backend/src/main/java/com/etotem/cfc/mapper/ButlerAssignmentMapper.java
+GIT_MASTER=1 git commit -m "feat: 新增butler_assignments实体与Mapper"
+```
+
+---
+
+### 任务 3:ButlerService 家庭端方法(浏览/查询/绑定)
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/ButlerService.java`
+
+已有依赖:`@Resource UserMapper userMapper; @Resource ButlerProfileMapper butlerProfileMapper; @Resource UserService userService;`
+需要新增依赖:`ButlerAssignmentMapper`、`MemberSubscriptionService`(Bean 名与类名首字母小写一致,符合注入规范)。
+
+- [ ] **步骤 3.1:添加依赖注入与 import**
+
+import 区新增:
+
+```java
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.etotem.cfc.entity.ButlerAssignment;
+import com.etotem.cfc.mapper.ButlerAssignmentMapper;
+import com.etotem.cfc.service.MemberSubscriptionService;
+import com.etotem.cfc.entity.MemberSubscription;
+```
+
+类内字段区新增:
+
+```java
+    @Resource
+    private ButlerAssignmentMapper butlerAssignmentMapper;
+
+    @Resource
+    private MemberSubscriptionService memberSubscriptionService;
+```
+
+- [ ] **步骤 3.2:追加三个家庭端方法**
+
+在类末尾(`assignMemberToButler(Long familyId, ...)` 方法 `}` 之后、类结束 `}` 之前)追加:
+
+```java
+    // ==================== 家庭端自助选择 ====================
+
+    /**
+     * 可选管家列表(浏览开放,不校验会员等级)
+     * 条件:approved + tier=L2 + accepting=1 + 有余量;返回脱敏档案
+     */
+    public Result<List<Map<String, Object>>> getAvailableButlers() {
+        LambdaQueryWrapper<ButlerProfile> query = new LambdaQueryWrapper<>();
+        query.eq(ButlerProfile::getStatus, "approved")
+             .eq(ButlerProfile::getTier, "L2")
+             .eq(ButlerProfile::getAccepting, 1)
+             .apply("member_count < max_members")
+             .orderByAsc(ButlerProfile::getCreatedAt);
+        SortUtil.applySort(query);
+        List<ButlerProfile> profiles = butlerProfileMapper.selectList(query);
+
+        List<Long> userIds = new java.util.ArrayList<>();
+        for (ButlerProfile p : profiles) {
+            userIds.add(p.getUserId());
+        }
+        Map<Long, User> userMap = new HashMap<>();
+        if (!userIds.isEmpty()) {
+            for (User u : userMapper.selectBatchIds(userIds)) {
+                userMap.put(u.getId(), u);
+            }
+        }
+
+        List<Map<String, Object>> result = new java.util.ArrayList<>();
+        for (ButlerProfile p : profiles) {
+            User u = userMap.get(p.getUserId());
+            Map<String, Object> item = new HashMap<>();
+            item.put("butlerUserId", p.getUserId());
+            item.put("nickname", u != null ? u.getNickname() : "管家");
+            item.put("avatar", u != null ? u.getAvatar() : "");
+            item.put("description", p.getDescription());
+            item.put("certificates", p.getCertificates());
+            item.put("remainingSlots", (p.getMaxMembers() == null ? 0 : p.getMaxMembers())
+                    - (p.getMemberCount() == null ? 0 : p.getMemberCount()));
+            result.add(item);
+        }
+        return Result.success(result);
+    }
+
+    /**
+     * 当前家庭的管家绑定信息(无绑定时 data 为 null)
+     */
+    public Result<Map<String, Object>> getMyButler(Long userId) {
+        Long familyId = requireFamilyId(userId);
+        if (familyId == null) {
+            return Result.error("未找到家庭信息");
+        }
+        ButlerAssignment assignment = butlerAssignmentMapper.selectOne(
+                new LambdaQueryWrapper<ButlerAssignment>()
+                        .eq(ButlerAssignment::getFamilyId, familyId)
+                        .last("LIMIT 1"));
+        if (assignment == null) {
+            return Result.success(null);
+        }
+        return Result.success(buildButlerSummary(assignment));
+    }
+
+    /**
+     * 自助绑定/更换管家:L2 校验 + 条件扣容 + INSERT/UPDATE 双路径
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public Result<Map<String, Object>> selectButler(Long userId, Long butlerUserId) {
+        Long familyId = requireFamilyId(userId);
+        if (familyId == null) {
+            return Result.error("未找到家庭信息");
+        }
+        // ① L2 有效订阅校验
+        MemberSubscription sub = memberSubscriptionService.getActiveSubscription(familyId);
+        boolean active = sub != null && sub.getExpireTime() != null
+                && sub.getExpireTime().after(new Date());
+        if (!active || !"L2".equals(sub.getLevel())) {
+            return Result.error("该功能面向久久一生(L2)会员家庭");
+        }
+        // ② 目标管家校验
+        ButlerProfile butler = butlerProfileMapper.selectOne(
+                new LambdaQueryWrapper<ButlerProfile>()
+                        .eq(ButlerProfile::getUserId, butlerUserId)
+                        .eq(ButlerProfile::getStatus, "approved")
+                        .eq(ButlerProfile::getTier, "L2")
+                        .eq(ButlerProfile::getAccepting, 1)
+                        .last("LIMIT 1"));
+        if (butler == null) {
+            return Result.error("该管家不可选择");
+        }
+        // ③ 条件原子扣容(防并发超卖)
+        LambdaUpdateWrapper<ButlerProfile> incr = new LambdaUpdateWrapper<>();
+        incr.eq(ButlerProfile::getId, butler.getId())
+            .apply("member_count < max_members")
+            .setSql("member_count = member_count + 1");
+        int affected = butlerProfileMapper.update(null, incr);
+        if (affected == 0) {
+            return Result.error("该管家名额已满");
+        }
+        // ④ assignments INSERT/UPDATE 双路径(激活自动分配只加计数不建行)
+        ButlerAssignment existing = butlerAssignmentMapper.selectOne(
+                new LambdaQueryWrapper<ButlerAssignment>()
+                        .eq(ButlerAssignment::getFamilyId, familyId)
+                        .last("LIMIT 1"));
+        if (existing != null) {
+            // 旧管家计数回退
+            LambdaUpdateWrapper<ButlerProfile> decr = new LambdaUpdateWrapper<>();
+            decr.eq(ButlerProfile::getUserId, existing.getButlerUserId())
+                .setSql("member_count = GREATEST(member_count - 1, 0)");
+            butlerProfileMapper.update(null, decr);
+
+            existing.setButlerUserId(butlerUserId);
+            existing.setLevel(sub.getLevel());
+            existing.setAssignedAt(new Date());
+            butlerAssignmentMapper.updateById(existing);
+        } else {
+            ButlerAssignment na = new ButlerAssignment();
+            na.setFamilyId(familyId);
+            na.setButlerUserId(butlerUserId);
+            na.setSubscriptionId(sub.getId());
+            na.setLevel(sub.getLevel());
+            na.setAssignedAt(new Date());
+            butlerAssignmentMapper.insert(na);
+        }
+        log.info("Family {} selected butler {}, level={}", familyId, butlerUserId, sub.getLevel());
+
+        ButlerAssignment saved = butlerAssignmentMapper.selectOne(
+                new LambdaQueryWrapper<ButlerAssignment>()
+                        .eq(ButlerAssignment::getFamilyId, familyId)
+                        .last("LIMIT 1"));
+        return Result.success(buildButlerSummary(saved));
+    }
+
+    /**
+     * userId -> familyId(与 MembershipController 同源:users.family_id)
+     */
+    private Long requireFamilyId(Long userId) {
+        User user = userMapper.selectById(userId);
+        return user != null ? user.getFamilyId() : null;
+    }
+
+    /**
+     * 组装绑定摘要(脱敏:不含联系方式)
+     */
+    private Map<String, Object> buildButlerSummary(ButlerAssignment assignment) {
+        Map<String, Object> summary = new HashMap<>();
+        summary.put("butlerUserId", assignment.getButlerUserId());
+        summary.put("level", assignment.getLevel());
+        summary.put("assignedAt", assignment.getAssignedAt());
+        User bu = userMapper.selectById(assignment.getButlerUserId());
+        summary.put("nickname", bu != null ? bu.getNickname() : "管家");
+        summary.put("avatar", bu != null ? bu.getAvatar() : "");
+        ButlerProfile bp = butlerProfileMapper.selectOne(
+                new LambdaQueryWrapper<ButlerProfile>()
+                        .eq(ButlerProfile::getUserId, assignment.getButlerUserId())
+                        .last("LIMIT 1"));
+        summary.put("description", bp != null ? bp.getDescription() : "");
+        return summary;
+    }
+```
+
+- [ ] **步骤 3.3:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS。若报 `getAccepting()` 不存在,检查任务 1 的实体字段;若报 `MemberSubscription` import 找不到,确认包路径 `com.etotem.cfc.entity.MemberSubscription`。
+
+- [ ] **步骤 3.4:Commit**
+
+```bash
+GIT_MASTER=1 git add cfc-backend/src/main/java/com/etotem/cfc/service/ButlerService.java
+GIT_MASTER=1 git commit -m "feat: ButlerService新增管家浏览/查询/自助绑定方法"
+```
+
+---
+
+### 任务 4:FamilyButlerSelectController 家庭端接口
+
+**文件:**
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/controller/butler/FamilyButlerSelectController.java`
+
+Bean 名 `familyButlerSelectController` 与既有 `butlerController` 无冲突。禁止 Controller 直连 Mapper——全部走 ButlerService(任务 3 已封装)。
+
+- [ ] **步骤 4.1:创建控制器**
+
+```java
+package com.etotem.cfc.controller.butler;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.service.ButlerService;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 家庭端管家自助选择(仅 L2 绑定受限,浏览开放)
+ */
+@RestController
+@RequestMapping("/api/butler")
+public class FamilyButlerSelectController {
+
+    @Resource
+    private ButlerService butlerService;
+
+    @ApiOperation("可选管家列表(浏览开放)")
+    @PostMapping("/available-list")
+    public Result<List<Map<String, Object>>> availableList() {
+        return butlerService.getAvailableButlers();
+    }
+
+    @ApiOperation("我家的管家绑定信息")
+    @PostMapping("/my-butler")
+    public Result<Map<String, Object>> myButler(@RequestAttribute("userId") Long userId) {
+        return butlerService.getMyButler(userId);
+    }
+
+    @ApiOperation("绑定/更换管家(仅L2)")
+    @PostMapping("/select")
+    public Result<Map<String, Object>> select(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> params) {
+        Object butlerUserIdObj = params.get("butlerUserId");
+        if (butlerUserIdObj == null || butlerUserIdObj.toString().trim().isEmpty()) {
+            return Result.error("缺少 butlerUserId 参数");
+        }
+        Long butlerUserId = Long.valueOf(butlerUserIdObj.toString());
+        return butlerService.selectButler(userId, butlerUserId);
+    }
+}
+```
+
+注意:`Result` 的实际包路径以 `ButlerService.java` 现有 import 为准(`com.etotem.cfc.common.Result`)。若 ButlerService 中三方法返回的 `Result` 泛型与此不一致,以 ButlerService 定义为准同步控制器签名。
+
+- [ ] **步骤 4.2:路由冲突检查**
+
+运行:`grep -rn '"/api/butler' cfc-backend/src/main/java/com/etotem/cfc/controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u`
+预期:新增的 `/available-list`、`/my-butler`、`/select` 不与任何既有路径重复。
+
+- [ ] **步骤 4.3:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 4.4:Commit**
+
+```bash
+GIT_MASTER=1 git add cfc-backend/src/main/java/com/etotem/cfc/controller/butler/FamilyButlerSelectController.java
+GIT_MASTER=1 git commit -m "feat: 家庭端管家自助选择接口"
+```
+
+---
+
+### 任务 5:管理端接口(分配列表/强制解绑/停接开关)
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/ButlerService.java`
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminButlerController.java`
+
+- [ ] **步骤 5.1:确认管理员鉴权模式**
+
+运行:`sed -n '1,60p' cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminButlerController.java`
+记录该控制器如何校验管理员(通常 `@RequestAttribute("role")` + `"admin".equals(role)` 判断)。后续端点完全复制该守卫模式。
+
+- [ ] **步骤 5.2:ButlerService 追加管理端方法**
+
+在任务 3 追加代码之后继续追加(import 额外需要 `com.baomidou.mybatisplus.extension.plugins.pagination.Page`,该文件头部已 import):
+
+```java
+    // ==================== 管理端分配管理 ====================
+
+    /**
+     * 分配关系分页列表
+     */
+    public Result<Map<String, Object>> listAssignments(Integer pageNum, Integer pageSize) {
+        int pn = pageNum == null || pageNum < 1 ? 1 : pageNum;
+        int ps = pageSize == null || pageSize < 1 ? 10 : Math.min(pageSize, 100);
+        Page<ButlerAssignment> page = butlerAssignmentMapper.selectPage(new Page<>(pn, ps),
+                new LambdaQueryWrapper<ButlerAssignment>()
+                        .orderByDesc(ButlerAssignment::getAssignedAt));
+
+        List<Map<String, Object>> rows = new java.util.ArrayList<>();
+        for (ButlerAssignment a : page.getRecords()) {
+            Map<String, Object> row = new HashMap<>();
+            row.put("id", a.getId());
+            row.put("familyId", a.getFamilyId());
+            row.put("butlerUserId", a.getButlerUserId());
+            row.put("level", a.getLevel());
+            row.put("assignedAt", a.getAssignedAt());
+            User bu = userMapper.selectById(a.getButlerUserId());
+            row.put("butlerNickname", bu != null ? bu.getNickname() : "");
+            rows.add(row);
+        }
+        Map<String, Object> data = new HashMap<>();
+        data.put("total", page.getTotal());
+        data.put("records", rows);
+        return Result.success(data);
+    }
+
+    /**
+     * 强制解绑(物理删除分配行 + 回退管家计数)
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public Result<Void> unassign(Long assignmentId) {
+        ButlerAssignment assignment = butlerAssignmentMapper.selectById(assignmentId);
+        if (assignment == null) {
+            return Result.error("分配记录不存在");
+        }
+        butlerAssignmentMapper.deleteById(assignmentId);
+
+        LambdaUpdateWrapper<ButlerProfile> decr = new LambdaUpdateWrapper<>();
+        decr.eq(ButlerProfile::getUserId, assignment.getButlerUserId())
+            .setSql("member_count = GREATEST(member_count - 1, 0)");
+        butlerProfileMapper.update(null, decr);
+
+        log.info("Admin unassigned assignmentId={}, family={}, butler={}",
+                assignmentId, assignment.getFamilyId(), assignment.getButlerUserId());
+        return Result.success(null);
+    }
+
+    /**
+     * 管家停接/恢复接单开关(不影响存量绑定)
+     */
+    public Result<Void> toggleAccepting(Long butlerUserId, Integer accepting) {
+        if (accepting == null || (accepting != 0 && accepting != 1)) {
+            return Result.error("accepting 取值只能为 0 或 1");
+        }
+        ButlerProfile profile = butlerProfileMapper.selectOne(
+                new LambdaQueryWrapper<ButlerProfile>()
+                        .eq(ButlerProfile::getUserId, butlerUserId)
+                        .last("LIMIT 1"));
+        if (profile == null) {
+            return Result.error("管家不存在");
+        }
+        profile.setAccepting(accepting);
+        profile.setUpdatedAt(new Date());
+        butlerProfileMapper.updateById(profile);
+        return Result.success(null);
+    }
+```
+
+- [ ] **步骤 5.3:AdminButlerController 追加端点**
+
+在该控制器最后一个方法之后追加(守卫代码从步骤 5.1 观察到的现有方法中逐字复制;下例假设标准模式):
+
+```java
+    @Operation(summary = "分配关系分页列表")
+    @PostMapping("/assignments")
+    public Result<Map<String, Object>> assignments(@RequestBody Map<String, Object> params) {
+        // ← 此处粘贴步骤 5.1 观察到的管理员守卫代码(与现有方法一致)
+        Integer pageNum = params.get("pageNum") != null ? Integer.valueOf(params.get("pageNum").toString()) : 1;
+        Integer pageSize = params.get("pageSize") != null ? Integer.valueOf(params.get("pageSize").toString()) : 10;
+        return butlerService.listAssignments(pageNum, pageSize);
+    }
+
+    @Operation(summary = "强制解绑")
+    @PostMapping("/unassign")
+    public Result<Void> unassign(@RequestBody Map<String, Object> params) {
+        // ← 同上粘贴守卫代码
+        if (params.get("assignmentId") == null) {
+            return Result.error("缺少 assignmentId");
+        }
+        return butlerService.unassign(Long.valueOf(params.get("assignmentId").toString()));
+    }
+
+    @Operation(summary = "管家停接开关")
+    @PostMapping("/toggle-accepting")
+    public Result<Void> toggleAccepting(@RequestBody Map<String, Object> params) {
+        // ← 同上粘贴守卫代码
+        if (params.get("butlerUserId") == null || params.get("accepting") == null) {
+            return Result.error("缺少参数");
+        }
+        return butlerService.toggleAccepting(
+                Long.valueOf(params.get("butlerUserId").toString()),
+                Integer.valueOf(params.get("accepting").toString()));
+    }
+```
+
+同时确认控制器中已存在 `@Resource private ButlerService butlerService;`(AdminButlerController 现有 assign-member 功能应已注入;若无则补上,字段名必须为 `butlerService`)。`@Operation` 注解包路径以该文件现有用法为准。
+
+- [ ] **步骤 5.4:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 5.5:Commit**
+
+```bash
+GIT_MASTER=1 git add cfc-backend/src/main/java/com/etotem/cfc/service/ButlerService.java \
+  cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminButlerController.java
+GIT_MASTER=1 git commit -m "feat: 管理端分配管理与停接开关接口"
+```
+
+---
+
+### 任务 6:/health-coach/send 注入教练人格
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java`(sendHealthCoachMessage 方法内,约 :420-447)
+
+现状:该方法只放 `child_id`,缺 mascot 注入(/chat/send :76-109 已有完整范式)。
+
+- [ ] **步骤 6.1:定位并插入注入代码**
+
+找到 `sendHealthCoachMessage` 内这一段:
+
+```java
+        Map<String, Object> inputs = new LinkedHashMap<>();
+        if (memberId != null) {
+            inputs.put("child_id", memberId);
+        }
+        Map<String, Object> resp = aiGateway.chat(query, userId, conversationId, inputs);
+```
+
+在 `inputs.put("child_id", memberId);` 的 `}` 之后、`Map<String, Object> resp = ...` 之前插入:
+
+```java
+        // 注入教练人格(对齐 /chat/send 的 mascot 范式;coach_id 用于 LangGraph prompt 路由)
+        com.etotem.cfc.entity.User hcUser = userService.getUserInfo(userId);
+        String hcMascotCode = hcUser != null ? hcUser.getMascot() : null;
+        if (hcMascotCode != null && !hcMascotCode.isEmpty()) {
+            com.etotem.cfc.enums.MascotEnum hcMascot = com.etotem.cfc.enums.MascotEnum.fromCode(hcMascotCode);
+            if (hcMascot != null) {
+                inputs.put("coach_id", hcMascot.getCode());
+                inputs.put("mascot_name", hcMascot.name());
+                inputs.put("mascot_gender", hcMascot.gender());
+                inputs.put("mascot_persona", hcMascot.persona());
+            }
+        } else {
+            inputs.put("coach_id", "");
+            inputs.put("mascot_name", "健康教练");
+            inputs.put("mascot_gender", "");
+            inputs.put("mascot_persona", "");
+        }
+```
+
+说明:变量名用 `hcUser/hcMascot/hcMascotCode` 防止与本方法其他局部变量冲突;`mascot.name()` 保持与 /chat/send 既有行为一致(不修无关行为);AiGateway 会将 String 类型键值放入 body.context 透传(AiGateway.java:162-168)。
+
+- [ ] **步骤 6.2:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS(若 `userService` 未注入报错,检查类头 `@Resource private UserService userService;` 是否存在——/chat/send 已在使用,必然存在)
+
+- [ ] **步骤 6.3:Commit**
+
+```bash
+GIT_MASTER=1 git add cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java
+GIT_MASTER=1 git commit -m "feat: 健康教练对话注入教练人格标识"
+```
+
+---
+
+### 任务 7:LangGraph 人格 prompt 路由
+
+**文件:**
+- 修改:`cfc-langgraph/app/graphs/health_coach_graph.py`(generate_answer 函数开头,约 :87-90)
+
+- [ ] **步骤 7.1:改写 generate_answer 开头**
+
+将现有:
+
+```python
+    async def generate_answer(state: HealthCoachState) -> dict:
+        """带健康知识检索的 LLM 生成"""
+        messages = [SystemMessage(content=await get_prompt("health_coach") or DEFAULT_COACH_PROMPT)]
+```
+
+替换为:
+
+```python
+    async def generate_answer(state: HealthCoachState) -> dict:
+        """带健康知识检索的 LLM 生成"""
+        # 人格路由:context.coach_id (xibao/fubao) -> 专属prompt,三级回退保证可用性
+        _ctx = state.get("context") or {}
+        _coach_id = _ctx.get("coach_id")
+        if _coach_id not in ("xibao", "fubao"):
+            _coach_id = None
+        _persona_prompt = await get_prompt(f"health_coach_{_coach_id}") if _coach_id else None
+        _base_prompt = await get_prompt("health_coach")
+        messages = [SystemMessage(content=_persona_prompt or _base_prompt or DEFAULT_COACH_PROMPT)]
+```
+
+函数其余部分(ctx 家庭上下文、检索、save_memory 等)一律不动。
+
+- [ ] **步骤 7.2:语法验证**
+
+运行:`cd cfc-langgraph && python3 -m py_compile app/graphs/health_coach_graph.py && echo OK`
+预期:输出 `OK`
+
+- [ ] **步骤 7.3:运营配置两段提示词(人工/管理端操作,非代码)**
+
+登录 Web 管理端「系统提示词管理」(SystemPromptManagement 页),新增两条记录:
+
+| promptKey | 内容要点 |
+|-----------|----------|
+| `health_coach_xibao` | 浠宝·温暖知心风:倾听共情先行、温和提问引导、鼓励性语言、建议委婉分层给出;保留默认 prompt 的就医红线与中文要求 |
+| `health_coach_fubao` | 福宝·活泼行动派:语气轻快直接、优先给可执行行动清单、打卡式督促口吻、建议具体到频次;保留就医红线与中文要求 |
+
+两段均以现有 `health_coach` 默认提示词(DEFAULT_COACH_PROMPT 同款结构:核心原则/职责/回答结构/原则四段)为骨架改写语气即可。未配置时三级回退自动生效,不阻塞上线。
+
+- [ ] **步骤 7.4:Commit**
+
+```bash
+GIT_MASTER=1 git add cfc-langgraph/app/graphs/health_coach_graph.py
+GIT_MASTER=1 git commit -m "feat: 健康教练graph按coach_id路由人格prompt"
+```
+
+---
+
+### 任务 8:小程序 API 封装 + 管家选择页 + 路由注册
+
+**文件:**
+- 修改:`cfc-frontend/utils/api.js`(文件尾部追加)
+- 创建:`cfc-frontend/pages/butler/select.vue`
+- 修改:`cfc-frontend/pages.json`(butler 子包,约 :628-638)
+
+- [ ] **步骤 8.1:api.js 追加封装**
+
+在 `utils/api.js` 末尾追加:
+
+```javascript
+// ── 管家自助选择 ──
+export function getAvailableButlers() {
+  return request('/api/butler/available-list', 'POST', {})
+}
+export function getMyButler() {
+  return request('/api/butler/my-butler', 'POST', {})
+}
+export function selectButler(butlerUserId) {
+  return request('/api/butler/select', 'POST', { butlerUserId })
+}
+```
+
+- [ ] **步骤 8.2:pages.json 注册**
+
+将 butler 子包改为:
+
+```json
+    {
+      "root": "pages/butler",
+      "pages": [
+        {
+          "path": "apply",
+          "style": {
+            "navigationBarTitleText": "管家入驻"
+          }
+        },
+        {
+          "path": "select",
+          "style": {
+            "navigationBarTitleText": "选择管家"
+          }
+        }
+      ]
+    },
+```
+
+- [ ] **步骤 8.3:创建 select.vue(完整页面)**
+
+`pages/butler/select.vue`(Vue2 Options API;无可选链;flexbox;`:key` 用方法;时间用字符串截取格式化):
+
+```vue
+<template>
+  <view class="select-page">
+    <!-- 顶部状态区 -->
+    <view class="status-card" v-if="pageState === 'bound'">
+      <view class="current-label">我的管家</view>
+      <view class="current-row">
+        <image class="current-avatar" :src="myButler.avatar || '/static/logo.png'" mode="aspectFill" />
+        <view class="current-info">
+          <text class="current-name">{{ myButler.nickname }}</text>
+          <text class="current-date">{{ formatDate(myButler.assignedAt) }} 绑定</text>
+        </view>
+      </view>
+      <view class="current-desc">{{ myButler.description }}</view>
+      <button class="btn-change" @click="scrollToList">更换管家</button>
+    </view>
+    <view class="upgrade-banner" v-if="pageState === 'locked'" @click="goUpgrade">
+      <text class="upgrade-text">久久一生(L2)会员专享,升级后可选择专属管家</text>
+      <text class="upgrade-arrow">›</text>
+    </view>
+
+    <!-- 管家列表 -->
+    <view class="list-section" id="butlerList">
+      <view class="section-title">可选管家</view>
+      <view class="butler-card" v-for="(item, idx) in butlers" :key="getItemKey(idx)"
+        :class="{ active: myButler && myButler.butlerUserId === item.butlerUserId }">
+        <image class="card-avatar" :src="item.avatar || '/static/logo.png'" mode="aspectFill" />
+        <view class="card-body">
+          <view class="card-top">
+            <text class="card-name">{{ item.nickname }}</text>
+            <text class="card-slots">剩余名额 {{ item.remainingSlots }}</text>
+          </view>
+          <text class="card-desc">{{ item.description || '专业家庭健康管家' }}</text>
+        </view>
+        <button class="btn-select"
+          :disabled="pageState === 'locked' || item.remainingSlots <= 0"
+          @click="onSelect(item)">
+          {{ myButler && myButler.butlerUserId === item.butlerUserId ? '当前' : '选择' }}
+        </button>
+      </view>
+      <view class="empty-tip" v-if="butlers.length === 0 && !loading">暂无可选管家</view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getAvailableButlers, getMyButler, selectButler } from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      loading: false,
+      pageState: 'browse', // browse=可浏览未绑定 / bound=已绑定 / locked=非L2
+      lockReason: '',
+      myButler: null,
+      butlers: [],
+      keySeq: 0
+    }
+  },
+  onLoad() {
+    this.initPage()
+  },
+  onShow() {
+    if (this.myButler || this.pageState === 'bound') {
+      this.refreshMyButler()
+    }
+  },
+  methods: {
+    getItemKey(idx) {
+      var item = this.butlers[idx]
+      return item && item.butlerUserId ? 'b_' + item.butlerUserId : 'idx_' + idx
+    },
+    formatDate(iso) {
+      if (!iso) return ''
+      return iso.substring(0, 19).replace('T', ' ').substring(0, 16)
+    },
+    initPage() {
+      var that = this
+      that.loading = true
+      Promise.all([getAvailableButlers(), getMyButler()])
+        .then(function (resArr) {
+          var listRes = resArr[0]
+          var myRes = resArr[1]
+          if (listRes && listRes.code === 200) {
+            that.butlers = listRes.data || []
+          }
+          if (myRes && myRes.code === 200 && myRes.data) {
+            that.myButler = myRes.data
+            that.pageState = 'bound'
+          }
+        })
+        .catch(function () {
+          uni.showToast({ title: '加载失败', icon: 'none' })
+        })
+        .then(function () { that.loading = false })
+    },
+    refreshMyButler() {
+      var that = this
+      getMyButler().then(function (res) {
+        if (res && res.code === 200) {
+          that.myButler = res.data
+          that.pageState = res.data ? 'bound' : 'browse'
+        }
+      })
+    },
+    markLocked(msg) {
+      this.pageState = 'locked'
+      this.lockReason = msg || ''
+    },
+    onSelect(item) {
+      var that = this
+      if (that.myButler && that.myButler.butlerUserId === item.butlerUserId) return
+      uni.showModal({
+        title: '确认选择',
+        content: '确定选择「' + item.nickname + '」作为您的专属管家?',
+        success: function (mr) {
+          if (!mr.confirm) return
+          selectButler(item.butlerUserId).then(function (res) {
+            if (res && res.code === 200) {
+              uni.showToast({ title: '绑定成功', icon: 'success' })
+              that.initPage()
+            } else if (res && res.message && res.message.indexOf('久久一生') !== -1) {
+              that.markLocked(res.message)
+              uni.showModal({
+                title: '会员权益',
+                content: res.message,
+                showCancel: false,
+                success: function () { that.goUpgrade() }
+              })
+            } else {
+              uni.showToast({ title: (res && res.message) || '绑定失败', icon: 'none' })
+            }
+          })
+        }
+      })
+    },
+    scrollToList() {
+      uni.pageScrollTo({ selector: '#butlerList', duration: 300 })
+    },
+    goUpgrade() {
+      uni.navigateTo({ url: '/pages/membership/upgrade' })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.select-page { min-height: 100vh; background: #f5fafе; padding: 24rpx; box-sizing: border-box; display: flex; flex-direction: column; }
+.status-card { background: #fff; border-radius: 20rpx; padding: 32rpx; margin-bottom: 24rpx; }
+.current-label { font-size: 24rpx; color: #999; margin-bottom: 16rpx; }
+.current-row { display: flex; align-items: center; }
+.current-avatar { width: 96rpx; height: 96rpx; border-radius: 50%; margin-right: 20rpx; }
+.current-info { display: flex; flex-direction: column; }
+.current-name { font-size: 34rpx; font-weight: bold; color: #333; }
+.current-date { font-size: 22rpx; color: #999; margin-top: 6rpx; }
+.current-desc { font-size: 26rpx; color: #666; margin-top: 20rpx; line-height: 1.6; }
+.btn-change { margin-top: 24rpx; background: #4a9bd7; color: #fff; font-size: 28rpx; border-radius: 40rpx; }
+.upgrade-banner { display: flex; justify-content: space-between; align-items: center; background: #fff7ed; border: 1rpx solid #ffb56b; border-radius: 16rpx; padding: 24rpx; margin-bottom: 24rpx; }
+.upgrade-text { font-size: 26rpx; color: #d4770a; flex: 1; }
+.upgrade-arrow { font-size: 36rpx; color: #d4770a; margin-left: 12rpx; }
+.section-title { font-size: 30rpx; font-weight: bold; color: #333; margin: 8rpx 0 20rpx; }
+.butler-card { display: flex; align-items: center; background: #fff; border-radius: 20rpx; padding: 28rpx; margin-bottom: 20rpx; }
+.butler-card.active { border: 2rpx solid #4a9bd7; }
+.card-avatar { width: 88rpx; height: 88rpx; border-radius: 50%; margin-right: 20rpx; flex-shrink: 0; }
+.card-body { flex: 1; min-width: 0; }
+.card-top { display: flex; align-items: center; justify-content: space-between; }
+.card-name { font-size: 30rpx; font-weight: bold; color: #333; }
+.card-slots { font-size: 22rpx; color: #10b981; }
+.card-desc { display: block; font-size: 24rpx; color: #888; margin-top: 8rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.btn-select { width: 120rpx; height: 60rpx; line-height: 60rpx; padding: 0; font-size: 26rpx; background: #4a9bd7; color: #fff; border-radius: 30rpx; flex-shrink: 0; margin-left: 16rpx; }
+.btn-select[disabled] { background: #ccc; color: #fff; }
+.empty-tip { text-align: center; color: #999; font-size: 26rpx; padding: 60rpx 0; }
+</style>
+```
+
+⚠️ 注意上面样式第一行的 `background: #f5fafе;` —— 其中 `е` 必须是半角英文字母 e(此处若粘贴出现异常字符请手动改正为 `#f5fafe`)。
+
+- [ ] **步骤 8.4:语法验证**
+
+运行:
+```bash
+cd cfc-frontend && sed -n '/<script>/,/<\/script>/p' pages/butler/select.vue | sed '1d;$d' > /tmp/opencode/select_check.js && node --check /tmp/opencode/select_check.js && echo SYNTAX_OK
+```
+预期:`SYNTAX_OK`
+
+- [ ] **步骤 8.5:Commit**
+
+```bash
+GIT_MASTER=1 git add cfc-frontend/utils/api.js cfc-frontend/pages/butler/select.vue cfc-frontend/pages.json
+GIT_MASTER=1 git commit -m "feat: 小程序管家选择页与接口封装"
+```
+
+---
+
+### 任务 9:chat.vue 欢迎语 + 会员中心入口卡片
+
+**文件:**
+- 修改:`cfc-frontend/pages/ai/chat.vue`(welcome-title,:41)
+- 修改:`cfc-frontend/pages/membership/index.vue`(入口卡片 + 方法)
+
+- [ ] **步骤 9.1:chat.vue 欢迎语带教练名**
+
+将 :41 行整行:
+
+```html
+          <text class="welcome-title">{{ isNutrition ? '你好!我是营养助手' : (isHealthCoach ? '你好!我是健康教练' : (isButler ? '你好!我是健康管家' : ('你好!我是' + (mascotName || '家庭助手')))) }}</text>
+```
+
+替换为:
+
+```html
+          <text class="welcome-title">{{ isNutrition ? '你好!我是营养助手' : (isHealthCoach ? ('你好!我是' + (mascotName || '健康教练')) : (isButler ? '你好!我是健康管家' : ('你好!我是' + (mascotName || '家庭助手')))) }}</text>
+```
+
+- [ ] **步骤 9.2:membership/index.vue 定位插入点**
+
+运行:`grep -n "class=\"\(benefits\|cards\|banner\|section\)\|<view class=\"benefit\|权益" cfc-frontend/pages/membership/index.vue | head`
+选择一个主内容区容器(如权益卡片区之前),在其后插入下方卡片。若该页有现成"特权入口"横向卡片区,跟随其结构放置。
+
+- [ ] **步骤 9.3:插入「我的管家」卡片(模板)**
+
+```html
+    <!-- 我的管家入口 -->
+    <view class="butler-entry" @click="goButlerSelect">
+      <view class="butler-entry-icon">🤵</view>
+      <view class="butler-entry-main">
+        <text class="butler-entry-title">{{ butlerEntryTitle }}</text>
+        <text class="butler-entry-sub">{{ butlerEntrySub }}</text>
+      </view>
+      <text class="butler-entry-arrow">›</text>
+    </view>
+```
+
+- [ ] **步骤 9.4:membership/index.vue 脚本接线**
+
+data 中追加:
+
+```javascript
+      myButlerInfo: null,
+      isL2Active: false,
+```
+
+methods 中追加(并确认该页已引入 `getMyButler`——若无则在顶部 api 引入行加入 `getMyButler`):
+
+```javascript
+    goButlerSelect() {
+      uni.navigateTo({ url: '/pages/butler/select' })
+    },
+    loadButlerEntry() {
+      var that = this
+      getMyButler().then(function (res) {
+        if (res && res.code === 200 && res.data) {
+          that.myButlerInfo = res.data
+        }
+      }).catch(function () {})
+    },
+```
+
+computed 中追加:
+
+```javascript
+    butlerEntryTitle() {
+      return this.myButlerInfo ? ('我的管家 · ' + this.myButlerInfo.nickname) : '我的管家'
+    },
+    butlerEntrySub() {
+      return this.myButlerInfo ? '点击查看或更换' : '久久一生会员可自助选择专属管家'
+    },
+```
+
+onLoad/onShow 中调用一次 `this.loadButlerEntry()`。
+
+样式追加:
+
+```css
+.butler-entry { display: flex; align-items: center; background: #fff; border-radius: 16rpx; padding: 28rpx; margin: 20rpx 0; }
+.butler-entry-icon { font-size: 48rpx; margin-right: 20rpx; }
+.butler-entry-main { flex: 1; display: flex; flex-direction: column; }
+.butler-entry-title { font-size: 30rpx; font-weight: bold; color: #333; }
+.butler-entry-sub { font-size: 24rpx; color: #999; margin-top: 6rpx; }
+.butler-entry-arrow { font-size: 40rpx; color: #ccc; }
+```
+
+- [ ] **步骤 9.5:语法验证**
+
+运行:
+```bash
+cd cfc-frontend && sed -n '/<script>/,/<\/script>/p' pages/ai/chat.vue | sed '1d;$d' > /tmp/opencode/chat_check.js && node --check /tmp/opencode/chat_check.js && sed -n '/<script>/,/<\/script>/p' pages/membership/index.vue | sed '1d;$d' > /tmp/opencode/memb_check.js && node --check /tmp/opencode/memb_check.js && echo BOTH_OK
+```
+预期:`BOTH_OK`
+
+- [ ] **步骤 9.6:Commit**
+
+```bash
+GIT_MASTER=1 git add cfc-frontend/pages/ai/chat.vue cfc-frontend/pages/membership/index.vue
+GIT_MASTER=1 git commit -m "feat: 健康教练欢迎语个性化与我的管家入口"
+```
+
+---
+
+### 任务 10:Web 管理端(封装 + 分配管理视图 + 开关列 + 路由权限)
+
+**文件:**
+- 修改:`cfc-web/src/api/butler.js`(尾部追加)
+- 创建:`cfc-web/src/views/admin/ButlerAssignments.vue`
+- 修改:`cfc-web/src/views/admin/review/ButlerAudit.vue`(开关列)
+- 修改:`cfc-web/src/router/index.js` 及权限配置(定位后照抄邻居条目)
+
+- [ ] **步骤 10.1:api/butler.js 追加封装**
+
+```javascript
+// 分配关系列表
+export function getButlerAssignments(params) {
+  return request({
+    url: '/api/admin/butler/assignments',
+    method: 'post',
+    data: params
+  })
+}
+
+// 强制解绑
+export function unassignButler(data) {
+  return request({
+    url: '/api/admin/butler/unassign',
+    method: 'post',
+    data: data
+  })
+}
+
+// 停接开关
+export function toggleButlerAccepting(data) {
+  return request({
+    url: '/api/admin/butler/toggle-accepting',
+    method: 'post',
+    data: data
+  })
+}
+```
+
+- [ ] **步骤 10.2:创建 ButlerAssignments.vue(完整视图)**
+
+`views/admin/ButlerAssignments.vue`:
+
+```vue
+<template>
+  <div class="app-container">
+    <el-card shadow="never">
+      <div slot="header">
+        <span>管家分配管理</span>
+      </div>
+      <el-table v-loading="loading" :data="records" border stripe>
+        <el-table-column prop="id" label="ID" width="80" />
+        <el-table-column prop="familyId" label="家庭ID" width="100" />
+        <el-table-column prop="butlerNickname" label="管家" min-width="140" />
+        <el-table-column prop="butlerUserId" label="管家用户ID" width="110" />
+        <el-table-column prop="level" label="订阅级别" width="100">
+          <template slot-scope="{ row }">
+            <el-tag :type="row.level === 'L2' ? 'warning' : 'info'" size="small">{{ row.level }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="绑定时间" width="170">
+          <template slot-scope="{ row }">{{ formatTime(row.assignedAt) }}</template>
+        </el-table-column>
+        <el-table-column label="操作" width="120" fixed="right">
+          <template slot-scope="{ row }">
+            <el-button size="mini" type="danger" @click="onUnassign(row)">强制解绑</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-pagination
+        style="margin-top: 16px; text-align: right;"
+        layout="total, prev, pager, next"
+        :total="total"
+        :page-size="pageSize"
+        :current-page.sync="pageNum"
+        @current-change="loadData"
+      />
+    </el-card>
+  </div>
+</template>
+
+<script>
+import { getButlerAssignments, unassignButler } from '@/api/butler'
+
+export default {
+  name: 'ButlerAssignments',
+  data() {
+    return {
+      loading: false,
+      records: [],
+      total: 0,
+      pageNum: 1,
+      pageSize: 10
+    }
+  },
+  created() {
+    this.loadData()
+  },
+  methods: {
+    formatTime(iso) {
+      if (!iso) return '-'
+      return iso.substring(0, 19).replace('T', ' ')
+    },
+    loadData() {
+      this.loading = true
+      getButlerAssignments({ pageNum: this.pageNum, pageSize: this.pageSize })
+        .then(res => {
+          const data = res.data || {}
+          this.records = data.records || []
+          this.total = data.total || 0
+        })
+        .finally(() => { this.loading = false })
+    },
+    onUnassign(row) {
+      this.$confirm(
+        `确定解除家庭 ${row.familyId} 与管家「${row.butlerNickname}」的绑定?`,
+        '强制解绑',
+        { type: 'warning' }
+      ).then(() => unassignButler({ assignmentId: row.id }))
+        .then(res => {
+          if (res.code === 200) {
+            this.$message.success('已解绑')
+            this.loadData()
+          } else {
+            this.$message.error(res.message || '操作失败')
+          }
+        }).catch(() => {})
+    }
+  }
+}
+</script>
+```
+
+响应包装取值方式(res.data / res.code)先对照 `api/butler.js` 现有消费方(如 ButlerAudit.vue 中 `getButlerList(...).then(...)` 的取值层级)并保持一致,若全局拦截器已剥壳则同步调整。
+
+- [ ] **步骤 10.3:ButlerAudit.vue 加停接开关列**
+
+运行:`grep -n "el-table-column" cfc-web/src/views/admin/review/ButlerAudit.vue | tail -5`
+找到表格最后一列的位置,其后插入(prop 名以该表格数据字段为准,若列表项含 `userId` 则如下):
+
+```html
+        <el-table-column label="接单状态" width="110">
+          <template slot-scope="{ row }">
+            <el-switch
+              :value="row.accepting !== 0"
+              active-color="#13ce66"
+              @change="onToggleAccepting(row)" />
+            <div v-if="row.accepting === 0" style="font-size: 12px; color: #f56c6c;">已停接</div>
+          </template>
+        </el-table-column>
+```
+
+methods 中追加(并顶部 import `toggleButlerAccepting`):
+
+```javascript
+    async onToggleAccepting(row) {
+      const next = row.accepting === 0 ? 1 : 0
+      try {
+        const res = await toggleButlerAccepting({ butlerUserId: row.userId, accepting: next })
+        if (res.code === 200) {
+          this.$set(row, 'accepting', next)
+          this.$message.success(next === 1 ? '已恢复接单' : '已停接新单')
+        } else {
+          this.$message.error(res.message || '操作失败')
+        }
+      } catch (e) {
+        this.$message.error('操作失败')
+      }
+    },
+```
+
+若管家列表接口未返回 `accepting` 字段,需回查后端 list 接口的组装逻辑把 `profile.getAccepting()` 放进行数据(在 AdminButlerController/相关 Service 中搜索现有 list 的组装点,加一行 `row.put("accepting", p.getAccepting())` 同级代码),并在本计划内完成该最小改动后再进入下一任务。
+
+- [ ] **步骤 10.4:路由与权限注册(定位式)**
+
+运行:`grep -rn "ButlerAudit" cfc-web/src/router/ cfc-web/src/permissions.js 2>/dev/null ; grep -rn "path: '/admin" cfc-web/src/router/index.js | head -8`
+- 若找到 ButlerAudit 的注册行:紧随其后照抄一条 ButlerAssignments 路由(component 改为 `() => import('@/views/admin/ButlerAssignments.vue')`,name/path/meta 结构逐字段镜像邻居条目)。
+- 若 router 中搜不到但页面可访问(动态注册机制):在 `src/router/index.js` 里找任一 admin 视图条目作为模板,同样镜像新增。
+- 若存在独立权限清单文件(步骤命令会暴露路径):为新路由追加同名权限码。
+
+- [ ] **步骤 10.5:Commit**
+
+```bash
+GIT_MASTER=1 git add cfc-web/src/api/butler.js cfc-web/src/views/admin/ButlerAssignments.vue \
+  cfc-web/src/views/admin/review/ButlerAudit.vue cfc-web/src/router/index.js
+GIT_MASTER=1 git commit -m "feat: 管理端分配管理与停接开关页面"
+```
+
+---
+
+### 任务 11:API 文档登记 + 全量验收
+
+**文件:**
+- 修改:`docs/superpowers/api/API_REFERENCE.md`
+
+- [ ] **步骤 11.1:登记 6 个新接口**
+
+运行:`grep -n "api/butler" docs/superpowers/api/API_REFERENCE.md | head -3` 定位管家章节,按该文档既有表格格式追加:
+
+| 接口 | 说明 | 权限 |
+|------|------|------|
+| `POST /api/butler/available-list` | 可选管家列表(浏览开放,脱敏) | 登录 |
+| `POST /api/butler/my-butler` | 我家当前管家绑定信息 | 登录 |
+| `POST /api/butler/select` | 绑定/更换管家(仅L2有效订阅) | 登录 |
+| `POST /api/admin/butler/assignments` | 分配关系分页列表 | 管理员 |
+| `POST /api/admin/butler/unassign` | 强制解绑 | 管理员 |
+| `POST /api/admin/butler/toggle-accepting` | 管家停接新单开关 | 管理员 |
+
+另在 AI 章节给 `/api/ai/health-coach/send` 的请求体补充一行说明:新增可选字段 `coach_id` 由服务端自动注入(客户端无需传)。
+
+- [ ] **步骤 11.2:最终验证清单(对照规格 §十一 验收标准)**
+
+```bash
+cd cfc-backend && mvn clean compile          # EXIT 0
+cd ../cfc-langgraph && python3 -m py_compile app/graphs/health_coach_graph.py && echo LG_OK
+for f in pages/butler/select.vue pages/ai/chat.vue pages/membership/index.vue; do
+  cd cfc-frontend && sed -n '/<script>/,/<\/script>/p' "$f" | sed '1d;$d' > /tmp/opencode/final_check.js && node --check /tmp/opencode/final_check.js && echo "OK: $f" && cd ..
+done
+```
+预期:BUILD SUCCESS / LG_OK / 三行 OK。
+
+- [ ] **步骤 11.3:Commit**
+
+```bash
+GIT_MASTER=1 git add docs/superpowers/api/API_REFERENCE.md
+GIT_MASTER=1 git commit -m "docs: 登记管家自选与健康教练接口文档"
+```
+
+---
+
+## 自检记录(编写者已完成)
+
+1. **规格覆盖度**:§1 数据模型→任务1;§2 家庭端接口→任务3/4;§2 AI端→任务6;§2 管理端→任务5;§3 LangGraph→任务7;§4 前端→任务8/9;§5 Web→任务10;§6 边界(条件扣容/GREATEST回退/双路径/三级回退/停接双拦截/物理删除)→任务3/5/7 对应实现;§十一 验收→任务11.2。无遗漏。
+2. **占位符扫描**:全文无"待定/TODO/类似任务N";两处"定位式步骤"(5.1/10.4)均为给出确切命令+两种已知形态的完整代码,属可执行指令而非缺口。
+3. **类型一致性**:`getAvailableButlers/getMyButler/selectButler/listAssignments/unassign/toggleAccepting` 在服务与控制器两侧签名一致;`accepting` 字段名贯穿迁移/schema/实体/接口/前端;`butlerUserId` 前后端统一驼峰;`coach_id` Java inputs 键与 Python `ctx.get("coach_id")` 一致。

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels