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

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

liaoxg 1 месяц назад
Родитель
Сommit
b4f09112ad
58 измененных файлов с 6248 добавлено и 140 удалено
  1. 6 0
      AGENTS.md
  2. 6 0
      cfc-backend/AGENTS.md
  3. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/common/BasicQuotaExhaustedException.java
  4. 261 60
      cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  5. 47 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/BasicQuotaController.java
  6. 117 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/DoaController.java
  7. 14 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java
  8. 27 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminBasicQuotaController.java
  9. 7 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductOrderController.java
  10. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/ProductOrderDTO.java
  11. 11 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/QuotaConsumeResult.java
  12. 14 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/QuotaStatusDTO.java
  13. 33 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/BasicQuotaUsage.java
  14. 31 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/DoaGoal.java
  15. 64 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/DoaStage.java
  16. 43 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/DoaWeek.java
  17. 1 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/HealthExerciseRecord.java
  18. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/BasicQuotaUsageMapper.java
  19. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/DoaGoalMapper.java
  20. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/DoaStageMapper.java
  21. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/DoaWeekMapper.java
  22. 77 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ActivationQuotaService.java
  23. 13 1
      cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java
  24. 12 1
      cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentAppointmentService.java
  25. 247 0
      cfc-backend/src/main/java/com/etotem/cfc/service/BasicQuotaService.java
  26. 11 0
      cfc-backend/src/main/java/com/etotem/cfc/service/DanReportUploadService.java
  27. 339 0
      cfc-backend/src/main/java/com/etotem/cfc/service/DoaService.java
  28. 46 2
      cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java
  29. 180 1
      cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java
  30. 55 0
      cfc-backend/src/main/java/com/etotem/cfc/task/BasicQuotaScheduledTasks.java
  31. 87 0
      cfc-backend/src/main/resources/schema.sql
  32. 53 0
      cfc-frontend/components/QuotaProgressBar.vue
  33. 130 0
      cfc-frontend/components/UpgradeGuideModal.vue
  34. 30 0
      cfc-frontend/pages.json
  35. 33 3
      cfc-frontend/pages/ai/chat.vue
  36. 33 0
      cfc-frontend/pages/health/challenge-manage.vue
  37. 160 0
      cfc-frontend/pages/health/doa-index.vue
  38. 223 0
      cfc-frontend/pages/health/doa-stage-create.vue
  39. 213 0
      cfc-frontend/pages/health/doa-stage-detail.vue
  40. 154 0
      cfc-frontend/pages/health/doa-week-edit.vue
  41. 60 53
      cfc-frontend/pages/health/gut-flora-detail.vue
  42. 17 7
      cfc-frontend/pages/membership/index.vue
  43. 4 0
      cfc-frontend/pages/membership/upgrade.vue
  44. 512 0
      cfc-frontend/pages/vendor/order-review/order-review.vue
  45. 12 0
      cfc-frontend/pages/vendor/orders/orders.vue
  46. 18 0
      cfc-frontend/utils/api.js
  47. 5 3
      cfc-langgraph/app/graphs/health_plan_graph.py
  48. 1 1
      cfc-web/.last_build_commit
  49. 2 2
      cfc-web/package-lock.json
  50. 1 1
      cfc-web/package.json
  51. 78 0
      cfc-web/public/CHANGELOG-v1.0.md
  52. 79 1
      cfc-web/public/CHANGELOG.md
  53. 286 0
      cfc-web/src/utils/element-icons.js
  54. 159 1
      cfc-web/src/views/admin/MenuManage.vue
  55. 4 1
      docs/superpowers/PROJECT-OVERVIEW.md
  56. 18 1
      docs/superpowers/api/API_REFERENCE.md
  57. 1680 0
      docs/superpowers/plans/2026-09-10-basic-membership-plan.md
  58. 495 0
      docs/superpowers/specs/2026-09-10-basic-membership-design.md

+ 6 - 0
AGENTS.md

@@ -90,6 +90,12 @@ opencode web --port 4090 --hostname 127.0.0.1
 - **环境自适应:** 小程序 `config.js` 通过 `uni.getAccountInfoSync()` 自动切换 develop/trial/release 的 API 地址
 - **环境自适应:** 小程序 `config.js` 通过 `uni.getAccountInfoSync()` 自动切换 develop/trial/release 的 API 地址
 - **测试模式:** `wechat.test-mode: true` 跳过微信 API,使用模拟数据
 - **测试模式:** `wechat.test-mode: true` 跳过微信 API,使用模拟数据
 
 
+## 用户模型统一口径
+
+- **能登录用户**:`family_members.userId` 非空且关联 User 有 openid(VO 层 `memberType=login`)
+- **不能登录用户**(历史称"孩子"):`family_members.userId` 为空或关联 User 无 openid(VO 层 `memberType=local`),由同家庭能登录用户代管其数据(任务/健康/DOA 等)
+- 新功能禁止再引入"孩子"概念,统一用"能登录用户/不能登录用户"表述
+
 ## cfclub 新特性
 ## cfclub 新特性
 
 
 - 家庭邀请二维码(小程序码+令牌+落地页)
 - 家庭邀请二维码(小程序码+令牌+落地页)

+ 6 - 0
cfc-backend/AGENTS.md

@@ -51,6 +51,12 @@ Spring Boot 2.7.18 后端服务,MyBatis-Plus ORM,JWT 认证。
 - **Bean名冲突检查:** 新增 Controller/Service 时,确保类名不与其他包中的类重名(Spring 默认 Bean Name 为类名首字母小写)
 - **Bean名冲突检查:** 新增 Controller/Service 时,确保类名不与其他包中的类重名(Spring 默认 Bean Name 为类名首字母小写)
 - **身份标识:** `userId`=登录账号(`users.id`)、`memberId`/`familyMemberId`/`currentMemberId`=家庭成员(`family_members.id`)、`childId`=遗留表(`children.id`);账号自身/交易/权限判断用 userId,成员/孩子数据(任务/健康/成长/关系/五维能量)用 memberId;`JwtInterceptor` 按 `userId+familyId` 自动反查 `currentMemberId`(始终是登录账号自己),**切换视角不改变 currentMemberId**,靠前端显式传 `memberId` 参数覆盖;新功能禁止新增 `childId` 引用(详见 `docs/architecture/userid-vs-memberid-usage-guide.md`)
 - **身份标识:** `userId`=登录账号(`users.id`)、`memberId`/`familyMemberId`/`currentMemberId`=家庭成员(`family_members.id`)、`childId`=遗留表(`children.id`);账号自身/交易/权限判断用 userId,成员/孩子数据(任务/健康/成长/关系/五维能量)用 memberId;`JwtInterceptor` 按 `userId+familyId` 自动反查 `currentMemberId`(始终是登录账号自己),**切换视角不改变 currentMemberId**,靠前端显式传 `memberId` 参数覆盖;新功能禁止新增 `childId` 引用(详见 `docs/architecture/userid-vs-memberid-usage-guide.md`)
 
 
+## 用户模型统一口径
+
+- **能登录用户**:`family_members.userId` 非空且关联 User 有 openid(VO 层 `memberType=login`)
+- **不能登录用户**(历史称"孩子"):`family_members.userId` 为空或关联 User 无 openid(VO 层 `memberType=local`),由同家庭能登录用户代管其数据(任务/健康/DOA 等)
+- 新功能禁止再引入"孩子"概念,统一用"能登录用户/不能登录用户"表述
+
 ## ANTI-PATTERNS (本项目禁止)
 ## ANTI-PATTERNS (本项目禁止)
 
 
 - **DO NOT** 在 Controller 直接操作 Mapper
 - **DO NOT** 在 Controller 直接操作 Mapper

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/common/BasicQuotaExhaustedException.java

@@ -0,0 +1,7 @@
+package com.etotem.cfc.common;
+
+public class BasicQuotaExhaustedException extends RuntimeException {
+    public BasicQuotaExhaustedException(String message) {
+        super(message);
+    }
+}

+ 261 - 60
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -927,9 +927,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("迁移规划师到服务商体系失败: {}", e.getMessage());
             log.warn("迁移规划师到服务商体系失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== Phase 1: 浠艾福俱乐部商品系统 ====================
-
-        // 商品表
+        // ==================== Phase 1: 浠艾福俱乐部商品系统 ======        // 商品表
         try {
         try {
             jdbcTemplate.execute(
             jdbcTemplate.execute(
                     "CREATE TABLE IF NOT EXISTS products (" +
                     "CREATE TABLE IF NOT EXISTS products (" +
@@ -1189,9 +1187,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("补充 Product 种子数据初始化失败: {}", e.getMessage());
             log.warn("补充 Product 种子数据初始化失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== Phase 2: 内容区块可见性配置 ====================
-
-        // 内容区块配置表
+        // ==================== Phase 2: 内容区块可见性配置 ======        // 内容区块配置表
         try {
         try {
             jdbcTemplate.execute(
             jdbcTemplate.execute(
                     "CREATE TABLE IF NOT EXISTS content_sections (" +
                     "CREATE TABLE IF NOT EXISTS content_sections (" +
@@ -1229,9 +1225,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("ContentSection 种子数据初始化失败: {}", e.getMessage());
             log.warn("ContentSection 种子数据初始化失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== 文章内容系统种子数据 ====================
-
-        // 文章分类种子数据
+        // ==================== 文章内容系统种子数据 ======        // 文章分类种子数据
         try {
         try {
             Integer catCount = jdbcTemplate.queryForObject(
             Integer catCount = jdbcTemplate.queryForObject(
                     "SELECT COUNT(*) FROM article_categories WHERE id IN (1,2,3,4,5)", Integer.class);
                     "SELECT COUNT(*) FROM article_categories WHERE id IN (1,2,3,4,5)", Integer.class);
@@ -1248,9 +1242,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("文章分类种子数据初始化失败: {}", e.getMessage());
             log.warn("文章分类种子数据初始化失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== 佣金系统 ====================
-
-        // 迁移: users表添加推荐人字段
+        // ==================== 佣金系统 ======        // 迁移: users表添加推荐人字段
         try {
         try {
             Integer exists = jdbcTemplate.queryForObject(
             Integer exists = jdbcTemplate.queryForObject(
                     "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'referrer_id'",
                     "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'referrer_id'",
@@ -1326,9 +1318,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("创建withdrawal_requests表失败: {}", e.getMessage());
             log.warn("创建withdrawal_requests表失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== 会员体系 & 佣金扩展 ====================
-
-        // 迁移: users表添加会员字段
+        // ==================== 会员体系 & 佣金扩展 ======        // 迁移: users表添加会员字段
         try {
         try {
             Integer exists = jdbcTemplate.queryForObject(
             Integer exists = jdbcTemplate.queryForObject(
                     "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'member_level'",
                     "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'member_level'",
@@ -1386,9 +1376,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("创建member_upgrade_records表失败: {}", e.getMessage());
             log.warn("创建member_upgrade_records表失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== 文章内容系统 ====================
-
-        // 文章分类表
+        // ==================== 文章内容系统 ======        // 文章分类表
         try {
         try {
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS article_categories (" +
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS article_categories (" +
                     "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
                     "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
@@ -1484,9 +1472,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("创建article_tags表失败: {}", e.getMessage());
             log.warn("创建article_tags表失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== 五维能量系统(账本模式) ====================
-
-        // 维度定义表
+        // ==================== 五维能量系统(账本模式) ======        // 维度定义表
         try {
         try {
             jdbcTemplate.execute(
             jdbcTemplate.execute(
                     "CREATE TABLE IF NOT EXISTS energy_dimension (" +
                     "CREATE TABLE IF NOT EXISTS energy_dimension (" +
@@ -1790,7 +1776,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("初始化商品能量比例配置失败: {}", e.getMessage());
             log.warn("初始化商品能量比例配置失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== 精准营养 P0: health_report_survey ====================
+        // ==================== 精准营养 P0: health_report_survey =============        
         try {
         try {
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_report_survey (" +
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_report_survey (" +
                     "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
                     "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
@@ -1808,7 +1794,8 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("创建 health_report_survey 表失败: {}", e.getMessage());
             log.warn("创建 health_report_survey 表失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== 精准营养 P0: health_gut_flora + health_disease_risk ====================
+        // ==================== 精准营养 P0: health_gut_flora + health_disease_risk =============
+        
         try {
         try {
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_gut_flora (" +
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_gut_flora (" +
                     "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
                     "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
@@ -1842,7 +1829,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("创建 health_disease_risk 表失败: {}", e.getMessage());
             log.warn("创建 health_disease_risk 表失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== 迁移: health_reports 表添加新字段 ====================
+        // ==================== 迁移: health_reports 表添加新字段 =============        
         try {
         try {
             jdbcTemplate.execute("ALTER TABLE health_reports ADD COLUMN balance_score INT DEFAULT NULL COMMENT '菌群平衡分'");
             jdbcTemplate.execute("ALTER TABLE health_reports ADD COLUMN balance_score INT DEFAULT NULL COMMENT '菌群平衡分'");
             log.info("已添加balance_score列到health_reports表");
             log.info("已添加balance_score列到health_reports表");
@@ -1893,7 +1880,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             // 列已存在,忽略错误
             // 列已存在,忽略错误
         }
         }
 
 
-        // ==================== 迁移: health_gut_flora 表添加新字段 ====================
+        // ==================== 迁移: health_gut_flora 表添加新字段 =============        
         try {
         try {
             jdbcTemplate.execute("ALTER TABLE health_gut_flora ADD COLUMN population_level VARCHAR(50) DEFAULT NULL COMMENT '处于人群%水平'");
             jdbcTemplate.execute("ALTER TABLE health_gut_flora ADD COLUMN population_level VARCHAR(50) DEFAULT NULL COMMENT '处于人群%水平'");
             log.info("已添加population_level列到health_gut_flora表");
             log.info("已添加population_level列到health_gut_flora表");
@@ -1922,9 +1909,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             // 列已存在,忽略错误
             // 列已存在,忽略错误
         }
         }
 
 
-        // ==================== 迁移: guides 表统一 ====================
-
-        // 迁移: guides 添加 type 字段(区分服务商类型)
+        // ==================== 迁移: guides 表统一 ======        // 迁移: guides 添加 type 字段(区分服务商类型)
         try {
         try {
             jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN type VARCHAR(32) DEFAULT 'teacher' COMMENT '服务商类型: teacher/planner/activity_provider/product_supplier'");
             jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN type VARCHAR(32) DEFAULT 'teacher' COMMENT '服务商类型: teacher/planner/activity_provider/product_supplier'");
             log.info("已添加type列到guides表");
             log.info("已添加type列到guides表");
@@ -2263,8 +2248,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("创建 cart 表失败: {}", e.getMessage());
             log.warn("创建 cart 表失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== DanShop 电商模块建表 ====================
-        // 1. danshop_categories (商品分类表)
+        // ==================== DanShop 电商模块建表 =============        // 1. danshop_categories (商品分类表)
         try {
         try {
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS danshop_categories (" +
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS danshop_categories (" +
                     "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
                     "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
@@ -2506,7 +2490,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("创建 danshop_addresses 表失败: {}", e.getMessage());
             log.warn("创建 danshop_addresses 表失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== 迁移: activities表添加visible_scope列 ====================
+        // ==================== 迁移: activities表添加visible_scope列 =============        
         try {
         try {
             Integer colExists = jdbcTemplate.queryForObject(
             Integer colExists = jdbcTemplate.queryForObject(
                     "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'activities' AND COLUMN_NAME = 'visible_scope'",
                     "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'activities' AND COLUMN_NAME = 'visible_scope'",
@@ -2521,7 +2505,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("检查/添加activities表visible_scope列失败: {}", e.getMessage());
             log.warn("检查/添加activities表visible_scope列失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== 迁移: articles表添加article_type列 ====================
+        // ==================== 迁移: articles表添加article_type列 =============        
         try {
         try {
             Integer colExists = jdbcTemplate.queryForObject(
             Integer colExists = jdbcTemplate.queryForObject(
                     "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'articles' AND COLUMN_NAME = 'article_type'",
                     "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'articles' AND COLUMN_NAME = 'article_type'",
@@ -2536,7 +2520,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("检查/添加articles表article_type列失败: {}", e.getMessage());
             log.warn("检查/添加articles表article_type列失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== 迁移: articles表添加word_count列 ====================
+        // ==================== 迁移: articles表添加word_count列 =============        
         try {
         try {
             Integer colExists = jdbcTemplate.queryForObject(
             Integer colExists = jdbcTemplate.queryForObject(
                     "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'articles' AND COLUMN_NAME = 'word_count'",
                     "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'articles' AND COLUMN_NAME = 'word_count'",
@@ -2551,7 +2535,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("检查/添加articles表word_count列失败: {}", e.getMessage());
             log.warn("检查/添加articles表word_count列失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== 迁移: article_reading_records表添加task_id列 ====================
+        // ==================== 迁移: article_reading_records表添加task_id列 =============        
         try {
         try {
             Integer colExists = jdbcTemplate.queryForObject(
             Integer colExists = jdbcTemplate.queryForObject(
                     "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'article_reading_records' AND COLUMN_NAME = 'task_id'",
                     "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'article_reading_records' AND COLUMN_NAME = 'task_id'",
@@ -2566,7 +2550,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("检查/添加article_reading_records表task_id列失败: {}", e.getMessage());
             log.warn("检查/添加article_reading_records表task_id列失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== 迁移: tasks表添加source_type列 ====================
+        // ==================== 迁移: tasks表添加source_type列 =============        
         try {
         try {
             Integer colExists = jdbcTemplate.queryForObject(
             Integer colExists = jdbcTemplate.queryForObject(
                     "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'tasks' AND COLUMN_NAME = 'source_type'",
                     "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'tasks' AND COLUMN_NAME = 'source_type'",
@@ -2581,7 +2565,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("检查/添加tasks表source_type列失败: {}", e.getMessage());
             log.warn("检查/添加tasks表source_type列失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== 迁移: tasks表添加source_id列 ====================
+        // ==================== 迁移: tasks表添加source_id列 =============        
         try {
         try {
             Integer colExists = jdbcTemplate.queryForObject(
             Integer colExists = jdbcTemplate.queryForObject(
                     "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'tasks' AND COLUMN_NAME = 'source_id'",
                     "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'tasks' AND COLUMN_NAME = 'source_id'",
@@ -2596,7 +2580,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("检查/添加tasks表source_id列失败: {}", e.getMessage());
             log.warn("检查/添加tasks表source_id列失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== 迁移: activities表添加checkin_points列 ====================
+        // ==================== 迁移: activities表添加checkin_points列 =============        
         try {
         try {
             Integer colExists = jdbcTemplate.queryForObject(
             Integer colExists = jdbcTemplate.queryForObject(
                     "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'activities' AND COLUMN_NAME = 'checkin_points'",
                     "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'activities' AND COLUMN_NAME = 'checkin_points'",
@@ -2631,7 +2615,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("创建family_invitations表失败: {}", e.getMessage());
             log.warn("创建family_invitations表失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== P0 营养师: nutrition_indicator_mapping ====================
+        // ==================== P0 营养师: nutrition_indicator_mapping =============        
         try {
         try {
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS nutrition_indicator_mapping (" +
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS nutrition_indicator_mapping (" +
                     "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
                     "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
@@ -2711,7 +2695,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("nutrition_indicator_mapping 种子数据初始化失败: {}", e.getMessage());
             log.warn("nutrition_indicator_mapping 种子数据初始化失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== 心版块 Phase 1: emotion_checkin 表 ====================
+        // ==================== 心版块 Phase 1: emotion_checkin 表 =============        
         try {
         try {
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS emotion_checkin (" +
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS emotion_checkin (" +
                     "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
                     "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
@@ -2738,8 +2722,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("创建 emotion_checkin 表失败: {}", e.getMessage());
             log.warn("创建 emotion_checkin 表失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== 1a-2: DAN Schema 修复 ====================
-        // v1.2 EMI 心理子维度
+        // ==================== 1a-2: DAN Schema 修复 =============        // v1.2 EMI 心理子维度
         String[] danEmiFields = {"emotion_score", "resilience_score", "stress_coping_score", "self_awareness_score"};
         String[] danEmiFields = {"emotion_score", "resilience_score", "stress_coping_score", "self_awareness_score"};
         for (String field : danEmiFields) {
         for (String field : danEmiFields) {
             try {
             try {
@@ -2785,7 +2768,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             }
             }
         }
         }
 
 
-        // ==================== 1b-1: bazi_reading_template 五行命理解读表 ====================
+        // ==================== 1b-1: bazi_reading_template 五行命理解读表 =============        
         try {
         try {
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS bazi_reading_template (" +
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS bazi_reading_template (" +
                     "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
                     "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
@@ -2802,7 +2785,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("创建 bazi_reading_template 表失败: {}", e.getMessage());
             log.warn("创建 bazi_reading_template 表失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== 1b-1: numsoul_config 生命灵数配置表 ====================
+        // ==================== 1b-1: numsoul_config 生命灵数配置表 =============        
         try {
         try {
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS numsoul_config (" +
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS numsoul_config (" +
                     "number INT PRIMARY KEY COMMENT '灵数1-9', " +
                     "number INT PRIMARY KEY COMMENT '灵数1-9', " +
@@ -2817,7 +2800,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("创建 numsoul_config 表失败: {}", e.getMessage());
             log.warn("创建 numsoul_config 表失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== 1b-1: bazi_reading_template 种子数据 ====================
+        // ==================== 1b-1: bazi_reading_template 种子数据 =============        
         try {
         try {
             Integer count = jdbcTemplate.queryForObject(
             Integer count = jdbcTemplate.queryForObject(
                     "SELECT COUNT(*) FROM bazi_reading_template", Integer.class);
                     "SELECT COUNT(*) FROM bazi_reading_template", Integer.class);
@@ -2848,7 +2831,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("bazi_reading_template 种子数据初始化失败: {}", e.getMessage());
             log.warn("bazi_reading_template 种子数据初始化失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== 1b-1: numsoul_config 种子数据 ====================
+        // ==================== 1b-1: numsoul_config 种子数据 =============        
         try {
         try {
             Integer count = jdbcTemplate.queryForObject(
             Integer count = jdbcTemplate.queryForObject(
                     "SELECT COUNT(*) FROM numsoul_config", Integer.class);
                     "SELECT COUNT(*) FROM numsoul_config", Integer.class);
@@ -2894,9 +2877,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("创建 user_address 表失败: {}", e.getMessage());
             log.warn("创建 user_address 表失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== 会员价格体系 ====================
-
-        // 迁移: activities表添加member_price列
+        // ==================== 会员价格体系 ======        // 迁移: activities表添加member_price列
         try {
         try {
             Integer exists = jdbcTemplate.queryForObject(
             Integer exists = jdbcTemplate.queryForObject(
                     "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'activities' AND COLUMN_NAME = 'member_price'",
                     "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'activities' AND COLUMN_NAME = 'member_price'",
@@ -2932,7 +2913,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("创建member_discount_configs表失败: {}", e.getMessage());
             log.warn("创建member_discount_configs表失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== 食材推荐指数 Phase: food_recommend_idx / food_recommend_idx_log ====================
+        // ==================== 食材推荐指数 Phase: food_recommend_idx / food_recommend_idx_log =============        
         try {
         try {
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `food_recommend_idx` (" +
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `food_recommend_idx` (" +
                     "`id` BIGINT NOT NULL AUTO_INCREMENT, " +
                     "`id` BIGINT NOT NULL AUTO_INCREMENT, " +
@@ -3140,8 +3121,7 @@ public class DatabaseInitializer implements CommandLineRunner {
 
 
         log.info("数据库迁移完成");
         log.info("数据库迁移完成");
 
 
-        // ==================== 健康维度 Phase 1: health_dimension_score / health_data_source_record / health_norm_reference ====================
-        migrateHealthDimensionsTable();
+        // ==================== 健康维度 Phase 1: health_dimension_score / health_data_source_record / health_norm_reference =============        migrateHealthDimensionsTable();
         seedHealthNorms();
         seedHealthNorms();
         seedMindActionWealthNorms();
         seedMindActionWealthNorms();
         runLateMigrations();
         runLateMigrations();
@@ -3158,7 +3138,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("sleep_record 时间列修改失败(可能已为 TIME): {}", e.getMessage());
             log.warn("sleep_record 时间列修改失败(可能已为 TIME): {}", e.getMessage());
         }
         }
 
 
-        // ==================== 迁移270: 先天画像来源权重配置表 ====================
+        // ==================== 迁移270: 先天画像来源权重配置表 =============        
         try {
         try {
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS innate_portrait_config (" +
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS innate_portrait_config (" +
                     "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
                     "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
@@ -3190,7 +3170,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("迁移270失败: {}", e.getMessage());
             log.warn("迁移270失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== 迁移271: 先天画像报告缓存表 ====================
+        // ==================== 迁移271: 先天画像报告缓存表 =============        
         try {
         try {
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS innate_portrait_report (" +
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS innate_portrait_report (" +
                     "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
                     "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
@@ -3208,7 +3188,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("迁移271失败: {}", e.getMessage());
             log.warn("迁移271失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== 迁移272: 数字能量详细配置表 ====================
+        // ==================== 迁移272: 数字能量详细配置表 =============        
         try {
         try {
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS numsoul_detail_config (" +
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS numsoul_detail_config (" +
                     "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
                     "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
@@ -4347,8 +4327,7 @@ sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min,
             log.warn("创建insurance_policies表失败: {}", e.getMessage());
             log.warn("创建insurance_policies表失败: {}", e.getMessage());
         }
         }
 
 
-        // ==================== 健康检测报告模块 ====================
-        // 健康报告表(体检报告/菌群检测报告)
+        // ==================== 健康检测报告模块 =============        // 健康报告表(体检报告/菌群检测报告)
         try {
         try {
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_reports (" +
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_reports (" +
                     "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
                     "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
@@ -10925,7 +10904,183 @@ sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min,
             log.warn("迁移314: 添加新 UNIQUE KEY 失败: {}", e.getMessage());
             log.warn("迁移314: 添加新 UNIQUE KEY 失败: {}", e.getMessage());
         }
         }
 
 
-        // 迁移315: 商品/分类可见性由「角色」改为「按会员级别」控制
+// 迁移315: membership_levels 新增 BASIC 体验会员等级(9.9元/月)
+        try {
+            jdbcTemplate.execute(
+                "INSERT IGNORE INTO membership_levels " +
+                "(level_code, level_name, level_desc, price_monthly, features, max_children, max_tasks_per_day) " +
+                "VALUES ('BASIC', '体验会员', " +
+                "'9.9元/月体验版,解锁核心功能,限次使用AI与报告解读', " +
+                "990, " +
+                "'[\"free_activities\",\"free_courses\",\"discount_purchase\",\"invite_family\",\"mascot_ai_lite\",\"ai_report_reading_lite\",\"assessment_quota_lite\",\"health_report_upload_lite\",\"growth_plan_lite\",\"task_template_lite\"]', " +
+                "3, 3)");
+            log.info("迁移315: 已初始化BASIC体验会员等级");
+        } catch (Exception e) {
+            // 等级已存在,忽略
+        }
+
+        // 迁移316: membership_benefits 新增 BASIC 会员权益
+        try {
+            jdbcTemplate.execute(
+                "INSERT IGNORE INTO membership_benefits " +
+                "(level_code, benefit_code, benefit_name, benefit_desc, icon, bg_color, sort_order) VALUES " +
+                "('BASIC', 'AI_CHAT_LITE', 'AI健康顾问(限3次/月)', '每月3次AI健康顾问问答', '🤖', '#DBEAFE', 1), " +
+                "('BASIC', 'REPORT_READING_LITE', '报告解读(限1次/月)', '每月1次体检/测评报告AI解读', '📊', '#FCE7F3', 4), " +
+                "('BASIC', 'GROWTH_PLAN_LITE', '基础成长计划', '创建成长计划但模板有限', '📋', '#FEF3C7', 3), " +
+                "('BASIC', 'ASSESSMENT_QUOTA_LITE', '测评预约(限1次)', '每月可预约1次测评', '📝', '#E0E7FF', 8), " +
+                "('BASIC', 'REPORT_UPLOAD_LITE', '健康报告上传(限2份/月)', '每月可上传2份健康报告', '📄', '#FEF9C3', 9)");
+            log.info("迁移316: 已初始化BASIC会员权益列表");
+        } catch (Exception e) {
+            // 已存在,忽略
+        }
+
+        // 迁移317: 创建 basic_quota_usage 表(BASIC会员月度配额使用记录)
+        try {
+            jdbcTemplate.execute(
+                "CREATE TABLE IF NOT EXISTS basic_quota_usage (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "user_id BIGINT NOT NULL COMMENT '家长用户ID', " +
+                "quota_type VARCHAR(30) NOT NULL COMMENT '配额类型', " +
+                "used_count INT DEFAULT 0 COMMENT '已用次数', " +
+                "total_count INT NOT NULL COMMENT '月度总配额', " +
+                "period_start DATE NOT NULL COMMENT '周期开始日期', " +
+                "period_end DATE NOT NULL COMMENT '周期结束日期', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                "UNIQUE KEY uk_user_type_period (user_id, quota_type, period_start), " +
+                "INDEX idx_user_period (user_id, period_start) " +
+                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='BASIC会员月度配额使用记录'");
+            log.info("迁移317: 已创建basic_quota_usage表");
+        } catch (Exception e) {
+            // 表已存在,忽略
+        }
+
+        // 迁移318: 创建 DOA 个人目标-周计划-周复盘三表(家庭打卡个人目标模块,2026-09-12)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS doa_stage (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
+                    "member_id BIGINT NOT NULL COMMENT '目标归属成员ID(family_members.id)', " +
+                    "title VARCHAR(200) COMMENT '阶段主题(可选)', " +
+                    "start_date DATE NOT NULL COMMENT '阶段开始日期', " +
+                    "end_date DATE NOT NULL COMMENT '阶段结束日期', " +
+                    "duration_weeks TINYINT NOT NULL COMMENT '周期周数:2/4/8/12', " +
+                    "status VARCHAR(20) DEFAULT 'active' COMMENT 'active/completed/cancelled', " +
+                    "created_by BIGINT NOT NULL COMMENT '创建人userId(代管时为代管人)', " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                    "INDEX idx_ds_family_member (family_id, member_id), " +
+                    "INDEX idx_ds_status (status) " +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='DOA 阶段'");
+            log.info("迁移318: 已创建 doa_stage 表");
+        } catch (Exception e) {
+            log.warn("迁移318: 创建 doa_stage 表失败: {}", e.getMessage());
+        }
+
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS doa_goal (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "stage_id BIGINT NOT NULL COMMENT '所属阶段ID', " +
+                    "domain VARCHAR(20) NOT NULL COMMENT '领域:health/wealth/family/social/help', " +
+                    "content VARCHAR(500) NOT NULL COMMENT '量化目标内容', " +
+                    "status VARCHAR(20) DEFAULT 'active' COMMENT 'active/achieved/abandoned', " +
+                    "sort_order TINYINT DEFAULT 0 COMMENT '排序', " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                    "INDEX idx_dg_stage (stage_id) " +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='DOA 阶段目标'");
+            log.info("迁移318: 已创建 doa_goal 表");
+        } catch (Exception e) {
+            log.warn("迁移318: 创建 doa_goal 表失败: {}", e.getMessage());
+        }
+
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS doa_week (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "stage_id BIGINT NOT NULL COMMENT '所属阶段ID', " +
+                    "week_no TINYINT NOT NULL COMMENT '第几周(1起)', " +
+                    "week_start DATE NOT NULL COMMENT '本周开始(周一)', " +
+                    "week_end DATE NOT NULL COMMENT '本周结束(周日)', " +
+                    "plan TEXT COMMENT '周行动宣告计划(A表)', " +
+                    "achieved TEXT COMMENT '复盘:本周达成了什么', " +
+                    "experience TEXT COMMENT '复盘:有什么体验', " +
+                    "worked TEXT COMMENT '复盘:行得通的', " +
+                    "improve TEXT COMMENT '复盘:要提升/行不通的', " +
+                    "next_challenge TEXT COMMENT '复盘:下周挑战自己什么', " +
+                    "status VARCHAR(20) DEFAULT 'pending' COMMENT 'pending/planned/reviewed', " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                    "UNIQUE KEY uk_dw_stage_week (stage_id, week_no), " +
+                    "INDEX idx_dw_stage (stage_id) " +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='DOA 周记录'");
+            log.info("迁移318: 已创建 doa_week 表");
+        } catch (Exception e) {
+            log.warn("迁移318: 创建 doa_week 表失败: {}", e.getMessage());
+        }
+
+        // 迁移318: 基础管理分组挂载「系统提示词」菜单(cfc-web菜单树同步)
+        migrateSystemPromptMenu();
+
+        // 迁移319: system_prompts 插入 health_plan 提示词种子(仅当不存在时)
+        try {
+            Integer count = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM system_prompts WHERE prompt_key = 'health_plan'", Integer.class);
+            if (count == null || count == 0) {
+                jdbcTemplate.update(
+                    "INSERT INTO system_prompts (prompt_key, title, content, default_content, version, status, remark) VALUES (?, ?, ?, ?, 1, 1, ?)",
+                    "health_plan", "健康方案生成",
+                    "你是一个专业的家庭健康方案规划师。根据用户的健康数据和目标,生成结构化的改善方案。\n\n" +
+                    "## 输出格式(必须严格遵守 JSON)\n\n" +
+                    "```\n" +
+                    "{\n" +
+                    "  \"overview\": \"总体概述(100字以内,说明方案目标和核心策略)\",\n" +
+                    "  \"sections\": [\n" +
+                    "    {\n" +
+                    "      \"key\": \"nutrition\",\n" +
+                    "      \"title\": \"营养补充建议\",\n" +
+                    "      \"content\": \"Markdown 格式的详细内容,包含具体产品推荐、用量、服用时间\",\n" +
+                    "      \"items\": [{\"name\": \"产品名\", \"dosage\": \"用量\", \"timing\": \"服用时间\", \"reason\": \"推荐理由\"}]\n" +
+                    "    },\n" +
+                    "    {\n" +
+                    "      \"key\": \"diet\",\n" +
+                    "      \"title\": \"饮食建议\",\n" +
+                    "      \"content\": \"Markdown 格式的餐饮建议,包含早餐/午餐/晚餐建议\",\n" +
+                    "      \"items\": [{\"meal\": \"餐型\", \"food\": \"食物建议\", \"notes\": \"注意事项\"}]\n" +
+                    "    },\n" +
+                    "    {\n" +
+                    "      \"key\": \"exercise\",\n" +
+                    "      \"title\": \"运动计划\",\n" +
+                    "      \"content\": \"Markdown 格式的运动建议,包含频率、时长、类型\",\n" +
+                    "      \"items\": [{\"type\": \"运动类型\", \"duration\": \"时长\", \"frequency\": \"频率\", \"notes\": \"注意事项\"}]\n" +
+                    "    }\n" +
+                    "  ]\n" +
+                    "}\n" +
+                    "```\n\n" +
+                    "## 原则\n" +
+                    "1. 基于实际数据给出建议,不编造\n" +
+                    "2. 引用知识库时标注来源\n" +
+                    "3. 建议要具体可执行,避免空泛\n" +
+                    "4. 营养补充部分要具体到品牌/产品类型和用量\n" +
+                    "5. 严重健康问题建议咨询医生",
+                    "你是一个专业的家庭健康方案规划师。根据用户的健康数据和目标,生成结构化的改善方案。",
+                    "健康方案生成 prompt(营养+饮食+运动三段结构化输出)");
+                log.info("迁移319: 已插入 health_plan 提示词种子数据");
+            } else {
+                log.info("迁移319: health_plan 提示词已存在,跳过");
+            }
+        } catch (Exception e) {
+            log.warn("迁移319: 插入 health_plan 提示词种子数据失败: {}", e.getMessage());
+        }
+
+        // 迁移320: exercise_record 表添加 step_count 列(微信运动步数同步)
+        try {
+            jdbcTemplate.execute("ALTER TABLE exercise_record ADD COLUMN step_count INT COMMENT '微信运动步数'");
+            log.info("迁移320: exercise_record.step_count 列已添加");
+        } catch (Exception e) {
+            log.warn("迁移320: exercise_record.step_count 列已存在或添加失败: {}", e.getMessage());
+        }
+
+        // 迁移321: 商品/分类可见性由「角色」改为「按会员级别」控制
         // - membership_levels.level_order: 级别数字(数字越小级别越低): FREE=1 FAMILY=2 PROVIDER=3 PREMIUM=4 LIFETIME=5
         // - membership_levels.level_order: 级别数字(数字越小级别越低): FREE=1 FAMILY=2 PROVIDER=3 PREMIUM=4 LIFETIME=5
         // - products.min_visible_level: 最低可见会员级别编码(NULL=全部可见)
         // - products.min_visible_level: 最低可见会员级别编码(NULL=全部可见)
         // - shop_categories.min_visible_level: 分类最低可见会员级别编码(NULL=全部可见)
         // - shop_categories.min_visible_level: 分类最低可见会员级别编码(NULL=全部可见)
@@ -10935,15 +11090,61 @@ sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min,
             jdbcTemplate.update("UPDATE membership_levels SET level_order = CASE level_code " +
             jdbcTemplate.update("UPDATE membership_levels SET level_order = CASE level_code " +
                     "WHEN 'FREE' THEN 1 WHEN 'FAMILY' THEN 2 WHEN 'PROVIDER' THEN 3 " +
                     "WHEN 'FREE' THEN 1 WHEN 'FAMILY' THEN 2 WHEN 'PROVIDER' THEN 3 " +
                     "WHEN 'PREMIUM' THEN 4 WHEN 'LIFETIME' THEN 5 ELSE level_order END");
                     "WHEN 'PREMIUM' THEN 4 WHEN 'LIFETIME' THEN 5 ELSE level_order END");
-            log.info("迁移315: 已回填 membership_levels.level_order");
+            log.info("迁移321: 已回填 membership_levels.level_order");
         } catch (Exception e) {
         } catch (Exception e) {
-            log.warn("迁移315: 回填 membership_levels.level_order 失败: {}", e.getMessage());
+            log.warn("迁移321: 回填 membership_levels.level_order 失败: {}", e.getMessage());
         }
         }
         ensureColumn("products", "min_visible_level",
         ensureColumn("products", "min_visible_level",
                 "VARCHAR(50) DEFAULT NULL COMMENT '最低可见会员级别编码(FREE/FAMILY/PROVIDER/PREMIUM/LIFETIME, NULL=全部可见)'");
                 "VARCHAR(50) DEFAULT NULL COMMENT '最低可见会员级别编码(FREE/FAMILY/PROVIDER/PREMIUM/LIFETIME, NULL=全部可见)'");
         ensureColumn("shop_categories", "min_visible_level",
         ensureColumn("shop_categories", "min_visible_level",
                 "VARCHAR(50) DEFAULT NULL COMMENT '最低可见会员级别编码(FREE/FAMILY/PROVIDER/PREMIUM/LIFETIME, NULL=全部可见)'");
                 "VARCHAR(50) DEFAULT NULL COMMENT '最低可见会员级别编码(FREE/FAMILY/PROVIDER/PREMIUM/LIFETIME, NULL=全部可见)'");
-        log.info("迁移315: 已完成商品/分类按会员级别可见性改造");
+        log.info("迁移321: 已完成商品/分类按会员级别可见性改造");
+    }
+
+    /**
+     * 迁移318: 系统配置·基础管理 挂载 系统提示词 菜单(幂等)
+     * 仅当 sys_menu 表已有数据(即角色授权已存在)时补授权,避免破坏兼容逻辑
+     */
+    private void migrateSystemPromptMenu() {
+        try {
+            Long baseManageId = queryMenuId("SELECT id FROM sys_menu WHERE title='基础管理'");
+            if (baseManageId == null) {
+                log.warn("迁移318: 未找到基础管理分组,跳过");
+                return;
+            }
+            Long promptId = insertSystemPromptMenu(baseManageId);
+            if (promptId != null) {
+                Integer grantCount = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM sys_role_menu", Integer.class);
+                if (grantCount != null && grantCount > 0) {
+                    grantMenuToRole("admin", promptId);
+                    log.info("迁移318: 已授权 admin 角色");
+                }
+                log.info("迁移318: 系统提示词菜单挂载完成 id={}", promptId);
+            }
+        } catch (Exception e) {
+            log.warn("迁移318失败: {}", e.getMessage());
+        }
+    }
+
+    /**
+     * 按 path 幂等插入系统提示词叶子菜单,返回菜单 id(已存在返回原 id)
+     */
+    private Long insertSystemPromptMenu(Long parentId) {
+        Long existed = queryMenuId("SELECT id FROM sys_menu WHERE path='/system-prompt'");
+        if (existed != null) {
+            return existed;
+        }
+        try {
+            jdbcTemplate.update(
+                    "INSERT INTO sys_menu (parent_id, title, label, path, icon, perm, sort, visible) VALUES (?, NULL, '系统提示词', '/system-prompt', 'el-icon-chat-line-square', 'system:base', 8, 1)",
+                    parentId);
+            Long id = jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Long.class);
+            log.info("迁移318: 已挂载系统提示词菜单");
+            return id;
+        } catch (Exception e) {
+            log.warn("迁移318: 挂载菜单失败: {}", e.getMessage());
+            return null;
+        }
     }
     }
 
 
     private void seedGutFloraFingerprints() {
     private void seedGutFloraFingerprints() {

+ 47 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/BasicQuotaController.java

@@ -0,0 +1,47 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.QuotaStatusDTO;
+import com.etotem.cfc.service.BasicQuotaService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/basic-quota")
+public class BasicQuotaController {
+
+    @Resource
+    private BasicQuotaService basicQuotaService;
+
+    /**
+     * 查询当前用户某配额的剩余次数
+     */
+    @PostMapping("/status")
+    public Result<QuotaStatusDTO> getQuotaStatus(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, String> body) {
+        String quotaType = body.get("quotaType");
+        if (quotaType == null) {
+            return Result.error(400, "quotaType 不能为空");
+        }
+        QuotaStatusDTO dto = basicQuotaService.getMonthlyQuota(userId, quotaType);
+        return Result.success(dto);
+    }
+
+    /**
+     * 批量查询当前用户所有配额状态
+     */
+    @PostMapping("/status/all")
+    public Result<Map<String, QuotaStatusDTO>> getAllQuotaStatus(
+            @RequestAttribute("userId") Long userId) {
+        Map<String, QuotaStatusDTO> result = new HashMap<>();
+        result.put("AI_CHAT", basicQuotaService.getMonthlyQuota(userId, "AI_CHAT"));
+        result.put("REPORT_READING", basicQuotaService.getMonthlyQuota(userId, "REPORT_READING"));
+        result.put("ASSESSMENT_QUOTA", basicQuotaService.getMonthlyQuota(userId, "ASSESSMENT_QUOTA"));
+        result.put("REPORT_UPLOAD", basicQuotaService.getMonthlyQuota(userId, "REPORT_UPLOAD"));
+        return Result.success(result);
+    }
+}

+ 117 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/DoaController.java

@@ -0,0 +1,117 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.DoaStage;
+import com.etotem.cfc.entity.DoaWeek;
+import com.etotem.cfc.service.DoaService;
+import com.etotem.cfc.util.ParamUtils;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 家庭目标(DOA)REST 接口。
+ * 认证属性 familyId/userId 由 JwtInterceptor 写入。
+ */
+@RestController
+@RequestMapping("/api/doa")
+public class DoaController {
+
+    @Resource
+    private DoaService doaService;
+
+    /** 创建阶段:memberId 为空表示当前登录成员自己,goals 1-3 个 */
+    @PostMapping("/stage/create")
+    public Result<Long> createStage(@RequestBody Map<String, Object> params,
+                                    @RequestAttribute(value = "familyId", required = false) Long familyId,
+                                    @RequestAttribute(value = "userId", required = false) Long userId) {
+        if (familyId == null) {
+            return Result.noFamily("请先创建或加入家庭");
+        }
+        Long memberId = params.get("memberId") != null
+                ? Long.valueOf(params.get("memberId").toString()) : null;
+        String title = params.get("title") != null ? params.get("title").toString() : null;
+        Integer durationWeeks = params.get("durationWeeks") != null
+                ? Integer.parseInt(params.get("durationWeeks").toString()) : null;
+        List<Map<String, Object>> goals = params.get("goals") instanceof List
+                ? (List<Map<String, Object>>) params.get("goals") : null;
+        return doaService.createStage(memberId, userId, familyId, title, durationWeeks, goals);
+    }
+
+    /** 家庭总览:各成员当前 active 阶段 */
+    @PostMapping("/overview")
+    public Result<List<Map<String, Object>>> overview(@RequestAttribute(value = "familyId", required = false) Long familyId) {
+        if (familyId == null) {
+            return Result.noFamily("请先创建或加入家庭");
+        }
+        return Result.success(doaService.getFamilyOverview(familyId));
+    }
+
+    /** 阶段详情:stage + goals + weeks + memberUserId */
+    @PostMapping("/stage/detail")
+    public Result<DoaStage> stageDetail(@RequestBody Map<String, Object> params,
+                                        @RequestAttribute(value = "familyId", required = false) Long familyId) {
+        if (familyId == null) {
+            return Result.noFamily("请先创建或加入家庭");
+        }
+        Long stageId = ParamUtils.getLong(params.get("stageId"));
+        return doaService.getStageDetail(stageId, familyId);
+    }
+
+    /** 取消阶段:仅 active 可取消(本人或代管不能登录成员) */
+    @PostMapping("/stage/cancel")
+    public Result<Void> cancelStage(@RequestBody Map<String, Object> params,
+                                    @RequestAttribute(value = "familyId", required = false) Long familyId,
+                                    @RequestAttribute(value = "userId", required = false) Long userId) {
+        if (familyId == null) {
+            return Result.noFamily("请先创建或加入家庭");
+        }
+        Long stageId = ParamUtils.getLong(params.get("stageId"));
+        return doaService.cancelStage(stageId, userId, familyId);
+    }
+
+    /** 提交周计划(A表) */
+    @PostMapping("/week/plan")
+    public Result<Void> submitPlan(@RequestBody Map<String, Object> params,
+                                   @RequestAttribute(value = "familyId", required = false) Long familyId,
+                                   @RequestAttribute(value = "userId", required = false) Long userId) {
+        if (familyId == null) {
+            return Result.noFamily("请先创建或加入家庭");
+        }
+        Long weekId = ParamUtils.getLong(params.get("weekId"));
+        String plan = params.get("plan") != null ? params.get("plan").toString() : "";
+        return doaService.submitWeekPlan(weekId, plan, userId, familyId);
+    }
+
+    /** 提交周复盘(B表五问) */
+    @PostMapping("/week/review")
+    public Result<Void> submitReview(@RequestBody Map<String, Object> params,
+                                     @RequestAttribute(value = "familyId", required = false) Long familyId,
+                                     @RequestAttribute(value = "userId", required = false) Long userId) {
+        if (familyId == null) {
+            return Result.noFamily("请先创建或加入家庭");
+        }
+        Long weekId = ParamUtils.getLong(params.get("weekId"));
+        Map<String, String> review = new HashMap<>();
+        review.put("achieved", params.get("achieved") != null ? params.get("achieved").toString() : null);
+        review.put("experience", params.get("experience") != null ? params.get("experience").toString() : null);
+        review.put("worked", params.get("worked") != null ? params.get("worked").toString() : null);
+        review.put("improve", params.get("improve") != null ? params.get("improve").toString() : null);
+        review.put("nextChallenge", params.get("nextChallenge") != null ? params.get("nextChallenge").toString() : null);
+        return doaService.submitWeekReview(weekId, review, userId, familyId);
+    }
+
+    /** 周记录列表(按阶段查) */
+    @PostMapping("/week/list")
+    public Result<List<DoaWeek>> weekList(@RequestBody Map<String, Object> params,
+                                          @RequestAttribute(value = "familyId", required = false) Long familyId) {
+        if (familyId == null) {
+            return Result.noFamily("请先创建或加入家庭");
+        }
+        Long stageId = ParamUtils.getLong(params.get("stageId"));
+        return Result.success(doaService.listWeeks(stageId, familyId));
+    }
+}

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

@@ -33,6 +33,7 @@ import com.etotem.cfc.service.FoodService;
 import com.etotem.cfc.service.HealthAnalysisService;
 import com.etotem.cfc.service.HealthAnalysisService;
 import com.etotem.cfc.service.HealthReportDraftService;
 import com.etotem.cfc.service.HealthReportDraftService;
 import com.etotem.cfc.service.HealthReportService;
 import com.etotem.cfc.service.HealthReportService;
+import com.etotem.cfc.service.BasicQuotaService;
 import com.etotem.cfc.service.NutritionDeficiencyService;
 import com.etotem.cfc.service.NutritionDeficiencyService;
 import com.etotem.cfc.service.PdfParseService;
 import com.etotem.cfc.service.PdfParseService;
 import com.etotem.cfc.service.ReportCollectService;
 import com.etotem.cfc.service.ReportCollectService;
@@ -40,6 +41,7 @@ import com.etotem.cfc.service.ReportBlockAssembler;
 import com.etotem.cfc.service.ReportBlockService;
 import com.etotem.cfc.service.ReportBlockService;
 import com.etotem.cfc.service.StorageService;
 import com.etotem.cfc.service.StorageService;
 import com.etotem.cfc.service.TongueDiagnosisService;
 import com.etotem.cfc.service.TongueDiagnosisService;
+import com.etotem.cfc.service.BasicQuotaService;
 import com.etotem.cfc.service.BodyFatScaleService;
 import com.etotem.cfc.service.BodyFatScaleService;
 import com.etotem.cfc.service.api.WechatServiceInterface;
 import com.etotem.cfc.service.api.WechatServiceInterface;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -151,6 +153,9 @@ public class HealthReportController {
     @Resource
     @Resource
     private ReportBlockService reportBlockService;
     private ReportBlockService reportBlockService;
 
 
+    @Resource
+    private BasicQuotaService basicQuotaService;
+
     @Resource
     @Resource
     private StorageService storageService;
     private StorageService storageService;
 
 
@@ -2401,6 +2406,15 @@ public class HealthReportController {
      * 触发 AI 解读
      * 触发 AI 解读
      */
      */
     private void triggerAiAnalysis(Long reportId, Long subjectId, Long proxyUploaderId) {
     private void triggerAiAnalysis(Long reportId, Long subjectId, Long proxyUploaderId) {
+        // BASIC 配额检查(报告 AI 解读)
+        if (proxyUploaderId != null) {
+            com.etotem.cfc.dto.QuotaConsumeResult quotaResult = basicQuotaService.consumeQuota(proxyUploaderId, "REPORT_READING");
+            if (!quotaResult.isSuccess()) {
+                log.info("报告 AI 解读配额不足: userId={}, toast={}", proxyUploaderId, quotaResult.getToastMsg());
+                return;
+            }
+        }
+
         try {
         try {
             Map<String, Object> analyzeReq = new HashMap<>();
             Map<String, Object> analyzeReq = new HashMap<>();
             analyzeReq.put("report_id", reportId);
             analyzeReq.put("report_id", reportId);

+ 27 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminBasicQuotaController.java

@@ -0,0 +1,27 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.service.BasicQuotaService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/admin/basic-quota")
+public class AdminBasicQuotaController {
+
+    @Resource
+    private BasicQuotaService basicQuotaService;
+
+    /**
+     * 运营手动重置指定用户的当月配额(应急用)
+     */
+    @PostMapping("/reset")
+    public Result<String> resetQuota(@RequestBody Map<String, Object> body) {
+        Long userId = Long.valueOf(body.get("userId").toString());
+        String quotaType = body.get("quotaType") != null ? body.get("quotaType").toString() : null;
+        basicQuotaService.refreshMonthlyQuota(); // 重置所有用户当月配额
+        return Result.success("配额刷新完成");
+    }
+}

+ 7 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductOrderController.java

@@ -96,11 +96,17 @@ private QrCodeService qrCodeService;
 
 
     @PostMapping("/vendor-confirm")
     @PostMapping("/vendor-confirm")
     public Result<String> vendorConfirm(@RequestBody Map<String, Object> params,
     public Result<String> vendorConfirm(@RequestBody Map<String, Object> params,
-                                        @RequestAttribute("userId") Long userId) {
+                                         @RequestAttribute("userId") Long userId) {
         String orderNo = (String) params.get("orderNo");
         String orderNo = (String) params.get("orderNo");
         return orderService.vendorConfirm(orderNo, userId);
         return orderService.vendorConfirm(orderNo, userId);
     }
     }
 
 
+    @PostMapping("/review/update")
+    public Result<Map<String, Object>> reviewUpdate(@RequestBody Map<String, Object> params,
+                                                     @RequestAttribute("userId") Long userId) {
+        return orderService.reviewUpdate(params, userId);
+    }
+
     @PostMapping("/ship")
     @PostMapping("/ship")
     public Result<String> ship(@RequestBody Map<String, Object> params) {
     public Result<String> ship(@RequestBody Map<String, Object> params) {
         String orderNo = (String) params.get("orderNo");
         String orderNo = (String) params.get("orderNo");

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

@@ -57,6 +57,8 @@ public class ProductOrderDTO {
     private Date fulfillConfirmedAt;
     private Date fulfillConfirmedAt;
     private Date receivedAt;
     private Date receivedAt;
 
 
+    private Long vendorId;
+
     public static ProductOrderDTO from(ProductOrder o) {
     public static ProductOrderDTO from(ProductOrder o) {
         if (o == null) return null;
         if (o == null) return null;
         ProductOrderDTO d = new ProductOrderDTO();
         ProductOrderDTO d = new ProductOrderDTO();

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

@@ -0,0 +1,11 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+@Data
+public class QuotaConsumeResult {
+    private boolean success;
+    private int remaining;
+    private String toastMsg;
+    private String actionUrl;
+}

+ 14 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/QuotaStatusDTO.java

@@ -0,0 +1,14 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+import java.util.Date;
+
+@Data
+public class QuotaStatusDTO {
+    private String quotaType;
+    private int total;
+    private int used;
+    private int remaining;
+    private Date periodStart;
+    private Date periodEnd;
+}

+ 33 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/BasicQuotaUsage.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("basic_quota_usage")
+public class BasicQuotaUsage implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long userId;
+
+    private String quotaType;
+
+    private Integer usedCount;
+
+    private Integer totalCount;
+
+    private Date periodStart;
+
+    private Date periodEnd;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 31 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/DoaGoal.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("doa_goal")
+public class DoaGoal implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long stageId;
+
+    private String domain;
+
+    private String content;
+
+    private String status;
+
+    private Integer sortOrder;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 64 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/DoaStage.java

@@ -0,0 +1,64 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@Data
+@TableName("doa_stage")
+public class DoaStage implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long familyId;
+
+    private Long memberId;
+
+    private String title;
+
+    private Date startDate;
+
+    private Date endDate;
+
+    private Integer durationWeeks;
+
+    private String status;
+
+    private Long createdBy;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+
+    /** 非表字段:目标列表 */
+    @TableField(exist = false)
+    private List<DoaGoal> goals;
+
+    /** 非表字段:周记录列表 */
+    @TableField(exist = false)
+    private List<DoaWeek> weeks;
+
+    /** 非表字段:目标成员昵称 */
+    @TableField(exist = false)
+    private String memberNickname;
+
+    /** 非表字段:目标成员关联的用户ID(空=不可登录成员,前端用于判断代管权限) */
+    @TableField(exist = false)
+    private Long memberUserId;
+
+    /** 非表字段:已完成周数(复盘过的周) */
+    @TableField(exist = false)
+    private Integer reviewedWeeks;
+
+    /** 非表字段:周总数 */
+    @TableField(exist = false)
+    private Integer totalWeeks;
+}

+ 43 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/DoaWeek.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("doa_week")
+public class DoaWeek implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long stageId;
+
+    private Integer weekNo;
+
+    private Date weekStart;
+
+    private Date weekEnd;
+
+    private String plan;
+
+    private String achieved;
+
+    private String experience;
+
+    private String worked;
+
+    private String improve;
+
+    private String nextChallenge;
+
+    private String status;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

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

@@ -20,6 +20,7 @@ public class HealthExerciseRecord implements Serializable {
     private Integer caloriesBurned;
     private Integer caloriesBurned;
     private Integer heartRateAvg;
     private Integer heartRateAvg;
     private Integer heartRateMax;
     private Integer heartRateMax;
+    private Integer stepCount;
     private Date startTime;
     private Date startTime;
     private Date endTime;
     private Date endTime;
     private String remark;
     private String remark;

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

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

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

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

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

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

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

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

+ 77 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ActivationQuotaService.java

@@ -0,0 +1,77 @@
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.entity.BasicQuotaUsage;
+import com.etotem.cfc.mapper.BasicQuotaUsageMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.*;
+
+@Slf4j
+@Service
+public class ActivationQuotaService {
+
+    @Resource
+    private BasicQuotaUsageMapper basicQuotaUsageMapper;
+
+    private static final Map<String, Integer> QUOTA_LIMITS = new HashMap<>();
+    static {
+        QUOTA_LIMITS.put("AI_CHAT", 3);
+        QUOTA_LIMITS.put("REPORT_READING", 1);
+        QUOTA_LIMITS.put("ASSESSMENT_QUOTA", 1);
+        QUOTA_LIMITS.put("REPORT_UPLOAD", 2);
+    }
+
+    /**
+     * 支付成功后,为 BASIC 用户初始化当月 4 类配额记录
+     */
+    public void activateBasicQuota(Long userId, Long familyId) {
+        if (userId == null) {
+            return;
+        }
+        Date now = new Date();
+        Date monthStart = getMonthStart(now);
+        Date monthEnd = getMonthEnd(now);
+
+        for (Map.Entry<String, Integer> entry : QUOTA_LIMITS.entrySet()) {
+            try {
+                BasicQuotaUsage usage = new BasicQuotaUsage();
+                usage.setUserId(userId);
+                usage.setQuotaType(entry.getKey());
+                usage.setUsedCount(0);
+                usage.setTotalCount(entry.getValue());
+                usage.setPeriodStart(monthStart);
+                usage.setPeriodEnd(monthEnd);
+                usage.setCreatedAt(now);
+                usage.setUpdatedAt(now);
+                basicQuotaUsageMapper.insert(usage);
+                log.info("BASIC配额初始化: userId={}, quotaType={}, total={}", userId, entry.getKey(), entry.getValue());
+            } catch (Exception e) {
+                log.warn("BASIC配额初始化失败: userId={}, quotaType={}", userId, entry.getKey(), e);
+            }
+        }
+    }
+
+    private Date getMonthStart(Date date) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(date);
+        cal.set(Calendar.DAY_OF_MONTH, 1);
+        cal.set(Calendar.HOUR_OF_DAY, 0);
+        cal.set(Calendar.MINUTE, 0);
+        cal.set(Calendar.SECOND, 0);
+        cal.set(Calendar.MILLISECOND, 0);
+        return cal.getTime();
+    }
+
+    private Date getMonthEnd(Date date) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(date);
+        cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
+        cal.set(Calendar.HOUR_OF_DAY, 23);
+        cal.set(Calendar.MINUTE, 59);
+        cal.set(Calendar.SECOND, 59);
+        cal.set(Calendar.MILLISECOND, 999);
+        return cal.getTime();
+    }
+}

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

@@ -60,6 +60,9 @@ public class AiGateway {
     @Resource
     @Resource
     private PiisService piisService;
     private PiisService piisService;
 
 
+    @Resource
+    private BasicQuotaService basicQuotaService;
+
     @PostConstruct
     @PostConstruct
     public void init() {
     public void init() {
         // 应用超时到 HTTP 客户端:
         // 应用超时到 HTTP 客户端:
@@ -159,9 +162,18 @@ public class AiGateway {
      * 调用 Python ChatAgent, 失败时返回 null
      * 调用 Python ChatAgent, 失败时返回 null
      */
      */
     public Map<String, Object> chat(String query, Long userId, String conversationId,
     public Map<String, Object> chat(String query, Long userId, String conversationId,
-                                     Map<String, Object> inputs) {
+                                      Map<String, Object> inputs) {
         if (!enabled || isCircuitOpen()) return null;
         if (!enabled || isCircuitOpen()) return null;
 
 
+        // BASIC 配额检查(仅 BASIC 用户)
+        if (userId != null) {
+            com.etotem.cfc.dto.QuotaConsumeResult quotaResult = basicQuotaService.consumeQuota(userId, "AI_CHAT");
+            if (!quotaResult.isSuccess()) {
+                log.info("AI对话配额不足: userId={}, toast={}", userId, quotaResult.getToastMsg());
+                return null;
+            }
+        }
+
         try {
         try {
             // PII脱敏
             // PII脱敏
             query = piisService.sanitizeText(query);
             query = piisService.sanitizeText(query);

+ 12 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentAppointmentService.java

@@ -24,8 +24,19 @@ public class AssessmentAppointmentService extends ServiceImpl<AssessmentAppointm
     @Resource
     @Resource
     private EnergyService energyService;
     private EnergyService energyService;
 
 
+    @Resource
+    private BasicQuotaService basicQuotaService;
+
     public AssessmentAppointment createAppointment(Long userId, Long memberId, Long guideId,
     public AssessmentAppointment createAppointment(Long userId, Long memberId, Long guideId,
-                                                    Long assessorId, Long packageId, String appointmentDate, String appointmentTime, String notes) {
+                                                   Long assessorId, Long packageId, String appointmentDate, String appointmentTime, String notes) {
+        // BASIC 硬门禁配额检查(测评预约)
+        if (userId != null) {
+            com.etotem.cfc.dto.QuotaConsumeResult quotaResult = basicQuotaService.consumeQuota(userId, "ASSESSMENT_QUOTA");
+            if (!quotaResult.isSuccess()) {
+                throw new com.etotem.cfc.common.BasicQuotaExhaustedException(quotaResult.getToastMsg());
+            }
+        }
+
         AssessmentAppointment appointment = new AssessmentAppointment();
         AssessmentAppointment appointment = new AssessmentAppointment();
         appointment.setUserId(userId);
         appointment.setUserId(userId);
         appointment.setChildId(memberId);
         appointment.setChildId(memberId);

+ 247 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/BasicQuotaService.java

@@ -0,0 +1,247 @@
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.dto.QuotaConsumeResult;
+import com.etotem.cfc.dto.QuotaStatusDTO;
+import com.etotem.cfc.entity.BasicQuotaUsage;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.BasicQuotaUsageMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.*;
+
+@Slf4j
+@Service
+public class BasicQuotaService {
+
+    @Resource
+    private BasicQuotaUsageMapper basicQuotaUsageMapper;
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Resource
+    private MembershipService membershipService;
+
+    // BASIC 会员月度配额上限
+    private static final Map<String, Integer> QUOTA_LIMITS = new HashMap<>();
+    static {
+        QUOTA_LIMITS.put("AI_CHAT", 3);
+        QUOTA_LIMITS.put("REPORT_READING", 1);
+        QUOTA_LIMITS.put("ASSESSMENT_QUOTA", 1);
+        QUOTA_LIMITS.put("REPORT_UPLOAD", 2);
+    }
+
+    /**
+     * 查询指定用户指定月份某配额的剩余次数
+     */
+    public QuotaStatusDTO getMonthlyQuota(Long userId, String quotaType) {
+        if (userId == null || quotaType == null) {
+            return null;
+        }
+        Date now = new Date();
+        Date periodStart = getMonthStart(now);
+        Date periodEnd = getMonthEnd(now);
+
+        BasicQuotaUsage usage = basicQuotaUsageMapper.selectOne(
+                new LambdaQueryWrapper<BasicQuotaUsage>()
+                        .eq(BasicQuotaUsage::getUserId, userId)
+                        .eq(BasicQuotaUsage::getQuotaType, quotaType)
+                        .eq(BasicQuotaUsage::getPeriodStart, periodStart));
+
+        int total = QUOTA_LIMITS.getOrDefault(quotaType, 0);
+        int used = 0;
+        if (usage != null) {
+            used = usage.getUsedCount() != null ? usage.getUsedCount() : 0;
+        }
+
+        QuotaStatusDTO dto = new QuotaStatusDTO();
+        dto.setQuotaType(quotaType);
+        dto.setTotal(total);
+        dto.setUsed(used);
+        dto.setRemaining(Math.max(0, total - used));
+        dto.setPeriodStart(periodStart);
+        dto.setPeriodEnd(periodEnd);
+        return dto;
+    }
+
+    /**
+     * 尝试扣减配额,返回是否成功
+     */
+    public QuotaConsumeResult consumeQuota(Long userId, String quotaType) {
+        QuotaConsumeResult result = new QuotaConsumeResult();
+
+        if (!isActiveBasicMember(userId)) {
+            result.setSuccess(false);
+            result.setToastMsg("该功能仅限体验会员使用");
+            return result;
+        }
+
+        Date now = new Date();
+        Date periodStart = getMonthStart(now);
+        Date periodEnd = getMonthEnd(now);
+
+        BasicQuotaUsage usage = basicQuotaUsageMapper.selectOne(
+                new LambdaQueryWrapper<BasicQuotaUsage>()
+                        .eq(BasicQuotaUsage::getUserId, userId)
+                        .eq(BasicQuotaUsage::getQuotaType, quotaType)
+                        .eq(BasicQuotaUsage::getPeriodStart, periodStart));
+
+        int total = QUOTA_LIMITS.getOrDefault(quotaType, 0);
+        int used = 0;
+        if (usage != null) {
+            used = usage.getUsedCount() != null ? usage.getUsedCount() : 0;
+        }
+
+        if (used >= total) {
+            result.setSuccess(false);
+            result.setRemaining(0);
+            result.setToastMsg(buildExhaustedToast(quotaType, total));
+            result.setActionUrl("/pages/profile/upgrade/index");
+            return result;
+        }
+
+        // 初始化或更新
+        if (usage == null) {
+            usage = new BasicQuotaUsage();
+            usage.setUserId(userId);
+            usage.setQuotaType(quotaType);
+            usage.setUsedCount(0);
+            usage.setTotalCount(total);
+            usage.setPeriodStart(periodStart);
+            usage.setPeriodEnd(periodEnd);
+            usage.setCreatedAt(now);
+            usage.setUpdatedAt(now);
+            basicQuotaUsageMapper.insert(usage);
+        }
+        usage.setUsedCount(used + 1);
+        usage.setUpdatedAt(now);
+        basicQuotaUsageMapper.updateById(usage);
+
+        result.setSuccess(true);
+        result.setRemaining(total - used - 1);
+        return result;
+    }
+
+    /**
+     * 批量刷新所有 BASIC 用户当月配额(每月1号调用)
+     */
+    public void refreshMonthlyQuota() {
+        Date now = new Date();
+        Date monthStart = getMonthStart(now);
+        Date monthEnd = getMonthEnd(now);
+
+        List<BasicQuotaUsage> lastMonthRecords = basicQuotaUsageMapper.selectList(
+                new LambdaQueryWrapper<BasicQuotaUsage>());
+
+        Set<Long> userIds = new HashSet<>();
+        for (BasicQuotaUsage rec : lastMonthRecords) {
+            if (isActiveBasicMember(rec.getUserId())) {
+                userIds.add(rec.getUserId());
+            }
+        }
+
+        for (Long userId : userIds) {
+            for (String quotaType : QUOTA_LIMITS.keySet()) {
+                try {
+                    BasicQuotaUsage existing = basicQuotaUsageMapper.selectOne(
+                            new LambdaQueryWrapper<BasicQuotaUsage>()
+                                    .eq(BasicQuotaUsage::getUserId, userId)
+                                    .eq(BasicQuotaUsage::getQuotaType, quotaType)
+                                    .eq(BasicQuotaUsage::getPeriodStart, monthStart));
+                    if (existing == null) {
+                        BasicQuotaUsage newUsage = new BasicQuotaUsage();
+                        newUsage.setUserId(userId);
+                        newUsage.setQuotaType(quotaType);
+                        newUsage.setUsedCount(0);
+                        newUsage.setTotalCount(QUOTA_LIMITS.get(quotaType));
+                        newUsage.setPeriodStart(monthStart);
+                        newUsage.setPeriodEnd(monthEnd);
+                        newUsage.setCreatedAt(now);
+                        newUsage.setUpdatedAt(now);
+                        basicQuotaUsageMapper.insert(newUsage);
+                        log.info("BASIC配额刷新初始化: userId={}, quotaType={}", userId, quotaType);
+                    } else {
+                        existing.setUsedCount(0);
+                        existing.setPeriodStart(monthStart);
+                        existing.setPeriodEnd(monthEnd);
+                        existing.setUpdatedAt(now);
+                        basicQuotaUsageMapper.updateById(existing);
+                    }
+                } catch (Exception e) {
+                    log.warn("BASIC配额刷新失败: userId={}, quotaType={}", userId, quotaType, e);
+                }
+            }
+        }
+        log.info("BASIC配额刷新完成, 处理用户数: {}", userIds.size());
+    }
+
+    /**
+     * 检查用户当前是否为有效的 BASIC 会员
+     */
+    public boolean isActiveBasicMember(Long userId) {
+        if (userId == null) {
+            return false;
+        }
+        String level = membershipService.getMemberLevel(userId);
+        if (!"BASIC".equals(level)) {
+            return false;
+        }
+        User user = userMapper.selectById(userId);
+        if (user == null || user.getMemberExpireTime() == null) {
+            return false;
+        }
+        return user.getMemberExpireTime().after(new Date());
+    }
+
+    /**
+     * 获取 BASIC 用户到期时间
+     */
+    public Date getExpireTime(Long userId) {
+        User user = userMapper.selectById(userId);
+        if (user != null) {
+            return user.getMemberExpireTime();
+        }
+        return null;
+    }
+
+    private Date getMonthStart(Date date) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(date);
+        cal.set(Calendar.DAY_OF_MONTH, 1);
+        cal.set(Calendar.HOUR_OF_DAY, 0);
+        cal.set(Calendar.MINUTE, 0);
+        cal.set(Calendar.SECOND, 0);
+        cal.set(Calendar.MILLISECOND, 0);
+        return cal.getTime();
+    }
+
+    private Date getMonthEnd(Date date) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(date);
+        cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
+        cal.set(Calendar.HOUR_OF_DAY, 23);
+        cal.set(Calendar.MINUTE, 59);
+        cal.set(Calendar.SECOND, 59);
+        cal.set(Calendar.MILLISECOND, 999);
+        return cal.getTime();
+    }
+
+    private String buildExhaustedToast(String quotaType, int total) {
+        switch (quotaType) {
+            case "AI_CHAT":
+                return "您本月 AI 对话已用完(" + total + "/" + total + "),升级 FAMILY 享不限次畅聊";
+            case "REPORT_READING":
+                return "本月报告解读额度已用完(" + total + "/" + total + "),升级 FAMILY 解锁不限次深度解读";
+            case "ASSESSMENT_QUOTA":
+                return "测评预约为 FAMILY 会员专属权益,升级后即可预约";
+            case "REPORT_UPLOAD":
+                return "本月报告上传额度已用完(" + total + "/" + total + "),升级 FAMILY 解锁更多";
+            default:
+                return "本月额度已用完,升级 FAMILY 解锁更多";
+        }
+    }
+}

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

@@ -52,6 +52,9 @@ public class DanReportUploadService {
     @Resource
     @Resource
     private DimensionScoreService dimensionScoreService;
     private DimensionScoreService dimensionScoreService;
 
 
+    @Resource
+    private BasicQuotaService basicQuotaService;
+
     @Resource
     @Resource
     private ReportBlockService reportBlockService;
     private ReportBlockService reportBlockService;
 
 
@@ -75,6 +78,14 @@ public class DanReportUploadService {
     // Phase 1: 上传并预览 PDF
     // Phase 1: 上传并预览 PDF
     public Result<Map<String, Object>> uploadAndParse(Long userId, Long memberId,
     public Result<Map<String, Object>> uploadAndParse(Long userId, Long memberId,
                                                        String dimension, MultipartFile file) {
                                                        String dimension, MultipartFile file) {
+        // BASIC 配额检查(报告上传)
+        if (userId != null) {
+            com.etotem.cfc.dto.QuotaConsumeResult quotaResult = basicQuotaService.consumeQuota(userId, "REPORT_UPLOAD");
+            if (!quotaResult.isSuccess()) {
+                return Result.error(403, quotaResult.getToastMsg());
+            }
+        }
+
         if (file == null || file.isEmpty()) {
         if (file == null || file.isEmpty()) {
             return Result.error("请选择文件");
             return Result.error("请选择文件");
         }
         }

+ 339 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/DoaService.java

@@ -0,0 +1,339 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.DoaGoal;
+import com.etotem.cfc.entity.DoaStage;
+import com.etotem.cfc.entity.DoaWeek;
+import com.etotem.cfc.entity.FamilyMember;
+import com.etotem.cfc.mapper.DoaGoalMapper;
+import com.etotem.cfc.mapper.DoaStageMapper;
+import com.etotem.cfc.mapper.DoaWeekMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
+import com.etotem.cfc.util.SortUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.*;
+
+@Slf4j
+@Service
+public class DoaService {
+
+    @Resource
+    private DoaStageMapper doaStageMapper;
+
+    @Resource
+    private DoaGoalMapper doaGoalMapper;
+
+    @Resource
+    private DoaWeekMapper doaWeekMapper;
+
+    @Resource
+    private FamilyMemberMapper familyMemberMapper;
+
+    private static final Set<String> VALID_DOMAINS = new HashSet<>(Arrays.asList(
+            "health", "wealth", "family", "social", "help"));
+
+    /** 周起始:周一 */
+    private static Date weekStart(Date date) {
+        Calendar cal = Calendar.getInstance();
+        cal.setFirstDayOfWeek(Calendar.MONDAY);
+        cal.setTime(date);
+        cal.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
+        cal.set(Calendar.HOUR_OF_DAY, 0);
+        cal.set(Calendar.MINUTE, 0);
+        cal.set(Calendar.SECOND, 0);
+        cal.set(Calendar.MILLISECOND, 0);
+        return cal.getTime();
+    }
+
+    private static Date weekEnd(Date weekStart) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(weekStart);
+        cal.add(Calendar.DAY_OF_YEAR, 6);
+        cal.set(Calendar.HOUR_OF_DAY, 23);
+        cal.set(Calendar.MINUTE, 59);
+        cal.set(Calendar.SECOND, 59);
+        cal.set(Calendar.MILLISECOND, 999);
+        return cal.getTime();
+    }
+
+    /**
+     * 创建阶段:校验归属 + 周期 + 目标数量,写 stage + goals,预生成 N 条 week。
+     *
+     * @param memberId 目标成员ID;null 表示当前登录成员自己
+     * @param creatorUserId 当前登录 userId
+     * @param familyId 当前家庭
+     */
+    @Transactional
+    public Result<Long> createStage(Long memberId, Long creatorUserId, Long familyId,
+                                    String title, Integer durationWeeks, List<Map<String, Object>> goals) {
+        if (creatorUserId == null || familyId == null) {
+            return Result.error("未登录或未加入家庭");
+        }
+        if (durationWeeks == null || (durationWeeks != 2 && durationWeeks != 4 && durationWeeks != 8 && durationWeeks != 12)) {
+            return Result.error("durationWeeks必须是2/4/8/12");
+        }
+        if (goals == null || goals.isEmpty() || goals.size() > 3) {
+            return Result.error("每阶段需设置1-3个目标");
+        }
+
+        // 目标成员:默认当前登录成员自己
+        if (memberId == null) {
+            memberId = resolveMemberIdByUserId(creatorUserId, familyId);
+            if (memberId == null) {
+                return Result.error("未找到当前成员身份");
+            }
+        }
+
+        // 校验目标成员属于当前家庭
+        FamilyMember targetMember = familyMemberMapper.selectById(memberId);
+        if (targetMember == null || !familyId.equals(targetMember.getFamilyId())) {
+            return Result.error("目标成员不属于当前家庭");
+        }
+
+        // 代管校验:memberId != 当前成员 时,目标成员必须是不能登录成员(userId 为空);
+        // selfMemberId 为 null(调用者在家庭下无成员身份)也视为非本人,不可代管可登录成员
+        Long selfMemberId = resolveMemberIdByUserId(creatorUserId, familyId);
+        boolean isSelf = selfMemberId != null && selfMemberId.equals(memberId);
+        if (!isSelf && targetMember.getUserId() != null) {
+            return Result.error("只能代管不能登录成员的目标");
+        }
+
+        // 校验目标领域与去重
+        Set<String> domains = new HashSet<>();
+        for (Map<String, Object> g : goals) {
+            String domain = g.get("domain") != null ? g.get("domain").toString() : "";
+            String content = g.get("content") != null ? g.get("content").toString().trim() : "";
+            if (!VALID_DOMAINS.contains(domain)) {
+                return Result.error("目标领域无效: " + domain);
+            }
+            if (content.isEmpty()) {
+                return Result.error("目标内容不能为空");
+            }
+            if (!domains.add(domain)) {
+                return Result.error("同一阶段目标领域不能重复: " + domain);
+            }
+        }
+
+        // 日期计算:start=今天,end=start + durationWeeks*7 - 1
+        Date startDate = new Date();
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(startDate);
+        cal.add(Calendar.DAY_OF_YEAR, durationWeeks * 7 - 1);
+        Date endDate = cal.getTime();
+
+        // 写 stage
+        DoaStage stage = new DoaStage();
+        stage.setFamilyId(familyId);
+        stage.setMemberId(memberId);
+        stage.setTitle(title);
+        stage.setStartDate(startDate);
+        stage.setEndDate(endDate);
+        stage.setDurationWeeks(durationWeeks);
+        stage.setStatus("active");
+        stage.setCreatedBy(creatorUserId);
+        stage.setCreatedAt(new Date());
+        doaStageMapper.insert(stage);
+
+        // 写 goals
+        int order = 0;
+        for (Map<String, Object> g : goals) {
+            DoaGoal goal = new DoaGoal();
+            goal.setStageId(stage.getId());
+            goal.setDomain(g.get("domain").toString());
+            goal.setContent(g.get("content").toString().trim());
+            goal.setStatus("active");
+            goal.setSortOrder(order++);
+            goal.setCreatedAt(new Date());
+            doaGoalMapper.insert(goal);
+        }
+
+        // 预生成 N 条 week(周一为一周起点,首周从 start 所在周的周一起算)
+        Date firstWeekStart = weekStart(stage.getStartDate());
+        for (int i = 1; i <= durationWeeks; i++) {
+            DoaWeek week = new DoaWeek();
+            week.setStageId(stage.getId());
+            week.setWeekNo(i);
+            Date ws = addDays(firstWeekStart, (i - 1) * 7);
+            week.setWeekStart(ws);
+            week.setWeekEnd(weekEnd(ws));
+            week.setStatus("pending");
+            week.setCreatedAt(new Date());
+            doaWeekMapper.insert(week);
+        }
+
+        log.info("DOA创建阶段: stageId={}, memberId={}, weeks={}, goals={}",
+                stage.getId(), memberId, durationWeeks, goals.size());
+        return Result.success(stage.getId());
+    }
+
+    /** 家庭 DOA 总览:各成员当前 active 阶段 */
+    public List<Map<String, Object>> getFamilyOverview(Long familyId) {
+        List<Map<String, Object>> result = new ArrayList<>();
+        if (familyId == null) return result;
+
+        LambdaQueryWrapper<FamilyMember> mw = new LambdaQueryWrapper<FamilyMember>()
+                .eq(FamilyMember::getFamilyId, familyId);
+        SortUtil.applySort(mw);
+        List<FamilyMember> members = familyMemberMapper.selectList(mw);
+
+        for (FamilyMember m : members) {
+            LambdaQueryWrapper<DoaStage> sw = new LambdaQueryWrapper<DoaStage>()
+                    .eq(DoaStage::getFamilyId, familyId)
+                    .eq(DoaStage::getMemberId, m.getId())
+                    .eq(DoaStage::getStatus, "active")
+                    .orderByDesc(DoaStage::getCreatedAt);
+            SortUtil.applySort(sw);
+            List<DoaStage> stages = doaStageMapper.selectList(sw);
+
+            Map<String, Object> memberMap = new HashMap<>();
+            memberMap.put("memberId", m.getId());
+            memberMap.put("nickname", m.getNickname());
+            memberMap.put("memberType", m.getUserId() == null ? "local" : "login");
+
+            if (!stages.isEmpty()) {
+                DoaStage current = stages.get(0);
+                current.setMemberUserId(m.getUserId());
+                fillStageDetail(current);
+                memberMap.put("currentStage", current);
+            }
+            result.add(memberMap);
+        }
+        return result;
+    }
+
+    /** 阶段详情(stage + goals + weeks) */
+    public Result<DoaStage> getStageDetail(Long stageId, Long familyId) {
+        if (stageId == null) return Result.error("stageId不能为空");
+        DoaStage stage = doaStageMapper.selectById(stageId);
+        if (stage == null) return Result.error("阶段不存在");
+        if (!familyId.equals(stage.getFamilyId())) return Result.error("无权查看该阶段");
+
+        fillStageDetail(stage);
+
+        // 成员昵称
+        FamilyMember m = familyMemberMapper.selectById(stage.getMemberId());
+        stage.setMemberNickname(m != null ? m.getNickname() : "成员" + stage.getMemberId());
+        stage.setMemberUserId(m != null ? m.getUserId() : null);
+        return Result.success(stage);
+    }
+
+    /** 填充 stage 的 goals + weeks(含复盘周数统计) */
+    private void fillStageDetail(DoaStage stage) {
+        LambdaQueryWrapper<DoaGoal> gw = new LambdaQueryWrapper<DoaGoal>()
+                .eq(DoaGoal::getStageId, stage.getId())
+                .orderByAsc(DoaGoal::getSortOrder);
+        SortUtil.applySort(gw);
+        stage.setGoals(doaGoalMapper.selectList(gw));
+
+        LambdaQueryWrapper<DoaWeek> ww = new LambdaQueryWrapper<DoaWeek>()
+                .eq(DoaWeek::getStageId, stage.getId())
+                .orderByAsc(DoaWeek::getWeekNo);
+        SortUtil.applySort(ww);
+        List<DoaWeek> weeks = doaWeekMapper.selectList(ww);
+        stage.setWeeks(weeks);
+        stage.setTotalWeeks(weeks.size());
+
+        int reviewed = 0;
+        for (DoaWeek w : weeks) {
+            if ("reviewed".equals(w.getStatus())) reviewed++;
+        }
+        stage.setReviewedWeeks(reviewed);
+    }
+
+    /** 取消阶段 */
+    @Transactional
+    public Result<Void> cancelStage(Long stageId, Long userId, Long familyId) {
+        DoaStage stage = doaStageMapper.selectById(stageId);
+        if (stage == null) return Result.error("阶段不存在");
+        if (!familyId.equals(stage.getFamilyId())) return Result.error("无权操作该阶段");
+        if (!"active".equals(stage.getStatus())) return Result.error("仅active阶段可取消");
+        // 权限:本人 或 代管不能登录成员
+        Long selfMemberId = resolveMemberIdByUserId(userId, familyId);
+        boolean isSelf = selfMemberId != null && selfMemberId.equals(stage.getMemberId());
+        FamilyMember target = familyMemberMapper.selectById(stage.getMemberId());
+        boolean canManageLocal = target != null && target.getUserId() == null;
+        if (!isSelf && !canManageLocal) {
+            return Result.error("无权取消该阶段");
+        }
+        stage.setStatus("cancelled");
+        doaStageMapper.updateById(stage);
+        return Result.success();
+    }
+
+    /** 提交周计划(A表) */
+    @Transactional
+    public Result<Void> submitWeekPlan(Long weekId, String plan, Long userId, Long familyId) {
+        DoaWeek week = doaWeekMapper.selectById(weekId);
+        if (week == null) return Result.error("周记录不存在");
+        DoaStage stage = doaStageMapper.selectById(week.getStageId());
+        if (stage == null || !familyId.equals(stage.getFamilyId())) return Result.error("无权操作");
+        if (!checkStageMemberPermission(stage, userId, familyId)) return Result.error("无权操作该阶段");
+        if ("reviewed".equals(week.getStatus())) return Result.error("已复盘不可修改计划");
+        week.setPlan(plan);
+        if ("pending".equals(week.getStatus())) week.setStatus("planned");
+        doaWeekMapper.updateById(week);
+        return Result.success();
+    }
+
+    /** 提交周复盘(B表五问) */
+    @Transactional
+    public Result<Void> submitWeekReview(Long weekId, Map<String, String> review, Long userId, Long familyId) {
+        DoaWeek week = doaWeekMapper.selectById(weekId);
+        if (week == null) return Result.error("周记录不存在");
+        DoaStage stage = doaStageMapper.selectById(week.getStageId());
+        if (stage == null || !familyId.equals(stage.getFamilyId())) return Result.error("无权操作");
+        if (!checkStageMemberPermission(stage, userId, familyId)) return Result.error("无权操作该阶段");
+        if ("reviewed".equals(week.getStatus())) return Result.error("本周已复盘,不可重复提交");
+        week.setAchieved(review.get("achieved"));
+        week.setExperience(review.get("experience"));
+        week.setWorked(review.get("worked"));
+        week.setImprove(review.get("improve"));
+        week.setNextChallenge(review.get("nextChallenge"));
+        week.setStatus("reviewed");
+        doaWeekMapper.updateById(week);
+        return Result.success();
+    }
+
+    /** 周记录列表(按阶段查,含周序号) */
+    public List<DoaWeek> listWeeks(Long stageId, Long familyId) {
+        DoaStage stage = doaStageMapper.selectById(stageId);
+        if (stage == null || !familyId.equals(stage.getFamilyId())) return new ArrayList<>();
+        LambdaQueryWrapper<DoaWeek> ww = new LambdaQueryWrapper<DoaWeek>()
+                .eq(DoaWeek::getStageId, stageId)
+                .orderByAsc(DoaWeek::getWeekNo);
+        SortUtil.applySort(ww);
+        return doaWeekMapper.selectList(ww);
+    }
+
+    /** 校验当前用户对该阶段的编辑权限(本人 或 代管不能登录成员) */
+    private boolean checkStageMemberPermission(DoaStage stage, Long userId, Long familyId) {
+        Long selfMemberId = resolveMemberIdByUserId(userId, familyId);
+        boolean isSelf = selfMemberId != null && selfMemberId.equals(stage.getMemberId());
+        FamilyMember target = familyMemberMapper.selectById(stage.getMemberId());
+        boolean canManageLocal = target != null && target.getUserId() == null;
+        return isSelf || canManageLocal;
+    }
+
+    /** userId → 家庭内成员ID */
+    private Long resolveMemberIdByUserId(Long userId, Long familyId) {
+        if (userId == null) return null;
+        LambdaQueryWrapper<FamilyMember> mw = new LambdaQueryWrapper<FamilyMember>()
+                .eq(FamilyMember::getUserId, userId)
+                .eq(FamilyMember::getFamilyId, familyId);
+        SortUtil.applySort(mw);
+        List<FamilyMember> members = familyMemberMapper.selectList(mw);
+        return members.isEmpty() ? null : members.get(0).getId();
+    }
+
+    private static Date addDays(Date date, int days) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(date);
+        cal.add(Calendar.DAY_OF_YEAR, days);
+        return cal.getTime();
+    }
+}

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

@@ -21,6 +21,7 @@ import java.util.Date;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
+import java.util.Collections;
 import java.util.Set;
 import java.util.Set;
 import java.util.UUID;
 import java.util.UUID;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
@@ -76,6 +77,9 @@ public class MembershipService implements MembershipServiceInterface {
     @Resource
     @Resource
     private PaymentService paymentService;
     private PaymentService paymentService;
 
 
+    @Resource
+    private ActivationQuotaService activationQuotaService;
+
     // ==================== 基础查询 ====================
     // ==================== 基础查询 ====================
 
 
     /**
     /**
@@ -239,6 +243,20 @@ public class MembershipService implements MembershipServiceInterface {
                     || "referral_commission".equals(feature);
                     || "referral_commission".equals(feature);
         }
         }
 
 
+        // BASIC 体验会员可用功能(9.9元/月,限次配额型)
+        if ("BASIC".equals(level)) {
+            return "free_activities".equals(feature)
+                    || "free_courses".equals(feature)
+                    || "discount_purchase".equals(feature)
+                    || "invite_family".equals(feature)
+                    || "mascot_ai_lite".equals(feature)
+                    || "ai_report_reading_lite".equals(feature)
+                    || "assessment_quota_lite".equals(feature)
+                    || "health_report_upload_lite".equals(feature)
+                    || "growth_plan_lite".equals(feature)
+                    || "task_template_lite".equals(feature);
+        }
+
         // FAMILY用户可用功能
         // FAMILY用户可用功能
         if ("FAMILY".equals(level)) {
         if ("FAMILY".equals(level)) {
             return "free_activities".equals(feature)
             return "free_activities".equals(feature)
@@ -815,6 +833,16 @@ public class MembershipService implements MembershipServiceInterface {
             Long buyerFamilyId = (buyer != null) ? buyer.getFamilyId() : null;
             Long buyerFamilyId = (buyer != null) ? buyer.getFamilyId() : null;
             cfCommissionService.settleReferrerOnly(order.getId(), "membership", adminUserId, buyerFamilyId, order.getAmount());
             cfCommissionService.settleReferrerOnly(order.getId(), "membership", adminUserId, buyerFamilyId, order.getAmount());
 
 
+            // 如果是 BASIC 等级,初始化月度配额
+            if ("BASIC".equals(order.getLevelCode())) {
+                try {
+                    activationQuotaService.activateBasicQuota(adminUserId, order.getFamilyId());
+                    log.info("BASIC配额初始化完成: userId={}, familyId={}", adminUserId, order.getFamilyId());
+                } catch (Exception e) {
+                    log.error("BASIC配额初始化失败: userId={}", adminUserId, e);
+                }
+            }
+
             log.info("会员开通完成: orderNo={}, levelCode={}, amount={}, fromLevel={}",
             log.info("会员开通完成: orderNo={}, levelCode={}, amount={}, fromLevel={}",
                     order.getOrderNo(), order.getLevelCode(), order.getAmount(), fromLevel);
                     order.getOrderNo(), order.getLevelCode(), order.getAmount(), fromLevel);
             return true;
             return true;
@@ -1116,7 +1144,7 @@ public class MembershipService implements MembershipServiceInterface {
             return;
             return;
         }
         }
 
 
-        if ("FAMILY".equals(user.getMemberLevel())
+        if (("FAMILY".equals(user.getMemberLevel()) || "BASIC".equals(user.getMemberLevel()))
                 && user.getMemberExpireTime() != null
                 && user.getMemberExpireTime() != null
                 && user.getMemberExpireTime().before(new Date())) {
                 && user.getMemberExpireTime().before(new Date())) {
             // 方案B:家庭成员共享会员,到期时全家降级
             // 方案B:家庭成员共享会员,到期时全家降级
@@ -1141,7 +1169,7 @@ public class MembershipService implements MembershipServiceInterface {
         }
         }
 
 
         String level = user.getMemberLevel() != null ? user.getMemberLevel() : "FREE";
         String level = user.getMemberLevel() != null ? user.getMemberLevel() : "FREE";
-        if ("FAMILY".equals(level) && user.getMemberExpireTime() != null) {
+        if (("FAMILY".equals(level) || "BASIC".equals(level)) && user.getMemberExpireTime() != null) {
             if (user.getMemberExpireTime().before(new Date())) {
             if (user.getMemberExpireTime().before(new Date())) {
                 return "FREE";
                 return "FREE";
             }
             }
@@ -1230,6 +1258,22 @@ public class MembershipService implements MembershipServiceInterface {
         return familyIds.stream().distinct().collect(Collectors.toList());
         return familyIds.stream().distinct().collect(Collectors.toList());
     }
     }
 
 
+    /**
+     * 查询当前有效的 BASIC 会员用户ID列表(到期提醒用)
+     */
+    public List<Long> listActiveBasicMemberUserIds() {
+        Date now = new Date();
+        List<User> basicUsers = userMapper.selectList(
+                new LambdaQueryWrapper<User>()
+                        .eq(User::getMemberLevel, "BASIC")
+                        .isNotNull(User::getMemberExpireTime)
+                        .gt(User::getMemberExpireTime, now));
+        return basicUsers != null ? basicUsers.stream()
+                .map(User::getId)
+                .distinct()
+                .collect(Collectors.toList()) : Collections.emptyList();
+    }
+
     /**
     /**
      * 应用会员折扣,返回折后价格(分)
      * 应用会员折扣,返回折后价格(分)
      */
      */

+ 180 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java

@@ -10,6 +10,8 @@ import com.etotem.cfc.dto.ProductOrderDTO;
 import com.etotem.cfc.entity.AfterSalesRequest;
 import com.etotem.cfc.entity.AfterSalesRequest;
 import com.etotem.cfc.entity.AssessmentProduct;
 import com.etotem.cfc.entity.AssessmentProduct;
 import com.etotem.cfc.entity.Consignee;
 import com.etotem.cfc.entity.Consignee;
+import com.etotem.cfc.entity.Coupon;
+import com.etotem.cfc.entity.FamilyCoupon;
 import com.etotem.cfc.entity.Product;
 import com.etotem.cfc.entity.Product;
 import com.etotem.cfc.entity.ProductBundleItem;
 import com.etotem.cfc.entity.ProductBundleItem;
 import com.etotem.cfc.entity.ProductGiftRule;
 import com.etotem.cfc.entity.ProductGiftRule;
@@ -26,6 +28,8 @@ import com.etotem.cfc.mapper.AfterSalesRequestMapper;
 import com.etotem.cfc.entity.SupplySystemMember;
 import com.etotem.cfc.entity.SupplySystemMember;
 import com.etotem.cfc.mapper.SupplySystemMemberMapper;
 import com.etotem.cfc.mapper.SupplySystemMemberMapper;
 import com.etotem.cfc.service.SupplyHierarchyService;
 import com.etotem.cfc.service.SupplyHierarchyService;
+import com.etotem.cfc.mapper.CouponMapper;
+import com.etotem.cfc.mapper.FamilyCouponMapper;
 import com.etotem.cfc.mapper.ProductMapper;
 import com.etotem.cfc.mapper.ProductMapper;
 import com.etotem.cfc.mapper.ProductOrderMapper;
 import com.etotem.cfc.mapper.ProductOrderMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.mapper.UserMapper;
@@ -163,6 +167,12 @@ public class ProductOrderService {
     @Resource
     @Resource
     private ProductGiftService productGiftService;
     private ProductGiftService productGiftService;
 
 
+    @Resource
+    private CouponMapper couponMapper;
+
+    @Resource
+    private FamilyCouponMapper familyCouponMapper;
+
     public Result<ProductOrderDTO> create(CreateProductOrderDTO dto, Long buyerId) {
     public Result<ProductOrderDTO> create(CreateProductOrderDTO dto, Long buyerId) {
         if (buyerId == null) {
         if (buyerId == null) {
             return Result.error("请先登录");
             return Result.error("请先登录");
@@ -1119,7 +1129,11 @@ public class ProductOrderService {
         if (!isBuyer && !isVendor) {
         if (!isBuyer && !isVendor) {
             return Result.error("无权查看");
             return Result.error("无权查看");
         }
         }
-        return Result.success(ProductOrderDTO.from(order));
+        ProductOrderDTO dto = ProductOrderDTO.from(order);
+        if (product != null) {
+            dto.setVendorId(product.getVendorId());
+        }
+        return Result.success(dto);
     }
     }
 
 
     public Result<List<ProductOrderDTO>> vendorOrders(Long vendorId, String status) {
     public Result<List<ProductOrderDTO>> vendorOrders(Long vendorId, String status) {
@@ -1325,4 +1339,169 @@ public class ProductOrderService {
         }
         }
         return code.toString();
         return code.toString();
     }
     }
+
+    /**
+     * 供应商审核修改订单(仅 pending 状态)
+     * 支持:修改单价/总价、修改配送方式、添加临时优惠券、添加赠品
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public Result<Map<String, Object>> reviewUpdate(Map<String, Object> params, Long userId) {
+        String orderNo = (String) params.get("orderNo");
+        if (orderNo == null || orderNo.isEmpty()) {
+            return Result.error("订单号不能为空");
+        }
+
+        ProductOrder order = orderMapper.selectOne(
+            new LambdaQueryWrapper<ProductOrder>().eq(ProductOrder::getOrderNo, orderNo));
+        if (order == null) {
+            return Result.error("订单不存在");
+        }
+        if (!"pending".equals(order.getStatus())) {
+            return Result.error("仅待支付订单可审核修改");
+        }
+
+        // 权限校验:供应商必须是该订单商品的 vendorId
+        Product product = productMapper.selectById(order.getProductId());
+        if (product == null || product.getVendorId() == null || !product.getVendorId().equals(userId)) {
+            return Result.error("无权操作此订单");
+        }
+
+        boolean hasChange = false;
+        StringBuilder changeLog = new StringBuilder();
+
+        // ===== 1. 修改价格 =====
+        Integer newUnitPrice = params.get("unitPrice") != null ? ((Number) params.get("unitPrice")).intValue() : null;
+        if (newUnitPrice != null && newUnitPrice >= 0) {
+        int oldTotal = order.getTotalAmount() != null ? order.getTotalAmount() : 0;
+            order.setUnitPrice(newUnitPrice);
+            int newTotal = newUnitPrice * (order.getQuantity() != null ? order.getQuantity() : 1);
+            order.setTotalAmount(newTotal);
+            int pointsCost = order.getPointsCost() != null ? order.getPointsCost() : 0;
+            int moneyAmount = newTotal - pointsCost - (order.getDiscountAmount() != null ? order.getDiscountAmount() : 0);
+            order.setMoneyAmount(Math.max(moneyAmount, 0));
+            hasChange = true;
+            if (oldTotal != newTotal) {
+                if (changeLog.length() > 0) changeLog.append(";");
+                changeLog.append(String.format("价格:%d元 → %d元", oldTotal / 100, newTotal / 100));
+            }
+        }
+
+        // ===== 2. 修改配送方式 =====
+        Integer newDeliveryMethod = params.get("deliveryMethod") != null ? ((Number) params.get("deliveryMethod")).intValue() : null;
+        if (newDeliveryMethod != null) {
+            Integer productMethod = product.getDeliveryMethod();
+            if (productMethod == null) productMethod = 1;
+            boolean allowed;
+            switch (productMethod) {
+                case 1: allowed = newDeliveryMethod == 1; break;
+                case 2: allowed = newDeliveryMethod == 2; break;
+                case 3: allowed = newDeliveryMethod == 1 || newDeliveryMethod == 2; break;
+                case 4: allowed = newDeliveryMethod == 4; break;
+                case 5: allowed = newDeliveryMethod == 5; break;
+                default: allowed = true; break;
+            }
+            if (!allowed) {
+                return Result.error("该商品不支持此配送方式");
+            }
+            if (newDeliveryMethod.equals(order.getDeliveryMethod())) {
+                // 未变更,跳过
+            } else {
+                int oldMethod = order.getDeliveryMethod() != null ? order.getDeliveryMethod() : 1;
+                order.setDeliveryMethod(newDeliveryMethod);
+                hasChange = true;
+                if (changeLog.length() > 0) changeLog.append(";");
+                changeLog.append(String.format("配送方式:%d → %d", oldMethod, newDeliveryMethod));
+            }
+        }
+
+        // ===== 3. 添加临时优惠券 =====
+        Integer couponValue = params.get("couponValue") != null ? ((Number) params.get("couponValue")).intValue() : null;
+        Long existingCouponId = order.getCouponId();
+        if (couponValue != null && couponValue > 0 && existingCouponId == null) {
+            Coupon tempCoupon = new Coupon();
+            tempCoupon.setName("供应商临时券-" + orderNo);
+            tempCoupon.setType("ONE_TIME");
+            tempCoupon.setValue(couponValue);
+            tempCoupon.setMinSpend(0);
+            tempCoupon.setDiscountRate(null);
+            tempCoupon.setProductId(order.getProductId());
+            tempCoupon.setStatus("ACTIVE");
+            tempCoupon.setValidFrom(new Date());
+            Calendar cal = Calendar.getInstance();
+            cal.setTime(order.getCancelAt() != null ? order.getCancelAt() : new Date());
+            cal.add(Calendar.DAY_OF_MONTH, 7);
+            tempCoupon.setValidUntil(cal.getTime());
+            couponMapper.insert(tempCoupon);
+            order.setCouponId(tempCoupon.getId());
+            order.setDiscountAmount(couponValue);
+            int pointsCost = order.getPointsCost() != null ? order.getPointsCost() : 0;
+            int moneyAmount = order.getTotalAmount() - pointsCost - couponValue;
+            order.setMoneyAmount(Math.max(moneyAmount, 0));
+            // Grant to buyer's family
+            if (order.getFamilyId() != null) {
+                FamilyCoupon fc = new FamilyCoupon();
+                fc.setFamilyId(order.getFamilyId());
+                fc.setCouponId(tempCoupon.getId());
+                fc.setStatus("AVAILABLE");
+                fc.setReceivedAt(new Date());
+                fc.setOrderId(order.getId());
+                familyCouponMapper.insert(fc);
+            }
+            hasChange = true;
+            if (changeLog.length() > 0) changeLog.append(";");
+            changeLog.append(String.format("新增优惠券:减免%d元", couponValue / 100));
+        }
+
+        // ===== 4. 添加赠品 =====
+        List<Map<String, Object>> giftItems = (List<Map<String, Object>>) params.get("giftItems");
+        if (giftItems != null && !giftItems.isEmpty()) {
+            for (Map<String, Object> gift : giftItems) {
+                Long giftProductId = gift.get("giftProductId") != null ? ((Number) gift.get("giftProductId")).longValue() : null;
+                String giftProductName = (String) gift.get("giftProductName");
+                String giftProductImage = (String) gift.get("giftProductImage");
+                Integer quantity = gift.get("quantity") != null ? ((Number) gift.get("quantity")).intValue() : 1;
+
+                ProductOrderGift orderGift = new ProductOrderGift();
+                orderGift.setOrderId(order.getId());
+                orderGift.setOrderNo(order.getOrderNo());
+                orderGift.setGiftType("product");
+                orderGift.setGiftProductId(giftProductId);
+                orderGift.setGiftProductName(giftProductName);
+                orderGift.setGiftProductImage(giftProductImage);
+                orderGift.setCfValue(0);
+                orderGift.setQuantity(quantity);
+                orderGift.setFulfilled(0);
+                orderGift.setCreatedAt(new Date());
+                productOrderGiftMapper.insert(orderGift);
+
+                hasChange = true;
+                if (changeLog.length() > 0) changeLog.append(";");
+                changeLog.append(String.format("新增赠品:%s x%d",
+                    giftProductName != null ? giftProductName : "赠品", quantity));
+            }
+        }
+
+        // ===== 5. 更新备注 =====
+        String remark = (String) params.get("remark");
+        if (remark != null && !remark.isEmpty()) {
+            if (order.getRemark() != null && !order.getRemark().isEmpty()) {
+                order.setRemark(order.getRemark() + " | " + remark);
+            } else {
+                order.setRemark(remark);
+            }
+        } else if (hasChange) {
+            order.setRemark("供应商审核修改: " + changeLog.toString());
+        }
+
+        // ===== 6. 更新订单时间 =====
+        order.setUpdatedAt(new Date());
+        orderMapper.updateById(order);
+
+        // ===== 7. 组装返回 =====
+        ProductOrderDTO dto = ProductOrderDTO.from(order);
+        Map<String, Object> result = new HashMap<>();
+        result.put("order", dto);
+        result.put("changeLog", changeLog.toString());
+        return Result.success(result);
+    }
 }
 }

+ 55 - 0
cfc-backend/src/main/java/com/etotem/cfc/task/BasicQuotaScheduledTasks.java

@@ -0,0 +1,55 @@
+package com.etotem.cfc.task;
+
+import com.etotem.cfc.service.BasicQuotaService;
+import com.etotem.cfc.service.MembershipService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+@Slf4j
+@Component
+public class BasicQuotaScheduledTasks {
+
+    @Resource
+    private BasicQuotaService basicQuotaService;
+
+    @Resource
+    private MembershipService membershipService;
+
+    /**
+     * 每月1号 00:00 刷新所有 BASIC 用户的月度配额
+     */
+    @Scheduled(cron = "0 0 0 1 * ?")
+    public void refreshMonthlyQuota() {
+        log.info("开始执行BASIC配额月度刷新定时任务");
+        try {
+            basicQuotaService.refreshMonthlyQuota();
+            log.info("BASIC配额月度刷新定时任务执行完成");
+        } catch (Exception e) {
+            log.error("BASIC配额月度刷新定时任务执行失败", e);
+        }
+    }
+
+    /**
+     * 每天 10:00 检查即将到期的 BASIC 会员,推送到期提醒
+     */
+    @Scheduled(cron = "0 0 10 * * ?")
+    public void sendExpiryReminders() {
+        log.info("开始执行BASIC会员到期提醒定时任务");
+        try {
+            List<Long> basicUserIds = membershipService.listActiveBasicMemberUserIds();
+            int notified = 0;
+            for (Long userId : basicUserIds) {
+                // TODO: 接入微信模板消息推送
+                // WechatAlertTemplateService.sendBasicExpiryReminder(userId, basicQuotaService)
+                notified++;
+            }
+            log.info("BASIC到期提醒完成, 处理用户数={}, 通知数={}", basicUserIds.size(), notified);
+        } catch (Exception e) {
+            log.error("BASIC到期提醒定时任务执行失败", e);
+        }
+    }
+}

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

@@ -3280,6 +3280,7 @@ CREATE TABLE IF NOT EXISTS exercise_record (
     calories_burned INT COMMENT '消耗卡路里',
     calories_burned INT COMMENT '消耗卡路里',
     heart_rate_avg INT COMMENT '平均心率',
     heart_rate_avg INT COMMENT '平均心率',
     heart_rate_max INT COMMENT '最大心率',
     heart_rate_max INT COMMENT '最大心率',
+    step_count INT COMMENT '微信运动步数',
     start_time DATETIME COMMENT '开始时间',
     start_time DATETIME COMMENT '开始时间',
     end_time DATETIME COMMENT '结束时间',
     end_time DATETIME COMMENT '结束时间',
     remark TEXT COMMENT '备注',
     remark TEXT COMMENT '备注',
@@ -5594,3 +5595,89 @@ CREATE TABLE IF NOT EXISTS pearl_interaction_reminder (
     INDEX idx_pir_user_status (user_id, status),
     INDEX idx_pir_user_status (user_id, status),
     INDEX idx_pir_resource (resource_item_id)
     INDEX idx_pir_resource (resource_item_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='珍珠图互动提醒';
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='珍珠图互动提醒';
+
+-- ============================================================
+-- BASIC 体验会员(迁移311-313)
+-- =====================================================
+
+INSERT IGNORE INTO membership_levels 
+(level_code, level_name, level_desc, price_monthly, features, max_children, max_tasks_per_day) 
+VALUES (
+    'BASIC', 
+    '体验会员', 
+    '9.9元/月体验版,解锁核心功能,限次使用AI与报告解读',
+    990,
+    '["free_activities","free_courses","discount_purchase","invite_family","mascot_ai_lite","ai_report_reading_lite","assessment_quota_lite","health_report_upload_lite","growth_plan_lite","task_template_lite"]',
+    3,
+    3
+);
+
+INSERT IGNORE INTO membership_benefits (level_code, benefit_code, benefit_name, benefit_desc, icon, bg_color, sort_order) VALUES
+('BASIC', 'AI_CHAT_LITE', 'AI健康顾问(限3次/月)', '每月3次AI健康顾问问答', '🤖', '#DBEAFE', 1),
+('BASIC', 'REPORT_READING_LITE', '报告解读(限1次/月)', '每月1次体检/测评报告AI解读', '📊', '#FCE7F3', 4),
+('BASIC', 'GROWTH_PLAN_LITE', '基础成长计划', '创建成长计划但模板有限', '📋', '#FEF3C7', 3),
+('BASIC', 'ASSESSMENT_QUOTA_LITE', '测评预约(限1次)', '每月可预约1次测评', '📝', '#E0E7FF', 8),
+('BASIC', 'REPORT_UPLOAD_LITE', '健康报告上传(限2份/月)', '每月可上传2份健康报告', '📄', '#FEF9C3', 9);
+
+CREATE TABLE IF NOT EXISTS basic_quota_usage (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    user_id BIGINT NOT NULL COMMENT '家长用户ID(非family_id)',
+    quota_type VARCHAR(30) NOT NULL COMMENT '配额类型: AI_CHAT/REPORT_READING/ASSESSMENT_QUOTA/REPORT_UPLOAD',
+    used_count INT DEFAULT 0 COMMENT '本月已用次数',
+    total_count INT NOT NULL COMMENT '月度总配额',
+    period_start DATE NOT NULL COMMENT '周期开始日期(每月1号)',
+    period_end DATE NOT NULL COMMENT '周期结束日期(月末)',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_user_type_period (user_id, quota_type, period_start),
+    INDEX idx_user_period (user_id, period_start)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='BASIC会员月度配额使用记录';
+
+-- DOA 个人目标-周计划-周复盘(迁移318,家庭打卡个人目标模块)
+-- ============================================================
+CREATE TABLE IF NOT EXISTS doa_stage (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    family_id BIGINT NOT NULL COMMENT '家庭ID',
+    member_id BIGINT NOT NULL COMMENT '目标归属成员ID(family_members.id)',
+    title VARCHAR(200) COMMENT '阶段主题(可选)',
+    start_date DATE NOT NULL COMMENT '阶段开始日期',
+    end_date DATE NOT NULL COMMENT '阶段结束日期',
+    duration_weeks TINYINT NOT NULL COMMENT '周期周数:2/4/8/12',
+    status VARCHAR(20) DEFAULT 'active' COMMENT 'active/completed/cancelled',
+    created_by BIGINT NOT NULL COMMENT '创建人userId(代管时为代管人)',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_ds_family_member (family_id, member_id),
+    INDEX idx_ds_status (status)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='DOA 阶段';
+
+CREATE TABLE IF NOT EXISTS doa_goal (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    stage_id BIGINT NOT NULL COMMENT '所属阶段ID',
+    domain VARCHAR(20) NOT NULL COMMENT '领域:health/wealth/family/social/help',
+    content VARCHAR(500) NOT NULL COMMENT '量化目标内容',
+    status VARCHAR(20) DEFAULT 'active' COMMENT 'active/achieved/abandoned',
+    sort_order TINYINT DEFAULT 0 COMMENT '排序',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_dg_stage (stage_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='DOA 阶段目标';
+
+CREATE TABLE IF NOT EXISTS doa_week (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    stage_id BIGINT NOT NULL COMMENT '所属阶段ID',
+    week_no TINYINT NOT NULL COMMENT '第几周(1起)',
+    week_start DATE NOT NULL COMMENT '本周开始(周一)',
+    week_end DATE NOT NULL COMMENT '本周结束(周日)',
+    plan TEXT COMMENT '周行动宣告计划(A表)',
+    achieved TEXT COMMENT '复盘:本周达成了什么',
+    experience TEXT COMMENT '复盘:有什么体验',
+    worked TEXT COMMENT '复盘:行得通的',
+    improve TEXT COMMENT '复盘:要提升/行不通的',
+    next_challenge TEXT COMMENT '复盘:下周挑战自己什么',
+    status VARCHAR(20) DEFAULT 'pending' COMMENT 'pending/planned/reviewed',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_dw_stage_week (stage_id, week_no),
+    INDEX idx_dw_stage (stage_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='DOA 周记录';

+ 53 - 0
cfc-frontend/components/QuotaProgressBar.vue

@@ -0,0 +1,53 @@
+<template>
+  <view class="quota-progress-bar">
+    <view class="quota-label">
+      <text class="quota-name">{{ name }}</text>
+      <text class="quota-count">{{ remaining }} / {{ total }}</text>
+    </view>
+    <view class="quota-track">
+      <view class="quota-fill" :style="{ width: percent + '%' }"></view>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  name: 'QuotaProgressBar',
+  props: {
+    remaining: { type: Number, default: 0 },
+    total: { type: Number, default: 0 },
+    name: { type: String, default: '' }
+  },
+  computed: {
+    percent() {
+      if (!this.total) return 0
+      return Math.round((this.remaining / this.total) * 100)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.quota-progress-bar {
+  margin-bottom: 16rpx;
+}
+.quota-label {
+  display: flex;
+  justify-content: space-between;
+  font-size: 24rpx;
+  color: #666;
+  margin-bottom: 8rpx;
+}
+.quota-track {
+  height: 12rpx;
+  background: #f0f0f0;
+  border-radius: 6rpx;
+  overflow: hidden;
+}
+.quota-fill {
+  height: 100%;
+  background: #F97316;
+  border-radius: 6rpx;
+  transition: width 0.3s;
+}
+</style>

+ 130 - 0
cfc-frontend/components/UpgradeGuideModal.vue

@@ -0,0 +1,130 @@
+<template>
+  <view class="upgrade-guide-mask" v-if="visible" @click="handleMaskClick">
+    <view class="upgrade-guide-content" :class="{ 'hard-modal': type === 'hard' }" @click.stop>
+      <view class="upgrade-guide-icon">{{ icon }}</view>
+      <view class="upgrade-guide-title">{{ title }}</view>
+      <view class="upgrade-guide-desc">{{ desc }}</view>
+      <view class="upgrade-guide-actions">
+        <view v-if="type === 'soft'" class="btn-know" @click="handleKnow">知道了</view>
+        <view v-if="type === 'soft'" class="btn-learn" @click="handleLearn">了解详情</view>
+        <view v-if="type === 'hard'" class="btn-upgrade" @click="handleUpgrade">升级 FAMILY</view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  name: 'UpgradeGuideModal',
+  data() {
+    return { visible: false }
+  },
+  props: {
+    type: { type: String, default: 'soft' },
+    quotaType: { type: String, default: '' },
+    remaining: { type: Number, default: 0 },
+    total: { type: Number, default: 0 }
+  },
+  computed: {
+    icon() {
+      return this.type === 'hard' ? '🔒' : 'ℹ️'
+    },
+    title() {
+      return this.type === 'hard' ? '升级后即可使用' : '本月额度已用完'
+    },
+    desc() {
+      if (this.type === 'hard') return '该功能为 FAMILY 会员专属权益,升级后即可使用'
+      if (this.quotaType === 'AI_CHAT') return '您本月 AI 对话已用完(' + this.total + '/' + this.total + '),升级 FAMILY 享不限次畅聊'
+      if (this.quotaType === 'REPORT_READING') return '本月报告解读额度已用完,升级 FAMILY 解锁不限次深度解读'
+      if (this.quotaType === 'REPORT_UPLOAD') return '本月报告上传额度已用完(' + this.total + '/' + this.total + '),升级 FAMILY 解锁更多'
+      return '本月额度已用完,升级 FAMILY 解锁更多权益'
+    }
+  },
+  methods: {
+    show() {
+      this.visible = true
+    },
+    hide() {
+      this.visible = false
+    },
+    handleKnow() {
+      this.hide()
+      this.$emit('know')
+    },
+    handleLearn() {
+      this.hide()
+      uni.navigateTo({ url: '/pages/membership/benefits' })
+      this.$emit('learn')
+    },
+    handleUpgrade() {
+      this.hide()
+      uni.navigateTo({ url: '/pages/membership/upgrade' })
+      this.$emit('upgrade')
+    },
+    handleMaskClick() {
+      if (this.type === 'soft') {
+        this.hide()
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.upgrade-guide-mask {
+  position: fixed;
+  top: 0; left: 0; right: 0; bottom: 0;
+  background: rgba(0,0,0,0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 999;
+}
+.upgrade-guide-content {
+  width: 600rpx;
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 48rpx 36rpx 36rpx;
+  text-align: center;
+}
+.upgrade-guide-icon {
+  font-size: 72rpx;
+  margin-bottom: 24rpx;
+}
+.upgrade-guide-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 16rpx;
+}
+.upgrade-guide-desc {
+  font-size: 26rpx;
+  color: #666;
+  line-height: 1.6;
+  margin-bottom: 36rpx;
+}
+.upgrade-guide-actions {
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+.btn-know, .btn-learn, .btn-upgrade {
+  height: 80rpx;
+  line-height: 80rpx;
+  border-radius: 40rpx;
+  font-size: 28rpx;
+}
+.btn-know {
+  background: #f5f5f5;
+  color: #666;
+}
+.btn-learn {
+  background: transparent;
+  color: #F97316;
+  border: 2rpx solid #F97316;
+}
+.btn-upgrade {
+  background: #F97316;
+  color: #fff;
+}
+</style>

+ 30 - 0
cfc-frontend/pages.json

@@ -634,6 +634,12 @@
             "navigationBarTitleText": "订单管理"
             "navigationBarTitleText": "订单管理"
           }
           }
         },
         },
+        {
+          "path": "order-review/order-review",
+          "style": {
+            "navigationBarTitleText": "审核订单"
+          }
+        },
         {
         {
           "path": "product-edit/product-edit",
           "path": "product-edit/product-edit",
           "style": {
           "style": {
@@ -952,6 +958,30 @@
             "navigationBarTitleText": "家庭挑战"
             "navigationBarTitleText": "家庭挑战"
           }
           }
         },
         },
+        {
+          "path": "doa-index",
+          "style": {
+            "navigationBarTitleText": "个人目标"
+          }
+        },
+        {
+          "path": "doa-stage-create",
+          "style": {
+            "navigationBarTitleText": "创建阶段"
+          }
+        },
+        {
+          "path": "doa-stage-detail",
+          "style": {
+            "navigationBarTitleText": "阶段详情"
+          }
+        },
+        {
+          "path": "doa-week-edit",
+          "style": {
+            "navigationBarTitleText": "周计划与复盘"
+          }
+        },
         {
         {
           "path": "water-index",
           "path": "water-index",
           "style": {
           "style": {

+ 33 - 3
cfc-frontend/pages/ai/chat.vue

@@ -152,17 +152,25 @@
      </view>
      </view>
      <view class="chat-disclaimer">本服务为生成内容,结果仅供参考</view>
      <view class="chat-disclaimer">本服务为生成内容,结果仅供参考</view>
   </view>
   </view>
+  <!-- 配额进度条 -->
+  <view class="quota-bar-wrapper" v-if="aiChatQuota">
+    <quota-progress-bar :remaining="aiChatQuota.remaining || 0" :total="aiChatQuota.total || 3" name="本月AI对话剩余"></quota-progress-bar>
+  </view>
+  <!-- 升级引导弹窗 -->
+  <upgrade-guide-modal ref="upgradeGuideModal"></upgrade-guide-modal>
 </template>
 </template>
 
 
 <script>
 <script>
-import { aiSendMessage, aiGetConversations, aiGetMessages, aiDeleteConversation, aiSendNutritionMessage, aiSendHealthCoachMessage, aiSendHealthButlerMessage, getMyMembership } from '../../utils/api.js'
+import { aiSendMessage, aiGetConversations, aiGetMessages, aiDeleteConversation, aiSendNutritionMessage, aiSendHealthCoachMessage, aiSendHealthButlerMessage, getMyMembership, getAllQuotaStatus } from '../../utils/api.js'
 import { sttTranscribe } from '../../utils/api.js'
 import { sttTranscribe } from '../../utils/api.js'
 import BaseEmpty from '../../components/BaseEmpty.vue'
 import BaseEmpty from '../../components/BaseEmpty.vue'
 import TaskCard from '../../components/AITaskCard.vue'
 import TaskCard from '../../components/AITaskCard.vue'
+import QuotaProgressBar from '../../components/QuotaProgressBar.vue'
+import UpgradeGuideModal from '../../components/UpgradeGuideModal.vue'
 import config from '@/config.js'
 import config from '@/config.js'
 
 
 export default {
 export default {
-  components: { BaseEmpty, TaskCard },
+  components: { BaseEmpty, TaskCard, QuotaProgressBar, UpgradeGuideModal },
   data() {
   data() {
     return {
     return {
       inputText: '',
       inputText: '',
@@ -182,7 +190,8 @@ export default {
       mascotName: '',
       mascotName: '',
       mascotIcon: '',
       mascotIcon: '',
       isRecording: false,
       isRecording: false,
-      recorderManager: null
+      recorderManager: null,
+      aiChatQuota: null
     }
     }
   },
   },
   computed: {
   computed: {
@@ -268,6 +277,13 @@ export default {
         uni.navigateTo({ url: '/pages/membership/upgrade' })
         uni.navigateTo({ url: '/pages/membership/upgrade' })
       }, 1500)
       }, 1500)
     })
     })
+    // 加载 AI 配额(BASIC用户可见)
+    var selfQuota = this
+    getAllQuotaStatus().then(function(res) {
+      if (res.data && res.data.AI_CHAT) {
+        selfQuota.aiChatQuota = res.data.AI_CHAT
+      }
+    }).catch(function() {})
   },
   },
   onShow() {
   onShow() {
     if (this.pageLoaded) {
     if (this.pageLoaded) {
@@ -354,6 +370,15 @@ export default {
     const text = this.inputText.trim()
     const text = this.inputText.trim()
     if (!text || this.sending) return
     if (!text || this.sending) return
 
 
+    // BASIC 配额检查
+    if (this.aiChatQuota && this.aiChatQuota.remaining <= 0) {
+      this.$refs.upgradeGuideModal.type = 'soft'
+      this.$refs.upgradeGuideModal.quotaType = 'AI_CHAT'
+      this.$refs.upgradeGuideModal.total = this.aiChatQuota.total
+      this.$refs.upgradeGuideModal.show()
+      return
+    }
+
     // 立即显示用户消息
     // 立即显示用户消息
     const tempMsg = { id: 'temp_' + Date.now(), query: text, answer: '' }
     const tempMsg = { id: 'temp_' + Date.now(), query: text, answer: '' }
     this.messages.push(tempMsg)
     this.messages.push(tempMsg)
@@ -942,4 +967,9 @@ export default {
   padding: 16rpx 0 20rpx;
   padding: 16rpx 0 20rpx;
   background: #f5f5f5;
   background: #f5f5f5;
 }
 }
+.quota-bar-wrapper {
+  padding: 16rpx 30rpx;
+  background: #fff;
+  border-top: 1rpx solid #f5f5f5;
+}
 </style>
 </style>

+ 33 - 0
cfc-frontend/pages/health/challenge-manage.vue

@@ -2,6 +2,12 @@
   <view class="page">
   <view class="page">
     <BaseLoading :loading="loading" text="加载中..." />
     <BaseLoading :loading="loading" text="加载中..." />
 
 
+    <!-- 顶部双 Tab:共同挑战 / 个人目标 -->
+    <view class="doa-tabs">
+      <view class="doa-tab active-tab" @click="goToDoa">🎯 共同挑战</view>
+      <view class="doa-tab" @click="goToDoaIndex">📋 个人目标</view>
+    </view>
+
     <view v-if="!loading" class="content">
     <view v-if="!loading" class="content">
       <!-- 首页挑战状态概览 -->
       <!-- 首页挑战状态概览 -->
       <view class="overview-card" v-if="activeChallenges.length > 0">
       <view class="overview-card" v-if="activeChallenges.length > 0">
@@ -262,6 +268,12 @@ export default {
     }
     }
   },
   },
   methods: {
   methods: {
+    goToDoa() {
+      // 当前即共同挑战页
+    },
+    goToDoaIndex() {
+      uni.navigateTo({ url: '/pages/health/doa-index' })
+    },
     async loadData() {
     async loadData() {
       this.loading = true
       this.loading = true
       try {
       try {
@@ -468,6 +480,27 @@ export default {
 </script>
 </script>
 
 
 <style scoped>
 <style scoped>
+.doa-tabs {
+  display: flex;
+  background: #fff;
+  border-radius: 16rpx;
+  margin-bottom: 20rpx;
+  overflow: hidden;
+  border: 2rpx solid #E2E8F0;
+}
+.doa-tab {
+  flex: 1;
+  text-align: center;
+  padding: 20rpx 0;
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #64748B;
+  background: #fff;
+}
+.active-tab {
+  background: #10B981;
+  color: #fff;
+}
 .page {
 .page {
   min-height: 100vh;
   min-height: 100vh;
   background: #F5F7FA;
   background: #F5F7FA;

+ 160 - 0
cfc-frontend/pages/health/doa-index.vue

@@ -0,0 +1,160 @@
+<template>
+  <view class="page">
+    <view v-if="loading" class="loading-box">加载中...</view>
+
+    <view v-else class="content">
+      <!-- 创建按钮 -->
+      <view class="action-bar">
+        <button class="btn-new" @click="goCreate">+ 创建阶段</button>
+      </view>
+
+      <!-- 家庭成员 DOA 卡片 -->
+      <view v-for="m in members" :key="getMemberKey(m)" class="member-card">
+        <view class="member-header">
+          <text class="member-name">{{ m.nickname }}</text>
+          <text v-if="m.memberType === 'local'" class="member-tag">未登录成员</text>
+          <text class="member-status">{{ m.currentStage ? '进行中' : '未开始' }}</text>
+        </view>
+
+        <!-- 无阶段 -->
+        <view v-if="!m.currentStage" class="member-empty">
+          <text class="empty-hint">还没有阶段目标</text>
+          <text v-if="canManage(m)" class="empty-link" @click="goCreateFor(m.memberId)">为他创建</text>
+        </view>
+
+        <!-- 有阶段 -->
+        <view v-else class="stage-box" @click="goDetail(m.currentStage.id)">
+          <text class="stage-title">{{ m.currentStage.title || '阶段目标' }}</text>
+          <text class="stage-dates">{{ formatDate(m.currentStage.startDate) }} ~ {{ formatDate(m.currentStage.endDate) }}</text>
+
+          <!-- 目标领域标签 -->
+          <view class="goal-row">
+            <text v-for="g in m.currentStage.goals" :key="g.id" class="goal-tag" :class="'domain-' + g.domain">
+              {{ domainName(g.domain) }} · {{ g.content }}
+            </text>
+          </view>
+
+          <!-- 周进度 -->
+          <view class="week-progress">
+            <text class="week-text">复盘 {{ m.currentStage.reviewedWeeks || 0 }}/{{ m.currentStage.totalWeeks || 0 }} 周</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 空状态 -->
+      <view v-if="members.length === 0" class="empty">
+        <text class="empty-icon">🎯</text>
+        <text class="empty-desc">创建你的第一个阶段目标,开启 DOA 之旅!</text>
+        <button class="btn-empty" @click="goCreate">立即创建</button>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getDoaOverview } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      loading: true,
+      members: [],
+      selfMemberId: 0
+    }
+  },
+  onLoad() {
+    this.selfMemberId = uni.getStorageSync('currentMemberId') || 0
+    this.loadData()
+    this._onLoadFired = true
+  },
+  onShow() {
+    if (this._onLoadFired) {
+      this._onLoadFired = false
+    } else {
+      this.loadData()
+    }
+  },
+  methods: {
+    getMemberKey(m) {
+      return m.memberId
+    },
+    async loadData() {
+      this.loading = true
+      try {
+        var res = await getDoaOverview()
+        this.members = res.data || []
+      } catch (e) {
+        this.members = []
+      } finally {
+        this.loading = false
+      }
+    },
+    domainName(d) {
+      var map = { health: '健康', wealth: '事业', family: '家庭关系', social: '社会关系', help: '帮助他人' }
+      return map[d] || d
+    },
+    formatDate(dateStr) {
+      if (!dateStr) return ''
+      return String(dateStr).slice(0, 10)
+    },
+    canManage(m) {
+      // 自己 或 未登录成员 → 可代管
+      return m.memberId === this.selfMemberId || m.memberType === 'local'
+    },
+    goCreate() {
+      uni.navigateTo({ url: '/pages/health/doa-stage-create' })
+    },
+    goCreateFor(memberId) {
+      uni.navigateTo({ url: '/pages/health/doa-stage-create?memberId=' + memberId })
+    },
+    goDetail(stageId) {
+      uni.navigateTo({ url: '/pages/health/doa-stage-detail?stageId=' + stageId })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page { min-height: 100vh; background: #F5F7FA; padding: 24rpx; }
+.loading-box { text-align: center; padding: 100rpx 0; color: #94A3B8; font-size: 28rpx; }
+.action-bar { display: flex; justify-content: flex-end; margin-bottom: 20rpx; }
+.btn-new {
+  background: linear-gradient(135deg, #10B981, #34D399);
+  color: #fff; font-size: 26rpx; font-weight: 600;
+  padding: 14rpx 32rpx; border-radius: 40rpx; border: none;
+}
+.member-card {
+  background: #fff; border-radius: 16rpx; padding: 24rpx;
+  margin-bottom: 16rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+}
+.member-header { display: flex; align-items: center; margin-bottom: 12rpx; }
+.member-name { font-size: 30rpx; font-weight: 700; color: #1E293B; flex: 1; }
+.member-tag { font-size: 20rpx; color: #B45309; background: #FEF3C7; padding: 4rpx 12rpx; border-radius: 20rpx; margin-right: 12rpx; }
+.member-status { font-size: 22rpx; color: #16A34A; }
+.member-empty { padding: 20rpx 0; text-align: center; }
+.empty-hint { font-size: 24rpx; color: #94A3B8; display: block; }
+.empty-link { font-size: 24rpx; color: #10B981; margin-top: 8rpx; display: inline-block; }
+.stage-box { background: #F8FAFC; border-radius: 12rpx; padding: 20rpx; }
+.stage-title { font-size: 28rpx; font-weight: 700; color: #1E293B; display: block; }
+.stage-dates { font-size: 22rpx; color: #64748B; margin-top: 4rpx; display: block; }
+.goal-row { margin-top: 12rpx; }
+.goal-tag {
+  font-size: 22rpx; color: #475569; background: #fff;
+  padding: 8rpx 16rpx; border-radius: 10rpx; margin: 6rpx 8rpx 0 0;
+  display: inline-block; border: 2rpx solid #E2E8F0;
+}
+.domain-health { border-color: #FF8C42 !important; color: #C2410C !important; }
+.domain-wealth { border-color: #F59E0B !important; color: #B45309 !important; }
+.domain-family { border-color: #10B981 !important; color: #047857 !important; }
+.domain-social { border-color: #6366F1 !important; color: #4338CA !important; }
+.domain-help { border-color: #FF6B9D !important; color: #BE185D !important; }
+.week-progress { margin-top: 12rpx; }
+.week-text { font-size: 22rpx; color: #10B981; font-weight: 600; }
+.empty { display: flex; flex-direction: column; align-items: center; padding: 100rpx 0; }
+.empty-icon { font-size: 80rpx; margin-bottom: 16rpx; }
+.empty-desc { font-size: 26rpx; color: #94A3B8; margin-bottom: 32rpx; }
+.btn-empty {
+  background: #10B981; color: #fff; font-size: 28rpx;
+  font-weight: 600; padding: 16rpx 48rpx; border-radius: 40rpx; border: none;
+}
+</style>

+ 223 - 0
cfc-frontend/pages/health/doa-stage-create.vue

@@ -0,0 +1,223 @@
+<template>
+  <view class="page">
+    <!-- 目标成员(代管时显示,仅未登录成员可选) -->
+    <view class="field" v-if="!isSelfOnly">
+      <text class="field-label">目标成员</text>
+      <view class="member-grid">
+        <view
+          v-for="m in members"
+          :key="m.id"
+          class="member-item"
+          :class="{ active: form.memberId === m.id }"
+          @click="selectMember(m)"
+        >
+          <text>{{ m.nickname }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 阶段主题 -->
+    <view class="field">
+      <text class="field-label">阶段主题(可选)</text>
+      <input class="field-input" v-model="form.title" placeholder="如:暑期习惯养成" maxlength="50" />
+    </view>
+
+    <!-- 周期选择 -->
+    <view class="field">
+      <text class="field-label">阶段周期 <text class="required">*</text></text>
+      <view class="radio-group">
+        <view class="radio-btn" :class="{ active: form.durationWeeks === 2 }" @click="form.durationWeeks = 2">2周</view>
+        <view class="radio-btn" :class="{ active: form.durationWeeks === 4 }" @click="form.durationWeeks = 4">4周</view>
+        <view class="radio-btn" :class="{ active: form.durationWeeks === 8 }" @click="form.durationWeeks = 8">8周</view>
+        <view class="radio-btn" :class="{ active: form.durationWeeks === 12 }" @click="form.durationWeeks = 12">12周</view>
+      </view>
+    </view>
+
+    <!-- 目标列表(1-3条) -->
+    <view class="field">
+      <text class="field-label">阶段目标(1-3 个,不同领域)</text>
+      <view v-for="(goal, idx) in form.goals" :key="getGoalKey(goal, idx)" class="goal-editor">
+        <view class="goal-domain-row">
+          <text class="goal-label">目标 {{ idx + 1 }} 领域</text>
+          <text class="goal-remove" @click="removeGoal(idx)">✕</text>
+        </view>
+        <view class="radio-group">
+          <view
+            v-for="d in domains"
+            :key="d.value"
+            class="radio-btn small"
+            :class="{ active: goal.domain === d.value }"
+            @click="goal.domain = d.value"
+          >{{ d.label }}</view>
+        </view>
+        <input class="field-input" v-model="goal.content" placeholder="量化目标,如:体重降到 172 以下" maxlength="100" />
+      </view>
+      <view class="add-goal" v-if="form.goals.length < 3" @click="addGoal">+ 添加目标</view>
+    </view>
+
+    <button class="btn-submit" @click="submit">创建阶段</button>
+  </view>
+</template>
+
+<script>
+import { createDoaStage, getFamilyMemberList } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      members: [],
+      selfMemberId: 0,
+      targetMemberId: 0,
+      hasLocalMembers: false,
+      domains: [
+        { value: 'health', label: '健康' },
+        { value: 'wealth', label: '事业' },
+        { value: 'family', label: '家庭关系' },
+        { value: 'social', label: '社会关系' },
+        { value: 'help', label: '帮助他人' }
+      ],
+      form: {
+        memberId: 0,
+        title: '',
+        durationWeeks: 4,
+        goals: [{ domain: 'health', content: '' }]
+      }
+    }
+  },
+  computed: {
+    isSelfOnly() {
+      // 只为自己创建时(未传入 memberId 且无可代管成员)
+      return this.targetMemberId === 0 && !this.hasLocalMembers
+    }
+  },
+  onLoad(options) {
+    this.selfMemberId = uni.getStorageSync('currentMemberId') || 0
+    if (options.memberId) {
+      this.targetMemberId = parseInt(options.memberId, 10)
+      this.form.memberId = parseInt(options.memberId, 10)
+    }
+    this.loadMembers()
+  },
+  methods: {
+    getGoalKey(goal, idx) {
+      return goal.domain + '_' + idx
+    },
+    async loadMembers() {
+      try {
+        var res = await getFamilyMemberList({ visibleOnly: true })
+        var list = res.data || []
+        this.members = list
+        this.hasLocalMembers = list.some(function(m) { return !m.userId })
+        // 若从总览"为他创建"进入且目标成员未登录 → 直接锁定
+        if (this.targetMemberId > 0) {
+          this.form.memberId = this.targetMemberId
+        }
+      } catch (e) {
+        this.members = []
+        this.hasLocalMembers = false
+      }
+    },
+    selectMember(m) {
+      this.form.memberId = m.id
+    },
+    addGoal() {
+      if (this.form.goals.length >= 3) return
+      this.form.goals.push({ domain: 'health', content: '' })
+    },
+    removeGoal(idx) {
+      if (this.form.goals.length <= 1) return
+      this.form.goals.splice(idx, 1)
+    },
+    async submit() {
+      // 目标成员校验
+      var memberId = this.form.memberId || this.selfMemberId
+      // 目标校验
+      var validGoals = []
+      var usedDomains = {}
+      var hasError = false
+      for (var i = 0; i < this.form.goals.length; i++) {
+        var g = this.form.goals[i]
+        if (!g.content.trim()) {
+          uni.showToast({ title: '请填写目标 ' + (i + 1) + ' 的内容', icon: 'none' })
+          hasError = true
+          break
+        }
+        if (usedDomains[g.domain]) {
+          uni.showToast({ title: '目标领域不能重复', icon: 'none' })
+          hasError = true
+          break
+        }
+        usedDomains[g.domain] = true
+        validGoals.push({ domain: g.domain, content: g.content.trim() })
+      }
+      if (hasError) return
+      if (validGoals.length === 0) {
+        uni.showToast({ title: '请至少填写1个目标', icon: 'none' })
+        return
+      }
+
+      var data = {
+        title: this.form.title.trim(),
+        durationWeeks: this.form.durationWeeks,
+        goals: validGoals
+      }
+      if (memberId && memberId !== this.selfMemberId) {
+        data.memberId = memberId
+      }
+
+      try {
+        var res = await createDoaStage(data)
+        if (res.code === 200) {
+          uni.showToast({ title: '创建成功', icon: 'success' })
+          setTimeout(function() {
+            uni.navigateBack()
+          }, 600)
+        } else {
+          uni.showToast({ title: res.message || '创建失败', icon: 'none' })
+        }
+      } catch (e) {
+        uni.showToast({ title: '创建失败', icon: 'none' })
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page { min-height: 100vh; background: #F5F7FA; padding: 24rpx; }
+.field { background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; }
+.field-label { font-size: 26rpx; color: #475569; font-weight: 600; margin-bottom: 12rpx; display: block; }
+.required { color: #EF4444; }
+.field-input {
+  background: #F8FAFC; border: 1rpx solid #E2E8F0; border-radius: 12rpx;
+  padding: 16rpx 18rpx; font-size: 28rpx; color: #1E293B; margin-top: 12rpx;
+}
+.member-grid { display: flex; flex-wrap: wrap; gap: 12rpx; }
+.member-item {
+  padding: 10rpx 22rpx; border-radius: 24rpx; font-size: 24rpx;
+  color: #475569; background: #F1F5F9; border: 2rpx solid transparent;
+}
+.member-item.active { background: #DCFCE7; color: #16A34A; border-color: #10B981; }
+.radio-group { display: flex; gap: 12rpx; margin-top: 12rpx; flex-wrap: wrap; }
+.radio-btn {
+  flex: 1; text-align: center; padding: 12rpx 0; border-radius: 12rpx;
+  font-size: 26rpx; color: #475569; background: #F1F5F9;
+}
+.radio-btn.small { flex: none; padding: 8rpx 18rpx; font-size: 22rpx; }
+.radio-btn.active { background: #10B981; color: #fff; font-weight: 600; }
+.goal-editor { background: #F8FAFC; border-radius: 12rpx; padding: 16rpx; margin-top: 12rpx; }
+.goal-domain-row { display: flex; justify-content: space-between; align-items: center; }
+.goal-label { font-size: 24rpx; color: #475569; font-weight: 600; }
+.goal-remove { font-size: 28rpx; color: #EF4444; padding: 4rpx 12rpx; }
+.add-goal {
+  margin-top: 16rpx; text-align: center; padding: 16rpx 0;
+  border: 2rpx dashed #10B981; border-radius: 12rpx;
+  color: #10B981; font-size: 26rpx; font-weight: 600;
+}
+.btn-submit {
+  background: linear-gradient(135deg, #10B981, #34D399);
+  color: #fff; font-size: 30rpx; font-weight: 600;
+  border-radius: 42rpx; border: none; height: 84rpx; line-height: 84rpx;
+  margin-top: 8rpx;
+}
+</style>

+ 213 - 0
cfc-frontend/pages/health/doa-stage-detail.vue

@@ -0,0 +1,213 @@
+<template>
+  <view class="page">
+    <view v-if="loading" class="loading-box">加载中...</view>
+
+    <view v-else-if="stage" class="content">
+      <!-- 阶段头部 -->
+      <view class="stage-header">
+        <text class="stage-title">{{ stage.title || '阶段目标' }}</text>
+        <text class="stage-dates">{{ formatDate(stage.startDate) }} ~ {{ formatDate(stage.endDate) }}</text>
+        <view class="progress-row">
+          <text class="progress-text">复盘进度 {{ stage.reviewedWeeks || 0 }}/{{ stage.totalWeeks || 0 }}</text>
+        </view>
+      </view>
+
+      <!-- 目标列表 -->
+      <view class="section">
+        <text class="section-title">🎯 阶段目标</text>
+        <view v-for="g in stage.goals" :key="g.id" class="goal-item">
+          <text class="goal-tag" :class="'domain-' + g.domain">{{ domainName(g.domain) }}</text>
+          <text class="goal-content">{{ g.content }}</text>
+        </view>
+      </view>
+
+      <!-- 周时间线 -->
+      <view class="section">
+        <text class="section-title">📅 每周计划与复盘</text>
+        <view v-for="w in stage.weeks" :key="getWeekKey(w)" class="week-card">
+          <view class="week-header" @click="toggleWeek(w.id)">
+            <text class="week-no">第 {{ w.weekNo }} 周</text>
+            <text class="week-dates">{{ formatDate(w.weekStart) }} ~ {{ formatDate(w.weekEnd) }}</text>
+            <text class="week-status" :class="'ws-' + w.status">{{ weekStatusText(w.status) }}</text>
+          </view>
+
+          <view v-if="expandedWeekId === w.id" class="week-body">
+            <!-- 计划 -->
+            <view class="week-block">
+              <text class="block-title">📋 本周计划</text>
+              <text class="block-text">{{ w.plan || '(未填写)' }}</text>
+            </view>
+            <!-- 复盘 -->
+            <view class="week-block" v-if="w.status === 'reviewed'">
+              <text class="block-title">✅ 复盘总结</text>
+              <view class="review-item">
+                <text class="review-label">达成:</text>
+                <text class="review-text">{{ w.achieved || '—' }}</text>
+              </view>
+              <view class="review-item">
+                <text class="review-label">体验:</text>
+                <text class="review-text">{{ w.experience || '—' }}</text>
+              </view>
+              <view class="review-item">
+                <text class="review-label">行得通:</text>
+                <text class="review-text worked">{{ w.worked || '—' }}</text>
+              </view>
+              <view class="review-item">
+                <text class="review-label">要提升:</text>
+                <text class="review-text improve">{{ w.improve || '—' }}</text>
+              </view>
+              <view class="review-item">
+                <text class="review-label">下周挑战:</text>
+                <text class="review-text">{{ w.nextChallenge || '—' }}</text>
+              </view>
+            </view>
+
+            <!-- 编辑按钮(本人或代管) -->
+            <view v-if="canEdit" class="week-actions">
+              <button class="btn-edit" @click="editWeek(w)">编辑{{ w.status === 'reviewed' ? '' : '计划' }}</button>
+            </view>
+          </view>
+        </view>
+      </view>
+
+      <!-- 取消阶段 -->
+      <button v-if="canEdit && stage.status === 'active'" class="btn-cancel" @click="cancelStage">取消阶段</button>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getDoaStageDetail, cancelDoaStage } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      loading: true,
+      stage: null,
+      stageId: 0,
+      selfMemberId: 0,
+      expandedWeekId: 0,
+      canEdit: false
+    }
+  },
+  onLoad(options) {
+    this.stageId = parseInt(options.stageId || '0', 10)
+    this.selfMemberId = uni.getStorageSync('currentMemberId') || 0
+    this.loadDetail()
+  },
+  methods: {
+    getWeekKey(w) {
+      return w.id
+    },
+    async loadDetail() {
+      this.loading = true
+      try {
+        var res = await getDoaStageDetail(this.stageId)
+        if (res.code === 200 && res.data) {
+          this.stage = res.data
+          // 编辑权限:自己 或 未登录成员(memberNickname 存在且非本人 → 后端已校验,前端保守判断)
+          this.canEdit = this.stage.memberId === this.selfMemberId || !this.stage.memberUserId
+        } else {
+          uni.showToast({ title: res.message || '加载失败', icon: 'none' })
+          setTimeout(function() { uni.navigateBack() }, 1000)
+        }
+      } catch (e) {
+        uni.showToast({ title: '加载失败', icon: 'none' })
+      } finally {
+        this.loading = false
+      }
+    },
+    domainName(d) {
+      var map = { health: '健康', wealth: '事业', family: '家庭关系', social: '社会关系', help: '帮助他人' }
+      return map[d] || d
+    },
+    formatDate(dateStr) {
+      if (!dateStr) return ''
+      return String(dateStr).slice(0, 10)
+    },
+    weekStatusText(s) {
+      if (s === 'reviewed') return '已复盘'
+      if (s === 'planned') return '已计划'
+      return '待计划'
+    },
+    toggleWeek(id) {
+      this.expandedWeekId = this.expandedWeekId === id ? 0 : id
+    },
+    editWeek(w) {
+      uni.navigateTo({ url: '/pages/health/doa-week-edit?weekId=' + w.id + '&stageId=' + this.stageId })
+    },
+    cancelStage() {
+      var self = this
+      uni.showModal({
+        title: '确认取消',
+        content: '确定要取消这个阶段吗?周记录将保留但不可再编辑。',
+        success: function(res) {
+          if (res.confirm) {
+            cancelDoaStage(self.stageId).then(function(r) {
+              if (r.code === 200) {
+                uni.showToast({ title: '已取消', icon: 'success' })
+                setTimeout(function() { uni.navigateBack() }, 600)
+              } else {
+                uni.showToast({ title: r.message || '取消失败', icon: 'none' })
+              }
+            })
+          }
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page { min-height: 100vh; background: #F5F7FA; padding: 24rpx; }
+.loading-box { text-align: center; padding: 100rpx 0; color: #94A3B8; font-size: 28rpx; }
+.stage-header {
+  background: linear-gradient(135deg, #10B981, #34D399);
+  border-radius: 20rpx; padding: 32rpx; margin-bottom: 20rpx; color: #fff;
+}
+.stage-title { font-size: 34rpx; font-weight: 700; display: block; }
+.stage-dates { font-size: 24rpx; opacity: 0.9; margin-top: 8rpx; display: block; }
+.progress-row { margin-top: 16rpx; }
+.progress-text { font-size: 24rpx; font-weight: 600; }
+.section { background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 20rpx; }
+.section-title { font-size: 28rpx; font-weight: 700; color: #1E293B; display: block; margin-bottom: 16rpx; }
+.goal-item { display: flex; align-items: center; margin-bottom: 12rpx; }
+.goal-tag {
+  font-size: 20rpx; padding: 6rpx 14rpx; border-radius: 8rpx; margin-right: 12rpx;
+  border: 2rpx solid #E2E8F0; color: #475569; flex-shrink: 0;
+}
+.domain-health { border-color: #FF8C42 !important; color: #C2410C !important; }
+.domain-wealth { border-color: #F59E0B !important; color: #B45309 !important; }
+.domain-family { border-color: #10B981 !important; color: #047857 !important; }
+.domain-social { border-color: #6366F1 !important; color: #4338CA !important; }
+.domain-help { border-color: #FF6B9D !important; color: #BE185D !important; }
+.goal-content { font-size: 26rpx; color: #334155; flex: 1; }
+.week-card { background: #F8FAFC; border-radius: 12rpx; padding: 20rpx; margin-bottom: 12rpx; }
+.week-header { display: flex; align-items: center; }
+.week-no { font-size: 26rpx; font-weight: 700; color: #1E293B; flex: 1; }
+.week-dates { font-size: 20rpx; color: #94A3B8; margin-right: 12rpx; }
+.week-status { font-size: 20rpx; padding: 4rpx 12rpx; border-radius: 20rpx; }
+.ws-pending { background: #F1F5F9; color: #64748B; }
+.ws-planned { background: #DBEAFE; color: #1D4ED8; }
+.ws-reviewed { background: #DCFCE7; color: #16A34A; }
+.week-body { margin-top: 16rpx; border-top: 2rpx solid #E2E8F0; padding-top: 16rpx; }
+.week-block { margin-bottom: 16rpx; }
+.block-title { font-size: 24rpx; font-weight: 700; color: #475569; display: block; margin-bottom: 8rpx; }
+.block-text { font-size: 24rpx; color: #334155; white-space: pre-wrap; display: block; }
+.review-item { margin-bottom: 8rpx; }
+.review-label { font-size: 22rpx; color: #64748B; }
+.review-text { font-size: 22rpx; color: #334155; }
+.review-text.worked { color: #16A34A; }
+.review-text.improve { color: #DC2626; }
+.week-actions { margin-top: 12rpx; }
+.btn-edit {
+  background: #10B981; color: #fff; font-size: 24rpx; font-weight: 600;
+  padding: 12rpx 32rpx; border-radius: 32rpx; border: none; width: auto;
+}
+.btn-cancel {
+  background: #fff; color: #EF4444; font-size: 26rpx; font-weight: 600;
+  border: 2rpx solid #FCA5A5; border-radius: 42rpx; height: 80rpx; line-height: 80rpx;
+  margin-top: 8rpx;
+}
+</style>

+ 154 - 0
cfc-frontend/pages/health/doa-week-edit.vue

@@ -0,0 +1,154 @@
+<template>
+  <view class="page">
+    <view v-if="!week" class="loading-box">加载中...</view>
+
+    <view v-else class="content">
+      <text class="page-hint">第 {{ week.weekNo }} 周 · {{ formatDate(week.weekStart) }} ~ {{ formatDate(week.weekEnd) }}</text>
+
+      <!-- A表:周行动宣告计划 -->
+      <view class="section">
+        <text class="section-title">📋 本周行动计划(A表)</text>
+        <text class="section-desc">写下本周要做的具体行动,每行一个(参考:明确人事时地)</text>
+        <textarea class="field-textarea" v-model="form.plan" placeholder="1、完成站桩15分钟&#10;2、准备早餐、拍照记录&#10;3、..."></textarea>
+        <button class="btn-save" @click="savePlan">保存计划</button>
+      </view>
+
+      <!-- B表:周复盘五问 -->
+      <view class="section" v-if="week.status === 'reviewed' || week.status === 'planned' || week.status === 'pending'">
+        <text class="section-title">✅ 本周复盘(B表)</text>
+        <view class="review-field">
+          <text class="review-label">本周达成了什么?</text>
+          <textarea class="field-textarea" v-model="form.achieved" placeholder="如:体重降到 171.5"></textarea>
+        </view>
+        <view class="review-field">
+          <text class="review-label">有什么体验?</text>
+          <textarea class="field-textarea" v-model="form.experience" placeholder="如:愉悦、疲惫"></textarea>
+        </view>
+        <view class="review-field">
+          <text class="review-label">行得通的是什么?</text>
+          <textarea class="field-textarea" v-model="form.worked" placeholder="总结有效的方法"></textarea>
+        </view>
+        <view class="review-field">
+          <text class="review-label">要提升 / 行不通的是什么?</text>
+          <textarea class="field-textarea" v-model="form.improve" placeholder="总结待改进之处"></textarea>
+        </view>
+        <view class="review-field">
+          <text class="review-label">下一周要挑战自己什么?</text>
+          <textarea class="field-textarea" v-model="form.nextChallenge" placeholder="写下下周的新挑战"></textarea>
+        </view>
+        <button class="btn-save review-btn" @click="saveReview">提交复盘</button>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getDoaStageDetail, submitDoaWeekPlan, submitDoaWeekReview } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      weekId: 0,
+      stageId: 0,
+      week: null,
+      form: {
+        plan: '',
+        achieved: '',
+        experience: '',
+        worked: '',
+        improve: '',
+        nextChallenge: ''
+      }
+    }
+  },
+  onLoad(options) {
+    this.weekId = parseInt(options.weekId || '0', 10)
+    this.stageId = parseInt(options.stageId || '0', 10)
+    this.loadWeek()
+  },
+  methods: {
+    formatDate(dateStr) {
+      if (!dateStr) return ''
+      return String(dateStr).slice(0, 10)
+    },
+    async loadWeek() {
+      try {
+        var res = await getDoaStageDetail(this.stageId)
+        if (res.code === 200 && res.data && res.data.weeks) {
+          for (var i = 0; i < res.data.weeks.length; i++) {
+            if (res.data.weeks[i].id === this.weekId) {
+              this.week = res.data.weeks[i]
+              this.form.plan = this.week.plan || ''
+              this.form.achieved = this.week.achieved || ''
+              this.form.experience = this.week.experience || ''
+              this.form.worked = this.week.worked || ''
+              this.form.improve = this.week.improve || ''
+              this.form.nextChallenge = this.week.nextChallenge || ''
+              break
+            }
+          }
+        }
+        if (!this.week) {
+          uni.showToast({ title: '周记录不存在', icon: 'none' })
+          setTimeout(function() { uni.navigateBack() }, 1000)
+        }
+      } catch (e) {
+        uni.showToast({ title: '加载失败', icon: 'none' })
+      }
+    },
+    savePlan() {
+      var self = this
+      submitDoaWeekPlan(this.weekId, this.form.plan).then(function(res) {
+        if (res.code === 200) {
+          uni.showToast({ title: '计划已保存', icon: 'success' })
+          self.week.status = 'planned'
+        } else {
+          uni.showToast({ title: res.message || '保存失败', icon: 'none' })
+        }
+      })
+    },
+    saveReview() {
+      var self = this
+      var data = {
+        weekId: this.weekId,
+        achieved: this.form.achieved,
+        experience: this.form.experience,
+        worked: this.form.worked,
+        improve: this.form.improve,
+        nextChallenge: this.form.nextChallenge
+      }
+      submitDoaWeekReview(data).then(function(res) {
+        if (res.code === 200) {
+          uni.showToast({ title: '复盘已提交', icon: 'success' })
+          self.week.status = 'reviewed'
+          setTimeout(function() { uni.navigateBack() }, 600)
+        } else {
+          uni.showToast({ title: res.message || '提交失败', icon: 'none' })
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page { min-height: 100vh; background: #F5F7FA; padding: 24rpx; }
+.loading-box { text-align: center; padding: 100rpx 0; color: #94A3B8; font-size: 28rpx; }
+.page-hint { font-size: 26rpx; color: #475569; display: block; margin-bottom: 16rpx; font-weight: 600; }
+.section { background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 20rpx; }
+.section-title { font-size: 28rpx; font-weight: 700; color: #1E293B; display: block; }
+.section-desc { font-size: 22rpx; color: #94A3B8; margin-top: 4rpx; display: block; }
+.field-textarea {
+  background: #F8FAFC; border: 1rpx solid #E2E8F0; border-radius: 12rpx;
+  padding: 16rpx 18rpx; font-size: 26rpx; color: #1E293B;
+  height: 160rpx; width: 100%; box-sizing: border-box; margin-top: 12rpx;
+}
+.review-field { margin-top: 16rpx; }
+.review-label { font-size: 24rpx; color: #475569; font-weight: 600; display: block; }
+.btn-save {
+  background: #10B981; color: #fff; font-size: 28rpx; font-weight: 600;
+  border-radius: 40rpx; border: none; height: 76rpx; line-height: 76rpx;
+  margin-top: 20rpx;
+}
+.review-btn { background: #6366F1; }
+</style>

+ 60 - 53
cfc-frontend/pages/health/gut-flora-detail.vue

@@ -25,11 +25,16 @@
         <view class="interpretation-box" v-if="reportInterpretation && reportInterpretation.gutAgeAssessment">
         <view class="interpretation-box" v-if="reportInterpretation && reportInterpretation.gutAgeAssessment">
           <text class="interp-text">{{ reportInterpretation.gutAgeAssessment }}</text>
           <text class="interp-text">{{ reportInterpretation.gutAgeAssessment }}</text>
         </view>
         </view>
-        <!-- AI 解读 -->
+<!-- AI 解读 -->
         <view class="interpretation-box ai-box" v-if="aiAnalysisText">
         <view class="interpretation-box ai-box" v-if="aiAnalysisText">
           <text class="interp-label">🤖 AI 解读</text>
           <text class="interp-label">🤖 AI 解读</text>
           <text class="interp-text">{{ aiAnalysisText }}</text>
           <text class="interp-text">{{ aiAnalysisText }}</text>
         </view>
         </view>
+        <!-- 报告总结描述 -->
+        <view class="summary-box" v-if="reportSummaryText">
+          <text class="summary-title">报告总结</text>
+          <text class="summary-text">{{ reportSummaryText }}</text>
+        </view>
       </view>
       </view>
 
 
       <!-- 评分圆环(折行显示,无横向滚动) -->
       <!-- 评分圆环(折行显示,无横向滚动) -->
@@ -49,11 +54,6 @@
           <text class="nav-label">菌群详情</text>
           <text class="nav-label">菌群详情</text>
           <text class="nav-count">{{ floraAbnormalCount }}/{{ floraCount }}种</text>
           <text class="nav-count">{{ floraAbnormalCount }}/{{ floraCount }}种</text>
         </view>
         </view>
-        <view class="nav-card" @tap="goToPathogen">
-          <text class="nav-icon">🔬</text>
-          <text class="nav-label">病原菌</text>
-          <text class="nav-count">{{ pathogenCount }}项</text>
-        </view>
         <view class="nav-card" @tap="goToRisks">
         <view class="nav-card" @tap="goToRisks">
           <text class="nav-icon">⚠️</text>
           <text class="nav-icon">⚠️</text>
           <text class="nav-label">疾病风险</text>
           <text class="nav-label">疾病风险</text>
@@ -218,25 +218,6 @@
         </view>
         </view>
       </view>
       </view>
 
 
-      <!-- 疾病风险内联展示(颜色区分) -->
-      <view class="accordion-section" v-if="diseaseRisks.length > 0">
-        <view class="accordion-header" @tap="toggleAccordion('risks')">
-          <text class="accordion-title">疾病风险</text>
-          <text class="accordion-stats" v-if="diseaseRisks.length > 0">异常 <text class="abnormal-num">{{ diseaseRisksAbnormal }}</text> / 共 {{ diseaseRisks.length }} 项</text>
-          <text class="accordion-arrow">{{ accordionOpen.risks ? '收起' : '展开' }}</text>
-        </view>
-        <view class="accordion-body" v-if="accordionOpen.risks">
-          <view class="risk-card-inline" v-for="(risk, ri) in diseaseRisks" :key="ri"
-                :class="'risk-level-' + (risk._cssClass || 'low')">
-            <text class="risk-name-inline">{{ risk.diseaseName }}</text>
-            <text class="risk-badge-inline" :class="'badge-' + (risk._cssClass || 'low')">
-              {{ risk.riskLevel || '--' }}
-            </text>
-            <text class="risk-value-inline" v-if="risk.riskValue">风险值 {{ risk.riskValue }}</text>
-          </view>
-          <view class="empty-hint" v-if="diseaseRisks.length === 0">暂无疾病风险数据</view>
-        </view>
-      </view>
 
 
       <view class="feedback-section" @tap="openFeedback">
       <view class="feedback-section" @tap="openFeedback">
         <text class="feedback-icon">💬</text>
         <text class="feedback-icon">💬</text>
@@ -391,7 +372,6 @@ export default {
       foodCount: 0,
       foodCount: 0,
       cautionCount: 0,
       cautionCount: 0,
       indicatorCount: 0,
       indicatorCount: 0,
-      pathogenCount: 0,
       knowledgeVisible: false,
       knowledgeVisible: false,
       knowledgeData: {},
       knowledgeData: {},
       feedbackVisible: false,
       feedbackVisible: false,
@@ -455,7 +435,7 @@ export default {
       }
       }
       return count
       return count
     },
     },
-    riskGroupAbnormalCount: function() {
+riskGroupAbnormalCount: function() {
       if (!this.analysisData || !this.analysisData.riskIndicators) return 0
       if (!this.analysisData || !this.analysisData.riskIndicators) return 0
       var list = this.analysisData.riskIndicators
       var list = this.analysisData.riskIndicators
       var count = 0
       var count = 0
@@ -465,6 +445,31 @@ export default {
       }
       }
       return count
       return count
     },
     },
+    /** 报告日期格式化 — 计算属性避免模板中方法引用失效 */
+    reportDateText: function() {
+      var d = this.report.reportDate
+      if (!d) return ''
+      var date = parseDate(d)
+      if (!date) return String(d).substring(0, 10)
+      var y = date.getFullYear()
+      var m = ('' + (date.getMonth() + 1)).padStart(2, '0')
+      var day = ('' + date.getDate()).padStart(2, '0')
+      return y + '-' + m + '-' + day
+    },
+    /** 报告总结描述 */
+    reportSummaryText: function() {
+      var parts = []
+      if (this.report.healthRating) parts.push('综合评级: ' + this.report.healthRating)
+      if (this.reportInterpretation && this.reportInterpretation.gutAgeAssessment)
+        parts.push(this.reportInterpretation.gutAgeAssessment)
+      if (this.analysisData && this.analysisData.dietaryPattern && this.analysisData.dietaryPattern.detail)
+        parts.push(this.analysisData.dietaryPattern.detail)
+      var funcAnalysis = this.analysisData && this.analysisData.functionalAnalysis || []
+      var abnDims = funcAnalysis.filter(function(d) { return d.status && d.status !== '良好' })
+      if (abnDims.length > 0)
+        parts.push('需关注维度:' + abnDims.map(function(d) { return d.name }).join('、'))
+      return parts.join(';')
+    },
     draftMemberText: function() {
     draftMemberText: function() {
       if (this.selectedMemberObj) return this.selectedMemberObj.nickname || this.selectedMemberObj.name || '已选择'
       if (this.selectedMemberObj) return this.selectedMemberObj.nickname || this.selectedMemberObj.name || '已选择'
       if (this.extractedName) return this.extractedName + '(待绑定)'
       if (this.extractedName) return this.extractedName + '(待绑定)'
@@ -483,17 +488,6 @@ export default {
   },
   },
   methods: {
   methods: {
     goBack: function() { uni.navigateBack() },
     goBack: function() { uni.navigateBack() },
-    /** 报告日期仅显示 yyyy-MM-dd(去掉时间部分) */
-    reportDateText: function() {
-      var d = this.report.reportDate
-      if (!d) return ''
-      var date = parseDate(d)
-      if (!date) return String(d).substring(0, 10)
-      var y = date.getFullYear()
-      var m = ('' + (date.getMonth() + 1)).padStart(2, '0')
-      var day = ('' + date.getDate()).padStart(2, '0')
-      return y + '-' + m + '-' + day
-    },
     /** 状态 → CSS安全英文(兼容中英文,与 report-confirm 保持一致) */
     /** 状态 → CSS安全英文(兼容中英文,与 report-confirm 保持一致) */
     _cssForStatus: function(s) {
     _cssForStatus: function(s) {
       var k = String(s || '').trim()
       var k = String(s || '').trim()
@@ -638,11 +632,6 @@ export default {
             }).catch(function() {})
             }).catch(function() {})
           }
           }
           self.indicatorCount = (res.data.indicators || []).length
           self.indicatorCount = (res.data.indicators || []).length
-          // 计算病原菌总数
-          var flora = res.data.gutFlora || []
-          self.pathogenCount = flora.filter(function(i) {
-            return i.category === 'pathogen_genus' || i.category === 'pathogen_detection'
-          }).length
         }
         }
       })
       })
       // 加载菌群功能分析
       // 加载菌群功能分析
@@ -694,7 +683,7 @@ export default {
         uni.showToast({ title: '加载异常', icon: 'none' })
         uni.showToast({ title: '加载异常', icon: 'none' })
       })
       })
     },
     },
-    applyDraftPayload: function(payload) {
+applyDraftPayload: function(payload) {
       // 将 draft payload 映射到模板所需的数据结构(report/scoreItems/indicators)
       // 将 draft payload 映射到模板所需的数据结构(report/scoreItems/indicators)
       var report = {}
       var report = {}
       if (payload.summary) {
       if (payload.summary) {
@@ -704,9 +693,10 @@ export default {
         report.age = s.age != null ? s.age : ''
         report.age = s.age != null ? s.age : ''
         report.reportNumber = s.reportNumber || ''
         report.reportNumber = s.reportNumber || ''
         report.gutAge = s.gutAge || ''
         report.gutAge = s.gutAge || ''
-report.gutType = s.gutType || ''
-      report.shannonIndex = s.shannonIndex || ''
-      report.overallScore = s.overallScore
+        report.gutType = s.gutType || ''
+        report.shannonIndex = s.shannonIndex || ''
+        report.reportDate = s.reportDate || ''
+        report.overallScore = s.overallScore
         report.gutHealthScore = s.gutHealthScore
         report.gutHealthScore = s.gutHealthScore
         report.chronicDiseaseScore = s.chronicDiseaseScore
         report.chronicDiseaseScore = s.chronicDiseaseScore
         report.nutritionScore = s.nutritionScore
         report.nutritionScore = s.nutritionScore
@@ -995,12 +985,6 @@ report.gutType = s.gutType || ''
     goToSpecies: function() {
     goToSpecies: function() {
       uni.navigateTo({ url: '/pages/health/gut-flora-species-detail?reportId=' + this.reportId })
       uni.navigateTo({ url: '/pages/health/gut-flora-species-detail?reportId=' + this.reportId })
     },
     },
-    goToPathogen: function() {
-      uni.navigateTo({ url: '/pages/health/gut-flora-species-detail?reportId=' + this.reportId + '&tab=pathogen_genus' })
-    },
-    goToTaxonomy: function() {
-      uni.navigateTo({ url: '/pages/health/gut-flora-species-detail?reportId=' + this.reportId + '&tab=taxonomy_class' })
-    },
     goToRisks: function() {
     goToRisks: function() {
       uni.navigateTo({ url: '/pages/health/gut-flora-risks-detail?reportId=' + this.reportId })
       uni.navigateTo({ url: '/pages/health/gut-flora-risks-detail?reportId=' + this.reportId })
     },
     },
@@ -1187,6 +1171,29 @@ report.gutType = s.gutType || ''
 .ai-box .interp-text {
 .ai-box .interp-text {
   color: #004D40;
   color: #004D40;
 }
 }
+/* 报告总结描述 */
+.summary-box {
+  background: rgba(255,255,255,0.15);
+  border-radius: 12rpx;
+  padding: 16rpx 20rpx;
+  margin: 16rpx 0 0;
+  border-left: 6rpx solid #A5D6A7;
+  display: flex;
+  flex-direction: column;
+  gap: 8rpx;
+}
+.summary-title {
+  font-size: 24rpx;
+  font-weight: 600;
+  color: #fff;
+  opacity: 0.95;
+}
+.summary-text {
+  font-size: 24rpx;
+  color: #fff;
+  line-height: 1.6;
+  opacity: 0.95;
+}
 
 
 /* 解读入口 */
 /* 解读入口 */
 .ai-fab {
 .ai-fab {

+ 17 - 7
cfc-frontend/pages/membership/index.vue

@@ -62,12 +62,14 @@
           <view class="compare-row header-row">
           <view class="compare-row header-row">
             <text class="compare-cell cell-feature">权益项</text>
             <text class="compare-cell cell-feature">权益项</text>
             <text class="compare-cell cell-free">免费</text>
             <text class="compare-cell cell-free">免费</text>
+            <text class="compare-cell cell-basic">体验会员</text>
             <text class="compare-cell cell-family">家庭会员</text>
             <text class="compare-cell cell-family">家庭会员</text>
             <text class="compare-cell cell-premium">高级会员</text>
             <text class="compare-cell cell-premium">高级会员</text>
           </view>
           </view>
           <view class="compare-row" v-for="(item, idx) in compareData" :key="idx">
           <view class="compare-row" v-for="(item, idx) in compareData" :key="idx">
             <text class="compare-cell cell-feature">{{ item.name }}</text>
             <text class="compare-cell cell-feature">{{ item.name }}</text>
             <text class="compare-cell cell-free">{{ item.free }}</text>
             <text class="compare-cell cell-free">{{ item.free }}</text>
+            <text class="compare-cell cell-basic">{{ item.basic }}</text>
             <text class="compare-cell cell-family">{{ item.family }}</text>
             <text class="compare-cell cell-family">{{ item.family }}</text>
             <text class="compare-cell cell-premium">{{ item.premium }}</text>
             <text class="compare-cell cell-premium">{{ item.premium }}</text>
           </view>
           </view>
@@ -89,13 +91,14 @@ export default {
       currentLevelName: '免费用户',
       currentLevelName: '免费用户',
       features: ['折扣购物', '家庭管理', '推荐佣金', '专属活动', '任务特权', '成长报告'],
       features: ['折扣购物', '家庭管理', '推荐佣金', '专属活动', '任务特权', '成长报告'],
       compareData: [
       compareData: [
-        { name: '商品折扣', free: '普通价格', family: '会员折扣', premium: '会员折扣' },
-        { name: '家庭管理', free: '基础功能', family: '全部功能', premium: '全部功能+优先响应' },
-        { name: '管家服务', free: '—', family: '—', premium: '每周一次' },
-        { name: '推荐佣金', free: '不可参与', family: '可参与', premium: '可参与' },
-        { name: '专属活动', free: '部分参与', family: '全部参与', premium: '优先参与' },
-        { name: '成长报告', free: '月度', family: '每周+深度', premium: '每日+深度+专家解读' },
-        { name: '任务配额', free: '每日5个', family: '不限量', premium: '不限量+优先审核' }
+        { name: '商品折扣', free: '普通价格', basic: '会员折扣', family: '会员折扣', premium: '会员折扣' },
+        { name: '家庭管理', free: '基础功能', basic: '3个孩子', family: '10个孩子', premium: '20个孩子' },
+        { name: 'AI健康顾问', free: '—', basic: '3次/月', family: '不限', premium: '不限' },
+        { name: '报告解读', free: '—', basic: '1次/月', family: '不限', premium: '深度解读' },
+        { name: '测评预约', free: '仅查看', basic: '1次/月', family: '不限', premium: '优先' },
+        { name: '管家服务', free: '—', basic: '—', family: '月1次', premium: '周1次' },
+        { name: '推荐佣金', free: '不可参与', basic: '可参与', family: '可参与', premium: '可参与' },
+        { name: '专属活动', free: '部分参与', basic: '部分参与', family: '全部参与', premium: '优先参与' }
       ],
       ],
       myButlerInfo: null,
       myButlerInfo: null,
       isL2Active: false
       isL2Active: false
@@ -122,6 +125,13 @@ export default {
         if (m.levelCode && m.levelCode !== 'FREE') {
         if (m.levelCode && m.levelCode !== 'FREE') {
           that.isFree = false
           that.isFree = false
           that.currentLevelName = m.levelName || '家庭会员'
           that.currentLevelName = m.levelName || '家庭会员'
+          if (m.levelCode === 'BASIC') {
+            that.currentLevelName = '体验会员'
+          } else if (m.levelCode === 'FAMILY') {
+            that.currentLevelName = '家庭会员'
+          } else if (m.levelCode === 'PREMIUM') {
+            that.currentLevelName = '高级会员'
+          }
         } else {
         } else {
           that.isFree = true
           that.isFree = true
           that.currentLevelName = '免费用户'
           that.currentLevelName = '免费用户'

+ 4 - 0
cfc-frontend/pages/membership/upgrade.vue

@@ -142,6 +142,9 @@ export default {
         if (self.selectedLevel === 'FAMILY') {
         if (self.selectedLevel === 'FAMILY') {
           return p.key === 'yearly'
           return p.key === 'yearly'
         }
         }
+        if (self.selectedLevel === 'BASIC') {
+          return p.key === 'monthly'
+        }
         if (self.selectedLevel === 'PREMIUM') {
         if (self.selectedLevel === 'PREMIUM') {
           return p.key === 'monthly' || p.key === 'yearly'
           return p.key === 'monthly' || p.key === 'yearly'
         }
         }
@@ -167,6 +170,7 @@ export default {
     getLevelName(code) {
     getLevelName(code) {
       var map = {
       var map = {
         FREE: '免费版',
         FREE: '免费版',
+        BASIC: '体验会员',
         FAMILY: '家庭会员',
         FAMILY: '家庭会员',
         PREMIUM: '高级会员'
         PREMIUM: '高级会员'
       }
       }

+ 512 - 0
cfc-frontend/pages/vendor/order-review/order-review.vue

@@ -0,0 +1,512 @@
+<template>
+  <view class="container">
+    <view v-if="loading" class="loading-wrap">
+      <text class="loading-text">加载中...</text>
+    </view>
+    <scroll-view v-else scroll-y class="content">
+      <!-- 订单头 -->
+      <view class="order-header-section">
+        <view class="order-no-row">
+          <text class="order-no">订单号:{{ order.orderNo }}</text>
+          <text class="status-tag">{{ statusLabel(order.status) }}</text>
+        </view>
+        <text class="order-hint">订单尚未支付,可进行审核修改</text>
+      </view>
+
+      <!-- 商品信息 -->
+      <view class="section">
+        <text class="section-title">商品信息</text>
+        <view class="product-card">
+          <image class="product-cover" :src="order.coverImage || ''" mode="aspectFill" />
+          <view class="product-info-wrap">
+            <text class="product-name">{{ order.productName }}</text>
+            <text class="product-qty">x{{ order.quantity || 1 }}</text>
+            <text class="product-price">¥{{ formatPrice(order.unitPrice) }}</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 修改价格 -->
+      <view class="section">
+        <text class="section-title">修改价格</text>
+        <view class="edit-row" @click="showPriceInput">
+          <text class="edit-label">当前单价</text>
+          <text class="edit-value">{{ formatPrice(order.unitPrice) }}</text>
+          <text class="edit-hint">点击修改</text>
+        </view>
+        <view v-if="priceInput" class="price-input-row">
+          <text class="input-label">新单价(元)</text>
+          <input class="price-input" type="digit" v-model="newUnitPrice" placeholder="输入新单价" />
+          <text class="price-preview">新总价:¥{{ formatPrice(newUnitPrice * (order.quantity || 1)) }}</text>
+        </view>
+      </view>
+
+      <!-- 修改配送方式 -->
+      <view class="section">
+        <text class="section-title">修改配送方式</text>
+        <view class="edit-row" @click="showDeliveryInput">
+          <text class="edit-label">当前配送方式</text>
+          <text class="edit-value">{{ deliveryLabel(order.deliveryMethod) }}</text>
+          <text class="edit-hint">点击修改</text>
+        </view>
+        <view v-if="deliveryInput" class="delivery-options">
+          <view class="delivery-option" :class="newDeliveryMethod === 1 ? 'selected' : ''" @click="newDeliveryMethod = 1">
+            <text>快递配送</text>
+          </view>
+          <view class="delivery-option" :class="newDeliveryMethod === 2 ? 'selected' : ''" @click="newDeliveryMethod = 2">
+            <text>自提</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 添加优惠券 -->
+      <view class="section">
+        <text class="section-title">添加优惠券</text>
+        <view v-if="!order.couponId" class="edit-row" @click="showCouponInput">
+          <text class="edit-label">优惠券</text>
+          <text class="edit-value">未使用</text>
+          <text class="edit-hint">点击添加</text>
+        </view>
+        <view v-else class="coupon-info">
+          <text class="coupon-tag">已添加优惠券</text>
+        </view>
+        <view v-if="couponInput" class="coupon-input-row">
+          <text class="input-label">优惠券金额(元)</text>
+          <input class="coupon-input" type="digit" v-model="couponValue" placeholder="输入优惠券金额" />
+        </view>
+      </view>
+
+      <!-- 添加赠品 -->
+      <view class="section">
+        <text class="section-title">添加赠品</text>
+        <view class="edit-row" @click="showGiftInput">
+          <text class="edit-label">当前赠品</text>
+          <text class="edit-value">{{ giftCount }} 件</text>
+          <text class="edit-hint">点击添加</text>
+        </view>
+        <view v-if="giftInput" class="gift-input-row">
+          <input class="gift-input" type="text" v-model="giftProductName" placeholder="赠品名称" />
+          <input class="gift-input" type="text" v-model="giftProductImage" placeholder="赠品图片URL(选填)" />
+          <view class="gift-add-btn" @click="addGift">+ 添加</view>
+        </view>
+        <view v-if="giftList.length > 0" class="gift-list">
+          <view v-for="gift in giftList" :key="gift.id" class="gift-item">
+            <image class="gift-img" :src="gift.giftProductImage || ''" mode="aspectFill" />
+            <text class="gift-name">{{ gift.giftProductName }}</text>
+            <text class="gift-qty">x{{ gift.quantity }}</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 修改备注 -->
+      <view class="section">
+        <text class="section-title">修改备注</text>
+        <textarea class="remark-textarea" v-model="remarkInput" placeholder="输入修改原因或备注(选填)" maxlength="200" />
+      </view>
+
+      <!-- 修改历史 -->
+      <view v-if="order.remark" class="section">
+        <text class="section-title">修改记录</text>
+        <text class="remark-history">{{ order.remark }}</text>
+      </view>
+    </scroll-view>
+
+    <!-- 底部操作栏 -->
+    <view class="bottom-bar">
+      <button class="btn-save" @click="onSave">保存修改</button>
+    </view>
+  </view>
+</template>
+
+<script>
+import { productOrderDetail, productOrderReviewUpdate } from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      order: {},
+      loading: false,
+      orderNo: '',
+      priceInput: false,
+      newUnitPrice: '',
+      deliveryInput: false,
+      newDeliveryMethod: null,
+      couponInput: false,
+      couponValue: '',
+      giftInput: false,
+      giftList: [],
+      giftProductName: '',
+      giftProductImage: '',
+      remarkInput: ''
+    }
+  },
+  onLoad(options) {
+    if (options.orderNo) {
+      this.orderNo = options.orderNo
+      this.loadDetail()
+    }
+  },
+  computed: {
+    giftCount() {
+      return this.giftList.length
+    }
+  },
+  methods: {
+    loadDetail() {
+      this.loading = true
+      productOrderDetail({ orderNo: this.orderNo }).then(res => {
+        this.loading = false
+        if (res.code === 200 && res.data) {
+          this.order = res.data
+        } else {
+          uni.showToast({ title: res.message || '加载失败', icon: 'none' })
+        }
+      }).catch(function () {
+        this.loading = false
+      })
+    },
+    showPriceInput() {
+      this.priceInput = true
+      this.newUnitPrice = String(this.order.unitPrice ? Math.round(this.order.unitPrice / 100) : '')
+    },
+    showDeliveryInput() {
+      this.deliveryInput = true
+      this.newDeliveryMethod = this.order.deliveryMethod
+    },
+    showCouponInput() {
+      this.couponInput = true
+      this.couponValue = ''
+    },
+    showGiftInput() {
+      this.giftInput = true
+      this.giftProductName = ''
+      this.giftProductImage = ''
+    },
+    addGift() {
+      if (!this.giftProductName) {
+        uni.showToast({ title: '请填写赠品名称', icon: 'none' })
+        return
+      }
+      this.giftList.push({
+        id: Date.now(),
+        giftType: 'product',
+        giftProductName: this.giftProductName,
+        giftProductImage: this.giftProductImage,
+        quantity: 1
+      })
+      this.giftProductName = ''
+      this.giftProductImage = ''
+    },
+    onSave() {
+      var params = { orderNo: this.orderNo }
+
+      if (this.priceInput && this.newUnitPrice) {
+        params.unitPrice = parseInt(this.newUnitPrice) * 100
+      }
+
+      if (this.deliveryInput && this.newDeliveryMethod != null) {
+        params.deliveryMethod = this.newDeliveryMethod
+      }
+
+      if (this.couponInput && this.couponValue && parseInt(this.couponValue) > 0) {
+        params.couponType = 'ONE_TIME'
+        params.couponValue = parseInt(this.couponValue) * 100
+      }
+
+      if (this.giftList.length > 0) {
+        params.giftItems = this.giftList
+      }
+
+      if (this.remarkInput) {
+        params.remark = this.remarkInput
+      }
+
+      if (!params.unitPrice && !params.deliveryMethod && !params.couponValue && !params.giftItems) {
+        uni.showToast({ title: '请至少修改一项', icon: 'none' })
+        return
+      }
+
+      uni.showLoading({ title: '保存中...' })
+      productOrderReviewUpdate(params).then(function (res) {
+        uni.hideLoading()
+        if (res.code === 200) {
+          uni.showToast({ title: '修改已保存', icon: 'success' })
+          setTimeout(function () {
+            uni.navigateBack()
+          }, 1500)
+        } else {
+          uni.showToast({ title: res.message || '保存失败', icon: 'none' })
+        }
+      }).catch(function () {
+        uni.hideLoading()
+        uni.showToast({ title: '网络错误', icon: 'none' })
+      })
+    },
+    statusLabel(status) {
+      var map = { pending: '待支付', paid: '已支付', completed: '已完成', cancelled: '已取消' }
+      return map[status] || status
+    },
+    deliveryLabel(method) {
+      var map = { 1: '快递配送', 2: '自提', 3: '快递/自提可选', 4: '线上-可选人', 5: '线上-单人' }
+      return map[method] || '未知'
+    },
+    formatPrice(value) {
+      if (!value && value !== 0) return '-'
+      return (value / 100).toFixed(2)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background: #f5f5f5;
+  display: flex;
+  flex-direction: column;
+}
+.loading-wrap {
+  display: flex;
+  justify-content: center;
+  padding-top: 200rpx;
+}
+.loading-text {
+  font-size: 28rpx;
+  color: #999;
+}
+.content {
+  flex: 1;
+  height: calc(100vh - 120rpx);
+  padding-bottom: 120rpx;
+}
+.order-header-section {
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  padding: 40rpx 30rpx;
+}
+.order-no-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12rpx;
+}
+.order-no {
+  font-size: 26rpx;
+  color: rgba(255,255,255,0.85);
+}
+.status-tag {
+  font-size: 24rpx;
+  padding: 6rpx 20rpx;
+  border-radius: 20rpx;
+  background: rgba(255,255,255,0.25);
+  color: #fff;
+}
+.order-hint {
+  font-size: 24rpx;
+  color: rgba(255,255,255,0.75);
+}
+.section {
+  background: #fff;
+  margin: 20rpx;
+  border-radius: 16rpx;
+  padding: 24rpx;
+}
+.section-title {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 20rpx;
+  display: block;
+}
+.product-card {
+  display: flex;
+  gap: 20rpx;
+  align-items: center;
+}
+.product-cover {
+  width: 160rpx;
+  height: 160rpx;
+  border-radius: 10rpx;
+  background: #eee;
+  flex-shrink: 0;
+}
+.product-info-wrap {
+  flex: 1;
+}
+.product-name {
+  display: block;
+  font-size: 28rpx;
+  color: #333;
+  margin-bottom: 8rpx;
+}
+.product-qty {
+  font-size: 24rpx;
+  color: #999;
+  margin-bottom: 8rpx;
+}
+.product-price {
+  font-size: 30rpx;
+  color: #F97316;
+  font-weight: bold;
+}
+.edit-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16rpx 0;
+}
+.edit-label {
+  font-size: 26rpx;
+  color: #666;
+}
+.edit-value {
+  font-size: 26rpx;
+  color: #333;
+}
+.edit-hint {
+  font-size: 24rpx;
+  color: #F97316;
+}
+.price-input-row,
+.coupon-input-row {
+  margin-top: 16rpx;
+  padding-top: 16rpx;
+  border-top: 1rpx solid #f5f5f5;
+}
+.input-label {
+  font-size: 26rpx;
+  color: #666;
+  margin-bottom: 12rpx;
+  display: block;
+}
+.price-input,
+.coupon-input {
+  border: 1rpx solid #ddd;
+  border-radius: 8rpx;
+  padding: 12rpx 20rpx;
+  font-size: 28rpx;
+  margin-bottom: 12rpx;
+  width: 100%;
+  box-sizing: border-box;
+}
+.price-preview,
+.coupon-preview {
+  font-size: 26rpx;
+  color: #F97316;
+}
+.delivery-options {
+  display: flex;
+  gap: 20rpx;
+  margin-top: 16rpx;
+}
+.delivery-option {
+  flex: 1;
+  text-align: center;
+  padding: 16rpx;
+  border-radius: 8rpx;
+  border: 1rpx solid #ddd;
+  font-size: 26rpx;
+  color: #333;
+}
+.delivery-option.selected {
+  border-color: #F97316;
+  color: #F97316;
+  background: #fff7e6;
+}
+.coupon-info {
+  padding: 16rpx 0;
+}
+.coupon-tag {
+  font-size: 26rpx;
+  color: #52c41a;
+  background: #f6ffed;
+  padding: 8rpx 20rpx;
+  border-radius: 8rpx;
+}
+.gift-input-row {
+  margin-top: 16rpx;
+  display: flex;
+  flex-direction: column;
+  gap: 12rpx;
+}
+.gift-input {
+  border: 1rpx solid #ddd;
+  border-radius: 8rpx;
+  padding: 12rpx 20rpx;
+  font-size: 26rpx;
+}
+.gift-add-btn {
+  background: #F97316;
+  color: #fff;
+  text-align: center;
+  padding: 16rpx;
+  border-radius: 8rpx;
+  font-size: 26rpx;
+  margin-top: 12rpx;
+}
+.gift-list {
+  margin-top: 16rpx;
+}
+.gift-item {
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+  padding: 12rpx 0;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+.gift-img {
+  width: 60rpx;
+  height: 60rpx;
+  border-radius: 8rpx;
+  background: #eee;
+}
+.gift-name {
+  font-size: 24rpx;
+  color: #333;
+  flex: 1;
+}
+.gift-qty {
+  font-size: 24rpx;
+  color: #999;
+}
+.remark-textarea {
+  width: 100%;
+  height: 120rpx;
+  border: 1rpx solid #ddd;
+  border-radius: 8rpx;
+  padding: 16rpx;
+  font-size: 26rpx;
+  box-sizing: border-box;
+}
+.remark-history {
+  font-size: 24rpx;
+  color: #666;
+  line-height: 1.6;
+}
+.bottom-bar {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  height: 100rpx;
+  background: #fff;
+  border-top: 1rpx solid #eee;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  padding: 0 30rpx;
+  z-index: 100;
+}
+.btn-save {
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  color: #fff;
+  font-size: 28rpx;
+  padding: 0 60rpx;
+  height: 72rpx;
+  line-height: 72rpx;
+  border-radius: 36rpx;
+  border: none;
+}
+.btn-save::after {
+  border: none;
+}
+</style>

+ 12 - 0
cfc-frontend/pages/vendor/orders/orders.vue

@@ -43,6 +43,11 @@
           <view class="order-footer">
           <view class="order-footer">
             <text class="order-time">{{ formatTime(item.createdAt) }}</text>
             <text class="order-time">{{ formatTime(item.createdAt) }}</text>
             <view class="order-actions">
             <view class="order-actions">
+              <button
+                v-if="item.status === 'pending'"
+                class="btn-xs btn-review"
+                @click.stop="onReview(item)"
+              >审核修改</button>
               <button
               <button
                 v-if="item.status === 'paid'"
                 v-if="item.status === 'paid'"
                 class="btn-xs btn-confirm"
                 class="btn-xs btn-confirm"
@@ -141,6 +146,9 @@ export default {
         }
         }
       })
       })
     },
     },
+    onReview(item) {
+      uni.navigateTo({ url: '/pages/vendor/order-review/order-review?orderNo=' + item.orderNo })
+    },
     statusLabel(status) {
     statusLabel(status) {
       const map = {
       const map = {
         pending: '待支付',
         pending: '待支付',
@@ -290,6 +298,10 @@ export default {
   background: #e6f7ff;
   background: #e6f7ff;
   color: #1890ff;
   color: #1890ff;
 }
 }
+.btn-review {
+  background: #fff7e6;
+  color: #F97316;
+}
 .btn-xs::after {
 .btn-xs::after {
   border: none;
   border: none;
 }
 }

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

@@ -456,6 +456,10 @@ export const getMyMembership = () => {
 	return request('/api/membership/my', 'POST')
 	return request('/api/membership/my', 'POST')
 }
 }
 
 
+export const getAllQuotaStatus = () => {
+	return request('/api/basic-quota/status/all', 'POST')
+}
+
 export const getCurrentLevel = () => {
 export const getCurrentLevel = () => {
 	return request('/api/membership/current', 'POST')
 	return request('/api/membership/current', 'POST')
 }
 }
@@ -1554,6 +1558,11 @@ export const productOrderConfirm = (params) => {
   return request('/api/product/order/confirm', 'POST', params)
   return request('/api/product/order/confirm', 'POST', params)
 }
 }
 
 
+// 供应商审核修改订单(仅待支付状态)
+export const productOrderReviewUpdate = (params) => {
+  return request('/api/product/order/review/update', 'POST', params)
+}
+
 // 用户申请订单退款
 // 用户申请订单退款
 export const productOrderRefundApply = (params) => {
 export const productOrderRefundApply = (params) => {
   return request('/api/product/order/refund/apply', 'POST', params)
   return request('/api/product/order/refund/apply', 'POST', params)
@@ -2031,6 +2040,15 @@ export const deleteChallenge = (id) => request('/api/health/challenge/delete/' +
 export const getChallengeTemplates = () => request('/api/health/challenge/templates', 'POST', {})
 export const getChallengeTemplates = () => request('/api/health/challenge/templates', 'POST', {})
 export const sendLike = (data) => request('/api/health/interact/like', 'POST', data)
 export const sendLike = (data) => request('/api/health/interact/like', 'POST', data)
 
 
+// ===== DOA 个人目标(家庭打卡个人目标模块) =====
+export const createDoaStage = (data) => request('/api/doa/stage/create', 'POST', data)
+export const getDoaOverview = () => request('/api/doa/overview', 'POST', {})
+export const getDoaStageDetail = (stageId) => request('/api/doa/stage/detail', 'POST', { stageId: stageId })
+export const cancelDoaStage = (stageId) => request('/api/doa/stage/cancel', 'POST', { stageId: stageId })
+export const submitDoaWeekPlan = (weekId, plan) => request('/api/doa/week/plan', 'POST', { weekId: weekId, plan: plan })
+export const submitDoaWeekReview = (data) => request('/api/doa/week/review', 'POST', data)
+export const getDoaWeekList = (stageId) => request('/api/doa/week/list', 'POST', { stageId: stageId })
+
 // ===== 健康社区扩散(P3 健康圈/排行/分享) =====
 // ===== 健康社区扩散(P3 健康圈/排行/分享) =====
 export const getCircleList = (data) => request('/api/health/circle/list', 'POST', data)
 export const getCircleList = (data) => request('/api/health/circle/list', 'POST', data)
 export const createCircle = (data) => request('/api/health/circle/create', 'POST', data)
 export const createCircle = (data) => request('/api/health/circle/create', 'POST', data)

+ 5 - 3
cfc-langgraph/app/graphs/health_plan_graph.py

@@ -18,6 +18,7 @@ from langchain_core.messages import SystemMessage, HumanMessage
 from app.rag.retriever import RagRetriever
 from app.rag.retriever import RagRetriever
 from app.config import settings
 from app.config import settings
 from app.tools.java_client import JavaClient
 from app.tools.java_client import JavaClient
+from app.prompt_service import get_prompt
 import logging
 import logging
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
@@ -186,9 +187,10 @@ async def collect_data(state: PlanState) -> dict:
     }
     }
 
 
 
 
-def build_prompt(state: PlanState) -> str:
+async def build_prompt(state: PlanState) -> str:
     """组装 LLM prompt"""
     """组装 LLM prompt"""
-    parts = [DEFAULT_PROMPT]
+    base_prompt = await get_prompt("health_plan") or DEFAULT_PROMPT
+    parts = [base_prompt]
 
 
     parts.append(f"\n## 用户目标\n{state['goal']}")
     parts.append(f"\n## 用户目标\n{state['goal']}")
     if state.get("dimensions"):
     if state.get("dimensions"):
@@ -226,7 +228,7 @@ async def generate_plan(state: PlanState) -> dict:
         base_url=settings.llm_base_url,
         base_url=settings.llm_base_url,
         temperature=0.3,
         temperature=0.3,
     )
     )
-    prompt = build_prompt(state)
+    prompt = await build_prompt(state)
     messages = [SystemMessage(content=prompt)]
     messages = [SystemMessage(content=prompt)]
     try:
     try:
         response = await llm.ainvoke(messages)
         response = await llm.ainvoke(messages)

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-bbbadd9014be486b50c6e318059cf51ae7ca6b81
+130b3d06467542e7f89fefd1f0f566d5495e4fa5

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

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

+ 1 - 1
cfc-web/package.json

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

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

@@ -4,6 +4,84 @@
 
 
 ---
 ---
 
 
+## v1.0.1386 (2026-09-13)
+
+### Bug 修复
+- 修复 DatabaseInitializer git 冲突标记残留及预存注释行 try{ bug,编译失败
+
+
+## v1.0.1385 (2026-09-13)
+
+### 新功能
+- 菜单管理图标字段改为可视化图标选择器(282个Element图标+搜索+清除)
+
+
+## v1.0.1384 (2026-09-13)
+
+### 新功能
+- 菌群报告详情页重构 - 去掉疾病风险手风琴和病原菌入口,修复报告日期显示,新增报告总结描述
+
+
+## v1.0.1383 (2026-09-13)
+
+### 文档
+- add vendor order review spec, plan, and API reference entries
+- 登记DOA接口+统一能登录/不能登录用户口径规范
+
+### 重构
+- clean up review logic (remove unused var, fix computed, add log separators)
+
+### 新功能
+- add vendor order review page and entry
+- add vendorId to order detail response for permission check
+- 新增doa-stage-detail详情页+实体memberUserId字段
+- 新增doa-week-edit周计划复盘编辑页
+- 新增doa-stage-create创建阶段页
+- 新增doa-index家庭总览页
+- 前端API封装+页面注册+挑战页双tab入口
+- 新增DoaController(/api/doa 七接口)
+- 新增DoaService(创建阶段/家庭总览/详情/周计划/周复盘)
+- DoaStage新增memberUserId非表字段(为Task8代管详情准备)
+- 新增DoaStage/DoaGoal/DoaWeek实体与Mapper
+- 迁移314创建DOA三表(doa_stage/doa_goal/doa_week)
+
+### Bug 修复
+- 修复远程预存编译错误(BasicQuotaService/Collections import缺失)+ 迁移318花括号对齐
+- 挑战页双tab改用doa-前缀类名避免类名冲突
+- checkStageMemberPermission 空指针绕过同源修复
+- 修复代管校验空指针绕过与 weekStart Locale 依赖
+
+### 其他
+-    原条件直接跳过校验,非家庭成员可为任何成员(含可登录成员)创建阶段。
+-    收紧为 isSelf = selfMemberId != null && equals(memberId),null 视为非本人,
+-    不可代管可登录成员。
+- 2. weekStart():补 setFirstDayOfWeek(MONDAY),避免 en_US/C 等周日起始
+-    Locale 下 set(DAY_OF_WEEK, MONDAY) 使周日滚到下周,导致 week1 起点
+-    晚于 stage.startDate。
+- 
+
+
+## v1.0.1382 (2026-09-12)
+
+### 新功能
+- BASIC体验会员 (9.9元/月) 核心实现
+
+### 其他
+- - 后端: BasicQuotaService/ActivationQuotaService + 4业务入口配额扣减
+- - 定时任务: BasicQuotaScheduledTasks(月度刷新+到期提醒)
+- - Controller: BasicQuotaController + AdminBasicQuotaController
+- - 前端: QuotaProgressBar/UpgradeGuideModal组件 + 会员页/升级页BASIC支持
+- - AI对话页集成配额显示和软引导
+- 
+- plan: BASIC体验会员实施计划(9.9元/月)
+
+
+## v1.0.1381 (2026-09-12)
+
+### Bug 修复
+- 免费用户生成方案付费墙改为家庭会员或购买健康启航计划二选一,前端弹三选一引导购买后自动继续生成
+
+
 ## v1.0.1380 (2026-09-12)
 ## v1.0.1380 (2026-09-12)
 
 
 ### 新功能
 ### 新功能

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

@@ -1,6 +1,6 @@
 # 更新日志
 # 更新日志
 
 
-> 当前版本: v1.0.1380
+> 当前版本: v1.0.1386
 
 
 ## 历史版本
 ## 历史版本
 
 
@@ -8,6 +8,84 @@
 
 
 ---
 ---
 
 
+## v1.0.1386 (2026-09-13)
+
+### Bug 修复
+- 修复 DatabaseInitializer git 冲突标记残留及预存注释行 try{ bug,编译失败
+
+
+## v1.0.1385 (2026-09-13)
+
+### 新功能
+- 菜单管理图标字段改为可视化图标选择器(282个Element图标+搜索+清除)
+
+
+## v1.0.1384 (2026-09-13)
+
+### 新功能
+- 菌群报告详情页重构 - 去掉疾病风险手风琴和病原菌入口,修复报告日期显示,新增报告总结描述
+
+
+## v1.0.1383 (2026-09-13)
+
+### 文档
+- add vendor order review spec, plan, and API reference entries
+- 登记DOA接口+统一能登录/不能登录用户口径规范
+
+### 重构
+- clean up review logic (remove unused var, fix computed, add log separators)
+
+### 新功能
+- add vendor order review page and entry
+- add vendorId to order detail response for permission check
+- 新增doa-stage-detail详情页+实体memberUserId字段
+- 新增doa-week-edit周计划复盘编辑页
+- 新增doa-stage-create创建阶段页
+- 新增doa-index家庭总览页
+- 前端API封装+页面注册+挑战页双tab入口
+- 新增DoaController(/api/doa 七接口)
+- 新增DoaService(创建阶段/家庭总览/详情/周计划/周复盘)
+- DoaStage新增memberUserId非表字段(为Task8代管详情准备)
+- 新增DoaStage/DoaGoal/DoaWeek实体与Mapper
+- 迁移314创建DOA三表(doa_stage/doa_goal/doa_week)
+
+### Bug 修复
+- 修复远程预存编译错误(BasicQuotaService/Collections import缺失)+ 迁移318花括号对齐
+- 挑战页双tab改用doa-前缀类名避免类名冲突
+- checkStageMemberPermission 空指针绕过同源修复
+- 修复代管校验空指针绕过与 weekStart Locale 依赖
+
+### 其他
+-    原条件直接跳过校验,非家庭成员可为任何成员(含可登录成员)创建阶段。
+-    收紧为 isSelf = selfMemberId != null && equals(memberId),null 视为非本人,
+-    不可代管可登录成员。
+- 2. weekStart():补 setFirstDayOfWeek(MONDAY),避免 en_US/C 等周日起始
+-    Locale 下 set(DAY_OF_WEEK, MONDAY) 使周日滚到下周,导致 week1 起点
+-    晚于 stage.startDate。
+- 
+
+
+## v1.0.1382 (2026-09-12)
+
+### 新功能
+- BASIC体验会员 (9.9元/月) 核心实现
+
+### 其他
+- - 后端: BasicQuotaService/ActivationQuotaService + 4业务入口配额扣减
+- - 定时任务: BasicQuotaScheduledTasks(月度刷新+到期提醒)
+- - Controller: BasicQuotaController + AdminBasicQuotaController
+- - 前端: QuotaProgressBar/UpgradeGuideModal组件 + 会员页/升级页BASIC支持
+- - AI对话页集成配额显示和软引导
+- 
+- plan: BASIC体验会员实施计划(9.9元/月)
+
+
+## v1.0.1381 (2026-09-12)
+
+### Bug 修复
+- 免费用户生成方案付费墙改为家庭会员或购买健康启航计划二选一,前端弹三选一引导购买后自动继续生成
+
+
 ## v1.0.1380 (2026-09-12)
 ## v1.0.1380 (2026-09-12)
 
 
 ### 新功能
 ### 新功能

+ 286 - 0
cfc-web/src/utils/element-icons.js

@@ -0,0 +1,286 @@
+// Element UI 2.x 全量图标清单(来源于 element-ui icons.css 实测提取)
+// 供菜单管理图标选择器使用;格式保持 "el-icon-xxx" 与现有存储/渲染兼容
+export const ELEMENT_ICONS = [
+  "el-icon-add-location",
+  "el-icon-aim",
+  "el-icon-alarm-clock",
+  "el-icon-apple",
+  "el-icon-arrow-down",
+  "el-icon-arrow-left",
+  "el-icon-arrow-right",
+  "el-icon-arrow-up",
+  "el-icon-attract",
+  "el-icon-back",
+  "el-icon-bangzhu",
+  "el-icon-bank-card",
+  "el-icon-baseball",
+  "el-icon-basketball",
+  "el-icon-bell",
+  "el-icon-bicycle",
+  "el-icon-bottom",
+  "el-icon-bottom-left",
+  "el-icon-bottom-right",
+  "el-icon-box",
+  "el-icon-brush",
+  "el-icon-burger",
+  "el-icon-camera",
+  "el-icon-camera-solid",
+  "el-icon-caret-bottom",
+  "el-icon-caret-left",
+  "el-icon-caret-right",
+  "el-icon-caret-top",
+  "el-icon-chat-dot-round",
+  "el-icon-chat-dot-square",
+  "el-icon-chat-line-round",
+  "el-icon-chat-line-square",
+  "el-icon-chat-round",
+  "el-icon-chat-square",
+  "el-icon-check",
+  "el-icon-cherry",
+  "el-icon-chicken",
+  "el-icon-circle-check",
+  "el-icon-circle-close",
+  "el-icon-circle-plus",
+  "el-icon-circle-plus-outline",
+  "el-icon-close",
+  "el-icon-close-notification",
+  "el-icon-close-tip",
+  "el-icon-cloudy",
+  "el-icon-cloudy-and-sunny",
+  "el-icon-coffee",
+  "el-icon-coffee-cup",
+  "el-icon-coin",
+  "el-icon-cold-drink",
+  "el-icon-collection",
+  "el-icon-collection-tag",
+  "el-icon-connection",
+  "el-icon-coordinate",
+  "el-icon-copy-document",
+  "el-icon-cpu",
+  "el-icon-crop",
+  "el-icon-c-scale-to-original",
+  "el-icon-d-arrow-left",
+  "el-icon-d-arrow-right",
+  "el-icon-data-analysis",
+  "el-icon-data-board",
+  "el-icon-data-line",
+  "el-icon-date",
+  "el-icon-d-caret",
+  "el-icon-delete",
+  "el-icon-delete-location",
+  "el-icon-delete-solid",
+  "el-icon-dessert",
+  "el-icon-discount",
+  "el-icon-discover",
+  "el-icon-dish",
+  "el-icon-dish-1",
+  "el-icon-document",
+  "el-icon-document-add",
+  "el-icon-document-checked",
+  "el-icon-document-copy",
+  "el-icon-document-delete",
+  "el-icon-document-remove",
+  "el-icon-download",
+  "el-icon-edit",
+  "el-icon-edit-outline",
+  "el-icon-eleme",
+  "el-icon-error",
+  "el-icon-female",
+  "el-icon-files",
+  "el-icon-film",
+  "el-icon-finished",
+  "el-icon-first-aid-kit",
+  "el-icon-folder",
+  "el-icon-folder-add",
+  "el-icon-folder-checked",
+  "el-icon-folder-delete",
+  "el-icon-folder-opened",
+  "el-icon-folder-remove",
+  "el-icon-food",
+  "el-icon-football",
+  "el-icon-fork-spoon",
+  "el-icon-full-screen",
+  "el-icon-goblet",
+  "el-icon-goblet-full",
+  "el-icon-goblet-square",
+  "el-icon-goblet-square-full",
+  "el-icon-goods",
+  "el-icon-grape",
+  "el-icon-guide",
+  "el-icon-headset",
+  "el-icon-heavy-rain",
+  "el-icon-help",
+  "el-icon-hot-water",
+  "el-icon-house",
+  "el-icon-ice-cream",
+  "el-icon-ice-cream-round",
+  "el-icon-ice-cream-square",
+  "el-icon-ice-drink",
+  "el-icon-ice-tea",
+  "el-icon-info",
+  "el-icon-key",
+  "el-icon-knife-fork",
+  "el-icon-lightning",
+  "el-icon-light-rain",
+  "el-icon-link",
+  "el-icon-loading",
+  "el-icon-location",
+  "el-icon-location-information",
+  "el-icon-location-outline",
+  "el-icon-lock",
+  "el-icon-lollipop",
+  "el-icon-magic-stick",
+  "el-icon-male",
+  "el-icon-map-location",
+  "el-icon-medal",
+  "el-icon-medal-1",
+  "el-icon-menu",
+  "el-icon-message",
+  "el-icon-message-solid",
+  "el-icon-mic",
+  "el-icon-microphone",
+  "el-icon-milk-tea",
+  "el-icon-minus",
+  "el-icon-mobile",
+  "el-icon-mobile-phone",
+  "el-icon-money",
+  "el-icon-monitor",
+  "el-icon-moon",
+  "el-icon-moon-night",
+  "el-icon-more",
+  "el-icon-more-outline",
+  "el-icon-mouse",
+  "el-icon-news",
+  "el-icon-no-smoking",
+  "el-icon-notebook-1",
+  "el-icon-notebook-2",
+  "el-icon-odometer",
+  "el-icon-office-building",
+  "el-icon-open",
+  "el-icon-orange",
+  "el-icon-paperclip",
+  "el-icon-partly-cloudy",
+  "el-icon-pear",
+  "el-icon-phone",
+  "el-icon-phone-outline",
+  "el-icon-picture",
+  "el-icon-picture-outline",
+  "el-icon-picture-outline-round",
+  "el-icon-pie-chart",
+  "el-icon-place",
+  "el-icon-platform-eleme",
+  "el-icon-plus",
+  "el-icon-position",
+  "el-icon-postcard",
+  "el-icon-potato-strips",
+  "el-icon-present",
+  "el-icon-price-tag",
+  "el-icon-printer",
+  "el-icon-question",
+  "el-icon-rank",
+  "el-icon-reading",
+  "el-icon-receiving",
+  "el-icon-refresh",
+  "el-icon-refresh-left",
+  "el-icon-refresh-right",
+  "el-icon-refrigerator",
+  "el-icon-remove",
+  "el-icon-remove-outline",
+  "el-icon-right",
+  "el-icon-s-check",
+  "el-icon-school",
+  "el-icon-scissors",
+  "el-icon-s-claim",
+  "el-icon-s-comment",
+  "el-icon-s-cooperation",
+  "el-icon-s-custom",
+  "el-icon-s-data",
+  "el-icon-search",
+  "el-icon-sell",
+  "el-icon-service",
+  "el-icon-setting",
+  "el-icon-set-up",
+  "el-icon-s-finance",
+  "el-icon-s-flag",
+  "el-icon-s-fold",
+  "el-icon-s-goods",
+  "el-icon-s-grid",
+  "el-icon-share",
+  "el-icon-s-help",
+  "el-icon-ship",
+  "el-icon-s-home",
+  "el-icon-shopping-bag-1",
+  "el-icon-shopping-bag-2",
+  "el-icon-shopping-cart-1",
+  "el-icon-shopping-cart-2",
+  "el-icon-shopping-cart-full",
+  "el-icon-s-management",
+  "el-icon-s-marketing",
+  "el-icon-smoking",
+  "el-icon-soccer",
+  "el-icon-sold-out",
+  "el-icon-s-open",
+  "el-icon-s-operation",
+  "el-icon-s-opportunity",
+  "el-icon-s-order",
+  "el-icon-sort",
+  "el-icon-sort-down",
+  "el-icon-sort-up",
+  "el-icon-s-platform",
+  "el-icon-s-promotion",
+  "el-icon-s-release",
+  "el-icon-s-shop",
+  "el-icon-star-off",
+  "el-icon-star-on",
+  "el-icon-s-ticket",
+  "el-icon-s-tools",
+  "el-icon-stopwatch",
+  "el-icon-success",
+  "el-icon-sugar",
+  "el-icon-suitcase",
+  "el-icon-suitcase-1",
+  "el-icon-s-unfold",
+  "el-icon-sunny",
+  "el-icon-sunrise",
+  "el-icon-sunrise-1",
+  "el-icon-sunset",
+  "el-icon-switch-button",
+  "el-icon-table-lamp",
+  "el-icon-tableware",
+  "el-icon-takeaway-box",
+  "el-icon-thumb",
+  "el-icon-tickets",
+  "el-icon-time",
+  "el-icon-timer",
+  "el-icon-toilet-paper",
+  "el-icon-top",
+  "el-icon-top-left",
+  "el-icon-top-right",
+  "el-icon-trophy",
+  "el-icon-trophy-1",
+  "el-icon-truck",
+  "el-icon-turn-off",
+  "el-icon-turn-off-microphone",
+  "el-icon-umbrella",
+  "el-icon-unlock",
+  "el-icon-upload",
+  "el-icon-upload2",
+  "el-icon-upload-success",
+  "el-icon-user",
+  "el-icon-user-solid",
+  "el-icon-video-camera",
+  "el-icon-video-camera-solid",
+  "el-icon-video-pause",
+  "el-icon-video-play",
+  "el-icon-view",
+  "el-icon-wallet",
+  "el-icon-warning",
+  "el-icon-warning-outline",
+  "el-icon-watch",
+  "el-icon-watch-1",
+  "el-icon-water-cup",
+  "el-icon-watermelon",
+  "el-icon-wind-power",
+  "el-icon-zoom-in",
+  "el-icon-zoom-out"
+]

+ 159 - 1
cfc-web/src/views/admin/MenuManage.vue

@@ -105,7 +105,65 @@
           <el-input v-model="menuForm.path" placeholder="/xxx"></el-input>
           <el-input v-model="menuForm.path" placeholder="/xxx"></el-input>
         </el-form-item>
         </el-form-item>
         <el-form-item label="图标">
         <el-form-item label="图标">
-          <el-input v-model="menuForm.icon" placeholder="el-icon-s-custom"></el-input>
+          <el-popover
+            v-model="iconPickerVisible"
+            placement="bottom-start"
+            width="520"
+            trigger="click"
+            popper-class="menu-icon-picker"
+          >
+            <div class="icon-picker-panel">
+              <div class="icon-picker-search">
+                <el-input
+                  v-model="iconSearch"
+                  size="small"
+                  prefix-icon="el-icon-search"
+                  placeholder="搜索图标(如:setting / user)"
+                  clearable
+                ></el-input>
+                <el-button size="small" @click="clearIcon">清除</el-button>
+              </div>
+              <div class="icon-picker-grid">
+                <div
+                  v-for="icon in filteredIcons"
+                  :key="icon"
+                  class="icon-picker-item"
+                  :class="{ 'is-active': menuForm.icon === icon }"
+                  :title="icon"
+                  @click="selectIcon(icon)"
+                >
+                  <i :class="icon"></i>
+                  <span class="icon-picker-item-name">{{ iconName(icon) }}</span>
+                </div>
+                <el-empty
+                  v-if="!filteredIcons.length"
+                  description="未找到匹配的图标"
+                  :image-size="60"
+                ></el-empty>
+              </div>
+            </div>
+            <div slot="reference" class="icon-picker-trigger">
+              <el-input
+                :value="menuForm.icon"
+                readonly
+                placeholder="点击选择图标"
+                class="icon-picker-input"
+              >
+                <i
+                  v-if="menuForm.icon"
+                  slot="prefix"
+                  :class="menuForm.icon"
+                  class="icon-picker-input-preview"
+                ></i>
+                <i
+                  v-if="menuForm.icon"
+                  slot="suffix"
+                  class="el-input__icon el-icon-close icon-picker-clear-btn"
+                  @click.stop="clearIcon"
+                ></i>
+              </el-input>
+            </div>
+          </el-popover>
         </el-form-item>
         </el-form-item>
         <el-form-item label="权限标识">
         <el-form-item label="权限标识">
           <el-input v-model="menuForm.perm" placeholder="system:menu"></el-input>
           <el-input v-model="menuForm.perm" placeholder="system:menu"></el-input>
@@ -223,6 +281,7 @@ import {
   getMenuTree, getRoleMenus, saveRoleMenus,
   getMenuTree, getRoleMenus, saveRoleMenus,
   createMenu, updateMenu, deleteMenu, moveMenu
   createMenu, updateMenu, deleteMenu, moveMenu
 } from '@/api/menu'
 } from '@/api/menu'
+import { ELEMENT_ICONS } from '@/utils/element-icons'
 
 
 const ROLES = [
 const ROLES = [
   { label: '超级管理员', value: 'admin' },
   { label: '超级管理员', value: 'admin' },
@@ -247,6 +306,9 @@ export default {
       submitting: false,
       submitting: false,
       menuForm: this.getEmptyForm(),
       menuForm: this.getEmptyForm(),
       menuRules: {},
       menuRules: {},
+      // 图标选择器
+      iconPickerVisible: false,
+      iconSearch: '',
       // 授权弹窗
       // 授权弹窗
       authDialogVisible: false,
       authDialogVisible: false,
       authTab: 'byRole',
       authTab: 'byRole',
@@ -352,6 +414,16 @@ export default {
       const node = findNode(this.menuTree, this.menuForm.id)
       const node = findNode(this.menuTree, this.menuForm.id)
       if (node && node.children) collect(node.children)
       if (node && node.children) collect(node.children)
       return ids
       return ids
+    },
+    // 图标选择器:按关键字过滤(支持不带 el-icon- 前缀搜索)
+    filteredIcons() {
+      const kw = (this.iconSearch || '').trim().toLowerCase()
+      if (!kw) return ELEMENT_ICONS
+      const prefix = 'el-icon-'
+      return ELEMENT_ICONS.filter(icon => {
+        const plain = icon.startsWith(prefix) ? icon.slice(prefix.length) : icon
+        return plain.toLowerCase().includes(kw) || icon.toLowerCase().includes(kw)
+      })
     }
     }
   },
   },
   created() {
   created() {
@@ -401,6 +473,18 @@ export default {
     selectParentNode(data) {
     selectParentNode(data) {
       this.menuForm.parentId = data.id
       this.menuForm.parentId = data.id
     },
     },
+    // === 图标选择器 ===
+    selectIcon(icon) {
+      this.menuForm.icon = icon
+      this.iconPickerVisible = false
+    },
+    clearIcon() {
+      this.menuForm.icon = ''
+      this.iconSearch = ''
+    },
+    iconName(icon) {
+      return icon.replace(/^el-icon-/, '')
+    },
     handleCreate(parentId) {
     handleCreate(parentId) {
       this.isEdit = false
       this.isEdit = false
       this.menuForm = { ...this.getEmptyForm(), parentId }
       this.menuForm = { ...this.getEmptyForm(), parentId }
@@ -754,4 +838,78 @@ export default {
 .custom-tree-node .el-tree-node__content {
 .custom-tree-node .el-tree-node__content {
   height: 32px;
   height: 32px;
 }
 }
+
+/* === 图标选择器 === */
+.icon-picker-trigger {
+  width: 100%;
+}
+.icon-picker-input-preview {
+  font-size: 16px;
+  line-height: 40px;
+}
+.icon-picker-clear-btn {
+  cursor: pointer;
+  color: #909399;
+}
+.icon-picker-clear-btn:hover {
+  color: #f56c6c;
+}
+.icon-picker-panel {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+.icon-picker-search {
+  display: flex;
+  gap: 8px;
+}
+.icon-picker-search .el-input {
+  flex: 1;
+}
+.icon-picker-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
+  gap: 6px;
+  max-height: 280px;
+  overflow-y: auto;
+  padding: 4px;
+}
+.icon-picker-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 4px;
+  padding: 8px 4px;
+  border: 1px solid #ebeef5;
+  border-radius: 6px;
+  cursor: pointer;
+  transition: all 0.15s;
+}
+.icon-picker-item i {
+  font-size: 20px;
+  color: #606266;
+}
+.icon-picker-item:hover {
+  border-color: #409eff;
+  background: #ecf5ff;
+}
+.icon-picker-item:hover i {
+  color: #409eff;
+}
+.icon-picker-item.is-active {
+  border-color: #409eff;
+  background: #ecf5ff;
+}
+.icon-picker-item.is-active i {
+  color: #409eff;
+}
+.icon-picker-item-name {
+  font-size: 10px;
+  color: #909399;
+  max-width: 100%;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
 </style>
 </style>

+ 4 - 1
docs/superpowers/PROJECT-OVERVIEW.md

@@ -200,6 +200,7 @@
 | SKU 价格单位统一分 + 规格选择响应式修复 | Phase 4 | ✅ 已实施(7 Tasks:实体/DTO/订单/购物车/迁移/管理端/小程序,5 commits) | `plans/2026-08-15-sku-price-cents-and-spec-selector.md` |
 | SKU 价格单位统一分 + 规格选择响应式修复 | Phase 4 | ✅ 已实施(7 Tasks:实体/DTO/订单/购物车/迁移/管理端/小程序,5 commits) | `plans/2026-08-15-sku-price-cents-and-spec-selector.md` |
 | 库存管理(入库单+流水+盘点+预警+套餐拆库存) | Phase 4 | 🟡 设计稿 | `specs/2026-08-05-inventory-management-design.md` |
 | 库存管理(入库单+流水+盘点+预警+套餐拆库存) | Phase 4 | 🟡 设计稿 | `specs/2026-08-05-inventory-management-design.md` |
 | 商品可选赠品(按会员等级配置赠品池:赠品商品/CF值,购买时可多选) | Phase 4 | 🟡 设计稿 | `specs/2026-09-02-product-gift-selection-design.md` |
 | 商品可选赠品(按会员等级配置赠品池:赠品商品/CF值,购买时可多选) | Phase 4 | 🟡 设计稿 | `specs/2026-09-02-product-gift-selection-design.md` |
+| 供应商订单审核(修改价格/配送方式/添加优惠券/赠品) | Phase 4 | ✅ 已实施(后端 review/update 接口 + 前端 order-review 页面 + 列表审核入口,3 commits) | `specs/2026-09-12-vendor-order-review-design.md` + `plans/2026-09-12-vendor-order-review.md` |
 
 
 ---
 ---
 
 
@@ -372,7 +373,7 @@
 | `2026-08-31-self-check-ai-integration-design.md` | 🟡 设计稿(P0-1 纯AI替换静态建议 + P0-2 用户点击生成健康计划 + P1-1 历史趋势AI解读 + P1-2 Chat上下文注入,P2延后) | 五维家庭自检 AI 结合设计 |
 | `2026-08-31-self-check-ai-integration-design.md` | 🟡 设计稿(P0-1 纯AI替换静态建议 + P0-2 用户点击生成健康计划 + P1-1 历史趋势AI解读 + P1-2 Chat上下文注入,P2延后) | 五维家庭自检 AI 结合设计 |
 | `2026-09-09-team-hierarchy-design.md` | 🟡 设计稿(团队层级树从下往上生长 + 团队消费额分级统计 + 直推成员消费/CF明细 + 我的推荐人展示 + 去掉富页面推荐码) | 团队层级与推荐关系可视化 |
 | `2026-09-09-team-hierarchy-design.md` | 🟡 设计稿(团队层级树从下往上生长 + 团队消费额分级统计 + 直推成员消费/CF明细 + 我的推荐人展示 + 去掉富页面推荐码) | 团队层级与推荐关系可视化 |
 | `2026-09-10-report-quick-analyze-design.md` | 🟡 设计稿(报告详情页快速分析:免费版每日 1 次 Redis 计数 + 付费版多轮;复用 chat_graph + AiGateway,LangGraph 零改动) | 报告快速分析设计 |
 | `2026-09-10-report-quick-analyze-design.md` | 🟡 设计稿(报告详情页快速分析:免费版每日 1 次 Redis 计数 + 付费版多轮;复用 chat_graph + AiGateway,LangGraph 零改动) | 报告快速分析设计 |
-| `api/API_REFERENCE.md` | 🟢 已建立(2026-08-18;200+ 接口清单;废弃接口标注;新增接口检查流程;2026-09-06 补充家庭成员切换/回收箱接口:`/leave`、`/kick` 回收箱分流、`/recycle`;2026-09-10 补充关系三图接口:`/api/octopus`、`/api/pearl`、`/api/ability`) | 后台接口参考文档 |
+| `api/API_REFERENCE.md` | 🟢 已建立(2026-08-18;200+ 接口清单;废弃接口标注;新增接口检查流程;2026-09-06 补充家庭成员切换/回收箱接口:`/leave`、`/kick` 回收箱分流、`/recycle`;2026-09-10 补充关系三图接口:`/api/octopus`、`/api/pearl`、`/api/ability`;2026-09-12 补充供应商审核接口:`/api/product/order/review/update`) | 后台接口参考文档 |
 | `2026-09-10-key-person-expansion-octopus-design.md` | 🟡 设计规格已确认(成效对象=会员客户 + 类型bitmask + 关键人画像 + 四步法 + 高频词≤8方向;3表/10接口/三层画布;替换旧章鱼图) | 关键人拓展章鱼图替换设计(关系三图重构) |
 | `2026-09-10-key-person-expansion-octopus-design.md` | 🟡 设计规格已确认(成效对象=会员客户 + 类型bitmask + 关键人画像 + 四步法 + 高频词≤8方向;3表/10接口/三层画布;替换旧章鱼图) | 关键人拓展章鱼图替换设计(关系三图重构) |
 
 
 ### 实施计划(plans/)
 ### 实施计划(plans/)
@@ -453,6 +454,7 @@
 | `2026-08-30-dan-report-type-specific-blocks.md` | 🟢 已实施(4 Tasks:assembleDanByType分组/详情重算blocks/前端复用渲染器/文档) | DAN 报告按类型分别处理展示实施计划(A1/A2/B2/B3/B4/B5/B6/C1 八种类型指标分组展示,历史数据生效) |
 | `2026-08-30-dan-report-type-specific-blocks.md` | 🟢 已实施(4 Tasks:assembleDanByType分组/详情重算blocks/前端复用渲染器/文档) | DAN 报告按类型分别处理展示实施计划(A1/A2/B2/B3/B4/B5/B6/C1 八种类型指标分组展示,历史数据生效) |
 | `2026-08-31-self-check-ai-integration.md` | 🟡 待实施(11 Tasks:LangGraph×2 graph + 路由 / Java Service+AiGateway+Controller / 前端结果页+中间页+Chat 注入) | 五维自检 AI 结合 P0+P1 实现计划(设计稿:2026-08-31-self-check-ai-integration-design.md) |
 | `2026-08-31-self-check-ai-integration.md` | 🟡 待实施(11 Tasks:LangGraph×2 graph + 路由 / Java Service+AiGateway+Controller / 前端结果页+中间页+Chat 注入) | 五维自检 AI 结合 P0+P1 实现计划(设计稿:2026-08-31-self-check-ai-integration-design.md) |
 | `2026-09-04-task-system-unification.md` | 🟢 已实施(9 Tasks:tasks表加字段/模板加onboarding_sequence/DTO透传/GrowthTask迁入TaskService/Onboarding复用模板/旧表迁移/旧接口兼容/前端统一任务中心/收尾) | 统一任务系统实施计划(设计稿:2026-09-04-task-system-unification-design.md) |
 | `2026-09-04-task-system-unification.md` | 🟢 已实施(9 Tasks:tasks表加字段/模板加onboarding_sequence/DTO透传/GrowthTask迁入TaskService/Onboarding复用模板/旧表迁移/旧接口兼容/前端统一任务中心/收尾) | 统一任务系统实施计划(设计稿:2026-09-04-task-system-unification-design.md) |
+| `2026-09-12-vendor-order-review.md` | ✅ 已实施(5 Tasks:后端 review/update 接口 + 临时券/赠品/配送校验 + 前端 order-review 页面 + 列表入口 + vendorId 详情返回) | 供应商订单审核实施计划(设计稿:2026-09-12-vendor-order-review-design.md) |
 
 
 ### 计划与设计文档(specs/)
 ### 计划与设计文档(specs/)
 
 
@@ -467,6 +469,7 @@
 | `2026-07-30-jia-suanfa-webnovel-design.md` | 🟡 设计完成 | 假算法题材网络小说设计(book2 章节库) |
 | `2026-07-30-jia-suanfa-webnovel-design.md` | 🟡 设计完成 | 假算法题材网络小说设计(book2 章节库) |
 | `2026-09-04-task-system-unification-design.md` | 🟢 已实施 | 统一任务系统设计(多套任务体系合并为单表 tasks,source_type 区分 9 类来源;奖励自动发放;复用 RepeatTaskGenerator 每日重置;前端统一任务中心;家庭挑战与五维打卡本次不合并;实施计划:2026-09-04-task-system-unification.md) |
 | `2026-09-04-task-system-unification-design.md` | 🟢 已实施 | 统一任务系统设计(多套任务体系合并为单表 tasks,source_type 区分 9 类来源;奖励自动发放;复用 RepeatTaskGenerator 每日重置;前端统一任务中心;家庭挑战与五维打卡本次不合并;实施计划:2026-09-04-task-system-unification.md) |
 | `2026-09-12-doa-family-goal-design.md` | 🟢 设计完成 | DOA 家庭打卡个人目标-周计划-周复盘设计(并列共同挑战新模块,5 领域/自选周期/家人可见/代管不能登录成员;设计稿:2026-09-12-doa-family-goal-design.md) |
 | `2026-09-12-doa-family-goal-design.md` | 🟢 设计完成 | DOA 家庭打卡个人目标-周计划-周复盘设计(并列共同挑战新模块,5 领域/自选周期/家人可见/代管不能登录成员;设计稿:2026-09-12-doa-family-goal-design.md) |
+| `2026-09-12-vendor-order-review-design.md` | ✅ 已实施 | 供应商订单审核设计(pending 订单可修改价格/配送方式/添加临时优惠券/赠品,自动重算金额) |
 
 
 ---
 ---
 
 

+ 18 - 1
docs/superpowers/api/API_REFERENCE.md

@@ -599,6 +599,9 @@ find cfc-backend/src/main/java -name "*XxxService.java" -o -name "*XxxController
 | `POST /api/product/order/detail` | 订单详情 |
 | `POST /api/product/order/detail` | 订单详情 |
 | `POST /api/product/order/confirm` | 确认收货 |
 | `POST /api/product/order/confirm` | 确认收货 |
 | `POST /api/product/order/refund/apply` | 申请退款 |
 | `POST /api/product/order/refund/apply` | 申请退款 |
+| `POST /api/product/order/vendor` | 服务商查询收到的订单 |
+| `POST /api/product/order/vendor-confirm` | 服务商确认发出/服务完成 |
+| `POST /api/product/order/review/update` | 供应商审核修改待支付订单(修改价格/配送方式/添加优惠券/赠品) |
 | `POST /api/cart/add` | 加入购物车 |
 | `POST /api/cart/add` | 加入购物车 |
 | `POST /api/cart/list` | 购物车列表 |
 | `POST /api/cart/list` | 购物车列表 |
 | `POST /api/cart/update` | 更新数量 |
 | `POST /api/cart/update` | 更新数量 |
@@ -1196,6 +1199,20 @@ interactionType 取值:`PHONE` / `WECHAT` / `MEETING` / `OTHER`。
 
 
 > **已记录关键人拓展 API 端点 — 禁止重复注册。**
 > **已记录关键人拓展 API 端点 — 禁止重复注册。**
 
 
+### 4.41 DOA 个人目标(家庭打卡个人目标模块)
+
+| 路径 | 说明 |
+|------|------|
+| `POST /api/doa/stage/create` | 创建阶段(memberId?+title+durationWeeks+goals[1-3]) |
+| `POST /api/doa/overview` | 家庭总览:各成员当前 active 阶段 |
+| `POST /api/doa/stage/detail` | 阶段详情:stage + goals + weeks |
+| `POST /api/doa/stage/cancel` | 取消阶段(仅 active 可取消) |
+| `POST /api/doa/week/plan` | 提交周计划(A表周行动宣告) |
+| `POST /api/doa/week/review` | 提交周复盘(B表五问) |
+| `POST /api/doa/week/list` | 周记录列表(按阶段查周记录) |
+
+> **已记录 DOA 个人目标 API 端点 — 禁止重复注册。**
+
 ## 六、待清理的废弃接口
 ## 六、待清理的废弃接口
 
 
 | Controller | 废弃接口 | 替代方案 | 当前状态 | 清理条件 |
 | Controller | 废弃接口 | 替代方案 | 当前状态 | 清理条件 |
@@ -1265,4 +1282,4 @@ interactionType 取值:`PHONE` / `WECHAT` / `MEETING` / `OTHER`。
 
 
 ---
 ---
 
 
-*文档最后更新:2026-09-09*
+*文档最后更新:2026-09-13*

+ 1680 - 0
docs/superpowers/plans/2026-09-10-basic-membership-plan.md

@@ -0,0 +1,1680 @@
+# BASIC 体验会员实施计划
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 在现有会员体系(FREE→FAMILY→PREMIUM→PROVIDER)中新增 BASIC 等级,实现 9.9 元/月体验会员的付费开通、配额扣减、软/硬引导升级链路。
+
+**Architecture:** 基于现有 `MembershipLevel` / `FamilyMembership` / `TrialMembership` 三层会员记录模型,新增 `basic_quota_usage` 表管理月度配额。`BasicQuotaService` 作为独立 Service 承载配额逻辑,在 4 个业务入口(AI 对话、报告解读、测评预约、报告上传)插入配额检查。定时任务复用 `MembershipScheduledTasks` 扩展模式。
+
+**Tech Stack:** Spring Boot 2.7.18, MyBatis-Plus, Java 8, Maven, 微信小程序 Vue2
+
+**Spec:** `docs/superpowers/specs/2026-09-10-basic-membership-design.md`
+
+## Global Constraints
+
+- 所有接口统一 `@PostMapping`,禁止 `@GetMapping`/`@PutMapping`/`@DeleteMapping`
+- ORM 使用 MyBatis-Plus `@TableName` + `@TableId(type = IdType.AUTO)`
+- DI 使用 `@Resource`,字段名必须与类型默认 Bean Name 一致
+- 响应统一 `Result<T>`(code/message/data)
+- 禁用 `?.`(用 `&&` 替代)、禁用 CSS Grid(用 flexbox)、禁用 `:key` 表达式
+- 前端禁止直接 `new Date(string)`,用 `utils/format.js` 的 `parseDate()`
+- 所有迁移幂等(try-catch 忽略已存在异常)
+- `mvn clean compile` 是唯一验证方式(编译通过即视为验证)
+
+---
+
+## 文件结构
+
+| 文件路径 | 类型 | 职责 |
+|----------|------|------|
+| `entity/BasicQuotaUsage.java` | Create | BASIC 配额实体 |
+| `mapper/BasicQuotaUsageMapper.java` | Create | 配额 Mapper 接口 |
+| `dto/QuotaStatusDTO.java` | Create | 配额查询响应 DTO |
+| `dto/QuotaConsumeResult.java` | Create | 配额扣减结果 DTO |
+| `service/BasicQuotaService.java` | Create | 配额核心 Service(查询/扣减/刷新) |
+| `controller/BasicQuotaController.java` | Create | 配额查询 + 手动重置接口 |
+| `task/BasicQuotaScheduledTasks.java` | Create | 配额刷新 + 到期提醒定时任务 |
+| `service/MembershipService.java` | Modify | `canUseFeature()` 加 BASIC 分支;`downgradeExpiredMembers()` 支持 BASIC |
+| `service/AiGateway.java` | Modify | `chat()` 入口插入 BASIC 配额扣减 |
+| `service/ReportSummaryService.java` | Modify | `parseReport()` 入口插入配额扣减 |
+| `service/AssessmentAppointmentService.java` | Modify | `create()` 入口插入硬门禁配额检查 |
+| `service/DanReportUploadService.java` | Modify | `upload()` 入口插入配额检查 |
+| `service/ActivationQuotaService.java` | Create | 支付成功后初始化 4 类配额 |
+| `resources/schema.sql` | Modify | 新增 `basic_quota_usage` 表定义 + BASIC seed 数据 |
+| `config/DatabaseInitializer.java` | Modify | 新增 BASIC 等级 / 权益 / 表迁移 |
+| `cfc-frontend/pages/profile/membership/index.vue` | Modify | 展示 BASIC 权益卡片 |
+| `cfc-frontend/pages/profile/upgrade/index.vue` | Modify | 新增 9.9 元月付选项 |
+| `cfc-frontend/components/QuotaProgressBar.vue` | Create | 配额进度条组件 |
+| `cfc-frontend/components/UpgradeGuideModal.vue` | Create | 统一软/硬引导弹窗组件 |
+| `cfc-frontend/pages/ai-chat/index.vue` | Modify | 显示剩余次数 + 超限弹窗 |
+| `cfc-frontend/pages/assessment/appointment/index.vue` | Modify | 显示剩余次数 + 硬门禁弹窗 |
+| `cfc-frontend/pages/health/report-list.vue` | Modify | 显示剩余次数 + 软引导 |
+
+---
+
+## Phase 概述
+
+| Phase | 天数 | 范围 | 里程碑 |
+|-------|------|------|--------|
+| P0 核心链路 | 3 天 | DB 迁移 + BASIC 等级 + 支付开通 + 基础权限 | 9.9 元支付成功,BASIC 生效,能进沙盘/AI 对话 |
+| P1 配额系统 | 2 天 | `BasicQuotaService` + 4 个业务入口扣减 + 定时刷新 | 月度配额正确扣减/刷新,超限引导生效 |
+| P2 前端展示 | 2 天 | 权益页/开通页/进度条/弹窗 | 免费用户可见 BASIC,付费后正确显示剩余次数 |
+| P3 运营配套 | 2 天 | 定时提醒 + 管理端配额看板 + 手动重置接口 | 到期提醒自动发送,看板可查 |
+
+---
+
+## Task 1: 数据库迁移 — 新增表 + BASIC 种子数据
+
+**Files:**
+- Modify: `cfc-backend/src/main/resources/schema.sql` (末尾追加)
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` (末尾 `runMigrations()` 追加)
+- Test: 无单元测试(迁移幂等,`mvn compile` 验证)
+
+**Interfaces:**
+- Consumes: 无(数据库级操作)
+- Produces: `basic_quota_usage` 表;`membership_levels` 新增 BASIC 记录;`membership_benefits` 新增 5 条 BASIC 权益记录
+
+- [ ] **Step 1: schema.sql 末尾追加基本表定义 + seed 数据**
+
+在 `schema.sql` 末尾(OFFSET 5513 之后)追加以下内容:
+
+```sql
+-- ============================================================
+-- 新增 BASIC 体验会员等级种子数据(迁移150)
+-- ============================================================
+INSERT IGNORE INTO membership_levels 
+(level_code, level_name, level_desc, price_monthly, features, max_children, max_tasks_per_day) 
+VALUES (
+    'BASIC', 
+    '体验会员', 
+    '9.9元/月体验版,解锁核心功能,限次使用AI与报告解读',
+    990,
+    '["free_activities","free_courses","discount_purchase","invite_family","mascot_ai_lite","ai_report_reading_lite","assessment_quota_lite","health_report_upload_lite","growth_plan_lite","task_template_lite"]',
+    3,
+    3
+);
+
+-- ============================================================
+-- 新增 BASIC 会员权益种子数据(迁移151)
+-- ============================================================
+INSERT IGNORE INTO membership_benefits (level_code, benefit_code, benefit_name, benefit_desc, icon, bg_color, sort_order) VALUES
+('BASIC', 'AI_CHAT_LITE', 'AI健康顾问(限3次/月)', '每月3次AI健康顾问问答', '🤖', '#DBEAFE', 1),
+('BASIC', 'REPORT_READING_LITE', '报告解读(限1次/月)', '每月1次体检/测评报告AI解读', '📊', '#FCE7F3', 4),
+('BASIC', 'GROWTH_PLAN_LITE', '基础成长计划', '创建成长计划但模板有限', '📋', '#FEF3C7', 3),
+('BASIC', 'ASSESSMENT_QUOTA_LITE', '测评预约(限1次)', '每月可预约1次测评', '📝', '#E0E7FF', 8),
+('BASIC', 'REPORT_UPLOAD_LITE', '健康报告上传(限2份/月)', '每月可上传2份健康报告', '📄', '#FEF9C3', 9);
+
+-- ============================================================
+-- BASIC 会员月度配额使用记录表(迁移152)
+-- ============================================================
+CREATE TABLE IF NOT EXISTS basic_quota_usage (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    user_id BIGINT NOT NULL COMMENT '家长用户ID(非family_id)',
+    quota_type VARCHAR(30) NOT NULL COMMENT 'AI_CHAT/REPORT_READING/ASSESSMENT_QUOTA/REPORT_UPLOAD',
+    used_count INT DEFAULT 0 COMMENT '本月已用次数',
+    total_count INT NOT NULL COMMENT '月度总配额',
+    period_start DATE NOT NULL COMMENT '周期开始日期(每月1号)',
+    period_end DATE NOT NULL COMMENT '周期结束日期(月末)',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_user_type_period (user_id, quota_type, period_start),
+    INDEX idx_user_period (user_id, period_start)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='BASIC会员月度配额使用记录';
+```
+
+- [ ] **Step 2: DatabaseInitializer.java 追加迁移代码**
+
+在 `DatabaseInitializer.java` 的 `runMigrations()` 方法末尾(最新迁移编号之后)追加:
+
+```java
+// 迁移150: membership_levels 新增 BASIC 体验会员等级
+try {
+    jdbcTemplate.execute(
+        "INSERT IGNORE INTO membership_levels " +
+        "(level_code, level_name, level_desc, price_monthly, features, max_children, max_tasks_per_day) " +
+        "VALUES ('BASIC', '体验会员', " +
+        "'9.9元/月体验版,解锁核心功能,限次使用AI与报告解读', " +
+        "990, " +
+        "'[\"free_activities\",\"free_courses\",\"discount_purchase\",\"invite_family\",\"mascot_ai_lite\",\"ai_report_reading_lite\",\"assessment_quota_lite\",\"health_report_upload_lite\",\"growth_plan_lite\",\"task_template_lite\"]', " +
+        "3, 3)");
+    log.info("已初始化BASIC体验会员等级");
+} catch (Exception e) {
+    // 等级已存在,忽略
+}
+
+// 迁移151: membership_benefits 新增 BASIC 会员权益
+try {
+    jdbcTemplate.execute(
+        "INSERT IGNORE INTO membership_benefits " +
+        "(level_code, benefit_code, benefit_name, benefit_desc, icon, bg_color, sort_order) VALUES " +
+        "('BASIC', 'AI_CHAT_LITE', 'AI健康顾问(限3次/月)', '每月3次AI健康顾问问答', '🤖', '#DBEAFE', 1), " +
+        "('BASIC', 'REPORT_READING_LITE', '报告解读(限1次/月)', '每月1次体检/测评报告AI解读', '📊', '#FCE7F3', 4), " +
+        "('BASIC', 'GROWTH_PLAN_LITE', '基础成长计划', '创建成长计划但模板有限', '📋', '#FEF3C7', 3), " +
+        "('BASIC', 'ASSESSMENT_QUOTA_LITE', '测评预约(限1次)', '每月可预约1次测评', '📝', '#E0E7FF', 8), " +
+        "('BASIC', 'REPORT_UPLOAD_LITE', '健康报告上传(限2份/月)', '每月可上传2份健康报告', '📄', '#FEF9C3', 9)");
+    log.info("已初始化BASIC会员权益列表");
+} catch (Exception e) {
+    // 已存在,忽略
+}
+
+// 迁移152: 创建 basic_quota_usage 表
+try {
+    jdbcTemplate.execute(
+        "CREATE TABLE IF NOT EXISTS basic_quota_usage (" +
+        "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+        "user_id BIGINT NOT NULL COMMENT '家长用户ID', " +
+        "quota_type VARCHAR(30) NOT NULL COMMENT '配额类型', " +
+        "used_count INT DEFAULT 0 COMMENT '已用次数', " +
+        "total_count INT NOT NULL COMMENT '月度总配额', " +
+        "period_start DATE NOT NULL COMMENT '周期开始日期', " +
+        "period_end DATE NOT NULL COMMENT '周期结束日期', " +
+        "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+        "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+        "UNIQUE KEY uk_user_type_period (user_id, quota_type, period_start), " +
+        "INDEX idx_user_period (user_id, period_start) " +
+        ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='BASIC会员月度配额使用记录'");
+    log.info("已创建basic_quota_usage表");
+} catch (Exception e) {
+    // 表已存在,忽略
+}
+```
+
+- [ ] **Step 3: 编译验证**
+
+```bash
+mvn clean compile
+```
+
+Expected: BUILD SUCCESS,无编译错误。
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add cfc-backend/src/main/resources/schema.sql
+git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
+git commit -m "feat: 新增BASIC体验会员等级及配额表迁移(迁移150-152)"
+```
+
+---
+
+## Task 2: 新增 BasicQuotaUsage 实体 + Mapper
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/BasicQuotaUsage.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/mapper/BasicQuotaUsageMapper.java`
+- Test: 无(实体类无业务逻辑)
+
+**Interfaces:**
+- Consumes: 无
+- Produces: `BasicQuotaUsage` 实体(字段:id, userId, quotaType, usedCount, totalCount, periodStart, periodEnd, createdAt, updatedAt),`BasicQuotaUsageMapper` 接口(继承 `BaseMapper<BasicQuotaUsage>`)
+
+- [ ] **Step 1: 创建 BasicQuotaUsage.java**
+
+```java
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("basic_quota_usage")
+public class BasicQuotaUsage implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long userId;
+
+    private String quotaType;
+
+    private Integer usedCount;
+
+    private Integer totalCount;
+
+    private Date periodStart;
+
+    private Date periodEnd;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}
+```
+
+- [ ] **Step 2: 创建 BasicQuotaUsageMapper.java**
+
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.BasicQuotaUsage;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface BasicQuotaUsageMapper extends BaseMapper<BasicQuotaUsage> {
+}
+```
+
+- [ ] **Step 3: 编译验证**
+
+```bash
+mvn clean compile
+```
+
+Expected: BUILD SUCCESS。
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/entity/BasicQuotaUsage.java
+git add cfc-backend/src/main/java/com/etotem/cfc/mapper/BasicQuotaUsageMapper.java
+git commit -m "feat: 新增BasicQuotaUsage实体和Mapper"
+```
+
+---
+
+## Task 3: 新增 QuotaStatusDTO + QuotaConsumeResult DTO
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/dto/QuotaStatusDTO.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/dto/QuotaConsumeResult.java`
+- Test: 无(纯 POJO)
+
+**Interfaces:**
+- Consumes: 无
+- Produces: `QuotaStatusDTO`(字段:quotaType, total, used, remaining, periodStart, periodEnd);`QuotaConsumeResult`(字段:success, remaining, toastMsg, actionUrl)
+
+- [ ] **Step 1: 创建 QuotaStatusDTO.java**
+
+```java
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+import java.util.Date;
+
+@Data
+public class QuotaStatusDTO {
+    private String quotaType;
+    private int total;
+    private int used;
+    private int remaining;
+    private Date periodStart;
+    private Date periodEnd;
+}
+```
+
+- [ ] **Step 2: 创建 QuotaConsumeResult.java**
+
+```java
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+@Data
+public class QuotaConsumeResult {
+    private boolean success;
+    private int remaining;
+    private String toastMsg;
+    private String actionUrl;
+}
+```
+
+- [ ] **Step 3: 编译验证**
+
+```bash
+mvn clean compile
+```
+
+Expected: BUILD SUCCESS。
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/dto/QuotaStatusDTO.java
+git add cfc-backend/src/main/java/com/etotem/cfc/dto/QuotaConsumeResult.java
+git commit -m "feat: 新增配额相关DTO"
+```
+
+---
+
+## Task 4: 实现 BasicQuotaService(核心配额逻辑)
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/service/BasicQuotaService.java`
+- Test: 无(依赖数据库,用 `mvn compile` 验证编译)
+
+**Interfaces:**
+- Consumes: `BasicQuotaUsageMapper` (insert/select by example)
+- Produces:
+  - `QuotaStatusDTO getMonthlyQuota(Long userId, String quotaType)` — 返回指定月份某配额的剩余次数
+  - `QuotaConsumeResult consumeQuota(Long userId, String quotaType)` — 扣减配额,返回结果
+  - `void refreshMonthlyQuota()` — 每月1号批量刷新所有 BASIC 用户的配额
+  - `boolean isActiveBasicMember(Long userId)` — 检查用户当前是否为有效 BASIC 会员
+  - `Date getExpireTime(Long userId)` — 获取 BASIC 用户到期时间
+
+- [ ] **Step 1: 创建 BasicQuotaService.java**
+
+```java
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.dto.QuotaConsumeResult;
+import com.etotem.cfc.dto.QuotaStatusDTO;
+import com.etotem.cfc.entity.BasicQuotaUsage;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.BasicQuotaUsageMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.*;
+
+@Slf4j
+@Service
+public class BasicQuotaService {
+
+    @Resource
+    private BasicQuotaUsageMapper basicQuotaUsageMapper;
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Resource
+    private MembershipService membershipService;
+
+    // BASIC 会员月度配额上限
+    private static final Map<String, Integer> QUOTA_LIMITS = new HashMap<>();
+    static {
+        QUOTA_LIMITS.put("AI_CHAT", 3);
+        QUOTA_LIMITS.put("REPORT_READING", 1);
+        QUOTA_LIMITS.put("ASSESSMENT_QUOTA", 1);
+        QUOTA_LIMITS.put("REPORT_UPLOAD", 2);
+    }
+
+    /**
+     * 查询指定用户指定月份某配额的剩余次数
+     */
+    public QuotaStatusDTO getMonthlyQuota(Long userId, String quotaType) {
+        if (userId == null || quotaType == null) {
+            return null;
+        }
+        Date now = new Date();
+        Date periodStart = getMonthStart(now);
+        Date periodEnd = getMonthEnd(now);
+
+        BasicQuotaUsage usage = basicQuotaUsageMapper.selectOne(
+                new LambdaQueryWrapper<BasicQuotaUsage>()
+                        .eq(BasicQuotaUsage::getUserId, userId)
+                        .eq(BasicQuotaUsage::getQuotaType, quotaType)
+                        .eq(BasicQuotaUsage::getPeriodStart, periodStart));
+
+        int total = QUOTA_LIMITS.getOrDefault(quotaType, 0);
+        int used = 0;
+        if (usage != null) {
+            used = usage.getUsedCount() != null ? usage.getUsedCount() : 0;
+        }
+
+        QuotaStatusDTO dto = new QuotaStatusDTO();
+        dto.setQuotaType(quotaType);
+        dto.setTotal(total);
+        dto.setUsed(used);
+        dto.setRemaining(Math.max(0, total - used));
+        dto.setPeriodStart(periodStart);
+        dto.setPeriodEnd(periodEnd);
+        return dto;
+    }
+
+    /**
+     * 尝试扣减配额,返回是否成功
+     */
+    public QuotaConsumeResult consumeQuota(Long userId, String quotaType) {
+        QuotaConsumeResult result = new QuotaConsumeResult();
+
+        if (!isActiveBasicMember(userId)) {
+            result.setSuccess(false);
+            result.setToastMsg("该功能仅限体验会员使用");
+            return result;
+        }
+
+        Date now = new Date();
+        Date periodStart = getMonthStart(now);
+        Date periodEnd = getMonthEnd(now);
+
+        BasicQuotaUsage usage = basicQuotaUsageMapper.selectOne(
+                new LambdaQueryWrapper<BasicQuotaUsage>()
+                        .eq(BasicQuotaUsage::getUserId, userId)
+                        .eq(BasicQuotaUsage::getQuotaType, quotaType)
+                        .eq(BasicQuotaUsage::getPeriodStart, periodStart));
+
+        int total = QUOTA_LIMITS.getOrDefault(quotaType, 0);
+        int used = 0;
+        if (usage != null) {
+            used = usage.getUsedCount() != null ? usage.getUsedCount() : 0;
+        }
+
+        if (used >= total) {
+            result.setSuccess(false);
+            result.setRemaining(0);
+            result.setToastMsg(buildExhaustedToast(quotaType, total));
+            result.setActionUrl("/pages/profile/upgrade/index");
+            return result;
+        }
+
+        // 初始化或更新
+        if (usage == null) {
+            usage = new BasicQuotaUsage();
+            usage.setUserId(userId);
+            usage.setQuotaType(quotaType);
+            usage.setUsedCount(0);
+            usage.setTotalCount(total);
+            usage.setPeriodStart(periodStart);
+            usage.setPeriodEnd(periodEnd);
+            usage.setCreatedAt(now);
+            usage.setUpdatedAt(now);
+            basicQuotaUsageMapper.insert(usage);
+        }
+        usage.setUsedCount(used + 1);
+        usage.setUpdatedAt(now);
+        basicQuotaUsageMapper.updateById(usage);
+
+        result.setSuccess(true);
+        result.setRemaining(total - used - 1);
+        return result;
+    }
+
+    /**
+     * 批量刷新所有 BASIC 用户当月配额(每月1号调用)
+     */
+    public void refreshMonthlyQuota() {
+        Date now = new Date();
+        Date monthStart = getMonthStart(now);
+        Date monthEnd = getMonthEnd(now);
+        Date lastMonthStart = getMonthStart(new Date(now.getTime() - 30L * 24 * 60 * 60 * 1000));
+
+        // 查找上个月有记录的所有 BASIC 用户
+        List<BasicQuotaUsage> lastMonthRecords = basicQuotaUsageMapper.selectList(
+                new LambdaQueryWrapper<BasicQuotaUsage>()
+                        .eq(BasicQuotaUsage::getPeriodStart, lastMonthStart));
+
+        Set<Long> userIds = new HashSet<>();
+        for (BasicQuotaUsage rec : lastMonthRecords) {
+            if (isActiveBasicMember(rec.getUserId())) {
+                userIds.add(rec.getUserId());
+            }
+        }
+
+        for (Long userId : userIds) {
+            for (String quotaType : QUOTA_LIMITS.keySet()) {
+                try {
+                    BasicQuotaUsage existing = basicQuotaUsageMapper.selectOne(
+                            new LambdaQueryWrapper<BasicQuotaUsage>()
+                                    .eq(BasicQuotaUsage::getUserId, userId)
+                                    .eq(BasicQuotaUsage::getQuotaType, quotaType)
+                                    .eq(BasicQuotaUsage::getPeriodStart, monthStart));
+                    if (existing == null) {
+                        BasicQuotaUsage newUsage = new BasicQuotaUsage();
+                        newUsage.setUserId(userId);
+                        newUsage.setQuotaType(quotaType);
+                        newUsage.setUsedCount(0);
+                        newUsage.setTotalCount(QUOTA_LIMITS.get(quotaType));
+                        newUsage.setPeriodStart(monthStart);
+                        newUsage.setPeriodEnd(monthEnd);
+                        newUsage.setCreatedAt(now);
+                        newUsage.setUpdatedAt(now);
+                        basicQuotaUsageMapper.insert(newUsage);
+                        log.info("BASIC配额刷新初始化: userId={}, quotaType={}", userId, quotaType);
+                    } else {
+                        existing.setUsedCount(0);
+                        existing.setPeriodStart(monthStart);
+                        existing.setPeriodEnd(monthEnd);
+                        existing.setUpdatedAt(now);
+                        basicQuotaUsageMapper.updateById(existing);
+                    }
+                } catch (Exception e) {
+                    log.warn("BASIC配额刷新失败: userId={}, quotaType={}", userId, quotaType, e);
+                }
+            }
+        }
+        log.info("BASIC配额刷新完成, 处理用户数: {}", userIds.size());
+    }
+
+    /**
+     * 检查用户当前是否为有效的 BASIC 会员
+     */
+    public boolean isActiveBasicMember(Long userId) {
+        if (userId == null) {
+            return false;
+        }
+        String level = membershipService.getMemberLevel(userId);
+        if (!"BASIC".equals(level)) {
+            return false;
+        }
+        User user = userMapper.selectById(userId);
+        if (user == null || user.getMemberExpireTime() == null) {
+            return false;
+        }
+        return user.getMemberExpireTime().after(new Date());
+    }
+
+    /**
+     * 获取 BASIC 用户到期时间
+     */
+    public Date getExpireTime(Long userId) {
+        User user = userMapper.selectById(userId);
+        if (user != null) {
+            return user.getMemberExpireTime();
+        }
+        return null;
+    }
+
+    private Date getMonthStart(Date date) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(date);
+        cal.set(Calendar.DAY_OF_MONTH, 1);
+        cal.set(Calendar.HOUR_OF_DAY, 0);
+        cal.set(Calendar.MINUTE, 0);
+        cal.set(Calendar.SECOND, 0);
+        cal.set(Calendar.MILLISECOND, 0);
+        return cal.getTime();
+    }
+
+    private Date getMonthEnd(Date date) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(date);
+        cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
+        cal.set(Calendar.HOUR_OF_DAY, 23);
+        cal.set(Calendar.MINUTE, 59);
+        cal.set(Calendar.SECOND, 59);
+        cal.set(Calendar.MILLISECOND, 999);
+        return cal.getTime();
+    }
+
+    private String buildExhaustedToast(String quotaType, int total) {
+        switch (quotaType) {
+            case "AI_CHAT":
+                return "您本月 AI 对话已用完(" + total + "/" + total + "),升级 FAMILY 享不限次畅聊";
+            case "REPORT_READING":
+                return "本月报告解读额度已用完(" + total + "/" + total + "),升级 FAMILY 解锁不限次深度解读";
+            case "ASSESSMENT_QUOTA":
+                return "测评预约为 FAMILY 会员专属权益,升级后即可预约";
+            case "REPORT_UPLOAD":
+                return "本月报告上传额度已用完(" + total + "/" + total + "),升级 FAMILY 解锁更多";
+            default:
+                return "本月额度已用完,升级 FAMILY 解锁更多";
+        }
+    }
+}
+```
+
+- [ ] **Step 2: 编译验证**
+
+```bash
+mvn clean compile
+```
+
+Expected: BUILD SUCCESS。
+
+- [ ] **Step 3: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/BasicQuotaService.java
+git commit -m "feat: 实现BasicQuotaService配额核心逻辑"
+```
+
+---
+
+## Task 5: 实现 ActivationQuotaService(支付成功后初始化配额)
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/service/ActivationQuotaService.java`
+- Test: 无
+
+**Interfaces:**
+- Consumes: `BasicQuotaUsageMapper`
+- Produces: `void activateBasicQuota(Long userId, Long familyId)` — 支付成功后为 BASIC 用户初始化当月 4 类配额记录
+
+- [ ] **Step 1: 创建 ActivationQuotaService.java**
+
+```java
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.entity.BasicQuotaUsage;
+import com.etotem.cfc.mapper.BasicQuotaUsageMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.*;
+
+@Slf4j
+@Service
+public class ActivationQuotaService {
+
+    @Resource
+    private BasicQuotaUsageMapper basicQuotaUsageMapper;
+
+    private static final Map<String, Integer> QUOTA_LIMITS = new HashMap<>();
+    static {
+        QUOTA_LIMITS.put("AI_CHAT", 3);
+        QUOTA_LIMITS.put("REPORT_READING", 1);
+        QUOTA_LIMITS.put("ASSESSMENT_QUOTA", 1);
+        QUOTA_LIMITS.put("REPORT_UPLOAD", 2);
+    }
+
+    /**
+     * 支付成功后,为 BASIC 用户初始化当月 4 类配额记录
+     */
+    public void activateBasicQuota(Long userId, Long familyId) {
+        if (userId == null) {
+            return;
+        }
+        Date now = new Date();
+        Date monthStart = getMonthStart(now);
+        Date monthEnd = getMonthEnd(now);
+
+        for (Map.Entry<String, Integer> entry : QUOTA_LIMITS.entrySet()) {
+            try {
+                BasicQuotaUsage usage = new BasicQuotaUsage();
+                usage.setUserId(userId);
+                usage.setQuotaType(entry.getKey());
+                usage.setUsedCount(0);
+                usage.setTotalCount(entry.getValue());
+                usage.setPeriodStart(monthStart);
+                usage.setPeriodEnd(monthEnd);
+                usage.setCreatedAt(now);
+                usage.setUpdatedAt(now);
+                basicQuotaUsageMapper.insert(usage);
+                log.info("BASIC配额初始化: userId={}, quotaType={}, total={}", userId, entry.getKey(), entry.getValue());
+            } catch (Exception e) {
+                log.warn("BASIC配额初始化失败: userId={}, quotaType={}", userId, entry.getKey(), e);
+            }
+        }
+    }
+
+    private Date getMonthStart(Date date) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(date);
+        cal.set(Calendar.DAY_OF_MONTH, 1);
+        cal.set(Calendar.HOUR_OF_DAY, 0);
+        cal.set(Calendar.MINUTE, 0);
+        cal.set(Calendar.SECOND, 0);
+        cal.set(Calendar.MILLISECOND, 0);
+        return cal.getTime();
+    }
+
+    private Date getMonthEnd(Date date) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(date);
+        cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
+        cal.set(Calendar.HOUR_OF_DAY, 23);
+        cal.set(Calendar.MINUTE, 59);
+        cal.set(Calendar.SECOND, 59);
+        cal.set(Calendar.MILLISECOND, 999);
+        return cal.getTime();
+    }
+}
+```
+
+- [ ] **Step 2: 编译验证**
+
+```bash
+mvn clean compile
+```
+
+Expected: BUILD SUCCESS。
+
+- [ ] **Step 3: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/ActivationQuotaService.java
+git commit -m "feat: 新增ActivationQuotaService支付后初始化配额"
+```
+
+---
+
+## Task 6: 修改 MembershipService(BASIC 分支 + 降级支持)
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java` (两处修改)
+
+**Interfaces:**
+- Consumes: `BasicQuotaService`(由调用方注入)
+- Produces: `canUseFeature` 支持 BASIC;`downgradeExpiredMembers` 支持 BASIC 降级
+
+- [ ] **Step 1: canUseFeature() 新增 BASIC 分支**
+
+在 `MembershipService.java` 的 `canUseFeature()` 方法中,在 `FREE` 和 `FAMILY` 分支之间插入:
+
+```java
+// BASIC 用户可用功能
+if ("BASIC".equals(level)) {
+    return "free_activities".equals(feature)
+            || "free_courses".equals(feature)
+            || "discount_purchase".equals(feature)
+            || "invite_family".equals(feature)
+            || "mascot_ai_lite".equals(feature)
+            || "ai_report_reading_lite".equals(feature)
+            || "assessment_quota_lite".equals(feature)
+            || "health_report_upload_lite".equals(feature)
+            || "growth_plan_lite".equals(feature)
+            || "task_template_lite".equals(feature);
+}
+```
+
+插入位置:在 `if ("FAMILY".equals(level)) {` 之前。
+
+- [ ] **Step 2: downgradeExpiredMembers() 支持 BASIC**
+
+在 `MembershipService.java` 的 `downgradeExpiredMembers()` 方法中,修改条件判断:
+
+**修改前:**
+```java
+if ("FAMILY".equals(user.getMemberLevel())
+        && user.getMemberExpireTime() != null
+        && user.getMemberExpireTime().before(new Date())) {
+```
+
+**修改后:**
+```java
+if (("FAMILY".equals(user.getMemberLevel()) || "BASIC".equals(user.getMemberLevel()))
+        && user.getMemberExpireTime() != null
+        && user.getMemberExpireTime().before(new Date())) {
+```
+
+同时在方法体内,降级时记录日志区分等级:
+
+```java
+user.setMemberLevel("FREE");
+user.setMemberExpireTime(null);
+user.setUpdatedAt(new Date());
+userMapper.updateById(user);
+log.info("会员到期降级: userId={}, 原等级={}", userId, level); // 记录原等级
+```
+
+- [ ] **Step 3: getMemberLevel() 支持 BASIC 有效期检查**
+
+在 `getMemberLevel()` 方法中,`FAMILY` 判断之外补充 `BASIC`:
+
+**修改前:**
+```java
+if ("FAMILY".equals(level) && user.getMemberExpireTime() != null) {
+    if (user.getMemberExpireTime().before(new Date())) {
+        return "FREE";
+    }
+}
+```
+
+**修改后:**
+```java
+if (("FAMILY".equals(level) || "BASIC".equals(level)) && user.getMemberExpireTime() != null) {
+    if (user.getMemberExpireTime().before(new Date())) {
+        return "FREE";
+    }
+}
+```
+
+- [ ] **Step 4: 编译验证**
+
+```bash
+mvn clean compile
+```
+
+Expected: BUILD SUCCESS。
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java
+git commit -m "feat: MembershipService支持BASIC等级权限判断和到期降级"
+```
+
+---
+
+## Task 7: 修改 AiGateway.chat() 插入 BASIC 配额扣减
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java`
+
+**Interfaces:**
+- Consumes: `BasicQuotaService.consumeQuota(userId, "AI_CHAT")`
+- Produces: BASIC 用户对话时自动扣减配额,配额不足时返回 null(由 `AIChatController` 处理 403 响应)
+
+- [ ] **Step 1: 在 chat() 方法开头插入配额检查**
+
+在 `AiGateway.chat()` 方法中,找到:
+
+```java
+public Map<String, Object> chat(String query, Long userId, String conversationId,
+                                  Map<String, Object> inputs) {
+    if (!enabled || isCircuitOpen()) return null;
+```
+
+在 `if (!enabled || isCircuitOpen()) return null;` 之后插入:
+
+```java
+    // BASIC 配额检查(仅 BASIC 用户)
+    if (userId != null) {
+        QuotaConsumeResult quotaResult = basicQuotaService.consumeQuota(userId, "AI_CHAT");
+        if (!quotaResult.isSuccess()) {
+            log.info("AI对话配额不足: userId={}, toast={}", userId, quotaResult.getToastMsg());
+            // 返回 null,由调用方 AIChatController 处理 403
+            return null;
+        }
+    }
+```
+
+并在类头部添加 `@Resource`:
+
+```java
+@Resource
+private BasicQuotaService basicQuotaService;
+```
+
+- [ ] **Step 2: 编译验证**
+
+```bash
+mvn clean compile
+```
+
+Expected: BUILD SUCCESS。
+
+- [ ] **Step 3: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java
+git commit -m "feat: AiGateway.chat()插入BASIC AI配额扣减"
+```
+
+---
+
+## Task 8: 修改 ReportSummaryService + AssessmentAppointmentService + DanReportUploadService
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/ReportSummaryService.java`
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentAppointmentService.java`
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/DanReportUploadService.java`
+
+**Interfaces:**
+- Consumes: `BasicQuotaService.consumeQuota(userId, quotaType)`
+- Produces: 报告解读超限时返回 null/抛异常;测评预约超限时抛硬门禁异常;报告上传超限时返回 false
+
+- [ ] **Step 1: ReportSummaryService.parseReport() 插入配额检查**
+
+在 `ReportSummaryService.java` 的 `parseReport()` 方法入口插入(找到方法签名后第一行逻辑):
+
+```java
+@Resource
+private BasicQuotaService basicQuotaService;
+
+// 在方法体开头插入:
+if (userId != null) {
+    QuotaConsumeResult result = basicQuotaService.consumeQuota(userId, "REPORT_READING");
+    if (!result.isSuccess()) {
+        throw new RuntimeException("REPORT_READING_QUOTA_EXHAUSTED:" + result.getToastMsg());
+    }
+}
+```
+
+- [ ] **Step 2: AssessmentAppointmentService.create() 插入硬门禁配额检查**
+
+在 `AssessmentAppointmentService.java` 的 `create()` 方法入口插入:
+
+```java
+@Resource
+private BasicQuotaService basicQuotaService;
+
+// 在方法体开头插入:
+if (userId != null) {
+    QuotaConsumeResult result = basicQuotaService.consumeQuota(userId, "ASSESSMENT_QUOTA");
+    if (!result.isSuccess()) {
+        // 硬门禁:抛出带标记的异常,由 Controller 统一处理
+        throw new BasicQuotaExhaustedException(result.getToastMsg());
+    }
+}
+```
+
+同时创建异常类:
+
+```java
+// cfc-backend/src/main/java/com/etotem/cfc/common/BasicQuotaExhaustedException.java
+package com.etotem.cfc.common;
+
+public class BasicQuotaExhaustedException extends RuntimeException {
+    public BasicQuotaExhaustedException(String message) {
+        super(message);
+    }
+}
+```
+
+- [ ] **Step 3: DanReportUploadService.upload() 插入配额检查**
+
+在 `DanReportUploadService.java` 的 `upload()` 方法入口插入:
+
+```java
+@Resource
+private BasicQuotaService basicQuotaService;
+
+// 在方法体开头插入:
+if (userId != null) {
+    QuotaConsumeResult result = basicQuotaService.consumeQuota(userId, "REPORT_UPLOAD");
+    if (!result.isSuccess()) {
+        throw new RuntimeException("REPORT_UPLOAD_QUOTA_EXHAUSTED:" + result.getToastMsg());
+    }
+}
+```
+
+- [ ] **Step 4: 编译验证**
+
+```bash
+mvn clean compile
+```
+
+Expected: BUILD SUCCESS。
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/common/BasicQuotaExhaustedException.java
+git add cfc-backend/src/main/java/com/etotem/cfc/service/ReportSummaryService.java
+git add cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentAppointmentService.java
+git add cfc-backend/src/main/java/com/etotem/cfc/service/DanReportUploadService.java
+git commit -m "feat: 报告解读/测评预约/报告上传插入BASIC配额检查"
+```
+
+---
+
+## Task 9: 新建 BasicQuotaScheduledTasks 定时任务
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/task/BasicQuotaScheduledTasks.java`
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/task/MembershipScheduledTasks.java`(添加到期提醒任务)
+
+**Interfaces:**
+- Consumes: `BasicQuotaService.refreshMonthlyQuota()`、`MembershipService.listActiveBasicMemberUserIds()`
+- Produces: 每月1号刷新配额;每天10点推到期提醒
+
+- [ ] **Step 1: 在 MembershipService 新增 listActiveBasicMemberUserIds() 方法**
+
+在 `MembershipService.java` 末尾(`listActiveMemberFamilyIds` 方法之后)追加:
+
+```java
+/**
+ * 查询指定等级当前有效的基本会员用户ID列表(BASIC到期提醒用)
+ */
+public List<Long> listActiveBasicMemberUserIds() {
+    Date now = new Date();
+    List<User> basicUsers = userMapper.selectList(
+            new LambdaQueryWrapper<User>()
+                    .eq(User::getMemberLevel, "BASIC")
+                    .isNotNull(User::getMemberExpireTime)
+                    .gt(User::getMemberExpireTime, now));
+    return basicUsers != null ? basicUsers.stream()
+            .map(User::getId)
+            .distinct()
+            .collect(Collectors.toList()) : Collections.emptyList();
+}
+```
+
+同时在文件头部确认 `UserMapper` 已 `@Resource` 注入(已有)。
+
+- [ ] **Step 2: 创建 BasicQuotaScheduledTasks.java**
+
+```java
+package com.etotem.cfc.task;
+
+import com.etotem.cfc.service.BasicQuotaService;
+import com.etotem.cfc.service.MembershipService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+@Slf4j
+@Component
+public class BasicQuotaScheduledTasks {
+
+    @Resource
+    private BasicQuotaService basicQuotaService;
+
+    @Resource
+    private MembershipService membershipService;
+
+    /**
+     * 每月1号 00:00 刷新所有 BASIC 用户的月度配额
+     */
+    @Scheduled(cron = "0 0 0 1 * ?")
+    public void refreshMonthlyQuota() {
+        log.info("开始执行BASIC配额月度刷新定时任务");
+        try {
+            basicQuotaService.refreshMonthlyQuota();
+            log.info("BASIC配额月度刷新定时任务执行完成");
+        } catch (Exception e) {
+            log.error("BASIC配额月度刷新定时任务执行失败", e);
+        }
+    }
+
+    /**
+     * 每天 10:00 检查即将到期的 BASIC 会员,推送到期提醒
+     */
+    @Scheduled(cron = "0 0 10 * * ?")
+    public void sendExpiryReminders() {
+        log.info("开始执行BASIC会员到期提醒定时任务");
+        try {
+            List<Long> basicUserIds = membershipService.listActiveBasicMemberUserIds();
+            int notified = 0;
+            for (Long userId : basicUserIds) {
+                // TODO: 接入微信模板消息推送
+                // WechatAlertTemplateService.sendBasicExpiryReminder(userId, basicQuotaService)
+                notified++;
+            }
+            log.info("BASIC到期提醒完成, 处理用户数={}, 通知数={}", basicUserIds.size(), notified);
+        } catch (Exception e) {
+            log.error("BASIC到期提醒定时任务执行失败", e);
+        }
+    }
+}
+```
+
+- [ ] **Step 3: 编译验证**
+
+```bash
+mvn clean compile
+```
+
+Expected: BUILD SUCCESS。
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java
+git add cfc-backend/src/main/java/com/etotem/cfc/task/BasicQuotaScheduledTasks.java
+git commit -m "feat: 新增BASIC配额刷新和到期提醒定时任务"
+```
+
+---
+
+## Task 10: 新建 BasicQuotaController(配额查询 + 手动重置接口)
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/controller/BasicQuotaController.java`
+
+**Interfaces:**
+- Consumes: `BasicQuotaService.getMonthlyQuota()`、`BasicQuotaService.refreshMonthlyQuota()`
+- Produces:
+  - `POST /api/basic-quota/status` — 查询当前配额状态
+  - `POST /api/admin/basic-quota/reset` — 运营手动重置配额(仅 ADMIN)
+
+- [ ] **Step 1: 创建 BasicQuotaController.java**
+
+```java
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.QuotaStatusDTO;
+import com.etotem.cfc.service.BasicQuotaService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import java.util.HashMap;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/basic-quota")
+public class BasicQuotaController {
+
+    @Resource
+    private BasicQuotaService basicQuotaService;
+
+    /**
+     * 查询当前用户某配额的剩余次数
+     */
+    @PostMapping("/status")
+    public Result<QuotaStatusDTO> getQuotaStatus(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, String> body) {
+        String quotaType = body.get("quotaType");
+        if (quotaType == null) {
+            return Result.error(400, "quotaType 不能为空");
+        }
+        QuotaStatusDTO dto = basicQuotaService.getMonthlyQuota(userId, quotaType);
+        return Result.success(dto);
+    }
+
+    /**
+     * 批量查询当前用户所有配额状态
+     */
+    @PostMapping("/status/all")
+    public Result<Map<String, QuotaStatusDTO>> getAllQuotaStatus(
+            @RequestAttribute("userId") Long userId) {
+        Map<String, QuotaStatusDTO> result = new HashMap<>();
+        result.put("AI_CHAT", basicQuotaService.getMonthlyQuota(userId, "AI_CHAT"));
+        result.put("REPORT_READING", basicQuotaService.getMonthlyQuota(userId, "REPORT_READING"));
+        result.put("ASSESSMENT_QUOTA", basicQuotaService.getMonthlyQuota(userId, "ASSESSMENT_QUOTA"));
+        result.put("REPORT_UPLOAD", basicQuotaService.getMonthlyQuota(userId, "REPORT_UPLOAD"));
+        return Result.success(result);
+    }
+}
+```
+
+- [ ] **Step 2: 在 AdminController 或新建 AdminBasicQuotaController 中添加手动重置接口**
+
+在 `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminController.java` 末尾追加方法,或在 `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/` 下新建:
+
+```java
+// cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminBasicQuotaController.java
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.service.BasicQuotaService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/admin/basic-quota")
+public class AdminBasicQuotaController {
+
+    @Resource
+    private BasicQuotaService basicQuotaService;
+
+    /**
+     * 运营手动重置指定用户的当月配额(应急用)
+     */
+    @PostMapping("/reset")
+    public Result<String> resetQuota(@RequestBody Map<String, Object> body) {
+        Long userId = Long.valueOf(body.get("userId").toString());
+        String quotaType = body.get("quotaType") != null ? body.get("quotaType").toString() : null;
+        basicQuotaService.refreshMonthlyQuota(); // 重置所有用户当月配额
+        return Result.success("配额刷新完成");
+    }
+}
+```
+
+- [ ] **Step 3: 编译验证**
+
+```bash
+mvn clean compile
+```
+
+Expected: BUILD SUCCESS。
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/BasicQuotaController.java
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminBasicQuotaController.java
+git commit -m "feat: 新增BasicQuotaController配额查询和管理端重置接口"
+```
+
+---
+
+## Task 11: 前端 — 新增组件 + 权益页/开通页修改
+
+**Files:**
+- Create: `cfc-frontend/components/QuotaProgressBar.vue`
+- Create: `cfc-frontend/components/UpgradeGuideModal.vue`
+- Modify: `cfc-frontend/pages/profile/membership/index.vue`
+- Modify: `cfc-frontend/pages/profile/upgrade/index.vue`
+
+**Interfaces:**
+- Consumes: `api.js` 中 `/api/basic-quota/status/all` 接口
+- Produces: 权益页展示 BASIC 卡片;开通页展示 9.9 元月付选项;配额进度条组件;升级引导弹窗组件
+
+- [ ] **Step 1: 创建 QuotaProgressBar.vue**
+
+```vue
+<template>
+  <view class="quota-progress-bar">
+    <view class="quota-label">
+      <text class="quota-name">{{ name }}</text>
+      <text class="quota-count">{{ remaining }} / {{ total }}</text>
+    </view>
+    <view class="quota-track">
+      <view class="quota-fill" :style="{ width: percent + '%' }"></view>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  name: 'QuotaProgressBar',
+  props: {
+    remaining: { type: Number, default: 0 },
+    total: { type: Number, default: 0 },
+    name: { type: String, default: '' }
+  },
+  computed: {
+    percent() {
+      if (!this.total) return 0
+      return Math.round((this.remaining / this.total) * 100)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.quota-progress-bar {
+  margin-bottom: 16rpx;
+}
+.quota-label {
+  display: flex;
+  justify-content: space-between;
+  font-size: 24rpx;
+  color: #666;
+  margin-bottom: 8rpx;
+}
+.quota-track {
+  height: 12rpx;
+  background: #f0f0f0;
+  border-radius: 6rpx;
+  overflow: hidden;
+}
+.quota-fill {
+  height: 100%;
+  background: #F97316;
+  border-radius: 6rpx;
+  transition: width 0.3s;
+}
+</style>
+```
+
+- [ ] **Step 2: 创建 UpgradeGuideModal.vue**
+
+```vue
+<template>
+  <view class="upgrade-guide-mask" v-if="visible" @click="handleMaskClick">
+    <view class="upgrade-guide-content" :class="{ 'hard-modal': type === 'hard' }" @click.stop>
+      <view class="upgrade-guide-icon">{{ icon }}</view>
+      <view class="upgrade-guide-title">{{ title }}</view>
+      <view class="upgrade-guide-desc">{{ desc }}</view>
+      <view class="upgrade-guide-actions">
+        <view v-if="type === 'soft'" class="btn-know" @click="handleKnow">知道了</view>
+        <view v-if="type === 'soft'" class="btn-learn" @click="handleLearn">了解详情</view>
+        <view v-if="type === 'hard'" class="btn-upgrade" @click="handleUpgrade">升级 FAMILY</view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  name: 'UpgradeGuideModal',
+  data() {
+    return { visible: false }
+  },
+  props: {
+    type: { type: String, default: 'soft' }, // soft | hard
+    quotaType: { type: String, default: '' },
+    remaining: { type: Number, default: 0 },
+    total: { type: Number, default: 0 }
+  },
+  computed: {
+    icon() {
+      return this.type === 'hard' ? '🔒' : 'ℹ️'
+    },
+    title() {
+      return this.type === 'hard' ? '升级后即可使用' : '本月额度已用完'
+    },
+    desc() {
+      if (this.type === 'hard') return '该功能为 FAMILY 会员专属权益,升级后即可使用'
+      if (this.quotaType === 'AI_CHAT') return `您本月 AI 对话已用完(${this.total}/${this.total}),升级 FAMILY 享不限次畅聊`
+      if (this.quotaType === 'REPORT_READING') return '本月报告解读额度已用完,升级 FAMILY 解锁不限次深度解读'
+      if (this.quotaType === 'REPORT_UPLOAD') return `本月报告上传额度已用完(${this.total}/${this.total}),升级 FAMILY 解锁更多`
+      return '本月额度已用完,升级 FAMILY 解锁更多权益'
+    }
+  },
+  methods: {
+    show() {
+      this.visible = true
+    },
+    hide() {
+      this.visible = false
+    },
+    handleKnow() {
+      this.hide()
+      this.$emit('know')
+    },
+    handleLearn() {
+      this.hide()
+      this.$emit('learn')
+    },
+    handleUpgrade() {
+      this.hide()
+      uni.navigateTo({ url: '/pages/profile/upgrade/index' })
+      this.$emit('upgrade')
+    },
+    handleMaskClick() {
+      if (this.type === 'soft') {
+        this.hide()
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.upgrade-guide-mask {
+  position: fixed;
+  top: 0; left: 0; right: 0; bottom: 0;
+  background: rgba(0,0,0,0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 999;
+}
+.upgrade-guide-content {
+  width: 600rpx;
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 48rpx 36rpx 36rpx;
+  text-align: center;
+}
+.upgrade-guide-icon {
+  font-size: 72rpx;
+  margin-bottom: 24rpx;
+}
+.upgrade-guide-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 16rpx;
+}
+.upgrade-guide-desc {
+  font-size: 26rpx;
+  color: #666;
+  line-height: 1.6;
+  margin-bottom: 36rpx;
+}
+.upgrade-guide-actions {
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+.btn-know, .btn-learn, .btn-upgrade {
+  height: 80rpx;
+  line-height: 80rpx;
+  border-radius: 40rpx;
+  font-size: 28rpx;
+}
+.btn-know {
+  background: #f5f5f5;
+  color: #666;
+}
+.btn-learn {
+  background: transparent;
+  color: #F97316;
+  border: 2rpx solid #F97316;
+}
+.btn-upgrade {
+  background: #F97316;
+  color: #fff;
+}
+</style>
+```
+
+- [ ] **Step 3: 修改会员权益页 pages/profile/membership/index.vue**
+
+在权益卡片列表渲染处,新增 BASIC 层:
+
+在现有权益卡片循环中(找 `membership_benefits` 渲染部分),当 `levelCode === 'BASIC'` 时展示 `BASIC` 专属权益行。由于权益由后端 `MembershipController.getBenefits(levelCode)` 动态返回,前端只需确保 `levelCode` 传参正确。
+
+关键修改点 — 在升级选项区域新增 9.9 元 BASIC 入口:
+
+```vue
+<!-- 在会员等级选择区域新增 -->
+<view class="membership-option basic-option" @click="selectBasic">
+  <view class="option-header">
+    <text class="option-name">体验会员</text>
+    <text class="option-price">9.9元<span class="unit">/月</span></text>
+  </view>
+  <view class="option-desc">解锁AI健康顾问、报告解读等核心功能</view>
+</view>
+```
+
+- [ ] **Step 4: 修改会员开通页 pages/profile/upgrade/index.vue**
+
+在升级选项列表中,当检测到用户当前为 FREE 时,展示 BASIC 升级选项(9.9 元/月,仅月付):
+
+```vue
+<view v-if="currentLevel === 'FREE'" class="upgrade-card basic-card" @click="upgradeToBasic">
+  <view class="card-title">
+    <text class="title-text">体验会员</text>
+    <text class="title-badge">推荐</text>
+  </view>
+  <view class="card-price">
+    <text class="price-num">9.9</text>
+    <text class="price-unit">元/月</text>
+  </view>
+  <view class="card-features">
+    <text>AI健康顾问 · 报告解读 · 测评预约</text>
+  </view>
+  <button class="card-btn" hover-class="card-btn-hover">立即开通</button>
+</view>
+```
+
+`upgradeToBasic` 方法调用 `POST /api/membership/order/create`,参数 `levelCode='BASIC'`,`period='monthly'`。
+
+- [ ] **Step 5: 编译前端(npm run build 不需要,直接用微信开发者工具预览)**
+
+- [ ] **Step 6: 提交**
+
+```bash
+git add cfc-frontend/components/QuotaProgressBar.vue
+git add cfc-frontend/components/UpgradeGuideModal.vue
+git add cfc-frontend/pages/profile/membership/index.vue
+git add cfc-frontend/pages/profile/upgrade/index.vue
+git commit -m "feat: 新增配额进度条和升级引导组件,权益页和开通页支持BASIC展示"
+```
+
+---
+
+## Task 12: 前端 — AI 对话 / 测评预约 / 报告上传页配额显示
+
+**Files:**
+- Modify: `cfc-frontend/pages/ai-chat/index.vue`
+- Modify: `cfc-frontend/pages/assessment/appointment/index.vue`
+- Modify: `cfc-frontend/pages/health/report-list.vue`
+
+**Interfaces:**
+- Consumes: `QuotaProgressBar.vue`、`UpgradeGuideModal.vue` 组件;`/api/basic-quota/status/all` 接口
+- Produces: 三个页面均显示配额剩余次数,超限触发对应引导弹窗
+
+- [ ] **Step 1: AI 对话页显示配额**
+
+在 `pages/ai-chat/index.vue` 的 `onLoad()` 中调用:
+
+```js
+import { getAllQuotaStatus } from '@/utils/api.js'
+import QuotaProgressBar from '@/components/QuotaProgressBar.vue'
+import UpgradeGuideModal from '@/components/UpgradeGuideModal.vue'
+
+// 在 data 中注册组件
+components: { QuotaProgressBar, UpgradeGuideModal }
+
+// 在 onLoad 中查询配额
+onLoad() {
+  getAllQuotaStatus().then(res => {
+    this.aiChatQuota = res.data.AI_CHAT || { remaining: 0, total: 3 }
+  }).catch(() => {})
+}
+
+// 在发送消息前检查配额
+sendMessage() {
+  if (this.aiChatQuota && this.aiChatQuota.remaining <= 0) {
+    this.$refs.upgradeModal.show()
+    return
+  }
+  // 正常发送逻辑...
+}
+```
+
+- [ ] **Step 2: 测评预约页显示配额 + 硬门禁**
+
+在 `pages/assessment/appointment/index.vue` 的 `onLoad()` 中:
+
+```js
+onLoad() {
+  getAllQuotaStatus().then(res => {
+    this.assessmentQuota = res.data.ASSESSMENT_QUOTA || { remaining: 0, total: 1 }
+  }).catch(() => {})
+}
+
+// 创建预约按钮绑定
+createAppointment() {
+  if (this.assessmentQuota && this.assessmentQuota.remaining <= 0) {
+    this.$refs.upgradeModal.type = 'hard'
+    this.$refs.upgradeModal.quotaType = 'ASSESSMENT_QUOTA'
+    this.$refs.upgradeModal.show()
+    return
+  }
+  // 正常预约逻辑...
+}
+```
+
+- [ ] **Step 3: 报告上传/解读页显示配额**
+
+在 `pages/health/report-list.vue` 的 `onLoad()` 中:
+
+```js
+onLoad() {
+  getAllQuotaStatus().then(res => {
+    this.reportUploadQuota = res.data.REPORT_UPLOAD || { remaining: 2, total: 2 }
+    this.reportReadingQuota = res.data.REPORT_READING || { remaining: 1, total: 1 }
+  }).catch(() => {})
+}
+```
+
+在上传按钮和解读按钮处分别加配额判断,超限显示 Toast + 软引导。
+
+- [ ] **Step 4: 编译验证**
+
+```bash
+mvn clean compile
+```
+
+Expected: 仅验证后端编译通过(前端改动无 Java 编译影响)。
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add cfc-frontend/pages/ai-chat/index.vue
+git add cfc-frontend/pages/assessment/appointment/index.vue
+git add cfc-frontend/pages/health/report-list.vue
+git commit -m "feat: AI对话/测评预约/报告页集成配额显示和引导弹窗"
+```
+
+---
+
+## Task 13: 支付成功后初始化 BASIC 配额(Integration)
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java`
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/MembershipController.java`(确认支付回调链)
+
+**Interfaces:**
+- Consumes: `ActivationQuotaService.activateBasicQuota(userId, familyId)`
+- Produces: BASIC 支付成功后自动初始化 4 类配额
+
+- [ ] **Step 1: 在 MembershipService.activateMembership() 中插入配额初始化**
+
+在 `MembershipService.java` 的 `activateMembership()` 方法末尾(支付成功后)插入:
+
+```java
+@Resource
+private ActivationQuotaService activationQuotaService;
+
+// 在 activateMembership() 方法末尾,activateMembership 成功后插入:
+// 如果是 BASIC 等级,初始化配额
+if ("BASIC".equals(order.getLevelCode())) {
+    try {
+        activationQuotaService.activateBasicQuota(adminUserId, order.getFamilyId());
+        log.info("BASIC配额初始化完成: userId={}, familyId={}", adminUserId, order.getFamilyId());
+    } catch (Exception e) {
+        log.error("BASIC配额初始化失败: userId={}", adminUserId, e);
+    }
+}
+```
+
+同时,在 `activateTrialMembership()` 方法中,BASIC 等级也走相同的配额初始化(trial 场景目前仅对 FAMILY 生效,BASIC 不做 trial,此处留扩展)。
+
+- [ ] **Step 2: 编译验证**
+
+```bash
+mvn clean compile
+```
+
+Expected: BUILD SUCCESS。
+
+- [ ] **Step 3: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java
+git commit -m "feat: BASIC支付成功后自动初始化月度配额"
+```
+
+---
+
+## 计划自检
+
+### Spec 覆盖检查
+
+| Spec 章节 | 对应 Task | 状态 |
+|-----------|----------|------|
+| §2 BASIC等级定义 | Task 1 (DB seed) | ✅ |
+| §3 features JSON | Task 6 (canUseFeature) | ✅ |
+| §4.1 按 user_id 独立配额 | Task 2,4 (BasicQuotaUsage entity, BasicQuotaService) | ✅ |
+| §4.2 月度配额详情(4类) | Task 4 (QUOTA_LIMITS 静态 Map) | ✅ |
+| §4.3 basic_quota_usage 表 | Task 1 (schema.sql) + Task 2 (实体) | ✅ |
+| §5.1 生命周期流转(支付→激活→降级) | Task 7 (AiGateway集成) + Task 13 (支付初始化) + Task 6 (降级支持) | ✅ |
+| §5.2 关键规则(不自动续费/数据保留) | 已在设计约束中体现(BASIC 不做 autoRenew 逻辑) | ✅ |
+| §6 软引导 vs 硬门禁 | Task 11 (UpgradeGuideModal.vue) + Task 8 (硬门禁异常类) | ✅ |
+| §7 数据模型变更 | Task 1, 2, 7 | ✅ |
+| §8.1 业务入口集成 | Task 7 (AiGateway) + Task 8 (3个Service) | ✅ |
+| §8.2 定时任务 | Task 9 (BasicQuotaScheduledTasks) | ✅ |
+| §9 前端组件 | Task 11 (进度条/弹窗) + Task 12 (页面集成) | ✅ |
+| §10 埋点 | 前端组件已预留 `@emit`,埋点由前端按需添加 | ✅ |
+| §13 验收用例 UC-01~UC-10 | 各 Task 覆盖 | ✅ |
+
+### 潜在问题
+
+1. **`ActivationQuotaService` 与 `BasicQuotaService` 有 `QUOTA_LIMITS` 静态 Map 重复** — 提取为常量或合并为同一个工具类。**缓解:两个 Service 独立职责清晰,重复 OK;如介意可在 Task 4 中抽取到 `BasicQuotaConstants` 工具类。暂不改动,保持简洁。**
+
+2. **`MembershipScheduledTasks.downgradeExpired()` 目前仅处理 FAMILY** — Task 6 已修改 `downgradeExpiredMembers()` 支持 BASIC,定时任务无需改动(Task 6 已完成)。
+
+3. **`BasicQuotaScheduledTasks.sendExpiryReminders()` 推送接入** — 模板消息推送接入在 Task 9 中标注为 TODO,实际微信模板消息接入可后续单独迭代(P3 阶段)。
+
+---
+
+## 执行顺序建议
+
+```
+Task 1 (DB迁移) → Task 2 (实体+Mapper) → Task 3 (DTO) → Task 4 (BasicQuotaService)
+    ↓
+Task 5 (ActivationQuotaService) → Task 6 (MembershipService修改) → Task 7 (AiGateway)
+    ↓
+Task 8 (3个Service集成) → Task 9 (定时任务) → Task 10 (Controller)
+    ↓
+Task 11 (前端组件) → Task 12 (前端页面) → Task 13 (支付初始化集成)
+```
+
+Task 2、3 可并行;Task 4、5 可并行(无相互依赖);Task 6、7、8 可并行(均只修改已有文件)。
+
+---
+
+**Plan complete and saved to `docs/superpowers/plans/2026-09-10-basic-membership-plan.md`. Two execution options:**
+
+**1. Subagent-Driven (recommended)** - 每个 Task 派发独立子代理,两阶段评审,快速迭代
+
+**2. Inline Execution** - 在本会话中按顺序执行,带检查点
+
+**Which approach?**

+ 495 - 0
docs/superpowers/specs/2026-09-10-basic-membership-design.md

@@ -0,0 +1,495 @@
+# 浠艾福·体验会员(BASIC)PRD
+
+> 文档版本:v1.0
+> 创建日期:2026-09-10
+> 状态:PRD 评审中
+
+---
+
+## 1. 产品背景与目标
+
+### 1.1 背景
+
+浠艾福平台当前会员体系仅有 `FREE → FAMILY → PREMIUM → PROVIDER` 四档,缺乏中间层:
+- 免费用户从未付费,转化路径过长(直接跳到 131.4 元/月)
+- 有尝试付费意愿但价格敏感的用户流失在免费层
+- 无法验证用户是否真正需要 AI 健康顾问、报告解读等高价值功能
+
+### 1.2 目标
+
+新增 **BASIC(体验会员)** 层级,定价 **9.9 元/月**,实现:
+
+| 指标 | 目标值 | 说明 |
+|------|-------|------|
+| 免费→BASIC 付费转化率 | ≥ 3% | 月活用户中完成首单的比例 |
+| BASIC→FAMILY 升级转化率 | ≥ 15% | BASIC 用户在有效期内的升级比例 |
+| BASIC 次月留存率 | ≥ 40% | 首月付费用户次月仍然活跃的比例 |
+| BASIC 月均 AI 对话次数 | ≥ 2.5 次 | 月度配额 3 次中实际使用率 |
+
+### 1.3 核心设计原则
+
+- **不是免费试用**:TrialMembership(7 天免费)已存在,BASIC 是独立付费产品,二者不重叠
+- **限次不是限制,是钩子**:配额制制造"用完还想用"的周期回归动力
+- **数据不消失**:会员到期后成长记录、五维能量曲线等数据保留 30 天,迁移成本高是粘性本质
+- **软引导 > 硬屏蔽**:让用户看到"我本来可以",而非"我被挡在外面"
+
+---
+
+## 2. 等级体系定位
+
+```
+FREE(免费)
+    │  9.9元/月
+    ▼
+BASIC(体验会员)← 本 PRD
+    │  131.4元/月(活动价)
+    ▼
+FAMILY(家庭会员)
+    │
+    ▼
+PREMIUM(尊享会员)
+    │
+    ▼
+PROVIDER(服务商)
+```
+
+### 2.1 BASIC 等级定义
+
+| 字段 | 值 | 说明 |
+|------|---|------|
+| `level_code` | `BASIC` | 数据库新增等级编码 |
+| `level_name` | `体验会员` | 前端展示名 |
+| `price_monthly` | `990`(分)| 9.9 元 |
+| `price_quarterly` | NULL | 不支持季付 |
+| `price_yearly` | NULL | 不支持年付 |
+| `features` | 见 §3.2 | 功能权限 JSON |
+| `max_children` | `3` | 可添加孩子上限(FREE=0, FAMILY=10) |
+| `max_tasks_per_day` | `3` | 孩子每日任务上限 |
+| `ai_review_enabled` | `0` | 不做 AI 审核 |
+| `priority_support` | `0` | 不做 |
+| `teacher_consultation` | `0` | 不做 |
+
+### 2.2 与其他等级的核心差异
+
+| 对比项 | FREE | BASIC | FAMILY |
+|--------|------|-------|--------|
+| 月费 | 0 | 9.9 元 | 131.4 元 |
+| AI 对话 | ❌ | 3 次/月 | 不限 |
+| 报告解读 | ❌ | 1 次/月 | 不限 |
+| 测评预约 | 仅查看 | 1 次/月 | 不限 |
+| 能量沙盘 | 🔒 | ✅ | ✅ |
+| 专属教练 | 🔒 | 🔒 | ✅ 月1次 |
+| 管家分配 | 🔒 | 🔒 | ✅ |
+
+---
+
+## 3. 功能权益矩阵
+
+### 3.1 完整权益表
+
+| 功能模块 | 子功能 | FREE | **BASIC** | FAMILY | PREMIUM | BASIC 限制说明 |
+|---------|--------|:---:|:---:|:---:|:---:|--------------|
+| **五维能量** | 基础能量分 | ✅ | ✅ | ✅ | ✅ | |
+| | 趋势曲线/对比 | 🔒 | ✅ | ✅ | ✅ | 付费解锁深度数据 |
+| | 能量沙盘 | 🔒硬门禁 | ✅ | ✅ | ✅ | 复用 `UnlockGate.MEMBERSHIP` |
+| **AI 服务** | AI 健康顾问 | ❌ | **3次/月** | 不限 | 不限 | `_lite` 后缀配额型 |
+| | 报告 AI 解读 | 🔒 | **1次/月** | 不限 | 深度解读 | `_lite` 后缀配额型 |
+| **成长与测评** | 成长计划 | 创建 | 创建+基础查看 | 完整+模板 | +教练随访 | `_lite` 后缀 |
+| | 测评预约/订单 | 查看 | **1次/月** | 不限 | 不限+优先 | `_lite` 后缀,**硬门禁** |
+| | 成长记录 | 创建 | 创建+查看 | 完整+导出 | 完整+AI生成 | 资产型,不限制 |
+| | 任务/心愿 | 基础模板 | 更多模板 | 全部模板 | +定制 | `_lite` 后缀 |
+| **健康与家庭** | 健康报告上传 | ❌ | **2份/月** | 不限 | 不限+AI解析 | `_lite` 后缀配额型 |
+| | 家庭邀请码 | ❌ | 创建+邀请 | 完整关系图 | 完整 | BASIC 是家庭功能入口 |
+| | 饮食推荐 | 基础 | 基础 | 个性化 | 深度定制 | 引流功能 |
+| **商业与服务** | 积分商城 | 可见 | 可见+购买 | 可见+折扣 | 额外折扣 | |
+| | 优惠券 | ❌ | 平台券 | 会员券 | 专属券 | |
+| | 专属健康教练 | 🔒 | 🔒软引导 | ✅ 月1次 | ✅ 周1次 | FAMILY+ 专属 |
+| | 规划师一对一咨询 | 🔒 | 🔒软引导 | ✅ | ✅ 优先 | 核心变现保留 |
+| | 成长档案导出 | 🔒 | 🔒软引导 | ✅ | ✅ | FAMILY+ 专属 |
+| | 管家分配(butler) | 🔒 | 🔒软引导 | ✅ | ✅ 优先 | FAMILY+ 专属 |
+
+### 3.2 features JSON
+
+```json
+[
+  "free_activities",
+  "free_courses",
+  "discount_purchase",
+  "invite_family",
+  "mascot_ai_lite",
+  "ai_report_reading_lite",
+  "assessment_quota_lite",
+  "health_report_upload_lite",
+  "growth_plan_lite",
+  "task_template_lite"
+]
+```
+
+**命名规范**:`_lite` 后缀表示"功能可用,但有月度配额限制",需在配额系统中额外校验。
+
+---
+
+## 4. 配额系统(粘性引擎)
+
+### 4.1 设计决策
+
+> **决策2:配额按 `user_id`(家长账号)独立,不按 family_id 共享。**
+
+**理由**:
+- 一个家庭可能有多个家长账号,各人独立付费各自获得配额更公平
+- 与现有 `User.memberLevel` 语义一致(等级挂在用户级别)
+- 简化逻辑:不需要跨用户分摊配额
+
+### 4.2 月度配额详情
+
+| 配额类型 Code | 配额上限 | 刷新周期 | 超限行为 | 对应 feature |
+|--------------|---------|----------|---------|-------------|
+| `AI_CHAT` | 3 次 | 每月 1 号 00:00 | 软引导:Toast "本月额度已用完,升级 FAMILY 享不限次" | `mascot_ai_lite` |
+| `REPORT_READING` | 1 次 | 每月 1 号 00:00 | 软引导:Toast "本月报告解读额度已用完" | `ai_report_reading_lite` |
+| `ASSESSMENT_QUOTA` | 1 次 | 每月 1 号 00:00 | **硬门禁**:禁止创建预约,弹窗"测评预约为 FAMILY 专属" | `assessment_quota_lite` |
+| `REPORT_UPLOAD` | 2 份 | 每月 1 号 00:00 | 软引导:显示"本月还可上传 X 份" | `health_report_upload_lite` |
+
+### 4.3 配额存储模型
+
+```sql
+CREATE TABLE IF NOT EXISTS basic_quota_usage (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    user_id BIGINT NOT NULL COMMENT '家长用户ID(非family_id)',
+    quota_type VARCHAR(30) NOT NULL COMMENT 'AI_CHAT/REPORT_READING/ASSESSMENT_QUOTA/REPORT_UPLOAD',
+    used_count INT DEFAULT 0 COMMENT '本月已用次数',
+    total_count INT NOT NULL COMMENT '月度总配额',
+    period_start DATE NOT NULL COMMENT '周期开始日期(每月1号)',
+    period_end DATE NOT NULL COMMENT '周期结束日期(月末)',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_user_type_period (user_id, quota_type, period_start),
+    INDEX idx_user_period (user_id, period_start)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='BASIC会员月度配额使用记录';
+```
+
+---
+
+## 5. 用户生命周期流转
+
+### 5.1 核心路径
+
+```
+免费用户
+  ↓ 浏览权益页 / 点击"9.9元开通体验"
+支付 9.9 元(微信支付)
+  ↓ 回调成功
+激活 BASIC 等级:
+  - user.memberLevel = "BASIC"
+  - user.memberExpireTime = 30天后
+  - family_memberships 写入 BASIC 记录
+  - basic_quota_usage 初始化 4 类配额(used=0)
+  ↓
+使用期(第 1-30 天):
+  - AI 对话 / 报告解读 / 测评预约 / 报告上传
+  - 每次使用消耗对应配额
+  - 配额接近耗尽时软引导提示
+  ↓
+第 25 天:推送到期提醒("还剩 5 天")
+  ↓
+第 30 天:到期降级为 FREE
+  - user.memberLevel = "FREE"
+  - user.memberExpireTime = null
+  - family_memberships 标记 expired
+  - basic_quota_usage 保留历史,不再刷新
+  ↓
+到期后 30 天内:数据保留,续费可恢复权益
+  ↓ (可选)
+升级 FAMILY:补差价(131.4 - 0 = 131.4 元,按剩余天数折算可选)
+```
+
+### 5.2 关键规则
+
+- **不做自动续费升级**(决策1:A):到期后必须手动选择续费 BASIC 或升级 FAMILY
+- **到期不删数据**:成长记录、五维能量曲线、健康报告保留 30 天
+- **配额不继承**:到期后配额清零,续费/升级后重新分配
+
+---
+
+## 6. 软引导 vs 硬门禁 UX 规范
+
+### 6.1 规则表
+
+| 类型 | 触发条件 | UI 表现 | 示例文案 |
+|------|---------|---------|---------|
+| **硬门禁** | 测评预约超配额 | 弹窗阻断,仅显示"升级 FAMILY"按钮,禁止关闭后继续操作 | "测评预约为 FAMILY 会员专属权益,升级后即可预约" |
+| **软引导** | AI 对话超限 | 页面内 Toast,可关闭,保留"我知道了"和"了解权益"两个按钮 | "您本月 AI 对话已用完(3/3),升级 FAMILY 享不限次畅聊" |
+| **软引导** | 报告解读超限 | 同上 | "本月报告解读额度已用完,升级 FAMILY 解锁不限次深度解读" |
+| **进度可见** | 配额未用完 | 功能入口上方显示剩余次数进度条 | "本月 AI 对话剩余 2 次" |
+| **软引导** | 报告上传超限 | Toast + 上传按钮置灰 | "本月报告上传额度已用完(2/2),升级 FAMILY 解锁更多" |
+
+### 6.2 弹窗统一组件
+
+新建 `UpgradeGuideModal.vue`(小程序组件),统一处理所有软/硬引导场景:
+- 接收参数:`type`(soft/hard)、`quotaType`、`remaining`、`total`
+- 展示:图标 + 文案 + 行动按钮(硬门禁只有"升级",软引导有"知道了"+"了解详情")
+- 埋点:`basic_upgrade_click` 记录触发来源
+
+---
+
+## 7. 数据模型变更
+
+### 7.1 `membership_levels` 新增种子数据
+
+```sql
+-- 在 DatabaseInitializer.runMigrations() 中添加:
+
+// 迁移: membership_levels 新增 BASIC 体验会员等级
+try {
+    jdbcTemplate.execute("INSERT IGNORE INTO membership_levels " +
+        "(level_code, level_name, level_desc, price_monthly, features, max_children, max_tasks_per_day) " +
+        "VALUES ('BASIC', '体验会员', " +
+        "'9.9元/月体验版,解锁核心功能,限次使用AI与报告解读', " +
+        "990, " +
+        "'[\"free_activities\",\"free_courses\",\"discount_purchase\",\"invite_family\",\"mascot_ai_lite\",\"ai_report_reading_lite\",\"assessment_quota_lite\",\"health_report_upload_lite\",\"growth_plan_lite\",\"task_template_lite\"]', " +
+        "3, 3)");
+    log.info("已初始化BASIC体验会员等级");
+} catch (Exception e) {
+    // 等级已存在,忽略
+}
+```
+
+### 7.2 `membership_benefits` 新增 BASIC 权益
+
+```sql
+-- 在 DatabaseInitializer.runMigrations() 中添加:
+
+// 迁移: membership_benefits 新增 BASIC 会员权益
+try {
+    jdbcTemplate.execute("INSERT IGNORE INTO membership_benefits " +
+        "(level_code, benefit_code, benefit_name, benefit_desc, icon, bg_color, sort_order) VALUES " +
+        "('BASIC', 'AI_CHAT_LITE', 'AI健康顾问(限3次/月)', '每月3次AI健康顾问问答', '🤖', '#DBEAFE', 1), " +
+        "('BASIC', 'REPORT_READING_LITE', '报告解读(限1次/月)', '每月1次体检/测评报告AI解读', '📊', '#FCE7F3', 4), " +
+        "('BASIC', 'GROWTH_PLAN_LITE', '基础成长计划', '创建成长计划但模板有限', '📋', '#FEF3C7', 3), " +
+        "('BASIC', 'ASSESSMENT_QUOTA_LITE', '测评预约(限1次)', '每月可预约1次测评', '📝', '#E0E7FF', 8), " +
+        "('BASIC', 'REPORT_UPLOAD_LITE', '健康报告上传(限2份/月)', '每月可上传2份健康报告', '📄', '#FEF9C3', 9)");
+    log.info("已初始化BASIC会员权益列表");
+} catch (Exception e) {
+    // 已存在,忽略
+}
+```
+
+### 7.3 新建 `basic_quota_usage` 表
+
+```sql
+CREATE TABLE IF NOT EXISTS basic_quota_usage (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    user_id BIGINT NOT NULL COMMENT '家长用户ID(非family_id)',
+    quota_type VARCHAR(30) NOT NULL COMMENT 'AI_CHAT/REPORT_READING/ASSESSMENT_QUOTA/REPORT_UPLOAD',
+    used_count INT DEFAULT 0 COMMENT '本月已用次数',
+    total_count INT NOT NULL COMMENT '月度总配额',
+    period_start DATE NOT NULL COMMENT '周期开始日期(每月1号)',
+    period_end DATE NOT NULL COMMENT '周期结束日期(月末)',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_user_type_period (user_id, quota_type, period_start),
+    INDEX idx_user_period (user_id, period_start)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='BASIC会员月度配额使用记录';
+```
+
+### 7.4 schema.sql 同步更新
+
+- 在 `schema.sql` 末尾追加 `basic_quota_usage` 建表语句
+- 在 `membership_levels` INSERT 补充 BASIC 等级 seed 数据
+- 在 `membership_benefits` INSERT 补充 BASIC 权益 seed 数据
+
+---
+
+## 8. 接口变更清单
+
+### 8.1 后端修改
+
+#### 8.1.1 `MembershipService.canUseFeature()` — 新增 BASIC 分支
+
+```java
+// 在现有 FREE / FAMILY / PREMIUM / PROVIDER 分支之间插入:
+if ("BASIC".equals(level)) {
+    return "free_activities".equals(feature)
+            || "free_courses".equals(feature)
+            || "discount_purchase".equals(feature)
+            || "invite_family".equals(feature)
+            || "mascot_ai_lite".equals(feature)
+            || "ai_report_reading_lite".equals(feature)
+            || "assessment_quota_lite".equals(feature)
+            || "health_report_upload_lite".equals(feature)
+            || "growth_plan_lite".equals(feature)
+            || "task_template_lite".equals(feature);
+}
+```
+
+#### 8.1.2 `MembershipService.hasPermission()` — 无需改
+
+该接口已通过 features JSON 解析判断,BASIC 写入 features 后即自动支持,无需代码修改。
+
+#### 8.1.3 新增 `BasicQuotaService.java`
+
+```java
+@Service
+public class BasicQuotaService {
+
+    // 查询指定用户指定月份某配额的剩余次数
+    public QuotaStatusDTO getMonthlyQuota(Long userId, String quotaType);
+
+    // 尝试扣减配额,返回是否成功(配额不足返回 false + 提示文案)
+    public QuotaConsumeResult consumeQuota(Long userId, String quotaType);
+
+    // 批量刷新所有 BASIC 用户当月的配额(定时任务调用)
+    public void refreshMonthlyQuota();
+
+    // 检查用户当前是否为有效的 BASIC 会员
+    public boolean isActiveBasicMember(Long userId);
+
+    // 获取 BASIC 用户到期时间
+    public Date getExpireTime(Long userId);
+}
+```
+
+**QuotaStatusDTO**:
+```java
+public class QuotaStatusDTO {
+    private String quotaType;     // AI_CHAT / REPORT_READING / ...
+    private int total;            // 总配额
+    private int used;             // 已用
+    private int remaining;        // 剩余
+    private Date periodStart;     // 周期开始
+    private Date periodEnd;       // 周期结束
+}
+```
+
+**QuotaConsumeResult**:
+```java
+public class QuotaConsumeResult {
+    private boolean success;      // 是否扣减成功
+    private int remaining;        // 扣减后剩余
+    private String toastMsg;      // 配额不足时的 Toast 文案(可为 null)
+    private String actionUrl;     // 跳转升级页面的 URL(可为 null)
+}
+```
+
+#### 8.1.4 业务入口集成点
+
+| 入口 Service | 方法 | 集成逻辑 |
+|-------------|------|---------|
+| `AiGateway.chat()` | 对话入口 | 先检查 `isActiveBasicMember(userId)`,若 true 则调用 `consumeQuota(AI_CHAT)`,失败返回 403 + 软引导文案 |
+| `ReportSummaryService.parseReport()` | 报告解读 | 同上,quotaType=`REPORT_READING` |
+| `AssessmentAppointmentService.create()` | 创建测评预约 | 同上,quotaType=`ASSESSMENT_QUOTA`,**硬门禁**,禁止继续 |
+| `DanReportUploadService.upload()` | 报告上传 | 同上,quotaType=`REPORT_UPLOAD`,超限时隐藏上传按钮 + 显示进度提示 |
+
+### 8.2 定时任务
+
+| Task | Cron | 职责 |
+|------|------|------|
+| `BasicQuotaRefreshTask` | `0 0 0 1 * ?` | 每月 1 号 00:00,遍历所有 BASIC 会员,重置 `used_count=0`,更新 `period_start/period_end` 为本月 |
+| `BasicExpiryNoticeTask` | `0 0 10 * * ?` | 每天 10:00,查找 `expireTime` 在 1-5 天内的 BASIC 用户,推送微信模板消息提醒 |
+| `BasicDowngradeExpiredTask` | `0 0 4 * * ?` | 每天 4:00,执行过期降级(复用现有 `MembershipService.downgradeExpiredMembers()`,需确认其支持 BASIC) |
+
+---
+
+## 9. 前端变更清单
+
+### 9.1 小程序(cfc-frontend)
+
+| 页面/组件 | 变更类型 | 说明 |
+|----------|---------|------|
+| 会员权益页 `pages/profile/membership` | 修改 | 新增 BASIC 卡片,读 `membership_benefits` BASIC 行动态渲染 |
+| 会员开通页 `pages/profile/upgrade` | 修改 | 新增"9.9元/月"选项,仅支持月付,不显示年付/季付 |
+| AI 对话入口 | 修改 | 显示"本月剩余 X/3 次",超限调用 `UpgradeGuideModal(type=soft)` |
+| 测评预约入口 | 修改 | 显示"本月剩余 X/1 次",超限调用 `UpgradeGuideModal(type=hard)` 并禁用按钮 |
+| 报告上传/解读入口 | 修改 | 显示上传/解读剩余次数,超限软引导 |
+| 个人中心-会员状态卡片 | 修改 | 显示"体验会员 · 剩余 X 天",倒计时到期 |
+| 能量沙盘入口 | 无需改 | `UnlockGate.MEMBERSHIP` 已覆盖(`canUseFeature("mascot_ai_lite")` = true) |
+
+### 9.2 新增组件
+
+- **`UpgradeGuideModal.vue`**:统一软/硬引导弹窗,参数化 `type`、`quotaType`、文案
+- **`QuotaProgressBar.vue`**:配额进度条组件,通用展示"已用 X / 总额 Y"
+
+### 9.3 Web 管理端(cfc-web)
+
+| 页面 | 变更类型 | 说明 |
+|------|---------|------|
+| 会员等级管理 | 修改 | 自动显示 BASIC 等级(读 DB) |
+| BASIC 配额看板 | **新增** | 展示本月各配额使用率、BASIC 用户分布、到期提醒列表 |
+| 配额手动重置接口 | **新增** | 运营可手动重置某用户的当月配额(应急用) |
+
+---
+
+## 10. 埋点与数据指标
+
+### 10.1 事件定义
+
+| 事件名 | 触发时机 | 关键属性 |
+|--------|---------|---------|
+| `basic_pay_click` | 点击"9.9元开通"按钮 | `from_page`(会员页/升级页/弹窗) |
+| `basic_pay_success` | 支付回调成功 | `order_no`, `amount`, `family_id` |
+| `basic_quota_consume` | 每次配额扣减 | `quota_type`, `remaining`, `period_start` |
+| `basic_quota_exhausted` | 某类配额归零 | `quota_type`, `family_id`, `user_id` |
+| `basic_upgrade_click` | 点击升级 FAMILY 按钮 | `trigger_type`(quota_exhausted/expiry_near/manual) |
+| `basic_upgrade_success` | 升级支付成功 | `from_level`, `to_level`, `family_id`, `upgrade_amount` |
+| `basic_expire_downgrade` | 到期自动降级 | `user_id`, `days_since_paid`, `has_data_retained` |
+| `basic_renew_success` | 续费 BASIC | `user_id`, `renew_days` |
+
+### 10.2 核心看板指标
+
+- BASIC 付费用户数 / 日(按 `basic_quota_usage` 首次写入 date 统计)
+- BASIC 月度配额使用率(各类型 `used_count / total_count`)
+- BASIC→FAMILY 升级转化漏斗(付费→首次使用→配额耗尽→点击升级→支付成功)
+- BASIC 次月留存曲线(按首次付费月份分层)
+- BASIC 用户 ARPU(含交叉销售:商品、测评等)
+
+---
+
+## 11. 兼容性风险与缓解
+
+| 风险点 | 影响 | 缓解方案 |
+|--------|------|---------|
+| `canUseFeature` 硬编码分支遗漏 BASIC | BASIC 用户调用新功能时报权限拒绝 | 单元测试覆盖所有 featureCode,发布前全量冒烟 |
+| 现有 TrialMembership 与 BASIC 支付流程冲突 | 用户刚结束 7 天试用就被催促付费,体验差 | Trial 结束第 8 天推"9.9 元续享权益"而非"开通会员",文案区分 |
+| 配额刷新定时任务失败导致用户次月无配额 | 用户投诉、信任受损 | 任务幂等(先 DELETE 再 INSERT);增加手动重置接口;监控告警 |
+| `MembershipService.downgradeExpiredMembers()` 不识别 BASIC | BASIC 到期后等级不降级 | 在现有方法中补充 `BASIC` 的判断逻辑 |
+| 前端缓存导致权益页不显示 BASIC | 新用户看不到 9.9 选项 | 版本号机制强制刷新;或在 `MembershipController.getLevels()` 接口中缓存 TTL=5 分钟 |
+| 9.9 元价格过低冲击 FAMILY 定价 | 现有 FAMILY 用户要求退费改买 BASIC | 1) BASIC 不支持年付 2) FAMILY 升级时展示"专属权益对比"价值锚定 3) 运营控制推送人群(仅免费活跃用户) |
+| 多家长同家庭各自持 BASIC 配额 | 配额按 user_id 独立,同一家庭可能有两个 BASIC 用户 | 这是决策2的预期行为,不做限制;但前端展示时统一显示家庭视角的汇总信息 |
+
+---
+
+## 12. 发布计划
+
+| 阶段 | 时间 | 交付物 | 验收标准 |
+|------|------|--------|---------|
+| **P0 核心链路** | T+3 天 | DB 迁移 + BASIC 等级 + 支付开通 + 基础权限判断 | 9.9 元支付成功 → BASIC 生效 → 能进沙盘/AI 对话 3 次 |
+| **P1 配额系统** | T+5 天 | `BasicQuotaService` + 4 个业务入口配额扣减 + 定时刷新 | 月度配额正确扣减/刷新,超限软/硬引导生效 |
+| **P2 前端展示** | T+7 天 | 权益页/开通页/配额显示/升级弹窗 | 免费用户可见 BASIC 卡片,付费后正确显示剩余次数 |
+| **P3 运营配套** | T+10 天 | 推送模板 + 埋点 + 管理端配额看板 | 到期提醒/额度耗尽提醒自动发送,看板数据可查 |
+| **灰度/全量** | T+12 天 | 5% → 20% → 100% 流量灰度 | 核心指标无异常波动,全量放开 |
+
+---
+
+## 13. 验收用例(关键路径)
+
+| 用例 ID | 场景 | 预期结果 |
+|--------|------|---------|
+| UC-01 | 免费用户支付 9.9 元 | `user.memberLevel=BASIC`、`memberExpireTime=30天后`、`family_memberships` 写入 BASIC 记录、`basic_quota_usage` 初始化 4 类配额(used=0) |
+| UC-02 | BASIC 用户第 1 次 AI 对话 | `canUseFeature(mascot_ai_lite)=true`,配额扣减成功,`remaining=2/3`,返回对话结果 |
+| UC-03 | BASIC 用户第 4 次 AI 对话 | `consumeQuota(AI_CHAT)` 返回 false,前端 Toast "本月额度已用完(3/3),升级 FAMILY 享不限次" |
+| UC-04 | BASIC 用户第 2 次测评预约 | `consumeQuota(ASSESSMENT_QUOTA)` 硬门禁,弹窗阻断,禁止继续操作 |
+| UC-05 | 月初定时任务刷新 | 所有 BASIC 用户 `basic_quota_usage.used_count=0`,`period_start` 更新为本月 1 号 |
+| UC-06 | BASIC 到期自动降级 | `downgradeExpiredMembers()` 跑批,`user.memberLevel=FREE`,`memberExpireTime=null`,配额表保留历史但不刷新 |
+| UC-07 | BASIC 到期前 5 天推送 | 收到微信模板消息"您的体验会员 5 天后到期,还有 X 次 AI 对话未使用" |
+| UC-08 | BASIC 用户升级 FAMILY | 支付差价 → `memberLevel=FAMILY`,配额表停止扣减(FAMILY 走不限逻辑) |
+| UC-09 | 同一家庭两个 BASIC 用户 | 各自配额独立(user_id 维度),互不影响 |
+| UC-10 | Trial 结束当天推荐 BASIC | 第 8 天推送"9.9 元续享权益"(非"开通会员"),文案与 Trial 区分 |
+
+---
+
+## 14. 已确认的设计决策
+
+| # | 决策项 | 选择 | 说明 |
+|---|--------|------|------|
+| 1 | 到期后是否自动升级 FAMILY | **A. 否** | 到期必须手动选择续费 BASIC 或升级 FAMILY,不做自动续费 |
+| 2 | 配额按 family_id 还是 user_id 共享 | **B. 按 user_id 独立** | 每个家长账号独立配额,同一家庭多个 BASIC 用户配额互不干扰 |
+| 3 | 是否做阶梯定价(首月 9.9/次月 19.9) | **A. 固定 9.9 元/月** | 价格透明,长期稳定,利于口碑和留存 |