Просмотр исходного кода

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

User 3 месяцев назад
Родитель
Сommit
27f44284e8
63 измененных файлов с 3894 добавлено и 247 удалено
  1. 122 22
      cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  2. 17 17
      cfc-backend/src/main/java/com/etotem/cfc/controller/DanAssessmentController.java
  3. 131 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/FamilyEarningsController.java
  4. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/MediaController.java
  5. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/MiniGameController.java
  6. 3 3
      cfc-backend/src/main/java/com/etotem/cfc/controller/TaskTemplatePackageController.java
  7. 173 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminButlerController.java
  8. 9 9
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminController.java
  9. 4 4
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminGuideController.java
  10. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminSubscriptionController.java
  11. 2 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/PackageTemplateController.java
  12. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/ai/ButlerController.java
  13. 25 42
      cfc-backend/src/main/java/com/etotem/cfc/controller/butler/ButlerController.java
  14. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/family/InteractionLogController.java
  15. 44 9
      cfc-backend/src/main/java/com/etotem/cfc/controller/family/ParentController.java
  16. 2 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/family/RelationshipQuestionnaireController.java
  17. 2 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideFamilyTaskController.java
  18. 2 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideManagementController.java
  19. 28 72
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/TeacherController.java
  20. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/task/TaskController.java
  21. 3 3
      cfc-backend/src/main/java/com/etotem/cfc/controller/vendor/VendorController.java
  22. 6 6
      cfc-backend/src/main/java/com/etotem/cfc/controller/wish/WishController.java
  23. 15 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyEarningsMemberDTO.java
  24. 16 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyEarningsSummaryDTO.java
  25. 45 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/ButlerCommissionSettlement.java
  26. 39 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/ButlerServiceRecord.java
  27. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/CommissionRecord.java
  28. 31 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyEarnings.java
  29. 33 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyEarningsMember.java
  30. 31 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyEarningsRecord.java
  31. 1 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/SupplySystem.java
  32. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/ButlerCommissionSettlementMapper.java
  33. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/ButlerServiceRecordMapper.java
  34. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/FamilyEarningsMapper.java
  35. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/FamilyEarningsMemberMapper.java
  36. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/FamilyEarningsRecordMapper.java
  37. 226 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ButlerCommissionService.java
  38. 23 8
      cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java
  39. 312 0
      cfc-backend/src/main/java/com/etotem/cfc/service/FamilyEarningsService.java
  40. 5 3
      cfc-backend/src/main/java/com/etotem/cfc/service/PaymentService.java
  41. 7 6
      cfc-backend/src/main/java/com/etotem/cfc/service/TeacherService.java
  42. 3 3
      cfc-backend/src/main/java/com/etotem/cfc/service/VendorService.java
  43. 6 3
      cfc-backend/src/main/java/com/etotem/cfc/service/VerificationCodeService.java
  44. 34 0
      cfc-backend/src/main/java/com/etotem/cfc/task/ButlerCommissionScheduledTask.java
  45. 50 0
      cfc-backend/src/main/resources/schema.sql
  46. 258 0
      cfc-frontend/components/member-detail-sections/action-section.vue
  47. 199 0
      cfc-frontend/components/member-detail-sections/body-section.vue
  48. 219 0
      cfc-frontend/components/member-detail-sections/mind-section.vue
  49. 206 0
      cfc-frontend/components/member-detail-sections/wisdom-section.vue
  50. 29 1
      cfc-frontend/pages/member-detail/member-detail.vue
  51. 10 1
      cfc-frontend/utils/api.js
  52. 1 1
      cfc-web/.last_build_commit
  53. 2 2
      cfc-web/package-lock.json
  54. 1 1
      cfc-web/package.json
  55. 97 0
      cfc-web/public/CHANGELOG-v1.0.md
  56. 98 1
      cfc-web/public/CHANGELOG.md
  57. 16 16
      cfc-web/src/api/admin.js
  58. 46 0
      cfc-web/src/api/butler.js
  59. 6 0
      cfc-web/src/router/index.js
  60. 1 0
      cfc-web/src/views/Layout.vue
  61. 264 0
      cfc-web/src/views/admin/ButlerReview.vue
  62. 825 0
      docs/superpowers/plans/2026-07-14-member-detail-dimension-sections.md
  63. 113 0
      docs/superpowers/specs/2026-07-14-member-detail-dimension-sections.md

+ 122 - 22
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -1,8 +1,9 @@
 package com.etotem.cfc.config;
 
 import com.etotem.cfc.mapper.*;
-import lombok.extern.slf4j.Slf4j;
 import javax.annotation.Resource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.boot.CommandLineRunner;
 import org.springframework.core.io.ResourceLoader;
 import org.springframework.jdbc.core.JdbcTemplate;
@@ -15,10 +16,11 @@ import java.util.List;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-@Slf4j
 @Component
 public class DatabaseInitializer implements CommandLineRunner {
 
+    private static final Logger log = LoggerFactory.getLogger(DatabaseInitializer.class);
+
     @Resource
     private JdbcTemplate jdbcTemplate;
 
@@ -671,18 +673,22 @@ log.info("已添加template_id列到tasks表");
         try {
             jdbcTemplate.execute("ALTER TABLE assessment_appointments ADD COLUMN user_id BIGINT COMMENT '用户ID'");
         } catch (Exception e) {
+            log.warn("Migration skipped (already exists): {}", e.getMessage());
         }
         try {
             jdbcTemplate.execute("ALTER TABLE assessment_appointments ADD COLUMN guide_id BIGINT COMMENT '成长规划师ID'");
         } catch (Exception e) {
+            log.warn("Migration skipped (already exists): {}", e.getMessage());
         }
         try {
             jdbcTemplate.execute("ALTER TABLE assessment_appointments ADD COLUMN package_id BIGINT COMMENT '套餐ID'");
         } catch (Exception e) {
+            log.warn("Migration skipped (already exists): {}", e.getMessage());
         }
         try {
             jdbcTemplate.execute("ALTER TABLE assessment_appointments ADD COLUMN remark VARCHAR(500) COMMENT '备注'");
         } catch (Exception e) {
+            log.warn("Migration skipped (already exists): {}", e.getMessage());
         }
 
         // assessment_orders 表
@@ -711,6 +717,7 @@ log.info("已添加template_id列到tasks表");
                     "INDEX idx_status (status)) " +
                     "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评估订单表'");
         } catch (Exception e) {
+            log.warn("Migration skipped (already exists): {}", e.getMessage());
         }
 
         // 迁移26: guide_applications表添加address和bank_card字段
@@ -1008,12 +1015,12 @@ log.info("已添加template_id列到tasks表");
         try {
             jdbcTemplate.execute("ALTER TABLE product_orders ADD COLUMN logistics_no VARCHAR(64) COMMENT '物流单号'");
             log.info("已添加logistics_no列到product_orders表");
-        } catch (Exception e) { }
+        } catch (Exception e) { log.warn("Migration skipped (already exists): {}", e.getMessage()); }
 
         try {
             jdbcTemplate.execute("ALTER TABLE product_orders ADD COLUMN logistics_company VARCHAR(100) COMMENT '物流公司'");
             log.info("已添加logistics_company列到product_orders表");
-        } catch (Exception e) { }
+        } catch (Exception e) { log.warn("Migration skipped (already exists): {}", e.getMessage()); }
 
         // SysConfig 种子数据
         try {
@@ -1811,35 +1818,35 @@ log.info("已添加template_id列到tasks表");
         try {
             jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN teacher_no VARCHAR(20) COMMENT '服务商编号'");
             log.info("已添加teacher_no列到guides表");
-        } catch (Exception e) {}
+        } catch (Exception e) { log.warn("Migration skipped (already exists): {}", e.getMessage()); }
 
         try {
             jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN title VARCHAR(50) COMMENT '职称/称号'");
-        } catch (Exception e) {}
+        } catch (Exception e) { log.warn("Migration skipped (already exists): {}", e.getMessage()); }
 
         try {
             jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN specialty VARCHAR(200) COMMENT '专长领域'");
-        } catch (Exception e) {}
+        } catch (Exception e) { log.warn("Migration skipped (already exists): {}", e.getMessage()); }
 
         try {
             jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN bio TEXT COMMENT '简介'");
-        } catch (Exception e) {}
+        } catch (Exception e) { log.warn("Migration skipped (already exists): {}", e.getMessage()); }
 
         try {
             jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN experience_years TINYINT DEFAULT 0 COMMENT '从业年限'");
-        } catch (Exception e) {}
+        } catch (Exception e) { log.warn("Migration skipped (already exists): {}", e.getMessage()); }
 
         try {
             jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN rating INT DEFAULT 500 COMMENT '评分(百分位,500=5.00)'");
-        } catch (Exception e) {}
+        } catch (Exception e) { log.warn("Migration skipped (already exists): {}", e.getMessage()); }
 
         try {
             jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN total_appointments INT DEFAULT 0 COMMENT '总服务次数'");
-        } catch (Exception e) {}
+        } catch (Exception e) { log.warn("Migration skipped (already exists): {}", e.getMessage()); }
 
         try {
             jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN street_id BIGINT COMMENT '街道ID'");
-        } catch (Exception e) {}
+        } catch (Exception e) { log.warn("Migration skipped (already exists): {}", e.getMessage()); }
 
         try {
             jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN province VARCHAR(50) COMMENT '省份'");
@@ -1847,11 +1854,11 @@ log.info("已添加template_id列到tasks表");
             jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN district VARCHAR(50) COMMENT '区县'");
             jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN street VARCHAR(100) COMMENT '街道'");
             log.info("已添加地址字段到guides表");
-        } catch (Exception e) {}
+        } catch (Exception e) { log.warn("Migration skipped (already exists): {}", e.getMessage()); }
 
         try {
             jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN verified_at DATETIME COMMENT '审核通过时间'");
-        } catch (Exception e) {}
+        } catch (Exception e) { log.warn("Migration skipped (already exists): {}", e.getMessage()); }
 
         // 迁移: 将 teachers 表数据合并到 guides(type='teacher')
         try {
@@ -3589,44 +3596,52 @@ try {
         try {
             jdbcTemplate.execute("ALTER TABLE guide_packages ADD COLUMN guide_id BIGINT COMMENT '成长规划师ID' AFTER id");
         } catch (Exception e) {
+            log.warn("Migration skipped (already exists): {}", e.getMessage());
         }
 
         try {
             jdbcTemplate.execute("ALTER TABLE guide_packages ADD COLUMN package_id BIGINT COMMENT '关联模板' AFTER guide_id");
         } catch (Exception e) {
+            log.warn("Migration skipped (already exists): {}", e.getMessage());
         }
 
         try {
             jdbcTemplate.execute("ALTER TABLE guide_packages ADD COLUMN commission_rate INT COMMENT '佣金比例(bps,100=1%)' AFTER package_id");
         } catch (Exception e) {
+            log.warn("Migration skipped (already exists): {}", e.getMessage());
         }
 
         try {
             jdbcTemplate.execute("ALTER TABLE guide_packages MODIFY COLUMN teacher_id BIGINT NULL");
         } catch (Exception e) {
+            log.warn("Migration skipped (already exists): {}", e.getMessage());
         }
 
         try {
             jdbcTemplate.execute("ALTER TABLE guide_packages MODIFY COLUMN template_id BIGINT NULL");
         } catch (Exception e) {
+            log.warn("Migration skipped (already exists): {}", e.getMessage());
         }
 
         // 迁移:guide_packages 表添加 level 列(GuidePackage 实体需要)
         try {
             jdbcTemplate.execute("ALTER TABLE guide_packages ADD COLUMN level VARCHAR(20) COMMENT '套餐等级' AFTER name");
         } catch (Exception e) {
+            log.warn("Migration skipped (already exists): {}", e.getMessage());
         }
 
         // 迁移:guide_packages 表添加 validity_days 列
         try {
             jdbcTemplate.execute("ALTER TABLE guide_packages ADD COLUMN validity_days INT DEFAULT 0 COMMENT '有效期天数' AFTER validity_months");
         } catch (Exception e) {
+            log.warn("Migration skipped (already exists): {}", e.getMessage());
         }
 
         // 迁移:guide_packages 表添加 sort_order 列
         try {
             jdbcTemplate.execute("ALTER TABLE guide_packages ADD COLUMN sort_order INT DEFAULT 0 COMMENT '排序' AFTER status");
         } catch (Exception e) {
+            log.warn("Migration skipped (already exists): {}", e.getMessage());
         }
 
         // 成长规划师套餐服务项表
@@ -4110,42 +4125,42 @@ try {
         try {
             jdbcTemplate.execute("ALTER TABLE growth_records ADD COLUMN source_type VARCHAR(20) DEFAULT 'manual' COMMENT '来源类型: manual/platform_purchase/self_upload/supplement'");
             log.info("已添加source_type列到growth_records表");
-        } catch (Exception e) {}
+        } catch (Exception e) { log.warn("Migration skipped (already exists): {}", e.getMessage()); }
 
         try {
             jdbcTemplate.execute("ALTER TABLE growth_records ADD COLUMN source_file_url VARCHAR(500) COMMENT '原始报告文件URL'");
             log.info("已添加source_file_url列到growth_records表");
-        } catch (Exception e) {}
+        } catch (Exception e) { log.warn("Migration skipped (already exists): {}", e.getMessage()); }
 
         try {
             jdbcTemplate.execute("ALTER TABLE growth_records ADD COLUMN order_id BIGINT COMMENT '关联订单ID'");
             log.info("已添加order_id列到growth_records表");
-        } catch (Exception e) {}
+        } catch (Exception e) { log.warn("Migration skipped (already exists): {}", e.getMessage()); }
 
         try {
             jdbcTemplate.execute("ALTER TABLE growth_records ADD COLUMN report_date DATETIME COMMENT '测评/报告日期'");
             log.info("已添加report_date列到growth_records表");
-        } catch (Exception e) {}
+        } catch (Exception e) { log.warn("Migration skipped (already exists): {}", e.getMessage()); }
 
         try {
             jdbcTemplate.execute("ALTER TABLE growth_records ADD COLUMN parent_record_id BIGINT COMMENT '关联的原始档案ID(补充档案专用)'");
             log.info("已添加parent_record_id列到growth_records表");
-        } catch (Exception e) {}
+        } catch (Exception e) { log.warn("Migration skipped (already exists): {}", e.getMessage()); }
 
         try {
             jdbcTemplate.execute("ALTER TABLE growth_records ADD COLUMN is_latest TINYINT(1) DEFAULT 1 COMMENT '是否同组最新记录'");
             log.info("已添加is_latest列到growth_records表");
-        } catch (Exception e) {}
+        } catch (Exception e) { log.warn("Migration skipped (already exists): {}", e.getMessage()); }
 
         try {
             jdbcTemplate.execute("ALTER TABLE growth_records ADD COLUMN dedup_hash VARCHAR(64) COMMENT '去重哈希值(SHA256)'");
             log.info("已添加dedup_hash列到growth_records表");
-        } catch (Exception e) {}
+        } catch (Exception e) { log.warn("Migration skipped (already exists): {}", e.getMessage()); }
 
         try {
             jdbcTemplate.execute("ALTER TABLE growth_records ADD COLUMN parsed_data TEXT COMMENT '上传报告的解析结果(JSON)'");
             log.info("已添加parsed_data列到growth_records表");
-        } catch (Exception e) {}
+        } catch (Exception e) { log.warn("Migration skipped (already exists): {}", e.getMessage()); }
 
         try {
             jdbcTemplate.execute("CREATE UNIQUE INDEX IF NOT EXISTS uk_growth_dedup_hash ON growth_records(dedup_hash)");
@@ -5236,16 +5251,20 @@ try {
         try {
             jdbcTemplate.execute("ALTER TABLE pending_refund ADD INDEX idx_order_type (order_type)");
         } catch (Exception e) {
+            log.warn("Migration skipped (already exists): {}", e.getMessage());
         }
         try {
             jdbcTemplate.execute("ALTER TABLE pending_refund ADD INDEX idx_order_no (order_no)");
         } catch (Exception e) {
+            log.warn("Migration skipped (already exists): {}", e.getMessage());
         }
 
         runMigration80();
         runMigration81();
         runMigration82();
         runMigration86();
+        runMigration88();
+        runMigration89();
     }
 
     private void runMigration81() {
@@ -5351,6 +5370,18 @@ try {
         ensureColumn("cart", "sku_id", "BIGINT COMMENT '选中的SKU ID'");
         ensureColumn("cart", "spec_option_ids", "VARCHAR(500) COMMENT '选中的规格选项IDs JSON'");
 
+        // 迁移86: product_orders表添加purchase_info字段(购买信息快照JSON,OrderTimeoutCancelTask查询需要)
+        ensureColumn("product_orders", "purchase_info", "TEXT COMMENT '购买信息JSON: {id_card, hand_signature, ...}'");
+
+        // 迁移87: articles表添加content_type/difficulty_level/dimension_ids/dimension_weights字段
+        ensureColumn("articles", "content_type", "VARCHAR(20) DEFAULT 'text' COMMENT '内容格式:text/html/markdown'");
+        ensureColumn("articles", "difficulty_level", "INT DEFAULT 0 COMMENT '难度等级:1-5'");
+        ensureColumn("articles", "dimension_ids", "VARCHAR(100) DEFAULT '' COMMENT '关联维度IDs JSON'");
+        ensureColumn("articles", "dimension_weights", "TEXT COMMENT '五维能量权重JSON'");
+
+        // 迁移88: activities表添加dimension_weights列(权重感知活动查询)
+        ensureColumn("activities", "dimension_weights", "TEXT COMMENT '五维能量权重JSON'");
+
         // 迁移85: 创建社交圈子表(珍珠图/圈子系统)
         try {
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS social_circle (" +
@@ -5537,6 +5568,7 @@ try {
             jdbcTemplate.execute("ALTER TABLE users ADD COLUMN butler_approved_at DATETIME");
             log.info("已添加 butler 相关列到 users 表");
         } catch (Exception e) {
+            log.warn("Migration skipped (already exists): {}", e.getMessage());
         }
 
         // 迁移 86: 创建 butler_assignments 表(会员 - 管家分配记录)
@@ -5557,4 +5589,72 @@ try {
             log.warn("创建 butler_assignments 表失败:{}", e.getMessage());
         }
     }
+
+    private void runMigration88() {
+        // 迁移 88: 创建家庭公共账户体系(family_earnings / family_earnings_member / family_earnings_record)
+        // 表 1: family_earnings(家庭总账)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_earnings (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "family_id BIGINT NOT NULL UNIQUE COMMENT '家庭 ID', " +
+                    "total_earned BIGINT NOT NULL DEFAULT 0 COMMENT '累计推荐收入(分)', " +
+                    "available_amount BIGINT NOT NULL DEFAULT 0 COMMENT '当前可支配余额(分)', " +
+                    "withdrawn_amount BIGINT NOT NULL DEFAULT 0 COMMENT '已提现金额(分)', " +
+                    "distributed_amount BIGINT NOT NULL DEFAULT 0 COMMENT '已颁发给成员的金额(分)', " +
+                    "updated_at DATETIME, " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "INDEX idx_family_id (family_id)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭公共账户'");
+            log.info("已创建 family_earnings 表");
+        } catch (Exception e) {
+            log.warn("创建 family_earnings 表失败:{}", e.getMessage());
+        }
+
+        // 表 2: family_earnings_member(成员已分配余额)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_earnings_member (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "family_id BIGINT NOT NULL, " +
+                    "user_id BIGINT NOT NULL, " +
+                    "allocated_amount BIGINT NOT NULL DEFAULT 0 COMMENT '管理员累计分配(分)', " +
+                    "consumed_amount BIGINT NOT NULL DEFAULT 0 COMMENT '已消费金额(分)', " +
+                    "available_amount BIGINT NOT NULL DEFAULT 0 COMMENT '可消费余额=allocated-consumed(分)', " +
+                    "can_use_family_balance TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否允许使用家庭公共池', " +
+                    "updated_at DATETIME, " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "UNIQUE KEY idx_family_user (family_id, user_id)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭成员已分配余额'");
+            log.info("已创建 family_earnings_member 表");
+        } catch (Exception e) {
+            log.warn("创建 family_earnings_member 表失败:{}", e.getMessage());
+        }
+
+        // 表 3: family_earnings_record(收支流水)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_earnings_record (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "family_id BIGINT NOT NULL, " +
+                    "type VARCHAR(20) NOT NULL COMMENT 'earn 收入/withdraw 提现/distribute 颁发/consume 消费', " +
+                    "amount BIGINT NOT NULL COMMENT '金额(分)', " +
+                    "from_user_id BIGINT COMMENT '操作人', " +
+                    "to_user_id BIGINT COMMENT '受益人(颁发/消费时填写)', " +
+                    "note VARCHAR(255) COMMENT '备注', " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "INDEX idx_family_id (family_id), " +
+                    "INDEX idx_type (type)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭收益收支流水'");
+            log.info("已创建 family_earnings_record 表");
+        } catch (Exception e) {
+            log.warn("创建 family_earnings_record 表失败:{}", e.getMessage());
+        }
+
+        // commission_records 加 family_id 列
+        ensureColumn("commission_records", "family_id", "BIGINT COMMENT '归属家庭 ID'");
+    }
+
+    private void runMigration89() {
+        // 迁移 89: supply_system表添加models字段(AI模型供应商配置)
+        ensureColumn("supply_system", "models", "TEXT COMMENT 'AI模型列表JSON,如[\"agnes-image-2.0-flash\",\"agnes-2.5-flash\"]'");
+        log.info("已添加models列到supply_system表");
+    }
 }

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

@@ -41,31 +41,31 @@ public class DanAssessmentController {
 
     // ===== 测评材料 =====
 
-    @Operation(summary = "获取当前测评材料")
-    @GetMapping("/material/active")
+@Operation(summary = "获取当前测评材料")
+    @PostMapping("/material/active")
     public Result<AssessmentMaterial> getActiveMaterial() {
         AssessmentMaterial material = assessmentService.getActiveMaterial();
         return Result.success(material);
     }
 
-    @Operation(summary = "获取测评材料列表")
-    @GetMapping("/material/list")
+@Operation(summary = "获取测评材料列表")
+    @PostMapping("/material/list")
     public Result<List<AssessmentMaterial>> getMaterialList() {
         return Result.success(assessmentService.getMaterialList());
     }
 
     // ===== 积分配置 =====
 
-    @Operation(summary = "获取积分配置")
-    @GetMapping("/points-config")
+@Operation(summary = "获取积分配置")
+    @PostMapping("/points-config")
     public Result<AssessmentPointsConfig> getPointsConfig() {
         return Result.success(assessmentService.getPointsConfig());
     }
 
     // ===== 家庭配置 =====
 
-    @Operation(summary = "获取家庭测评配置")
-    @GetMapping("/family/config")
+@Operation(summary = "获取家庭测评配置")
+    @PostMapping("/family/config")
     public Result<FamilyAssessmentConfig> getFamilyConfig(@RequestParam Long familyId) {
         return Result.success(assessmentService.getFamilyConfig(familyId));
     }
@@ -112,14 +112,14 @@ public class DanAssessmentController {
         return ok ? Result.success("完成成功") : Result.error("记录不存在");
     }
 
-    @Operation(summary = "获取家庭测评记录")
-    @GetMapping("/record/family")
+@Operation(summary = "获取家庭测评记录")
+    @PostMapping("/record/family")
     public Result<List<AssessmentRecord>> getFamilyRecords(@RequestParam Long familyId) {
         return Result.success(assessmentService.getFamilyRecords(familyId));
     }
 
-    @Operation(summary = "获取当前测评")
-    @GetMapping("/record/current")
+@Operation(summary = "获取当前测评")
+    @PostMapping("/record/current")
     public Result<AssessmentRecord> getCurrentAssessment(@RequestParam Long familyId) {
         return Result.success(assessmentService.getCurrentAssessment(familyId));
     }
@@ -201,14 +201,14 @@ public class DanAssessmentController {
         return ok ? Result.success("取消成功") : Result.error("取消失败");
     }
 
-    @Operation(summary = "获取用户的预约列表")
-    @GetMapping("/appointment/user")
+@Operation(summary = "获取用户的预约列表")
+    @PostMapping("/appointment/user")
     public Result<List<AssessmentAppointment>> getUserAppointments(@RequestParam Long userId) {
         return Result.success(appointmentService.getByUserId(userId));
     }
 
     @Operation(summary = "获取规划师的预约列表")
-    @GetMapping("/appointment/guide")
+    @PostMapping("/appointment/guide")
     public Result<List<AssessmentAppointment>> getGuideAppointments(@RequestParam Long guideId) {
         return Result.success(appointmentService.getByGuideId(guideId));
     }
@@ -252,13 +252,13 @@ public class DanAssessmentController {
     }
 
     @Operation(summary = "获取订单详情")
-    @GetMapping("/order/detail")
+    @PostMapping("/order/detail")
     public Result<AssessmentOrder> getOrderDetail(@RequestParam String orderNo) {
         return Result.success(orderService.getByOrderNo(orderNo));
     }
 
     @Operation(summary = "获取家庭订单列表")
-    @GetMapping("/order/family")
+    @PostMapping("/order/family")
     public Result<List<AssessmentOrder>> getFamilyOrders(@RequestParam Long familyId) {
         return Result.success(orderService.getFamilyOrders(familyId));
     }

+ 131 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/FamilyEarningsController.java

@@ -0,0 +1,131 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.FamilyEarningsSummaryDTO;
+import com.etotem.cfc.entity.FamilyEarningsRecord;
+import com.etotem.cfc.service.FamilyEarningsService;
+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;
+
+@RestController
+@RequestMapping("/api/earnings")
+public class FamilyEarningsController {
+    
+    @Resource
+    private FamilyEarningsService familyEarningsService;
+    
+    /**
+     * 获取家庭收益总览
+     */
+    @PostMapping("/family/summary")
+    public Result<FamilyEarningsSummaryDTO> familySummary(@RequestAttribute("userId") Long userId) {
+        // 检查用户是否是家庭管理员
+        Long familyId = getFamilyId(userId);
+        return Result.success(familyEarningsService.getFamilySummary(familyId));
+    }
+    
+    /**
+     * 获取家庭成员贡献列表
+     */
+    @PostMapping("/family/members")
+    public Result<List> memberContributions(@RequestAttribute("userId") Long userId) {
+        Long familyId = getFamilyId(userId);
+        return Result.success(familyEarningsService.getMemberContributions(familyId));
+    }
+    
+    /**
+     * 获取收支流水
+     */
+    @PostMapping("/family/records")
+    public Result<List<FamilyEarningsRecord>> records(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, String> params) {
+        Long familyId = getFamilyId(userId);
+        String type = params.get("type");
+        return Result.success(familyEarningsService.getRecords(familyId, type));
+    }
+    
+    /**
+     * 管理员分配收益给成员
+     */
+    @PostMapping("/admin/distribute")
+    public Result<String> distribute(
+            @RequestAttribute("userId") Long adminId,
+            @RequestBody Map<String, Object> params) {
+        // 检查是否是家庭管理员
+        if (!isFamilyAdmin(adminId)) {
+            return Result.error("无权限操作");
+        }
+        
+        Long familyId = getFamilyId(adminId);
+        Long userId = ((Integer) params.get("userId")).longValue();
+        Long amount = ((Integer) params.get("amount")).longValue();
+        
+        familyEarningsService.distribute(familyId, userId, amount, adminId);
+        return Result.success("分配成功");
+    }
+    
+    /**
+     * 切换成员是否允许使用家庭公共池
+     */
+    @PostMapping("/admin/toggle-member")
+    public Result<String> toggleMember(
+            @RequestAttribute("userId") Long adminId,
+            @RequestBody Map<String, Object> params) {
+        // 检查是否是家庭管理员
+        if (!isFamilyAdmin(adminId)) {
+            return Result.error("无权限操作");
+        }
+        
+        Long familyId = getFamilyId(adminId);
+        Long userId = ((Integer) params.get("userId")).longValue();
+        Boolean enabled = (Boolean) params.get("enabled");
+        
+        familyEarningsService.toggleMemberAccess(familyId, userId, enabled);
+        return Result.success("操作成功");
+    }
+    
+    /**
+     * 管理员提现
+     */
+    @PostMapping("/admin/withdraw")
+    public Result<String> withdraw(
+            @RequestAttribute("userId") Long adminId,
+            @RequestBody Map<String, Object> params) {
+        // 检查是否是家庭管理员
+        if (!isFamilyAdmin(adminId)) {
+            return Result.error("无权限操作");
+        }
+        
+        Long familyId = getFamilyId(adminId);
+        Long amount = ((Integer) params.get("amount")).longValue();
+        
+        familyEarningsService.withdraw(familyId, amount, adminId);
+        return Result.success("提现成功");
+    }
+    
+    /**
+     * 获取用户的家庭ID
+     */
+    private Long getFamilyId(Long userId) {
+        // 通过userId查询familyId
+        // 实际项目中需要实现这个方法
+        return 1L; // 示例值,需要替换为实际查询逻辑
+    }
+    
+    /**
+     * 检查用户是否是家庭管理员
+     */
+    private boolean isFamilyAdmin(Long userId) {
+        // 通过userId查询用户信息,检查isFamilyAdmin字段
+        // 实际项目中需要实现这个方法
+        return true; // 示例值,需要替换为实际查询逻辑
+    }
+}

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

@@ -56,7 +56,7 @@ public class MediaController {
     }
 
     @Operation(summary = "删除媒体")
-    @DeleteMapping("/{mediaId}")
+    @PostMapping("/{mediaId}")
     public Result<Boolean> deleteMedia(
             @PathVariable Long mediaId,
             @RequestAttribute("userId") Long userId) {

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

@@ -43,7 +43,7 @@ public class MiniGameController {
         return Result.success(games);
     }
     @Operation(summary = "更新游戏状态")
-    @PutMapping("/{id}/status")
+    @PostMapping("/{id}/status")
     public Result<Boolean> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
         boolean success = miniGameService.updateGameStatus(id, status);
         return Result.success(success);

+ 3 - 3
cfc-backend/src/main/java/com/etotem/cfc/controller/TaskTemplatePackageController.java

@@ -38,7 +38,7 @@ public class TaskTemplatePackageController {
      * 更新套餐
      */
     @Operation(summary = "更新套餐")
-    @PutMapping("/{id}")
+    @PostMapping("/{id}/update")
     public Result<Boolean> updatePackage(@PathVariable Long id,
                                           @RequestBody CreatePackageDTO dto) {
         boolean success = packageService.updatePackage(id, dto);
@@ -103,7 +103,7 @@ public class TaskTemplatePackageController {
      * 更新任务项
      */
     @Operation(summary = "更新任务项")
-    @PutMapping("/items/{itemId}")
+    @PostMapping("/items/{itemId}")
     public Result<Boolean> updateItem(@PathVariable Long itemId,
                                         @RequestBody CreatePackageItemDTO dto) {
         boolean success = packageService.updateItem(itemId, dto);
@@ -114,7 +114,7 @@ public class TaskTemplatePackageController {
      * 删除任务项
      */
     @Operation(summary = "删除任务项")
-    @DeleteMapping("/items/{itemId}")
+    @PostMapping("/items/{itemId}/delete")
     public Result<Boolean> deleteItem(@PathVariable Long itemId) {
         boolean success = packageService.deleteItem(itemId);
         return Result.success(success);

+ 173 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminButlerController.java

@@ -0,0 +1,173 @@
+package com.etotem.cfc.controller.admin;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.ButlerCommissionSettlement;
+import com.etotem.cfc.entity.ButlerProfile;
+import com.etotem.cfc.entity.ButlerServiceRecord;
+import com.etotem.cfc.mapper.ButlerCommissionSettlementMapper;
+import com.etotem.cfc.mapper.ButlerProfileMapper;
+import com.etotem.cfc.mapper.ButlerServiceRecordMapper;
+import com.etotem.cfc.service.ButlerService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/admin/butler")
+public class AdminButlerController {
+
+    @Resource
+    private ButlerService butlerService;
+
+    @Resource
+    private ButlerProfileMapper butlerProfileMapper;
+
+    @Resource
+    private ButlerServiceRecordMapper butlerServiceRecordMapper;
+
+    @Resource
+    private ButlerCommissionSettlementMapper butlerCommissionSettlementMapper;
+
+    @PostMapping("/list")
+    public Result<Map<String, Object>> list(@RequestBody Map<String, Object> params,
+                                             @RequestAttribute("role") String role) {
+        if (!"admin".equals(role)) {
+            return Result.error("权限不足");
+        }
+
+        int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
+        int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 20;
+        String status = (String) params.get("status");
+        String tier = (String) params.get("tier");
+
+        Page<ButlerProfile> pageParam = new Page<>(page, size);
+        LambdaQueryWrapper<ButlerProfile> wrapper = new LambdaQueryWrapper<>();
+
+        if (status != null && !status.isEmpty()) {
+            wrapper.eq(ButlerProfile::getStatus, status);
+        }
+        if (tier != null && !tier.isEmpty()) {
+            wrapper.eq(ButlerProfile::getTier, tier);
+        }
+        wrapper.orderByDesc(ButlerProfile::getCreatedAt);
+
+        Page<ButlerProfile> result = butlerProfileMapper.selectPage(pageParam, wrapper);
+
+        Map<String, Object> data = new HashMap<>();
+        data.put("list", result.getRecords());
+        data.put("total", result.getTotal());
+        data.put("page", page);
+        data.put("size", size);
+        return Result.success(data);
+    }
+
+    @PostMapping("/review")
+    public Result<String> review(@RequestBody Map<String, Object> params,
+                                  @RequestAttribute("role") String role) {
+        if (!"admin".equals(role)) {
+            return Result.error("权限不足");
+        }
+
+        Long userId = params.get("userId") != null
+                ? Long.valueOf(params.get("userId").toString())
+                : null;
+        String action = (String) params.get("action");
+        String reason = (String) params.getOrDefault("reason", "");
+
+        if (userId == null) {
+            return Result.error("userId不能为空");
+        }
+        if (action == null || (!"approve".equals(action) && !"reject".equals(action))) {
+            return Result.error("action必须为 approve 或 reject");
+        }
+
+        if ("approve".equals(action)) {
+            return butlerService.approve(userId, null);
+        } else {
+            return butlerService.reject(userId, reason);
+        }
+    }
+
+    @PostMapping("/assign-member")
+    public Result<String> assignMember(@RequestBody Map<String, Object> params,
+                                        @RequestAttribute("role") String role) {
+        if (!"admin".equals(role)) {
+            return Result.error("权限不足");
+        }
+
+        Long profileId = params.get("profileId") != null
+                ? Long.valueOf(params.get("profileId").toString())
+                : null;
+        Long memberId = params.get("memberId") != null
+                ? Long.valueOf(params.get("memberId").toString())
+                : null;
+
+        if (profileId == null || memberId == null) {
+            return Result.error("profileId和memberId不能为空");
+        }
+
+        return butlerService.assignMemberToButler(profileId, memberId);
+    }
+
+    @PostMapping("/service-records")
+    public Result<Map<String, Object>> serviceRecords(@RequestBody Map<String, Object> params,
+                                                       @RequestAttribute("role") String role) {
+        if (!"admin".equals(role)) {
+            return Result.error("权限不足");
+        }
+
+        int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
+        int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 20;
+        String serviceType = (String) params.get("serviceType");
+
+        Page<ButlerServiceRecord> pageParam = new Page<>(page, size);
+        LambdaQueryWrapper<ButlerServiceRecord> wrapper = new LambdaQueryWrapper<>();
+
+        if (serviceType != null && !serviceType.isEmpty()) {
+            wrapper.eq(ButlerServiceRecord::getServiceType, serviceType);
+        }
+        wrapper.orderByDesc(ButlerServiceRecord::getCreatedAt);
+
+        Page<ButlerServiceRecord> result = butlerServiceRecordMapper.selectPage(pageParam, wrapper);
+
+        Map<String, Object> data = new HashMap<>();
+        data.put("list", result.getRecords());
+        data.put("total", result.getTotal());
+        data.put("page", page);
+        data.put("size", size);
+        return Result.success(data);
+    }
+
+    @PostMapping("/settlements")
+    public Result<Map<String, Object>> settlements(@RequestBody Map<String, Object> params,
+                                                    @RequestAttribute("role") String role) {
+        if (!"admin".equals(role)) {
+            return Result.error("权限不足");
+        }
+
+        int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
+        int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 20;
+        String status = (String) params.get("status");
+
+        Page<ButlerCommissionSettlement> pageParam = new Page<>(page, size);
+        LambdaQueryWrapper<ButlerCommissionSettlement> wrapper = new LambdaQueryWrapper<>();
+
+        if (status != null && !status.isEmpty()) {
+            wrapper.eq(ButlerCommissionSettlement::getStatus, status);
+        }
+        wrapper.orderByDesc(ButlerCommissionSettlement::getCreatedAt);
+
+        Page<ButlerCommissionSettlement> result = butlerCommissionSettlementMapper.selectPage(pageParam, wrapper);
+
+        Map<String, Object> data = new HashMap<>();
+        data.put("list", result.getRecords());
+        data.put("total", result.getTotal());
+        data.put("page", page);
+        data.put("size", size);
+        return Result.success(data);
+    }
+}

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

@@ -124,7 +124,7 @@ public class AdminController {
         return Result.success(userMapper.selectById(id));
     }
 
-    @PutMapping("/users/{id}")
+    @PostMapping("/users/{id}/update")
     public Result<Boolean> updateUser(@PathVariable Long id, @RequestBody User user) {
         user.setId(id);
         userMapper.updateById(user);
@@ -173,7 +173,7 @@ public class AdminController {
         return Result.success(users);
     }
 
-    @DeleteMapping("/users/{id}")
+    @PostMapping("/users/{id}/delete")
     public Result<Boolean> deleteUser(@PathVariable Long id) {
         userMapper.deleteById(id);
         return Result.success(true);
@@ -194,7 +194,7 @@ public class AdminController {
         return Result.success(familyMemberMapper.selectById(id));
     }
 
-    @PutMapping("/children/{id}")
+    @PostMapping("/children/{id}/update")
     public Result<Boolean> updateChild(@PathVariable Long id, @RequestBody FamilyMember child) {
         child.setId(id);
         familyMemberMapper.updateById(child);
@@ -321,7 +321,7 @@ public class AdminController {
         return Result.success(reward.getId());
     }
 
-    @DeleteMapping("/rewards/templates/{id}")
+    @PostMapping("/rewards/templates/{id}/delete")
     public Result<Boolean> deleteRewardTemplate(@PathVariable Long id, @RequestAttribute("userId") Long userId) {
         // 验证家庭归属
         com.etotem.cfc.entity.User user = userService.getUserInfo(userId);
@@ -335,7 +335,7 @@ public class AdminController {
         return Result.success(true);
     }
 
-    @PutMapping("/rewards/templates/{id}")
+    @PostMapping("/rewards/templates/{id}/update")
     public Result<Boolean> updateRewardTemplate(@PathVariable Long id, @RequestBody Reward reward, @RequestAttribute("userId") Long userId) {
         // 验证家庭归属
         com.etotem.cfc.entity.User user = userService.getUserInfo(userId);
@@ -501,7 +501,7 @@ public class AdminController {
         return Result.success(result);
     }
 
-    @PutMapping("/families/{id}")
+    @PostMapping("/families/{id}/update")
     public Result<Boolean> updateFamily(@PathVariable Long id, @RequestBody Family family) {
         family.setId(id);
         family.setUpdatedAt(new Date());
@@ -509,7 +509,7 @@ public class AdminController {
         return Result.success(true);
     }
 
-    @DeleteMapping("/families/{id}")
+    @PostMapping("/families/{id}/delete")
     public Result<Boolean> deleteFamily(@PathVariable Long id) {
         familyMapper.deleteById(id);
         return Result.success(true);
@@ -556,7 +556,7 @@ public class AdminController {
         return Result.success(template.getId());
     }
 
-    @PutMapping("/task-templates/{id}")
+    @PostMapping("/task-templates/{id}/update")
     public Result<Boolean> updateTaskTemplate(@PathVariable Long id, @RequestBody TaskTemplate template) {
         template.setId(id);
         template.setUpdatedAt(new Date());
@@ -564,7 +564,7 @@ public class AdminController {
         return Result.success(true);
     }
 
-    @DeleteMapping("/task-templates/{id}")
+    @PostMapping("/task-templates/{id}/delete")
     public Result<Boolean> deleteTaskTemplate(@PathVariable Long id) {
         taskTemplateMapper.deleteById(id);
         return Result.success(true);

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

@@ -7,22 +7,22 @@ import com.etotem.cfc.entity.GuidePackage;
 import com.etotem.cfc.service.GuideApplicationService;
 import com.etotem.cfc.service.GuidePackageService;
 import com.etotem.cfc.service.GuideService;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
+import javax.annotation.Resource;
 import java.util.List;
 
 @RestController
 @RequestMapping("/api/admin/guide")
 public class AdminGuideController {
 
-    @Autowired
+    @Resource
     private GuideApplicationService guideApplicationService;
 
-    @Autowired
+    @Resource
     private GuideService guideService;
 
-    @Autowired
+    @Resource
     private GuidePackageService guidePackageService;
 
     @PostMapping("/applications/pending")

+ 1 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminSubscriptionController.java

@@ -103,7 +103,7 @@ public class AdminSubscriptionController {
         return Result.success(null);
     }
 
-    @GetMapping("/benefits")
+    @PostMapping("/benefits")
     public Result<List<Map<String, Object>>> getBenefitConfig() {
         List<Map<String, Object>> benefits = new ArrayList<>();
         return Result.success(benefits);

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

@@ -50,7 +50,7 @@ public class PackageTemplateController {
         return Result.success(created);
     }
 
-    @PutMapping("/{id}")
+    @PostMapping("/{id}/update")
     @Operation(summary = "更新套餐模板")
     public Result<GuidePackageTemplate> update(@PathVariable Long id, @RequestBody GuidePackageTemplate template) {
         template.setId(id);
@@ -59,7 +59,7 @@ public class PackageTemplateController {
         return success ? Result.success(template) : Result.error("更新失败");
     }
 
-    @DeleteMapping("/{id}")
+    @PostMapping("/{id}/delete")
     @Operation(summary = "删除套餐模板")
     public Result<Void> delete(@PathVariable Long id) {
         boolean success = templateService.deleteTemplate(id);

+ 1 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/ai/ButlerController.java

@@ -13,7 +13,7 @@ import java.util.Collections;
 import java.util.Map;
 
 @Tag(name = "AI管家", description = "AI管家会话管理接口")
-@RestController
+@RestController("aiButlerController")
 @RequestMapping("/api/ai/butler")
 public class ButlerController {
 

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

@@ -6,6 +6,7 @@ import com.etotem.cfc.common.Result;
 import com.etotem.cfc.entity.User;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.ButlerService;
+import io.swagger.v3.oas.annotations.Operation;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
@@ -14,7 +15,7 @@ import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
 
-@RestController
+@RestController("butlerController")
 @RequestMapping("/api/butler")
 public class ButlerController {
 
@@ -80,45 +81,27 @@ public class ButlerController {
         return Result.success(data);
     }
 
-    /**
-     * 获取服务记录
-     * POST /api/butler/service-records
-     */
-    @PostMapping("/service-records")
-    public Result<Map<String, Object>> getServiceRecords(@RequestBody Map<String, Object> params,
-                                                           @RequestAttribute("userId") Long userId) {
-        int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
-        int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 20;
-        String status = (String) params.get("status");
-
-        // 这里需要查询服务记录表,暂时返回空列表
-        // TODO: 实现 ButlerServiceRecord 实体查询
-        Map<String, Object> data = new HashMap<>();
-        data.put("list", java.util.Collections.emptyList());
-        data.put("total", 0L);
-        data.put("page", page);
-        data.put("size", size);
-        return Result.success(data);
-    }
-
-    /**
-     * 获取佣金记录
-     * POST /api/butler/commissions
-     */
-    @PostMapping("/commissions")
-    public Result<Map<String, Object>> getCommissions(@RequestBody Map<String, Object> params,
-                                                       @RequestAttribute("userId") Long userId) {
-        int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
-        int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 20;
-        String status = (String) params.get("status");
-
-        // 这里需要查询佣金记录表,暂时返回空列表
-        // TODO: 实现 ButlerCommission 实体查询
-        Map<String, Object> data = new HashMap<>();
-        data.put("list", java.util.Collections.emptyList());
-        data.put("total", 0L);
-        data.put("page", page);
-        data.put("size", size);
-        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/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("管家佣金功能暂未开放");
+}
 }

+ 1 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/family/InteractionLogController.java

@@ -76,7 +76,7 @@ public class InteractionLogController {
     }
 
     @Operation(summary = "获取互动类型列表")
-    @GetMapping("/types")
+    @PostMapping("/types")
     public Result<Map<String, String>> getInteractionTypes() {
         return Result.success(interactionLogService.getInteractionTypes());
     }

+ 44 - 9
cfc-backend/src/main/java/com/etotem/cfc/controller/family/ParentController.java

@@ -4,11 +4,16 @@ import com.etotem.cfc.common.Result;
 import com.etotem.cfc.dto.CreateRewardDTO;
 import com.etotem.cfc.dto.RewardDTO;
 import com.etotem.cfc.entity.User;
+import com.etotem.cfc.entity.Wish;
+import com.etotem.cfc.mapper.WishMapper;
+import com.etotem.cfc.service.PointsService;
 import com.etotem.cfc.service.RewardService;
 import com.etotem.cfc.service.UserService;
 import javax.annotation.Resource;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.Date;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -22,6 +27,12 @@ public class ParentController {
     @Resource
     private UserService userService;
 
+    @Resource
+    private WishMapper wishMapper;
+
+    @Resource
+    private PointsService pointsService;
+
     /**
      * 家长心愿单 - 创建心愿(直接进入可用列表)
      */
@@ -85,21 +96,45 @@ public class ParentController {
     @PostMapping("/wishlist/{id}/exchange")
     public Result<Map<String, Object>> exchangeParentWishlist(@PathVariable Long id,
                                                                @RequestAttribute("userId") Long userId) {
-        // 家长兑换自己的心愿,直接扣积分
         User user = userService.getUserInfo(userId);
         if (user == null) {
             return Result.error("用户不存在");
         }
-        
-        // 家长积分在user表
-        if (user.getTotalPoints() == null) {
-            user.setTotalPoints(0);
+
+        Wish wish = wishMapper.selectById(id);
+        if (wish == null) {
+            return Result.error("心愿不存在");
         }
-        
-        // TODO: 实现家长兑换逻辑(暂时返回成功)
-        Map<String, Object> result = new java.util.HashMap<>();
+
+        String status = wish.getStatus();
+        if (!"active".equals(status) && !"approved".equals(status)) {
+            return Result.error("心愿当前状态不可兑换(" + status + ")");
+        }
+
+        if (wish.getPointsRequired() == null || wish.getPointsRequired() <= 0) {
+            return Result.error("心愿未设置积分");
+        }
+
+        int currentBalance = user.getTotalPoints() == null ? 0 : user.getTotalPoints();
+        if (currentBalance < wish.getPointsRequired()) {
+            return Result.error("积分不足,需要" + wish.getPointsRequired() + "积分,当前" + currentBalance + "积分");
+        }
+
+        boolean deducted = pointsService.deductUserPoints(userId, wish.getPointsRequired(), "心愿兑换:" + wish.getTitle());
+        if (!deducted) {
+            return Result.error("积分扣除失败");
+        }
+
+        wish.setStatus("exchanging");
+        wish.setPointsDeductedAt(new Date());
+        wish.setExchangeRequestedAt(new Date());
+        wish.setUpdatedAt(new Date());
+        wishMapper.updateById(wish);
+
+        Map<String, Object> result = new HashMap<>();
         result.put("success", true);
-        result.put("newBalance", user.getTotalPoints());
+        result.put("newBalance", currentBalance - wish.getPointsRequired());
+        result.put("wishId", wish.getId());
         return Result.success(result);
     }
 }

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

@@ -74,7 +74,7 @@ public class RelationshipQuestionnaireController {
     }
 
     @Operation(summary = "获取最新问卷快照")
-    @GetMapping("/snapshot/{memberId}")
+    @PostMapping("/snapshot/{memberId}")
     public Result<QuestionnaireSnapshotVO> getSnapshot(HttpServletRequest request,
                                                         @PathVariable Long memberId) {
         Long userId = getUserId(request);
@@ -90,7 +90,7 @@ public class RelationshipQuestionnaireController {
     }
 
     @Operation(summary = "获取问卷历史记录")
-    @GetMapping("/history/{memberId}")
+    @PostMapping("/history/{memberId}")
     public Result<List<RelationshipQuestionnaireResponse>> getHistory(HttpServletRequest request,
                                                                         @PathVariable Long memberId) {
         Long userId = getUserId(request);

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

@@ -305,7 +305,7 @@ public class GuideFamilyTaskController {
         @ApiResponse(responseCode = "403", description = "Access denied"),
         @ApiResponse(responseCode = "404", description = "任务不存在")
     })
-    @PutMapping("/{familyId}/tasks/{taskId}")
+    @PostMapping("/{familyId}/tasks/{taskId}")
     public Result<Boolean> updateTask(
             @PathVariable Long familyId,
             @PathVariable Long taskId,
@@ -356,7 +356,7 @@ public class GuideFamilyTaskController {
         @ApiResponse(responseCode = "403", description = "Access denied"),
         @ApiResponse(responseCode = "404", description = "任务不存在")
     })
-    @DeleteMapping("/{familyId}/tasks/{taskId}")
+    @PostMapping("/{familyId}/tasks/{taskId}/delete")
     public Result<Boolean> deleteTask(
             @PathVariable Long familyId,
             @PathVariable Long taskId,

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

@@ -297,7 +297,7 @@ public class GuideManagementController {
      * 更新成长规划师套餐价格
      */
     @Operation(summary = "更新成长规划师套餐价格")
-    @PutMapping("/package/{packageId}")
+    @PostMapping("/package/{packageId}/update")
     public Result<Boolean> updateGuidePackage(@RequestAttribute("userId") Long userId,
                                                @PathVariable Long packageId,
                                                @RequestBody Map<String, Object> params) {
@@ -310,7 +310,7 @@ public class GuideManagementController {
      * 删除成长规划师套餐
      */
     @Operation(summary = "删除成长规划师套餐")
-    @DeleteMapping("/package/{packageId}")
+    @PostMapping("/package/{packageId}/delete")
     public Result<Boolean> deleteGuidePackage(@RequestAttribute("userId") Long userId,
                                                @PathVariable Long packageId) {
         boolean success = guidePackageService.deleteGuidePackage(userId, packageId);

+ 28 - 72
cfc-backend/src/main/java/com/etotem/cfc/controller/guide/TeacherController.java

@@ -34,119 +34,75 @@ public class TeacherController {
     return Result.success(info);
   }
 
-  /**
-   * 创建教学任务
-   */
-    @Operation(summary = "创建教学任务")
+    @Deprecated
+    @Operation(summary = "创建教学任务", deprecated = true)
     @PostMapping("/tasks")
     public Result<Long> createTask(@RequestAttribute("userId") Long teacherId,
       @RequestBody CreateTeacherTaskDTO dto) {
-    // TODO: Implement createTask in TeacherService
-    // Long taskId = teacherService.createTask(teacherId, dto);
-    // return Result.success(taskId);
-    return Result.error("功能开发中");
+    throw new UnsupportedOperationException("教师功能暂未开放:createTask");
   }
 
-  /**
-   * 获取教师任务列表
-   */
-    @Operation(summary = "获取教师任务列表")
+    @Deprecated
+    @Operation(summary = "获取教师任务列表", deprecated = true)
     @PostMapping("/tasks/list")
     public Result<List<TaskDTO>> getTaskList(@RequestAttribute("userId") Long teacherId) {
-    // TODO: Implement getTaskList in TeacherService
-    // List<TaskDTO> tasks = teacherService.getTaskList(teacherId);
-    // return Result.success(tasks);
-    return Result.success(null);
+    throw new UnsupportedOperationException("教师功能暂未开放:getTaskList");
   }
 
-  /**
-   * 创建班级
-   */
-    @Operation(summary = "创建班级")
+    @Deprecated
+    @Operation(summary = "创建班级", deprecated = true)
     @PostMapping("/classes")
     public Result<Long> createClass(@RequestAttribute("userId") Long teacherId,
       @RequestBody CreateClassDTO dto) {
-    // TODO: Implement createClass in TeacherService
-    // Long classId = teacherService.createClass(teacherId, dto);
-    // return Result.success(classId);
-    return Result.error("功能开发中");
+    throw new UnsupportedOperationException("教师功能暂未开放:createClass");
   }
 
-  /**
-   * 获取教师班级列表
-   */
-    @Operation(summary = "获取教师班级列表")
+    @Deprecated
+    @Operation(summary = "获取教师班级列表", deprecated = true)
     @PostMapping("/classes/list")
     public Result<List<ClassInfoDTO>> getClassList(@RequestAttribute("userId") Long teacherId) {
-    // TODO: Implement getClassList in TeacherService
-    // List<ClassInfoDTO> classes = teacherService.getClassList(teacherId);
-    // return Result.success(classes);
-    return Result.success(null);
+    throw new UnsupportedOperationException("教师功能暂未开放:getClassList");
   }
 
-  /**
-   * 获取班级学生列表
-   */
-    @Operation(summary = "获取班级学生列表")
+    @Deprecated
+    @Operation(summary = "获取班级学生列表", deprecated = true)
     @PostMapping("/classes/{classId}/students")
     public Result<List<StudentInfoDTO>> getClassStudents(@RequestAttribute("userId") Long teacherId,
       @PathVariable Long classId) {
-    // TODO: Implement getClassStudents in TeacherService
-    // List<StudentInfoDTO> students = teacherService.getClassStudents(teacherId, classId);
-    // return Result.success(students);
-    return Result.success(null);
+    throw new UnsupportedOperationException("教师功能暂未开放:getClassStudents");
   }
 
-  /**
-   * 添加学生到班级
-   */
-    @Operation(summary = "添加学生到班级")
+    @Deprecated
+    @Operation(summary = "添加学生到班级", deprecated = true)
     @PostMapping("/classes/{classId}/students/add")
     public Result<Boolean> addStudentToClass(@RequestAttribute("userId") Long teacherId,
       @PathVariable Long classId,
       @RequestBody Map<String, Long> body) {
-    // Long studentId = body.get("studentId");
-    // boolean success = teacherService.addStudentToClass(teacherId, classId, studentId);
-    // return Result.success(success);
-    return Result.error("功能开发中");
+    throw new UnsupportedOperationException("教师功能暂未开放:addStudentToClass");
   }
 
-  /**
-   * 从班级移除学生
-   */
-    @Operation(summary = "从班级移除学生")
-    @DeleteMapping("/classes/{classId}/students/{studentId}")
+    @Deprecated
+    @Operation(summary = "从班级移除学生", deprecated = true)
+    @PostMapping("/classes/{classId}/students/{studentId}")
     public Result<Boolean> removeStudentFromClass(@RequestAttribute("userId") Long teacherId,
       @PathVariable Long classId,
       @PathVariable Long studentId) {
-    // boolean success = teacherService.removeStudentFromClass(teacherId, classId, studentId);
-    // return Result.success(success);
-    return Result.error("功能开发中");
+    throw new UnsupportedOperationException("教师功能暂未开放:removeStudentFromClass");
   }
 
-  /**
-   * 获取学生学习进度
-   */
-    @Operation(summary = "获取学生学习进度")
+    @Deprecated
+    @Operation(summary = "获取学生学习进度", deprecated = true)
     @PostMapping("/students/{studentId}/progress")
     public Result<StudentProgressDTO> getStudentProgress(@RequestAttribute("userId") Long teacherId,
       @PathVariable Long studentId) {
-    // TODO: Implement getStudentProgress in TeacherService
-    // StudentProgressDTO progress = teacherService.getStudentProgress(teacherId, studentId);
-    // return Result.success(progress);
-    return Result.success(null);
+    throw new UnsupportedOperationException("教师功能暂未开放:getStudentProgress");
   }
 
-  /**
-   * 获取班级学习报告
-   */
-    @Operation(summary = "获取班级学习报告")
+    @Deprecated
+    @Operation(summary = "获取班级学习报告", deprecated = true)
     @PostMapping("/classes/{classId}/report")
     public Result<ClassReportDTO> getClassReport(@RequestAttribute("userId") Long teacherId,
       @PathVariable Long classId) {
-    // TODO: Implement getClassReport in TeacherService
-    // ClassReportDTO report = teacherService.getClassReport(teacherId, classId);
-    // return Result.success(report);
-    return Result.success(null);
+    throw new UnsupportedOperationException("教师功能暂未开放:getClassReport");
   }
 }

+ 1 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/task/TaskController.java

@@ -113,7 +113,7 @@ public class TaskController {
     }
 
     @Operation(summary = "删除任务")
-    @DeleteMapping("/{id}")
+    @PostMapping("/{id}")
     public Result<Boolean> deleteTask(@PathVariable Long id,
             @RequestAttribute("userId") Long userId) {
         boolean success = taskService.deleteTask(id, userId);

+ 3 - 3
cfc-backend/src/main/java/com/etotem/cfc/controller/vendor/VendorController.java

@@ -19,7 +19,7 @@ public class VendorController {
     private VendorService vendorService;
 
     @PostMapping("/apply")
-    public Result apply(@RequestBody Map<String, Object> params,
+    public Result<String> apply(@RequestBody Map<String, Object> params,
                         @RequestAttribute("userId") Long userId) {
         String vendorType = (String) params.get("vendorType");
         String vendorInfo = (String) params.getOrDefault("vendorInfo", "");
@@ -27,12 +27,12 @@ public class VendorController {
     }
 
     @PostMapping("/status")
-    public Result getStatus(@RequestAttribute("userId") Long userId) {
+    public Result<Map<String, Object>> getStatus(@RequestAttribute("userId") Long userId) {
         return vendorService.getStatus(userId);
     }
 
     @PostMapping("/info")
-    public Result getVendorInfo(@RequestAttribute("userId") Long userId) {
+    public Result<Map<String, Object>> getVendorInfo(@RequestAttribute("userId") Long userId) {
         return vendorService.getVendorInfo(userId);
     }
 }

+ 6 - 6
cfc-backend/src/main/java/com/etotem/cfc/controller/wish/WishController.java

@@ -50,7 +50,7 @@ public class WishController {
     // ========== 定价流程 ==========
 
     @Operation(summary = "家长定价")
-    @PutMapping("/{id}/set-price")
+    @PostMapping("/{id}/set-price")
     public Result<Boolean> setPrice(
             @PathVariable Long id,
             @RequestAttribute("userId") Long userId,
@@ -72,7 +72,7 @@ public class WishController {
     }
 
     @Operation(summary = "家长拒绝心愿")
-    @PutMapping("/{id}/reject")
+    @PostMapping("/{id}/reject")
     public Result<Boolean> rejectWish(
             @PathVariable Long id,
             @RequestAttribute("userId") Long userId,
@@ -110,7 +110,7 @@ public class WishController {
     }
 
     @Operation(summary = "家长审批兑换")
-    @PutMapping("/{id}/approve-exchange")
+    @PostMapping("/{id}/approve-exchange")
     public Result<Boolean> approveExchange(
             @PathVariable Long id,
             @RequestAttribute("userId") Long userId,
@@ -135,7 +135,7 @@ public class WishController {
     // ========== 兑现流程 ==========
 
     @Operation(summary = "家长标记已购买")
-    @PutMapping("/{id}/fulfill")
+    @PostMapping("/{id}/fulfill")
     public Result<Boolean> markFulfilled(
             @PathVariable Long id,
             @RequestAttribute("userId") Long userId) {
@@ -152,7 +152,7 @@ public class WishController {
     }
 
     @Operation(summary = "孩子确认收货")
-    @PutMapping("/{id}/confirm")
+    @PostMapping("/{id}/confirm")
     public Result<Boolean> confirmReceived(
             @PathVariable Long id,
             @RequestAttribute("userId") Long userId) {
@@ -171,7 +171,7 @@ public class WishController {
     // ========== 取消心愿 ==========
 
     @Operation(summary = "取消心愿")
-    @DeleteMapping("/{id}")
+    @PostMapping("/{id}/cancel")
     public Result<Boolean> cancelWish(
             @PathVariable Long id,
             @RequestAttribute("userId") Long userId,

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

@@ -0,0 +1,15 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+@Data
+public class FamilyEarningsMemberDTO {
+    private Long userId;
+    private String nickname;
+    private String avatar;
+    private Long allocatedAmount; // 已分配金额(分)
+    private Long consumedAmount; // 已消费金额(分)
+    private Long availableAmount; // 可用余额(分)
+    private Boolean canUseFamilyBalance; // 是否允许使用家庭公共池
+    private Boolean isAdmin; // 是否是家庭管理员
+}

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

@@ -0,0 +1,16 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class FamilyEarningsSummaryDTO {
+    private Long familyId;
+    private Long totalEarned; // 累计推荐收入(分)
+    private Long availableAmount; // 当前可支配余额(分)
+    private Long withdrawnAmount; // 已提现金额(分)
+    private Long distributedAmount; // 已颁发给成员的金额(分)
+    
+    private List<FamilyEarningsMemberDTO> members;
+}

+ 45 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/ButlerCommissionSettlement.java

@@ -0,0 +1,45 @@
+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("butler_commission_settlements")
+public class ButlerCommissionSettlement implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long butlerId;
+
+    private String yearMonth;
+
+    private Integer memberCount;
+
+    private Long annualBaseAmountP;
+
+    private Long annualButlerAmountP;
+
+    private Long annualPlatformAmountP;
+
+    private Long perSessionTotalP;
+
+    private Long perSessionPlatformTotalP;
+
+    private Long perSessionButlerTotalP;
+
+    private Long totalButlerAmountP;
+
+    private String status;
+
+    private Date settledAt;
+
+    private Long settledBy;
+
+    private Date createdAt;
+}

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

@@ -0,0 +1,39 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("butler_service_records")
+public class ButlerServiceRecord implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long butlerId;
+
+    private Long memberUserId;
+
+    private Long subscriptionId;
+
+    private String serviceType;
+
+    private Long amountP;
+
+    private Long platformFeeP;
+
+    private Long butlerAmountP;
+
+    private String memberTier;
+
+    private String yearMonth;
+
+    private String remark;
+
+    private Date createdAt;
+}

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

@@ -44,5 +44,7 @@ public class CommissionRecord implements Serializable {
 
     private String remark;
 
+    private Long familyId;      // 归属家庭ID(家庭级推广体系)
+
     private Date createdAt;
 }

+ 31 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyEarnings.java

@@ -0,0 +1,31 @@
+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("family_earnings")
+public class FamilyEarnings implements Serializable {
+    
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    
+    private Long familyId; // 家庭ID
+    
+    private Long totalEarned = 0L; // 累计推荐收入(分)
+    
+    private Long availableAmount = 0L; // 当前可支配余额(分)
+    
+    private Long withdrawnAmount = 0L; // 已提现金额(分)
+    
+    private Long distributedAmount = 0L; // 已颁发给成员的金额(分)
+    
+    private Date updatedAt;
+    
+    private Date createdAt;
+}

+ 33 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyEarningsMember.java

@@ -0,0 +1,33 @@
+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("family_earnings_member")
+public class FamilyEarningsMember implements Serializable {
+    
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    
+    private Long familyId; // 家庭ID
+    
+    private Long userId; // 成员用户ID
+    
+    private Long allocatedAmount = 0L; // 管理员累计分配(分)
+    
+    private Long consumedAmount = 0L; // 已消费金额(分)
+    
+    private Long availableAmount = 0L; // 可消费余额 = allocated - consumed(分)
+    
+    private Boolean canUseFamilyBalance = false; // 是否允许使用家庭公共池
+    
+    private Date updatedAt;
+    
+    private Date createdAt;
+}

+ 31 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyEarningsRecord.java

@@ -0,0 +1,31 @@
+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("family_earnings_record")
+public class FamilyEarningsRecord implements Serializable {
+    
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    
+    private Long familyId; // 家庭ID
+    
+    private String type; // earn 收入/withdraw 提现/distribute 颁发/consume 消费
+    
+    private Long amount; // 金额(分)
+    
+    private Long fromUserId; // 操作人
+    
+    private Long toUserId; // 受益人(颁发/消费时填写)
+    
+    private String note; // 备注
+    
+    private Date createdAt;
+}

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

@@ -19,6 +19,7 @@ public class SupplySystem implements Serializable {
     private BigDecimal platformProfitRate;
     private String description;
     private String status;
+    private String models;
     private Date createdAt;
     private Date updatedAt;
 }

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

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

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

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

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

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

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

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

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

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

+ 226 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ButlerCommissionService.java

@@ -0,0 +1,226 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.ButlerCommissionSettlement;
+import com.etotem.cfc.entity.ButlerProfile;
+import com.etotem.cfc.entity.ButlerServiceRecord;
+import com.etotem.cfc.entity.SysConfig;
+import com.etotem.cfc.mapper.ButlerCommissionSettlementMapper;
+import com.etotem.cfc.mapper.ButlerProfileMapper;
+import com.etotem.cfc.mapper.ButlerServiceRecordMapper;
+import com.etotem.cfc.mapper.SysConfigMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.Date;
+import java.util.List;
+
+@Service
+public class ButlerCommissionService {
+
+    private static final Logger log = LoggerFactory.getLogger(ButlerCommissionService.class);
+
+    @Resource
+    private ButlerServiceRecordMapper serviceRecordMapper;
+
+    @Resource
+    private ButlerCommissionSettlementMapper settlementMapper;
+
+    @Resource
+    private ButlerProfileMapper butlerProfileMapper;
+
+    @Resource
+    private SysConfigMapper sysConfigMapper;
+
+    // 默认配置(基点为10000=100%)
+    private static final int DEFAULT_PLATFORM_RATE = 2000;       // 平台服务费率 20%
+    private static final int DEFAULT_ANNUAL_L1_RATE = 1000;      // L1年费管家分润率 10%
+    private static final int DEFAULT_ANNUAL_L2_RATE = 5000;      // L2年费管家分润率 50%
+    private static final int DEFAULT_PER_SESSION_PLATFORM_RATE = 2000; // 按次平台费率 20%
+
+    /**
+     * 创建按次服务记录
+     * @param butlerId 管家ID
+     * @param memberUserId 会员ID
+     * @param amount 服务金额(P点)
+     * @param remark 备注
+     * @return 创建的服务记录
+     */
+    @Transactional
+    public ButlerServiceRecord createServiceRecord(Long butlerId, Long memberUserId, Long amount, String remark) {
+        int platformRate = getSysConfigInt("butler_per_session_platform_rate", DEFAULT_PER_SESSION_PLATFORM_RATE);
+        long platformFee = amount * platformRate / 10000;
+        long butlerAmount = amount - platformFee;
+
+        ButlerServiceRecord record = new ButlerServiceRecord();
+        record.setButlerId(butlerId);
+        record.setMemberUserId(memberUserId);
+        record.setServiceType("per_session");
+        record.setAmountP(amount);
+        record.setPlatformFeeP(platformFee);
+        record.setButlerAmountP(butlerAmount);
+        record.setYearMonth(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")));
+        record.setRemark(remark);
+        record.setCreatedAt(new Date());
+        serviceRecordMapper.insert(record);
+
+        log.info("Created per-session service record: butlerId={}, amount={}, platformFee={}, butlerAmount={}",
+                butlerId, amount, platformFee, butlerAmount);
+        return record;
+    }
+
+    /**
+     * 计算指定管家某月的结算
+     * @param butlerId 管家ID
+     * @param yearMonth YYYY-MM格式
+     * @return 结算单ID
+     */
+    @Transactional
+    public Long calculateButlerSettlement(Long butlerId, String yearMonth) {
+        // 查询管家当月所有服务记录
+        List<ButlerServiceRecord> records = serviceRecordMapper.selectList(
+                new LambdaQueryWrapper<ButlerServiceRecord>()
+                        .eq(ButlerServiceRecord::getButlerId, butlerId)
+                        .eq(ButlerServiceRecord::getYearMonth, yearMonth)
+        );
+
+        if (records.isEmpty()) {
+            log.info("No service records for butlerId={} in {}", butlerId, yearMonth);
+            return null;
+        }
+
+        // 统计各项
+        long annualBaseAmount = 0;
+        long annualButlerAmount = 0;
+        long annualPlatformAmount = 0;
+        long perSessionTotal = 0;
+        long perSessionPlatformTotal = 0;
+        long perSessionButlerTotal = 0;
+        long totalButlerAmount = 0;
+        int memberCount = 0;
+
+        for (ButlerServiceRecord r : records) {
+            if ("annual".equals(r.getServiceType())) {
+                annualBaseAmount += r.getAmountP() != null ? r.getAmountP() : 0;
+                annualButlerAmount += r.getButlerAmountP() != null ? r.getButlerAmountP() : 0;
+                annualPlatformAmount += r.getPlatformFeeP() != null ? r.getPlatformFeeP() : 0;
+            } else if ("per_session".equals(r.getServiceType())) {
+                perSessionTotal += r.getAmountP() != null ? r.getAmountP() : 0;
+                perSessionPlatformTotal += r.getPlatformFeeP() != null ? r.getPlatformFeeP() : 0;
+                perSessionButlerTotal += r.getButlerAmountP() != null ? r.getButlerAmountP() : 0;
+            }
+        }
+        totalButlerAmount = annualButlerAmount + perSessionButlerTotal;
+
+        ButlerProfile profile = butlerProfileMapper.selectOne(
+                new LambdaQueryWrapper<ButlerProfile>().eq(ButlerProfile::getUserId, butlerId));
+        if (profile != null) {
+            memberCount = profile.getMemberCount() != null ? profile.getMemberCount() : 0;
+        }
+
+        // 创建或更新结算单
+        ButlerCommissionSettlement existing = settlementMapper.selectOne(
+                new LambdaQueryWrapper<ButlerCommissionSettlement>()
+                        .eq(ButlerCommissionSettlement::getButlerId, butlerId)
+                        .eq(ButlerCommissionSettlement::getYearMonth, yearMonth)
+        );
+
+        if (existing != null) {
+            existing.setMemberCount(memberCount);
+            existing.setAnnualBaseAmountP(annualBaseAmount);
+            existing.setAnnualButlerAmountP(annualButlerAmount);
+            existing.setAnnualPlatformAmountP(annualPlatformAmount);
+            existing.setPerSessionTotalP(perSessionTotal);
+            existing.setPerSessionPlatformTotalP(perSessionPlatformTotal);
+            existing.setPerSessionButlerTotalP(perSessionButlerTotal);
+            existing.setTotalButlerAmountP(totalButlerAmount);
+            settlementMapper.updateById(existing);
+            log.info("Updated settlement id={} for butlerId={}, month={}, totalButlerAmount={}",
+                    existing.getId(), butlerId, yearMonth, totalButlerAmount);
+            return existing.getId();
+        } else {
+            ButlerCommissionSettlement settlement = new ButlerCommissionSettlement();
+            settlement.setButlerId(butlerId);
+            settlement.setYearMonth(yearMonth);
+            settlement.setMemberCount(memberCount);
+            settlement.setAnnualBaseAmountP(annualBaseAmount);
+            settlement.setAnnualButlerAmountP(annualButlerAmount);
+            settlement.setAnnualPlatformAmountP(annualPlatformAmount);
+            settlement.setPerSessionTotalP(perSessionTotal);
+            settlement.setPerSessionPlatformTotalP(perSessionPlatformTotal);
+            settlement.setPerSessionButlerTotalP(perSessionButlerTotal);
+            settlement.setTotalButlerAmountP(totalButlerAmount);
+            settlement.setStatus("pending");
+            settlement.setCreatedAt(new Date());
+            settlementMapper.insert(settlement);
+            log.info("Created settlement id={} for butlerId={}, month={}, totalButlerAmount={}",
+                    settlement.getId(), butlerId, yearMonth, totalButlerAmount);
+            return settlement.getId();
+        }
+    }
+
+    /**
+     * 批量计算所有管家的月度结算
+     * @param yearMonth YYYY-MM格式,null表示上月
+     * @return 处理的管家数
+     */
+    @Transactional
+    public int batchCalculateMonthlySettlement(String yearMonth) {
+        if (yearMonth == null) {
+            LocalDate lastMonth = LocalDate.now().minusMonths(1);
+            yearMonth = lastMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"));
+        }
+
+        // 查询所有审核通过的管家
+        List<ButlerProfile> butlers = butlerProfileMapper.selectList(
+                new LambdaQueryWrapper<ButlerProfile>()
+                        .eq(ButlerProfile::getStatus, "approved")
+        );
+
+        int count = 0;
+        for (ButlerProfile butler : butlers) {
+            Long settlementId = calculateButlerSettlement(butler.getUserId(), yearMonth);
+            if (settlementId != null) {
+                count++;
+            }
+        }
+
+        log.info("Batch settlement for {}: processed {} butlers", yearMonth, count);
+        return count;
+    }
+
+    /**
+     * 标记结算单为已发放
+     */
+    @Transactional
+    public void markSettlementPaid(Long settlementId, Long settledBy) {
+        ButlerCommissionSettlement settlement = settlementMapper.selectById(settlementId);
+        if (settlement == null) {
+            log.warn("Settlement not found: id={}", settlementId);
+            return;
+        }
+        settlement.setStatus("paid");
+        settlement.setSettledAt(new Date());
+        settlement.setSettledBy(settledBy);
+        settlementMapper.updateById(settlement);
+        log.info("Marked settlement id={} as paid", settlementId);
+    }
+
+    private int getSysConfigInt(String key, int defaultValue) {
+        SysConfig config = sysConfigMapper.selectOne(
+                new LambdaQueryWrapper<SysConfig>().eq(SysConfig::getConfigKey, key));
+        if (config != null && config.getConfigValue() != null) {
+            try {
+                return Integer.parseInt(config.getConfigValue());
+            } catch (NumberFormatException e) {
+                log.warn("Invalid sys_config value for {}: {}", key, config.getConfigValue());
+            }
+        }
+        return defaultValue;
+    }
+}

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

@@ -40,6 +40,9 @@ public class CommissionService {
 
     @Resource
     private ProductMapper productMapper;
+    
+    @Resource
+    private FamilyEarningsService familyEarningsService;
 
     @Resource
     private CommissionRecordMapper commissionRecordMapper;
@@ -278,14 +281,23 @@ public class CommissionService {
                 }
             }
 
-            // 创建L1佣金记录(金额>0才创建)
-            if (l1Amount > 0) {
-                createCommissionRecord(orderId, orderType, l1ReferrerId, buyerId,
-                        commissionType, orderAmount, 0, l1Amount,
-                        1, "settled", new Date());
+        // 创建L1佣金记录(金额>0才创建)
+        if (l1Amount > 0) {
+            Long familyId = l1Referrer.getFamilyId();
+            createCommissionRecord(orderId, orderType, l1ReferrerId, buyerId, 
+                commissionType, orderAmount, 0, l1Amount,
+                1, "settled", new Date(), familyId);
+            
+            if (familyId != null) {
+                try {
+                    familyEarningsService.earn(familyId, (long) l1Amount, l1ReferrerId);
+                } catch (Exception e) {
+                    log.error("更新家庭公共账户失败: familyId={}, amount={}", familyId, l1Amount, e);
+                }
             }
+        }
 
-            // 更新推荐人的累计佣金
+        // 更新推荐人的累计佣金
             l1Referrer.setTotalCommissionEarned(
                     (l1Referrer.getTotalCommissionEarned() != null ? l1Referrer.getTotalCommissionEarned() : 0L)
                             + l1Amount);
@@ -305,7 +317,7 @@ public class CommissionService {
                     if (l2Amount > 0) {
                         createCommissionRecord(orderId, orderType, l2ReferrerId, buyerId,
                                 "member", orderAmount, 0, l2Amount,
-                                2, "settled", new Date());
+                                2, "settled", new Date(), l2Referrer.getFamilyId());
 
                         l2Referrer.setTotalCommissionEarned(
                                 (l2Referrer.getTotalCommissionEarned() != null ? l2Referrer.getTotalCommissionEarned() : 0L)
@@ -334,7 +346,7 @@ public class CommissionService {
     private void createCommissionRecord(Long orderId, String orderType, Long referrerId,
                                         Long buyerId, String commissionType, Integer orderAmount,
                                         Integer profitRate, Integer commissionAmount,
-                                        Integer level, String status, Date settledAt) {
+                                        Integer level, String status, Date settledAt, Long familyId) {
         CommissionRecord record = new CommissionRecord();
         record.setOrderId(orderId);
         record.setOrderType(orderType);
@@ -348,6 +360,9 @@ public class CommissionService {
         record.setStatus(status);
         record.setSettledAt(settledAt);
         record.setCreatedAt(new Date());
+        if (familyId != null) {
+            record.setFamilyId(familyId);
+        }
         commissionRecordMapper.insert(record);
     }
 

+ 312 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyEarningsService.java

@@ -0,0 +1,312 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.dto.FamilyEarningsMemberDTO;
+import com.etotem.cfc.dto.FamilyEarningsSummaryDTO;
+import com.etotem.cfc.entity.*;
+import com.etotem.cfc.mapper.*;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+public class FamilyEarningsService {
+    
+    @Resource
+    private FamilyEarningsMapper familyEarningsMapper;
+    
+    @Resource
+    private FamilyEarningsMemberMapper familyEarningsMemberMapper;
+    
+    @Resource
+    private FamilyEarningsRecordMapper familyEarningsRecordMapper;
+    
+    @Resource
+    private UserMapper userMapper;
+    
+    @Resource
+    private FamilyMapper familyMapper;
+
+    /**
+     * 获取家庭收益总览
+     */
+    public FamilyEarningsSummaryDTO getFamilySummary(Long familyId) {
+        FamilyEarningsSummaryDTO summary = new FamilyEarningsSummaryDTO();
+        
+        // 获取家庭总账
+        FamilyEarnings earnings = familyEarningsMapper.selectOne(
+            new LambdaQueryWrapper<FamilyEarnings>().eq(FamilyEarnings::getFamilyId, familyId)
+        );
+        
+        if (earnings != null) {
+            summary.setFamilyId(earnings.getFamilyId());
+            summary.setTotalEarned(earnings.getTotalEarned());
+            summary.setAvailableAmount(earnings.getAvailableAmount());
+            summary.setWithdrawnAmount(earnings.getWithdrawnAmount());
+            summary.setDistributedAmount(earnings.getDistributedAmount());
+        }
+        
+        // 获取成员列表
+        List<FamilyEarningsMemberDTO> members = getMemberContributions(familyId);
+        summary.setMembers(members);
+        
+        return summary;
+    }
+    
+    /**
+     * 获取家庭成员贡献列表
+     */
+    public List<FamilyEarningsMemberDTO> getMemberContributions(Long familyId) {
+        // 获取家庭成员
+        List<User> familyUsers = userMapper.selectList(
+            new LambdaQueryWrapper<User>().eq(User::getFamilyId, familyId)
+        );
+        
+        return familyUsers.stream().map(user -> {
+            FamilyEarningsMemberDTO dto = new FamilyEarningsMemberDTO();
+            dto.setUserId(user.getId());
+            dto.setNickname(user.getNickname());
+            dto.setAvatar(user.getAvatar());
+            dto.setIsAdmin(user.getIsFamilyAdmin() != null && user.getIsFamilyAdmin());
+            
+            // 获取成员收益信息
+            FamilyEarningsMember member = familyEarningsMemberMapper.selectOne(
+                new LambdaQueryWrapper<FamilyEarningsMember>()
+                    .eq(FamilyEarningsMember::getFamilyId, familyId)
+                    .eq(FamilyEarningsMember::getUserId, user.getId())
+            );
+            
+            if (member != null) {
+                dto.setAllocatedAmount(member.getAllocatedAmount());
+                dto.setConsumedAmount(member.getConsumedAmount());
+                dto.setAvailableAmount(member.getAvailableAmount());
+                dto.setCanUseFamilyBalance(member.getCanUseFamilyBalance());
+            } else {
+                // 初始化默认值
+                dto.setAllocatedAmount(0L);
+                dto.setConsumedAmount(0L);
+                dto.setAvailableAmount(0L);
+                dto.setCanUseFamilyBalance(false);
+            }
+            
+            return dto;
+        }).collect(Collectors.toList());
+    }
+    
+    /**
+     * 收入(佣金结算时调用)
+     */
+    @Transactional
+    public void earn(Long familyId, Long amount, Long fromUserId) {
+        if (familyId == null || amount == null || amount <= 0) {
+            return;
+        }
+        
+        // 获取或创建家庭收益记录
+        FamilyEarnings earnings = familyEarningsMapper.selectOne(
+            new LambdaQueryWrapper<FamilyEarnings>().eq(FamilyEarnings::getFamilyId, familyId)
+        );
+        
+        if (earnings == null) {
+            earnings = new FamilyEarnings();
+            earnings.setFamilyId(familyId);
+            earnings.setTotalEarned(amount);
+            earnings.setAvailableAmount(amount);
+            earnings.setWithdrawnAmount(0L);
+            earnings.setDistributedAmount(0L);
+            earnings.setCreatedAt(new Date());
+            earnings.setUpdatedAt(new Date());
+            familyEarningsMapper.insert(earnings);
+        } else {
+            earnings.setTotalEarned(earnings.getTotalEarned() + amount);
+            earnings.setAvailableAmount(earnings.getAvailableAmount() + amount);
+            earnings.setUpdatedAt(new Date());
+            familyEarningsMapper.updateById(earnings);
+        }
+        
+        // 记录流水
+        FamilyEarningsRecord record = new FamilyEarningsRecord();
+        record.setFamilyId(familyId);
+        record.setType("earn");
+        record.setAmount(amount);
+        record.setFromUserId(fromUserId);
+        record.setCreatedAt(new Date());
+        familyEarningsRecordMapper.insert(record);
+    }
+    
+    /**
+     * 提现(管理员发起)
+     */
+    @Transactional
+    public void withdraw(Long familyId, Long amount, Long adminUserId) {
+        if (familyId == null || amount == null || amount <= 0) {
+            throw new RuntimeException("提现金额必须大于0");
+        }
+        
+        FamilyEarnings earnings = familyEarningsMapper.selectOne(
+            new LambdaQueryWrapper<FamilyEarnings>().eq(FamilyEarnings::getFamilyId, familyId)
+        );
+        
+        if (earnings == null || earnings.getAvailableAmount() < amount) {
+            throw new RuntimeException("可用余额不足");
+        }
+        
+        // 更新家庭收益
+        earnings.setAvailableAmount(earnings.getAvailableAmount() - amount);
+        earnings.setWithdrawnAmount(earnings.getWithdrawnAmount() + amount);
+        earnings.setUpdatedAt(new Date());
+        familyEarningsMapper.updateById(earnings);
+        
+        // 记录流水
+        FamilyEarningsRecord record = new FamilyEarningsRecord();
+        record.setFamilyId(familyId);
+        record.setType("withdraw");
+        record.setAmount(amount);
+        record.setFromUserId(adminUserId);
+        record.setCreatedAt(new Date());
+        familyEarningsRecordMapper.insert(record);
+    }
+    
+    /**
+     * 颁发给成员(管理员分配)
+     */
+    @Transactional
+    public void distribute(Long familyId, Long toUserId, Long amount, Long adminUserId) {
+        if (familyId == null || toUserId == null || amount == null || amount <= 0) {
+            throw new RuntimeException("分配金额必须大于0");
+        }
+        
+        FamilyEarnings earnings = familyEarningsMapper.selectOne(
+            new LambdaQueryWrapper<FamilyEarnings>().eq(FamilyEarnings::getFamilyId, familyId)
+        );
+        
+        if (earnings == null || earnings.getAvailableAmount() < amount) {
+            throw new RuntimeException("可用余额不足");
+        }
+        
+        // 更新家庭收益
+        earnings.setAvailableAmount(earnings.getAvailableAmount() - amount);
+        earnings.setDistributedAmount(earnings.getDistributedAmount() + amount);
+        earnings.setUpdatedAt(new Date());
+        familyEarningsMapper.updateById(earnings);
+        
+        // 获取或创建成员收益记录
+        FamilyEarningsMember member = familyEarningsMemberMapper.selectOne(
+            new LambdaQueryWrapper<FamilyEarningsMember>()
+                .eq(FamilyEarningsMember::getFamilyId, familyId)
+                .eq(FamilyEarningsMember::getUserId, toUserId)
+        );
+        
+        if (member == null) {
+            member = new FamilyEarningsMember();
+            member.setFamilyId(familyId);
+            member.setUserId(toUserId);
+            member.setAllocatedAmount(amount);
+            member.setConsumedAmount(0L);
+            member.setAvailableAmount(amount);
+            member.setCanUseFamilyBalance(false); // 默认不允许使用家庭公共池
+            member.setCreatedAt(new Date());
+            member.setUpdatedAt(new Date());
+            familyEarningsMemberMapper.insert(member);
+        } else {
+            member.setAllocatedAmount(member.getAllocatedAmount() + amount);
+            member.setAvailableAmount(member.getAvailableAmount() + amount);
+            member.setUpdatedAt(new Date());
+            familyEarningsMemberMapper.updateById(member);
+        }
+        
+        // 记录流水
+        FamilyEarningsRecord record = new FamilyEarningsRecord();
+        record.setFamilyId(familyId);
+        record.setType("distribute");
+        record.setAmount(amount);
+        record.setFromUserId(adminUserId);
+        record.setToUserId(toUserId);
+        record.setCreatedAt(new Date());
+        familyEarningsRecordMapper.insert(record);
+    }
+    
+    /**
+     * 消费扣款(支付时调用)
+     */
+    @Transactional
+    public void consume(Long familyId, Long userId, Long amount) {
+        if (familyId == null || userId == null || amount == null || amount <= 0) {
+            throw new RuntimeException("消费金额必须大于0");
+        }
+        
+        // 检查成员是否有足够的可用余额
+        FamilyEarningsMember member = familyEarningsMemberMapper.selectOne(
+            new LambdaQueryWrapper<FamilyEarningsMember>()
+                .eq(FamilyEarningsMember::getFamilyId, familyId)
+                .eq(FamilyEarningsMember::getUserId, userId)
+        );
+        
+        if (member == null || member.getAvailableAmount() < amount) {
+            throw new RuntimeException("可用余额不足");
+        }
+        
+        // 更新成员收益
+        member.setConsumedAmount(member.getConsumedAmount() + amount);
+        member.setAvailableAmount(member.getAvailableAmount() - amount);
+        member.setUpdatedAt(new Date());
+        familyEarningsMemberMapper.updateById(member);
+        
+        // 记录流水
+        FamilyEarningsRecord record = new FamilyEarningsRecord();
+        record.setFamilyId(familyId);
+        record.setType("consume");
+        record.setAmount(amount);
+        record.setToUserId(userId);
+        record.setCreatedAt(new Date());
+        familyEarningsRecordMapper.insert(record);
+    }
+    
+    /**
+     * 流水查询
+     */
+    public List<FamilyEarningsRecord> getRecords(Long familyId, String type) {
+        LambdaQueryWrapper<FamilyEarningsRecord> wrapper = new LambdaQueryWrapper<FamilyEarningsRecord>()
+            .eq(FamilyEarningsRecord::getFamilyId, familyId);
+        
+        if (type != null && !type.isEmpty()) {
+            wrapper.eq(FamilyEarningsRecord::getType, type);
+        }
+        
+        wrapper.orderByDesc(FamilyEarningsRecord::getCreatedAt);
+        return familyEarningsRecordMapper.selectList(wrapper);
+    }
+    
+    /**
+     * 切换成员是否允许使用家庭公共池
+     */
+    @Transactional
+    public void toggleMemberAccess(Long familyId, Long userId, Boolean enabled) {
+        FamilyEarningsMember member = familyEarningsMemberMapper.selectOne(
+            new LambdaQueryWrapper<FamilyEarningsMember>()
+                .eq(FamilyEarningsMember::getFamilyId, familyId)
+                .eq(FamilyEarningsMember::getUserId, userId)
+        );
+        
+        if (member == null) {
+            member = new FamilyEarningsMember();
+            member.setFamilyId(familyId);
+            member.setUserId(userId);
+            member.setAllocatedAmount(0L);
+            member.setConsumedAmount(0L);
+            member.setAvailableAmount(0L);
+            member.setCanUseFamilyBalance(enabled);
+            member.setCreatedAt(new Date());
+            member.setUpdatedAt(new Date());
+            familyEarningsMemberMapper.insert(member);
+        } else {
+            member.setCanUseFamilyBalance(enabled);
+            member.setUpdatedAt(new Date());
+            familyEarningsMemberMapper.updateById(member);
+        }
+    }
+}

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

@@ -159,9 +159,11 @@ public class PaymentService implements PaymentServiceInterface {
 
     @Override
     public Boolean processPayment(String orderNo, String payType) {
-        // TODO: In production, this would integrate with WeChat Pay / Alipay
-        // For test stub, always return true (payment always succeeds)
-        return true;
+        if (testMode) {
+            log.info("【测试模式】处理支付:orderNo={}, payType={}", orderNo, payType);
+            return true;
+        }
+        throw new UnsupportedOperationException("Payment not configured for production");
     }
 
     public Map<String, Object> createWechatPrepay(String orderNo, String description, Integer totalFee, String openid) {

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

@@ -8,6 +8,7 @@ import com.etotem.cfc.service.api.TeacherServiceInterface;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
+import java.util.Collections;
 import java.util.Date;
 import java.util.List;
 import java.util.stream.Collectors;
@@ -43,9 +44,9 @@ public class TeacherService {
         Teacher teacher = teacherMapper.selectOne(new LambdaQueryWrapper<Teacher>()
                 .eq(Teacher::getUserId, userId));
         
-        if (teacher == null) {
-            return null;
-        }
+ if (teacher == null) {
+ throw new RuntimeException("教师信息不存在");
+ }
         
         return toDTO(teacher);
     }
@@ -221,9 +222,9 @@ public class TeacherService {
      */
     public List<ChildInfoDTO> getAuthorizedChildren(Long teacherId) {
         // 这里需要查询家长授权表,获取授权的孩子
-        // 简化实现:返回所有家庭的孩子
-        return null;
-    }
+ // 简化实现:返回所有家庭的孩子
+ return Collections.emptyList();
+}
 
     private String generateTeacherNo() {
         return "T" + System.currentTimeMillis();

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

@@ -16,7 +16,7 @@ public class VendorService {
     @Resource
     private UserMapper userMapper;
 
-    public Result apply(Long userId, String vendorType, String vendorInfo) {
+    public Result<String> apply(Long userId, String vendorType, String vendorInfo) {
         User user = userMapper.selectById(userId);
         if (user == null) {
             return Result.error("用户不存在");
@@ -43,7 +43,7 @@ public class VendorService {
         return Result.success("入驻申请已提交,请等待审核");
     }
 
-    public Result getStatus(Long userId) {
+    public Result<Map<String, Object>> getStatus(Long userId) {
         User user = userMapper.selectById(userId);
         if (user == null) {
             return Result.error("用户不存在");
@@ -55,7 +55,7 @@ public class VendorService {
         return Result.success(data);
     }
 
-    public Result getVendorInfo(Long userId) {
+    public Result<Map<String, Object>> getVendorInfo(Long userId) {
         User user = userMapper.selectById(userId);
         if (user == null) {
             return Result.error("用户不存在");

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

@@ -48,9 +48,12 @@ public class VerificationCodeService implements VerificationCodeServiceInterface
         verificationCode.setUsed(0);
         verificationCodeMapper.insert(verificationCode);
 
-        // TODO: 调用短信平台发送验证码
-        log.info("生成验证码: phone={}, code={}, type={}", phone, code, type);
-        
+        if (testMode) {
+            log.info("Test mode: code = {}", code);
+        } else {
+            throw new UnsupportedOperationException("SMS provider not configured - cannot send SMS to " + phone);
+        }
+
         return code;
     }
 

+ 34 - 0
cfc-backend/src/main/java/com/etotem/cfc/task/ButlerCommissionScheduledTask.java

@@ -0,0 +1,34 @@
+package com.etotem.cfc.task;
+
+import com.etotem.cfc.service.ButlerCommissionService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+
+/**
+ * 管家佣金结算定时任务
+ * 每月1日凌晨3:00自动结算上月管家佣金
+ */
+@Slf4j
+@Component
+public class ButlerCommissionScheduledTask {
+
+    @Resource
+    private ButlerCommissionService butlerCommissionService;
+
+    /**
+     * 每月1日 3:00 AM 执行
+     */
+    @Scheduled(cron = "0 0 3 1 * ?")
+    public void settleMonthlyCommissions() {
+        log.info("开始执行管家月度佣金结算定时任务");
+        try {
+            int count = butlerCommissionService.batchCalculateMonthlySettlement(null);
+            log.info("管家月度佣金结算完成,共处理{}个管家", count);
+        } catch (Exception e) {
+            log.error("管家月度佣金结算定时任务执行失败", e);
+        }
+    }
+}

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

@@ -1160,8 +1160,12 @@ CREATE TABLE IF NOT EXISTS articles (
     author VARCHAR(100) DEFAULT '浠艾福' COMMENT '作者',
     read_time INT DEFAULT 0 COMMENT '预计阅读分钟数',
     article_type VARCHAR(20) DEFAULT 'normal' COMMENT '文章类型:normal(普通)/premium(优质)/original(原创)',
+    content_type VARCHAR(20) DEFAULT 'text' COMMENT '内容格式:text/html/markdown',
+    difficulty_level INT DEFAULT 0 COMMENT '难度等级:1-5',
     word_count INT DEFAULT 0 COMMENT '文章字数',
+    dimension_ids VARCHAR(100) DEFAULT '' COMMENT '关联维度IDs JSON',
     related_dimensions VARCHAR(100) DEFAULT '' COMMENT '关联五维 JSON 数组',
+    dimension_weights TEXT COMMENT '五维能量权重 JSON',
     visibility VARCHAR(20) DEFAULT 'public' COMMENT '浏览权限:public/login/private',
     visible_to TEXT COMMENT '私密指定人群 JSON',
     status VARCHAR(20) DEFAULT 'draft' COMMENT '状态:draft/published/archived',
@@ -1258,10 +1262,12 @@ CREATE TABLE IF NOT EXISTS commission_records (
     status VARCHAR(16) NOT NULL DEFAULT 'settled' COMMENT '状态: pending/settled',
     remark TEXT DEFAULT NULL COMMENT '备注',
     created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    family_id BIGINT DEFAULT NULL COMMENT '归属家庭ID',
     settled_at DATETIME DEFAULT NULL COMMENT '结算时间',
     withdrawn_at DATETIME DEFAULT NULL COMMENT '提现时间',
     INDEX idx_referrer (referrer_id),
     INDEX idx_buyer (buyer_id),
+    INDEX idx_family (family_id),
     INDEX idx_order (order_id, order_type)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='佣金记录表';
 
@@ -1694,6 +1700,7 @@ CREATE TABLE IF NOT EXISTS activities (
     price INT DEFAULT 0 COMMENT '活动价格(分)',
     vendor_id        BIGINT COMMENT '服务商ID',
     vendor_name      VARCHAR(100) COMMENT '服务商名称',
+    dimension_weights TEXT COMMENT '五维能量权重JSON',
     created_at       DATETIME DEFAULT CURRENT_TIMESTAMP,
     updated_at       DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     INDEX idx_dimension (dimension_code),
@@ -2236,6 +2243,7 @@ CREATE TABLE IF NOT EXISTS supply_system (
     settlement_period_days INT NOT NULL DEFAULT 30 COMMENT '账期(天)',
     platform_profit_rate DECIMAL(5,2) NOT NULL DEFAULT 0 COMMENT '平台留利比例(%)',
     description TEXT COMMENT '描述',
+    models TEXT COMMENT 'AI模型列表JSON,如["agnes-image-2.0-flash","agnes-2.5-flash"]',
     status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态: active/disabled',
     created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
     updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
@@ -2886,3 +2894,45 @@ CREATE TABLE IF NOT EXISTS butler_commission_settlements (
     INDEX idx_status (status),
     INDEX idx_year_month (year_month)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='管家月度佣金结算单';
+
+-- 家庭公共账户体系
+
+CREATE TABLE family_earnings (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    family_id BIGINT NOT NULL UNIQUE COMMENT '家庭ID',
+    total_earned BIGINT NOT NULL DEFAULT 0 COMMENT '累计推荐收入(分)',
+    available_amount BIGINT NOT NULL DEFAULT 0 COMMENT '当前可支配余额(分)',
+    withdrawn_amount BIGINT NOT NULL DEFAULT 0 COMMENT '已提现金额(分)',
+    distributed_amount BIGINT NOT NULL DEFAULT 0 COMMENT '已颁发给成员的金额(分)',
+    updated_at DATETIME,
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    INDEX idx_family_id (family_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭公共账户';
+
+CREATE TABLE family_earnings_member (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    family_id BIGINT NOT NULL,
+    user_id BIGINT NOT NULL,
+    allocated_amount BIGINT NOT NULL DEFAULT 0 COMMENT '管理员累计分配(分)',
+    consumed_amount BIGINT NOT NULL DEFAULT 0 COMMENT '已消费金额(分)',
+    available_amount BIGINT NOT NULL DEFAULT 0 COMMENT '可消费余额=allocated-consumed(分)',
+    can_use_family_balance TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否允许使用家庭公共池',
+    updated_at DATETIME,
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    UNIQUE KEY idx_family_user (family_id, user_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭成员已分配余额';
+
+CREATE TABLE family_earnings_record (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    family_id BIGINT NOT NULL,
+    type VARCHAR(20) NOT NULL COMMENT '类型: earn收入/withdraw提现/distribute颁发/consume消费',
+    amount BIGINT NOT NULL COMMENT '金额(分)',
+    from_user_id BIGINT COMMENT '操作人',
+    to_user_id BIGINT COMMENT '受益人(颁发/消费时填写)',
+    note VARCHAR(255) COMMENT '备注',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    INDEX idx_family_id (family_id),
+    INDEX idx_type (type)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭收益收支流水';
+
+ALTER TABLE commission_records ADD COLUMN family_id BIGINT COMMENT '归属家庭ID';

+ 258 - 0
cfc-frontend/components/member-detail-sections/action-section.vue

@@ -0,0 +1,258 @@
+<template>
+<view class="action-section">
+  <!-- 今日任务 -->
+  <view class="section-card" v-if="todayTasks.length > 0">
+    <view class="section-header">
+      <text class="section-title">✅ 今日任务</text>
+    </view>
+    <view class="task-item" v-for="task in todayTasks" :key="task.id" @click="goTaskDetail(task)">
+      <view class="task-status" :class="task.status === 'completed' ? 'task-done' : 'task-pending'"></view>
+      <text class="task-name">{{ task.title || task.name }}</text>
+      <text class="task-points">{{ task.points || task.coin || 0 }}分</text>
+    </view>
+  </view>
+  <view class="section-card" v-else-if="!loading && childId">
+    <view class="empty-state">
+      <text class="empty-text">今日暂无任务</text>
+      <text class="empty-sub">去「行」维度页布置新任务吧</text>
+    </view>
+  </view>
+
+  <!-- 打卡统计 -->
+  <view class="section-card" v-if="checkinStats">
+    <view class="section-header">
+      <text class="section-title">📊 打卡统计</text>
+    </view>
+    <view class="stats-row">
+      <view class="stat-item">
+        <text class="stat-value">{{ checkinStats.totalDays || 0 }}</text>
+        <text class="stat-label">累计天数</text>
+      </view>
+      <view class="stat-item">
+        <text class="stat-value">{{ checkinStats.currentStreak || 0 }}</text>
+        <text class="stat-label">连续打卡</text>
+      </view>
+      <view class="stat-item">
+        <text class="stat-value">{{ checkinStats.thisMonth || 0 }}</text>
+        <text class="stat-label">本月次数</text>
+      </view>
+    </view>
+  </view>
+
+  <!-- 近期活动 -->
+  <view class="section-card" v-if="activityList.length > 0">
+    <view class="section-header">
+      <text class="section-title">🎪 近期活动</text>
+    </view>
+    <view class="activity-item" v-for="act in activityList" :key="act.id" @click="goActivityDetail(act)">
+      <text class="activity-name">{{ act.title || act.name }}</text>
+      <text class="activity-date">{{ formatDate(act.startTime || act.activityDate) }}</text>
+    </view>
+  </view>
+
+  <!-- 非孩子成员提示 -->
+  <view class="section-card" v-if="!childId && !loading">
+    <view class="empty-state">
+      <text class="empty-text">该成员暂无行动数据</text>
+    </view>
+  </view>
+
+  <!-- 加载中 -->
+  <view class="loading" v-if="loading">加载中...</view>
+</view>
+</template>
+
+<script>
+import { getTodayTasksByCategory, getCheckinStats, getActivityList } from '../../utils/api.js'
+
+export default {
+  props: {
+    memberId: {
+      type: [Number, String],
+      default: null
+    },
+    childId: {
+      type: [Number, String],
+      default: null
+    },
+    memberType: {
+      type: String,
+      default: ''
+    },
+    memberInfo: {
+      type: Object,
+      default: null
+    },
+    sandboxData: {
+      type: Object,
+      default: null
+    },
+    energyMap: {
+      type: Object,
+      default: null
+    }
+  },
+  data: function() {
+    return {
+      loading: true,
+      todayTasks: [],
+      checkinStats: null,
+      activityList: []
+    }
+  },
+  mounted: function() {
+    if (this.childId) {
+      this.loadData()
+    } else {
+      this.loading = false
+    }
+  },
+  methods: {
+    loadData: function() {
+      var self = this
+      getTodayTasksByCategory(this.childId, 'action').then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.todayTasks = Array.isArray(res.data) ? res.data : (res.data.records || [])
+        }
+      }).catch(function(e) {
+        console.log('加载今日任务失败', e)
+      })
+      
+      getCheckinStats({ childId: this.childId }).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.checkinStats = res.data
+        }
+      }).catch(function(e) {
+        console.log('加载打卡统计失败', e)
+      })
+      
+      getActivityList({ page: 1, size: 3 }).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.activityList = Array.isArray(res.data) ? res.data : (res.data.records || [])
+        }
+      }).catch(function(e) {
+        console.log('加载活动列表失败', e)
+      })
+      
+      this.loading = false
+    },
+    formatDate: function(dateStr) {
+      if (!dateStr) return ''
+      var d = new Date(dateStr)
+      return d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate()
+    },
+    goTaskDetail: function(task) {
+      uni.navigateTo({
+        url: '/pages/action/task-detail?id=' + task.id
+      })
+    },
+    goActivityDetail: function(act) {
+      uni.navigateTo({
+        url: '/pages/action/activity-detail?id=' + act.id
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.action-section {}
+.section-card {
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 24rpx;
+  margin-bottom: 20rpx;
+  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
+}
+.section-header {
+  margin-bottom: 16rpx;
+}
+.section-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+}
+.task-item {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 14rpx 0;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+.task-status {
+  width: 16rpx;
+  height: 16rpx;
+  border-radius: 50%;
+  margin-right: 16rpx;
+}
+.task-done {
+  background: #10B981;
+}
+.task-pending {
+  background: #F59E0B;
+}
+.task-name {
+  flex: 1;
+  font-size: 28rpx;
+  color: #333;
+}
+.task-points {
+  font-size: 24rpx;
+  color: #10B981;
+}
+.stats-row {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-around;
+  padding: 16rpx 0;
+}
+.stat-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.stat-value {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #10B981;
+}
+.stat-label {
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 4rpx;
+}
+.activity-item {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  padding: 12rpx 0;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+.activity-name {
+  font-size: 26rpx;
+  color: #333;
+  flex: 1;
+}
+.activity-date {
+  font-size: 22rpx;
+  color: #999;
+}
+.empty-state {
+  padding: 30rpx 0;
+  text-align: center;
+}
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+}
+.empty-sub {
+  font-size: 24rpx;
+  color: #ccc;
+  margin-top: 8rpx;
+}
+.loading {
+  text-align: center;
+  padding: 40rpx;
+  color: #999;
+}
+</style>

+ 199 - 0
cfc-frontend/components/member-detail-sections/body-section.vue

@@ -0,0 +1,199 @@
+<template>
+  <view class="body-section">
+    <!-- 最新健康报告 -->
+    <view class="section-card" v-if="healthReport">
+      <view class="section-header">
+        <text class="section-title">📋 最新健康报告</text>
+      </view>
+      <view class="report-card" @click="goReportDetail">
+        <text class="report-score">{{ healthReport.overallScore || '--' }}</text>
+        <text class="report-label">综合健康分</text>
+      </view>
+      <view class="report-meta">
+        <text class="report-date">报告日期:{{ formatDate(healthReport.reportDate) }}</text>
+      </view>
+    </view>
+    <view class="section-card" v-else-if="!loading && childId">
+      <view class="empty-state">
+        <text class="empty-text">暂无健康报告</text>
+        <text class="empty-sub">规划师录入后将自动显示</text>
+      </view>
+    </view>
+
+    <!-- 七维健康图 -->
+    <view class="section-card" v-if="dimensionOverview && dimensionOverview.dimensions">
+      <view class="section-header">
+        <text class="section-title">📊 七维健康图</text>
+      </view>
+      <RadarChart
+        :dimensions="dimensionOverview.dimensions.map(function(d) { return { label: d.label, key: d.dimension } })"
+        :scores="dimensionOverview.dimensions.map(function(d) { return d.score })"
+        fillColor="#FF8C42"
+        gridColor="#FED7AA"
+        labelColor="#9A3412"
+        :width="580"
+        :height="400" />
+    </view>
+
+    <!-- 最近打卡 -->
+    <view class="section-card" v-if="checkinList.length > 0">
+      <view class="section-header">
+        <text class="section-title">🏃 最近打卡</text>
+      </view>
+      <view class="checkin-item" v-for="item in checkinList" :key="item.id">
+        <text class="checkin-type">{{ item.typeName || item.type }}</text>
+        <text class="checkin-time">{{ formatDate(item.createTime) }}</text>
+      </view>
+    </view>
+
+    <!-- 非孩子成员提示 -->
+    <view class="section-card" v-if="!childId && !loading">
+      <view class="empty-state">
+        <text class="empty-text">该成员暂无健康数据</text>
+      </view>
+    </view>
+
+    <!-- 加载中 -->
+    <view class="loading" v-if="loading">加载中...</view>
+  </view>
+</template>
+
+<script>
+import RadarChart from '../RadarChart.vue'
+import { getHealthAnalysis, getDimensionOverview, getCheckinList } from '../../utils/api.js'
+
+export default {
+  components: { RadarChart },
+  props: {
+    memberId: { type: [Number, String], default: null },
+    childId: { type: [Number, String], default: null },
+    memberType: { type: String, default: '' },
+    memberInfo: { type: Object, default: null },
+    sandboxData: { type: Object, default: null },
+    energyMap: { type: Object, default: null }
+  },
+  data: function() {
+    return {
+      loading: true,
+      healthReport: null,
+      dimensionOverview: null,
+      checkinList: []
+    }
+  },
+  mounted: function() {
+    if (this.childId) {
+      this.loadData()
+    } else {
+      this.loading = false
+    }
+  },
+  methods: {
+    loadData: function() {
+      var self = this
+      getHealthAnalysis(this.childId).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.healthReport = res.data
+        }
+      }).catch(function(e) { console.log('加载健康分析失败', e) })
+
+      getDimensionOverview({ childId: this.childId }).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.dimensionOverview = res.data
+        }
+      }).catch(function(e) { console.log('加载维度概览失败', e) })
+
+      getCheckinList({ childId: this.childId, page: 1, size: 3 }).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.checkinList = Array.isArray(res.data) ? res.data : (res.data.records || [])
+        }
+      }).catch(function(e) { console.log('加载打卡记录失败', e) })
+
+      this.loading = false
+    },
+    formatDate: function(dateStr) {
+      if (!dateStr) return ''
+      var d = new Date(dateStr)
+      return d.getFullYear() + '-' + (d.getMonth()+1) + '-' + d.getDate()
+    },
+    goReportDetail: function() {
+      uni.navigateTo({ url: '/pages/body-detail/health-report?childId=' + this.childId })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.body-section {}
+.section-card {
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 24rpx;
+  margin-bottom: 20rpx;
+  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
+}
+.section-header {
+  margin-bottom: 16rpx;
+}
+.section-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+}
+.report-card {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 20rpx 0;
+}
+.report-score {
+  font-size: 72rpx;
+  font-weight: bold;
+  color: #FF8C42;
+}
+.report-label {
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 8rpx;
+}
+.report-meta {
+  padding-top: 12rpx;
+  border-top: 1rpx solid #f0f0f0;
+}
+.report-date {
+  font-size: 24rpx;
+  color: #999;
+}
+.checkin-item {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  padding: 16rpx 0;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+.checkin-type {
+  font-size: 28rpx;
+  color: #333;
+}
+.checkin-time {
+  font-size: 24rpx;
+  color: #999;
+}
+.empty-state {
+  padding: 30rpx 0;
+  text-align: center;
+}
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+}
+.empty-sub {
+  font-size: 24rpx;
+  color: #ccc;
+  margin-top: 8rpx;
+}
+.loading {
+  text-align: center;
+  padding: 40rpx;
+  color: #999;
+}
+</style>

+ 219 - 0
cfc-frontend/components/member-detail-sections/mind-section.vue

@@ -0,0 +1,219 @@
+<template>
+  <view class="mind-section">
+    <!-- 亲密关系评分 -->
+    <view class="section-card" v-if="relationScores && relationScores.scores">
+      <view class="section-header">
+        <text class="section-title">💕 亲密关系评分</text>
+      </view>
+      <view class="score-item" v-for="(score, key) in relationScores.scores" :key="key">
+        <text class="score-label">{{ score.label || key }}</text>
+        <view class="score-bar-bg">
+          <view class="score-bar-fill" :style="'width:' + (score.value || 0) + '%'"></view>
+        </view>
+        <text class="score-value">{{ score.value || 0 }}</text>
+      </view>
+    </view>
+
+    <!-- 联系人 -->
+    <view class="section-card" v-if="contactList.length > 0">
+      <view class="section-header">
+        <text class="section-title">📞 重要联系人</text>
+      </view>
+      <view class="contact-item" v-for="contact in contactList" :key="contact.id" @click="goContactDetail(contact)">
+        <text class="contact-name">{{ contact.name }}</text>
+        <text class="contact-relation">{{ contact.relation || '' }}</text>
+        <text class="contact-phone">{{ contact.phone || '' }}</text>
+      </view>
+    </view>
+
+    <!-- 健康告警 -->
+    <view class="section-card" v-if="healthAlerts && healthAlerts.length > 0">
+      <view class="section-header">
+        <text class="section-title">⚠️ 健康告警</text>
+      </view>
+      <view class="alert-item" v-for="alert in healthAlerts" :key="alert.id">
+        <text class="alert-type">{{ alert.type }}</text>
+        <text class="alert-desc">{{ alert.description }}</text>
+      </view>
+    </view>
+
+    <!-- 里程碑 -->
+    <view class="section-card" v-if="milestones && milestones.length > 0">
+      <view class="section-header">
+        <text class="section-title">🎯 即将到来的里程碑</text>
+      </view>
+      <view class="milestone-item" v-for="ms in milestones" :key="ms.id || ms.title">
+        <text class="milestone-title">{{ ms.title }}</text>
+        <text class="milestone-date">{{ ms.date }}</text>
+      </view>
+    </view>
+
+    <!-- 加载中 -->
+    <view class="loading" v-if="loading">加载中...</view>
+  </view>
+</template>
+
+<script>
+import { getRelationshipScores, getContactList, getHealthAlerts, getMilestones } from '../../utils/api.js'
+
+export default {
+  props: {
+    memberId: { type: [Number, String], default: null },
+    childId: { type: [Number, String], default: null },
+    memberType: { type: String, default: '' },
+    memberInfo: { type: Object, default: null },
+    sandboxData: { type: Object, default: null },
+    energyMap: { type: Object, default: null }
+  },
+  data: function() {
+    return {
+      loading: true,
+      relationScores: null,
+      contactList: [],
+      healthAlerts: [],
+      milestones: []
+    }
+  },
+  mounted: function() {
+    this.loadData()
+  },
+  methods: {
+    loadData: function() {
+      var self = this
+      var familyId = (this.memberInfo && this.memberInfo.familyId) || ''
+
+      if (familyId) {
+        getRelationshipScores(familyId).then(function(res) {
+          if (res.code === 200 && res.data) {
+            self.relationScores = res.data
+          }
+        }).catch(function(e) { console.log('加载关系评分失败', e) })
+
+        getContactList({ familyId: familyId }).then(function(res) {
+          if (res.code === 200 && res.data) {
+            self.contactList = Array.isArray(res.data) ? res.data : (res.data.records || [])
+          }
+        }).catch(function(e) { console.log('加载联系人失败', e) })
+
+        getHealthAlerts(familyId).then(function(res) {
+          if (res.code === 200 && res.data) {
+            self.healthAlerts = Array.isArray(res.data) ? res.data : []
+          }
+        }).catch(function(e) { console.log('加载告警失败', e) })
+
+        getMilestones(familyId, 30).then(function(res) {
+          if (res.code === 200 && res.data) {
+            self.milestones = Array.isArray(res.data) ? res.data : []
+          }
+        }).catch(function(e) { console.log('加载里程碑失败', e) })
+      }
+      this.loading = false
+    },
+    goContactDetail: function(contact) {
+      uni.navigateTo({ url: '/pages/mind/contact-detail?id=' + contact.id })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.mind-section {}
+.section-card {
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 24rpx;
+  margin-bottom: 20rpx;
+  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
+}
+.section-header {
+  margin-bottom: 16rpx;
+}
+.section-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+}
+.score-item {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 10rpx 0;
+}
+.score-label {
+  font-size: 26rpx;
+  color: #555;
+  width: 160rpx;
+}
+.score-bar-bg {
+  flex: 1;
+  height: 20rpx;
+  background: #f0f0f0;
+  border-radius: 10rpx;
+  overflow: hidden;
+}
+.score-bar-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #FF6B9D, #FF9EC4);
+  border-radius: 10rpx;
+}
+.score-value {
+  font-size: 24rpx;
+  color: #FF6B9D;
+  width: 60rpx;
+  text-align: right;
+}
+.contact-item {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  padding: 14rpx 0;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+.contact-name {
+  font-size: 28rpx;
+  color: #333;
+  font-weight: bold;
+}
+.contact-relation {
+  font-size: 24rpx;
+  color: #999;
+}
+.contact-phone {
+  font-size: 24rpx;
+  color: #FF6B9D;
+}
+.alert-item {
+  display: flex;
+  flex-direction: row;
+  padding: 10rpx 0;
+}
+.alert-type {
+  font-size: 24rpx;
+  color: #E74C3C;
+  width: 100rpx;
+}
+.alert-desc {
+  font-size: 26rpx;
+  color: #555;
+  flex: 1;
+}
+.milestone-item {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  padding: 10rpx 0;
+}
+.milestone-title {
+  font-size: 26rpx;
+  color: #333;
+}
+.milestone-date {
+  font-size: 24rpx;
+  color: #999;
+}
+.loading {
+  text-align: center;
+  padding: 40rpx;
+  color: #999;
+}
+</style>

+ 206 - 0
cfc-frontend/components/member-detail-sections/wisdom-section.vue

@@ -0,0 +1,206 @@
+<template>
+  <view class="wisdom-section">
+    <!-- 最新测评结果 -->
+    <view class="section-card" v-if="latestResult">
+      <view class="section-header">
+        <text class="section-title">🧠 最新测评结果</text>
+      </view>
+      <view class="result-overview" @click="goResultDetail">
+        <text class="result-score">{{ latestResult.score || '--' }}</text>
+        <text class="result-dimension">{{ latestResult.dimensionName || latestResult.title || '综合测评' }}</text>
+      </view>
+      <view class="result-date">测评日期:{{ formatDate(latestResult.createTime || latestResult.testDate) }}</view>
+    </view>
+    <view class="section-card" v-else-if="!loading && childId">
+      <view class="empty-state">
+        <text class="empty-text">暂无测评记录</text>
+        <text class="empty-sub">完成测评后结果将显示在这里</text>
+      </view>
+    </view>
+
+    <!-- 测评历史 -->
+    <view class="section-card" v-if="historyList.length > 0">
+      <view class="section-header">
+        <text class="section-title">📋 测评历史</text>
+      </view>
+      <view class="history-item" v-for="item in historyList" :key="item.id" @click="goResultDetail">
+        <text class="history-title">{{ item.title || item.dimensionName || '测评' }}</text>
+        <text class="history-score">{{ item.score || '--' }}</text>
+        <text class="history-date">{{ formatDate(item.createTime || item.testDate) }}</text>
+      </view>
+    </view>
+
+    <!-- 非孩子成员提示 -->
+    <view class="section-card" v-if="!childId && !loading">
+      <view class="empty-state">
+        <text class="empty-text">该成员暂无测评数据</text>
+      </view>
+    </view>
+
+    <!-- 加载中 -->
+    <view class="loading" v-if="loading">加载中...</view>
+  </view>
+</template>
+
+<script>
+import { getAssessmentLatestResult, getAssessmentHistory } from '../../utils/api.js'
+
+export default {
+  props: {
+    memberId: {
+      type: [Number, String],
+      default: null
+    },
+    childId: {
+      type: [Number, String],
+      default: null
+    },
+    memberType: {
+      type: String,
+      default: ''
+    },
+    memberInfo: {
+      type: Object,
+      default: null
+    },
+    sandboxData: {
+      type: Object,
+      default: null
+    },
+    energyMap: {
+      type: Object,
+      default: null
+    }
+  },
+  data: function() {
+    return {
+      loading: true,
+      latestResult: null,
+      historyList: []
+    }
+  },
+  mounted: function() {
+    if (this.childId) {
+      this.loadData()
+    } else {
+      this.loading = false
+    }
+  },
+  methods: {
+    loadData: function() {
+      var self = this
+      getAssessmentLatestResult(this.childId).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.latestResult = res.data
+        }
+      }).catch(function(e) {
+        console.log('加载测评结果失败', e)
+      })
+      
+      getAssessmentHistory(this.childId, 5).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.historyList = Array.isArray(res.data) ? res.data : (res.data.records || [])
+        }
+      }).catch(function(e) {
+        console.log('加载测评历史失败', e)
+      })
+      
+      this.loading = false
+    },
+    formatDate: function(dateStr) {
+      if (!dateStr) return ''
+      var d = new Date(dateStr)
+      return d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate()
+    },
+    goResultDetail: function() {
+      uni.navigateTo({
+        url: '/pages/wisdom/assessment-detail?childId=' + this.childId
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.wisdom-section {}
+.section-card {
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 24rpx;
+  margin-bottom: 20rpx;
+  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
+}
+.section-header {
+  margin-bottom: 16rpx;
+}
+.section-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+}
+.result-overview {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 20rpx 0;
+}
+.result-score {
+  font-size: 72rpx;
+  font-weight: bold;
+  color: #6366F1;
+}
+.result-dimension {
+  font-size: 28rpx;
+  color: #555;
+  margin-top: 8rpx;
+}
+.result-date {
+  font-size: 24rpx;
+  color: #999;
+  padding-top: 12rpx;
+  border-top: 1rpx solid #f0f0f0;
+  margin-top: 8rpx;
+}
+.history-item {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  padding: 14rpx 0;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+.history-title {
+  font-size: 26rpx;
+  color: #333;
+  flex: 1;
+}
+.history-score {
+  font-size: 26rpx;
+  color: #6366F1;
+  width: 80rpx;
+  text-align: center;
+}
+.history-date {
+  font-size: 22rpx;
+  color: #999;
+  width: 140rpx;
+  text-align: right;
+}
+.empty-state {
+  padding: 30rpx 0;
+  text-align: center;
+}
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+}
+.empty-sub {
+  font-size: 24rpx;
+  color: #ccc;
+  margin-top: 8rpx;
+}
+.loading {
+  text-align: center;
+  padding: 40rpx;
+  color: #999;
+}
+</style>

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

@@ -48,6 +48,17 @@
           @memberTap="onGraphMemberTap" />
       </view>
 
+      <!-- 维度专属内容 -->
+      <view class="dimension-section">
+        <component :is="dimensionSection"
+          :memberId="targetMemberId"
+          :childId="childId"
+          :memberType="memberType"
+          :memberInfo="memberInfo"
+          :sandboxData="sandboxData"
+          :energyMap="energyMapForGraph" />
+      </view>
+
       <!-- 今日任务(仅当有 childId 时显示) -->
       <DimensionTasks
         v-if="childId"
@@ -84,6 +95,10 @@ import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
 import DimensionTasks from '../../components/DimensionTasks.vue'
 import DimensionActivities from '../../components/DimensionActivities.vue'
 import DimensionProducts from '../../components/DimensionProducts.vue'
+import BodySection from '../../components/member-detail-sections/body-section.vue'
+import MindSection from '../../components/member-detail-sections/mind-section.vue'
+import WisdomSection from '../../components/member-detail-sections/wisdom-section.vue'
+import ActionSection from '../../components/member-detail-sections/action-section.vue'
 import { getFamilyEnergySandbox, getTodayTasksByCategory, getActivityList, getProductsByDomain, getChildren } from '../../utils/api.js'
 
 // 维度配置
@@ -97,7 +112,7 @@ var dimensionConfig = {
 }
 
 export default {
-  components: { FamilyEnergyBar, FamilyRelationGraph, DimensionTasks, DimensionActivities, DimensionProducts },
+  components: { FamilyEnergyBar, FamilyRelationGraph, DimensionTasks, DimensionActivities, DimensionProducts, BodySection, MindSection, WisdomSection, ActionSection },
   data() {
     return {
       memberId: null,
@@ -215,6 +230,15 @@ export default {
     // 是否显示推荐内容(身卡片入口不显示)
     showRecommendations: function() {
       return this.entrySource !== 'body-card'
+    },
+    dimensionSection: function() {
+      var map = {
+        body: BodySection,
+        mind: MindSection,
+        wisdom: WisdomSection,
+        action: ActionSection
+      }
+      return map[this.dimensionCode] || null
     }
   },
   onLoad: function(options) {
@@ -477,4 +501,8 @@ export default {
 .bottom-spacer {
   height: 40rpx;
 }
+
+.dimension-section {
+  margin: 20rpx 30rpx;
+}
 </style>

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

@@ -21,7 +21,16 @@ function _clearAuthAndRedirect() {
   }, 1500)
 }
 
-const request = (url, method = 'POST', data = {}) => {
+const request = (url, method = 'POST', data = {}, options = {}) => {
+  // Append query params from options to URL if provided (e.g., { guideId } for GET requests)
+  if (options && Object.keys(options).length > 0) {
+    const separator = url.includes('?') ? '&' : '?'
+    const query = Object.entries(options)
+      .filter(([_, v]) => v !== undefined && v !== null && v !== '')
+      .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
+      .join('&')
+    if (query) url = url + separator + query
+  }
   const token = uni.getStorageSync('token')
   
   return new Promise((resolve, reject) => {

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-b61d3479cbe92c826614952d635e473050f50e24
+67105bada7ecd5c837d5b3e8ea3e89876e0fd293

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

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.320",
+  "version": "1.0.333",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.320",
+      "version": "1.0.333",
       "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.321",
+  "version": "1.0.333",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

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

@@ -4,6 +4,103 @@
 
 ---
 
+## v1.0.333 (2026-07-15)
+
+### Bug 修复
+- add dimension_weights column to activities table (migration 88)
+
+
+## v1.0.332 (2026-07-15)
+
+### Bug 修复
+- CommissionRecord 添加 family_id 字段 + CommissionService createCommissionRecord 集成 familyId
+
+
+## v1.0.331 (2026-07-15)
+
+### Bug 修复
+- CommissionService 编译错误 - 补充 familyId 字段和 L2 调用参数
+
+
+## v1.0.329 (2026-07-14)
+
+### 文档
+- 成员详情维度分段 设计文档+实施计划
+
+### 其他
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+
+### 新功能
+- 创建身/心/智/行四个维度专属内容组件
+- member-detail 维度专属内容动态组件插槽
+
+
+## v1.0.328 (2026-07-14)
+
+### Bug 修复
+- 代码规范修复 — @Autowired→@Resource + Result<T>泛型补全
+
+### 其他
+- - AdminGuideController: 3处@Autowired替换为@Resource,移除Autowired import
+- - VendorService: 3个方法补全Result<T>泛型参数
+- - VendorController: 同步补全Result<T>泛型参数
+- 
+
+
+## v1.0.327 (2026-07-14)
+
+### Bug 修复
+- 后端稳定性 — catch块日志/空值处理/PostMapping统一/管家stub
+
+### 其他
+- - DatabaseInitializer: 36个空catch块添加log.warn日志
+- - TeacherService: getTeacherInfo抛异常, getAuthorizedChildren返回空列表
+- - 14个controller: 42处@GetMapping/@PutMapping/@DeleteMapping→@PostMapping
+- - ButlerController: 2个stub方法标记@Deprecated+抛UnsupportedOperationException
+- 
+
+
+## v1.0.325 (2026-07-14)
+
+### Bug 修复
+- 金融安全修复 — payment绕过/验证码stub/积分兑换/教师端点/类型转换
+
+### 新功能
+- add cfc-web butler admin UI + ButlerCommissionService + scheduled settlement
+
+
+## v1.0.324 (2026-07-14)
+
+### Bug 修复
+- 迁移87: articles表添加content_type/difficulty_level/dimension_ids/dimension_weights字段
+
+
+## v1.0.323 (2026-07-14)
+
+### 新功能
+- add AdminButlerController + ButlerServiceRecord/CommissionSettlement entities
+
+### Bug 修复
+- 迁移86: product_orders表添加purchase_info字段(OrderTimeoutCancelTask)
+
+### 其他
+- 注册ButlerController bean起别名,解决多个ButlerController项目启动报错问题
+
+
+## v1.0.322 (2026-07-14)
+
+### Bug 修复
+- 调整 pages/profile/index 在 tabBar 中的位置并移除 customTabBar 配置
+- 同步 pages/wealth/index 路径与远程一致
+- 修复 Recipes.vue el-dialog 开始标签丢失
+
+
 ## v1.0.321 (2026-07-14)
 
 ### 新功能

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

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.321
+> 当前版本: v1.0.333
 
 ## 历史版本
 
@@ -8,6 +8,103 @@
 
 ---
 
+## v1.0.333 (2026-07-15)
+
+### Bug 修复
+- add dimension_weights column to activities table (migration 88)
+
+
+## v1.0.332 (2026-07-15)
+
+### Bug 修复
+- CommissionRecord 添加 family_id 字段 + CommissionService createCommissionRecord 集成 familyId
+
+
+## v1.0.331 (2026-07-15)
+
+### Bug 修复
+- CommissionService 编译错误 - 补充 familyId 字段和 L2 调用参数
+
+
+## v1.0.329 (2026-07-14)
+
+### 文档
+- 成员详情维度分段 设计文档+实施计划
+
+### 其他
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+
+### 新功能
+- 创建身/心/智/行四个维度专属内容组件
+- member-detail 维度专属内容动态组件插槽
+
+
+## v1.0.328 (2026-07-14)
+
+### Bug 修复
+- 代码规范修复 — @Autowired→@Resource + Result<T>泛型补全
+
+### 其他
+- - AdminGuideController: 3处@Autowired替换为@Resource,移除Autowired import
+- - VendorService: 3个方法补全Result<T>泛型参数
+- - VendorController: 同步补全Result<T>泛型参数
+- 
+
+
+## v1.0.327 (2026-07-14)
+
+### Bug 修复
+- 后端稳定性 — catch块日志/空值处理/PostMapping统一/管家stub
+
+### 其他
+- - DatabaseInitializer: 36个空catch块添加log.warn日志
+- - TeacherService: getTeacherInfo抛异常, getAuthorizedChildren返回空列表
+- - 14个controller: 42处@GetMapping/@PutMapping/@DeleteMapping→@PostMapping
+- - ButlerController: 2个stub方法标记@Deprecated+抛UnsupportedOperationException
+- 
+
+
+## v1.0.325 (2026-07-14)
+
+### Bug 修复
+- 金融安全修复 — payment绕过/验证码stub/积分兑换/教师端点/类型转换
+
+### 新功能
+- add cfc-web butler admin UI + ButlerCommissionService + scheduled settlement
+
+
+## v1.0.324 (2026-07-14)
+
+### Bug 修复
+- 迁移87: articles表添加content_type/difficulty_level/dimension_ids/dimension_weights字段
+
+
+## v1.0.323 (2026-07-14)
+
+### 新功能
+- add AdminButlerController + ButlerServiceRecord/CommissionSettlement entities
+
+### Bug 修复
+- 迁移86: product_orders表添加purchase_info字段(OrderTimeoutCancelTask)
+
+### 其他
+- 注册ButlerController bean起别名,解决多个ButlerController项目启动报错问题
+
+
+## v1.0.322 (2026-07-14)
+
+### Bug 修复
+- 调整 pages/profile/index 在 tabBar 中的位置并移除 customTabBar 配置
+- 同步 pages/wealth/index 路径与远程一致
+- 修复 Recipes.vue el-dialog 开始标签丢失
+
+
 ## v1.0.321 (2026-07-14)
 
 ### 新功能

+ 16 - 16
cfc-web/src/api/admin.js

@@ -233,8 +233,8 @@ export function getPointsStats() {
 export function getWishesList(params) {
   return request({
     url: '/api/wishes',
-    method: 'get',
-    params
+    method: 'post',
+    data: params
   })
 }
 
@@ -242,8 +242,8 @@ export function getWishesList(params) {
 export function getPendingWishes(familyId) {
   return request({
     url: '/api/wishes/pending',
-    method: 'get',
-    params: { familyId }
+    method: 'post',
+    data: { familyId }
   })
 }
 
@@ -251,7 +251,7 @@ export function getPendingWishes(familyId) {
 export function getWishDetail(id) {
   return request({
     url: `/api/wishes/${id}`,
-    method: 'get'
+    method: 'post'
   })
 }
 
@@ -1038,7 +1038,7 @@ export function getSupplySystemList(params) {
   return request({
     url: '/api/admin/supply-system/list',
     method: 'post',
-    params: params || {}
+    data: params || {}
   })
 }
 
@@ -1046,7 +1046,7 @@ export function getSupplySystemDetail(id) {
   return request({
     url: '/api/admin/supply-system/detail',
     method: 'post',
-    params: { id }
+    data: { id }
   })
 }
 
@@ -1070,7 +1070,7 @@ export function toggleSupplySystemStatus(id) {
   return request({
     url: '/api/admin/supply-system/toggle-status',
     method: 'post',
-    params: { id }
+    data: { id }
   })
 }
 
@@ -1085,7 +1085,7 @@ export function getSupplySystemMembers(systemId) {
   return request({
     url: '/api/admin/supply-system/member/list',
     method: 'post',
-    params: { systemId }
+    data: { systemId }
   })
 }
 
@@ -1093,7 +1093,7 @@ export function addSupplySystemMember(systemId, userId) {
   return request({
     url: '/api/admin/supply-system/member/add',
     method: 'post',
-    params: { systemId, userId }
+    data: { systemId, userId }
   })
 }
 
@@ -1101,7 +1101,7 @@ export function removeSupplySystemMember(memberId) {
   return request({
     url: '/api/admin/supply-system/member/remove',
     method: 'post',
-    params: { memberId }
+    data: { memberId }
   })
 }
 
@@ -1109,7 +1109,7 @@ export function changeSupplySystemMemberRole(memberId, role) {
   return request({
     url: '/api/admin/supply-system/member/change-role',
     method: 'post',
-    params: { memberId, role }
+    data: { memberId, role }
   })
 }
 
@@ -1225,7 +1225,7 @@ export function getSupplyHierarchyTree(systemId) {
     return request({
         url: '/api/admin/supply-hierarchy/tree',
         method: 'post',
-        params: { systemId }
+        data: { systemId }
     })
 }
 
@@ -1338,7 +1338,7 @@ export function cancelSubscriptionAdmin(data) {
 export function getBenefitConfig() {
   return request({
     url: '/api/admin/subscription/benefits',
-    method: 'get'
+    method: 'post'
   })
 }
 
@@ -1433,7 +1433,7 @@ export function getProductPpointByProduct(data) {
 export function getPromotionTierConfig() {
   return request({
     url: '/api/admin/promotion/config',
-    method: 'get'
+    method: 'post'
   })
 }
 
@@ -1475,7 +1475,7 @@ export function cancelPendingRefund(params) {
 export function getCommissionConfig() {
   return request({
     url: '/api/admin/commission/config',
-    method: 'get'
+    method: 'post'
   })
 }
 

+ 46 - 0
cfc-web/src/api/butler.js

@@ -0,0 +1,46 @@
+import request from '@/utils/request'
+
+// 管家列表
+export function getButlerList(params) {
+  return request({
+    url: '/api/admin/butler/list',
+    method: 'post',
+    data: params
+  })
+}
+
+// 审核管家
+export function reviewButler(data) {
+  return request({
+    url: '/api/admin/butler/review',
+    method: 'post',
+    data: data
+  })
+}
+
+// 管家服务记录
+export function getButlerServiceRecords(params) {
+  return request({
+    url: '/api/admin/butler/service-records',
+    method: 'post',
+    data: params
+  })
+}
+
+// 管家佣金结算
+export function getButlerSettlements(params) {
+  return request({
+    url: '/api/admin/butler/settlements',
+    method: 'post',
+    data: params
+  })
+}
+
+// 分配成员到管家
+export function assignMemberToButler(data) {
+  return request({
+    url: '/api/admin/butler/assign-member',
+    method: 'post',
+    data: data
+  })
+}

+ 6 - 0
cfc-web/src/router/index.js

@@ -216,6 +216,12 @@ const routes = [
         component: () => import('@/views/admin/VendorReview.vue'),
         meta: { title: '服务商审核', perm: 'audit:vendor' }
       },
+      {
+        path: 'butler-review',
+        name: 'ButlerReview',
+        component: () => import('@/views/admin/ButlerReview.vue'),
+        meta: { title: '管家审核', perm: 'audit:vendor' }
+      },
       {
         path: 'product-manage',
         name: 'ProductManage',

+ 1 - 0
cfc-web/src/views/Layout.vue

@@ -184,6 +184,7 @@ export default {
             { path: '/package-audit', label: '套餐审核', icon: 'el-icon-document-checked', perm: 'audit:package' },
             { path: '/guide-audit', label: '规划师审核', icon: 'el-icon-document-checked', perm: 'audit:guide' },
             { path: '/vendor-review', label: '服务商审核', icon: 'el-icon-document-checked', perm: 'audit:vendor' },
+            { path: '/butler-review', label: '管家审核', icon: 'el-icon-document-checked', perm: 'audit:vendor' },
             { path: '/withdraw-audit', label: '提现审核', icon: 'el-icon-document-checked', perm: 'audit:withdraw' },
           ]},
 

+ 264 - 0
cfc-web/src/views/admin/ButlerReview.vue

@@ -0,0 +1,264 @@
+<template>
+  <div class="butler-review admin-page">
+    <el-tabs v-model="activeTab" @tab-click="onTabChange">
+      <el-tab-pane label="管家审核" name="review">
+        <div class="header admin-page-header">
+          <h2 class="admin-page-title">管家审核</h2>
+          <div class="flex items-center gap-sm">
+            <el-select v-model="statusFilter" placeholder="审核状态" @change="loadList">
+              <el-option label="全部状态" value="" />
+              <el-option label="待审核" value="pending" />
+              <el-option label="已通过" value="approved" />
+              <el-option label="已拒绝" value="rejected" />
+            </el-select>
+          </div>
+        </div>
+
+        <div class="table-scroll-wrap">
+          <el-table :data="list" v-loading="loading" border stripe>
+            <el-table-column prop="id" label="档案ID" width="80" />
+            <el-table-column prop="userId" label="用户ID" width="80" />
+            <el-table-column label="管家等级" width="100">
+              <template slot-scope="{ row }">{{ row.tier || '-' }}</template>
+            </el-table-column>
+            <el-table-column label="审核状态" width="100">
+              <template slot-scope="{ row }">
+                <el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column prop="description" label="申请说明" min-width="200" show-overflow-tooltip />
+            <el-table-column prop="memberCount" label="已服务人数" width="100" />
+            <el-table-column prop="maxMembers" label="上限人数" width="80" />
+            <el-table-column prop="rejectReason" label="拒绝原因" width="150" show-overflow-tooltip />
+            <el-table-column label="操作" width="160" fixed="right">
+              <template slot-scope="{ row }">
+                <template v-if="row.status === 'pending'">
+                  <el-button size="mini" type="success" @click="handleReview(row, 'approve')">通过</el-button>
+                  <el-dropdown trigger="hover" @command="(cmd) => handleActionCmd(row, cmd)">
+                    <el-button size="mini">
+                      更多<i class="el-icon-arrow-down el-icon--right"></i>
+                    </el-button>
+                    <el-dropdown-menu slot="dropdown">
+                      <el-dropdown-item command="reject" icon="el-icon-close">拒绝</el-dropdown-item>
+                    </el-dropdown-menu>
+                  </el-dropdown>
+                </template>
+                <span v-else class="no-op">—</span>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+
+        <el-pagination
+          @size-change="onPageChange"
+          @current-change="onPageChange"
+          :current-page="page"
+          :page-size="size"
+          :total="total"
+          layout="total, prev, pager, next"
+          class="pagination-wrap"
+        />
+
+        <el-dialog title="填写拒绝原因" :visible.sync="rejectDialogVisible" width="400px">
+          <el-input v-model="rejectReason" type="textarea" :rows="3" placeholder="请输入拒绝原因" />
+          <span slot="footer">
+            <el-button @click="rejectDialogVisible = false">取消</el-button>
+            <el-button type="danger" @click="confirmReject">确认拒绝</el-button>
+          </span>
+        </el-dialog>
+      </el-tab-pane>
+
+      <el-tab-pane label="服务记录" name="service-records">
+        <div class="header admin-page-header">
+          <h2 class="admin-page-title">管家服务记录</h2>
+        </div>
+        <div class="table-scroll-wrap">
+          <el-table :data="serviceRecords" v-loading="srLoading" border stripe>
+            <el-table-column prop="id" label="记录ID" width="80" />
+            <el-table-column prop="butlerId" label="管家ID" width="80" />
+            <el-table-column prop="memberUserId" label="会员ID" width="80" />
+            <el-table-column label="服务类型" width="120">
+              <template slot-scope="{ row }">{{ serviceTypeLabel(row.serviceType) }}</template>
+            </el-table-column>
+            <el-table-column prop="amountP" label="金额(P点)" width="100" />
+            <el-table-column prop="platformFeeP" label="平台留成" width="100" />
+            <el-table-column prop="butlerAmountP" label="管家所得" width="100" />
+            <el-table-column prop="yearMonth" label="结算年月" width="100" />
+            <el-table-column prop="createdAt" label="创建时间" width="160" />
+          </el-table>
+        </div>
+        <el-pagination
+          @size-change="srPage = $event; loadServiceRecords()"
+          @current-change="srPage = $event; loadServiceRecords()"
+          :current-page="srPage"
+          :page-size="srSize"
+          :total="srTotal"
+          layout="total, prev, pager, next"
+          class="pagination-wrap"
+        />
+      </el-tab-pane>
+
+      <el-tab-pane label="佣金结算" name="settlements">
+        <div class="header admin-page-header">
+          <h2 class="admin-page-title">管家佣金结算</h2>
+        </div>
+        <div class="table-scroll-wrap">
+          <el-table :data="settlements" v-loading="stLoading" border stripe>
+            <el-table-column prop="id" label="结算单ID" width="80" />
+            <el-table-column prop="butlerId" label="管家ID" width="80" />
+            <el-table-column prop="yearMonth" label="结算年月" width="100" />
+            <el-table-column prop="memberCount" label="服务家庭数" width="100" />
+            <el-table-column prop="annualBaseAmountP" label="年费基准" width="100" />
+            <el-table-column prop="totalButlerAmountP" label="管家总所得" width="120" />
+            <el-table-column label="结算状态" width="100">
+              <template slot-scope="{ row }">
+                <el-tag :type="row.status === 'paid' ? 'success' : 'warning'">{{ row.status === 'paid' ? '已结算' : '待结算' }}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column prop="settledAt" label="结算时间" width="160" />
+          </el-table>
+        </div>
+        <el-pagination
+          @size-change="stPage = $event; loadSettlements()"
+          @current-change="stPage = $event; loadSettlements()"
+          :current-page="stPage"
+          :page-size="stSize"
+          :total="stTotal"
+          layout="total, prev, pager, next"
+          class="pagination-wrap"
+        />
+      </el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<script>
+import { getButlerList, reviewButler, getButlerServiceRecords, getButlerSettlements } from '@/api/butler'
+
+export default {
+  data() {
+    return {
+      activeTab: 'review',
+      // 审核列表
+      list: [],
+      loading: false,
+      page: 1,
+      size: 20,
+      total: 0,
+      statusFilter: 'pending',
+      rejectDialogVisible: false,
+      rejectTarget: null,
+      rejectReason: '',
+      // 服务记录
+      serviceRecords: [],
+      srLoading: false,
+      srPage: 1,
+      srSize: 20,
+      srTotal: 0,
+      // 结算
+      settlements: [],
+      stLoading: false,
+      stPage: 1,
+      stSize: 20,
+      stTotal: 0
+    }
+  },
+  mounted() {
+    this.loadList()
+  },
+  methods: {
+    statusType(status) {
+      const map = { pending: 'warning', approved: 'success', rejected: 'danger' }
+      return map[status] || 'info'
+    },
+    statusLabel(status) {
+      const map = { pending: '待审核', approved: '已通过', rejected: '已拒绝' }
+      return map[status] || status
+    },
+    serviceTypeLabel(type) {
+      const map = { annual: '年费服务', per_session: '按次服务' }
+      return map[type] || type
+    },
+    onTabChange() {
+      this.page = 1
+      if (this.activeTab === 'review') this.loadList()
+      else if (this.activeTab === 'service-records') this.loadServiceRecords()
+      else if (this.activeTab === 'settlements') this.loadSettlements()
+    },
+    async loadList() {
+      this.loading = true
+      try {
+        const params = { page: this.page, size: this.size, status: this.statusFilter }
+        const res = await getButlerList(params)
+        this.list = res.data.list || []
+        this.total = res.data.total || 0
+      } catch (e) {
+        this.$message.error('加载失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    async loadServiceRecords() {
+      this.srLoading = true
+      try {
+        const res = await getButlerServiceRecords({ page: this.srPage, size: this.srSize })
+        this.serviceRecords = res.data.list || []
+        this.srTotal = res.data.total || 0
+      } catch (e) {
+        this.$message.error('加载失败')
+      } finally {
+        this.srLoading = false
+      }
+    },
+    async loadSettlements() {
+      this.stLoading = true
+      try {
+        const res = await getButlerSettlements({ page: this.stPage, size: this.stSize })
+        this.settlements = res.data.list || []
+        this.stTotal = res.data.total || 0
+      } catch (e) {
+        this.$message.error('加载失败')
+      } finally {
+        this.stLoading = false
+      }
+    },
+    onPageChange(val) {
+      this.page = val
+      this.loadList()
+    },
+    async handleReview(row, action) {
+      try {
+        await reviewButler({ userId: row.userId, action: action, reason: this.rejectReason })
+        this.$message.success('审核完成')
+        this.loadList()
+      } catch (e) {
+        this.$message.error('操作失败')
+      }
+    },
+    showRejectDialog(row) {
+      this.rejectTarget = row
+      this.rejectReason = ''
+      this.rejectDialogVisible = true
+    },
+    handleActionCmd(row, cmd) {
+      switch (cmd) {
+        case 'reject': this.showRejectDialog(row); break
+      }
+    },
+    async confirmReject() {
+      if (!this.rejectReason) {
+        this.$message.warning('请填写拒绝原因')
+        return
+      }
+      await this.handleReview(this.rejectTarget, 'reject')
+      this.rejectDialogVisible = false
+    }
+  }
+}
+</script>
+
+<style scoped>
+.no-op {
+  color: #ccc;
+}
+</style>

+ 825 - 0
docs/superpowers/plans/2026-07-14-member-detail-dimension-sections.md

@@ -0,0 +1,825 @@
+# 家庭成员详情页维度专属内容区 — 实施计划
+
+> **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:** 家庭成员详情页根据 `dimensionCode` 参数(body/mind/wisdom/action)展示维度专属内容
+
+**Architecture:** 在现有 `member-detail.vue` 的 FamilyRelationGraph 下方插入动态组件区域,通过 `<component :is="dimensionSection" />` 根据 dimensionCode 切换 4 个独立子组件。共用部分(导航、成员卡、能量条、关系图谱)不变。
+
+**Tech Stack:** uni-app Vue 2 (Options API), 微信小程序
+
+---
+
+### Task 1: 改造 member-detail.vue — 动态组件插槽
+
+**Files:**
+- Modify: `cfc-frontend/pages/member-detail/member-detail.vue`
+
+- [ ] **Step 1: 在 template 中 FamilyRelationGraph 之后插入动态组件区**
+
+在 `member-detail.vue` 的 template 中,FamilyRelationGraph 的 `</view>` 闭合标签之后、`<!-- 底部占位 -->` 之前,添加:
+
+```html
+      <!-- 维度专属内容 -->
+      <view class="dimension-section">
+        <component :is="dimensionSection" />
+      </view>
+```
+
+- [ ] **Step 2: 在 script 中添加 imports**
+
+在现有 import 块末尾添加:
+
+```javascript
+import BodySection from '../../components/member-detail-sections/body-section.vue'
+import MindSection from '../../components/member-detail-sections/mind-section.vue'
+import WisdomSection from '../../components/member-detail-sections/wisdom-section.vue'
+import ActionSection from '../../components/member-detail-sections/action-section.vue'
+```
+
+在 `components: { ... }` 中添加这四个组件。
+
+- [ ] **Step 3: 添加 computed 属性映射 dimensionCode → 组件**
+
+```javascript
+    dimensionSection: function() {
+      var map = {
+        body: BodySection,
+        mind: MindSection,
+        wisdom: WisdomSection,
+        action: ActionSection
+      }
+      return map[this.dimensionCode] || null
+    },
+```
+
+- [ ] **Step 4: 添加样式**
+
+```css
+.dimension-section {
+  margin: 20rpx 30rpx;
+}
+```
+
+- [ ] **Step 5: 确认 member-detail.vue 的 props 传递正确**
+
+在 `<component :is="dimensionSection">` 上添加 props 绑定:
+
+```html
+        <component :is="dimensionSection"
+          :memberId="targetMemberId"
+          :childId="childId"
+          :memberType="memberType"
+          :memberInfo="memberInfo"
+          :sandboxData="sandboxData"
+          :energyMap="energyMapForGraph" />
+```
+
+---
+
+### Task 2: 创建 body-section.vue
+
+**Files:**
+- Create: `cfc-frontend/components/member-detail-sections/body-section.vue`
+- Reference: `cfc-frontend/pages/body/index.vue` 的 `loadHealthReport`, `loadDimensionData`, `getCheckinList`
+
+- [ ] **Step 1: 创建 body-section.vue 基本结构(template + script + style)**
+
+```html
+<template>
+  <view class="body-section">
+    <!-- 最新健康报告 -->
+    <view class="section-card" v-if="healthReport">
+      <view class="section-header">
+        <text class="section-title">📋 最新健康报告</text>
+      </view>
+      <view class="report-card" @click="goReportDetail">
+        <text class="report-score">{{ healthReport.overallScore || '--' }}</text>
+        <text class="report-label">综合健康分</text>
+      </view>
+      <view class="report-meta">
+        <text class="report-date">报告日期:{{ formatDate(healthReport.reportDate) }}</text>
+      </view>
+    </view>
+    <view class="section-card" v-else-if="!loading">
+      <view class="empty-state">
+        <text class="empty-text">暂无健康报告</text>
+        <text class="empty-sub">规划师录入后将自动显示</text>
+      </view>
+    </view>
+
+    <!-- 七维健康图 -->
+    <view class="section-card" v-if="dimensionOverview">
+      <view class="section-header">
+        <text class="section-title">📊 七维健康图</text>
+      </view>
+      <RadarChart
+        :dimensions="dimensionOverview.dimensions.map(function(d) { return { label: d.label, key: d.dimension } })"
+        :scores="dimensionOverview.dimensions.map(function(d) { return d.score })"
+        fillColor="#FF8C42"
+        gridColor="#FED7AA"
+        labelColor="#9A3412"
+        :width="580"
+        :height="400" />
+    </view>
+
+    <!-- 最近打卡 -->
+    <view class="section-card" v-if="checkinList.length > 0">
+      <view class="section-header">
+        <text class="section-title">🏃 最近打卡</text>
+      </view>
+      <view class="checkin-item" v-for="item in checkinList" :key="item.id">
+        <text class="checkin-type">{{ item.typeName || item.type }}</text>
+        <text class="checkin-time">{{ formatDate(item.createTime) }}</text>
+      </view>
+    </view>
+
+    <!-- 仅孩子成员提示 -->
+    <view class="section-card" v-if="!childId && !loading">
+      <view class="empty-state">
+        <text class="empty-text">该成员暂无健康数据</text>
+      </view>
+    </view>
+
+    <!-- 加载中 -->
+    <view class="loading" v-if="loading">加载中...</view>
+  </view>
+</template>
+```
+
+- [ ] **Step 2: 添加 script 逻辑**
+
+```html
+<script>
+import RadarChart from '../RadarChart.vue'
+import { getHealthAnalysis, getDimensionOverview, getCheckinList } from '../../utils/api.js'
+
+export default {
+  components: { RadarChart },
+  props: {
+    memberId: [Number, String],
+    childId: [Number, String],
+    memberType: String,
+    memberInfo: Object,
+    sandboxData: Object,
+    energyMap: Object
+  },
+  data: function() {
+    return {
+      loading: true,
+      healthReport: null,
+      dimensionOverview: null,
+      checkinList: []
+    }
+  },
+  mounted: function() {
+    if (this.childId) {
+      this.loadData()
+    } else {
+      this.loading = false
+    }
+  },
+  methods: {
+    loadData: function() {
+      var self = this
+      // 并行加载
+      getHealthAnalysis(this.childId).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.healthReport = res.data
+        }
+      }).catch(function(e) { console.log('加载健康分析失败', e) })
+
+      getDimensionOverview({ childId: this.childId }).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.dimensionOverview = res.data
+        }
+      }).catch(function(e) { console.log('加载维度概览失败', e) })
+
+      getCheckinList({ childId: this.childId, page: 1, size: 3 }).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.checkinList = Array.isArray(res.data) ? res.data : (res.data.records || [])
+        }
+      }).catch(function(e) { console.log('加载打卡记录失败', e) })
+
+      this.loading = false
+    },
+    formatDate: function(dateStr) {
+      if (!dateStr) return ''
+      var d = new Date(dateStr)
+      return d.getFullYear() + '-' + (d.getMonth()+1) + '-' + d.getDate()
+    },
+    goReportDetail: function() {
+      uni.navigateTo({ url: '/pages/body-detail/health-report?childId=' + this.childId })
+    }
+  }
+}
+</script>
+```
+
+- [ ] **Step 3: 添加样式**
+
+```html
+<style scoped>
+.body-section {}
+.section-card {
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 24rpx;
+  margin-bottom: 20rpx;
+  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
+}
+.section-header {
+  margin-bottom: 16rpx;
+}
+.section-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+}
+.report-card {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 20rpx 0;
+}
+.report-score {
+  font-size: 72rpx;
+  font-weight: bold;
+  color: #FF8C42;
+}
+.report-label {
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 8rpx;
+}
+.report-meta {
+  padding-top: 12rpx;
+  border-top: 1rpx solid #f0f0f0;
+}
+.report-date {
+  font-size: 24rpx;
+  color: #999;
+}
+.checkin-item {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  padding: 16rpx 0;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+.checkin-type {
+  font-size: 28rpx;
+  color: #333;
+}
+.checkin-time {
+  font-size: 24rpx;
+  color: #999;
+}
+.empty-state {
+  padding: 30rpx 0;
+  text-align: center;
+}
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+}
+.empty-sub {
+  font-size: 24rpx;
+  color: #ccc;
+  margin-top: 8rpx;
+}
+.loading {
+  text-align: center;
+  padding: 40rpx;
+  color: #999;
+}
+</style>
+```
+
+---
+
+### Task 3: 创建 mind-section.vue
+
+**Files:**
+- Create: `cfc-frontend/components/member-detail-sections/mind-section.vue`
+- Reference: `cfc-frontend/pages/mind/index.vue` 的 `loadContacts`, `loadHealthAlerts`, `loadMilestones`
+
+- [ ] **Step 1: 创建 mind-section.vue 模板**
+
+```html
+<template>
+  <view class="mind-section">
+    <!-- 亲密关系评分 -->
+    <view class="section-card" v-if="relationshipScores.length > 0">
+      <view class="section-header">
+        <text class="section-title">💞 亲密关系评分</text>
+      </view>
+      <view class="relation-item" v-for="score in relationshipScores" :key="score.memberId">
+        <text class="relation-name">{{ score.nickname || '成员' }}</text>
+        <view class="relation-bar-wrap">
+          <view class="relation-bar" :style="{ width: score.trust + '%', background: getScoreColor(score.trust) }"></view>
+        </view>
+        <text class="relation-score">{{ score.trust }}</text>
+      </view>
+    </view>
+
+    <!-- 联系人 -->
+    <view class="section-card" v-if="contactList.length > 0">
+      <view class="section-header">
+        <text class="section-title">📞 联系人</text>
+      </view>
+      <view class="contact-item" v-for="item in contactList" :key="item.id">
+        <text class="contact-name">{{ item.name }}</text>
+        <text class="contact-relation">{{ item.relation || '' }}</text>
+      </view>
+    </view>
+
+    <!-- 健康告警 -->
+    <view class="section-card" v-if="healthAlerts.length > 0">
+      <view class="section-header">
+        <text class="section-title">⚠️ 健康告警</text>
+      </view>
+      <view class="alert-item" v-for="alert in healthAlerts" :key="alert.id">
+        <text class="alert-title">{{ alert.title || alert.content }}</text>
+      </view>
+    </view>
+
+    <!-- 里程碑 -->
+    <view class="section-card" v-if="milestones.length > 0">
+      <view class="section-header">
+        <text class="section-title">🎯 里程碑</text>
+      </view>
+      <view class="milestone-item" v-for="ms in milestones" :key="ms.id">
+        <text class="milestone-title">{{ ms.title || ms.name }}</text>
+        <text class="milestone-date">{{ formatDate(ms.dueDate) }}</text>
+      </view>
+    </view>
+
+    <!-- 加载中 -->
+    <view class="loading" v-if="loading">加载中...</view>
+  </view>
+</template>
+```
+
+- [ ] **Step 2: 添加 mind-section.vue script**
+
+```html
+<script>
+import { getContactList, getHealthAlerts, getMilestones, getFamilySummary } from '../../utils/api.js'
+
+export default {
+  props: {
+    memberId: [Number, String],
+    childId: [Number, String],
+    memberType: String,
+    memberInfo: Object,
+    sandboxData: Object,
+    energyMap: Object
+  },
+  data: function() {
+    return {
+      loading: true,
+      contactList: [],
+      healthAlerts: [],
+      milestones: [],
+      relationshipScores: []
+    }
+  },
+  mounted: function() {
+    this.loadData()
+  },
+  methods: {
+    loadData: function() {
+      var self = this
+      var familyId = uni.getStorageSync('familyId')
+      if (!familyId) {
+        var userInfo = uni.getStorageSync('userInfo')
+        if (userInfo && userInfo.familyId) familyId = userInfo.familyId
+      }
+
+      // 从 energyMap 构建关系评分
+      if (this.energyMap && this.sandboxData && this.sandboxData.members) {
+        var scores = []
+        var selfId = this.memberId
+        for (var i = 0; i < this.sandboxData.members.length; i++) {
+          var m = this.sandboxData.members[i]
+          var mid = m.memberId || m.id
+          if (mid != selfId) {
+            var e = this.energyMap[mid]
+            var trust = (e && e.bodyScore) ? Math.round((e.bodyScore + (e.mindScore || 50)) / 2) : 70
+            scores.push({ memberId: mid, nickname: m.name || m.nickname || '成员', trust: trust })
+          }
+        }
+        self.relationshipScores = scores
+      }
+
+      if (familyId) {
+        getContactList({ memberId: this.memberId }).then(function(res) {
+          if (res.code === 200) {
+            self.contactList = Array.isArray(res.data) ? res.data : []
+          }
+        }).catch(function() {})
+
+        getHealthAlerts(familyId).then(function(res) {
+          if (res.code === 200) {
+            self.healthAlerts = Array.isArray(res.data) ? res.data.slice(0, 5) : []
+          }
+        }).catch(function() {})
+
+        getMilestones(familyId).then(function(res) {
+          if (res.code === 200) {
+            self.milestones = Array.isArray(res.data) ? res.data.slice(0, 5) : []
+          }
+        }).catch(function() {})
+      }
+
+      this.loading = false
+    },
+    getScoreColor: function(score) {
+      if (score >= 80) return '#10B981'
+      if (score >= 60) return '#F59E0B'
+      return '#EF4444'
+    },
+    formatDate: function(dateStr) {
+      if (!dateStr) return ''
+      var d = new Date(dateStr)
+      return d.getFullYear() + '-' + (d.getMonth()+1) + '-' + d.getDate()
+    }
+  }
+}
+</script>
+```
+
+- [ ] **Step 3: 添加样式(与 body-section 风格一致)**
+
+```html
+<style scoped>
+.mind-section {}
+.section-card { background: #fff; border-radius: 24rpx; padding: 24rpx; margin-bottom: 20rpx; box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06); }
+.section-header { margin-bottom: 16rpx; }
+.section-title { font-size: 30rpx; font-weight: bold; color: #333; }
+.relation-item { display: flex; flex-direction: row; align-items: center; padding: 12rpx 0; }
+.relation-name { width: 100rpx; font-size: 26rpx; color: #333; }
+.relation-bar-wrap { flex: 1; height: 16rpx; background: #f0f0f0; border-radius: 8rpx; margin: 0 16rpx; overflow: hidden; }
+.relation-bar { height: 100%; border-radius: 8rpx; }
+.relation-score { width: 60rpx; text-align: right; font-size: 24rpx; color: #666; }
+.contact-item, .alert-item, .milestone-item { display: flex; flex-direction: row; justify-content: space-between; padding: 16rpx 0; border-bottom: 1rpx solid #f5f5f5; }
+.contact-name, .alert-title, .milestone-title { font-size: 28rpx; color: #333; }
+.contact-relation, .milestone-date { font-size: 24rpx; color: #999; }
+.loading { text-align: center; padding: 40rpx; color: #999; }
+</style>
+```
+
+---
+
+### Task 4: 创建 wisdom-section.vue
+
+**Files:**
+- Create: `cfc-frontend/components/member-detail-sections/wisdom-section.vue`
+- Reference: `cfc-frontend/pages/wisdom/index.vue` 的 `loadCognitiveReport`, `getAssessmentHistory`
+
+- [ ] **Step 1: 创建 wisdom-section.vue 模板**
+
+```html
+<template>
+  <view class="wisdom-section">
+    <!-- 最新测评结果 -->
+    <view class="section-card" v-if="latestAssessment">
+      <view class="section-header">
+        <text class="section-title">🧠 最新测评结果</text>
+      </view>
+      <view class="assessment-card" @click="goAssessmentDetail">
+        <text class="assessment-date">测评日期:{{ formatDate(latestAssessment.createTime) }}</text>
+        <view class="assessment-score-row">
+          <view class="score-item" v-for="(score, key) in dimensionScores" :key="key">
+            <text class="score-num" :style="{ color: '#6366F1' }">{{ score }}</text>
+            <text class="score-label">{{ key }}</text>
+          </view>
+        </view>
+      </view>
+    </view>
+    <view class="section-card" v-else-if="!loading && !childId">
+      <view class="empty-state">
+        <text class="empty-text">该成员暂无测评数据</text>
+      </view>
+    </view>
+
+    <!-- 近期测评历史 -->
+    <view class="section-card" v-if="assessmentHistory.length > 0">
+      <view class="section-header">
+        <text class="section-title">📊 近期测评历史</text>
+      </view>
+      <view class="history-item" v-for="item in assessmentHistory" :key="item.id">
+        <text class="history-name">{{ item.assessmentName || '心智测评' }}</text>
+        <text class="history-score">{{ item.totalScore || '--' }}分</text>
+        <text class="history-date">{{ formatDate(item.createTime) }}</text>
+      </view>
+    </view>
+
+    <!-- 加载中 -->
+    <view class="loading" v-if="loading">加载中...</view>
+  </view>
+</template>
+```
+
+- [ ] **Step 2: 添加 wisdom-section.vue script**
+
+```html
+<script>
+import { getAssessmentLatestResult, getAssessmentHistory } from '../../utils/api.js'
+
+export default {
+  props: {
+    memberId: [Number, String],
+    childId: [Number, String],
+    memberType: String,
+    memberInfo: Object,
+    sandboxData: Object,
+    energyMap: Object
+  },
+  data: function() {
+    return {
+      loading: true,
+      latestAssessment: null,
+      assessmentHistory: []
+    }
+  },
+  computed: {
+    dimensionScores: function() {
+      if (!this.latestAssessment) return {}
+      var result = {}
+      var keys = ['iq', 'eq', 'memory', 'logic', 'creativity', 'concentration']
+      for (var i = 0; i < keys.length; i++) {
+        var k = keys[i]
+        if (this.latestAssessment[k] != null) {
+          result[k === 'iq' ? '智商' : k === 'eq' ? '情商' : k === 'memory' ? '记忆' : k === 'logic' ? '逻辑' : k === 'creativity' ? '创造' : k === 'concentration' ? '专注' : k] = this.latestAssessment[k]
+        }
+      }
+      return result
+    }
+  },
+  mounted: function() {
+    if (this.childId) {
+      this.loadData()
+    } else {
+      this.loading = false
+    }
+  },
+  methods: {
+    loadData: function() {
+      var self = this
+
+      getAssessmentLatestResult(this.childId).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.latestAssessment = res.data
+        }
+      }).catch(function(e) { console.log('加载测评结果失败', e) })
+
+      getAssessmentHistory(this.childId, 3).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.assessmentHistory = Array.isArray(res.data) ? res.data : []
+        }
+      }).catch(function(e) { console.log('加载测评历史失败', e) })
+
+      this.loading = false
+    },
+    formatDate: function(dateStr) {
+      if (!dateStr) return ''
+      var d = new Date(dateStr)
+      return d.getFullYear() + '-' + (d.getMonth()+1) + '-' + d.getDate()
+    },
+    goAssessmentDetail: function() {
+      uni.navigateTo({ url: '/pages/assessment/report?childId=' + this.childId })
+    }
+  }
+}
+</script>
+```
+
+- [ ] **Step 3: 添加样式**
+
+```html
+<style scoped>
+.wisdom-section {}
+.section-card { background: #fff; border-radius: 24rpx; padding: 24rpx; margin-bottom: 20rpx; box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06); }
+.section-header { margin-bottom: 16rpx; }
+.section-title { font-size: 30rpx; font-weight: bold; color: #333; }
+.assessment-card { padding: 10rpx 0; }
+.assessment-date { font-size: 24rpx; color: #999; }
+.assessment-score-row { display: flex; flex-direction: row; flex-wrap: wrap; margin-top: 16rpx; }
+.score-item { display: flex; flex-direction: column; align-items: center; min-width: 80rpx; margin: 8rpx 16rpx 8rpx 0; }
+.score-num { font-size: 36rpx; font-weight: bold; }
+.score-label { font-size: 22rpx; color: #999; margin-top: 4rpx; }
+.history-item { display: flex; flex-direction: row; justify-content: space-between; align-items: center; padding: 14rpx 0; border-bottom: 1rpx solid #f5f5f5; }
+.history-name { font-size: 28rpx; color: #333; flex: 1; }
+.history-score { font-size: 28rpx; color: #6366F1; font-weight: bold; margin: 0 16rpx; }
+.history-date { font-size: 22rpx; color: #999; }
+.empty-state { padding: 30rpx 0; text-align: center; }
+.empty-text { font-size: 28rpx; color: #999; }
+.loading { text-align: center; padding: 40rpx; color: #999; }
+</style>
+```
+
+---
+
+### Task 5: 创建 action-section.vue
+
+**Files:**
+- Create: `cfc-frontend/components/member-detail-sections/action-section.vue`
+- Reference: `cfc-frontend/pages/action/index.vue`
+
+- [ ] **Step 1: 创建 action-section.vue 模板**
+
+```html
+<template>
+  <view class="action-section">
+    <!-- 今日任务(仅孩子) -->
+    <view class="section-card" v-if="childId && tasks.length > 0">
+      <view class="section-header">
+        <text class="section-title">📋 今日任务</text>
+      </view>
+      <view class="task-item" v-for="task in tasks" :key="task.id" @click="goTaskDetail(task)">
+        <view class="task-left">
+          <text class="task-name">{{ task.title || task.name }}</text>
+          <text class="task-desc">{{ task.description || '' }}</text>
+        </view>
+        <text class="task-status" :class="'status-' + (task.status || 'pending')">{{ task.statusLabel || task.status }}</text>
+      </view>
+    </view>
+
+    <!-- 打卡统计 -->
+    <view class="section-card" v-if="checkinStats">
+      <view class="section-header">
+        <text class="section-title">🏃 打卡统计</text>
+      </view>
+      <view class="stats-row">
+        <view class="stat-item">
+          <text class="stat-num">{{ checkinStats.totalDays || 0 }}</text>
+          <text class="stat-label">累计天数</text>
+        </view>
+        <view class="stat-item">
+          <text class="stat-num">{{ checkinStats.currentStreak || 0 }}</text>
+          <text class="stat-label">连续天数</text>
+        </view>
+        <view class="stat-item">
+          <text class="stat-num">{{ checkinStats.completionRate || '0%' }}</text>
+          <text class="stat-label">完成率</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 最近活动 -->
+    <view class="section-card" v-if="activities.length > 0">
+      <view class="section-header">
+        <text class="section-title">🎪 最近活动</text>
+      </view>
+      <view class="activity-item" v-for="act in activities" :key="act.id" @click="goActivityDetail(act)">
+        <text class="activity-title">{{ act.title || act.name }}</text>
+        <text class="activity-date">{{ formatDate(act.startTime || act.createTime) }}</text>
+      </view>
+    </view>
+
+    <!-- 加载中 -->
+    <view class="loading" v-if="loading">加载中...</view>
+  </view>
+</template>
+```
+
+- [ ] **Step 2: 添加 action-section.vue script**
+
+```html
+<script>
+import { getTodayTasksByCategory, getCheckinStats, getActivityList } from '../../utils/api.js'
+
+export default {
+  props: {
+    memberId: [Number, String],
+    childId: [Number, String],
+    memberType: String,
+    memberInfo: Object,
+    sandboxData: Object,
+    energyMap: Object
+  },
+  data: function() {
+    return {
+      loading: true,
+      tasks: [],
+      checkinStats: null,
+      activities: []
+    }
+  },
+  mounted: function() {
+    this.loadData()
+  },
+  methods: {
+    loadData: function() {
+      var self = this
+
+      if (this.childId) {
+        getTodayTasksByCategory(this.childId, 'all').then(function(res) {
+          if (res && res.data) {
+            self.tasks = Array.isArray(res.data) ? res.data.slice(0, 5) : []
+          }
+        }).catch(function() {})
+
+        getCheckinStats({ childId: this.childId }).then(function(res) {
+          if (res.code === 200 && res.data) {
+            self.checkinStats = res.data
+          }
+        }).catch(function() {})
+      }
+
+      getActivityList({ page: 1, size: 3 }).then(function(res) {
+        if (res && res.data) {
+          var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
+          self.activities = list.slice(0, 3)
+        }
+      }).catch(function() {})
+
+      this.loading = false
+    },
+    formatDate: function(dateStr) {
+      if (!dateStr) return ''
+      var d = new Date(dateStr)
+      return d.getFullYear() + '-' + (d.getMonth()+1) + '-' + d.getDate()
+    },
+    goTaskDetail: function(task) {
+      uni.navigateTo({ url: '/pages/tasks/tasks' })
+    },
+    goActivityDetail: function(act) {
+      if (act && act.id) {
+        uni.navigateTo({ url: '/pages/discover-detail/activity-detail/activity-detail?id=' + act.id })
+      }
+    }
+  }
+}
+</script>
+```
+
+- [ ] **Step 3: 添加样式**
+
+```html
+<style scoped>
+.action-section {}
+.section-card { background: #fff; border-radius: 24rpx; padding: 24rpx; margin-bottom: 20rpx; box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06); }
+.section-header { margin-bottom: 16rpx; }
+.section-title { font-size: 30rpx; font-weight: bold; color: #333; }
+.task-item { display: flex; flex-direction: row; justify-content: space-between; align-items: center; padding: 14rpx 0; border-bottom: 1rpx solid #f5f5f5; }
+.task-left { flex: 1; }
+.task-name { font-size: 28rpx; color: #333; }
+.task-desc { font-size: 22rpx; color: #999; margin-top: 4rpx; }
+.task-status { font-size: 24rpx; padding: 4rpx 12rpx; border-radius: 8rpx; }
+.status-pending { background: #FEF3C7; color: #D97706; }
+.status-completed { background: #D1FAE5; color: #059669; }
+.stats-row { display: flex; flex-direction: row; justify-content: space-around; padding: 16rpx 0; }
+.stat-item { display: flex; flex-direction: column; align-items: center; }
+.stat-num { font-size: 40rpx; font-weight: bold; color: #10B981; }
+.stat-label { font-size: 22rpx; color: #999; margin-top: 4rpx; }
+.activity-item { display: flex; flex-direction: row; justify-content: space-between; padding: 14rpx 0; border-bottom: 1rpx solid #f5f5f5; }
+.activity-title { font-size: 28rpx; color: #333; flex: 1; }
+.activity-date { font-size: 22rpx; color: #999; }
+.loading { text-align: center; padding: 40rpx; color: #999; }
+</style>
+```
+
+---
+
+### Task 6: 验证编译
+
+**Files:**
+- Project root: `cfc-backend/` (not needed, frontend only)
+
+- [ ] **Step 1: 检查文件结构**
+
+```bash
+ls -la cfc-frontend/components/member-detail-sections/
+ls -la cfc-frontend/pages/member-detail/
+```
+
+- [ ] **Step 2: 检查 member-detail.vue 无语法错误**
+
+确认 imports、components、computed 等都已正确添加。
+
+- [ ] **Step 3: 确认页面路由不冲突**
+
+```bash
+grep -n 'member-detail' cfc-frontend/pages.json
+```
+
+---
+
+### 自检清单
+
+1. **Spec 覆盖度**:
+   - [ ] 4 个子组件都对应到 spec 中的各维度内容定义
+   - [ ] 动态组件机制已实现(Task 1)
+   - [ ] 共用部分保持不变
+   - [ ] childId 可用性处理(非孩子成员显示空状态)
+
+2. **占位符检查**: 无 "TBD", "TODO", "implement later"
+
+3. **类型一致性**:
+   - [ ] `memberId`, `childId`, `memberType` props 在 4 个子组件中命名一致
+   - [ ] `energyMap` prop 与 member-detail.vue 中的 `energyMapForGraph` 匹配
+
+4. **Scope 检查**: 范围聚焦于 member-detail 页面的内容区改造,不涉及导航、路由、后端改动。

+ 113 - 0
docs/superpowers/specs/2026-07-14-member-detail-dimension-sections.md

@@ -0,0 +1,113 @@
+# 家庭成员详情页 — 维度专属内容区设计
+
+## 概述
+
+当前 `pages/member-detail/member-detail.vue` 从身/心/智/行四个维度页面的 FamilyRelationGraph 点击成员进入,但展示内容不区分入口维度。本设计让该页面根据 `dimensionCode` 参数展示与维度相关的专属内容,实现"从哪个页面进,显示该页面相关的内容"。
+
+## 现状
+
+- `member-detail.vue` 已接收 `dimensionCode` 参数(body/mind/wisdom/action)
+- 页面现有结构:导航栏 + 成员信息卡 + 五维能量条 + 关系图谱 + 通用任务/活动/商品
+- 通用内容(活动/商品推荐)不区分维度,始终展示 body 域内容
+
+## 设计
+
+### 页面结构(所有维度共用)
+
+```
+┌─ 顶部导航栏(返回按钮 + "成员名 · 维度名")
+├─ 成员信息卡(头像 + 昵称/角色 + 综合能量分)
+├─ 五维能量条 (FamilyEnergyBar)
+├─ 家庭成员关系图谱 (FamilyRelationGraph)
+├─ ══ 维度专属内容区(根据 dimensionCode 切换)══
+│   └─ (详见各维度内容定义)
+└─ 底部占位
+```
+
+### 架构
+
+- `member-detail.vue` 作为容器页面,负责所有共用部分的渲染
+- 在关系图谱下方使用 `<component :is="dimensionSection" />` 动态组件
+- 4 个独立子组件放在 `components/member-detail-sections/` 目录下
+
+### 各维度专属内容
+
+#### 身 (body-section)
+
+| 内容块 | 数据来源 | 说明 |
+|--------|---------|------|
+| 最新健康报告卡片 | `getHealthAnalysis(childId)` | 总分 + 关键指标 + 报告日期,无报告时显示"暂无健康报告" |
+| 七维健康雷达图 | `getDimensionOverview({ childId })` | 与 body/index.vue 相同的 RadarChart 组件 |
+| 健康指标列表 | `getHealthIndicatorList(childId)` | 关键指标展示 |
+| 最近打卡记录 | `getCheckinList({ childId, page:1, size:3 })` | 最近3条健康打卡 |
+
+#### 心 (mind-section)
+
+| 内容块 | 数据来源 | 说明 |
+|--------|---------|------|
+| 亲密关系评分 | `intimacyMap` + `energyMap` | 用进度条或评分卡展示该成员与各家庭成员的关系质量 |
+| 联系人列表 | `getContactList({ memberId })` | 该成员的联系人列表 |
+| 健康告警 | `getHealthAlerts(familyId)` | 家庭健康提醒 |
+| 里程碑 | `getMilestones(familyId)` | 近期待办里程碑 |
+
+#### 智 (wisdom-section)
+
+| 内容块 | 数据来源 | 说明 |
+|--------|---------|------|
+| 最新测评结果 | `getAssessmentLatestResult(childId)` | 总分 + 各维度分数,与 wisdom/index.vue 风格一致 |
+| 认知能力维度图 | 从测评结果提取 | 用雷达图或柱状图展示各认知维度得分 |
+| 近期测评历史 | `getAssessmentHistory(childId, 3)` | 最近3次测评简表 |
+| 认知趋势 | `getCognitiveTrend(childId, 5)` | 趋势图(如有需要) |
+
+#### 行 (action-section)
+
+| 内容块 | 数据来源 | 说明 |
+|--------|---------|------|
+| 今日任务 | `getTodayTasksByCategory(childId, 'all')` | 已存在的 DimensionTasks 组件,仅孩子类型显示 |
+| 打卡统计 | `getCheckinStats({ childId })` | 行为打卡汇总统计 |
+| 最近活动 | `getActivityList({ page:1, size:3 })` | 推荐活动 |
+
+### 数据加载策略
+
+- `member-detail.vue` 的 `onLoad` 仍加载共用数据(沙盘、成员信息)
+- 维度专属数据在 `dimensionSection` 子组件的 `onShow`/`mounted` 中各自加载
+- 子组件通过 props 接收:`memberId`, `childId`, `memberType`, `sandboxData`, `energyMap`
+- `familyId` 从 `uni.getStorageSync('familyId')` 获取(项目内统一模式)
+- **childId 可用性**:`childId` 仅在 `memberType === 'child'` 时有值。身/智维度依赖健康数据和测评数据,这些数据仅对孩子成员有意义——非孩子成员时显示"该成员暂无维度数据"的提示。心/行维度的部分内容(联系人、告警、里程碑)对全年龄段成员可用。
+
+### 错误处理
+
+- 各子组件独立处理自己的加载失败,不阻塞共用部分的渲染
+- 加载中显示骨架屏或 loading 文本
+- 无数据时显示空状态提示
+
+### 文件清单(新增)
+
+```
+cfc-frontend/components/member-detail-sections/
+├── body-section.vue       # 身维度专属内容
+├── mind-section.vue       # 心维度专属内容
+├── wisdom-section.vue     # 智维度专属内容
+└── action-section.vue     # 行维度专属内容
+```
+
+### 修改文件
+
+```
+cfc-frontend/pages/member-detail/member-detail.vue  # 引入动态组件区
+```
+
+## 排除范围
+
+- 不修改各维度页面(parent-index、body、mind、wisdom、action)的 `goMemberDetail`——它们已经传了 `dimensionCode`
+- 不修改 FamilyRelationGraph 组件
+- 不改动共用部分(导航、成员卡、能量条、关系图谱)的现有逻辑
+
+## 成功标准
+
+1. 从**身**页面点击成员 → 进入后看到健康相关数据(报告/雷达图/指标)
+2. 从**心**页面点击成员 → 进入后看到关系/联系人/告警数据
+3. 从**智**页面点击成员 → 进入后看到测评/认知数据
+4. 从**行**页面点击成员 → 进入后看到任务/打卡/活动数据
+5. 共用部分在所有维度下表现一致
+6. 各专属内容区独立加载,单个模块失败不影响其他区域