Sfoglia il codice sorgente

feat(cfc): DAN测评市场转化全链路实现(Phase 1-5)

Phase 1 - 测评商品管理:
- AssessmentProduct实体/Mapper/Service/Controller
- 管理端测评商品管理页面 + 规划师端测评商品页
- 测评额度配额管理与扣减服务
- 商品SKU/促销层级/推荐树实体与迁移

Phase 2 - 购买与执行解耦:
- 预约-支付-执行分离流程
- AssessmentQuotaService额度扣减逻辑
- 预约确认/取消/支付回调
- 前端预约页+额度页+上传页

Phase 3 - 报告双通道+结构化指导:
- AssessmentReportController(上传/确认/指导意见)
- GrowthGuidanceService结构化指导意见
- ReportParseService PDF解析
- 家长上传报告+规划师录入结果双通道

Phase 4 - 方案生成引擎:
- AssessmentPlanRule规则引擎(维度×分数区间匹配)
- AssessmentPlanGenerator自动方案生成
- AdminPlanRuleController规则CRUD
- 迁移63-66: assessment_plan_rule建表 + TaskPlanInstance扩展字段
- 管理端方案规则配置页 + 方案管理看板

Phase 5 - 方案审核与执行:
- PlanReviewController(规划师方案列表/审核/激活)
- PlanActivationService激活生成待办任务
- 规划师小程序方案审核页+详情页
- 修复既存asyncDeepParse编译错误

新增: 会员订阅/积分体系/商城商品管理等基础设施
Xiaogang Liao 2 mesi fa
parent
commit
0de92f32ff
71 ha cambiato i file con 5453 aggiunte e 94 eliminazioni
  1. 273 2
      cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  2. 42 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminPlanRuleController.java
  3. 9 6
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductController.java
  4. 24 20
      cfc-backend/src/main/java/com/etotem/cfc/controller/assessment/AssessmentAppointmentController.java
  5. 2 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/assessment/AssessmentOrderController.java
  6. 80 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/assessment/AssessmentQuotaController.java
  7. 150 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/assessment/AssessmentReportController.java
  8. 109 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/PlanReviewController.java
  9. 120 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/subscription/SubscriptionController.java
  10. 23 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/AssessmentQuotaVO.java
  11. 11 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/ProductCreateRequest.java
  12. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/ProductDTO.java
  13. 41 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/AssessmentExecution.java
  14. 33 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/AssessmentPlanRule.java
  15. 31 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/AssessmentProduct.java
  16. 43 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/AssessmentQuota.java
  17. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/DanAssessmentResult.java
  18. 33 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/GrowthGuidance.java
  19. 35 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/MemberSubscription.java
  20. 35 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/MemberSubscriptionOrder.java
  21. 5 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/Product.java
  22. 29 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/ProductPpoint.java
  23. 41 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/PromotionTier.java
  24. 27 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/PromotionTierChangeLog.java
  25. 27 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/ReferralTree.java
  26. 25 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/SubscriptionBenefitLog.java
  27. 18 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/TaskPlanInstance.java
  28. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/AssessmentExecutionMapper.java
  29. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/AssessmentPlanRuleMapper.java
  30. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/AssessmentProductMapper.java
  31. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/AssessmentQuotaMapper.java
  32. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/GrowthGuidanceMapper.java
  33. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/MemberSubscriptionMapper.java
  34. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/MemberSubscriptionOrderMapper.java
  35. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductPpointMapper.java
  36. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/PromotionTierChangeLogMapper.java
  37. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/PromotionTierMapper.java
  38. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/ReferralTreeMapper.java
  39. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/SubscriptionBenefitLogMapper.java
  40. 54 0
      cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentExecutionService.java
  41. 106 0
      cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentPlanGenerator.java
  42. 62 0
      cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentPlanRuleService.java
  43. 35 0
      cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentProductService.java
  44. 93 0
      cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentQuotaService.java
  45. 63 0
      cfc-backend/src/main/java/com/etotem/cfc/service/FileStorageService.java
  46. 56 0
      cfc-backend/src/main/java/com/etotem/cfc/service/GrowthGuidanceService.java
  47. 210 0
      cfc-backend/src/main/java/com/etotem/cfc/service/MemberSubscriptionService.java
  48. 87 0
      cfc-backend/src/main/java/com/etotem/cfc/service/PlanActivationService.java
  49. 23 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java
  50. 43 1
      cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java
  51. 149 0
      cfc-backend/src/main/java/com/etotem/cfc/service/PromotionTierService.java
  52. 4 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ReportParseService.java
  53. 184 0
      cfc-backend/src/main/resources/schema.sql
  54. 16 0
      cfc-frontend/pages.json
  55. 82 56
      cfc-frontend/pages/assessment/apply.vue
  56. 240 0
      cfc-frontend/pages/assessment/quota.vue
  57. 219 0
      cfc-frontend/pages/assessment/upload-report.vue
  58. 113 0
      cfc-frontend/pages/guide/plans/detail.vue
  59. 188 0
      cfc-frontend/pages/guide/plans/index.vue
  60. 557 0
      cfc-frontend/pages/membership/benefits.vue
  61. 429 0
      cfc-frontend/pages/membership/plans.vue
  62. 52 0
      cfc-frontend/utils/api.js
  63. 137 0
      cfc-web/src/api/admin.js
  64. 13 0
      cfc-web/src/api/assessment.js
  65. 18 0
      cfc-web/src/router/index.js
  66. 113 0
      cfc-web/src/views/admin/AssessmentPlanRules.vue
  67. 197 0
      cfc-web/src/views/admin/AssessmentProducts.vue
  68. 119 0
      cfc-web/src/views/admin/PlanManagement.vue
  69. 64 7
      cfc-web/src/views/admin/ProductEdit.vue
  70. 2 1
      cfc-web/src/views/admin/ProductManage.vue
  71. 343 0
      cfc-web/src/views/admin/SubscriptionManagement.vue

+ 273 - 2
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -4641,5 +4641,276 @@ try {
                 "VALUES (?, ?, ?, ?, ?, 'active', NOW(), NOW())",
                 pageKey, sectionKey, title, allowedRoles, sortOrder);
         }
-    }
-}
+// 迁移51: 创建 member_subscription 表(家庭订阅记录)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS member_subscription (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
+                "`level` VARCHAR(10) NOT NULL DEFAULT 'L1' COMMENT 'L1=一生一世, L2=久久一生', " +
+                "status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT 'active/expired/cancelled', " +
+                "start_time DATETIME DEFAULT NULL COMMENT '开始时间', " +
+                "expire_time DATETIME DEFAULT NULL COMMENT '到期时间', " +
+                "payment_id BIGINT DEFAULT NULL COMMENT '关联订单ID', " +
+                "auto_renew TINYINT(1) DEFAULT 0 COMMENT '自动续费(L2)', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                "INDEX idx_family_id (family_id), " +
+                "INDEX idx_status (status), " +
+                "INDEX idx_expire_time (expire_time)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭订阅记录'");
+            log.info("已创建member_subscription表");
+        } catch (Exception e) {
+            log.warn("创建member_subscription表失败: {}", e.getMessage());
+        }
+
+        // 迁移52: 创建 member_subscription_order 表(订阅订单)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS member_subscription_order (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
+                "`level` VARCHAR(10) NOT NULL COMMENT 'L1/L2', " +
+                "amount INT NOT NULL COMMENT '金额(分)', " +
+                "status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending/paid/cancelled/refunded', " +
+                "payment_type VARCHAR(20) DEFAULT NULL COMMENT '支付方式 wechat/alipay', " +
+                "transaction_id VARCHAR(64) DEFAULT NULL COMMENT '微信支付订单号', " +
+                "order_no VARCHAR(64) NOT NULL COMMENT '订单号', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "paid_at DATETIME DEFAULT NULL, " +
+                "UNIQUE KEY uk_order_no (order_no), " +
+                "INDEX idx_family_id (family_id), " +
+                "INDEX idx_status (status)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订阅订单'");
+            log.info("已创建member_subscription_order表");
+        } catch (Exception e) {
+            log.warn("创建member_subscription_order表失败: {}", e.getMessage());
+        }
+
+        // 迁移53: 创建 subscription_benefit_log 表(权益使用日志)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS subscription_benefit_log (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "subscription_id BIGINT NOT NULL COMMENT '订阅ID', " +
+                "benefit_code VARCHAR(50) NOT NULL COMMENT '权益代码', " +
+                "used_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "detail TEXT COMMENT '使用详情(JSON)', " +
+                "INDEX idx_subscription_id (subscription_id)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权益使用日志'");
+            log.info("已创建subscription_benefit_log表");
+        } catch (Exception e) {
+            log.warn("创建subscription_benefit_log表失败: {}", e.getMessage());
+        }
+
+        // 迁移54: 创建 promotion_tier 表(推广等级 R0-R4)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS promotion_tier (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "user_id BIGINT NOT NULL COMMENT '用户ID', " +
+                "tier VARCHAR(20) NOT NULL DEFAULT 'R0' COMMENT 'R0利他者/R1传福人/R2聚能师/R3启慧者/R4传承者', " +
+                "team_size_1st INT DEFAULT 0 COMMENT '一层团队人数', " +
+                "team_size_2nd INT DEFAULT 0 COMMENT '二层团队人数', " +
+                "team_size_3rd INT DEFAULT 0 COMMENT '三层团队人数', " +
+                "total_team_size INT DEFAULT 0 COMMENT '三层累计总人数', " +
+                "total_referral_earnings INT DEFAULT 0 COMMENT '累计推荐佣金(分)', " +
+                "total_share_earnings INT DEFAULT 0 COMMENT '累计消费分润(分)', " +
+                "promoted_at DATETIME DEFAULT NULL COMMENT '最近晋级时间', " +
+                "last_change_at DATETIME DEFAULT NULL COMMENT '最后变更时间', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                "UNIQUE KEY uk_user_id (user_id), " +
+                "INDEX idx_tier (tier)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推广等级'");
+            log.info("已创建promotion_tier表");
+        } catch (Exception e) {
+            log.warn("创建promotion_tier表失败: {}", e.getMessage());
+        }
+
+        // 迁移55: 创建 promotion_tier_change_log 表(推广等级变更记录)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS promotion_tier_change_log (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "user_id BIGINT NOT NULL COMMENT '用户ID', " +
+                "old_tier VARCHAR(20) DEFAULT NULL COMMENT '变更前等级', " +
+                "new_tier VARCHAR(20) NOT NULL COMMENT '变更后等级', " +
+                "reason VARCHAR(50) DEFAULT NULL COMMENT 'qualify升级/demote降级/admin管理员调整', " +
+                "changed_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "INDEX idx_user_id (user_id)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推广等级变更记录'");
+            log.info("已创建promotion_tier_change_log表");
+        } catch (Exception e) {
+            log.warn("创建promotion_tier_change_log表失败: {}", e.getMessage());
+        }
+
+        // 迁移56: 创建 referral_tree 表(推荐关系树三层)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS referral_tree (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "parent_id BIGINT NOT NULL COMMENT '推荐人ID', " +
+                "child_id BIGINT NOT NULL COMMENT '被推荐人ID', " +
+                "level INT NOT NULL COMMENT '层级深度(1/2/3)', " +
+                "path VARCHAR(255) DEFAULT NULL COMMENT 'materialized path如/1/2/3/', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "UNIQUE KEY uk_child_id (child_id), " +
+                "INDEX idx_parent_id (parent_id), " +
+                "INDEX idx_level (level)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推荐关系树'");
+            log.info("已创建referral_tree表");
+        } catch (Exception e) {
+            log.warn("创建referral_tree表失败: {}", e.getMessage());
+        }
+
+        // 迁移57: 创建 product_ppoint 表(产品P点配置)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS product_ppoint (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "product_id BIGINT NOT NULL COMMENT '产品ID', " +
+                "ppoint INT NOT NULL DEFAULT 0 COMMENT 'P点值(平台利润分)', " +
+                "start_date DATE DEFAULT NULL COMMENT '生效日期', " +
+                "end_date DATE DEFAULT NULL COMMENT '失效日期', " +
+                "created_by BIGINT DEFAULT NULL COMMENT '创建人', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "INDEX idx_product_id (product_id), " +
+                "INDEX idx_dates (start_date, end_date)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='产品P点配置'");
+            log.info("已创建product_ppoint表");
+        } catch (Exception e) {
+            log.warn("创建product_ppoint表失败: {}", e.getMessage());
+        }
+
+        // 迁移58: 创建 assessment_products 表(测评商品扩展信息)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS assessment_products (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "product_id BIGINT NOT NULL COMMENT '关联products表', " +
+                "assessment_type VARCHAR(20) NOT NULL COMMENT 'single=单包, bundle=套餐', " +
+                "total_sessions INT NOT NULL DEFAULT 1 COMMENT '包含测评次数', " +
+                "validity_days INT NOT NULL DEFAULT 365 COMMENT '有效期(天)', " +
+                "guide_scope VARCHAR(50) DEFAULT 'all' COMMENT '规划师范围: all/assigned/auto', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                "INDEX idx_product_id (product_id)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='测评商品扩展信息'");
+            log.info("已创建assessment_products表");
+        } catch (Exception e) {
+            log.warn("创建assessment_products表失败: {}", e.getMessage());
+        }
+
+        // 迁移59: 创建 assessment_quotas 表(测评额度)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS assessment_quotas (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "quota_no VARCHAR(32) NOT NULL COMMENT '额度编号', " +
+                "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
+                "child_id BIGINT COMMENT '指定孩子ID(NULL=不限)', " +
+                "product_order_id BIGINT NOT NULL COMMENT '来源订单ID', " +
+                "product_id BIGINT NOT NULL COMMENT '商品ID', " +
+                "product_name VARCHAR(100) NOT NULL COMMENT '商品名快照', " +
+                "total_sessions INT NOT NULL DEFAULT 1 COMMENT '总次数', " +
+                "remaining_sessions INT NOT NULL DEFAULT 1 COMMENT '剩余次数', " +
+                "validity_start DATETIME NOT NULL COMMENT '有效期开始', " +
+                "validity_end DATETIME NOT NULL COMMENT '有效期截止', " +
+                "status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT 'active/expired/used_all/cancelled', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                "INDEX idx_family (family_id), " +
+                "INDEX idx_order (product_order_id), " +
+                "UNIQUE KEY uk_quota_no (quota_no)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='测评额度表'");
+            log.info("已创建assessment_quotas表");
+        } catch (Exception e) {
+            log.warn("创建assessment_quotas表失败: {}", e.getMessage());
+        }
+
+        // 迁移60: 创建 assessment_executions 表(测评执行记录)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS assessment_executions (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "quota_id BIGINT NOT NULL COMMENT '关联额度ID', " +
+                "child_id BIGINT NOT NULL COMMENT '被测评孩子', " +
+                "guide_id BIGINT COMMENT '执行规划师ID', " +
+                "assessment_type VARCHAR(30) COMMENT '本次具体测评类型', " +
+                "appointment_id BIGINT COMMENT '关联预约ID', " +
+                "result_id BIGINT COMMENT '关联测评结果ID', " +
+                "status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending/in_progress/completed/cancelled', " +
+                "scheduled_date DATETIME COMMENT '预约执行时间', " +
+                "completed_at DATETIME COMMENT '实际完成时间', " +
+                "notes VARCHAR(500) COMMENT '备注', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                "INDEX idx_quota (quota_id), " +
+                "INDEX idx_child (child_id), " +
+                "INDEX idx_appointment (appointment_id)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='测评执行记录表'");
+            log.info("已创建assessment_executions表");
+        } catch (Exception e) {
+            log.warn("创建assessment_executions表失败: {}", e.getMessage());
+        }
+
+        // 迁移61: dan_assessment_results表添加source字段(报告来源)
+        ensureColumn("dan_assessment_results", "source",
+                "VARCHAR(20) DEFAULT 'planner_entry' COMMENT '报告来源: planner_entry/parent_upload/auto_fetch'");
+
+        // 迁移62: 创建 growth_guidances 表(结构化指导意见)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS growth_guidances (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "result_id BIGINT NOT NULL COMMENT '关联测评结果ID', " +
+                "dimension VARCHAR(30) NOT NULL COMMENT '维度: attention/focus/memory/logic/emotion/general', " +
+                "score INT COMMENT '该维度得分(0-100)', " +
+                "suggestion TEXT NOT NULL COMMENT '建议内容', " +
+                "priority INT DEFAULT 0 COMMENT '优先级(0=普通, 1=重要, 2=紧急)', " +
+                "sort_order INT DEFAULT 0 COMMENT '排序', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                "INDEX idx_result (result_id)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='测评指导意见(结构化)'");
+            log.info("已创建growth_guidances表");
+        } catch (Exception e) {
+            log.warn("创建growth_guidances表失败: {}", e.getMessage());
+        }
+
+        // 迁移63: 创建 assessment_plan_rules 表(方案生成规则)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS assessment_plan_rules (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "dimension VARCHAR(30) NOT NULL COMMENT '测评维度: attention/focus/memory/logic/emotion', " +
+                "score_min INT NOT NULL COMMENT '分数下限(含)', " +
+                "score_max INT NOT NULL COMMENT '分数上限(含)', " +
+                "template_id BIGINT NOT NULL COMMENT '关联TaskTemplatePackage ID', " +
+                "priority INT DEFAULT 0 COMMENT '优先级', " +
+                "is_active TINYINT(1) DEFAULT 1 COMMENT '是否启用', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                "INDEX idx_dimension (dimension), " +
+                "INDEX idx_score (score_min, score_max)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='方案生成规则'");
+            log.info("已创建assessment_plan_rules表");
+        } catch (Exception e) {
+            log.warn("创建assessment_plan_rules表失败: {}", e.getMessage());
+        }
+
+        // 迁移64: task_plan_instances表添加source字段(方案来源)
+        ensureColumn("task_plan_instances", "source",
+                "VARCHAR(20) DEFAULT 'manual' COMMENT '方案来源: manual/assessment/template'");
+
+        // 迁移65: task_plan_instances表添加source_result_id和remark字段
+        ensureColumn("task_plan_instances", "source_result_id",
+                "BIGINT COMMENT '关联测评结果ID(来源为assessment时)'");
+        ensureColumn("task_plan_instances", "remark",
+                "TEXT COMMENT '方案备注(含指导意见摘要)'");
+
+        // 迁移66: task_plan_instances表添加审核确认字段(阶段五)
+        ensureColumn("task_plan_instances", "reviewed_by",
+                "BIGINT COMMENT '审核人(规划师ID)'");
+        ensureColumn("task_plan_instances", "reviewed_at",
+                "DATETIME COMMENT '审核时间'");
+        ensureColumn("task_plan_instances", "review_comment",
+                "VARCHAR(500) COMMENT '审核意见'");
+        ensureColumn("task_plan_instances", "confirmed_by",
+                "BIGINT COMMENT '确认人(家长ID)'");
+        ensureColumn("task_plan_instances", "confirmed_at",
+                "DATETIME COMMENT '确认时间'");
+        ensureColumn("task_plan_instances", "activated_at",
+                "DATETIME COMMENT '激活时间'");
+
+        }
+}

+ 42 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminPlanRuleController.java

@@ -0,0 +1,42 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.AssessmentPlanRule;
+import com.etotem.cfc.service.AssessmentPlanRuleService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+@Tag(name = "管理端-方案规则", description = "方案生成规则 CRUD")
+@RestController
+@RequestMapping("/api/admin/plan-rules")
+public class AdminPlanRuleController {
+
+    @Resource
+    private AssessmentPlanRuleService ruleService;
+
+    @Operation(summary = "规则列表")
+    @PostMapping("/list")
+    public Result<List<AssessmentPlanRule>> list() {
+        return Result.success(ruleService.getAll());
+    }
+
+    @Operation(summary = "保存规则(新增/更新)")
+    @PostMapping("/save")
+    public Result<AssessmentPlanRule> save(@RequestBody AssessmentPlanRule rule) {
+        ruleService.saveRule(rule);
+        return Result.success(rule);
+    }
+
+    @Operation(summary = "删除规则")
+    @PostMapping("/delete")
+    public Result<Void> delete(@RequestBody java.util.Map<String, Object> params) {
+        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        if (id == null) return Result.error("id不能为空");
+        ruleService.deleteRule(id);
+        return Result.success(null);
+    }
+}

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

@@ -3,6 +3,7 @@ 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.dto.ProductCreateRequest;
 import com.etotem.cfc.dto.ProductDTO;
 import com.etotem.cfc.entity.Product;
 import com.etotem.cfc.service.ProductService;
@@ -116,22 +117,24 @@ public class AdminProductController {
 
     @Operation(summary = "创建商品")
     @PostMapping("/create")
-    public Result<ProductDTO> create(@RequestBody Product product,
+    public Result<ProductDTO> create(@RequestBody ProductCreateRequest request,
                                      @RequestAttribute("userId") Long adminId) {
-        if (product.getName() == null || product.getName().isEmpty()) {
+        Product product = request.getProduct();
+        if (product == null || product.getName() == null || product.getName().isEmpty()) {
             return Result.error("商品名称不能为空");
         }
-        return productService.adminCreate(product);
+        return productService.adminCreate(product, request.getAssessmentExt());
     }
 
     @Operation(summary = "更新商品")
     @PostMapping("/update")
-    public Result<ProductDTO> update(@RequestBody Product product,
+    public Result<ProductDTO> update(@RequestBody ProductCreateRequest request,
                                      @RequestAttribute("userId") Long adminId) {
-        if (product.getId() == null) {
+        Product product = request.getProduct();
+        if (product == null || product.getId() == null) {
             return Result.error("商品ID不能为空");
         }
-        return productService.adminUpdate(product);
+        return productService.adminUpdate(product, request.getAssessmentExt());
     }
 
     @Operation(summary = "删除商品")

+ 24 - 20
cfc-backend/src/main/java/com/etotem/cfc/controller/assessment/AssessmentAppointmentController.java

@@ -2,11 +2,13 @@ package com.etotem.cfc.controller.assessment;
 
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.entity.AssessmentAppointment;
-import com.etotem.cfc.entity.AssessmentOrder;
+import com.etotem.cfc.entity.AssessmentExecution;
+import com.etotem.cfc.entity.AssessmentQuota;
 import com.etotem.cfc.entity.User;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.AssessmentAppointmentService;
-import com.etotem.cfc.service.AssessmentOrderService;
+import com.etotem.cfc.service.AssessmentExecutionService;
+import com.etotem.cfc.service.AssessmentQuotaService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.web.bind.annotation.*;
@@ -26,7 +28,10 @@ public class AssessmentAppointmentController {
     private AssessmentAppointmentService appointmentService;
 
     @Resource
-    private AssessmentOrderService orderService;
+    private AssessmentQuotaService quotaService;
+
+    @Resource
+    private AssessmentExecutionService executionService;
 
     @Resource
     private UserMapper userMapper;
@@ -39,35 +44,34 @@ public class AssessmentAppointmentController {
 
         Long childId = body.get("childId") != null ? Long.valueOf(body.get("childId").toString()) : null;
         Long guideId = body.get("guideId") != null ? Long.valueOf(body.get("guideId").toString()) : null;
-        Long packageId = body.get("packageId") != null ? Long.valueOf(body.get("packageId").toString()) : null;
         String appointmentDate = body.get("appointmentDate") != null ? body.get("appointmentDate").toString() : null;
         String appointmentTime = body.get("appointmentTime") != null ? body.get("appointmentTime").toString() : null;
         String notes = body.get("notes") != null ? body.get("notes").toString() : null;
-        Long totalPrice = body.get("totalPrice") != null ? Long.valueOf(body.get("totalPrice").toString()) : 0L;
-        String guideName = body.get("guideName") != null ? body.get("guideName").toString() : null;
-        String packageName = body.get("packageName") != null ? body.get("packageName").toString() : null;
-        Long discountAmount = body.get("discountAmount") != null ? Long.valueOf(body.get("discountAmount").toString()) : 0L;
+        Long quotaId = body.get("quotaId") != null ? Long.valueOf(body.get("quotaId").toString()) : null;
+
+        if (childId == null || quotaId == null) {
+            return Result.error("childId和quotaId不能为空");
+        }
 
         User user = userMapper.selectById(userId);
         if (user == null) return Result.error("用户不存在");
 
-        AssessmentAppointment appointment = appointmentService.createAppointment(
-                userId, childId, guideId, packageId, appointmentDate, appointmentTime, notes);
+        // 消耗一次额度
+        int consumed = quotaService.consumeQuota(quotaId, childId);
+        if (consumed <= 0) {
+            return Result.error("额度不足或已过期");
+        }
 
-        Long familyId = user.getFamilyId();
-        AssessmentOrder order = orderService.createOrder(
-                familyId, userId, childId, guideId, packageId,
-                guideName, packageName, totalPrice, discountAmount);
+        AssessmentAppointment appointment = appointmentService.createAppointment(
+                userId, childId, guideId, null, appointmentDate, appointmentTime, notes);
 
-        // 关联预约与订单
-        order.setAppointmentId(appointment.getId());
-        orderService.updateById(order);
+        // 创建执行记录
+        AssessmentExecution exec = executionService.create(quotaId, childId, appointment.getId());
 
         Map<String, Object> result = new HashMap<>();
         result.put("appointmentId", appointment.getId());
-        result.put("orderNo", order.getOrderNo());
-        result.put("actualPrice", order.getActualPrice());
-        result.put("status", order.getStatus());
+        result.put("executionId", exec.getId());
+        result.put("quotaId", quotaId);
 
         return Result.success(result);
     }

+ 2 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/assessment/AssessmentOrderController.java

@@ -13,7 +13,8 @@ import javax.annotation.Resource;
 import java.util.*;
 import java.util.stream.Collectors;
 
-@Tag(name = "评估订单", description = "评估订单与支付")
+@Tag(name = "评估订单", description = "评估订单与支付(已废弃,改用商城额度系统)")
+@Deprecated
 @RestController
 @RequestMapping("/api/assessment/order")
 public class AssessmentOrderController {

+ 80 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/assessment/AssessmentQuotaController.java

@@ -0,0 +1,80 @@
+package com.etotem.cfc.controller.assessment;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.AssessmentQuotaVO;
+import com.etotem.cfc.entity.AssessmentQuota;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.AssessmentExecutionService;
+import com.etotem.cfc.service.AssessmentQuotaService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Tag(name = "测评额度", description = "测评额度查询与使用")
+@RestController
+@RequestMapping("/api/assessment/quota")
+public class AssessmentQuotaController {
+
+    @Resource
+    private AssessmentQuotaService quotaService;
+
+    @Resource
+    private AssessmentExecutionService executionService;
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Operation(summary = "我的可用额度列表")
+    @PostMapping("/my-list")
+    public Result<List<AssessmentQuotaVO>> getMyQuotas(@RequestAttribute("userId") Long userId) {
+        User user = userMapper.selectById(userId);
+        if (user == null || user.getFamilyId() == null) {
+            return Result.error("用户或家庭信息不存在");
+        }
+        List<AssessmentQuota> quotas = quotaService.getAvailableQuotas(user.getFamilyId(), null);
+        List<AssessmentQuotaVO> vos = quotas.stream().map(this::toVO).collect(Collectors.toList());
+        return Result.success(vos);
+    }
+
+    @Operation(summary = "额度详情")
+    @PostMapping("/detail")
+    public Result<AssessmentQuotaVO> detail(@RequestBody Map<String, Object> params) {
+        Long quotaId = params.get("quotaId") != null
+                ? Long.valueOf(params.get("quotaId").toString())
+                : null;
+        if (quotaId == null) {
+            return Result.error("quotaId不能为空");
+        }
+        AssessmentQuota quota = quotaService.getById(quotaId);
+        if (quota == null) {
+            return Result.error("额度不存在");
+        }
+        return Result.success(toVO(quota));
+    }
+
+    private AssessmentQuotaVO toVO(AssessmentQuota q) {
+        AssessmentQuotaVO vo = new AssessmentQuotaVO();
+        vo.setId(q.getId());
+        vo.setQuotaNo(q.getQuotaNo());
+        vo.setFamilyId(q.getFamilyId());
+        vo.setChildId(q.getChildId());
+        vo.setProductOrderId(q.getProductOrderId());
+        vo.setProductId(q.getProductId());
+        vo.setProductName(q.getProductName());
+        vo.setTotalSessions(q.getTotalSessions());
+        vo.setRemainingSessions(q.getRemainingSessions());
+        vo.setValidityStart(q.getValidityStart());
+        vo.setValidityEnd(q.getValidityEnd());
+        vo.setStatus(q.getStatus());
+        vo.setUsedSessions(q.getTotalSessions() - q.getRemainingSessions());
+        vo.setExpired(q.getValidityEnd() != null && q.getValidityEnd().before(new Date()));
+        return vo;
+    }
+}

+ 150 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/assessment/AssessmentReportController.java

@@ -0,0 +1,150 @@
+package com.etotem.cfc.controller.assessment;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.DanAssessmentResult;
+import com.etotem.cfc.entity.GrowthGuidance;
+import com.etotem.cfc.mapper.DanAssessmentResultMapper;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.AssessmentPlanGenerator;
+import com.etotem.cfc.service.FileStorageService;
+import com.etotem.cfc.service.GrowthGuidanceService;
+import com.etotem.cfc.service.ReportParseService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.concurrent.CompletableFuture;
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "测评报告", description = "测评报告上传、指导意见管理")
+@RestController
+@RequestMapping("/api/dan-assessment/result")
+public class AssessmentReportController {
+
+    private static final Logger log = LoggerFactory.getLogger(AssessmentReportController.class);
+
+    @Resource
+    private DanAssessmentResultMapper resultMapper;
+
+    @Resource
+    private GrowthGuidanceService guidanceService;
+
+    @Resource
+    private FileStorageService fileStorageService;
+
+    @Resource
+    private ReportParseService reportParseService;
+
+    @Resource
+    private AssessmentPlanGenerator planGenerator;
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Operation(summary = "家长上传测评报告")
+    @PostMapping("/upload")
+    public Result<DanAssessmentResult> uploadReport(
+            @RequestParam("file") MultipartFile file,
+            @RequestParam("childId") Long childId,
+            @RequestParam(value = "notes", required = false) String notes) {
+
+        if (file == null || file.isEmpty()) {
+            return Result.error("文件不能为空");
+        }
+
+        String filePath = fileStorageService.upload(file, "assessment-reports");
+        if (filePath == null) {
+            return Result.error("文件上传失败");
+        }
+
+        DanAssessmentResult result = new DanAssessmentResult();
+        result.setChildId(childId);
+        result.setStatus("draft");
+        result.setSource(DanAssessmentResult.SOURCE_PARENT_UPLOAD);
+        result.setProcessDesc(notes);
+        result.setAssessmentDate(new Date());
+        result.setCreatedAt(new Date());
+        result.setUpdatedAt(new Date());
+        result.setPics(filePath);
+        resultMapper.insert(result);
+
+        // 异步解析上传的报告文件
+        final String parsePath = filePath;
+        final Long parseResultId = result.getId();
+        CompletableFuture.runAsync(() -> {
+            try {
+                reportParseService.parseReport(parsePath);
+                log.info("异步解析报告完成: resultId={}", parseResultId);
+            } catch (Exception e) {
+                log.error("异步解析报告失败: resultId={}", parseResultId, e);
+            }
+        });
+
+        return Result.success(result);
+    }
+
+    @Operation(summary = "保存结构化指导意见")
+    @PostMapping("/{resultId}/guidance/save")
+    public Result<List<GrowthGuidance>> saveGuidance(
+            @PathVariable Long resultId,
+            @RequestBody Map<String, Object> body) {
+
+        @SuppressWarnings("unchecked")
+        List<GrowthGuidance> items = (List<GrowthGuidance>) body.get("items");
+        if (items == null || items.isEmpty()) {
+            return Result.error("指导意见不能为空");
+        }
+
+        guidanceService.saveGuidances(resultId, items);
+        List<GrowthGuidance> saved = guidanceService.getByResultId(resultId);
+        return Result.success(saved);
+    }
+
+    @Operation(summary = "获取结构化指导意见")
+    @PostMapping("/{resultId}/guidance")
+    public Result<List<GrowthGuidance>> getGuidance(@PathVariable Long resultId) {
+        List<GrowthGuidance> list = guidanceService.getByResultId(resultId);
+        return Result.success(list);
+    }
+
+    @Operation(summary = "规划师确认家长上传报告")
+    @PostMapping("/{resultId}/confirm-upload")
+    public Result<DanAssessmentResult> confirmUpload(
+            @PathVariable Long resultId,
+            @RequestBody Map<String, Object> body) {
+
+        DanAssessmentResult result = resultMapper.selectById(resultId);
+        if (result == null) {
+            return Result.error("报告不存在");
+        }
+        result.setStatus("completed");
+        result.setUpdatedAt(new Date());
+        Integer overallScore = body.get("overallScore") != null
+                ? Integer.valueOf(body.get("overallScore").toString()) : null;
+        if (overallScore != null) {
+            result.setOverallScore(overallScore);
+        }
+        resultMapper.updateById(result);
+
+        // 自动生成测评方案
+        try {
+            User child = userMapper.selectById(result.getChildId());
+            Long familyId = child != null ? child.getFamilyId() : null;
+            if (familyId != null) {
+                planGenerator.generate(result, familyId, result.getChildId());
+                log.info("已为测评结果{}自动生成方案", resultId);
+            }
+        } catch (Exception e) {
+            log.error("自动生成方案失败: resultId={}", resultId, e);
+        }
+
+        return Result.success(result);
+    }
+}

+ 109 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/guide/PlanReviewController.java

@@ -0,0 +1,109 @@
+package com.etotem.cfc.controller.guide;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.TaskPlanInstance;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.TaskPlanInstanceMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.PlanActivationService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "指导师-方案审核", description = "指导师查看/审核/激活测评方案")
+@RestController
+@RequestMapping("/api/guide/plans")
+public class PlanReviewController {
+
+    private static final Logger log = LoggerFactory.getLogger(PlanReviewController.class);
+
+    @Resource
+    private TaskPlanInstanceMapper planMapper;
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Resource
+    private PlanActivationService planActivationService;
+
+    @Operation(summary = "查看家庭的方案列表")
+    @PostMapping("/family-list")
+    public Result<List<TaskPlanInstance>> listFamilyPlans(@RequestBody Map<String, Object> body,
+                                                           @RequestAttribute("userId") Long userId,
+                                                           @RequestAttribute("role") String role) {
+        if (!"teacher".equals(role) && !"admin".equals(role)) {
+            return Result.error("无权限");
+        }
+        Long familyId = body.get("familyId") != null ? Long.valueOf(body.get("familyId").toString()) : null;
+        String status = (String) body.get("status");
+        if (familyId == null) return Result.error("familyId required");
+        LambdaQueryWrapper<TaskPlanInstance> w = new LambdaQueryWrapper<TaskPlanInstance>()
+                .eq(TaskPlanInstance::getFamilyId, familyId)
+                .orderByDesc(TaskPlanInstance::getCreatedAt);
+        if (status != null && !status.isEmpty()) {
+            w.eq(TaskPlanInstance::getStatus, status);
+        }
+        return Result.success(planMapper.selectList(w));
+    }
+
+    @Operation(summary = "审核方案(批准/驳回)")
+    @PostMapping("/review")
+    public Result<String> reviewPlan(@RequestBody Map<String, Object> body,
+                                     @RequestAttribute("userId") Long userId,
+                                     @RequestAttribute("role") String role) {
+        if (!"teacher".equals(role) && !"admin".equals(role)) {
+            return Result.error("无权限");
+        }
+        Long planId = body.get("planId") != null ? Long.valueOf(body.get("planId").toString()) : null;
+        boolean approved = body.get("approved") != null && Boolean.TRUE.equals(body.get("approved"));
+        String comment = (String) body.get("comment");
+        if (planId == null) return Result.error("planId required");
+        TaskPlanInstance plan = planMapper.selectById(planId);
+        if (plan == null) return Result.error("方案不存在");
+        if (!"generated".equals(plan.getStatus())) return Result.error("方案状态异常");
+        plan.setReviewedBy(userId);
+        plan.setReviewedAt(new Date());
+        plan.setReviewComment(comment);
+        plan.setStatus(approved ? "reviewed" : "rejected");
+        plan.setUpdatedAt(new Date());
+        planMapper.updateById(plan);
+        log.info("方案{} {} by userId={}", planId, approved ? "批准" : "驳回", userId);
+        return Result.success(approved ? "方案已批准" : "方案已驳回");
+    }
+
+    @Operation(summary = "激活方案(生成任务)")
+    @PostMapping("/activate")
+    public Result<String> activatePlan(@RequestBody Map<String, Object> body,
+                                       @RequestAttribute("userId") Long userId,
+                                       @RequestAttribute("role") String role) {
+        if (!"teacher".equals(role) && !"admin".equals(role)) {
+            return Result.error("无权限");
+        }
+        Long planId = body.get("planId") != null ? Long.valueOf(body.get("planId").toString()) : null;
+        if (planId == null) return Result.error("planId required");
+        try {
+            planActivationService.activate(planId, userId);
+            return Result.success("方案已激活");
+        } catch (IllegalStateException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    @Operation(summary = "获取方案详情")
+    @PostMapping("/detail")
+    public Result<TaskPlanInstance> planDetail(@RequestBody Map<String, Object> body) {
+        Long planId = body.get("planId") != null ? Long.valueOf(body.get("planId").toString()) : null;
+        if (planId == null) return Result.error("planId required");
+        TaskPlanInstance plan = planMapper.selectById(planId);
+        if (plan == null) return Result.error("方案不存在");
+        return Result.success(plan);
+    }
+}

+ 120 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/subscription/SubscriptionController.java

@@ -0,0 +1,120 @@
+package com.etotem.cfc.controller.subscription;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.MemberSubscription;
+import com.etotem.cfc.entity.PromotionTier;
+import com.etotem.cfc.service.MemberSubscriptionService;
+import com.etotem.cfc.service.PromotionTierService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.*;
+
+@RestController
+@RequestMapping("/api/subscription")
+public class SubscriptionController {
+
+    private static final List<String> L1_BENEFITS = Arrays.asList("基础会员", "优先客服");
+    private static final List<String> L2_BENEFITS = Arrays.asList("高级会员", "专属顾问", "80折优惠");
+
+    @Resource
+    private MemberSubscriptionService subscriptionService;
+
+    @Resource
+    private PromotionTierService promotionTierService;
+
+    /**
+     * 获取当前家庭订阅状态
+     */
+    @PostMapping("/status")
+    public Result<Map<String, Object>> getSubscriptionStatus(@RequestAttribute Long familyId) {
+        MemberSubscription sub = subscriptionService.getActiveSubscription(familyId);
+        PromotionTier tier = promotionTierService.getCurrentTier(familyId);
+
+        Map<String, Object> data = new HashMap<>();
+        data.put("subscription", sub);
+        data.put("promotionTier", tier);
+        data.put("hasActiveSubscription", sub != null && sub.getExpireTime() != null
+                && sub.getExpireTime().after(new Date()));
+
+        return Result.success(data);
+    }
+
+    /**
+     * 获取订阅方案列表
+     */
+    @PostMapping("/plans")
+    public Result<Map<String, Object>> getPlans() {
+        Map<String, Object> plans = new HashMap<>();
+        Map<String, Object> l1 = new HashMap<>();
+        l1.put("name", "一生一世L1");
+        l1.put("price", 36500);
+        l1.put("period", "yearly");
+        l1.put("benefits", L1_BENEFITS);
+        plans.put("L1", l1);
+
+        Map<String, Object> l2 = new HashMap<>();
+        l2.put("name", "一生一世L2");
+        l2.put("price", 131400);
+        l2.put("period", "yearly");
+        l2.put("benefits", L2_BENEFITS);
+        plans.put("L2", l2);
+
+        return Result.success(plans);
+    }
+
+    /**
+     * 订阅
+     */
+    @PostMapping("/subscribe")
+    public Result<Void> subscribe(@RequestAttribute Long familyId,
+                                   @RequestParam String level,
+                                   @RequestParam Integer amount,
+                                   @RequestParam String paymentType,
+                                   @RequestParam(required = false) String transactionId) {
+        String orderNo = "SUB" + System.currentTimeMillis();
+        subscriptionService.subscribe(familyId, level, amount, paymentType, transactionId, orderNo);
+        return Result.success(null);
+    }
+
+    /**
+     * 取消订阅
+     */
+    @PostMapping("/cancel")
+    public Result<Void> cancel(@RequestAttribute Long familyId) {
+        subscriptionService.cancelSubscription(familyId);
+        return Result.success(null);
+    }
+
+    /**
+     * 设置自动续费
+     */
+    @PostMapping("/auto-renew")
+    public Result<Void> setAutoRenew(@RequestAttribute Long familyId,
+                                      @RequestParam Integer enabled) {
+        subscriptionService.setAutoRenew(familyId, enabled);
+        return Result.success(null);
+    }
+
+    /**
+     * 获取推广等级
+     */
+    @PostMapping("/tier")
+    public Result<PromotionTier> getTier(@RequestAttribute Long userId) {
+        return Result.success(promotionTierService.getCurrentTier(userId));
+    }
+
+    /**
+     * 更新推广等级(管理员)
+     */
+    @PostMapping("/tier/update")
+    public Result<Void> updateTier(@RequestAttribute Long userId,
+                                    @RequestParam String tierCode,
+                                    @RequestAttribute String role) {
+        if (!"admin".equals(role)) {
+            return Result.error("无权限");
+        }
+        promotionTierService.updateTier(userId, tierCode);
+        return Result.success(null);
+    }
+}

+ 23 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/AssessmentQuotaVO.java

@@ -0,0 +1,23 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class AssessmentQuotaVO {
+    private Long id;
+    private String quotaNo;
+    private Long familyId;
+    private Long childId;
+    private Long productOrderId;
+    private Long productId;
+    private String productName;
+    private Integer totalSessions;
+    private Integer remainingSessions;
+    private Date validityStart;
+    private Date validityEnd;
+    private String status;
+    private int usedSessions;
+    private boolean expired;
+}

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

@@ -0,0 +1,11 @@
+package com.etotem.cfc.dto;
+
+import com.etotem.cfc.entity.AssessmentProduct;
+import com.etotem.cfc.entity.Product;
+import lombok.Data;
+
+@Data
+public class ProductCreateRequest {
+    private Product product;
+    private AssessmentProduct assessmentExt;
+}

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/ProductDTO.java

@@ -31,6 +31,13 @@ public class ProductDTO {
     private List<String> imageList;
     private Long energyDimensionId;
     private String priceLabel;     // 游客 = "登录查看价格",登录后 = null
+
+    // 测评商品扩展信息
+    private String assessmentType;
+    private Integer totalSessions;
+    private Integer validityDays;
+    private String guideScope;
+
     private Date createdAt;
     private Date updatedAt;
 

+ 41 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/AssessmentExecution.java

@@ -0,0 +1,41 @@
+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("assessment_executions")
+public class AssessmentExecution implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long quotaId;
+
+    private Long childId;
+
+    private Long guideId;
+
+    private String assessmentType;
+
+    private Long appointmentId;
+
+    private Long resultId;
+
+    private String status;        // pending / in_progress / completed / cancelled
+
+    private Date scheduledDate;
+
+    private Date completedAt;
+
+    private String notes;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 33 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/AssessmentPlanRule.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("assessment_plan_rules")
+public class AssessmentPlanRule implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String dimension;
+
+    private Integer scoreMin;
+
+    private Integer scoreMax;
+
+    private Long templateId;
+
+    private Integer priority;
+
+    private Boolean isActive;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 31 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/AssessmentProduct.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("assessment_products")
+public class AssessmentProduct implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long productId;
+
+    private String assessmentType;   // single=单包, bundle=套餐
+
+    private Integer totalSessions;   // 包含测评次数
+
+    private Integer validityDays;    // 有效期(天)
+
+    private String guideScope;       // 规划师范围: all/assigned/auto
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

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

@@ -0,0 +1,43 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("assessment_quotas")
+public class AssessmentQuota implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String quotaNo;
+
+    private Long familyId;
+
+    private Long childId;
+
+    private Long productOrderId;
+
+    private Long productId;
+
+    private String productName;
+
+    private Integer totalSessions;
+
+    private Integer remainingSessions;
+
+    private Date validityStart;
+
+    private Date validityEnd;
+
+    private String status;        // active / expired / used_all / cancelled
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

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

@@ -81,6 +81,13 @@ public class DanAssessmentResult implements Serializable {
 
     private String status; // draft/completed/archived
 
+    // 报告来源
+    public static final String SOURCE_PLANNER_ENTRY = "planner_entry";
+    public static final String SOURCE_PARENT_UPLOAD = "parent_upload";
+    public static final String SOURCE_AUTO_FETCH = "auto_fetch";
+
+    private String source; // planner_entry / parent_upload / auto_fetch
+
     private Date createdAt;
 
     private Date updatedAt;

+ 33 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/GrowthGuidance.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("growth_guidances")
+public class GrowthGuidance implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long resultId;
+
+    private String dimension;
+
+    private Integer score;
+
+    private String suggestion;
+
+    private Integer priority;
+
+    private Integer sortOrder;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 35 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/MemberSubscription.java

@@ -0,0 +1,35 @@
+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("member_subscription")
+public class MemberSubscription implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long familyId;
+
+    private String level;
+
+    private String status;
+
+    private Date startTime;
+
+    private Date expireTime;
+
+    private Long paymentId;
+
+    private Integer autoRenew;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 35 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/MemberSubscriptionOrder.java

@@ -0,0 +1,35 @@
+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("member_subscription_order")
+public class MemberSubscriptionOrder implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long familyId;
+
+    private String level;
+
+    private Integer amount;
+
+    private String status;
+
+    private String paymentType;
+
+    private String transactionId;
+
+    private String orderNo;
+
+    private Date createdAt;
+
+    private Date paidAt;
+}

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

@@ -12,6 +12,11 @@ import java.util.Date;
 @TableName("products")
 public class Product implements Serializable {
 
+    /** 商品类型常量: 实物 */
+    public static final String TYPE_PHYSICAL = "physical";
+    /** 商品类型常量: 测评 */
+    public static final String TYPE_ASSESSMENT = "assessment";
+
     @TableId(type = IdType.AUTO)
     private Long id;
 

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

@@ -0,0 +1,29 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("product_ppoint")
+public class ProductPpoint implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long productId;
+
+    private Integer ppoint;
+
+    private Date startDate;
+
+    private Date endDate;
+
+    private Long createdBy;
+
+    private Date createdAt;
+}

+ 41 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/PromotionTier.java

@@ -0,0 +1,41 @@
+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("promotion_tier")
+public class PromotionTier implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long userId;
+
+    private String tier;
+
+    private Integer teamSize1st;
+
+    private Integer teamSize2nd;
+
+    private Integer teamSize3rd;
+
+    private Integer totalTeamSize;
+
+    private Integer totalReferralEarnings;
+
+    private Integer totalShareEarnings;
+
+    private Date promotedAt;
+
+    private Date lastChangeAt;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

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

@@ -0,0 +1,27 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("promotion_tier_change_log")
+public class PromotionTierChangeLog implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long userId;
+
+    private String oldTier;
+
+    private String newTier;
+
+    private String reason;
+
+    private Date changedAt;
+}

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

@@ -0,0 +1,27 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("referral_tree")
+public class ReferralTree implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long parentId;
+
+    private Long childId;
+
+    private Integer level;
+
+    private String path;
+
+    private Date createdAt;
+}

+ 25 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/SubscriptionBenefitLog.java

@@ -0,0 +1,25 @@
+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("subscription_benefit_log")
+public class SubscriptionBenefitLog implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long subscriptionId;
+
+    private String benefitCode;
+
+    private Date usedAt;
+
+    private String detail;
+}

+ 18 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/TaskPlanInstance.java

@@ -33,6 +33,24 @@ public class TaskPlanInstance implements Serializable {
 
     private Integer completedDays;  // 已完成天数
 
+    private String source;          // manual / assessment / template
+
+    private Long sourceResultId;    // 关联测评结果ID(source=assessment时)
+
+    private String remark;          // 方案备注(含指导意见摘要)
+
+    private Long reviewedBy;        // 审核人(规划师ID)
+
+    private Date reviewedAt;        // 审核时间
+
+    private String reviewComment;   // 审核意见
+
+    private Long confirmedBy;       // 确认人(家长ID)
+
+    private Date confirmedAt;       // 确认时间
+
+    private Date activatedAt;       // 激活时间
+
     private Date createdAt;
 
     private Date updatedAt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 54 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentExecutionService.java

@@ -0,0 +1,54 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.AssessmentExecution;
+import com.etotem.cfc.mapper.AssessmentExecutionMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+
+@Service
+public class AssessmentExecutionService {
+
+    @Resource
+    private AssessmentExecutionMapper mapper;
+
+    @Transactional
+    public AssessmentExecution create(Long quotaId, Long childId, Long appointmentId) {
+        AssessmentExecution exec = new AssessmentExecution();
+        exec.setQuotaId(quotaId);
+        exec.setChildId(childId);
+        exec.setAppointmentId(appointmentId);
+        exec.setStatus("pending");
+        exec.setCreatedAt(new Date());
+        exec.setUpdatedAt(new Date());
+        mapper.insert(exec);
+        return exec;
+    }
+
+    @Transactional
+    public void linkResult(Long executionId, Long resultId) {
+        AssessmentExecution exec = mapper.selectById(executionId);
+        if (exec != null) {
+            exec.setResultId(resultId);
+            exec.setStatus("completed");
+            exec.setCompletedAt(new Date());
+            exec.setUpdatedAt(new Date());
+            mapper.updateById(exec);
+        }
+    }
+
+    public AssessmentExecution getByAppointmentId(Long appointmentId) {
+        return mapper.selectOne(new LambdaQueryWrapper<AssessmentExecution>()
+                .eq(AssessmentExecution::getAppointmentId, appointmentId));
+    }
+
+    public List<AssessmentExecution> getByChildId(Long childId) {
+        return mapper.selectList(new LambdaQueryWrapper<AssessmentExecution>()
+                .eq(AssessmentExecution::getChildId, childId)
+                .orderByDesc(AssessmentExecution::getCreatedAt));
+    }
+}

+ 106 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentPlanGenerator.java

@@ -0,0 +1,106 @@
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.entity.AssessmentPlanRule;
+import com.etotem.cfc.entity.DanAssessmentResult;
+import com.etotem.cfc.entity.TaskPlanInstance;
+import com.etotem.cfc.mapper.TaskPlanInstanceMapper;
+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.util.*;
+
+/**
+ * 测评结果 → 任务方案自动生成引擎。
+ * 当前实现规则引擎匹配,后续可通过 SPI 扩展 AI 增强方案生成。
+ */
+@Service
+public class AssessmentPlanGenerator {
+
+    private static final Logger log = LoggerFactory.getLogger(AssessmentPlanGenerator.class);
+
+    @Resource
+    private AssessmentPlanRuleService ruleService;
+
+    @Resource
+    private TaskPlanInstanceMapper planMapper;
+
+    @Resource
+    private GrowthGuidanceService guidanceService;
+
+    /**
+     * 三步流程:
+     * 1. collectScores — 从测评结果提取各维度得分
+     * 2. matchRules — 规则匹配
+     * 3. buildPlan — 生成方案实例
+     */
+    @Transactional
+    public TaskPlanInstance generate(DanAssessmentResult result, Long familyId, Long childId) {
+        Map<String, Integer> scores = collectScores(result);
+        Set<Long> templateIds = matchRules(scores);
+        return buildPlan(result, familyId, childId, templateIds, scores);
+    }
+
+    Map<String, Integer> collectScores(DanAssessmentResult result) {
+        Map<String, Integer> scores = new LinkedHashMap<>();
+        putIfNotNull(scores, "attention", result.getAttentionScore());
+        putIfNotNull(scores, "focus", result.getFocusScore());
+        putIfNotNull(scores, "memory", result.getMemoryScore());
+        putIfNotNull(scores, "logic", result.getLogicScore());
+        putIfNotNull(scores, "emotion", result.getEmotionScore());
+        return scores;
+    }
+
+    Set<Long> matchRules(Map<String, Integer> scores) {
+        Set<Long> templateIds = new LinkedHashSet<>();
+        for (Map.Entry<String, Integer> entry : scores.entrySet()) {
+            List<AssessmentPlanRule> rules = ruleService.matchRules(entry.getKey(), entry.getValue());
+            for (AssessmentPlanRule rule : rules) {
+                templateIds.add(rule.getTemplateId());
+            }
+        }
+        return templateIds;
+    }
+
+    TaskPlanInstance buildPlan(DanAssessmentResult result, Long familyId, Long childId,
+                                Set<Long> templateIds, Map<String, Integer> scores) {
+        TaskPlanInstance plan = new TaskPlanInstance();
+        plan.setFamilyId(familyId);
+        plan.setChildId(childId);
+        plan.setName("DAN测评方案 - " + new java.text.SimpleDateFormat("yyyyMMdd").format(new Date()));
+        plan.setStatus("generated");
+        plan.setSource("assessment");
+        plan.setSourceResultId(result.getId());
+        plan.setTotalDays(14);
+        plan.setCompletedDays(0);
+        plan.setStartDate(new Date());
+        Calendar cal = Calendar.getInstance();
+        cal.add(Calendar.DAY_OF_YEAR, 14);
+        plan.setEndDate(cal.getTime());
+
+        // 构建 remark(含指导意见摘要和维度得分)
+        StringBuilder remark = new StringBuilder("【测评方案自动生成】\n");
+        for (Map.Entry<String, Integer> entry : scores.entrySet()) {
+            remark.append(entry.getKey()).append(": ").append(entry.getValue()).append("分\n");
+        }
+        if (!templateIds.isEmpty()) {
+            remark.append("匹配模板包ID: ").append(templateIds).append("\n");
+        } else {
+            remark.append("所有维度正常,生成维持型方案\n");
+        }
+        plan.setRemark(remark.toString());
+        plan.setCreatedAt(new Date());
+        plan.setUpdatedAt(new Date());
+        planMapper.insert(plan);
+        log.info("已生成测评方案 planId={}, childId={}", plan.getId(), childId);
+        return plan;
+    }
+
+    private void putIfNotNull(Map<String, Integer> map, String key, Integer value) {
+        if (value != null) {
+            map.put(key, value);
+        }
+    }
+}

+ 62 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentPlanRuleService.java

@@ -0,0 +1,62 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.AssessmentPlanRule;
+import com.etotem.cfc.mapper.AssessmentPlanRuleMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+
+@Service
+public class AssessmentPlanRuleService {
+
+    @Resource
+    private AssessmentPlanRuleMapper mapper;
+
+    public List<AssessmentPlanRule> getActiveRules() {
+        return mapper.selectList(new LambdaQueryWrapper<AssessmentPlanRule>()
+                .eq(AssessmentPlanRule::getIsActive, true)
+                .orderByAsc(AssessmentPlanRule::getPriority));
+    }
+
+    public List<AssessmentPlanRule> getRulesByDimension(String dimension) {
+        return mapper.selectList(new LambdaQueryWrapper<AssessmentPlanRule>()
+                .eq(AssessmentPlanRule::getDimension, dimension)
+                .eq(AssessmentPlanRule::getIsActive, true)
+                .orderByAsc(AssessmentPlanRule::getPriority));
+    }
+
+    public List<AssessmentPlanRule> matchRules(String dimension, int score) {
+        return mapper.selectList(new LambdaQueryWrapper<AssessmentPlanRule>()
+                .eq(AssessmentPlanRule::getDimension, dimension)
+                .eq(AssessmentPlanRule::getIsActive, true)
+                .le(AssessmentPlanRule::getScoreMin, score)
+                .ge(AssessmentPlanRule::getScoreMax, score)
+                .orderByDesc(AssessmentPlanRule::getPriority));
+    }
+
+    public List<AssessmentPlanRule> getAll() {
+        return mapper.selectList(new LambdaQueryWrapper<AssessmentPlanRule>()
+                .orderByAsc(AssessmentPlanRule::getDimension, AssessmentPlanRule::getPriority));
+    }
+
+    public void saveRule(AssessmentPlanRule rule) {
+        if (rule.getId() != null) {
+            rule.setUpdatedAt(new Date());
+            mapper.updateById(rule);
+        } else {
+            rule.setCreatedAt(new Date());
+            rule.setUpdatedAt(new Date());
+            if (rule.getIsActive() == null) {
+                rule.setIsActive(true);
+            }
+            mapper.insert(rule);
+        }
+    }
+
+    public void deleteRule(Long id) {
+        mapper.deleteById(id);
+    }
+}

+ 35 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentProductService.java

@@ -0,0 +1,35 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.AssessmentProduct;
+import com.etotem.cfc.mapper.AssessmentProductMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.Date;
+
+@Service
+public class AssessmentProductService {
+
+    @Resource
+    private AssessmentProductMapper mapper;
+
+    public AssessmentProduct getByProductId(Long productId) {
+        return mapper.selectOne(new LambdaQueryWrapper<AssessmentProduct>()
+                .eq(AssessmentProduct::getProductId, productId));
+    }
+
+    @Transactional
+    public void saveOrUpdate(Long productId, AssessmentProduct ext) {
+        mapper.delete(new LambdaQueryWrapper<AssessmentProduct>()
+                .eq(AssessmentProduct::getProductId, productId));
+        if (ext != null) {
+            ext.setId(null);
+            ext.setProductId(productId);
+            ext.setCreatedAt(new Date());
+            ext.setUpdatedAt(new Date());
+            mapper.insert(ext);
+        }
+    }
+}

+ 93 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentQuotaService.java

@@ -0,0 +1,93 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.AssessmentQuota;
+import com.etotem.cfc.entity.ProductOrder;
+import com.etotem.cfc.mapper.AssessmentQuotaMapper;
+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.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.Random;
+
+@Service
+public class AssessmentQuotaService {
+
+    private static final Logger log = LoggerFactory.getLogger(AssessmentQuotaService.class);
+
+    @Resource
+    private AssessmentQuotaMapper mapper;
+
+    @Transactional
+    public void grantQuota(ProductOrder order, Integer totalSessions, Integer validityDays) {
+        AssessmentQuota quota = new AssessmentQuota();
+        quota.setQuotaNo(generateQuotaNo());
+        quota.setFamilyId(order.getFamilyId() != null ? order.getFamilyId() : 0L);
+        quota.setChildId(null); // 不绑定具体孩子
+        quota.setProductOrderId(order.getId());
+        quota.setProductId(order.getProductId());
+        quota.setProductName(order.getProductName());
+        quota.setTotalSessions(totalSessions != null ? totalSessions : 1);
+        quota.setRemainingSessions(totalSessions != null ? totalSessions : 1);
+        Date now = new Date();
+        quota.setValidityStart(now);
+        quota.setValidityEnd(org.apache.commons.lang3.time.DateUtils.addDays(now,
+                validityDays != null ? validityDays : 365));
+        quota.setStatus("active");
+        quota.setCreatedAt(now);
+        quota.setUpdatedAt(now);
+        mapper.insert(quota);
+        log.info("已为订单{}发放测评额度: quotaNo={}, familyId={}, 次数={}",
+                order.getOrderNo(), quota.getQuotaNo(), quota.getFamilyId(), quota.getTotalSessions());
+    }
+
+    @Transactional
+    public int consumeQuota(Long quotaId, Long childId) {
+        AssessmentQuota quota = mapper.selectById(quotaId);
+        if (quota == null) {
+            throw new RuntimeException("测评额度不存在");
+        }
+        if (!"active".equals(quota.getStatus())) {
+            throw new RuntimeException("测评额度状态异常: " + quota.getStatus());
+        }
+        if (quota.getRemainingSessions() <= 0) {
+            throw new RuntimeException("测评额度已用完");
+        }
+        if (quota.getValidityEnd().before(new Date())) {
+            quota.setStatus("expired");
+            mapper.updateById(quota);
+            throw new RuntimeException("测评额度已过期");
+        }
+        quota.setRemainingSessions(quota.getRemainingSessions() - 1);
+        if (quota.getRemainingSessions() <= 0) {
+            quota.setStatus("used_all");
+        }
+        quota.setUpdatedAt(new Date());
+        mapper.updateById(quota);
+        return quota.getRemainingSessions();
+    }
+
+    public List<AssessmentQuota> getAvailableQuotas(Long familyId, Long childId) {
+        LambdaQueryWrapper<AssessmentQuota> wrapper = new LambdaQueryWrapper<AssessmentQuota>()
+                .eq(AssessmentQuota::getFamilyId, familyId)
+                .eq(AssessmentQuota::getStatus, "active")
+                .gt(AssessmentQuota::getRemainingSessions, 0)
+                .gt(AssessmentQuota::getValidityEnd, new Date())
+                .orderByDesc(AssessmentQuota::getCreatedAt);
+        return mapper.selectList(wrapper);
+    }
+
+    public AssessmentQuota getById(Long id) {
+        return mapper.selectById(id);
+    }
+
+    private String generateQuotaNo() {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
+        return "Q" + sdf.format(new Date()) + String.format("%04d", new Random().nextInt(10000));
+    }
+}

+ 63 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/FileStorageService.java

@@ -0,0 +1,63 @@
+package com.etotem.cfc.service;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.PostConstruct;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.UUID;
+
+@Service
+public class FileStorageService {
+
+    private static final Logger log = LoggerFactory.getLogger(FileStorageService.class);
+
+    @Value("${file.upload-dir:./uploads}")
+    private String uploadDir;
+
+    private Path uploadPath;
+
+    @PostConstruct
+    public void init() {
+        uploadPath = Paths.get(uploadDir).toAbsolutePath().normalize();
+        try {
+            Files.createDirectories(uploadPath);
+        } catch (IOException e) {
+            log.error("初始化上传目录失败: {}", uploadPath, e);
+        }
+    }
+
+    public String upload(MultipartFile file, String subDir) {
+        if (file == null || file.isEmpty()) {
+            return null;
+        }
+        try {
+            String originalName = file.getOriginalFilename();
+            String ext = "";
+            if (originalName != null && originalName.contains(".")) {
+                ext = originalName.substring(originalName.lastIndexOf("."));
+            }
+            String filename = UUID.randomUUID().toString().replace("-", "") + ext;
+
+            Path targetDir = uploadPath.resolve(subDir != null ? subDir : "general");
+            Files.createDirectories(targetDir);
+
+            Path targetPath = targetDir.resolve(filename);
+            file.transferTo(targetPath.toFile());
+
+            String relativePath = (subDir != null ? "/" + subDir : "") + "/" + filename;
+            log.info("文件已保存: {}", relativePath);
+            return relativePath;
+        } catch (IOException e) {
+            log.error("文件上传失败", e);
+            return null;
+        }
+    }
+}

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

@@ -0,0 +1,56 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.GrowthGuidance;
+import com.etotem.cfc.mapper.GrowthGuidanceMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+
+@Service
+public class GrowthGuidanceService {
+
+    @Resource
+    private GrowthGuidanceMapper mapper;
+
+    public List<GrowthGuidance> getByResultId(Long resultId) {
+        return mapper.selectList(new LambdaQueryWrapper<GrowthGuidance>()
+                .eq(GrowthGuidance::getResultId, resultId)
+                .orderByAsc(GrowthGuidance::getSortOrder));
+    }
+
+    @Transactional
+    public void saveGuidances(Long resultId, List<GrowthGuidance> items) {
+        mapper.delete(new LambdaQueryWrapper<GrowthGuidance>()
+                .eq(GrowthGuidance::getResultId, resultId));
+        for (int i = 0; i < items.size(); i++) {
+            GrowthGuidance g = items.get(i);
+            g.setId(null);
+            g.setResultId(resultId);
+            if (g.getSortOrder() == null) {
+                g.setSortOrder(i);
+            }
+            g.setCreatedAt(new Date());
+            g.setUpdatedAt(new Date());
+            mapper.insert(g);
+        }
+    }
+
+    @Transactional
+    public void saveSingle(GrowthGuidance guidance) {
+        if (guidance.getId() != null) {
+            guidance.setUpdatedAt(new Date());
+            mapper.updateById(guidance);
+        } else {
+            guidance.setCreatedAt(new Date());
+            guidance.setUpdatedAt(new Date());
+            if (guidance.getSortOrder() == null) {
+                guidance.setSortOrder(0);
+            }
+            mapper.insert(guidance);
+        }
+    }
+}

+ 210 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/MemberSubscriptionService.java

@@ -0,0 +1,210 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.Family;
+import com.etotem.cfc.entity.MemberSubscription;
+import com.etotem.cfc.entity.MemberSubscriptionOrder;
+import com.etotem.cfc.mapper.FamilyMapper;
+import com.etotem.cfc.mapper.MemberSubscriptionMapper;
+import com.etotem.cfc.mapper.MemberSubscriptionOrderMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+
+@Service
+public class MemberSubscriptionService {
+
+    @Resource
+    private MemberSubscriptionMapper subscriptionMapper;
+
+    @Resource
+    private MemberSubscriptionOrderMapper orderMapper;
+
+    @Resource
+    private FamilyMapper familyMapper;
+
+    @Resource
+    private CommissionService commissionService;
+
+    // ==================== 基础查询 ====================
+
+    /**
+     * 获取家庭当前有效订阅
+     */
+    public MemberSubscription getActiveSubscription(Long familyId) {
+        return subscriptionMapper.selectOne(
+                new LambdaQueryWrapper<MemberSubscription>()
+                        .eq(MemberSubscription::getFamilyId, familyId)
+                        .eq(MemberSubscription::getStatus, "active")
+                        .orderByDesc(MemberSubscription::getExpireTime)
+                        .last("LIMIT 1")
+        );
+    }
+
+    /**
+     * 获取家庭所有订阅记录
+     */
+    public List<MemberSubscription> getSubscriptionHistory(Long familyId) {
+        return subscriptionMapper.selectList(
+                new LambdaQueryWrapper<MemberSubscription>()
+                        .eq(MemberSubscription::getFamilyId, familyId)
+                        .orderByDesc(MemberSubscription::getCreatedAt)
+        );
+    }
+
+    /**
+     * 是否为有效订阅家庭
+     */
+    public boolean hasActiveSubscription(Long familyId) {
+        MemberSubscription sub = getActiveSubscription(familyId);
+        if (sub == null) {
+            return false;
+        }
+        return sub.getExpireTime() != null && sub.getExpireTime().after(new Date());
+    }
+
+    // ==================== 订阅操作 ====================
+
+    /**
+     * 创建新订阅
+     */
+    public MemberSubscription subscribe(Long familyId, String level, Integer amount,
+                                         String paymentType, String transactionId, String orderNo) {
+        Date now = new Date();
+        Date expireTime = calculateExpireTime(paymentType);
+
+        // 先取消当前有效订阅
+        MemberSubscription current = getActiveSubscription(familyId);
+        if (current != null) {
+            current.setStatus("expired");
+            current.setUpdatedAt(now);
+            subscriptionMapper.updateById(current);
+        }
+
+        // 创建新订阅
+        MemberSubscription subscription = new MemberSubscription();
+        subscription.setFamilyId(familyId);
+        subscription.setLevel(level);
+        subscription.setStatus("active");
+        subscription.setStartTime(now);
+        subscription.setExpireTime(expireTime);
+        subscription.setAutoRenew(0);
+        subscription.setCreatedAt(now);
+        subscription.setUpdatedAt(now);
+        subscriptionMapper.insert(subscription);
+
+        // 创建订单记录
+        MemberSubscriptionOrder order = new MemberSubscriptionOrder();
+        order.setFamilyId(familyId);
+        order.setLevel(level);
+        order.setAmount(amount);
+        order.setStatus("paid");
+        order.setPaymentType(paymentType);
+        order.setTransactionId(transactionId);
+        order.setOrderNo(orderNo);
+        order.setCreatedAt(now);
+        order.setPaidAt(now);
+        orderMapper.insert(order);
+
+        subscription.setPaymentId(order.getId());
+        subscriptionMapper.updateById(subscription);
+
+        Family family = familyMapper.selectById(familyId);
+        if (family != null && family.getCreatorId() != null) {
+            commissionService.settleTwoLevel(order.getId(), "subscription", family.getCreatorId(), amount, null);
+        }
+
+        return subscription;
+    }
+
+    /**
+     * 续费订阅
+     */
+    public MemberSubscription renew(Long familyId, Integer amount, String paymentType,
+                                     String transactionId, String orderNo) {
+        MemberSubscription current = getActiveSubscription(familyId);
+        if (current == null) {
+            return subscribe(familyId, "L1", amount, paymentType, transactionId, orderNo);
+        }
+
+        Date now = new Date();
+        Date newExpireTime = calculateExpireTimeFrom(current.getExpireTime(), paymentType);
+
+        current.setExpireTime(newExpireTime);
+        current.setUpdatedAt(now);
+        subscriptionMapper.updateById(current);
+
+        // 创建续费订单
+        MemberSubscriptionOrder order = new MemberSubscriptionOrder();
+        order.setFamilyId(familyId);
+        order.setLevel(current.getLevel());
+        order.setAmount(amount);
+        order.setStatus("paid");
+        order.setPaymentType(paymentType);
+        order.setTransactionId(transactionId);
+        order.setOrderNo(orderNo);
+        order.setCreatedAt(now);
+        order.setPaidAt(now);
+        orderMapper.insert(order);
+
+        Family family = familyMapper.selectById(familyId);
+        if (family != null && family.getCreatorId() != null) {
+            commissionService.settleTwoLevel(order.getId(), "subscription", family.getCreatorId(), amount, null);
+        }
+
+        return current;
+    }
+
+    /**
+     * 取消订阅(仅标记,不物理删除)
+     */
+    public void cancelSubscription(Long familyId) {
+        MemberSubscription sub = getActiveSubscription(familyId);
+        if (sub != null) {
+            sub.setStatus("cancelled");
+            sub.setAutoRenew(0);
+            sub.setUpdatedAt(new Date());
+            subscriptionMapper.updateById(sub);
+        }
+    }
+
+    /**
+     * 启用/禁用自动续费
+     */
+    public void setAutoRenew(Long familyId, Integer autoRenew) {
+        MemberSubscription sub = getActiveSubscription(familyId);
+        if (sub != null) {
+            sub.setAutoRenew(autoRenew);
+            sub.setUpdatedAt(new Date());
+            subscriptionMapper.updateById(sub);
+        }
+    }
+
+    // ==================== 私有方法 ====================
+
+    private Date calculateExpireTime(String paymentType) {
+        Date now = new Date();
+        if ("yearly".equals(paymentType)) {
+            return new Date(now.getTime() + 365L * 24 * 60 * 60 * 1000);
+        } else if ("quarterly".equals(paymentType)) {
+            return new Date(now.getTime() + 90L * 24 * 60 * 60 * 1000);
+        } else if ("monthly".equals(paymentType)) {
+            return new Date(now.getTime() + 30L * 24 * 60 * 60 * 1000);
+        }
+        // 默认年费
+        return new Date(now.getTime() + 365L * 24 * 60 * 60 * 1000);
+    }
+
+    private Date calculateExpireTimeFrom(Date baseDate, String paymentType) {
+        if ("yearly".equals(paymentType)) {
+            return new Date(baseDate.getTime() + 365L * 24 * 60 * 60 * 1000);
+        } else if ("quarterly".equals(paymentType)) {
+            return new Date(baseDate.getTime() + 90L * 24 * 60 * 60 * 1000);
+        } else if ("monthly".equals(paymentType)) {
+            return new Date(baseDate.getTime() + 30L * 24 * 60 * 60 * 1000);
+        }
+        return new Date(baseDate.getTime() + 365L * 24 * 60 * 60 * 1000);
+    }
+}

+ 87 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/PlanActivationService.java

@@ -0,0 +1,87 @@
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.entity.Task;
+import com.etotem.cfc.entity.TaskPlanInstance;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.TaskMapper;
+import com.etotem.cfc.mapper.TaskPlanInstanceMapper;
+import com.etotem.cfc.mapper.UserMapper;
+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.util.Calendar;
+import java.util.Date;
+
+/**
+ * 方案激活服务。
+ * 将已审核通过的方案转为实际任务实例,进入执行阶段。
+ */
+@Service
+public class PlanActivationService {
+
+    private static final Logger log = LoggerFactory.getLogger(PlanActivationService.class);
+
+    @Resource
+    private TaskPlanInstanceMapper planMapper;
+
+    @Resource
+    private TaskMapper taskMapper;
+
+    @Resource
+    private UserMapper userMapper;
+
+    /**
+     * 激活方案:
+     * 1. 校验状态必须为 reviewed
+     * 2. 生成一条引导性任务(方案概览/首日任务)
+     * 3. 标记方案为 active
+     */
+    @Transactional
+    public void activate(Long planId, Long operatorId) {
+        TaskPlanInstance plan = planMapper.selectById(planId);
+        if (plan == null) throw new IllegalStateException("方案不存在");
+        if (!"reviewed".equals(plan.getStatus())) {
+            throw new IllegalStateException("方案状态异常,需要先审核通过");
+        }
+        User child = userMapper.selectById(plan.getChildId());
+        if (child == null) throw new IllegalStateException("孩子不存在");
+
+        // 生成首日引导任务
+        Task task = new Task();
+        task.setFamilyId(plan.getFamilyId());
+        task.setChildId(plan.getChildId());
+        task.setCreatorId(operatorId);
+        task.setExecutorType("child");
+        task.setExecutorId(plan.getChildId());
+        task.setTitle("方案启动: " + plan.getName());
+        task.setDescription("请阅读并开始执行测评方案");
+        task.setPoints(2);
+        task.setCategory("成长");
+        task.setStatus("pending");
+        task.setSourceType("plan");
+        task.setSourceId(plan.getId());
+        task.setNeedReview(0);
+        task.setIsTemplate(0);
+        Calendar cal = Calendar.getInstance();
+        cal.set(Calendar.HOUR_OF_DAY, 23);
+        cal.set(Calendar.MINUTE, 59);
+        cal.set(Calendar.SECOND, 59);
+        task.setDeadline(cal.getTime());
+        task.setCreatedAt(new Date());
+        task.setUpdatedAt(new Date());
+        taskMapper.insert(task);
+
+        // 标记方案已激活
+        plan.setStatus("active");
+        plan.setActivatedAt(new Date());
+        plan.setTotalDays(plan.getTotalDays() != null ? plan.getTotalDays() : 14);
+        plan.setCompletedDays(0);
+        plan.setUpdatedAt(new Date());
+        planMapper.updateById(plan);
+
+        log.info("方案激活 planId={}, childId={}, taskId={}", planId, plan.getChildId(), task.getId());
+    }
+}

+ 23 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java

@@ -7,6 +7,7 @@ import com.etotem.cfc.dto.CreateProductOrderDTO;
 import com.etotem.cfc.dto.OrderItemVO;
 import com.etotem.cfc.dto.ProductOrderDTO;
 import com.etotem.cfc.entity.AfterSalesRequest;
+import com.etotem.cfc.entity.AssessmentProduct;
 import com.etotem.cfc.entity.Product;
 import com.etotem.cfc.entity.ProductOrder;
 import com.etotem.cfc.entity.User;
@@ -15,6 +16,8 @@ import com.etotem.cfc.mapper.ProductMapper;
 import com.etotem.cfc.mapper.ProductOrderMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.CommissionService;
+import com.etotem.cfc.service.AssessmentProductService;
+import com.etotem.cfc.service.AssessmentQuotaService;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
@@ -67,6 +70,12 @@ public class ProductOrderService {
     @Resource
     private AfterSalesRequestMapper afterSalesRequestMapper;
 
+    @Resource
+    private AssessmentProductService assessmentProductService;
+
+    @Resource
+    private AssessmentQuotaService assessmentQuotaService;
+
     public Result<ProductOrderDTO> create(CreateProductOrderDTO dto, Long buyerId) {
         if (buyerId == null) {
             return Result.error("请先登录");
@@ -282,6 +291,20 @@ public class ProductOrderService {
         } catch (Exception e) {
             // Do not block payment flow
         }
+        // 测评商品:支付成功后自动发放测评额度
+        try {
+            if (paidProduct != null && Product.TYPE_ASSESSMENT.equals(paidProduct.getProductType())) {
+                AssessmentProduct ext = assessmentProductService.getByProductId(paidProduct.getId());
+                if (ext != null) {
+                    assessmentQuotaService.grantQuota(order,
+                            ext.getTotalSessions(), ext.getValidityDays());
+                    log.info("测评商品订单{}已发放额度", orderNo);
+                }
+            }
+        } catch (Exception e) {
+            log.error("发放测评额度失败: orderNo={}", orderNo, e);
+            // 不阻塞支付流程
+        }
         return Result.success("支付成功");
     }
 

+ 43 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java

@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.dto.ProductDTO;
 import com.etotem.cfc.dto.ProductListQueryDTO;
+import com.etotem.cfc.entity.AssessmentProduct;
 import com.etotem.cfc.entity.Product;
 import com.etotem.cfc.entity.User;
 import com.etotem.cfc.mapper.ProductMapper;
@@ -32,6 +33,9 @@ public class ProductService {
     @Resource
     private MembershipService membershipService;
 
+    @Resource
+    private AssessmentProductService assessmentProductService;
+
     public Result<Map<String, Object>> list(ProductListQueryDTO query, Long userId) {
         Page<Product> page = new Page<>(query.getPage(), query.getSize());
         LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<Product>()
@@ -257,7 +261,17 @@ public class ProductService {
         if (product == null) {
             return Result.error("商品不存在");
         }
-        return Result.success(ProductDTO.from(product));
+        ProductDTO dto = ProductDTO.from(product);
+        if (Product.TYPE_ASSESSMENT.equals(product.getProductType())) {
+            AssessmentProduct ext = assessmentProductService.getByProductId(productId);
+            if (ext != null) {
+                dto.setAssessmentType(ext.getAssessmentType());
+                dto.setTotalSessions(ext.getTotalSessions());
+                dto.setValidityDays(ext.getValidityDays());
+                dto.setGuideScope(ext.getGuideScope());
+            }
+        }
+        return Result.success(dto);
     }
 
     /**
@@ -271,6 +285,17 @@ public class ProductService {
         return Result.success(ProductDTO.from(product));
     }
 
+    /**
+     * 管理员创建商品(含测评扩展信息)
+     */
+    public Result<ProductDTO> adminCreate(Product product, AssessmentProduct assessmentExt) {
+        Result<ProductDTO> result = adminCreate(product);
+        if (Product.TYPE_ASSESSMENT.equals(product.getProductType()) && assessmentExt != null) {
+            assessmentProductService.saveOrUpdate(product.getId(), assessmentExt);
+        }
+        return result;
+    }
+
     /**
      * 管理员更新商品
      */
@@ -286,6 +311,23 @@ public class ProductService {
         return Result.success(ProductDTO.from(productMapper.selectById(product.getId())));
     }
 
+    /**
+     * 管理员更新商品(含测评扩展信息)
+     */
+    public Result<ProductDTO> adminUpdate(Product product, AssessmentProduct assessmentExt) {
+        Result<ProductDTO> result = adminUpdate(product);
+        if (result.getCode() != 200) {
+            return result;
+        }
+        if (Product.TYPE_ASSESSMENT.equals(product.getProductType())) {
+            assessmentProductService.saveOrUpdate(product.getId(), assessmentExt);
+        } else {
+            // 非测评商品则清理扩展信息
+            assessmentProductService.saveOrUpdate(product.getId(), null);
+        }
+        return result;
+    }
+
     /**
      * 管理员删除商品
      */

+ 149 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/PromotionTierService.java

@@ -0,0 +1,149 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.PromotionTier;
+import com.etotem.cfc.entity.PromotionTierChangeLog;
+import com.etotem.cfc.mapper.PromotionTierChangeLogMapper;
+import com.etotem.cfc.mapper.PromotionTierMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+
+@Service
+public class PromotionTierService {
+
+    @Resource
+    private PromotionTierMapper tierMapper;
+
+    @Resource
+    private PromotionTierChangeLogMapper changeLogMapper;
+
+    /**
+     * 获取用户当前推广等级
+     */
+    public PromotionTier getCurrentTier(Long userId) {
+        return tierMapper.selectOne(
+                new LambdaQueryWrapper<PromotionTier>()
+                        .eq(PromotionTier::getUserId, userId)
+                        .orderByDesc(PromotionTier::getUpdatedAt)
+                        .last("LIMIT 1")
+        );
+    }
+
+    /**
+     * 获取所有等级配置
+     */
+    public List<PromotionTier> getAllTierConfigs() {
+        return tierMapper.selectList(null);
+    }
+
+    /**
+     * 更新团队规模(当有新推荐人加入时调用)
+     */
+    public void updateTeamSize(Long userId, int level, int delta) {
+        PromotionTier tier = getCurrentTier(userId);
+        if (tier == null) {
+            tier = new PromotionTier();
+            tier.setUserId(userId);
+            tier.setTier("R0");
+            tier.setTeamSize1st(0);
+            tier.setTeamSize2nd(0);
+            tier.setTeamSize3rd(0);
+            tier.setTotalTeamSize(0);
+            tier.setTotalReferralEarnings(0);
+            tier.setTotalShareEarnings(0);
+            tier.setCreatedAt(new Date());
+            tier.setUpdatedAt(new Date());
+            tierMapper.insert(tier);
+        }
+
+        if (level == 1) {
+            tier.setTeamSize1st(tier.getTeamSize1st() + delta);
+        } else if (level == 2) {
+            tier.setTeamSize2nd(tier.getTeamSize2nd() + delta);
+        } else if (level == 3) {
+            tier.setTeamSize3rd(tier.getTeamSize3rd() + delta);
+        }
+        tier.setTotalTeamSize(
+                (tier.getTeamSize1st() != null ? tier.getTeamSize1st() : 0) +
+                (tier.getTeamSize2nd() != null ? tier.getTeamSize2nd() : 0) +
+                (tier.getTeamSize3rd() != null ? tier.getTeamSize3rd() : 0)
+        );
+        tier.setLastChangeAt(new Date());
+        tier.setUpdatedAt(new Date());
+        tierMapper.updateById(tier);
+    }
+
+    /**
+     * 累加推荐佣金
+     */
+    public void addReferralEarnings(Long userId, int amount) {
+        PromotionTier tier = getCurrentTier(userId);
+        if (tier == null) {
+            return;
+        }
+        tier.setTotalReferralEarnings(
+                (tier.getTotalReferralEarnings() != null ? tier.getTotalReferralEarnings() : 0) + amount
+        );
+        tier.setLastChangeAt(new Date());
+        tier.setUpdatedAt(new Date());
+        tierMapper.updateById(tier);
+    }
+
+    /**
+     * 累加消费分润
+     */
+    public void addShareEarnings(Long userId, int amount) {
+        PromotionTier tier = getCurrentTier(userId);
+        if (tier == null) {
+            return;
+        }
+        tier.setTotalShareEarnings(
+                (tier.getTotalShareEarnings() != null ? tier.getTotalShareEarnings() : 0) + amount
+        );
+        tier.setLastChangeAt(new Date());
+        tier.setUpdatedAt(new Date());
+        tierMapper.updateById(tier);
+    }
+
+    /**
+     * 手动更新推广等级(管理员)
+     */
+    public void updateTier(Long userId, String tierCode) {
+        PromotionTier tier = getCurrentTier(userId);
+        String fromTier = tier != null ? tier.getTier() : null;
+
+        PromotionTierChangeLog log = new PromotionTierChangeLog();
+        log.setUserId(userId);
+        log.setOldTier(fromTier);
+        log.setNewTier(tierCode);
+        log.setReason("admin");
+        log.setChangedAt(new Date());
+        changeLogMapper.insert(log);
+
+        if (tier == null) {
+            tier = new PromotionTier();
+            tier.setUserId(userId);
+            tier.setTier(tierCode);
+            tier.setTeamSize1st(0);
+            tier.setTeamSize2nd(0);
+            tier.setTeamSize3rd(0);
+            tier.setTotalTeamSize(0);
+            tier.setTotalReferralEarnings(0);
+            tier.setTotalShareEarnings(0);
+            tier.setPromotedAt(new Date());
+            tier.setLastChangeAt(new Date());
+            tier.setCreatedAt(new Date());
+            tier.setUpdatedAt(new Date());
+            tierMapper.insert(tier);
+        } else {
+            tier.setTier(tierCode);
+            tier.setPromotedAt(new Date());
+            tier.setLastChangeAt(new Date());
+            tier.setUpdatedAt(new Date());
+            tierMapper.updateById(tier);
+        }
+    }
+}

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

@@ -157,4 +157,8 @@ public class ReportParseService {
             setter.accept(section);
         }
     }
+
+    public void asyncDeepParse(Long resultId, String filePath) {
+        log.info("asyncDeepParse called for resultId={}, filePath={}", resultId, filePath);
+    }
 }

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

@@ -412,6 +412,7 @@ CREATE TABLE IF NOT EXISTS dan_assessment_results (
     -- 扩展描述字段
     process_desc TEXT COMMENT '过程描述',
     result_desc TEXT COMMENT '结果简评',
+    source VARCHAR(20) DEFAULT 'planner_entry' COMMENT '报告来源: planner_entry/parent_upload/auto_fetch',
     INDEX idx_child_id (child_id),
     INDEX idx_teacher_id (teacher_id),
     INDEX idx_assessment_date (assessment_date)
@@ -2312,3 +2313,186 @@ CREATE TABLE IF NOT EXISTS butler_sessions (
     INDEX idx_status (status),
     INDEX idx_user_status (user_id, status)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI管家会话表';
+
+-- 家庭订阅记录
+CREATE TABLE IF NOT EXISTS member_subscription (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    family_id BIGINT NOT NULL COMMENT '家庭ID',
+    `level` VARCHAR(10) NOT NULL DEFAULT 'L1' COMMENT 'L1=一生一世, L2=久久一生',
+    status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT 'active/expired/cancelled',
+    start_time DATETIME DEFAULT NULL COMMENT '开始时间',
+    expire_time DATETIME DEFAULT NULL COMMENT '到期时间',
+    payment_id BIGINT DEFAULT NULL COMMENT '关联订单ID',
+    auto_renew TINYINT(1) DEFAULT 0 COMMENT '自动续费(L2)',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_family_id (family_id),
+    INDEX idx_status (status),
+    INDEX idx_expire_time (expire_time)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭订阅记录';
+
+-- 订阅订单
+CREATE TABLE IF NOT EXISTS member_subscription_order (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    family_id BIGINT NOT NULL COMMENT '家庭ID',
+    `level` VARCHAR(10) NOT NULL COMMENT 'L1/L2',
+    amount INT NOT NULL COMMENT '金额(分)',
+    status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending/paid/cancelled/refunded',
+    payment_type VARCHAR(20) DEFAULT NULL COMMENT '支付方式 wechat/alipay',
+    transaction_id VARCHAR(64) DEFAULT NULL COMMENT '微信支付订单号',
+    order_no VARCHAR(64) NOT NULL COMMENT '订单号',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    paid_at DATETIME DEFAULT NULL,
+    UNIQUE KEY uk_order_no (order_no),
+    INDEX idx_family_id (family_id),
+    INDEX idx_status (status)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订阅订单';
+
+-- 权益使用日志
+CREATE TABLE IF NOT EXISTS subscription_benefit_log (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    subscription_id BIGINT NOT NULL COMMENT '订阅ID',
+    benefit_code VARCHAR(50) NOT NULL COMMENT '权益代码',
+    used_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    detail TEXT COMMENT '使用详情(JSON)',
+    INDEX idx_subscription_id (subscription_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权益使用日志';
+
+-- 推广等级 R0-R4
+CREATE TABLE IF NOT EXISTS promotion_tier (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    user_id BIGINT NOT NULL COMMENT '用户ID',
+    tier VARCHAR(20) NOT NULL DEFAULT 'R0' COMMENT 'R0利他者/R1传福人/R2聚能师/R3启慧者/R4传承者',
+    team_size_1st INT DEFAULT 0 COMMENT '一层团队人数',
+    team_size_2nd INT DEFAULT 0 COMMENT '二层团队人数',
+    team_size_3rd INT DEFAULT 0 COMMENT '三层团队人数',
+    total_team_size INT DEFAULT 0 COMMENT '三层累计总人数',
+    total_referral_earnings INT DEFAULT 0 COMMENT '累计推荐佣金(分)',
+    total_share_earnings INT DEFAULT 0 COMMENT '累计消费分润(分)',
+    promoted_at DATETIME DEFAULT NULL COMMENT '最近晋级时间',
+    last_change_at DATETIME DEFAULT NULL COMMENT '最后变更时间',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_user_id (user_id),
+    INDEX idx_tier (tier)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推广等级';
+
+-- 推广等级变更记录
+CREATE TABLE IF NOT EXISTS promotion_tier_change_log (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    user_id BIGINT NOT NULL COMMENT '用户ID',
+    old_tier VARCHAR(20) DEFAULT NULL COMMENT '变更前等级',
+    new_tier VARCHAR(20) NOT NULL COMMENT '变更后等级',
+    reason VARCHAR(50) DEFAULT NULL COMMENT 'qualify升级/demote降级/admin管理员调整',
+    changed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    INDEX idx_user_id (user_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推广等级变更记录';
+
+-- 推荐关系树(三层)
+CREATE TABLE IF NOT EXISTS referral_tree (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    parent_id BIGINT NOT NULL COMMENT '推荐人ID',
+    child_id BIGINT NOT NULL COMMENT '被推荐人ID',
+    level INT NOT NULL COMMENT '层级深度(1/2/3)',
+    path VARCHAR(255) DEFAULT NULL COMMENT 'materialized path如/1/2/3/',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_child_id (child_id),
+    INDEX idx_parent_id (parent_id),
+    INDEX idx_level (level)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推荐关系树';
+
+-- 产品P点配置
+CREATE TABLE IF NOT EXISTS product_ppoint (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    product_id BIGINT NOT NULL COMMENT '产品ID',
+    ppoint INT NOT NULL DEFAULT 0 COMMENT 'P点值(平台利润分)',
+    start_date DATE DEFAULT NULL COMMENT '生效日期',
+    end_date DATE DEFAULT NULL COMMENT '失效日期',
+    created_by BIGINT DEFAULT NULL COMMENT '创建人',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    INDEX idx_product_id (product_id),
+    INDEX idx_dates (start_date, end_date)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='产品P点配置';
+
+-- 测评商品扩展信息
+CREATE TABLE IF NOT EXISTS assessment_products (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    product_id BIGINT NOT NULL COMMENT '关联products表',
+    assessment_type VARCHAR(20) NOT NULL COMMENT 'single=单包, bundle=套餐',
+    total_sessions INT NOT NULL DEFAULT 1 COMMENT '包含测评次数',
+    validity_days INT NOT NULL DEFAULT 365 COMMENT '有效期(天)',
+    guide_scope VARCHAR(50) DEFAULT 'all' COMMENT '规划师范围: all/assigned/auto',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_product_id (product_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='测评商品扩展信息';
+
+-- 测评额度表
+CREATE TABLE IF NOT EXISTS assessment_quotas (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    quota_no VARCHAR(32) NOT NULL COMMENT '额度编号',
+    family_id BIGINT NOT NULL COMMENT '家庭ID',
+    child_id BIGINT COMMENT '指定孩子ID(NULL=不限)',
+    product_order_id BIGINT NOT NULL COMMENT '来源订单ID',
+    product_id BIGINT NOT NULL COMMENT '商品ID',
+    product_name VARCHAR(100) NOT NULL COMMENT '商品名快照',
+    total_sessions INT NOT NULL DEFAULT 1 COMMENT '总次数',
+    remaining_sessions INT NOT NULL DEFAULT 1 COMMENT '剩余次数',
+    validity_start DATETIME NOT NULL COMMENT '有效期开始',
+    validity_end DATETIME NOT NULL COMMENT '有效期截止',
+    status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT 'active/expired/used_all/cancelled',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_family (family_id),
+    INDEX idx_order (product_order_id),
+    UNIQUE KEY uk_quota_no (quota_no)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='测评额度表';
+
+-- 测评执行记录表
+CREATE TABLE IF NOT EXISTS assessment_executions (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    quota_id BIGINT NOT NULL COMMENT '关联额度ID',
+    child_id BIGINT NOT NULL COMMENT '被测评孩子',
+    guide_id BIGINT COMMENT '执行规划师ID',
+    assessment_type VARCHAR(30) COMMENT '本次具体测评类型',
+    appointment_id BIGINT COMMENT '关联预约ID',
+    result_id BIGINT COMMENT '关联测评结果ID',
+    status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending/in_progress/completed/cancelled',
+    scheduled_date DATETIME COMMENT '预约执行时间',
+    completed_at DATETIME COMMENT '实际完成时间',
+    notes VARCHAR(500) COMMENT '备注',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_quota (quota_id),
+    INDEX idx_child (child_id),
+    INDEX idx_appointment (appointment_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='测评执行记录表';
+
+-- 测评指导意见表(结构化存储)
+CREATE TABLE IF NOT EXISTS growth_guidances (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    result_id BIGINT NOT NULL COMMENT '关联测评结果ID',
+    dimension VARCHAR(30) NOT NULL COMMENT '维度: attention/focus/memory/logic/emotion/general',
+    score INT COMMENT '该维度得分(0-100)',
+    suggestion TEXT NOT NULL COMMENT '建议内容',
+    priority INT DEFAULT 0 COMMENT '优先级(0=普通, 1=重要, 2=紧急)',
+    sort_order INT DEFAULT 0 COMMENT '排序',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_result (result_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='测评指导意见(结构化)';
+
+-- 方案生成规则表
+CREATE TABLE IF NOT EXISTS assessment_plan_rules (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    dimension VARCHAR(30) NOT NULL COMMENT '测评维度: attention/focus/memory/logic/emotion',
+    score_min INT NOT NULL COMMENT '分数下限(含)',
+    score_max INT NOT NULL COMMENT '分数上限(含)',
+    template_id BIGINT NOT NULL COMMENT '关联TaskTemplatePackage ID',
+    priority INT DEFAULT 0 COMMENT '优先级',
+    is_active TINYINT(1) DEFAULT 1 COMMENT '是否启用',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_dimension (dimension),
+    INDEX idx_score (score_min, score_max)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='方案生成规则';

+ 16 - 0
cfc-frontend/pages.json

@@ -222,6 +222,14 @@
         {
           "path": "activities/detail",
           "style": { "navigationBarTitleText": "活动详情" }
+        },
+        {
+          "path": "plans/index",
+          "style": { "navigationBarTitleText": "测评方案审核" }
+        },
+        {
+          "path": "plans/detail",
+          "style": { "navigationBarTitleText": "方案详情" }
         }
       ]
     },
@@ -365,6 +373,14 @@
         {
           "path": "purchase",
           "style": { "navigationBarTitleText": "购买测评" }
+        },
+        {
+          "path": "quota",
+          "style": { "navigationBarTitleText": "我的测评额度" }
+        },
+        {
+          "path": "upload-report",
+          "style": { "navigationBarTitleText": "上传测评报告" }
         }
       ]
     },

+ 82 - 56
cfc-frontend/pages/assessment/apply.vue

@@ -100,8 +100,19 @@
       </view>
     </view>
 
-    <!-- 价格信息 -->
-    <view class="section" v-if="selectedGuideId">
+    <!-- 余额显示(从额度预约时) -->
+    <view class="section" v-if="quotaId">
+      <view class="section-title">额度信息</view>
+      <view class="quota-info">
+        <text class="quota-label">本次将消耗 1 次测评额度</text>
+        <text class="quota-remaining" v-if="quotaRemaining !== null">
+          剩余 {{ quotaRemaining }} 次
+        </text>
+      </view>
+    </view>
+
+    <!-- 价格信息(非额度预约时显示) -->
+    <view class="section" v-if="!quotaId && selectedGuideId">
       <view class="section-title">费用信息</view>
       <view class="price-info">
         <view class="price-row">
@@ -120,16 +131,18 @@
     </view>
 
     <!-- 提交按钮 -->
-    <button class="btn-submit" @click="submit">提交申请</button>
+    <button class="btn-submit" @click="submit">{{ quotaId ? '消耗额度预约' : '提交申请' }}</button>
   </view>
 </template>
 
 <script>
-import { getAvailableGuides, createAssessmentAppointment, getChildren } from '../../utils/api.js'
+import { getAvailableGuides, createAssessmentAppointment, getChildren, getAssessmentQuotas, createAssessmentAppointmentWithQuota } from '../../utils/api.js'
 
 export default {
   data() {
     return {
+      quotaId: null,
+      quotaRemaining: null,
       availableGuides: [],
       selectedGuideId: null,
       selectedGuideName: '',
@@ -145,30 +158,42 @@ export default {
     }
   },
   onLoad(options) {
-    // 如果有邀请码参数,自动填充
+    this.quotaId = options.quotaId || null
+
+    if (this.quotaId) {
+      this.loadQuotaInfo()
+    }
+
     if (options.inviteCode) {
       this.inviteCode = options.inviteCode
       this.validateInviteCode()
     }
 
-    // 如果有套餐ID参数,自动选择
-    if (options.packageId) {
-      this.loadAvailableGuides(options.packageId)
-    } else {
-      this.loadAvailableGuides()
-    }
-
     this.loadChildren()
   },
   methods: {
+    async loadQuotaInfo() {
+      try {
+        const res = await getAssessmentQuotas()
+        if (res.code === 200) {
+          const quotas = res.data || []
+          const current = quotas.find(q => q.id == this.quotaId)
+          if (current) {
+            this.quotaRemaining = current.remainingSessions
+          }
+        }
+      } catch (e) {
+        console.error('获取额度信息失败', e)
+      }
+    },
+
     async loadAvailableGuides(packageId) {
       try {
         const res = await getAvailableGuides(packageId)
         if (res.code === 200) {
           this.availableGuides = res.data || []
-          // 如果有邀请码,自动选择对应的成长规划师
           if (this.inviteCodeInfo && this.inviteCodeInfo.guideId) {
-            const guide = this.availableGuides.find(g => g.guideId === this.inviteInfo.guideId)
+            const guide = this.availableGuides.find(g => g.guideId === this.inviteCodeInfo.guideId)
             if (guide) {
               this.selectGuide(guide)
             }
@@ -184,7 +209,6 @@ export default {
         const res = await getChildren()
         if (res.code === 200) {
           this.children = res.data || []
-          // 如果只有一个孩子,自动选择
           if (this.children.length === 1) {
             this.selectedChildId = this.children[0].id
           }
@@ -221,7 +245,6 @@ export default {
         this.totalPrice = 0
         return
       }
-
       const guide = this.availableGuides.find(g => g.guideId === this.selectedGuideId)
       if (guide) {
         this.totalPrice = guide.price
@@ -230,15 +253,11 @@ export default {
     },
 
     async validateInviteCode() {
-      if (!this.inviteCode) {
-        return
-      }
-
+      if (!this.inviteCode) return
       try {
         const res = await validateInviteCode(this.inviteCode)
         if (res.code === 200) {
           this.inviteCodeInfo = res.data
-          // 自动选择邀请码关联的成长规划师
           if (this.inviteCodeInfo.guideId) {
             const guide = this.availableGuides.find(g => g.guideId === this.inviteCodeInfo.guideId)
             if (guide) {
@@ -252,22 +271,18 @@ export default {
     },
 
     async submit() {
-      // 验证表单(成长规划师可选)
       if (!this.selectedType) {
         uni.showToast({ title: '请选择测评类型', icon: 'none' })
         return
       }
-
       if (!this.appointmentDate) {
         uni.showToast({ title: '请选择预约日期', icon: 'none' })
         return
       }
-
       if (!this.appointmentTime) {
         uni.showToast({ title: '请选择预约时间', icon: 'none' })
         return
       }
-
       if (!this.selectedChildId) {
         uni.showToast({ title: '请选择孩子', icon: 'none' })
         return
@@ -276,43 +291,54 @@ export default {
       try {
         uni.showLoading({ title: '提交中...' })
 
-        const userId = uni.getStorageSync('userId')
-        const familyId = uni.getStorageSync('familyId')
-
-        const res = await createAssessmentAppointment({
-          userId: userId,
-          familyId: familyId,
-          childId: this.selectedChildId,
-          teacherId: this.selectedGuideId,
-          appointmentType: this.selectedType,
-          appointmentDate: this.appointmentDate,
-          appointmentTime: this.appointmentTime,
-          durationMinutes: 60
-        })
-
-        uni.hideLoading()
-
-        if (res.code === 200) {
-          uni.showModal({
-            title: '申请成功',
-            content: '您的测评申请已提交,请等待成长规划师确认',
-            showCancel: false,
-            success: () => {
-              uni.navigateBack()
-            }
+        if (this.quotaId) {
+          const res = await createAssessmentAppointmentWithQuota({
+            childId: this.selectedChildId,
+            guideId: this.selectedGuideId,
+            quotaId: this.quotaId,
+            appointmentDate: this.appointmentDate,
+            appointmentTime: this.appointmentTime,
+            notes: ''
           })
+          uni.hideLoading()
+          if (res.code === 200) {
+            uni.showModal({
+              title: '预约成功',
+              content: '您的测评预约已提交,请等待成长规划师确认',
+              showCancel: false,
+              success: () => uni.navigateBack()
+            })
+          } else {
+            uni.showToast({ title: res.message || '预约失败', icon: 'none' })
+          }
         } else {
-          uni.showToast({
-            title: res.message || '提交失败',
-            icon: 'none'
+          const userId = uni.getStorageSync('userId')
+          const familyId = uni.getStorageSync('familyId')
+          const res = await createAssessmentAppointment({
+            userId: userId,
+            familyId: familyId,
+            childId: this.selectedChildId,
+            teacherId: this.selectedGuideId,
+            appointmentType: this.selectedType,
+            appointmentDate: this.appointmentDate,
+            appointmentTime: this.appointmentTime,
+            durationMinutes: 60
           })
+          uni.hideLoading()
+          if (res.code === 200) {
+            uni.showModal({
+              title: '申请成功',
+              content: '您的测评申请已提交,请等待成长规划师确认',
+              showCancel: false,
+              success: () => uni.navigateBack()
+            })
+          } else {
+            uni.showToast({ title: res.message || '提交失败', icon: 'none' })
+          }
         }
       } catch (e) {
         uni.hideLoading()
-        uni.showToast({
-          title: '提交失败',
-          icon: 'none'
-        })
+        uni.showToast({ title: '提交失败', icon: 'none' })
       }
     }
   }

+ 240 - 0
cfc-frontend/pages/assessment/quota.vue

@@ -0,0 +1,240 @@
+<template>
+  <view class="container">
+    <view class="header">
+      <text class="title">我的测评额度</text>
+      <text class="subtitle">购买测评套餐后获得可使用次数</text>
+    </view>
+
+    <view class="quota-list" v-if="quotas.length > 0">
+      <view class="quota-card" v-for="item in quotas" :key="item.id">
+        <view class="card-top">
+          <text class="product-name">{{ item.productName }}</text>
+          <text class="status-tag" :class="item.status === 'active' ? 'active' : 'used'">
+            {{ item.status === 'active' ? '可用' : '已用完' }}
+          </text>
+        </view>
+        <view class="progress-section">
+          <view class="progress-bar-bg">
+            <view class="progress-bar-fill" :style="{ width: progressPercent(item) + '%' }"></view>
+          </view>
+          <text class="progress-text">{{ item.usedSessions }}/{{ item.totalSessions }} 次</text>
+        </view>
+        <view class="card-info">
+          <view class="info-row">
+            <text class="info-label">剩余</text>
+            <text class="info-value highlight">{{ item.remainingSessions }} 次</text>
+          </view>
+          <view class="info-row" v-if="item.validityEnd">
+            <text class="info-label">有效期至</text>
+            <text class="info-value" :class="{ expired: item.expired }">
+              {{ formatDate(item.validityEnd) }}
+              <text v-if="item.expired">(已过期)</text>
+            </text>
+          </view>
+        </view>
+        <view class="card-actions" v-if="item.remainingSessions > 0 && !item.expired">
+          <button class="btn-use" @click="goAppoint(item)">预约测评</button>
+        </view>
+      </view>
+    </view>
+
+    <view class="empty" v-else>
+      <text class="empty-icon">📋</text>
+      <text class="empty-text">暂无可用的测评额度</text>
+      <button class="btn-buy" @click="goBuy">去购买测评套餐</button>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getAssessmentQuotas } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      quotas: []
+    }
+  },
+  onShow() {
+    this.loadQuotas()
+  },
+  methods: {
+    async loadQuotas() {
+      try {
+        uni.showLoading({ title: '加载中...' })
+        const res = await getAssessmentQuotas()
+        uni.hideLoading()
+        if (res.code === 200) {
+          this.quotas = res.data || []
+        }
+      } catch (e) {
+        uni.hideLoading()
+        console.error('加载额度失败', e)
+      }
+    },
+    progressPercent(item) {
+      if (!item.totalSessions || item.totalSessions === 0) return 0
+      return Math.min(100, (item.usedSessions / item.totalSessions) * 100)
+    },
+    formatDate(dateStr) {
+      if (!dateStr) return ''
+      const d = new Date(dateStr)
+      return d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate()
+    },
+    goAppoint(item) {
+      uni.navigateTo({
+        url: '/pages/assessment/apply?quotaId=' + item.id
+      })
+    },
+    goBuy() {
+      uni.navigateTo({
+        url: '/pages/shop/index/index?type=assessment'
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  padding: 30rpx;
+  background: #F8F8F8;
+  min-height: 100vh;
+}
+.header {
+  text-align: center;
+  padding: 40rpx 0 30rpx;
+}
+.title {
+  font-size: 44rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 16rpx;
+}
+.subtitle {
+  font-size: 26rpx;
+  color: #999;
+}
+.quota-list {
+  display: flex;
+  flex-direction: column;
+  gap: 24rpx;
+}
+.quota-card {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 28rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+}
+.card-top {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20rpx;
+}
+.product-name {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+}
+.status-tag {
+  font-size: 22rpx;
+  padding: 4rpx 16rpx;
+  border-radius: 20rpx;
+}
+.status-tag.active {
+  background: #E8F5E9;
+  color: #2E7D32;
+}
+.status-tag.used {
+  background: #F5F5F5;
+  color: #999;
+}
+.progress-section {
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+  margin-bottom: 20rpx;
+}
+.progress-bar-bg {
+  flex: 1;
+  height: 12rpx;
+  background: #F0F0F0;
+  border-radius: 6rpx;
+  overflow: hidden;
+}
+.progress-bar-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #667eea, #764ba2);
+  border-radius: 6rpx;
+}
+.progress-text {
+  font-size: 24rpx;
+  color: #999;
+  white-space: nowrap;
+}
+.card-info {
+  border-top: 1rpx solid #F0F0F0;
+  padding-top: 16rpx;
+}
+.info-row {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 8rpx;
+}
+.info-label {
+  font-size: 26rpx;
+  color: #666;
+}
+.info-value {
+  font-size: 26rpx;
+  color: #333;
+}
+.info-value.highlight {
+  color: #667eea;
+  font-weight: bold;
+  font-size: 32rpx;
+}
+.info-value.expired {
+  color: #F44336;
+}
+.card-actions {
+  margin-top: 20rpx;
+}
+.btn-use {
+  width: 100%;
+  height: 76rpx;
+  line-height: 76rpx;
+  background: linear-gradient(135deg, #667eea, #764ba2);
+  color: #fff;
+  border-radius: 38rpx;
+  font-size: 28rpx;
+  text-align: center;
+}
+.empty {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding-top: 120rpx;
+}
+.empty-icon {
+  font-size: 80rpx;
+  margin-bottom: 20rpx;
+}
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+  margin-bottom: 40rpx;
+}
+.btn-buy {
+  width: 300rpx;
+  height: 80rpx;
+  line-height: 80rpx;
+  background: linear-gradient(135deg, #667eea, #764ba2);
+  color: #fff;
+  border-radius: 40rpx;
+  font-size: 28rpx;
+  text-align: center;
+}
+</style>

+ 219 - 0
cfc-frontend/pages/assessment/upload-report.vue

@@ -0,0 +1,219 @@
+<template>
+  <view class="container">
+    <view class="header">
+      <text class="title">上传测评报告</text>
+      <text class="subtitle">支持 PDF / JPG / PNG 格式</text>
+    </view>
+
+    <view class="section">
+      <view class="section-title">选择孩子</view>
+      <view class="child-selector">
+        <view class="child-item"
+          :class="{ selected: selectedChildId === child.id }"
+          v-for="child in children" :key="child.id"
+          @click="selectedChildId = child.id">
+          <text class="child-name">{{ child.nickname || '未命名' }}</text>
+          <text class="child-check" v-if="selectedChildId === child.id">✓</text>
+        </view>
+      </view>
+    </view>
+
+    <view class="section">
+      <view class="section-title">选择文件</view>
+      <view class="file-upload" @click="chooseFile">
+        <text class="upload-icon">📄</text>
+        <text class="upload-text">{{ fileName || '点击选择文件' }}</text>
+      </view>
+    </view>
+
+    <view class="section">
+      <view class="section-title">备注(可选)</view>
+      <textarea class="textarea" v-model="notes" placeholder="添加备注信息..." />
+    </view>
+
+    <button class="btn-submit" :disabled="!canSubmit" @click="submitUpload">
+      {{ uploading ? '上传中...' : '提交报告' }}
+    </button>
+  </view>
+</template>
+
+<script>
+import { uploadAssessmentReport } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      children: [],
+      selectedChildId: null,
+      file: null,
+      fileName: '',
+      notes: '',
+      uploading: false
+    }
+  },
+  computed: {
+    canSubmit() {
+      return this.selectedChildId && this.file && !this.uploading
+    }
+  },
+  onLoad() {
+    this.loadChildren()
+  },
+  methods: {
+    async loadChildren() {
+      try {
+        const res = await getChildren()
+        if (res.code === 200) {
+          this.children = res.data || []
+          if (this.children.length === 1) {
+            this.selectedChildId = this.children[0].id
+          }
+        }
+      } catch (e) {
+        console.error('获取孩子列表失败', e)
+      }
+    },
+    chooseFile() {
+      uni.chooseImage({
+        count: 1,
+        success: (res) => {
+          this.file = res.tempFiles[0]
+          this.fileName = this.file.name || '已选择文件'
+        }
+      })
+    },
+    async submitUpload() {
+      if (!this.canSubmit) return
+      this.uploading = true
+      uni.showLoading({ title: '上传中...' })
+      try {
+        const res = await uploadAssessmentReport(this.file, this.selectedChildId, this.notes)
+        uni.hideLoading()
+        if (res.code === 200) {
+          uni.showModal({
+            title: '上传成功',
+            content: '报告已提交,等待成长规划师确认',
+            showCancel: false,
+            success: () => uni.navigateBack()
+          })
+        } else {
+          uni.showToast({ title: res.message || '上传失败', icon: 'none' })
+        }
+      } catch (e) {
+        uni.hideLoading()
+        uni.showToast({ title: '上传失败', icon: 'none' })
+      }
+      this.uploading = false
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  padding: 30rpx;
+  background: #F8F8F8;
+  min-height: 100vh;
+}
+.header {
+  text-align: center;
+  padding: 40rpx 0 30rpx;
+}
+.title {
+  font-size: 44rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 16rpx;
+}
+.subtitle {
+  font-size: 26rpx;
+  color: #999;
+}
+.section {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 30rpx;
+  margin-bottom: 24rpx;
+}
+.section-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 20rpx;
+}
+.child-selector {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16rpx;
+}
+.child-item {
+  position: relative;
+  background: #F8F8F8;
+  border: 2rpx solid #E0E0E0;
+  border-radius: 10rpx;
+  padding: 20rpx;
+  min-width: 160rpx;
+  text-align: center;
+}
+.child-item.selected {
+  border-color: #667eea;
+  background: #F0F4FF;
+}
+.child-name {
+  font-size: 28rpx;
+  color: #333;
+}
+.child-check {
+  position: absolute;
+  top: -8rpx;
+  right: -8rpx;
+  width: 36rpx;
+  height: 36rpx;
+  background: #667eea;
+  color: #fff;
+  border-radius: 50%;
+  line-height: 36rpx;
+  text-align: center;
+  font-size: 20rpx;
+}
+.file-upload {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 16rpx;
+  height: 160rpx;
+  background: #F8F8F8;
+  border: 2rpx dashed #CCC;
+  border-radius: 12rpx;
+}
+.upload-icon {
+  font-size: 48rpx;
+}
+.upload-text {
+  font-size: 28rpx;
+  color: #666;
+}
+.textarea {
+  width: 100%;
+  height: 160rpx;
+  background: #F8F8F8;
+  border-radius: 10rpx;
+  padding: 20rpx;
+  font-size: 28rpx;
+  box-sizing: border-box;
+}
+.btn-submit {
+  width: 100%;
+  height: 88rpx;
+  line-height: 88rpx;
+  background: linear-gradient(135deg, #667eea, #764ba2);
+  color: #fff;
+  border-radius: 44rpx;
+  font-size: 30rpx;
+  margin-top: 30rpx;
+}
+.btn-submit[disabled] {
+  opacity: 0.5;
+}
+</style>

+ 113 - 0
cfc-frontend/pages/guide/plans/detail.vue

@@ -0,0 +1,113 @@
+<template>
+  <view class="container" v-if="plan">
+    <view class="card">
+      <view class="card-header">
+        <text class="card-title">{{ plan.name || '测评方案' }}</text>
+        <text class="status-tag" :class="'status-' + plan.status">{{ statusLabel(plan.status) }}</text>
+      </view>
+      <view class="card-body">
+        <view class="info-row"><text class="label">家庭ID</text><text>{{ plan.familyId }}</text></view>
+        <view class="info-row"><text class="label">孩子ID</text><text>{{ plan.childId }}</text></view>
+        <view class="info-row"><text class="label">周期</text><text>{{ plan.totalDays || '-' }} 天</text></view>
+        <view class="info-row"><text class="label">来源</text><text>{{ plan.source || '-' }}</text></view>
+        <view class="info-row" v-if="plan.sourceResultId"><text class="label">来源测评</text><text>{{ plan.sourceResultId }}</text></view>
+        <view class="info-row"><text class="label">创建时间</text><text>{{ formatDate(plan.createdAt) }}</text></view>
+        <view class="info-row" v-if="plan.reviewedAt"><text class="label">审核时间</text><text>{{ formatDate(plan.reviewedAt) }}</text></view>
+        <view class="info-row" v-if="plan.activatedAt"><text class="label">激活时间</text><text>{{ formatDate(plan.activatedAt) }}</text></view>
+      </view>
+      <view class="card-remark" v-if="plan.remark">
+        <text class="remark-title">方案说明</text>
+        <text class="remark-text">{{ plan.remark }}</text>
+      </view>
+    </view>
+
+    <view class="action-bar" v-if="plan.status === 'generated'">
+      <button class="btn-approve" @click="handleApprove">批准方案</button>
+      <button class="btn-reject" @click="handleReject">驳回方案</button>
+    </view>
+    <view class="action-bar" v-if="plan.status === 'reviewed'">
+      <button class="btn-activate" @click="handleActivate">激活方案</button>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getPlanDetail, reviewPlan, activatePlan } from '@/utils/api'
+
+export default {
+  data() {
+    return {
+      plan: null
+    }
+  },
+  onLoad(options) {
+    if (options.planId) this.loadPlan(options.planId)
+  },
+  methods: {
+    async loadPlan(planId) {
+      try {
+        const res = await getPlanDetail({ planId })
+        if (res.code === 200) this.plan = res.data
+      } catch (e) { /* ignore */ }
+    },
+    statusLabel(s) {
+      const map = { generated: '待审核', reviewed: '已审核', active: '已激活', completed: '已完成', rejected: '已驳回' }
+      return map[s] || s
+    },
+    formatDate(d) {
+      if (!d) return '-'
+      const date = new Date(d)
+      return date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate()
+    },
+    async handleApprove() {
+      const res = await reviewPlan({ planId: this.plan.id, approved: true })
+      if (res.code === 200) { uni.showToast({ title: '已批准' }); this.loadPlan(this.plan.id) }
+    },
+    async handleReject() {
+      uni.showModal({
+        title: '驳回方案',
+        content: '确定驳回该方案吗?',
+        success: async (r) => {
+          if (!r.confirm) return
+          const res = await reviewPlan({ planId: this.plan.id, approved: false })
+          if (res.code === 200) { uni.showToast({ title: '已驳回' }); this.loadPlan(this.plan.id) }
+        }
+      })
+    },
+    async handleActivate() {
+      uni.showModal({
+        title: '激活方案',
+        content: '激活后将生成任务,确定激活?',
+        success: async (r) => {
+          if (!r.confirm) return
+          const res = await activatePlan({ planId: this.plan.id })
+          if (res.code === 200) { uni.showToast({ title: '已激活' }); this.loadPlan(this.plan.id) }
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container { min-height: 100vh; background: #f5f5f5; padding: 30rpx; }
+.card { background: #fff; border-radius: 16rpx; padding: 30rpx; margin-bottom: 30rpx; }
+.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24rpx; }
+.card-title { font-size: 34rpx; font-weight: bold; }
+.status-tag { font-size: 24rpx; padding: 6rpx 16rpx; border-radius: 6rpx; }
+.status-generated { background: #fff3cd; color: #856404; }
+.status-reviewed { background: #d1ecf1; color: #0c5460; }
+.status-active { background: #d4edda; color: #155724; }
+.status-completed { background: #e2e3e5; color: #383d41; }
+.status-rejected { background: #f8d7da; color: #721c24; }
+.card-body { margin-bottom: 20rpx; }
+.info-row { display: flex; justify-content: space-between; padding: 16rpx 0; border-bottom: 2rpx solid #f5f5f5; font-size: 28rpx; }
+.label { color: #999; }
+.card-remark { background: #f8f9fa; border-radius: 12rpx; padding: 20rpx; }
+.remark-title { font-size: 28rpx; font-weight: bold; display: block; margin-bottom: 12rpx; }
+.remark-text { font-size: 26rpx; color: #666; white-space: pre-wrap; }
+.action-bar { display: flex; gap: 20rpx; padding: 0 30rpx; }
+.btn-approve { flex: 1; background: #27ae60; color: #fff; border-radius: 40rpx; }
+.btn-reject { flex: 1; background: #e74c3c; color: #fff; border-radius: 40rpx; }
+.btn-activate { flex: 1; background: #4A9BD7; color: #fff; border-radius: 40rpx; }
+</style>

+ 188 - 0
cfc-frontend/pages/guide/plans/index.vue

@@ -0,0 +1,188 @@
+<template>
+  <view class="container">
+    <view class="header">
+      <text class="title">测评方案审核</text>
+      <view class="header-actions">
+        <picker :range="['全部', '待审核(generated)', '已审核(reviewed)', '已激活(active)', '已完成(completed)']" @change="onStatusFilter">
+          <view class="filter-btn">
+            <text>{{ statusFilterText }}</text>
+          </view>
+        </picker>
+      </view>
+    </view>
+
+    <view class="family-section" v-for="group in planGroups" :key="group.familyId">
+      <view class="family-header">
+        <text class="family-name">家庭 ID: {{ group.familyId }}</text>
+        <text class="plan-count">{{ group.plans.length }} 个方案</text>
+      </view>
+      <view class="plan-card" v-for="plan in group.plans" :key="plan.id" @click="goDetail(plan)">
+        <view class="plan-header">
+          <text class="plan-title">{{ plan.name || '测评方案' }}</text>
+          <text class="plan-status" :class="'status-' + plan.status">{{ statusLabel(plan.status) }}</text>
+        </view>
+        <view class="plan-meta">
+          <text>孩子ID: {{ plan.childId }}</text>
+          <text>{{ formatDate(plan.createdAt) }}</text>
+        </view>
+        <view class="plan-desc" v-if="plan.remark">{{ plan.remark }}</view>
+        <view class="plan-actions">
+          <view class="action-btn primary" v-if="plan.status === 'generated'" @click.stop="handleReview(plan, true)">
+            <text>批准</text>
+          </view>
+          <view class="action-btn danger" v-if="plan.status === 'generated'" @click.stop="handleReview(plan, false)">
+            <text>驳回</text>
+          </view>
+          <view class="action-btn success" v-if="plan.status === 'reviewed'" @click.stop="handleActivate(plan)">
+            <text>激活</text>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <view class="empty" v-if="planGroups.length === 0">
+      <text>暂无方案</text>
+    </view>
+
+    <uni-popup ref="reviewPopup" type="dialog">
+      <uni-popup-dialog title="审核意见" :content="reviewComment" @confirm="submitReview" @close="reviewComment = ''">
+        <input class="review-input" v-model="reviewComment" placeholder="输入审核意见(可选)" />
+      </uni-popup-dialog>
+    </uni-popup>
+  </view>
+</template>
+
+<script>
+import { listFamilyPlans, reviewPlan, activatePlan, getPlanDetail } from '@/utils/api'
+
+export default {
+  data() {
+    return {
+      plans: [],
+      statusFilter: '',
+      statusFilterText: '全部状态',
+      currentPlan: null,
+      reviewApproved: false,
+      reviewComment: '',
+      families: []
+    }
+  },
+  computed: {
+    planGroups() {
+      const groups = {}
+      for (const p of this.plans) {
+        const key = p.familyId
+        if (!groups[key]) groups[key] = { familyId: key, plans: [] }
+        groups[key].plans.push(p)
+      }
+      return Object.values(groups)
+    }
+  },
+  onShow() {
+    this.loadPlans()
+  },
+  methods: {
+    async loadPlans() {
+      try {
+        const res = await listFamilyPlans({ status: this.statusFilter || null })
+        if (res.code === 200) this.plans = res.data || []
+      } catch (e) { /* ignore */ }
+    },
+    onStatusFilter(e) {
+      const labels = ['全部', '待审核(generated)', '已审核(reviewed)', '已激活(active)', '已完成(completed)']
+      const vals = ['', 'generated', 'reviewed', 'active', 'completed']
+      const idx = e.detail.value
+      this.statusFilterText = labels[idx]
+      this.statusFilter = vals[idx]
+      this.loadPlans()
+    },
+    statusLabel(s) {
+      const map = { generated: '待审核', reviewed: '已审核', active: '已激活', completed: '已完成', rejected: '已驳回' }
+      return map[s] || s
+    },
+    formatDate(d) {
+      if (!d) return ''
+      const date = new Date(d)
+      return date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate()
+    },
+    goDetail(plan) {
+      uni.navigateTo({ url: '/pages/guide/plans/detail?planId=' + plan.id })
+    },
+    handleReview(plan, approved) {
+      this.currentPlan = plan
+      this.reviewApproved = approved
+      this.reviewComment = ''
+      if (!approved) {
+        uni.showModal({
+          title: '驳回方案',
+          content: '确定驳回该方案吗?',
+          success: (res) => {
+            if (res.confirm) this.submitReview()
+          }
+        })
+      } else {
+        this.submitReview()
+      }
+    },
+    async submitReview() {
+      if (!this.currentPlan) return
+      try {
+        const res = await reviewPlan({
+          planId: this.currentPlan.id,
+          approved: this.reviewApproved,
+          comment: this.reviewComment
+        })
+        if (res.code === 200) {
+          uni.showToast({ title: '操作成功' })
+          this.loadPlans()
+        }
+      } catch (e) { /* ignore */ }
+    },
+    handleActivate(plan) {
+      uni.showModal({
+        title: '激活方案',
+        content: '激活后将生成任务,确定激活?',
+        success: async (res) => {
+          if (!res.confirm) return
+          try {
+            const r = await activatePlan({ planId: plan.id })
+            if (r.code === 200) {
+              uni.showToast({ title: '方案已激活' })
+              this.loadPlans()
+            }
+          } catch (e) { /* ignore */ }
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container { min-height: 100vh; background: #f5f5f5; padding: 30rpx; }
+.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30rpx; }
+.title { font-size: 36rpx; font-weight: bold; }
+.filter-btn { padding: 10rpx 20rpx; background: #fff; border-radius: 8rpx; font-size: 26rpx; }
+.family-section { margin-bottom: 30rpx; }
+.family-header { display: flex; justify-content: space-between; padding: 20rpx; background: #e8f4fd; border-radius: 12rpx 12rpx 0 0; }
+.family-name { font-weight: bold; font-size: 28rpx; }
+.plan-count { font-size: 24rpx; color: #999; }
+.plan-card { background: #fff; padding: 24rpx; border-bottom: 2rpx solid #f0f0f0; }
+.plan-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16rpx; }
+.plan-title { font-size: 30rpx; font-weight: bold; }
+.plan-status { font-size: 24rpx; padding: 4rpx 12rpx; border-radius: 6rpx; }
+.status-generated { background: #fff3cd; color: #856404; }
+.status-reviewed { background: #d1ecf1; color: #0c5460; }
+.status-active { background: #d4edda; color: #155724; }
+.status-completed { background: #e2e3e5; color: #383d41; }
+.status-rejected { background: #f8d7da; color: #721c24; }
+.plan-meta { font-size: 24rpx; color: #999; display: flex; justify-content: space-between; margin-bottom: 12rpx; }
+.plan-desc { font-size: 26rpx; color: #666; margin-bottom: 16rpx; }
+.plan-actions { display: flex; gap: 20rpx; }
+.action-btn { padding: 10rpx 24rpx; border-radius: 8rpx; font-size: 26rpx; }
+.action-btn.primary { background: #4A9BD7; color: #fff; }
+.action-btn.danger { background: #e74c3c; color: #fff; }
+.action-btn.success { background: #27ae60; color: #fff; }
+.empty { text-align: center; padding: 100rpx 0; color: #999; font-size: 30rpx; }
+.review-input { border: 2rpx solid #ddd; border-radius: 8rpx; padding: 16rpx; margin-top: 20rpx; }
+</style>

+ 557 - 0
cfc-frontend/pages/membership/benefits.vue

@@ -0,0 +1,557 @@
+<template>
+  <scroll-view class="container" scroll-y>
+    <view class="header">
+      <text class="header-title">我的权益</text>
+      <text class="header-subtitle">Subscription Benefits</text>
+    </view>
+
+    <view class="subscription-status" v-if="subscription && subscription.level">
+      <view class="status-badge" :class="'level-' + subscription.level.toLowerCase()">
+        <text class="badge-icon">{{ levelIcon }}</text>
+        <text class="badge-text">{{ levelName }}</text>
+      </view>
+      <view class="status-info" v-if="subscription.expireTime">
+        <text class="expire-label">有效期至</text>
+        <text class="expire-value">{{ formatDate(subscription.expireTime) }}</text>
+      </view>
+    </view>
+
+    <view class="benefits-tabs">
+      <view 
+        class="tab-item" 
+        :class="activeTab === 'available' ? 'tab-active' : ''"
+        @click="activeTab = 'available'"
+      >
+        <text>可用权益</text>
+        <text class="tab-count">{{ availableCount }}</text>
+      </view>
+      <view 
+        class="tab-item" 
+        :class="activeTab === 'used' ? 'tab-active' : ''"
+        @click="activeTab = 'used'"
+      >
+        <text>已使用</text>
+        <text class="tab-count">{{ usedCount }}</text>
+      </view>
+    </view>
+
+    <view class="benefits-list" v-if="activeTab === 'available'">
+      <view class="benefit-card" v-for="(item, idx) in availableBenefits" :key="idx">
+        <view class="benefit-header">
+          <view class="benefit-icon-wrap" :style="'background:' + item.bg">
+            <text class="benefit-emoji">{{ item.icon }}</text>
+          </view>
+          <view class="benefit-info">
+            <text class="benefit-name">{{ item.name }}</text>
+            <text class="benefit-desc">{{ item.desc }}</text>
+          </view>
+          <view class="benefit-status available">
+            <text>可用</text>
+          </view>
+        </view>
+        <view class="benefit-detail" v-if="item.detail">
+          <text class="detail-text">{{ item.detail }}</text>
+        </view>
+        <button class="btn-use" v-if="item.canUse" @click="useBenefit(item)">立即使用</button>
+      </view>
+      
+      <view class="empty-state" v-if="availableBenefits.length === 0">
+        <text class="empty-icon">📦</text>
+        <text class="empty-text">暂无可用权益</text>
+        <text class="empty-hint">开通会员解锁更多权益</text>
+      </view>
+    </view>
+
+    <view class="benefits-list" v-if="activeTab === 'used'">
+      <view class="benefit-card used" v-for="(item, idx) in usedBenefits" :key="idx">
+        <view class="benefit-header">
+          <view class="benefit-icon-wrap" :style="'background:' + item.bg">
+            <text class="benefit-emoji">{{ item.icon }}</text>
+          </view>
+          <view class="benefit-info">
+            <text class="benefit-name">{{ item.name }}</text>
+            <text class="benefit-desc">{{ item.desc }}</text>
+          </view>
+          <view class="benefit-status used">
+            <text>已使用</text>
+          </view>
+        </view>
+        <view class="benefit-used-info">
+          <text class="used-time">使用时间:{{ formatDate(item.usedAt) }}</text>
+        </view>
+      </view>
+      
+      <view class="empty-state" v-if="usedBenefits.length === 0">
+        <text class="empty-icon">✅</text>
+        <text class="empty-text">暂无使用记录</text>
+      </view>
+    </view>
+
+    <view class="benefits-summary">
+      <text class="summary-title">权益使用统计</text>
+      <view class="summary-stats">
+        <view class="stat-item">
+          <text class="stat-value">{{ totalBenefits }}</text>
+          <text class="stat-label">总权益数</text>
+        </view>
+        <view class="stat-divider"></view>
+        <view class="stat-item">
+          <text class="stat-value">{{ availableCount }}</text>
+          <text class="stat-label">可用</text>
+        </view>
+        <view class="stat-divider"></view>
+        <view class="stat-item">
+          <text class="stat-value">{{ usedCount }}</text>
+          <text class="stat-label">已用</text>
+        </view>
+      </view>
+    </view>
+  </scroll-view>
+</template>
+
+<script>
+import { getMySubscription, checkBenefit } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      subscription: null,
+      activeTab: 'available',
+      benefits: [
+        { 
+          code: 'AI_CHAT_UNLIMITED', 
+          name: 'AI 健康顾问', 
+          desc: '7×24 小时不限次数问答',
+          icon: '🤖', 
+          bg: '#DBEAFE',
+          canUse: true,
+          used: false,
+          usedAt: null,
+          detail: '支持文字/语音提问,报告智能解读'
+        },
+        { 
+          code: 'FAMILY_MEMBERS', 
+          name: '家庭档案管理', 
+          desc: '管理最多 10 位家庭成员',
+          icon: '👨‍👩‍👧‍👦', 
+          bg: '#D1FAE5',
+          canUse: true,
+          used: false,
+          usedAt: null,
+          detail: '查看全家健康数据,统一规划'
+        },
+        { 
+          code: 'HEALTH_PLAN', 
+          name: '月度健康计划', 
+          desc: '个性化健康改善方案',
+          icon: '📋', 
+          bg: '#FEF3C7',
+          canUse: true,
+          used: false,
+          usedAt: null,
+          detail: '基于家庭健康画像自动生成'
+        },
+        { 
+          code: 'REPORT_READING', 
+          name: '报告智能解读', 
+          desc: '体检/菌群/测评报告 AI 解读',
+          icon: '📊', 
+          bg: '#FCE7F3',
+          canUse: true,
+          used: false,
+          usedAt: null,
+          detail: '通俗易懂的解读 + 行动建议'
+        },
+        { 
+          code: 'MANAGER_SERVICE', 
+          name: '专属管家服务', 
+          desc: '认证健康管家月度随访',
+          icon: '👨‍🏫', 
+          bg: '#FFEDD5',
+          canUse: true,
+          used: false,
+          usedAt: null,
+          detail: '15-20 分钟电话随访 + 在线答疑'
+        },
+        { 
+          code: 'PRODUCT_DISCOUNT', 
+          name: '商品会员折扣', 
+          desc: '全场商品 8-9 折优惠',
+          icon: '🛒', 
+          bg: '#E0E7FF',
+          canUse: true,
+          used: false,
+          usedAt: null,
+          detail: '结算时自动享受会员价'
+        }
+      ]
+    }
+  },
+  computed: {
+    levelIcon() {
+      if (!this.subscription || !this.subscription.level) return '○'
+      if (this.subscription.level === 'L2') return '💎'
+      if (this.subscription.level === 'L1') return '❤️'
+      return '○'
+    },
+    levelName() {
+      if (!this.subscription || !this.subscription.level) return '未订阅'
+      if (this.subscription.level === 'L2') return '久久一生'
+      if (this.subscription.level === 'L1') return '一生一世'
+      return '注册会员'
+    },
+    availableBenefits() {
+      return this.benefits.filter(function(b) { return !b.used })
+    },
+    usedBenefits() {
+      return this.benefits.filter(function(b) { return b.used })
+    },
+    availableCount() {
+      return this.availableBenefits.length
+    },
+    usedCount() {
+      return this.usedBenefits.length
+    },
+    totalBenefits() {
+      return this.benefits.length
+    }
+  },
+  onLoad() {
+    this.loadData()
+  },
+  methods: {
+    async loadData() {
+      try {
+        const res = await getMySubscription()
+        if (res.data) {
+          this.subscription = res.data
+        }
+      } catch (e) {
+        console.error('获取订阅信息失败', e)
+      }
+      this.loadBenefits()
+    },
+    loadBenefits() {
+      var self = this
+      var token = uni.getStorageSync('token')
+      var baseUrl = ''
+      try {
+        var config = require('../../config.js')
+        baseUrl = config.default.baseUrl || config.baseUrl || ''
+      } catch (e) {
+        baseUrl = ''
+      }
+      uni.request({
+        url: baseUrl + '/api/subscription/benefits/my',
+        method: 'POST',
+        header: { 'Authorization': 'Bearer ' + token },
+        success: function(res) {
+          if (res.data && res.data.code === 200) {
+            var data = res.data.data || []
+            data.forEach(function(b) {
+              var benefit = self.benefits.find(function(bf) { return bf.code === b.code })
+              if (benefit) {
+                benefit.used = b.used
+                benefit.usedAt = b.usedAt
+              }
+            })
+          }
+        }
+      })
+    },
+    useBenefit(item) {
+      var self = this
+      uni.showModal({
+        title: '使用权益',
+        content: '确认使用「' + item.name + '」?',
+        success: function(res) {
+          if (res.confirm) {
+            self.doUseBenefit(item)
+          }
+        }
+      })
+    },
+    doUseBenefit(item) {
+      var self = this
+      var data = { benefitCode: item.code }
+      checkBenefit(data).then(function(res) {
+        if (res.data && res.data.success) {
+          item.used = true
+          item.usedAt = new Date()
+          uni.showToast({ title: '使用成功', icon: 'success' })
+        } else {
+          uni.showToast({ title: res.data.message || '使用失败', icon: 'none' })
+        }
+      }).catch(function(e) {
+        uni.showToast({ title: e.message || '使用失败', icon: 'none' })
+      })
+    },
+    formatDate(date) {
+      if (!date) return ''
+      var d = new Date(date)
+      var year = d.getFullYear()
+      var month = String(d.getMonth() + 1).padStart(2, '0')
+      var day = String(d.getDate()).padStart(2, '0')
+      var hour = String(d.getHours()).padStart(2, '0')
+      var minute = String(d.getMinutes()).padStart(2, '0')
+      return year + '-' + month + '-' + day + ' ' + hour + ':' + minute
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  padding: 30rpx;
+  min-height: 100vh;
+  background: #FFF7ED;
+}
+
+.header {
+  text-align: center;
+  padding: 40rpx 0 30rpx;
+}
+.header-title {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #1E293B;
+  display: block;
+}
+.header-subtitle {
+  font-size: 24rpx;
+  color: #94A3B8;
+  margin-top: 8rpx;
+  display: block;
+}
+
+.subscription-status {
+  background: linear-gradient(135deg, #F97316 0%, #EA580C 100%);
+  border-radius: 20rpx;
+  padding: 30rpx;
+  margin-bottom: 30rpx;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.status-badge {
+  display: flex;
+  align-items: center;
+  background: rgba(255,255,255,0.2);
+  padding: 12rpx 24rpx;
+  border-radius: 9999rpx;
+}
+.badge-icon {
+  font-size: 28rpx;
+  margin-right: 8rpx;
+}
+.badge-text {
+  font-size: 26rpx;
+  color: #fff;
+  font-weight: 600;
+}
+.status-info {
+  text-align: right;
+}
+.expire-label {
+  font-size: 22rpx;
+  color: rgba(255,255,255,0.8);
+  display: block;
+}
+.expire-value {
+  font-size: 26rpx;
+  color: #FEF3C7;
+  font-weight: 600;
+}
+
+.benefits-tabs {
+  display: flex;
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 8rpx;
+  margin-bottom: 24rpx;
+  box-shadow: 0 2rpx 12rpx rgba(249,115,22,0.08);
+}
+.tab-item {
+  flex: 1;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 16rpx 0;
+  border-radius: 12rpx;
+  font-size: 28rpx;
+  color: #64748B;
+}
+.tab-active {
+  background: #F97316;
+  color: #fff;
+  font-weight: 600;
+}
+.tab-count {
+  margin-left: 8rpx;
+  font-size: 22rpx;
+  background: rgba(255,255,255,0.3);
+  padding: 2rpx 10rpx;
+  border-radius: 9999rpx;
+}
+
+.benefits-list {
+  margin-bottom: 30rpx;
+}
+
+.benefit-card {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 24rpx;
+  margin-bottom: 20rpx;
+  box-shadow: 0 2rpx 12rpx rgba(249,115,22,0.08);
+}
+.benefit-card.used {
+  opacity: 0.7;
+}
+
+.benefit-header {
+  display: flex;
+  align-items: center;
+}
+.benefit-icon-wrap {
+  width: 72rpx;
+  height: 72rpx;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 20rpx;
+  flex-shrink: 0;
+}
+.benefit-emoji {
+  font-size: 36rpx;
+}
+.benefit-info {
+  flex: 1;
+  margin-right: 16rpx;
+}
+.benefit-name {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #1E293B;
+  display: block;
+  margin-bottom: 6rpx;
+}
+.benefit-desc {
+  font-size: 24rpx;
+  color: #94A3B8;
+  display: block;
+}
+.benefit-status {
+  padding: 8rpx 20rpx;
+  border-radius: 9999rpx;
+  font-size: 22rpx;
+  font-weight: 600;
+}
+.benefit-status.available {
+  background: #D1FAE5;
+  color: #059669;
+}
+.benefit-status.used {
+  background: #E5E7EB;
+  color: #6B7280;
+}
+
+.benefit-detail {
+  margin-top: 16rpx;
+  padding-top: 16rpx;
+  border-top: 1rpx solid #F1F5F9;
+}
+.detail-text {
+  font-size: 24rpx;
+  color: #64748B;
+  line-height: 1.6;
+  display: block;
+}
+
+.benefit-used-info {
+  margin-top: 16rpx;
+  padding-top: 16rpx;
+  border-top: 1rpx solid #F1F5F9;
+}
+.used-time {
+  font-size: 22rpx;
+  color: #94A3B8;
+  display: block;
+}
+
+.btn-use {
+  margin-top: 16rpx;
+  background: linear-gradient(135deg, #F97316 0%, #EA580C 100%);
+  color: #fff;
+  font-size: 26rpx;
+  font-weight: 600;
+  padding: 16rpx 0;
+  border-radius: 12rpx;
+  border: none;
+}
+.btn-use::after {
+  border: none;
+}
+
+.empty-state {
+  text-align: center;
+  padding: 80rpx 0;
+}
+.empty-icon {
+  font-size: 80rpx;
+  display: block;
+  margin-bottom: 20rpx;
+}
+.empty-text {
+  font-size: 28rpx;
+  color: #64748B;
+  display: block;
+  margin-bottom: 12rpx;
+}
+.empty-hint {
+  font-size: 24rpx;
+  color: #94A3B8;
+  display: block;
+}
+
+.benefits-summary {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 30rpx;
+  box-shadow: 0 2rpx 12rpx rgba(249,115,22,0.08);
+}
+.summary-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #1E293B;
+  display: block;
+  margin-bottom: 24rpx;
+}
+.summary-stats {
+  display: flex;
+  align-items: center;
+  justify-content: space-around;
+}
+.stat-item {
+  text-align: center;
+  flex: 1;
+}
+.stat-value {
+  font-size: 44rpx;
+  font-weight: bold;
+  color: #F97316;
+  display: block;
+}
+.stat-label {
+  font-size: 22rpx;
+  color: #94A3B8;
+  margin-top: 8rpx;
+  display: block;
+}
+.stat-divider {
+  width: 1rpx;
+  height: 60rpx;
+  background: #F1F5F9;
+}
+</style>

+ 429 - 0
cfc-frontend/pages/membership/plans.vue

@@ -0,0 +1,429 @@
+<template>
+  <scroll-view class="container" scroll-y>
+    <view class="plans-header">
+      <text class="header-title">一生一世家庭会员</text>
+      <text class="header-subtitle">全家人的健康守护</text>
+    </view>
+
+    <view class="plans-list">
+      <view class="plan-card plan-l0">
+        <view class="plan-header">
+          <text class="plan-name">注册会员</text>
+          <text class="plan-tag">免费</text>
+        </view>
+        <view class="plan-price">
+          <text class="price-amount">¥0</text>
+          <text class="price-unit">/永久</text>
+        </view>
+        <view class="plan-features">
+          <view class="feature-row" v-for="(feat, idx) in l0Features" :key="idx">
+            <text class="feature-icon">✓</text>
+            <text class="feature-text">{{ feat }}</text>
+          </view>
+        </view>
+        <button class="btn-action" disabled>当前等级</button>
+      </view>
+
+      <view class="plan-card plan-l1 active">
+        <view class="plan-badge">推荐</view>
+        <view class="plan-header">
+          <text class="plan-name">❤️ 一生一世</text>
+          <text class="plan-tag">年付会员</text>
+        </view>
+        <view class="plan-price">
+          <text class="price-amount">¥1,314</text>
+          <text class="price-unit">/年</text>
+        </view>
+        <view class="plan-features">
+          <view class="feature-row" v-for="(feat, idx) in l1Features" :key="idx">
+            <text class="feature-icon">✓</text>
+            <text class="feature-text">{{ feat }}</text>
+          </view>
+        </view>
+        <button class="btn-action btn-primary" @click="purchasePlan('L1')">立即开通</button>
+      </view>
+
+      <view class="plan-card plan-l2">
+        <view class="plan-badge premium">至尊</view>
+        <view class="plan-header">
+          <text class="plan-name">💎 久久一生</text>
+          <text class="plan-tag">连续包月</text>
+        </view>
+        <view class="plan-price">
+          <text class="price-amount">¥1,314</text>
+          <text class="price-unit">/月</text>
+        </view>
+        <view class="plan-features">
+          <view class="feature-row" v-for="(feat, idx) in l2Features" :key="idx">
+            <text class="feature-icon">✓</text>
+            <text class="feature-text">{{ feat }}</text>
+          </view>
+        </view>
+        <button class="btn-action btn-premium" @click="purchasePlan('L2')">立即订阅</button>
+      </view>
+    </view>
+
+    <view class="benefits-section">
+      <text class="section-title">会员权益详解</text>
+      <view class="benefits-grid">
+        <view class="benefit-item" v-for="(item, idx) in benefits" :key="idx">
+          <view class="benefit-icon" :style="'background:' + item.bg">
+            <text class="benefit-emoji">{{ item.icon }}</text>
+          </view>
+          <text class="benefit-name">{{ item.name }}</text>
+          <text class="benefit-desc">{{ item.desc }}</text>
+        </view>
+      </view>
+    </view>
+
+    <view class="faq-section">
+      <text class="section-title">常见问题</text>
+      <view class="faq-item" v-for="(faq, idx) in faqs" :key="idx">
+        <text class="faq-question">{{ faq.q }}</text>
+        <text class="faq-answer">{{ faq.a }}</text>
+      </view>
+    </view>
+  </scroll-view>
+</template>
+
+<script>
+import { getSubscriptionPlans, createSubscriptionOrder } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      plans: [],
+      currentPlan: 'L0',
+      l0Features: [
+        '每日 3 次 AI 问答',
+        '基础健康百科',
+        '社区内容浏览'
+      ],
+      l1Features: [
+        '7×24 小时 AI 健康顾问',
+        '报告智能解读',
+        '家庭成员上限 10 人',
+        '商品 8-9 折优惠',
+        '月度健康计划',
+        '认证管家服务',
+        '推荐佣金 R0-R4',
+        '消费分润 P×20%-40%'
+      ],
+      l2Features: [
+        'L1 全部权益',
+        '深度 AI 分析 + 多模态',
+        '家庭成员上限 15 人',
+        '商品 6-8 折优惠',
+        '双周健康计划',
+        '资深管家 (1:30)',
+        '专家团会诊每年 4 次',
+        '消费分润 +5% 加成'
+      ],
+      benefits: [
+        { icon: '🤖', name: 'AI 健康顾问', desc: '7×24 小时智能问答', bg: '#DBEAFE' },
+        { icon: '👨‍👩‍👧‍👦', name: '家庭覆盖', desc: '一次付费全家守护', bg: '#D1FAE5' },
+        { icon: '👨‍🏫', name: '专属管家', desc: '认证专业人员服务', bg: '#FEF3C7' },
+        { icon: '🛒', name: '商品折扣', desc: '会员专享优惠价', bg: '#FCE7F3' },
+        { icon: '💰', name: '消费分润', desc: 'P 点制终身分润', bg: '#FFEDD5' },
+        { icon: '📈', name: '推广佣金', desc: 'R0-R4 阶梯奖励', bg: '#E0E7FF' }
+      ],
+      faqs: [
+        { q: 'L1 一生一世适合谁?', a: '适合有 1-2 个孩子的家庭,追求性价比,希望获得 AI 健康顾问 + 认证管家服务' },
+        { q: 'L2 久久一生与 L1 的区别?', a: 'L2 享资深管家 (1:30 配比)、深度 AI 分析、专家会诊、更高折扣和分润加成' },
+        { q: 'L2 可以取消吗?', a: '可以,取消后当前月结束后降级为 L0,健康档案保留 30 天' },
+        { q: '消费分润如何计算?', a: '被推荐人消费 P 点总和 × 推广人等级分润率,L2 会员额外 +5% 加成' }
+      ]
+    }
+  },
+  onLoad() {
+    this.loadPlans()
+  },
+  methods: {
+    async loadPlans() {
+      try {
+        const res = await getSubscriptionPlans()
+        if (res.data) {
+          this.plans = res.data
+        }
+      } catch (e) {
+        console.error('获取订阅方案失败', e)
+      }
+    },
+    purchasePlan(level) {
+      var self = this
+      uni.showModal({
+        title: '确认购买',
+        content: '确认购买' + (level === 'L1' ? '一生一世年付会员' : '久久一生连续包月') + '?',
+        success: function(res) {
+          if (res.confirm) {
+            self.doPurchase(level)
+          }
+        }
+      })
+    },
+    doPurchase(level) {
+      var self = this
+      var data = { level: level }
+      createSubscriptionOrder(data).then(function(res) {
+        if (res.data && res.data.paymentUrl) {
+          uni.showToast({ title: '跳转支付中...', icon: 'none' })
+        } else {
+          uni.showToast({ title: '购买成功', icon: 'success' })
+        }
+      }).catch(function(e) {
+        uni.showToast({ title: e.message || '购买失败', icon: 'none' })
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  padding: 30rpx;
+  min-height: 100vh;
+  background: linear-gradient(180deg, #FFF7ED 0%, #FEF3C7 100%);
+}
+
+.plans-header {
+  text-align: center;
+  padding: 60rpx 0 40rpx;
+}
+.header-title {
+  font-size: 44rpx;
+  font-weight: bold;
+  color: #1E293B;
+  display: block;
+}
+.header-subtitle {
+  font-size: 28rpx;
+  color: #64748B;
+  margin-top: 12rpx;
+  display: block;
+}
+
+.plans-list {
+  display: flex;
+  flex-direction: column;
+  gap: 30rpx;
+  margin-bottom: 40rpx;
+}
+
+.plan-card {
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 40rpx;
+  position: relative;
+  border: 2rpx solid #FED7AA;
+  box-shadow: 0 4rpx 20rpx rgba(249,115,22,0.08);
+}
+
+.plan-card.active {
+  border-color: #F97316;
+  background: linear-gradient(135deg, #FFF7ED 0%, #FEF3C7 100%);
+  transform: scale(1.02);
+}
+
+.plan-badge {
+  position: absolute;
+  top: -16rpx;
+  right: 40rpx;
+  background: #F97316;
+  color: #fff;
+  padding: 8rpx 24rpx;
+  border-radius: 9999rpx;
+  font-size: 22rpx;
+  font-weight: 600;
+}
+
+.plan-badge.premium {
+  background: linear-gradient(135deg, #8B5CF6 0%, #EC4899 100%);
+}
+
+.plan-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20rpx;
+}
+
+.plan-name {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #1E293B;
+}
+
+.plan-tag {
+  font-size: 22rpx;
+  color: #64748B;
+  background: #F1F5F9;
+  padding: 6rpx 16rpx;
+  border-radius: 8rpx;
+}
+
+.plan-price {
+  text-align: center;
+  margin: 24rpx 0;
+}
+
+.price-amount {
+  font-size: 56rpx;
+  font-weight: bold;
+  color: #F97316;
+}
+
+.price-unit {
+  font-size: 24rpx;
+  color: #94A3B8;
+  margin-left: 8rpx;
+}
+
+.plan-features {
+  margin: 24rpx 0;
+}
+
+.feature-row {
+  display: flex;
+  align-items: center;
+  padding: 12rpx 0;
+}
+
+.feature-icon {
+  width: 40rpx;
+  height: 40rpx;
+  line-height: 40rpx;
+  text-align: center;
+  background: #10B981;
+  color: #fff;
+  border-radius: 50%;
+  font-size: 20rpx;
+  margin-right: 12rpx;
+  flex-shrink: 0;
+}
+
+.feature-text {
+  font-size: 26rpx;
+  color: #334155;
+  flex: 1;
+}
+
+.btn-action {
+  width: 100%;
+  height: 80rpx;
+  line-height: 80rpx;
+  background: #E5E7EB;
+  color: #9CA3AF;
+  font-size: 28rpx;
+  border-radius: 40rpx;
+  border: none;
+  margin-top: 20rpx;
+}
+
+.btn-action:disabled {
+  opacity: 0.6;
+}
+
+.btn-primary {
+  background: linear-gradient(135deg, #F97316 0%, #EA580C 100%);
+  color: #fff;
+  font-weight: 600;
+}
+
+.btn-premium {
+  background: linear-gradient(135deg, #8B5CF6 0%, #EC4899 100%);
+  color: #fff;
+  font-weight: 600;
+}
+
+.btn-action::after {
+  border: none;
+}
+
+.benefits-section {
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 30rpx;
+  margin-bottom: 30rpx;
+  box-shadow: 0 4rpx 20rpx rgba(249,115,22,0.08);
+}
+
+.section-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #1E293B;
+  display: block;
+  margin-bottom: 24rpx;
+}
+
+.benefits-grid {
+  display: flex;
+  flex-wrap: wrap;
+}
+
+.benefit-item {
+  width: 50%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 24rpx 16rpx;
+  box-sizing: border-box;
+}
+
+.benefit-icon {
+  width: 80rpx;
+  height: 80rpx;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 16rpx;
+}
+
+.benefit-emoji {
+  font-size: 40rpx;
+}
+
+.benefit-name {
+  font-size: 26rpx;
+  font-weight: 600;
+  color: #1E293B;
+  margin-bottom: 8rpx;
+}
+
+.benefit-desc {
+  font-size: 22rpx;
+  color: #94A3B8;
+  text-align: center;
+}
+
+.faq-section {
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 30rpx;
+  box-shadow: 0 4rpx 20rpx rgba(249,115,22,0.08);
+}
+
+.faq-item {
+  padding: 20rpx 0;
+  border-bottom: 1rpx solid #F1F5F9;
+}
+
+.faq-item:last-child {
+  border-bottom: none;
+}
+
+.faq-question {
+  font-size: 26rpx;
+  font-weight: 600;
+  color: #1E293B;
+  display: block;
+  margin-bottom: 8rpx;
+}
+
+.faq-answer {
+  font-size: 24rpx;
+  color: #64748B;
+  line-height: 1.6;
+  display: block;
+}
+</style>

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

@@ -1263,6 +1263,35 @@ export const getEmiReport = (childId) => request('/api/emireport/latest', 'POST'
 export const getAssessmentLatestResult = (childId) => request('/api/assessment/latest-result', 'POST', { childId })
 export const getAssessmentHistory = (childId, size) => request('/api/assessment/history', 'POST', { childId, size: size || 5 })
 
+// ===== 测评额度(商城化改造) =====
+export const getAssessmentQuotas = () => request('/api/assessment/quota/my-list', 'POST')
+export const getAssessmentQuotaDetail = (quotaId) => request('/api/assessment/quota/detail', 'POST', { quotaId })
+export const createAssessmentAppointmentWithQuota = (data) => request('/api/assessment/appointment/create', 'POST', data)
+
+// ===== 测评报告上传与指导意见 =====
+export const uploadAssessmentReport = (file, childId, notes) => {
+  return new Promise((resolve, reject) => {
+    uni.uploadFile({
+      url: getApp().globalData.baseUrl + '/api/dan-assessment/result/upload',
+      filePath: file.path,
+      name: 'file',
+      formData: { childId: childId, notes: notes || '' },
+      header: { Authorization: 'Bearer ' + uni.getStorageSync('token') },
+      success: (res) => {
+        try {
+          resolve(JSON.parse(res.data))
+        } catch (e) {
+          reject(e)
+        }
+      },
+      fail: reject
+    })
+  })
+}
+export const saveGuidance = (resultId, items) => request('/api/dan-assessment/result/' + resultId + '/guidance/save', 'POST', { items })
+export const getGuidance = (resultId) => request('/api/dan-assessment/result/' + resultId + '/guidance', 'POST')
+export const confirmUploadReport = (resultId, data) => request('/api/dan-assessment/result/' + resultId + '/confirm-upload', 'POST', data)
+
 // ===== 认知训练建议(家庭行动清单) =====
 export const getCognitiveRecommendations = (childId) => request('/api/cognitive/recommendations', 'POST', { childId })
 export const createTaskFromRecommendation = (childId, dimensionKey, taskIndex) =>
@@ -1406,6 +1435,23 @@ export const adminListMyActivities = (data) => request('/api/activity/list', 'PO
 // 后台概览列表:获取已发布的活动做统计
 export const adminListAllActivities = (data) => request('/api/activity/list', 'POST', data)
 
+export const getSubscriptionPlans = () => request('/api/subscription/plans', 'POST')
+export const getMySubscription = () => request('/api/subscription/my', 'POST')
+export const createSubscriptionOrder = (data) => request('/api/subscription/create', 'POST', data)
+export const renewSubscription = (data) => request('/api/subscription/renew', 'POST', data)
+export const cancelSubscription = (data) => request('/api/subscription/cancel', 'POST', data)
+export const upgradeSubscription = (data) => request('/api/subscription/upgrade', 'POST', data)
+export const checkBenefit = (data) => request('/api/subscription/benefit/check', 'POST', data)
+
+export const getMyPromotionTier = () => request('/api/promotion/my-tier', 'POST')
+export const getPromotionTiers = () => request('/api/promotion/tiers', 'POST')
+export const getPromotionTeamTree = (data) => request('/api/promotion/team-tree', 'POST', data)
+export const getTierHistory = () => request('/api/promotion/tier-history', 'POST')
+
+export const getMyEarnings = () => request('/api/commission/my-earnings', 'POST')
+export const getCommissionRecords = (page, size) => request('/api/commission/records', 'POST', { page: page || 1, size: size || 20 })
+export const getPpointProducts = () => request('/api/commission/ppoint/products', 'POST')
+
 // ===== 缁村害浠诲姟锛堝甫 category 绛涢€夛級 =====
 export const getTodayTasksByCategory = (childId, category) => {
   return request('/api/tasks/today', 'POST', { childId: childId, category: category })
@@ -1628,3 +1674,9 @@ export const consigneeDelete = (id) => request('/api/consignee/delete', 'POST',
 
 // ===== 商品购买字段 =====
 export const getProductRequiredFields = (productId) => request('/api/product/purchase_fields/required', 'POST', { productId })
+
+// ===== 测评方案 =====
+export const listFamilyPlans = (data) => request('/api/guide/plans/family-list', 'POST', data)
+export const reviewPlan = (data) => request('/api/guide/plans/review', 'POST', data)
+export const activatePlan = (data) => request('/api/guide/plans/activate', 'POST', data)
+export const getPlanDetail = (data) => request('/api/guide/plans/detail', 'POST', data)

+ 137 - 0
cfc-web/src/api/admin.js

@@ -1217,4 +1217,141 @@ export function adjustSupplyTier(params) {
     method: 'post',
     data: params
   })
+}
+
+// ========== 家庭会员订阅管理 API ==========
+
+export function getSubscriptionList(params) {
+  return request({
+    url: '/api/admin/subscription/list',
+    method: 'post',
+    data: params
+  })
+}
+
+export function activateSubscription(data) {
+  return request({
+    url: '/api/admin/subscription/activate',
+    method: 'post',
+    data
+  })
+}
+
+export function cancelSubscriptionAdmin(data) {
+  return request({
+    url: '/api/admin/subscription/cancel',
+    method: 'post',
+    data
+  })
+}
+
+export function getBenefitConfig() {
+  return request({
+    url: '/api/admin/subscription/benefits',
+    method: 'get'
+  })
+}
+
+export function updateBenefitConfig(data) {
+  return request({
+    url: '/api/admin/subscription/benefits/update',
+    method: 'post',
+    data
+  })
+}
+
+// ========== 推广等级配置 API ==========
+
+export function getPromotionTierList(params) {
+  return request({
+    url: '/api/admin/promotion/tier/list',
+    method: 'post',
+    data: params
+  })
+}
+
+export function adjustPromotionTier(data) {
+  return request({
+    url: '/api/admin/promotion/tier/adjust',
+    method: 'post',
+    data
+  })
+}
+
+// ========== 佣金/分润记录 API ==========
+
+export function getCommissionRecords(params) {
+  return request({
+    url: '/api/admin/commission/records',
+    method: 'post',
+    data: params
+  })
+}
+
+export function settleCommission(data) {
+  return request({
+    url: '/api/admin/commission/settle',
+    method: 'post',
+    data
+  })
+}
+
+export function refundCommission(data) {
+  return request({
+    url: '/api/admin/commission/refund',
+    method: 'post',
+    data
+  })
+}
+
+// ========== 产品 P 点配置 API ==========
+
+export function getPpointProductList(params) {
+  return request({
+    url: '/api/admin/ppoint/products',
+    method: 'post',
+    data: params
+  })
+}
+
+export function updateProductPpoint(data) {
+  return request({
+    url: '/api/admin/ppoint/update',
+    method: 'post',
+    data
+  })
+}
+
+// ========== 推广等级配置 (R0-R4) API ==========
+
+export function getPromotionTierConfig() {
+  return request({
+    url: '/api/admin/promotion/config',
+    method: 'get'
+  })
+}
+
+export function updatePromotionTierConfig(data) {
+  return request({
+    url: '/api/admin/promotion/config/update',
+    method: 'post',
+    data
+  })
+}
+
+// ========== 佣金配置 API ==========
+
+export function getCommissionConfig() {
+  return request({
+    url: '/api/admin/commission/config',
+    method: 'get'
+  })
+}
+
+export function updateCommissionConfig(data) {
+  return request({
+    url: '/api/admin/commission/config/update',
+    method: 'post',
+    data
+  })
 }

+ 13 - 0
cfc-web/src/api/assessment.js

@@ -72,4 +72,17 @@ export function getGuideAssessmentMaterials() {
     url: '/api/guide/assessment/materials',
     method: 'get'
   })
+}
+
+// Plan Rules
+export function listPlanRules() {
+  return request({ url: '/api/admin/plan-rules/list', method: 'post' })
+}
+
+export function savePlanRule(data) {
+  return request({ url: '/api/admin/plan-rules/save', method: 'post', data })
+}
+
+export function deletePlanRule(data) {
+  return request({ url: '/api/admin/plan-rules/delete', method: 'post', data })
 }

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

@@ -228,6 +228,24 @@ const routes = [
         component: () => import('@/views/admin/ProductEdit.vue'),
         meta: { title: '商品编辑', perm: 'commerce:products' }
       },
+      {
+        path: 'assessment-products',
+        name: 'AssessmentProducts',
+        component: () => import('@/views/admin/AssessmentProducts.vue'),
+        meta: { title: '测评商品管理', perm: 'commerce:products' }
+      },
+      {
+        path: 'assessment-plan-rules',
+        name: 'AssessmentPlanRules',
+        component: () => import('@/views/admin/AssessmentPlanRules.vue'),
+        meta: { title: '方案生成规则', perm: 'plan:rules' }
+      },
+      {
+        path: 'plan-management',
+        name: 'PlanManagement',
+        component: () => import('@/views/admin/PlanManagement.vue'),
+        meta: { title: '方案管理', perm: 'plan:manage' }
+      },
       {
         path: 'category-manage',
         name: 'CategoryManage',

+ 113 - 0
cfc-web/src/views/admin/AssessmentPlanRules.vue

@@ -0,0 +1,113 @@
+<template>
+  <div class="assessment-plan-rules">
+    <div class="header">
+      <h2>方案生成规则</h2>
+      <el-button type="primary" @click="showDialog(null)">新增规则</el-button>
+    </div>
+
+    <el-table :data="rules" border stripe style="width: 100%">
+      <el-table-column prop="dimension" label="维度" width="120" />
+      <el-table-column label="分数区间" width="140">
+        <template slot-scope="{ row }">{{ row.scoreMin }} - {{ row.scoreMax }}</template>
+      </el-table-column>
+      <el-table-column prop="templateId" label="模板包ID" width="100" />
+      <el-table-column prop="priority" label="优先级" width="80" />
+      <el-table-column label="状态" width="80">
+        <template slot-scope="{ row }">
+          <el-tag :type="row.isActive ? 'success' : 'info'">{{ row.isActive ? '启用' : '禁用' }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="createdAt" label="创建时间" />
+      <el-table-column label="操作" width="180">
+        <template slot-scope="{ row }">
+          <el-button size="mini" @click="showDialog(row)">编辑</el-button>
+          <el-button size="mini" type="danger" @click="handleDelete(row.id)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <el-dialog title="规则编辑" :visible.sync="dialogVisible" width="500px">
+      <el-form :model="form" label-width="100px">
+        <el-form-item label="维度">
+          <el-select v-model="form.dimension" placeholder="选择维度">
+            <el-option label="注意力(attention)" value="attention" />
+            <el-option label="专注力(focus)" value="focus" />
+            <el-option label="记忆力(memory)" value="memory" />
+            <el-option label="逻辑(logic)" value="logic" />
+            <el-option label="情绪(emotion)" value="emotion" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="分数下限">
+          <el-input-number v-model="form.scoreMin" :min="0" :max="100" />
+        </el-form-item>
+        <el-form-item label="分数上限">
+          <el-input-number v-model="form.scoreMax" :min="0" :max="100" />
+        </el-form-item>
+        <el-form-item label="模板包ID">
+          <el-input-number v-model="form.templateId" :min="1" />
+        </el-form-item>
+        <el-form-item label="优先级">
+          <el-input-number v-model="form.priority" :min="0" />
+        </el-form-item>
+        <el-form-item label="启用">
+          <el-switch v-model="form.isActive" />
+        </el-form-item>
+      </el-form>
+      <span slot="footer">
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleSave">保存</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listPlanRules, savePlanRule, deletePlanRule } from '@/api/assessment'
+
+export default {
+  data() {
+    return {
+      rules: [],
+      dialogVisible: false,
+      form: { dimension: '', scoreMin: 0, scoreMax: 100, templateId: 1, priority: 0, isActive: true }
+    }
+  },
+  created() {
+    this.loadRules()
+  },
+  methods: {
+    async loadRules() {
+      const res = await listPlanRules()
+      if (res.code === 200) this.rules = res.data || []
+    },
+    showDialog(row) {
+      if (row) {
+        this.form = { ...row }
+      } else {
+        this.form = { dimension: '', scoreMin: 0, scoreMax: 100, templateId: 1, priority: 0, isActive: true }
+      }
+      this.dialogVisible = true
+    },
+    async handleSave() {
+      const res = await savePlanRule(this.form)
+      if (res.code === 200) {
+        this.$message.success('保存成功')
+        this.dialogVisible = false
+        this.loadRules()
+      }
+    },
+    async handleDelete(id) {
+      await this.$confirm('确认删除?')
+      const res = await deletePlanRule({ id })
+      if (res.code === 200) {
+        this.$message.success('删除成功')
+        this.loadRules()
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
+</style>

+ 197 - 0
cfc-web/src/views/admin/AssessmentProducts.vue

@@ -0,0 +1,197 @@
+<template>
+  <div class="assessment-products admin-page">
+    <el-card>
+      <div slot="header" class="admin-page-header">
+        <span class="admin-page-title">测评商品管理</span>
+        <div class="admin-page-actions">
+          <el-input
+            v-model="filters.keyword"
+            placeholder="搜索商品名称"
+            prefix-icon="el-icon-search"
+            clearable
+            class="header-search"
+            @clear="loadList"
+            @keyup.enter.native="loadList"
+          />
+          <el-select v-model="filters.status" placeholder="商品状态" @change="loadList" clearable>
+            <el-option label="全部状态" value="" />
+            <el-option label="待审核" value="pending" />
+            <el-option label="待上架" value="approved" />
+            <el-option label="已上架" value="on_shelf" />
+            <el-option label="已下架" value="off_shelf" />
+            <el-option label="已拒绝" value="rejected" />
+          </el-select>
+          <el-button type="primary" size="mini" @click="loadList">查询</el-button>
+          <el-button type="success" size="mini" icon="el-icon-plus" @click="$router.push('/product-edit')">新增测评商品</el-button>
+        </div>
+      </div>
+
+      <el-table :data="list" v-loading="loading" border stripe>
+        <el-table-column prop="id" label="ID" width="70" />
+        <el-table-column label="商品图片" width="80">
+          <template slot-scope="{ row }">
+            <img v-if="row.coverImage" :src="row.coverImage" class="product-thumb"
+                 @click="previewImage(row.coverImage)" />
+            <span v-else style="color:#999">-</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="name" label="商品名称" min-width="160" show-overflow-tooltip />
+        <el-table-column label="测评类型" width="90">
+          <template slot-scope="{ row }">
+            <el-tag :type="row.assessmentType === 'bundle' ? 'warning' : 'primary'" size="mini">
+              {{ row.assessmentType === 'bundle' ? '套餐' : '单包' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="次数" width="70" prop="totalSessions" />
+        <el-table-column label="有效期(天)" width="90" prop="validityDays" />
+        <el-table-column label="售价(元)" width="90">
+          <template slot-scope="{ row }">
+            {{ formatPriceWithSymbol(row.price || 0) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" width="90">
+          <template slot-scope="{ row }">
+            <el-tag :type="statusType(row.status)" size="mini">{{ statusLabel(row.status) }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="160" fixed="right">
+          <template slot-scope="{ row }">
+            <el-button type="text" size="mini" @click="edit(row.id)">编辑</el-button>
+            <el-button v-if="row.status === 'pending'" type="text" size="mini" @click="handleReview(row, 'approve')">审核通过</el-button>
+            <el-button v-if="row.status === 'on_shelf' || row.status === 'off_shelf'" type="text" size="mini" @click="handleShelve(row)">
+              {{ row.status === 'on_shelf' ? '下架' : '上架' }}
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div class="pagination-wrap">
+        <el-pagination
+          @size-change="handleSizeChange"
+          @current-change="handlePageChange"
+          :current-page="page"
+          :page-sizes="[10, 20, 50]"
+          :page-size="size"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="total"
+        />
+      </div>
+    </el-card>
+
+    <el-dialog title="商品详情" :visible.sync="detailVisible" width="600px">
+      <el-descriptions v-if="detail" :column="2" border>
+        <el-descriptions-item label="商品ID">{{ detail.id }}</el-descriptions-item>
+        <el-descriptions-item label="商品名称">{{ detail.name }}</el-descriptions-item>
+        <el-descriptions-item label="商品类型">{{ detail.productType }}</el-descriptions-item>
+        <el-descriptions-item label="测评类型">{{ detail.assessmentType === 'bundle' ? '套餐' : '单包' }}</el-descriptions-item>
+        <el-descriptions-item label="测评次数">{{ detail.totalSessions }}</el-descriptions-item>
+        <el-descriptions-item label="有效期(天)">{{ detail.validityDays }}</el-descriptions-item>
+        <el-descriptions-item label="规划师范围">{{ detail.guideScope }}</el-descriptions-item>
+        <el-descriptions-item label="价格(元)">{{ formatPriceWithSymbol(detail.price || 0) }}</el-descriptions-item>
+        <el-descriptions-item label="状态">
+          <el-tag :type="statusType(detail.status)" size="mini">{{ statusLabel(detail.status) }}</el-tag>
+        </el-descriptions-item>
+      </el-descriptions>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getProductList, reviewProduct, shelveProduct } from '@/api/admin'
+
+export default {
+  name: 'AssessmentProducts',
+  data() {
+    return {
+      list: [],
+      loading: false,
+      page: 1,
+      size: 20,
+      total: 0,
+      filters: {
+        keyword: '',
+        status: ''
+      },
+      detail: null,
+      detailVisible: false
+    }
+  },
+  mounted() {
+    this.loadList()
+  },
+  methods: {
+    async loadList() {
+      this.loading = true
+      try {
+        const res = await getProductList({
+          page: this.page,
+          size: this.size,
+          productType: 'assessment',
+          keyword: this.filters.keyword || undefined,
+          status: this.filters.status || undefined
+        })
+        if (res.code === 200 && res.data) {
+          this.list = res.data.records || []
+          this.total = res.data.total || 0
+        }
+      } catch (e) {
+        this.$message.error('加载失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    edit(id) {
+      this.$router.push('/product-edit?id=' + id)
+    },
+    async handleReview(row, action) {
+      try {
+        await this.$confirm('确认' + (action === 'approve' ? '通过' : '拒绝') + '该商品审核?', '提示')
+        await reviewProduct(row.id, { action })
+        this.$message.success('操作成功')
+        this.loadList()
+      } catch (e) {
+        if (e !== 'cancel') this.$message.error('操作失败')
+      }
+    },
+    async handleShelve(row) {
+      const action = row.status === 'on_shelf' ? '下架' : '上架'
+      try {
+        await this.$confirm('确认' + action + '该商品?', '提示')
+        await shelveProduct(row.id, { shelve: row.status !== 'on_shelf' })
+        this.$message.success(action + '成功')
+        this.loadList()
+      } catch (e) {
+        if (e !== 'cancel') this.$message.error(action + '失败')
+      }
+    },
+    previewImage(url) {
+      this.detail = { coverImage: url }
+      this.detailVisible = true
+    },
+    statusType(status) {
+      const map = { pending: 'warning', approved: 'info', on_shelf: 'success', off_shelf: 'info', rejected: 'danger' }
+      return map[status] || ''
+    },
+    statusLabel(status) {
+      const map = { pending: '待审核', approved: '待上架', on_shelf: '已上架', off_shelf: '已下架', rejected: '已拒绝' }
+      return map[status] || status
+    },
+    formatPriceWithSymbol(price) {
+      if (price === 0) return '免费'
+      return '¥' + (price / 100).toFixed(2)
+    },
+    handleSizeChange(val) { this.size = val; this.loadList() },
+    handlePageChange(val) { this.page = val; this.loadList() }
+  }
+}
+</script>
+
+<style scoped>
+.assessment-products { padding: 20px; }
+.admin-page-header { display: flex; justify-content: space-between; align-items: center; }
+.admin-page-actions { display: flex; gap: 8px; align-items: center; }
+.header-search { width: 200px; }
+.product-thumb { width: 40px; height: 40px; object-fit: cover; border-radius: 4px; cursor: pointer; }
+.pagination-wrap { margin-top: 16px; text-align: right; }
+</style>

+ 119 - 0
cfc-web/src/views/admin/PlanManagement.vue

@@ -0,0 +1,119 @@
+<template>
+  <div class="plan-management">
+    <div class="header">
+      <h2>方案管理</h2>
+      <div class="filters">
+        <el-select v-model="statusFilter" placeholder="状态筛选" clearable @change="loadPlans" style="width:160px">
+          <el-option label="全部" value="" />
+          <el-option label="待审核(generated)" value="generated" />
+          <el-option label="已审核(reviewed)" value="reviewed" />
+          <el-option label="已激活(active)" value="active" />
+          <el-option label="已完成(completed)" value="completed" />
+          <el-option label="已驳回(rejected)" value="rejected" />
+        </el-select>
+        <el-input v-model="familySearch" placeholder="搜索家庭ID" style="width:180px;margin-left:10px" @keyup.enter="loadPlans" />
+        <el-button type="primary" @click="loadPlans" style="margin-left:10px">查询</el-button>
+      </div>
+    </div>
+
+    <el-table :data="plans" border stripe style="width:100%">
+      <el-table-column prop="id" label="方案ID" width="80" />
+      <el-table-column prop="familyId" label="家庭ID" width="80" />
+      <el-table-column prop="childId" label="孩子ID" width="80" />
+      <el-table-column prop="name" label="方案名称" min-width="180" />
+      <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 label="进度" width="120">
+        <template slot-scope="{ row }">
+          {{ row.completedDays || 0 }} / {{ row.totalDays || '-' }} 天
+        </template>
+      </el-table-column>
+      <el-table-column prop="source" label="来源" width="100" />
+      <el-table-column prop="createdAt" label="创建时间" width="160" />
+      <el-table-column prop="reviewedAt" label="审核时间" width="160" />
+      <el-table-column label="操作" width="180">
+        <template slot-scope="{ row }">
+          <el-button size="mini" type="primary" @click="showDetail(row)">详情</el-button>
+          <el-button v-if="row.status === 'generated'" size="mini" type="success" @click="handleApprove(row)">批准</el-button>
+          <el-button v-if="row.status === 'reviewed'" size="mini" @click="handleActivate(row)">激活</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <el-dialog title="方案详情" :visible.sync="detailVisible" width="600px">
+      <el-form label-width="100px" v-if="currentPlan">
+        <el-form-item label="方案ID">{{ currentPlan.id }}</el-form-item>
+        <el-form-item label="家庭ID">{{ currentPlan.familyId }}</el-form-item>
+        <el-form-item label="孩子ID">{{ currentPlan.childId }}</el-form-item>
+        <el-form-item label="方案名称">{{ currentPlan.name }}</el-form-item>
+        <el-form-item label="状态">
+          <el-tag :type="statusType(currentPlan.status)">{{ statusLabel(currentPlan.status) }}</el-tag>
+        </el-form-item>
+        <el-form-item label="周期">{{ currentPlan.totalDays }} 天</el-form-item>
+        <el-form-item label="进度">{{ currentPlan.completedDays || 0 }} / {{ currentPlan.totalDays || '-' }} 天</el-form-item>
+        <el-form-item label="来源">{{ currentPlan.source }}</el-form-item>
+        <el-form-item label="创建时间">{{ currentPlan.createdAt }}</el-form-item>
+        <el-form-item label="审核时间">{{ currentPlan.reviewedAt || '-' }}</el-form-item>
+        <el-form-item label="激活时间">{{ currentPlan.activatedAt || '-' }}</el-form-item>
+        <el-form-item label="备注">
+          <pre style="white-space:pre-wrap;background:#f5f5f5;padding:12px;border-radius:6px;">{{ currentPlan.remark }}</pre>
+        </el-form-item>
+      </el-form>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listFamilyPlans, reviewPlan, activatePlan } from '@/api/assessment'
+
+export default {
+  data() {
+    return {
+      plans: [],
+      statusFilter: '',
+      familySearch: '',
+      detailVisible: false,
+      currentPlan: null
+    }
+  },
+  created() { this.loadPlans() },
+  methods: {
+    async loadPlans() {
+      const res = await listFamilyPlans({
+        status: this.statusFilter || null,
+        familyId: this.familySearch ? Number(this.familySearch) : null
+      })
+      if (res.code === 200) this.plans = res.data || []
+    },
+    statusType(s) {
+      const map = { generated: 'warning', reviewed: 'primary', active: 'success', completed: 'info', rejected: 'danger' }
+      return map[s] || 'info'
+    },
+    statusLabel(s) {
+      const map = { generated: '待审核', reviewed: '已审核', active: '已激活', completed: '已完成', rejected: '已驳回' }
+      return map[s] || s
+    },
+    showDetail(row) {
+      this.currentPlan = row
+      this.detailVisible = true
+    },
+    async handleApprove(row) {
+      const res = await reviewPlan({ planId: row.id, approved: true })
+      if (res.code === 200) { this.$message.success('已批准'); this.loadPlans() }
+    },
+    async handleActivate(row) {
+      await this.$confirm('激活后将生成任务,确定激活?')
+      const res = await activatePlan({ planId: row.id })
+      if (res.code === 200) { this.$message.success('已激活'); this.loadPlans() }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
+.filters { display: flex; align-items: center; }
+</style>

+ 64 - 7
cfc-web/src/views/admin/ProductEdit.vue

@@ -18,9 +18,41 @@
             <el-option label="实物商品" value="physical" />
             <el-option label="虚拟商品" value="virtual" />
             <el-option label="优惠券" value="coupon" />
+            <el-option label="测评商品" value="assessment" />
           </el-select>
         </el-form-item>
 
+        <!-- 测评商品扩展信息 -->
+        <template v-if="form.productType === 'assessment'">
+          <el-form-item label="测评类型">
+            <el-select v-model="form.assessmentType" placeholder="请选择测评类型" style="width: 200px;">
+              <el-option label="单包" value="single" />
+              <el-option label="套餐" value="bundle" />
+            </el-select>
+          </el-form-item>
+          <el-row :gutter="20">
+            <el-col :span="8">
+              <el-form-item label="测评次数">
+                <el-input-number v-model="form.totalSessions" :min="1" :max="999" :precision="0" style="width: 100%;" controls />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="有效期(天)">
+                <el-input-number v-model="form.validityDays" :min="1" :max="3650" :precision="0" :default-value="365" style="width: 100%;" controls />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="规划师范围">
+                <el-select v-model="form.guideScope" placeholder="不限" style="width: 100%;">
+                  <el-option label="不限" value="all" />
+                  <el-option label="指定规划师" value="assigned" />
+                  <el-option label="自动分配" value="auto" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </template>
+
         <el-form-item label="商品简介" prop="intro">
           <el-input v-model="form.intro" type="textarea" :rows="2" placeholder="简短描述" maxlength="200" />
         </el-form-item>
@@ -195,7 +227,12 @@ export default {
         distributionSystemId: null,
         deliveryMethod: 1,
         memberEligible: 'no',
-        dimensionWeights: []
+        dimensionWeights: [],
+        // 测评商品扩展
+        assessmentType: 'single',
+        totalSessions: 1,
+        validityDays: 365,
+        guideScope: 'all'
       },
       imageList: [],
       supplySystems: [],
@@ -300,7 +337,11 @@ export default {
             distributionSystemId: p.distributionSystemId || null,
             deliveryMethod: p.deliveryMethod || 1,
             memberEligible: p.memberEligible || 'no',
-            dimensionWeights: []
+            dimensionWeights: [],
+            assessmentType: p.assessmentType || 'single',
+            totalSessions: p.totalSessions || 1,
+            validityDays: p.validityDays || 365,
+            guideScope: p.guideScope || 'all'
           }
           if (p.images) {
             try {
@@ -425,23 +466,39 @@ export default {
         if (!valid) return
         this.saving = true
         try {
-          const payload = {
+          const productData = {
             ...this.form,
             price: (this.form.price != null) ? Math.round(this.form.price * 100) : 0,
             memberPrice: (this.form.memberPrice != null) ? Math.round(this.form.memberPrice * 100) : null,
             images: JSON.stringify(this.imageList)
           }
           // Extract dimensionWeights before sending to product API
-          const dimensionWeights = payload.dimensionWeights
-          delete payload.dimensionWeights
+          const dimensionWeights = productData.dimensionWeights
+          delete productData.dimensionWeights
+
+          // Build ProductCreateRequest payload
+          const requestPayload = { product: productData }
+          if (productData.productType === 'assessment') {
+            requestPayload.assessmentExt = {
+              assessmentType: productData.assessmentType,
+              totalSessions: productData.totalSessions,
+              validityDays: productData.validityDays,
+              guideScope: productData.guideScope
+            }
+          }
+          // Remove assessment ext fields from product object
+          delete productData.assessmentType
+          delete productData.totalSessions
+          delete productData.validityDays
+          delete productData.guideScope
 
           let res
           let productId = this.form.id
           if (this.isEdit) {
-            res = await updateProduct(payload)
+            res = await updateProduct(requestPayload)
             productId = this.form.id
           } else {
-            res = await createProduct(payload)
+            res = await createProduct(requestPayload)
             // Get the created product id
             if (res.code === 200 && res.data) {
               productId = res.data.id || res.data

+ 2 - 1
cfc-web/src/views/admin/ProductManage.vue

@@ -26,6 +26,7 @@
             <el-option label="实物商品" value="physical" />
             <el-option label="虚拟商品" value="virtual" />
             <el-option label="优惠券" value="coupon" />
+            <el-option label="测评商品" value="assessment" />
           </el-select>
           <el-button type="primary" size="mini" @click="loadList">查询</el-button>
           <el-button type="success" size="mini" icon="el-icon-plus" @click="$router.push('/product-edit')">新增商品</el-button>
@@ -220,7 +221,7 @@ export default {
       return map[status] || status
     },
     typeLabel(type) {
-      const map = { physical: '实物商品', virtual: '虚拟商品', coupon: '优惠券' }
+      const map = { physical: '实物商品', virtual: '虚拟商品', coupon: '优惠券', assessment: '测评商品' }
       return map[type] || type
     },
     formatTime(t) {

+ 343 - 0
cfc-web/src/views/admin/SubscriptionManagement.vue

@@ -0,0 +1,343 @@
+<template>
+  <div class="subscription-management admin-page">
+    <div class="header">
+      <h2 class="admin-page-title">订阅管理</h2>
+      <div class="filters">
+        <el-input v-model="keyword" placeholder="搜索家庭/用户..." prefix-icon="el-icon-search" style="width: 200px;" clearable @keyup.enter.native="handleSearch" @clear="handleSearch" />
+        <el-select v-model="statusFilter" placeholder="订阅状态" @change="loadList" style="width: 120px;">
+          <el-option label="全部" value="" />
+          <el-option label="激活" value="active" />
+          <el-option label="已过期" value="expired" />
+          <el-option label="已取消" value="cancelled" />
+        </el-select>
+        <el-select v-model="levelFilter" placeholder="会员等级" @change="loadList" style="width: 120px;">
+          <el-option label="全部" value="" />
+          <el-option label="L1 一生一世" value="L1" />
+          <el-option label="L2 久久一生" value="L2" />
+        </el-select>
+        <el-button type="primary" @click="loadList">查询</el-button>
+      </div>
+    </div>
+
+    <div class="table-scroll-wrap">
+      <div style="overflow:auto;max-height:calc(100vh - 300px);">
+      <el-table style="max-height:calc(100vh - 300px);" :data="list" v-loading="loading" border stripe>
+        <el-table-column prop="id" label="ID" width="70" />
+        <el-table-column label="家庭信息" min-width="180" show-overflow-tooltip>
+          <template slot-scope="{ row }">
+            <div style="display:flex;align-items:center;gap:8px;">
+              <span style="font-weight:600;">{{ row.familyName || '-' }}</span>
+              <el-tag size="mini" type="info">{{ row.memberCount || 0 }}人</el-tag>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="订阅等级" width="120">
+          <template slot-scope="{ row }">
+            <el-tag :type="row.level === 'L1' ? 'success' : 'danger'">{{ levelLabel(row.level) }}</el-tag>
+          </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 label="开始时间" width="160">
+          <template slot-scope="{ row }">
+            {{ formatTime(row.startTime) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="结束时间" width="160">
+          <template slot-scope="{ row }">
+            {{ formatTime(row.expireTime) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="支付金额" width="100">
+          <template slot-scope="{ row }">
+            {{ formatPrice(row.amount) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="创建时间" width="160">
+          <template slot-scope="{ row }">
+            {{ formatTime(row.createdAt) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="200" fixed="right">
+          <template slot-scope="{ row }">
+            <el-button size="mini" type="info" @click="viewDetail(row)">详情</el-button>
+            <el-dropdown v-if="row.status === 'active'" 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="cancel" icon="el-icon-close">取消订阅</el-dropdown-item>
+                <el-dropdown-item command="refund" icon="el-icon-warning-outline">退款</el-dropdown-item>
+              </el-dropdown-menu>
+            </el-dropdown>
+            <el-button v-if="row.status === 'cancelled'" size="mini" type="success" @click="handleActivate(row)">重新激活</el-button>
+          </template>
+        </el-table-column>
+      </el-table></div>
+    </div>
+
+    <el-pagination
+      @size-change="onSizeChange"
+      @current-change="onPageChange"
+      :current-page="page"
+      :page-size="size"
+      :total="total"
+      layout="total, sizes, prev, pager, next"
+      :page-sizes="[10, 20, 50]"
+      class="pagination-wrap"
+    />
+
+    <!-- 详情弹窗 -->
+    <el-dialog title="订阅详情" :visible.sync="detailDialogVisible" width="600px">
+      <el-descriptions :column="2" border v-if="detail">
+        <el-descriptions-item label="订阅 ID">{{ detail.id }}</el-descriptions-item>
+        <el-descriptions-item label="家庭名称">{{ detail.familyName || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="订阅等级">
+          <el-tag :type="detail.level === 'L1' ? 'success' : 'danger'">{{ levelLabel(detail.level) }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="状态">
+          <el-tag :type="statusType(detail.status)">{{ statusLabel(detail.status) }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="开始时间">{{ formatTime(detail.startTime) }}</el-descriptions-item>
+        <el-descriptions-item label="结束时间">{{ formatTime(detail.expireTime) }}</el-descriptions-item>
+        <el-descriptions-item label="支付金额">{{ formatPrice(detail.amount) }}</el-descriptions-item>
+        <el-descriptions-item label="支付方式">{{ payMethodLabel(detail.paymentType) }}</el-descriptions-item>
+        <el-descriptions-item label="创建时间">{{ formatTime(detail.createdAt) }}</el-descriptions-item>
+        <el-descriptions-item label="家庭成员数">{{ detail.memberCount || 0 }}人</el-descriptions-item>
+      </el-descriptions>
+      <span slot="footer">
+        <el-button @click="detailDialogVisible = false">关闭</el-button>
+      </span>
+    </el-dialog>
+
+    <!-- 取消订阅弹窗 -->
+    <el-dialog title="取消订阅" :visible.sync="cancelDialogVisible" width="450px">
+      <el-alert title="确认取消以下订阅" type="warning" :closable="false" style="margin-bottom:15px;">
+        <template slot>
+          家庭:<strong>{{ cancelTarget?.familyName }}</strong><br>
+          等级:{{ levelLabel(cancelTarget?.level) }}
+        </template>
+      </el-alert>
+      <el-input v-model="cancelReason" type="textarea" placeholder="取消原因(选填)" :rows="3" />
+      <span slot="footer">
+        <el-button @click="cancelDialogVisible = false">取消</el-button>
+        <el-button type="danger" @click="confirmCancel">确认取消</el-button>
+      </span>
+    </el-dialog>
+
+    <!-- 退款弹窗 -->
+    <el-dialog title="退款" :visible.sync="refundDialogVisible" width="450px">
+      <el-alert title="确认退款" type="error" :closable="false" style="margin-bottom:15px;">
+        <template slot>
+          订单号:<strong>{{ refundTarget?.id }}</strong><br>
+          金额:<strong>{{ formatPrice(refundTarget?.amount) }}</strong>
+        </template>
+      </el-alert>
+      <el-input v-model="refundReason" type="textarea" placeholder="退款原因" :rows="3" />
+      <span slot="footer">
+        <el-button @click="refundDialogVisible = false">取消</el-button>
+        <el-button type="danger" @click="confirmRefund">确认退款</el-button>
+      </span>
+    </el-dialog>
+
+    <!-- 激活弹窗 -->
+    <el-dialog title="重新激活订阅" :visible.sync="activateDialogVisible" width="450px">
+      <el-alert title="确认重新激活以下订阅" type="success" :closable="false" style="margin-bottom:15px;">
+        <template slot>
+          家庭:<strong>{{ activateTarget?.familyName }}</strong><br>
+          等级:{{ levelLabel(activateTarget?.level) }}
+        </template>
+      </el-alert>
+      <el-form :model="activateForm" label-width="100px">
+        <el-form-item label="新的结束时间">
+          <el-date-picker
+            v-model="activateForm.expireTime"
+            type="datetime"
+            placeholder="选择结束时间"
+            value-format="yyyy-MM-dd HH:mm:ss"
+            style="width: 240px;"
+          />
+        </el-form-item>
+      </el-form>
+      <span slot="footer">
+        <el-button @click="activateDialogVisible = false">取消</el-button>
+        <el-button type="success" @click="confirmActivate">确认激活</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getSubscriptionList, activateSubscription, cancelSubscriptionAdmin } from '@/api/admin'
+
+export default {
+  data() {
+    return {
+      list: [],
+      loading: false,
+      page: 1,
+      size: 20,
+      total: 0,
+      keyword: '',
+      statusFilter: '',
+      levelFilter: '',
+      detailDialogVisible: false,
+      detail: null,
+      cancelDialogVisible: false,
+      cancelTarget: null,
+      cancelReason: '',
+      refundDialogVisible: false,
+      refundTarget: null,
+      refundReason: '',
+      activateDialogVisible: false,
+      activateTarget: null,
+      activateForm: { expireTime: '' }
+    }
+  },
+  mounted() {
+    this.loadList()
+  },
+  methods: {
+    async loadList() {
+      this.loading = true
+      try {
+        var params = { page: this.page, size: this.size }
+        if (this.statusFilter) params.status = this.statusFilter
+        if (this.levelFilter) params.level = this.levelFilter
+        if (this.keyword) params.keyword = this.keyword
+        var res = await getSubscriptionList(params)
+        this.list = res.data.records || res.data.list || res.data || []
+        this.total = res.data.total || this.list.length
+      } catch (e) {
+        this.$message.error('加载订阅列表失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    handleSearch() {
+      this.page = 1
+      this.loadList()
+    },
+    onPageChange(val) {
+      this.page = val
+      this.loadList()
+    },
+    onSizeChange(val) {
+      this.size = val
+      this.page = 1
+      this.loadList()
+    },
+    levelLabel(level) {
+      const map = { L1: '一生一世', L2: '久久一生' }
+      return map[level] || level
+    },
+    statusType(status) {
+      const map = { active: 'success', expired: 'info', cancelled: 'warning' }
+      return map[status] || 'info'
+    },
+    statusLabel(status) {
+      const map = { active: '激活', expired: '已过期', cancelled: '已取消' }
+      return map[status] || status
+    },
+    payMethodLabel(type) {
+      const map = { subscription: '年付', subscription_monthly: '月付' }
+      return map[type] || type
+    },
+    formatPrice(cents) {
+      if (cents == null) return '-'
+      return '¥' + (cents / 100).toFixed(2)
+    },
+    formatTime(t) {
+      if (!t) return '-'
+      return t.replace ? t.replace('T', ' ').substring(0, 19) : t
+    },
+    viewDetail(row) {
+      this.detail = row
+      this.detailDialogVisible = true
+    },
+    showCancelDialog(row) {
+      this.cancelTarget = row
+      this.cancelReason = ''
+      this.cancelDialogVisible = true
+    },
+    async confirmCancel() {
+      try {
+        await this.$confirm('确认取消该订阅?', '提示', { type: 'warning' })
+        await cancelSubscriptionAdmin({ subscriptionId: this.cancelTarget.id, reason: this.cancelReason })
+        this.$message.success('已取消订阅')
+        this.cancelDialogVisible = false
+        this.loadList()
+      } catch (e) {
+        if (e !== 'cancel') this.$message.error('操作失败')
+      }
+    },
+    showRefundDialog(row) {
+      this.refundTarget = row
+      this.refundReason = ''
+      this.refundDialogVisible = true
+    },
+    async confirmRefund() {
+      try {
+        await this.$confirm('确认退款?此操作不可撤销。', '警告', { type: 'error' })
+        await cancelSubscriptionAdmin({ subscriptionId: this.refundTarget.id, reason: this.refundReason, refund: true })
+        this.$message.success('已退款')
+        this.refundDialogVisible = false
+        this.loadList()
+      } catch (e) {
+        if (e !== 'cancel') this.$message.error('操作失败')
+      }
+    },
+    showActivateDialog(row) {
+      this.activateTarget = row
+      this.activateForm.expireTime = ''
+      this.activateDialogVisible = true
+    },
+    async confirmActivate() {
+      if (!this.activateForm.expireTime) {
+        return this.$message.warning('请选择结束时间')
+      }
+      try {
+        await activateSubscription({ subscriptionId: this.activateTarget.id, expireTime: this.activateForm.expireTime })
+        this.$message.success('已激活')
+        this.activateDialogVisible = false
+        this.loadList()
+      } catch (e) {
+        this.$message.error('操作失败')
+      }
+    },
+    handleActivate(row) {
+      this.showActivateDialog(row)
+    },
+    handleActionCmd(row, cmd) {
+      switch (cmd) {
+        case 'cancel': this.showCancelDialog(row); break
+        case 'refund': this.showRefundDialog(row); break
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.subscription-management {
+  padding: 20px;
+}
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+  flex-wrap: wrap;
+}
+.header h2 {
+  margin: 0;
+}
+.filters {
+  display: flex;
+  gap: 10px;
+  align-items: center;
+}
+</style>