Преглед изворни кода

refactor: zxyj → cfc 全局重命名

- 目录: zxyj-backend → cfc-backend, zxyj-frontend → cfc-frontend, zxyj-web → cfc-web
- Java 包: com.zxyj → com.etotem.cfc (主程序 + 测试)
- pom.xml groupId/artifactId/name, application.yml 数据库/JWT 配置
- AGENTS.md, DESIGN.md, README.md 引用更新
- 前端/Web package.json name 更新
- 分支: feature/modules-A-E-complete → cfclub
User пре 3 месеци
родитељ
комит
4dda54ca9b
100 измењених фајлова са 10860 додато и 37 уклоњено
  1. 26 26
      AGENTS.md
  2. 11 11
      README.md
  3. BIN
      cfc-backend.tar
  4. 0 0
      cfc-backend/.classpath
  5. 0 0
      cfc-backend/.factorypath
  6. 0 0
      cfc-backend/.project
  7. 0 0
      cfc-backend/.settings/org.eclipse.core.resources.prefs
  8. 0 0
      cfc-backend/.settings/org.eclipse.jdt.apt.core.prefs
  9. 0 0
      cfc-backend/.settings/org.eclipse.jdt.core.prefs
  10. 0 0
      cfc-backend/.settings/org.eclipse.m2e.core.prefs
  11. 96 0
      cfc-backend/AGENTS.md
  12. 149 0
      cfc-backend/pom.xml
  13. 0 0
      cfc-backend/sql/init.sql
  14. 17 0
      cfc-backend/src/main/java/com/etotem/cfc/ZxyjApplication.java
  15. 12 0
      cfc-backend/src/main/java/com/etotem/cfc/common/ProductStatus.java
  16. 50 0
      cfc-backend/src/main/java/com/etotem/cfc/common/Result.java
  17. 1819 0
      cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  18. 14 0
      cfc-backend/src/main/java/com/etotem/cfc/config/GlobalExceptionHandler.java
  19. 70 0
      cfc-backend/src/main/java/com/etotem/cfc/config/JwtConfig.java
  20. 67 0
      cfc-backend/src/main/java/com/etotem/cfc/config/JwtInterceptor.java
  21. 18 0
      cfc-backend/src/main/java/com/etotem/cfc/config/MyBatisPlusConfig.java
  22. 154 0
      cfc-backend/src/main/java/com/etotem/cfc/config/OperationLogInterceptor.java
  23. 20 0
      cfc-backend/src/main/java/com/etotem/cfc/config/RestTemplateConfig.java
  24. 289 0
      cfc-backend/src/main/java/com/etotem/cfc/config/SchemaSyncHandler.java
  25. 29 0
      cfc-backend/src/main/java/com/etotem/cfc/config/SecurityHeadersFilter.java
  26. 39 0
      cfc-backend/src/main/java/com/etotem/cfc/config/SfmsDataSourceConfig.java
  27. 49 0
      cfc-backend/src/main/java/com/etotem/cfc/config/WebConfig.java
  28. 105 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/InviteCardController.java
  29. 131 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/MarketController.java
  30. 84 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/MediaController.java
  31. 110 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/MembershipController.java
  32. 77 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/MiniGameController.java
  33. 120 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/PackagePaymentController.java
  34. 71 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/StreakController.java
  35. 96 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/StreetController.java
  36. 132 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/TaskTemplatePackageController.java
  37. 131 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/UserController.java
  38. 295 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminAuthController.java
  39. 883 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminController.java
  40. 78 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminGuideController.java
  41. 53 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminPackageController.java
  42. 113 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AssessmentAdminController.java
  43. 32 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/DataMigrationController.java
  44. 36 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/MigrationController.java
  45. 69 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/OperationLogController.java
  46. 68 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/PackageTemplateController.java
  47. 105 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/ServiceContentController.java
  48. 71 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/ServiceTypeController.java
  49. 141 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/assessment/AssessmentAppointmentController.java
  50. 143 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/assessment/AssessmentOrderController.java
  51. 291 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/auth/AuthController.java
  52. 38 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/config/SysConfigController.java
  53. 87 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/family/BindController.java
  54. 42 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/family/BindInviteController.java
  55. 136 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyController.java
  56. 252 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyUserController.java
  57. 105 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/family/ParentController.java
  58. 68 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/family/PointsController.java
  59. 95 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/growth/GrowthPlanController.java
  60. 167 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/growth/GrowthRecordController.java
  61. 77 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/AssessmentController.java
  62. 120 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideAssessmentController.java
  63. 162 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideController.java
  64. 437 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideFamilyTaskController.java
  65. 319 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideManagementController.java
  66. 96 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideOrderController.java
  67. 151 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuidePackageController.java
  68. 108 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideRecordController.java
  69. 98 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideTeamController.java
  70. 82 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/PackagePurchaseController.java
  71. 94 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/PaymentController.java
  72. 152 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/TeacherController.java
  73. 112 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/TeacherMessageController.java
  74. 129 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/parent/ParentAssessmentController.java
  75. 75 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/parent/ParentDashboardController.java
  76. 60 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductController.java
  77. 71 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductOrderController.java
  78. 66 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/reward/RewardController.java
  79. 70 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/reward/RewardWishlistController.java
  80. 38 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/sncp/ZkxzSyncController.java
  81. 72 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/stats/StatsController.java
  82. 161 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/task/TaskController.java
  83. 145 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/task/TaskReminderController.java
  84. 38 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/vendor/VendorController.java
  85. 255 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/wish/WishController.java
  86. 17 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/AddGuidePackageDTO.java
  87. 25 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/AdjustPointsDTO.java
  88. 17 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/AdminAdjustPointsDTO.java
  89. 15 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/AdminApproveRewardDTO.java
  90. 15 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/AdminChangePasswordDTO.java
  91. 32 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/AdminCreateGuidePackageDTO.java
  92. 18 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/AdminCreateRewardDTO.java
  93. 42 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/AdminCreateTaskTemplateDTO.java
  94. 21 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/AdminCreateUserDTO.java
  95. 10 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/AdminLoginDTO.java
  96. 17 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/AdminResetPasswordDTO.java
  97. 18 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/AdminResetPwdDTO.java
  98. 21 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/AdminUpdateChildDTO.java
  99. 21 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/AdminUpdateFamilyDTO.java
  100. 29 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/AdminUpdateGuidePackageDTO.java

+ 26 - 26
AGENTS.md

@@ -11,9 +11,9 @@
 ## STRUCTURE
 
 ```
-zxyj/
-├── zxyj-backend/       # Spring Boot 后端服务
-│   └── src/main/java/com/zxyj/
+cfc/
+├── cfc-backend/       # Spring Boot 后端服务
+│   └── src/main/java/com/cfc/
 │       ├── controller/ # REST API (按功能分组)
 │       │   ├── admin/      # 管理端
 │       │   ├── assessment/ # 测评订单/预约
@@ -33,7 +33,7 @@ zxyj/
 │       ├── config/      # 配置 (JWT, 多数据源, DatabaseInitializer)
 │       └── common/      # 公共类 (Result, JwtUtil)
-├── zxyj-frontend/      # uni-app 微信小程序
+├── cfc-frontend/      # uni-app 微信小程序
 │   ├── pages/          # 页面 (20+ 目录)
 │   │   ├── index/      # 首页(角色分流)
 │   │   ├── assessment/ # 测评申请/下单/报告 (6页)
@@ -46,7 +46,7 @@ zxyj/
 │   ├── store/          # Vuex (双角色状态管理)
 │   └── utils/          # API调用封装
-├── zxyj-web/           # Vue 2 + Element UI Web管理端
+├── cfc-web/           # Vue 2 + Element UI Web管理端
 │   └── src/
 │       ├── views/      # 页面 (admin/ + teacher/)
 │       ├── api/        # API封装
@@ -66,17 +66,17 @@ zxyj/
 
 | Task | Location | Notes |
 |------|----------|-------|
-| 添加 REST API | `zxyj-backend/.../controller/` | 按功能模块分组 |
-| 修改业务逻辑 | `zxyj-backend/.../service/` | 命名: `XxxService.java` |
-| 添加数据实体 | `zxyj-backend/.../entity/` | MyBatis-Plus @TableName |
-| 数据库迁移 | `zxyj-backend/.../config/DatabaseInitializer.java` | 启动时自动执行 |
-| 多数据源 | `zxyj-backend/.../config/SfmsDataSourceConfig.java` | sfms 只读源 |
-| 数据迁移 | `zxyj-backend/.../service/DataMigrationService.java` | 从sfms导入历史数据 |
-| 添加小程序页面 | `zxyj-frontend/pages/` | 需在 pages.json 注册 |
-| 前端API | `zxyj-frontend/utils/api.js` | 后端接口封装 |
-| 修改全局样式 | `zxyj-frontend/uni.scss` | SCSS变量 |
-| Web前端 | `zxyj-web/src/views/` + `src/router/index.js` | 角色菜单+路由 |
-| 数据库配置 | `zxyj-backend/src/main/resources/application.yml` | MySQL + sfms |
+| 添加 REST API | `cfc-backend/.../controller/` | 按功能模块分组 |
+| 修改业务逻辑 | `cfc-backend/.../service/` | 命名: `XxxService.java` |
+| 添加数据实体 | `cfc-backend/.../entity/` | MyBatis-Plus @TableName |
+| 数据库迁移 | `cfc-backend/.../config/DatabaseInitializer.java` | 启动时自动执行 |
+| 多数据源 | `cfc-backend/.../config/SfmsDataSourceConfig.java` | sfms 只读源 |
+| 数据迁移 | `cfc-backend/.../service/DataMigrationService.java` | 从sfms导入历史数据 |
+| 添加小程序页面 | `cfc-frontend/pages/` | 需在 pages.json 注册 |
+| 前端API | `cfc-frontend/utils/api.js` | 后端接口封装 |
+| 修改全局样式 | `cfc-frontend/uni.scss` | SCSS变量 |
+| Web前端 | `cfc-web/src/views/` + `src/router/index.js` | 角色菜单+路由 |
+| 数据库配置 | `cfc-backend/src/main/resources/application.yml` | MySQL + sfms |
 | 需求/设计文档 | `docs/需求分析/`, `docs/系统设计/` | Markdown格式 |
 | 分层测试 | `tests/` | unit/integration/e2e |
 
@@ -89,7 +89,7 @@ zxyj/
 | `AssessmentOrderController` | Controller | `assessment/AssessmentOrderController.java` | 测评订单 CRUD、支付 |
 | `AssessmentAppointmentController` | Controller | `assessment/AssessmentAppointmentController.java` | 测评预约管理 |
 | `TaskService` | Service | `service/TaskService.java` | 任务业务逻辑 |
-| `DataMigrationService` | Service | `service/DataMigrationService.java` | sfms → zxyj 全量迁移 |
+| `DataMigrationService` | Service | `service/DataMigrationService.java` | sfms → cfc 全量迁移 |
 | `User` | Entity | `entity/User.java` | 用户 (parent/child/teacher) |
 | `Task` | Entity | `entity/Task.java` | 任务 |
 | `DanAssessmentResult` | Entity | `entity/DanAssessmentResult.java` | DAN测评结果 (含孩子快照) |
@@ -126,20 +126,20 @@ zxyj/
 - **心愿审批:** 孩子创建 → 家长审批 → 扣减积分
 - **测评订单:** 家长申请 → 选择规划师 → 孩子快照 → 支付 → 规划师录入结果
 - **街道地址:** 四级地址结构(省市区街道),区域回退匹配
-- **多数据源:** zxyj 主库 + sfms 源库 (只读迁移)
+- **多数据源:** cfc 主库 + sfms 源库 (只读迁移)
 
 ## COMMANDS
 
 ```bash
 # 后端
-cd zxyj-backend && mvn clean compile  # 编译验证
+cd cfc-backend && mvn clean compile  # 编译验证
 mvn spring-boot:run                   # 启动 (localhost:8080)
 
 # 前端小程序
-cd zxyj-frontend && npm run dev:mp-weixin
+cd cfc-frontend && npm run dev:mp-weixin
 
 # Web管理端
-cd zxyj-web && npm run serve          # (localhost:8082)
+cd cfc-web && npm run serve          # (localhost:8082)
 
 # 迁移 sfms 历史数据
 curl -X POST http://localhost:8080/api/migration/run
@@ -147,7 +147,7 @@ curl -X POST http://localhost:8080/api/migration/run
 
 ## NOTES
 
-- 数据库: MySQL 8.0 `192.168.16.251:3306/zxyj` (账号: zxyj/zxyj@123)
+- 数据库: MySQL 8.0 `192.168.16.251:3306/cfc` (账号: cfc/cfc@123)
 - sfms 源库: 同服务器 `sfms` 库 (需在 application.yml 确认)
 - 后端端口: 8080, Web端口: 8082
 - 小程序 AppID: `wx5ba8038ef16fb245`
@@ -157,11 +157,11 @@ curl -X POST http://localhost:8080/api/migration/run
 ## HIERARCHICAL DOCUMENTATION
 
 ```
-zxyj/
+cfc/
 ├── AGENTS.md                       # 项目总览 (本文件)
-├── zxyj-backend/AGENTS.md          # 后端详细文档
-├── zxyj-frontend/AGENTS.md         # 小程序前端文档
-├── zxyj-web/AGENTS.md              # Web管理端文档
+├── cfc-backend/AGENTS.md          # 后端详细文档
+├── cfc-frontend/AGENTS.md         # 小程序前端文档
+├── cfc-web/AGENTS.md              # Web管理端文档
 ├── tests/AGENTS.md                 # 测试策略文档
 ├── docs/superpowers/AGENTS.md      # AI开发工具配置
 └── docs/需求分析/ 系统设计/ 系统测试/  # 需求/设计/测试文档

+ 11 - 11
README.md

@@ -18,9 +18,9 @@
 ## 项目结构
 
 ```
-zxyj/
-├── zxyj-backend/                    # Java后端服务
-│   ├── src/main/java/com/zxyj/
+cfc/
+├── cfc-backend/                    # Java后端服务
+│   ├── src/main/java/com/etotem/cfc/
 │   │   ├── controller/             # REST API (按功能分组)
 │   │   │   ├── admin/              # 管理端接口
 │   │   │   ├── auth/               # 认证接口
@@ -37,7 +37,7 @@ zxyj/
 │       ├── application.yml         # 配置文件
 │       └── schema.sql              # 数据库初始化
-├── zxyj-frontend/                   # 微信小程序前端
+├── cfc-frontend/                   # 微信小程序前端
 │   ├── pages/                      # 页面组件
 │   │   ├── index/                  # 首页(角色分流)
 │   │   ├── parent/                # 家长端
@@ -48,7 +48,7 @@ zxyj/
 │   ├── utils/                     # 工具函数
 │   └── pages.json                 # 页面注册
-├── zxyj-web/                        # Web管理端
+├── cfc-web/                        # Web管理端
 │   └── src/
 │       ├── views/                  # 页面组件
 │       ├── api/                   # API调用
@@ -74,16 +74,16 @@ zxyj/
 
 ```bash
 mysql -u root -p
-CREATE DATABASE zxyj CHARACTER SET utf8mb4;
+CREATE DATABASE cfc CHARACTER SET utf8mb4;
 
 # 执行初始化脚本
-mysql -u root -p zxyj < zxyj-backend/src/main/resources/schema.sql
+mysql -u root -p cfc < cfc-backend/src/main/resources/schema.sql
 ```
 
 ### 3. 启动后端
 
 ```bash
-cd zxyj-backend
+cd cfc-backend
 mvn clean install
 mvn spring-boot:run
 ```
@@ -91,7 +91,7 @@ mvn spring-boot:run
 ### 4. 启动小程序前端
 
 ```bash
-cd zxyj-frontend
+cd cfc-frontend
 npm install
 npm run dev:mp-weixin
 ```
@@ -99,7 +99,7 @@ npm run dev:mp-weixin
 ### 5. 启动Web管理端
 
 ```bash
-cd zxyj-web
+cd cfc-web
 npm install
 npm run serve
 ```
@@ -176,7 +176,7 @@ npm run serve
 ## 测试
 
 ```bash
-cd zxyj-backend
+cd cfc-backend
 mvn test
 ```
 


+ 0 - 0
zxyj-backend/.classpath → cfc-backend/.classpath


+ 0 - 0
zxyj-backend/.factorypath → cfc-backend/.factorypath


+ 0 - 0
zxyj-backend/.project → cfc-backend/.project


+ 0 - 0
zxyj-backend/.settings/org.eclipse.core.resources.prefs → cfc-backend/.settings/org.eclipse.core.resources.prefs


+ 0 - 0
zxyj-backend/.settings/org.eclipse.jdt.apt.core.prefs → cfc-backend/.settings/org.eclipse.jdt.apt.core.prefs


+ 0 - 0
zxyj-backend/.settings/org.eclipse.jdt.core.prefs → cfc-backend/.settings/org.eclipse.jdt.core.prefs


+ 0 - 0
zxyj-backend/.settings/org.eclipse.m2e.core.prefs → cfc-backend/.settings/org.eclipse.m2e.core.prefs


+ 96 - 0
cfc-backend/AGENTS.md

@@ -0,0 +1,96 @@
+# cfc-backend
+
+Spring Boot 2.7.18 后端服务,MyBatis-Plus ORM,JWT 认证。
+
+## WHERE TO LOOK
+
+| Task | Location | Notes |
+|------|----------|-------|
+| REST API 端点 | `src/main/java/com/etotem/cfc/controller/` | 按功能分组: admin/, auth/, assessment/, family/, guide/, task/, reward/ |
+| 业务逻辑 | `src/main/java/com/etotem/cfc/service/` | Service 命名: `XxxService.java` |
+| 数据实体 | `src/main/java/com/etotem/cfc/entity/` | 使用 MyBatis-Plus 注解 |
+| MyBatis Mapper | `src/main/java/com/etotem/cfc/mapper/` | 接口文件,与XML映射对应 |
+| SQL 映射文件 | `src/main/resources/mapper/` | MyBatis XML 文件 |
+| 配置类 | `src/main/java/com/etotem/cfc/config/` | JWT, Security, Swagger, DatabaseInitializer |
+| DTO传输对象 | `src/main/java/com/etotem/cfc/dto/` | 请求/响应数据结构 |
+| 数据迁移 | `src/main/java/com/etotem/cfc/service/DataMigrationService.java` | sfms → cfc |
+
+## CODE MAP (关键符号)
+
+| Symbol | Type | Location | Role |
+|--------|------|----------|------|
+| `AuthController` | Controller | `controller/auth/AuthController.java` | 微信登录、JWT 认证 |
+| `TaskService` | Service | `service/TaskService.java` | 任务业务逻辑核心 |
+| `AssessmentOrderController` | Controller | `controller/assessment/AssessmentOrderController.java` | 测评订单(创建/详情/支付) |
+| `AssessmentAppointmentController` | Controller | `controller/assessment/AssessmentAppointmentController.java` | 测评预约管理 |
+| `DataMigrationService` | Service | `service/DataMigrationService.java` | sfms → cfc 全量数据迁移 |
+| `User` | Entity | `entity/User.java` | 用户实体 (parent/child/teacher) |
+| `Task` | Entity | `entity/Task.java` | 任务实体 |
+| `DanAssessmentResult` | Entity | `entity/DanAssessmentResult.java` | DAN测评结果(含孩子快照字段) |
+| `DatabaseInitializer` | Config | `config/DatabaseInitializer.java` | 数据库迁移和初始化 |
+| `SfmsDataSourceConfig` | Config | `config/SfmsDataSourceConfig.java` | sfms 多数据源配置 |
+
+## CONVENTIONS
+
+- **分层架构:** Controller → Service → Mapper,禁止跨层调用
+- **ORM:** MyBatis-Plus,实体使用 `@TableName`、`@TableId(type = IdType.AUTO)`
+- **响应包装:** 统一 `Result<T>` 返回,包含 code/message/data
+- **认证:** JWT Token,Header `Authorization: Bearer {token}`
+- **命名:** Controller 以 `Controller` 结尾,Service 以 `Service` 结尾
+- **控制器分组:** 按功能模块分为 admin, auth, assessment, family, guide, task, reward 等子目录
+- **DI注解:** 使用 `@Resource` 代替 `@Autowired`,字段名必须与类型默认 Bean Name 一致(如 `PaymentService` 的字段名必须为 `paymentService`)
+- **接口方法:** 所有接口统一使用 `@PostMapping`(POST 方法),禁止使用 `@GetMapping`/`@PutMapping`/`@DeleteMapping`
+- **新增接口前:** 必须运行 mvn clean compile 并启动项目验证无冲突;使用以下命令检查重复路由:
+  ```
+  grep -rn '@GetMapping\|@PostMapping\|@PutMapping\|@DeleteMapping' src/main/java/com/etotem/cfc/controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u
+  ```
+- **Bean名冲突检查:** 新增 Controller/Service 时,确保类名不与其他包中的类重名(Spring 默认 Bean Name 为类名首字母小写)
+
+## ANTI-PATTERNS (本项目禁止)
+
+- **DO NOT** 在 Controller 直接操作 Mapper
+- **DO NOT** 跳过 Service 层直接调用 Mapper  
+- **DO NOT** 硬编码数据库连接,使用 `application.yml`
+- **NEVER** 在代码中硬编码 JWT 密钥
+- **NEVER** 使用 `@ts-ignore`、`as any` 绕过类型检查
+- **NEVER** 跳过 JWT 认证直接调用需登录接口
+- **NEVER** 使用 `@Resource(name="xxx")` 与字段名不一致的注入方式,保持字段名与类型默认 Bean Name 一致
+- **NEVER** 使用 `@GetMapping`/`@PutMapping`/`@DeleteMapping` — 统一使用 `@PostMapping`
+- **NEVER** 直接 `new XxxMapper()` — Mapper 必须通过 `@Resource` 注入
+- **NEVER** 使用 `awk '!seen[$0]++'` 对 Java 源文件去重 — 会删除合法的重复行(如多个方法的 `}` 闭合括号)
+
+## UNIQUE STYLES
+
+- 双角色系统:家长端/孩子端,登录后根据 `role` 字段分流,家长端可切孩子端,切换过来的孩子端可退出切换,即时出需要密码。直接登录的孩子端不能切换到家长 端
+- 成长规划师角色系统:成长规划师是个单独的角色,经过认证的成长规划师可以以成长规划师身份登录,成长规划师可切换自己作为家长端
+- 积分机制:完成任务加分、超时扣分,逻辑在 `PointsService`
+- 心愿审批流程:孩子创建 → 家长审批 → 扣减积分
+- 测评订单:家长申请 → 选择规划师 → 孩子信息快照 → 支付 → 规划师录入结果
+- 街道地址匹配:四级地址结构(省市区街道),区域回退匹配逻辑
+- 孩子快照:每次测评时保存孩子身高/体重/学校等动态信息到 DanAssessmentResult
+- 多数据源:cfc 主库 `spring.datasource.*` + sfms 源库 `sfms.datasource.*`(只读)
+
+## COMMANDS
+
+```bash
+mvn clean compile        # 编译验证
+mvn spring-boot:run      # 启动开发服务器 (localhost:8080)
+                         # 注意:每次完成后端任务后,必须通过此命令启动验证应用能正常启动
+mvn test                 # 运行测试 (目前只有1个测试类)
+```
+
+## 工作流程
+
+1. 修改代码后执行 `mvn clean compile` 验证编译通过
+2. 执行 `mvn spring-boot:run` 启动应用
+3. 验证应用启动成功(控制台显示 Started 字样)
+4. 测试相关接口确保功能正常
+
+## NOTES
+
+- 数据库:MySQL 8.0,地址 `192.168.16.251:3306/cfc`
+- 后端端口:8080,JWT 密钥在 `application.yml`
+- 街道数据:使用静态种子数据,街道匹配有回退机制
+- API冲突已修复:检查所有Controller确保无重复路径映射
+- sfms 源库连接信息在 `application.yml` 的 `sfms.datasource.*`
+- 数据迁移 POST `/api/migration/run`

+ 149 - 0
cfc-backend/pom.xml

@@ -0,0 +1,149 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>com.etotem.cfc</groupId>
+    <artifactId>cfc-backend</artifactId>
+    <version>1.0.0</version>
+    <packaging>jar</packaging>
+
+    <name>cfc-backend</name>
+    <description>知行益家后端服务</description>
+
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.7.18</version>
+        <relativePath/>
+    </parent>
+
+    <properties>
+        <java.version>1.8</java.version>
+        <mybatis-plus.version>3.5.3.1</mybatis-plus.version>
+        <jwt.version>0.11.5</jwt.version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+    </properties>
+
+    <dependencies>
+        <!-- Spring Boot Web -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <!-- MyBatis-Plus -->
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+            <version>${mybatis-plus.version}</version>
+        </dependency>
+
+        <!-- MySQL -->
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+            <version>8.0.33</version>
+        </dependency>
+
+        <!-- JWT -->
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt-api</artifactId>
+            <version>${jwt.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt-impl</artifactId>
+            <version>${jwt.version}</version>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt-jackson</artifactId>
+            <version>${jwt.version}</version>
+            <scope>runtime</scope>
+        </dependency>
+
+        <!-- Lombok -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <!-- Validation -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+
+        <!-- Apache Commons -->
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+
+        <!-- OkHttp -->
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+            <version>3.14.9</version>
+        </dependency>
+
+        <!-- FastJSON -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>2.0.40</version>
+        </dependency>
+
+        <!-- SpringDoc OpenAPI (Swagger) -->
+        <dependency>
+            <groupId>org.springdoc</groupId>
+            <artifactId>springdoc-openapi-ui</artifactId>
+            <version>1.8.0</version>
+        </dependency>
+
+        <!-- Test -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <configuration>
+                    <excludes>
+                        <exclude>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok</artifactId>
+                        </exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>1.8</source>
+                    <target>1.8</target>
+                    <annotationProcessorPaths>
+                        <path>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok</artifactId>
+                            <version>1.18.32</version>
+                        </path>
+                    </annotationProcessorPaths>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 0 - 0
zxyj-backend/sql/init.sql → cfc-backend/sql/init.sql


+ 17 - 0
cfc-backend/src/main/java/com/etotem/cfc/ZxyjApplication.java

@@ -0,0 +1,17 @@
+package com.etotem.cfc;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
+@MapperScan("com.etotem.cfc.mapper")
+@EnableScheduling
+public class ZxyjApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(ZxyjApplication.class, args);
+    }
+}

+ 12 - 0
cfc-backend/src/main/java/com/etotem/cfc/common/ProductStatus.java

@@ -0,0 +1,12 @@
+package com.etotem.cfc.common;
+
+public final class ProductStatus {
+    private ProductStatus() {}
+
+    public static final String DRAFT = "draft";
+    public static final String PENDING = "pending";
+    public static final String APPROVED = "approved";
+    public static final String REJECTED = "rejected";
+    public static final String ON_SHELF = "on_shelf";
+    public static final String OFF_SHELF = "off_shelf";
+}

+ 50 - 0
cfc-backend/src/main/java/com/etotem/cfc/common/Result.java

@@ -0,0 +1,50 @@
+package com.etotem.cfc.common;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class Result<T> implements Serializable {
+
+    private Integer code;
+    private String message;
+    private T data;
+
+    public static <T> Result<T> success() {
+        Result<T> result = new Result<>();
+        result.setCode(200);
+        result.setMessage("success");
+        return result;
+    }
+
+    public static <T> Result<T> success(T data) {
+        Result<T> result = new Result<>();
+        result.setCode(200);
+        result.setMessage("success");
+        result.setData(data);
+        return result;
+    }
+
+    public static <T> Result<T> success(String message, T data) {
+        Result<T> result = new Result<>();
+        result.setCode(200);
+        result.setMessage(message);
+        result.setData(data);
+        return result;
+    }
+
+    public static <T> Result<T> error(String message) {
+        Result<T> result = new Result<>();
+        result.setCode(500);
+        result.setMessage(message);
+        return result;
+    }
+
+    public static <T> Result<T> error(Integer code, String message) {
+        Result<T> result = new Result<>();
+        result.setCode(code);
+        result.setMessage(message);
+        return result;
+    }
+}

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

@@ -0,0 +1,1819 @@
+package com.etotem.cfc.config;
+
+import com.etotem.cfc.mapper.*;
+import lombok.extern.slf4j.Slf4j;
+import javax.annotation.Resource;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Component;
+
+import java.util.Arrays;
+import java.util.List;
+
+@Slf4j
+@Component
+public class DatabaseInitializer implements CommandLineRunner {
+
+    @Resource
+    private JdbcTemplate jdbcTemplate;
+
+    @Override
+    public void run(String... args) throws Exception {
+        log.info("开始初始化数据库...");
+
+        // 1. 检查并创建数据库
+        initializeDatabase();
+
+        // 2. 检查并创建表
+        initializeTables();
+
+        // 3. 执行数据库迁移(添加新列)
+        runMigrations();
+
+        // 4. 初始化默认数据
+        initializeDefaultData();
+
+        log.info("数据库初始化完成!");
+    }
+
+    private void initializeDatabase() {
+        try {
+            // 尝试连接数据库,如果不存在则创建
+            jdbcTemplate.execute("CREATE DATABASE IF NOT EXISTS xzyj DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
+            log.info("数据库检查完成");
+        } catch (Exception e) {
+            log.warn("创建数据库失败: {}", e.getMessage());
+        }
+    }
+
+    private void initializeTables() {
+        // 表创建SQL - 使用IF NOT EXISTS确保幂等性
+        List<String> createTableSQLs = Arrays.asList(
+            // 家庭表
+            "CREATE TABLE IF NOT EXISTS families (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "name VARCHAR(100), " +
+            "invite_code VARCHAR(8) UNIQUE, " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+            "INDEX idx_invite_code (invite_code)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
+            
+            // 用户表
+            "CREATE TABLE IF NOT EXISTS users (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "openid VARCHAR(64) NOT NULL, " +
+            "unionid VARCHAR(64), " +
+            "family_id BIGINT NOT NULL, " +
+            "role ENUM('parent','child','admin') NOT NULL, " +
+            "nickname VARCHAR(50), " +
+            "avatar VARCHAR(255), " +
+            "password VARCHAR(128), " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+            "INDEX idx_openid (openid), " +
+            "INDEX idx_family_id (family_id)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
+            
+// 孩子详情表
+"CREATE TABLE IF NOT EXISTS children (" +
+"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+"user_id BIGINT NOT NULL, " +
+"family_id BIGINT NOT NULL, " +
+"nickname VARCHAR(50) NOT NULL, " +
+"age TINYINT NOT NULL, " +
+"dan_level VARCHAR(2), " +
+"penalty_enabled TINYINT DEFAULT 1, " +
+"theme VARCHAR(32) DEFAULT \'default\', " +
+"total_points INT DEFAULT 0, " +
+"streak_days INT DEFAULT 0, " +
+"last_task_date DATE, " +
+"focus_max_daily TINYINT DEFAULT 3, " +
+"focus_remaining TINYINT DEFAULT 3, " +
+"focus_reset_date DATE, " +
+"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+"INDEX idx_user_id (user_id), " +
+"INDEX idx_family_id (family_id)" +
+") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
+            
+            // 任务表
+            "CREATE TABLE IF NOT EXISTS tasks (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "family_id BIGINT NOT NULL, " +
+            "creator_id BIGINT NOT NULL, " +
+            "child_id BIGINT COMMENT '执行者ID(child任务时必填,parent任务时可空)', " +
+            "executor_type VARCHAR(20) COMMENT '执行者类型: child/parent', " +
+            "executor_id BIGINT COMMENT '执行者ID', " +
+            "title VARCHAR(200) NOT NULL, " +
+            "description VARCHAR(500), " +
+            "points TINYINT DEFAULT 2, " +
+            "deadline DATETIME, " +
+            "repeat_type ENUM('none','daily','weekly') DEFAULT 'none', " +
+            "category VARCHAR(32), " +
+            "need_review TINYINT DEFAULT 0, " +
+            "review_by_category TINYINT DEFAULT 0, " +
+            "status ENUM('pending','completed','overdue','cancelled') DEFAULT 'pending', " +
+            "completed_at DATETIME, " +
+            "bonus_rule VARCHAR(500), " +
+            "points_expire_days INT DEFAULT 0, " +
+            "template_id BIGINT, " +
+            "is_template TINYINT DEFAULT 0, " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+            "INDEX idx_child_id_deadline (child_id, deadline), " +
+            "INDEX idx_status (status), " +
+            "INDEX idx_category (category), " +
+            "INDEX idx_executor (executor_type, executor_id)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
+            
+            // 积分流水表
+            "CREATE TABLE IF NOT EXISTS points_log (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "child_id BIGINT NOT NULL, " +
+            "task_id BIGINT, " +
+            "amount INT NOT NULL, " +
+            "type ENUM('earn','spend','bonus','penalty','adjust','reset','refund') NOT NULL, " +
+            "description VARCHAR(255), " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "INDEX idx_child_id_created (child_id, created_at), " +
+            "INDEX idx_type (type)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
+            
+            // 积分清零记录表
+            "CREATE TABLE IF NOT EXISTS points_reset_log (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "child_id BIGINT NOT NULL, " +
+            "balance_before INT NOT NULL, " +
+            "balance_after INT DEFAULT 0, " +
+            "reset_type ENUM('monthly','manual') NOT NULL, " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "INDEX idx_child_id (child_id)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
+            
+// 奖励表
+"CREATE TABLE IF NOT EXISTS rewards (" +
+"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+"family_id BIGINT NOT NULL, " +
+"child_id BIGINT NULL COMMENT '孩子ID,模板奖励为NULL', " +
+"title VARCHAR(100) NOT NULL, " +
+"points_required INT NOT NULL, " +
+"category VARCHAR(32), " +
+"status ENUM(\'available\',\'pending\',\'exchanged\',\'rejected\') DEFAULT \'available\', " +
+"exchange_approved TINYINT, " +
+"exchanged_at DATETIME, " +
+"is_template TINYINT DEFAULT 0, " +
+"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+"INDEX idx_child_id_status (child_id, status), " +
+"INDEX idx_is_template (is_template)" +
+") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
+            
+            // 奖励分类表
+            "CREATE TABLE IF NOT EXISTS reward_categories (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "family_id BIGINT NOT NULL, " +
+            "name VARCHAR(32) NOT NULL, " +
+            "icon VARCHAR(64), " +
+            "sort_order INT DEFAULT 0, " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "INDEX idx_family_id (family_id)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
+            
+            // 敏感词表
+            "CREATE TABLE IF NOT EXISTS sensitive_words (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "word VARCHAR(50) NOT NULL, " +
+            "category VARCHAR(32) NOT NULL, " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "INDEX idx_word (word)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
+            
+            // 任务审核表
+            "CREATE TABLE IF NOT EXISTS task_reviews (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "task_id BIGINT NOT NULL, " +
+            "child_id BIGINT NOT NULL, " +
+            "photo_url VARCHAR(255), " +
+            "voice_note_url VARCHAR(255), " +
+            "ai_result ENUM('pass','review','pending'), " +
+            "ai_confidence DECIMAL(3,2), " +
+            "ai_suggestion VARCHAR(255), " +
+            "ai_threshold ENUM('strict','normal','loose') DEFAULT 'normal', " +
+            "parent_result ENUM('pass','reject'), " +
+            "parent_override TINYINT DEFAULT 0, " +
+            "review_type ENUM('ai','manual') NOT NULL, " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "reviewed_at DATETIME, " +
+            "INDEX idx_task_id (task_id), " +
+            "INDEX idx_ai_result (ai_result)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
+            
+            // 勋章定义表
+            "CREATE TABLE IF NOT EXISTS badge_definitions (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "badge_type VARCHAR(32) NOT NULL, " +
+            "name VARCHAR(50) NOT NULL, " +
+            "description VARCHAR(200), " +
+            "icon VARCHAR(64), " +
+            "condition_type VARCHAR(32) NOT NULL, " +
+            "condition_value INT NOT NULL, " +
+            "reward_points INT DEFAULT 0, " +
+            "INDEX idx_badge_type (badge_type)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
+            
+            // 勋章表
+            "CREATE TABLE IF NOT EXISTS badges (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "child_id BIGINT NOT NULL, " +
+            "badge_type VARCHAR(32) NOT NULL, " +
+            "unlocked_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "INDEX idx_child_id_type (child_id, badge_type)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
+            
+            // 打卡里程碑表
+            "CREATE TABLE IF NOT EXISTS streak_milestones (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "days INT NOT NULL, " +
+            "reward_points INT DEFAULT 0, " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
+            
+            // 专注训练记录表
+            "CREATE TABLE IF NOT EXISTS focus_sessions (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "child_id BIGINT NOT NULL, " +
+            "mode ENUM('simple','pomodoro') NOT NULL, " +
+            "duration TINYINT NOT NULL, " +
+            "stars TINYINT, " +
+            "points_earned INT, " +
+            "voice_note_url VARCHAR(255), " +
+            "started_at DATETIME NOT NULL, " +
+            "completed_at DATETIME, " +
+            "INDEX idx_child_id_completed (child_id, completed_at)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
+            
+            // 专注训练配置表
+            "CREATE TABLE IF NOT EXISTS focus_config (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "family_id BIGINT, " +
+            "max_daily_count TINYINT DEFAULT 3, " +
+            "allow_exit TINYINT DEFAULT 1, " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
+            
+            // DAN测评表
+            "CREATE TABLE IF NOT EXISTS dan_assessments (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "child_id BIGINT NOT NULL, " +
+            "dan_level VARCHAR(2) NOT NULL, " +
+            "assessment_date DATE NOT NULL, " +
+            "details JSON, " +
+            "source ENUM('api','manual') DEFAULT 'manual', " +
+            "photo_url VARCHAR(255), " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "INDEX idx_child_id_level (child_id, dan_level)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
+            
+            // 任务模板表
+            "CREATE TABLE IF NOT EXISTS task_templates (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "dan_level VARCHAR(2) NOT NULL, " +
+            "title VARCHAR(200) NOT NULL, " +
+            "points TINYINT DEFAULT 2, " +
+            "duration_minutes TINYINT, " +
+            "category VARCHAR(32), " +
+            "is_default TINYINT DEFAULT 1, " +
+            "family_id BIGINT, " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "INDEX idx_dan_level (dan_level), " +
+            "INDEX idx_is_default (is_default)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
+            
+// 管理员表
+"CREATE TABLE IF NOT EXISTS admins (" +
+"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+"username VARCHAR(50) NOT NULL, " +
+"password VARCHAR(128) NOT NULL, " +
+"real_name VARCHAR(50), " +
+"phone VARCHAR(11), " +
+"email VARCHAR(100), " +
+"role VARCHAR(20) DEFAULT 'admin', " +
+"status TINYINT DEFAULT 1, " +
+"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+"INDEX idx_phone (phone), " +
+"INDEX idx_username (username)" +
+") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
+            
+// 验证码表
+"CREATE TABLE IF NOT EXISTS verification_codes (" +
+"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+"phone VARCHAR(11) NOT NULL, " +
+"code VARCHAR(6) NOT NULL, " +
+"type VARCHAR(20) NOT NULL, " +
+"expires_in INT DEFAULT 300, " +
+"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+"expires_at DATETIME NOT NULL, " +
+"used TINYINT DEFAULT 0, " +
+"INDEX idx_phone_type (phone, type), " +
+"INDEX idx_expires_at (expires_at)" +
+") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
+
+// 管理后台任务模板表
+"CREATE TABLE IF NOT EXISTS admin_task_templates (" +
+"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+"title VARCHAR(200) NOT NULL, " +
+"description TEXT, " +
+"points INT DEFAULT 10, " +
+"category VARCHAR(32), " +
+"need_review TINYINT DEFAULT 0, " +
+"review_by_category TINYINT DEFAULT 0, " +
+"recommended_age INT, " +
+"difficulty ENUM('easy', 'medium', 'hard') DEFAULT 'medium', " +
+"is_active TINYINT DEFAULT 1, " +
+"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+"INDEX idx_category (category), " +
+            "INDEX idx_difficulty (difficulty), " +
+            "INDEX idx_is_active (is_active)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
+            
+            // 测评资料表
+            "CREATE TABLE IF NOT EXISTS assessment_materials (" +
+            "id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', " +
+            "title VARCHAR(100) NOT NULL COMMENT '资料标题', " +
+            "content TEXT COMMENT '测评介绍内容', " +
+            "cover_image VARCHAR(255) COMMENT '封面图片', " +
+            "status VARCHAR(20) DEFAULT 'active' COMMENT '状态:active/inactive', " +
+            "created_at DATETIME COMMENT '创建时间', " +
+            "updated_at DATETIME COMMENT '更新时间', " +
+            "INDEX idx_status (status)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='测评资料表'",
+
+            // 积分配置表
+            "CREATE TABLE IF NOT EXISTS assessment_points_config (" +
+            "id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', " +
+            "points_value INT DEFAULT 100 COMMENT '完成测评获得积分', " +
+            "points_value_yuan DECIMAL(10,2) DEFAULT 0.01 COMMENT '1积分等于多少元', " +
+            "is_active TINYINT DEFAULT 1 COMMENT '是否启用', " +
+            "created_at DATETIME COMMENT '创建时间', " +
+            "updated_at DATETIME COMMENT '更新时间'" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='积分配置表'",
+
+            // 家庭测评配置表
+            "CREATE TABLE IF NOT EXISTS family_assessment_config (" +
+            "id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', " +
+            "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
+            "material_id BIGINT COMMENT '测评资料ID', " +
+            "remind_months INT DEFAULT 12 COMMENT '提醒周期(月)', " +
+            "last_remind_at DATETIME COMMENT '上次提醒时间', " +
+            "next_remind_at DATETIME COMMENT '下次提醒时间', " +
+            "is_enabled TINYINT DEFAULT 1 COMMENT '是否启用:1启用0关闭', " +
+            "created_by BIGINT COMMENT '创建人(规划师ID)', " +
+            "created_at DATETIME COMMENT '创建时间', " +
+            "updated_at DATETIME COMMENT '更新时间', " +
+            "UNIQUE KEY uk_family (family_id), " +
+            "INDEX idx_next_remind (next_remind_at), " +
+            "INDEX idx_enabled (is_enabled)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭测评配置表'",
+
+            // 测评记录表
+            "CREATE TABLE IF NOT EXISTS assessment_records (" +
+            "id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', " +
+            "family_id BIGINT COMMENT '家庭ID', " +
+            "child_id BIGINT COMMENT '孩子ID', " +
+            "material_id BIGINT COMMENT '测评资料ID', " +
+            "status VARCHAR(20) DEFAULT 'pending' COMMENT '状态:pending待完成/completed已完成', " +
+            "completed_at DATETIME COMMENT '完成时间', " +
+            "created_at DATETIME COMMENT '创建时间', " +
+            "INDEX idx_family (family_id), " +
+            "INDEX idx_status (status)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='测评记录表'",
+
+            // 成长规划师表(已认证的规划师)
+            "CREATE TABLE IF NOT EXISTS guides (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', " +
+            "user_id BIGINT NOT NULL UNIQUE COMMENT '用户ID(唯一)', " +
+            "level VARCHAR(20) NOT NULL DEFAULT 'junior' COMMENT '等级: junior-初级, intermediate-中级, senior-高级', " +
+            "status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态: active-活跃, inactive-停用', " +
+            "service_count INT DEFAULT 0 COMMENT '当前服务家庭数', " +
+            "package_id BIGINT COMMENT '购买套餐ID', " +
+            "nickname VARCHAR(100) COMMENT '规划师昵称', " +
+            "phone VARCHAR(20) COMMENT '联系电话', " +
+            "intro TEXT COMMENT '个人介绍', " +
+            "avatar VARCHAR(500) COMMENT '头像URL', " +
+            "commission_rate DECIMAL(5,4) DEFAULT 0.1000 COMMENT '佣金比例', " +
+            "approved_at DATETIME COMMENT '审核通过时间', " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', " +
+            "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', " +
+            "INDEX idx_user_id (user_id), " +
+            "INDEX idx_level (level), " +
+            "INDEX idx_status (status)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成长规划师表'",
+
+            // 成长规划师申请表
+            "CREATE TABLE IF NOT EXISTS guide_applications (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', " +
+            "user_id BIGINT NOT NULL COMMENT '申请人用户ID', " +
+            "type VARCHAR(20) NOT NULL DEFAULT 'new_apply' COMMENT '申请类型: new_apply-新注册, upgrade-升级', " +
+            "target_level VARCHAR(20) NOT NULL COMMENT '目标等级: junior-初级, intermediate-中级, senior-高级', " +
+            "current_level VARCHAR(20) DEFAULT NULL COMMENT '当前等级(升级时填写)', " +
+            "package_id BIGINT COMMENT '申请的套餐ID', " +
+            "payment_proof VARCHAR(500) COMMENT '付款凭证URL', " +
+            "payment_amount DECIMAL(10,2) COMMENT '付款金额', " +
+            "status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT '状态: pending-待审核, approved-已通过, rejected-已拒绝', " +
+            "reject_reason VARCHAR(500) COMMENT '拒绝原因', " +
+            "reviewed_at DATETIME COMMENT '审核时间', " +
+            "reviewed_by BIGINT COMMENT '审核人ID', " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', " +
+            "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', " +
+            "INDEX idx_user_id (user_id), " +
+            "INDEX idx_status (status), " +
+            "INDEX idx_type (type)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成长规划师申请表'",
+
+            // 小游戏表
+            "CREATE TABLE IF NOT EXISTS mini_games (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "game_code VARCHAR(20) NOT NULL UNIQUE COMMENT '游戏代码', " +
+            "game_name VARCHAR(50) NOT NULL COMMENT '游戏名称', " +
+            "description VARCHAR(255) COMMENT '游戏描述', " +
+            "icon VARCHAR(50) COMMENT '图标emoji', " +
+            "default_duration INT DEFAULT 5 COMMENT '默认时长(分钟)', " +
+            "default_points INT DEFAULT 5 COMMENT '默认奖励积分', " +
+            "game_url VARCHAR(255) COMMENT '游戏H5页面URL', " +
+            "sort_order INT DEFAULT 0 COMMENT '排序', " +
+            "status TINYINT DEFAULT 1 COMMENT '状态:0禁用1启用', " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+            "INDEX idx_game_code (game_code), " +
+            "INDEX idx_status (status)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
+);
+
+        // 执行创建表SQL
+        for (String sql : createTableSQLs) {
+            try {
+                jdbcTemplate.execute(sql);
+            } catch (Exception e) {
+                log.warn("创建表失败: {}", e.getMessage());
+            }
+        }
+        
+        log.info("数据表检查完成");
+    }
+
+    private void runMigrations() {
+        // 迁移1: 任务表添加执行者字段
+        try {
+            jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN executor_type ENUM('child', 'parent') DEFAULT 'child' COMMENT '执行者类型'");
+            log.info("已添加executor_type列到tasks表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN executor_id BIGINT COMMENT '执行者ID'");
+            log.info("已添加executor_id列到tasks表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE tasks ADD INDEX idx_executor (executor_type, executor_id)");
+            log.info("已添加idx_executor索引到tasks表");
+        } catch (Exception e) {
+            // 索引已存在,忽略错误
+        }
+
+        // 迁移2: 用户表添加角色和积分字段
+        try {
+            jdbcTemplate.execute("ALTER TABLE users ADD COLUMN roles VARCHAR(100) COMMENT '角色列表'");
+            log.info("已添加roles列到users表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE users ADD COLUMN total_points INT DEFAULT 0 COMMENT '家长总积分'");
+            log.info("已添加total_points列到users表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE users ADD INDEX idx_roles (roles(20))");
+            log.info("已添加idx_roles索引到users表");
+        } catch (Exception e) {
+            // 索引已存在,忽略错误
+        }
+
+        // 迁移3: users表添加新字段
+        try {
+            jdbcTemplate.execute("ALTER TABLE users ADD COLUMN id_card VARCHAR(18) COMMENT '身份证号'");
+            log.info("已添加id_card列到users表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE users ADD COLUMN real_name VARCHAR(50) COMMENT '真实姓名'");
+            log.info("已添加real_name列到users表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE users ADD COLUMN phone VARCHAR(11) COMMENT '手机号'");
+            log.info("已添加phone列到users表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE users ADD COLUMN birthday DATE COMMENT '生日'");
+            log.info("已添加birthday列到users表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE users ADD COLUMN gender ENUM('male', 'female') COMMENT '性别'");
+            log.info("已添加gender列到users表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE users ADD INDEX idx_phone (phone)");
+            log.info("已添加idx_phone索引到users表");
+        } catch (Exception e) {
+            // 索引已存在,忽略错误
+        }
+
+        // 迁移4: children表添加新字段
+        try {
+            jdbcTemplate.execute("ALTER TABLE children ADD COLUMN birthday DATE COMMENT '生日'");
+            log.info("已添加birthday列到children表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE children ADD COLUMN gender ENUM('male', 'female') COMMENT '性别'");
+            log.info("已添加gender列到children表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        // 迁移5: 修改users.role为支持teacher
+        try {
+            jdbcTemplate.execute("ALTER TABLE users MODIFY COLUMN role ENUM('parent', 'child', 'teacher') NOT NULL");
+            log.info("已更新users.role字段支持teacher角色");
+        } catch (Exception e) {
+            // 修改失败,忽略错误
+        }
+
+        // 迁移5.1: 修改users.role为支持admin
+        try {
+            jdbcTemplate.execute("ALTER TABLE users MODIFY COLUMN role ENUM('parent', 'child', 'teacher', 'admin') NOT NULL");
+            log.info("已更新users.role字段支持admin角色");
+        } catch (Exception e) {
+            // 修改失败,忽略错误
+        }
+
+        // 迁移5.2: 确保管理员角色为admin(修复旧数据遗留问题)
+        try {
+            int updated = jdbcTemplate.update("UPDATE admins SET role = 'admin' WHERE username = 'admin'");
+            if (updated > 0) {
+                log.info("已修正管理员角色为admin");
+            }
+        } catch (Exception e) {
+            // 更新失败,忽略错误
+        }
+
+        // 迁移6: 创建streets表(街道地址维度表)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS streets (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "province VARCHAR(50) COMMENT '省份', " +
+                    "province_code VARCHAR(20) COMMENT '省份编码', " +
+                    "city VARCHAR(50) COMMENT '城市', " +
+                    "city_code VARCHAR(20) COMMENT '城市编码', " +
+                    "district VARCHAR(50) COMMENT '区县', " +
+                    "district_code VARCHAR(20) COMMENT '区县编码', " +
+                    "street VARCHAR(100) COMMENT '街道', " +
+                    "street_code VARCHAR(20) COMMENT '街道编码', " +
+                    "full_name VARCHAR(200) COMMENT '完整地址', " +
+                    "level INT COMMENT '层级:1省2市3区4街道', " +
+                    "parent_id BIGINT COMMENT '父级ID', " +
+                    "sort_order INT DEFAULT 0 COMMENT '排序', " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                    "INDEX idx_level (level), " +
+                    "INDEX idx_parent_id (parent_id), " +
+                    "INDEX idx_street_code (street_code), " +
+                    "INDEX idx_full_name (full_name(50))" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
+            log.info("已创建streets表");
+        } catch (Exception e) {
+            // 表已存在,忽略错误
+        }
+
+        // 迁移7: families表添加地址字段
+        try {
+            jdbcTemplate.execute("ALTER TABLE families ADD COLUMN street_id BIGINT COMMENT '街道ID'");
+            log.info("已添加street_id列到families表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE families ADD COLUMN province VARCHAR(50) COMMENT '省份'");
+            jdbcTemplate.execute("ALTER TABLE families ADD COLUMN city VARCHAR(50) COMMENT '城市'");
+            jdbcTemplate.execute("ALTER TABLE families ADD COLUMN district VARCHAR(50) COMMENT '区县'");
+            jdbcTemplate.execute("ALTER TABLE families ADD COLUMN street VARCHAR(100) COMMENT '街道'");
+            log.info("已添加地址字段到families表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE families ADD INDEX idx_street_id (street_id)");
+            log.info("已添加idx_street_id索引到families表");
+        } catch (Exception e) {
+            // 索引已存在,忽略错误
+        }
+
+        // 迁移8: teachers表添加地址字段
+        try {
+            jdbcTemplate.execute("ALTER TABLE teachers ADD COLUMN street_id BIGINT COMMENT '街道ID'");
+            log.info("已添加street_id列到teachers表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE teachers ADD COLUMN province VARCHAR(50) COMMENT '省份'");
+            jdbcTemplate.execute("ALTER TABLE teachers ADD COLUMN city VARCHAR(50) COMMENT '城市'");
+            jdbcTemplate.execute("ALTER TABLE teachers ADD COLUMN district VARCHAR(50) COMMENT '区县'");
+            jdbcTemplate.execute("ALTER TABLE teachers ADD COLUMN street VARCHAR(100) COMMENT '街道'");
+            log.info("已添加地址字段到teachers表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE teachers ADD INDEX idx_street_id (street_id)");
+            log.info("已添加idx_street_id索引到teachers表");
+        } catch (Exception e) {
+            // 索引已存在,忽略错误
+        }
+
+        // 迁移9: users表添加成长规划师相关字段
+        try {
+            jdbcTemplate.execute("ALTER TABLE users ADD COLUMN teacher_no VARCHAR(50) COMMENT '成长规划师证书号'");
+            log.info("已添加teacher_no列到users表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE users ADD COLUMN teacher_photo VARCHAR(500) COMMENT '成长规划师照片'");
+            log.info("已添加teacher_photo列到users表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE users ADD COLUMN certificate_image VARCHAR(500) COMMENT '成长规划师证书照片'");
+            log.info("已添加certificate_image列到users表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE users ADD COLUMN teacher_status VARCHAR(20) DEFAULT 'pending' COMMENT '成长规划师审核状态'");
+            log.info("已添加teacher_status列到users表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE users ADD INDEX idx_teacher_status (teacher_status)");
+            log.info("已添加idx_teacher_status索引到users表");
+        } catch (Exception e) {
+            // 索引已存在,忽略错误
+        }
+
+        // 迁移10: users表添加teacherFamilyIds字段(成长规划师关联的家庭ID列表)
+        try {
+            jdbcTemplate.execute("ALTER TABLE users ADD COLUMN teacher_family_ids VARCHAR(500) COMMENT '成长规划师关联的家庭ID列表(逗号分隔)'");
+            log.info("已添加teacher_family_ids列到users表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        // 迁移11: families表添加teacherId字段(绑定的成长规划师ID)
+        try {
+            jdbcTemplate.execute("ALTER TABLE families ADD COLUMN teacher_id BIGINT COMMENT '绑定的成长规划师ID'");
+            log.info("已添加teacher_id列到families表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE families ADD INDEX idx_teacher_id (teacher_id)");
+            log.info("已添加idx_teacher_id索引到families表");
+        } catch (Exception e) {
+            // 索引已存在,忽略错误
+        }
+
+        // 迁移12: 创建 teacher_messages 表(家长咨询成长规划师)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS teacher_messages (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
+                    "parent_id BIGINT NOT NULL COMMENT '家长用户ID', " +
+                    "teacher_id BIGINT NOT NULL COMMENT '成长规划师用户ID', " +
+                    "content TEXT NOT NULL COMMENT '消息内容', " +
+                    "reply_content TEXT COMMENT '回复内容', " +
+                    "status VARCHAR(20) DEFAULT 'pending' COMMENT '状态:pending(待回复)/replied(已回复)', " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "replied_at DATETIME COMMENT '回复时间', " +
+                    "INDEX idx_family_id (family_id), " +
+                    "INDEX idx_teacher_id (teacher_id), " +
+                    "INDEX idx_parent_id (parent_id), " +
+                    "INDEX idx_status (status)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
+            log.info("已创建teacher_messages表");
+        } catch (Exception e) {
+            // 表已存在,忽略错误
+        }
+
+// 迁移13: admin_task_templates表添加teacher_id字段
+try {
+jdbcTemplate.execute("ALTER TABLE admin_task_templates ADD COLUMN teacher_id BIGINT COMMENT '成长规划师ID'");
+log.info("已添加teacher_id列到admin_task_templates表");
+} catch (Exception e) {
+// 列已存在,忽略错误
+}
+
+// 迁移13b: admin_task_templates表添加新字段
+try {
+jdbcTemplate.execute("ALTER TABLE admin_task_templates ADD COLUMN complete_types VARCHAR(100) COMMENT '完成方式JSON'");
+log.info("已添加complete_types列到admin_task_templates表");
+} catch (Exception e) {
+// 列已存在,忽略错误
+}
+
+try {
+jdbcTemplate.execute("ALTER TABLE admin_task_templates ADD COLUMN duration INT COMMENT '预计完成时长(分钟)'");
+log.info("已添加duration列到admin_task_templates表");
+} catch (Exception e) {
+// 列已存在,忽略错误
+}
+
+try {
+jdbcTemplate.execute("ALTER TABLE admin_task_templates ADD COLUMN review_type VARCHAR(20) COMMENT '审核人类型: creator/parent/teacher/ai'");
+log.info("已添加review_type列到admin_task_templates表");
+} catch (Exception e) {
+// 列已存在,忽略错误
+}
+
+try {
+jdbcTemplate.execute("ALTER TABLE admin_task_templates ADD COLUMN task_type VARCHAR(20) COMMENT '任务性质: onetime/recurring'");
+log.info("已添加task_type列到admin_task_templates表");
+} catch (Exception e) {
+// 列已存在,忽略错误
+}
+
+try {
+jdbcTemplate.execute("ALTER TABLE admin_task_templates ADD COLUMN frequency VARCHAR(20) COMMENT '频率: daily/weekly/monthly'");
+log.info("已添加frequency列到admin_task_templates表");
+} catch (Exception e) {
+// 列已存在,忽略错误
+}
+
+try {
+jdbcTemplate.execute("ALTER TABLE admin_task_templates ADD COLUMN max_frequency INT COMMENT '周期内最多完成次数'");
+log.info("已添加max_frequency列到admin_task_templates表");
+} catch (Exception e) {
+// 列已存在,忽略错误
+}
+
+// 迁移14: rewards表child_id字段允许NULL(模板奖励不需要child_id)
+        try {
+            jdbcTemplate.execute("ALTER TABLE rewards MODIFY COLUMN child_id BIGINT NULL COMMENT '孩子ID,模板奖励为NULL'");
+            log.info("已修改rewards.child_id字段允许NULL");
+        } catch (Exception e) {
+            // 修改失败,忽略错误
+        }
+
+        // 迁移15: admins表添加缺失列
+        try {
+            jdbcTemplate.execute("ALTER TABLE admins ADD COLUMN nickname VARCHAR(50) DEFAULT NULL AFTER password");
+            log.info("已添加nickname列到admins表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE admins ADD COLUMN email VARCHAR(100) DEFAULT NULL AFTER phone");
+            log.info("已添加email列到admins表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE admins MODIFY COLUMN status VARCHAR(20) DEFAULT 'active'");
+            log.info("已修改admins.status字段类型");
+        } catch (Exception e) {
+            // 修改失败,忽略错误
+        }
+
+        // 迁移16: 创建用户操作日志表
+try {
+jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS user_operation_logs (" +
+"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+"user_id BIGINT COMMENT \'用户ID\', " +
+"nickname VARCHAR(50) COMMENT \'用户昵称\', " +
+"operation_type VARCHAR(20) NOT NULL COMMENT \'操作类型: login/logout/create/update/delete/query等\', " +
+"description VARCHAR(500) COMMENT \'操作描述\', " +
+"request_path VARCHAR(200) COMMENT \'请求路径\', " +
+"request_method VARCHAR(10) COMMENT \'请求方法\', " +
+"request_params TEXT COMMENT \'请求参数(脱敏)\', " +
+"response_status INT COMMENT \'响应状态码\', " +
+"client_ip VARCHAR(50) COMMENT \'客户端IP\', " +
+"user_agent VARCHAR(500) COMMENT \'User-Agent\', " +
+"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+"INDEX idx_user_id (user_id), " +
+"INDEX idx_operation_type (operation_type), " +
+"INDEX idx_created_at (created_at)" +
+") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
+log.info("已创建user_operation_logs表");
+} catch (Exception e) {
+// 表已存在,忽略错误
+}
+
+// 迁移16: tasks表添加bonus_rule字段(POINT-002: 超额积分规则)
+try {
+jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN bonus_rule VARCHAR(500) COMMENT \'超额积分规则JSON\'");
+log.info("已添加bonus_rule列到tasks表");
+} catch (Exception e) {
+// 列已存在,忽略错误
+}
+
+// 迁移17: tasks表添加points_expire_days字段(POINT-009: 积分有效期)
+try {
+jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN points_expire_days INT DEFAULT 0 COMMENT \'积分有效期天数,0表示永久有效\'");
+log.info("已添加points_expire_days列到tasks表");
+} catch (Exception e) {
+// 列已存在,忽略错误
+}
+
+// 迁移18: points_log表添加expire_at字段(POINT-009: 积分有效期)
+try {
+jdbcTemplate.execute("ALTER TABLE points_log ADD COLUMN expire_at DATE COMMENT \'积分过期日期\'");
+log.info("已添加expire_at列到points_log表");
+} catch (Exception e) {
+// 列已存在,忽略错误
+}
+
+// 迁移19: points_log表添加expired字段(POINT-009: 过期状态标记)
+try {
+jdbcTemplate.execute("ALTER TABLE points_log ADD COLUMN expired TINYINT DEFAULT 0 COMMENT \'是否已过期: 0否1是\'");
+log.info("已添加expired列到points_log表");
+} catch (Exception e) {
+// 列已存在,忽略错误
+}
+
+// 迁移20: tasks表添加template_id字段(TASK-004: 重复任务模板追踪)
+try {
+jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN template_id BIGINT COMMENT \'模板任务ID\'");
+log.info("已添加template_id列到tasks表");
+} catch (Exception e) {
+// 列已存在,忽略错误
+}
+
+        // 迁移21: tasks表添加is_template字段(TASK-004: 标记模板任务)
+        try {
+            jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN is_template TINYINT DEFAULT 0 COMMENT '是否为模板任务'");
+            log.info("已添加is_template列到tasks表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        // 迁移22: 修复tasks表child_id和deadline允许NULL(支持多孩子和家长任务)
+        try {
+            jdbcTemplate.execute("ALTER TABLE tasks MODIFY COLUMN child_id BIGINT NULL COMMENT '执行者ID'");
+            log.info("已修改tasks.child_id允许NULL");
+        } catch (Exception e) {
+            // 修改失败,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE tasks MODIFY COLUMN deadline DATETIME NULL COMMENT '截止时间'");
+            log.info("已修改tasks.deadline允许NULL");
+        } catch (Exception e) {
+            // 修改失败,忽略错误
+        }
+
+        // 迁移23: tasks表添加新字段
+        try {
+            jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN task_type VARCHAR(20) COMMENT '任务性质: onetime/recurring'");
+            log.info("已添加task_type列到tasks表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN frequency VARCHAR(20) COMMENT '频率: daily/weekly/monthly'");
+            log.info("已添加frequency列到tasks表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN max_frequency INT COMMENT '周期内最多完成次数'");
+            log.info("已添加max_frequency列到tasks表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN complete_types VARCHAR(100) COMMENT '完成方式JSON'");
+            log.info("已添加complete_types列到tasks表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN review_type VARCHAR(20) COMMENT '审核人类型: creator/parent/teacher/ai'");
+            log.info("已添加review_type列到tasks表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN earliest_start DATETIME COMMENT '最早开始时间'");
+            log.info("已添加earliest_start列到tasks表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN latest_end DATETIME COMMENT '最晚结束时间'");
+            log.info("已添加latest_end列到tasks表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        // 迁移24: tasks表添加minigame_code字段(小游戏任务)
+        try {
+            jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN minigame_code VARCHAR(50) DEFAULT NULL COMMENT '小游戏代码: schulte/1a2b/sudoku'");
+            log.info("已添加minigame_code列到tasks表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        // 迁移25: tasks表添加duration字段(TASK-011: 任务预计时长)
+        try {
+            jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN duration INT DEFAULT 0 COMMENT '预计时长(分钟)'");
+            log.info("已添加duration列到tasks表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE tasks ADD INDEX idx_minigame (minigame_code) WHERE minigame_code IS NOT NULL");
+            log.info("已添加idx_minigame索引到tasks表");
+        } catch (Exception e) {
+            // 索引已存在,忽略错误
+        }
+
+        // assessment_appointments 新字段
+        try {
+            jdbcTemplate.execute("ALTER TABLE assessment_appointments ADD COLUMN user_id BIGINT COMMENT '用户ID'");
+        } catch (Exception e) {
+        }
+        try {
+            jdbcTemplate.execute("ALTER TABLE assessment_appointments ADD COLUMN guide_id BIGINT COMMENT '成长规划师ID'");
+        } catch (Exception e) {
+        }
+        try {
+            jdbcTemplate.execute("ALTER TABLE assessment_appointments ADD COLUMN package_id BIGINT COMMENT '套餐ID'");
+        } catch (Exception e) {
+        }
+        try {
+            jdbcTemplate.execute("ALTER TABLE assessment_appointments ADD COLUMN remark VARCHAR(500) COMMENT '备注'");
+        } catch (Exception e) {
+        }
+
+        // assessment_orders 表
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS assessment_orders (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "order_no VARCHAR(64) NOT NULL UNIQUE, " +
+                    "family_id BIGINT NOT NULL, " +
+                    "user_id BIGINT NOT NULL, " +
+                    "child_id BIGINT, " +
+                    "guide_id BIGINT NOT NULL, " +
+                    "package_id BIGINT NOT NULL, " +
+                    "guide_name VARCHAR(50), " +
+                    "package_name VARCHAR(100), " +
+                    "total_price BIGINT DEFAULT 0, " +
+                    "discount_amount BIGINT DEFAULT 0, " +
+                    "actual_price BIGINT DEFAULT 0, " +
+                    "status VARCHAR(20) DEFAULT 'pending', " +
+                    "pay_time VARCHAR(50), " +
+                    "pay_type VARCHAR(20), " +
+                    "transaction_id VARCHAR(64), " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                    "INDEX idx_family (family_id), " +
+                    "INDEX idx_guide (guide_id), " +
+                    "INDEX idx_status (status)) " +
+                    "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评估订单表'");
+        } catch (Exception e) {
+        }
+
+        // 迁移26: guide_applications表添加address和bank_card字段
+        try {
+            jdbcTemplate.execute("ALTER TABLE guide_applications ADD COLUMN address VARCHAR(500) COMMENT '地址'");
+            log.info("已添加address列到guide_applications表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE guide_applications ADD COLUMN bank_card VARCHAR(50) COMMENT '银行卡号'");
+            log.info("已添加bank_card列到guide_applications表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE guide_applications ADD COLUMN bank_name VARCHAR(100) COMMENT '开户行'");
+            log.info("已添加bank_name列到guide_applications表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        // 迁移27: users表添加address字段(成长规划师地址)
+        try {
+            jdbcTemplate.execute("ALTER TABLE users ADD COLUMN address VARCHAR(500) COMMENT '地址'");
+            log.info("已添加address列到users表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        // 迁移28: 创建guide_application_records表(申请过程记录)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS guide_application_records (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "application_id BIGINT NOT NULL COMMENT '关联申请ID', " +
+                    "user_id BIGINT NOT NULL COMMENT '操作人用户ID', " +
+                    "action VARCHAR(50) NOT NULL COMMENT '操作: submit/approve/reject/withdraw/resubmit', " +
+                    "from_status VARCHAR(20) COMMENT '操作前状态', " +
+                    "to_status VARCHAR(20) NOT NULL COMMENT '操作后状态', " +
+                    "remark VARCHAR(500) COMMENT '备注/原因', " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "INDEX idx_application_id (application_id), " +
+                    "INDEX idx_user_id (user_id), " +
+                    "INDEX idx_action (action)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成长规划师申请过程记录表'");
+            log.info("已创建guide_application_records表");
+        } catch (Exception e) {
+            // 表已存在,忽略错误
+        }
+
+        // 迁移20: children表添加system_points字段
+        try {
+            Integer exists = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'children' AND COLUMN_NAME = 'system_points'",
+                Integer.class);
+            if (exists == null || exists == 0) {
+                jdbcTemplate.execute("ALTER TABLE children ADD COLUMN system_points INT DEFAULT 0 COMMENT '系统积分'");
+                log.info("已添加system_points列到children表");
+            } else {
+                log.info("system_points列已存在,跳过");
+            }
+        } catch (Exception e) {
+            log.warn("检查/添加system_points列失败: {}", e.getMessage());
+        }
+
+        // 迁移21: points_log表添加category字段
+        try {
+            Integer exists = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'points_log' AND COLUMN_NAME = 'category'",
+                Integer.class);
+            if (exists == null || exists == 0) {
+                jdbcTemplate.execute("ALTER TABLE points_log ADD COLUMN category VARCHAR(20) DEFAULT 'task' COMMENT '积分分类: task任务积分, system系统积分'");
+                log.info("已添加category列到points_log表");
+            } else {
+                log.info("category列已存在,跳过");
+            }
+        } catch (Exception e) {
+            log.warn("检查/添加category列失败: {}", e.getMessage());
+        }
+
+        // 迁移22: 创建invite_card表
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS invite_card (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "type VARCHAR(20) NOT NULL COMMENT 'child/family/guide', " +
+                    "code VARCHAR(64) NOT NULL UNIQUE COMMENT '邀请码', " +
+                    "ref_id BIGINT NOT NULL COMMENT '关联ID(childId/familyId)', " +
+                    "creator_id BIGINT NOT NULL COMMENT '创建人', " +
+                    "expires_at DATETIME NOT NULL, " +
+                    "used TINYINT DEFAULT 0, " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                    "INDEX idx_code (code), " +
+                    "INDEX idx_ref_type (ref_id, type)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='邀请卡片'");
+            log.info("已创建invite_card表");
+        } catch (Exception e) {
+            // 表已存在,忽略错误
+        }
+
+        // 迁移23: 创建wishes表
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS wishes (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "family_id BIGINT COMMENT '家庭ID', " +
+                    "child_id BIGINT COMMENT '目标孩子ID', " +
+                    "created_by BIGINT COMMENT '创建者用户ID', " +
+                    "creator_type VARCHAR(20) COMMENT '创建者类型: child/parent', " +
+                    "title VARCHAR(200) NOT NULL COMMENT '心愿标题', " +
+                    "description VARCHAR(500) COMMENT '心愿描述', " +
+                    "category VARCHAR(32) COMMENT '心愿分类', " +
+                    "points_required INT COMMENT '所需积分', " +
+                    "points_set_by BIGINT COMMENT '定价者ID', " +
+                    "points_set_at DATETIME COMMENT '定价时间', " +
+                    "status VARCHAR(32) DEFAULT 'active' COMMENT '状态: draft/active/pending_exchange/approved/exchanging/fulfilled/cancelled/rejected', " +
+                    "exchange_requested_at DATETIME COMMENT '申请兑换时间', " +
+                    "exchange_approved_by BIGINT COMMENT '批准者ID', " +
+                    "exchange_approved_at DATETIME COMMENT '批准时间', " +
+                    "points_deducted_at DATETIME COMMENT '扣积分时间', " +
+                    "fulfilled_by BIGINT COMMENT '兑现者ID', " +
+                    "fulfilled_at DATETIME COMMENT '兑现时间', " +
+                    "child_confirmed_at DATETIME COMMENT '孩子确认收货时间', " +
+                    "rejected_by BIGINT COMMENT '拒绝者ID', " +
+                    "rejected_at DATETIME COMMENT '拒绝时间', " +
+                    "reject_reason VARCHAR(500) COMMENT '拒绝原因', " +
+                    "reject_count INT DEFAULT 0 COMMENT '被拒绝次数', " +
+                    "cancelled_by BIGINT COMMENT '取消者ID', " +
+                    "cancelled_at DATETIME COMMENT '取消时间', " +
+                    "cancel_reason VARCHAR(500) COMMENT '取消原因', " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                    "INDEX idx_family_id (family_id), " +
+                    "INDEX idx_child_id (child_id), " +
+                    "INDEX idx_status (status)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='心愿单表'");
+            log.info("已创建wishes表");
+        } catch (Exception e) {
+            log.warn("创建wishes表可能已存在: {}", e.getMessage());
+        }
+
+        // 迁移24: users表添加个人信息补充字段
+        try {
+            Integer exists = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'ethnicity'",
+                Integer.class);
+            if (exists == null || exists == 0) {
+                jdbcTemplate.execute("ALTER TABLE users " +
+                    "ADD COLUMN ethnicity VARCHAR(32) DEFAULT NULL COMMENT '民族', " +
+                    "ADD COLUMN blood_type VARCHAR(16) DEFAULT NULL COMMENT '血型', " +
+                    "ADD COLUMN highest_education VARCHAR(32) DEFAULT NULL COMMENT '最高学历', " +
+                    "ADD COLUMN marital_status VARCHAR(16) DEFAULT NULL COMMENT '婚姻状态', " +
+                    "ADD COLUMN family_role VARCHAR(16) DEFAULT NULL COMMENT '家长身份: 爸爸/妈妈/爷爷/奶奶/姥爷/姥姥/其他'");
+                log.info("已添加个人信息补充字段到users表");
+            } else {
+                log.info("个人信息补充字段已存在,跳过");
+            }
+        } catch (Exception e) {
+            log.warn("检查/添加个人信息补充字段失败: {}", e.getMessage());
+        }
+
+        // Phase 0: 汐福俱乐部 — 服务商字段
+        try {
+            Integer exists = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'vendor_type'",
+                Integer.class);
+            if (exists == null || exists == 0) {
+                jdbcTemplate.execute("ALTER TABLE users ADD COLUMN vendor_type VARCHAR(32) DEFAULT NULL COMMENT '服务商类型: planner/activity_provider/product_supplier'");
+                jdbcTemplate.execute("ALTER TABLE users ADD COLUMN vendor_status VARCHAR(16) DEFAULT NULL COMMENT '服务商审核状态: pending/approved/rejected'");
+                jdbcTemplate.execute("ALTER TABLE users ADD COLUMN vendor_reject_reason VARCHAR(500) DEFAULT NULL COMMENT '审核拒绝原因'");
+                jdbcTemplate.execute("ALTER TABLE users ADD COLUMN is_family_admin TINYINT DEFAULT 0 COMMENT '是否家庭管理员'");
+                jdbcTemplate.execute("ALTER TABLE users ADD COLUMN vendor_info VARCHAR(2000) DEFAULT NULL COMMENT '服务商资质信息(JSON)'");
+                log.info("已添加汐福俱乐部服务商字段到users表");
+            } else {
+                log.info("汐福俱乐部服务商字段已存在,跳过");
+            }
+        } catch (Exception e) {
+            log.warn("检查/添加服务商字段失败: {}", e.getMessage());
+        }
+
+        // Phase 0: 迁移已有规划师到服务商体系
+        try {
+            int updated = jdbcTemplate.update(
+                "UPDATE users SET vendor_type = 'planner', vendor_status = 'approved' WHERE teacher_status = 'approved' AND vendor_type IS NULL"
+            );
+            if (updated > 0) {
+                log.info("已迁移 {} 名规划师到服务商体系", updated);
+            }
+        } catch (Exception e) {
+            log.warn("迁移规划师到服务商体系失败: {}", e.getMessage());
+        }
+
+        // ==================== Phase 1: 汐福俱乐部商品系统 ====================
+
+        // 商品表
+        try {
+            jdbcTemplate.execute(
+                "CREATE TABLE IF NOT EXISTS products (" +
+                "  id BIGINT AUTO_INCREMENT PRIMARY KEY," +
+                "  name VARCHAR(200) NOT NULL," +
+                "  description VARCHAR(1000)," +
+                "  intro TEXT," +
+                "  cover_image VARCHAR(500)," +
+                "  images TEXT," +
+                "  price DECIMAL(10,2) NOT NULL DEFAULT 0," +
+                "  stock INT NOT NULL DEFAULT 0," +
+                "  sales_count INT NOT NULL DEFAULT 0," +
+                "  product_type VARCHAR(32) NOT NULL COMMENT 'activity/course/physical/digital/service'," +
+                "  vendor_id BIGINT," +
+                "  vendor_name VARCHAR(100)," +
+                "  status VARCHAR(32) NOT NULL DEFAULT 'draft' COMMENT 'draft/pending/approved/rejected/on_shelf/off_shelf'," +
+                "  reject_reason VARCHAR(500)," +
+                "  member_price DECIMAL(10,2)," +
+                "  member_eligible VARCHAR(32) DEFAULT 'none' COMMENT 'all/budget/none'," +
+                "  domain VARCHAR(32) COMMENT 'action/mind/body/wisdom/wealth'," +
+                "  external_source VARCHAR(64)," +
+                "  external_id VARCHAR(128)," +
+                "  external_data TEXT," +
+                "  created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
+                "  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
+                ")"
+            );
+        } catch (Exception e) {
+            log.warn("创建products表失败: {}", e.getMessage());
+        }
+
+        // 订单表
+        try {
+            jdbcTemplate.execute(
+                "CREATE TABLE IF NOT EXISTS product_orders (" +
+                "  id BIGINT AUTO_INCREMENT PRIMARY KEY," +
+                "  order_no VARCHAR(64) NOT NULL UNIQUE," +
+                "  product_id BIGINT NOT NULL," +
+                "  product_name VARCHAR(200)," +
+                "  product_type VARCHAR(32)," +
+                "  buyer_id BIGINT NOT NULL," +
+                "  family_id BIGINT," +
+                "  quantity INT NOT NULL DEFAULT 1," +
+                "  unit_price DECIMAL(10,2) NOT NULL," +
+                "  total_amount DECIMAL(10,2) NOT NULL," +
+                "  status VARCHAR(32) NOT NULL DEFAULT 'pending' COMMENT 'pending/paid/completed/cancelled/refunded'," +
+                "  payment_method VARCHAR(32) COMMENT 'wechat/points/membership'," +
+                "  remark VARCHAR(500)," +
+                "  paid_at DATETIME," +
+                "  created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
+                "  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
+                ")"
+            );
+        } catch (Exception e) {
+            log.warn("创建product_orders表失败: {}", e.getMessage());
+        }
+
+        // 系统配置表
+        try {
+            jdbcTemplate.execute(
+                "CREATE TABLE IF NOT EXISTS sys_config (" +
+                "  id BIGINT AUTO_INCREMENT PRIMARY KEY," +
+                "  config_key VARCHAR(64) NOT NULL UNIQUE," +
+                "  config_value TEXT," +
+                "  description VARCHAR(500)," +
+                "  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
+                ")"
+            );
+        } catch (Exception e) {
+            log.warn("创建sys_config表失败: {}", e.getMessage());
+        }
+
+        // SysConfig 种子数据
+        try {
+            insertSysConfigSeed("points_exchange_rate", "0.01", "积分兑换汇率(1积分=0.01元)");
+            insertSysConfigSeed("points_deduct_cap", "0.5", "积分抵扣上限(50%)");
+            insertSysConfigSeed("member_fee_standard", "1314", "标准会员年费(元)");
+            insertSysConfigSeed("member_fee_premium", "3650", "高级会员年费(元)");
+            insertSysConfigSeed("member_eligible_budget", "1314", "等值商品选择预算上限(元)");
+            insertSysConfigSeed("renewal_discount_rate", "0.85", "续费折扣率");
+            insertSysConfigSeed("member_discount_standard", "0.9", "标准会员折扣率");
+            insertSysConfigSeed("member_discount_premium", "0.8", "高级会员折扣率");
+            log.info("SysConfig 种子数据已加载");
+        } catch (Exception e) {
+            log.warn("SysConfig 种子数据初始化失败: {}", e.getMessage());
+        }
+
+        // Product 种子数据(5 个示例商品,覆盖全部 5 种 productType)
+        try {
+            insertProductSeed("亲子关系工作坊", "线下半日工作坊,重建家庭对话模式", "activity", new java.math.BigDecimal("388.00"), "action", "all");
+            insertProductSeed("情绪管理21天训练营", "21天视频课程,识别并管理情绪", "course", new java.math.BigDecimal("199.00"), "mind", "all");
+            insertProductSeed("肠道菌群检测套装", "家用检测 + 报告解读", "physical", new java.math.BigDecimal("599.00"), "body", "budget");
+            insertProductSeed("儿童多模态认知测评", "在线测评 + 个性化建议", "digital", new java.math.BigDecimal("99.00"), "wisdom", "none");
+            insertProductSeed("成长规划师1对1咨询", "60分钟专业规划", "service", new java.math.BigDecimal("500.00"), "wealth", "budget");
+            log.info("Product 种子数据已加载");
+        } catch (Exception e) {
+            log.warn("Product 种子数据初始化失败: {}", e.getMessage());
+        }
+
+        log.info("数据库迁移完成");
+    }
+
+    private void initializeDefaultData() {
+        // 初始化预设奖励模板
+        try {
+            // 检查是否已有预设奖励
+            Integer count = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM rewards WHERE is_template = 1", Integer.class);
+            
+if (count == null || count == 0) {
+// 插入预设奖励模板(child_id为NULL表示模板)
+jdbcTemplate.execute("INSERT INTO rewards (family_id, child_id, title, points_required, category, is_template) VALUES " +
+"(0, NULL, \'看动画片30分钟\', 30, \'精神类\', 1), " +
+"(0, NULL, \'玩平板游戏30分钟\', 40, \'特权类\', 1), " +
+"(0, NULL, \'去游乐场\', 100, \'物质类\', 1), " +
+"(0, NULL, \'买玩具\', 150, \'物质类\', 1), " +
+"(0, NULL, \'吃冰淇淋\', 20, \'物质类\', 1), " +
+"(0, NULL, \'选择晚餐\', 25, \'特权类\', 1)");
+log.info("预设奖励模板已初始化");
+}
+        } catch (Exception e) {
+            log.warn("初始化预设奖励失败: {}", e.getMessage());
+        }
+
+        // 初始化奖励分类
+        try {
+            Integer count = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM reward_categories WHERE family_id = 0", Integer.class);
+            
+            if (count == null || count == 0) {
+                jdbcTemplate.execute("INSERT INTO reward_categories (family_id, name, icon, sort_order) VALUES " +
+                    "(0, '物质类', 'gift', 1), " +
+                    "(0, '精神类', 'star', 2), " +
+                    "(0, '特权类', 'crown', 3)");
+                log.info("奖励分类已初始化");
+            }
+        } catch (Exception e) {
+            log.warn("初始化奖励分类失败: {}", e.getMessage());
+        }
+
+        // 初始化打卡里程碑
+        try {
+            Integer count = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM streak_milestones", Integer.class);
+            
+            if (count == null || count == 0) {
+                jdbcTemplate.execute("INSERT INTO streak_milestones (days, reward_points) VALUES " +
+                    "(3, 5), (7, 10), (30, 30), (100, 100)");
+                log.info("打卡里程碑已初始化");
+            }
+        } catch (Exception e) {
+            log.warn("初始化打卡里程碑失败: {}", e.getMessage());
+        }
+
+        // 初始化勋章定义
+        try {
+            Integer count = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM badge_definitions", Integer.class);
+            
+            if (count == null || count == 0) {
+                jdbcTemplate.execute("INSERT INTO badge_definitions (badge_type, name, description, icon, condition_type, condition_value, reward_points) VALUES " +
+                    "('first_task', '初次完成任务', '首次完成任意任务', 'star', 'task_count', 1, 0), " +
+                    "('streak_3', '连续3天', '连续3天按时完成任务', 'fire', 'streak_days', 3, 5), " +
+                    "('streak_7', '连续7天', '连续7天按时完成任务', 'fire', 'streak_days', 7, 10), " +
+                    "('points_50', '积分达人', '一周获得50积分', 'trophy', 'weekly_points', 50, 0), " +
+                    "('task_100', '任务达人', '完成100个任务', 'medal', 'task_count', 100, 20)");
+                log.info("勋章定义已初始化");
+            }
+        } catch (Exception e) {
+            log.warn("初始化勋章定义失败: {}", e.getMessage());
+        }
+
+        // 初始化小游戏数据
+        try {
+            Integer count = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM mini_games", Integer.class);
+            
+            if (count == null || count == 0) {
+                jdbcTemplate.execute("INSERT INTO mini_games (game_code, game_name, description, icon, default_duration, default_points, game_url, sort_order, status) VALUES " +
+                    "('schulte', '舒尔特方格', '训练专注力和眼动能力,找到1-25按顺序点击', '🎯', 5, 5, '/pages/games/schulte', 1, 1), " +
+                    "('1a2b', '猜数字', '猜对数字和位置获得积分,锻炼逻辑思维', '🔢', 10, 5, '/pages/games/1a2b', 2, 1), " +
+                    "('sudoku', '数独', '填充数字使每行每列每个宫格都包含1-9', '🔳', 15, 8, '/pages/games/sudoku', 3, 1)");
+                log.info("小游戏数据已初始化");
+            }
+        } catch (Exception e) {
+            log.warn("初始化小游戏数据失败: {}", e.getMessage());
+        }
+
+// 初始化默认管理员
+try {
+  // 先检查并添加role列(如果不存在)
+  try {
+    jdbcTemplate.execute("ALTER TABLE admins ADD COLUMN role VARCHAR(20) DEFAULT \'admin\' AFTER email");
+    log.info("已添加role列到admins表");
+  } catch (Exception e) {
+    // 列已存在,忽略错误
+  }
+
+  Integer count = jdbcTemplate.queryForObject(
+  "SELECT COUNT(*) FROM admins", Integer.class);
+
+  if (count == null || count == 0) {
+    // 默认管理员: 手机号 13800138000, 密码 admin123
+    String encryptedPassword = org.springframework.util.DigestUtils.md5DigestAsHex("xzyj_admin_salt_admin123".getBytes());
+    jdbcTemplate.execute("INSERT INTO admins (username, password, real_name, phone, email, role, status) VALUES " +
+    "(\'admin\', \'" + encryptedPassword + "\', \'系统管理员\', \'13800138000\', \'admin@xzyj.com\', \'admin\', 1)");
+    log.info("默认管理员已初始化: 手机号 13800138000, 密码 admin123");
+  }
+} catch (Exception e) {
+  log.warn("初始化默认管理员失败: {}", e.getMessage());
+}
+
+    // 初始化成长规划师WEB管理账号
+    try {
+      Integer teacherCount = jdbcTemplate.queryForObject(
+        "SELECT COUNT(*) FROM admins WHERE username = 'teacher'", Integer.class);
+      if (teacherCount == null || teacherCount == 0) {
+        String teacherPassword = org.springframework.util.DigestUtils.md5DigestAsHex("xzyj_admin_salt_teacher123".getBytes());
+        jdbcTemplate.execute("INSERT INTO admins (username, password, real_name, phone, role, status) VALUES " +
+          "('teacher', '" + teacherPassword + "', '成长规划师', '13800138003', 'teacher', 1)");
+        log.info("成长规划师管理账号已初始化: 用户名 teacher, 密码 teacher123");
+      }
+    } catch (Exception e) {
+      log.warn("初始化成长规划师管理账号失败: {}", e.getMessage());
+    }
+
+        // 初始化任务模板示例数据
+        try {
+            Integer count = jdbcTemplate.queryForObject(
+                    "SELECT COUNT(*) FROM admin_task_templates", Integer.class);
+
+            if (count == null || count == 0) {
+                jdbcTemplate.execute("INSERT INTO admin_task_templates (title, description, points, category, need_review, recommended_age, difficulty, is_active) VALUES " +
+                        "('完成作业', '按时完成学校布置的家庭作业', 10, '学习', 1, 6, 'medium', 1), " +
+                        "('阅读30分钟', '自主阅读课外书籍30分钟', 8, '阅读', 0, 5, 'easy', 1), " +
+                        "('整理房间', '收拾整理自己的房间和书桌', 6, '家务', 0, 6, 'easy', 1), " +
+                        "('洗碗', '饭后帮忙清洗餐具', 5, '家务', 1, 8, 'medium', 1), " +
+                        "('户外运动1小时', '进行跑步、球类等户外运动', 12, '运动', 0, 7, 'hard', 1), " +
+                        "('练习书法', '练习毛笔或硬笔书法20分钟', 7, '学习', 0, 7, 'medium', 1), " +
+                        "('早睡早起', '晚上9点前睡觉,早上7点前起床', 5, '生活习惯', 0, 5, 'easy', 1), " +
+                        "('背诵古诗', '背诵一首古诗词', 8, '学习', 1, 6, 'medium', 1)");
+                log.info("任务模板示例数据已初始化");
+            }
+        } catch (Exception e) {
+            log.warn("初始化任务模板失败: {}", e.getMessage());
+        }
+
+        // 初始化街道数据示例(广东省广州市)
+        try {
+            Integer count = jdbcTemplate.queryForObject(
+                    "SELECT COUNT(*) FROM streets", Integer.class);
+
+            if (count == null || count == 0) {
+                // 省
+                jdbcTemplate.execute("INSERT INTO streets (province, province_code, full_name, level, parent_id, sort_order) VALUES " +
+                        "('广东省', '44', '广东省', 1, NULL, 1)");
+                
+                // 市
+                jdbcTemplate.execute("INSERT INTO streets (province, province_code, city, city_code, full_name, level, parent_id, sort_order) VALUES " +
+                        "('广东省', '44', '广州市', '4401', '广东省广州市', 2, 1, 1), " +
+                        "('广东省', '44', '深圳市', '4403', '广东省深圳市', 2, 1, 2), " +
+                        "('广东省', '44', '珠海市', '4404', '广东省珠海市', 2, 1, 3)");
+                
+                // 区
+                jdbcTemplate.execute("INSERT INTO streets (province, province_code, city, city_code, district, district_code, full_name, level, parent_id, sort_order) VALUES " +
+                        "('广东省', '44', '广州市', '4401', '天河区', '440106', '广东省广州市天河区', 3, 2, 1), " +
+                        "('广东省', '44', '广州市', '4401', '越秀区', '440104', '广东省广州市越秀区', 3, 2, 2), " +
+                        "('广东省', '44', '广州市', '4401', '番禺区', '440113', '广东省广州市番禺区', 3, 2, 3), " +
+                        "('广东省', '44', '深圳市', '4403', '南山区', '440305', '广东省深圳市南山区', 3, 3, 1), " +
+                        "('广东省', '44', '深圳市', '4403', '福田区', '440304', '广东省深圳市福田区', 3, 3, 2)");
+                
+                // 街道
+                jdbcTemplate.execute("INSERT INTO streets (province, province_code, city, city_code, district, district_code, street, street_code, full_name, level, parent_id, sort_order) VALUES " +
+                        "('广东省', '44', '广州市', '4401', '天河区', '440106', '天河南街道', '440106001', '广东省广州市天河区天河南街道', 4, 4, 1), " +
+                        "('广东省', '44', '广州市', '4401', '天河区', '440106', '沙河街道', '440106002', '广东省广州市天河区沙河街道', 4, 4, 2), " +
+                        "('广东省', '44', '广州市', '4401', '越秀区', '440104', '东山街道', '440104001', '广东省广州市越秀区东山街道', 4, 5, 1), " +
+                        "('广东省', '44', '广州市', '4401', '番禺区', '440113', '市桥街道', '440113001', '广东省广州市番禺区市桥街道', 4, 6, 1), " +
+                        "('广东省', '44', '深圳市', '4403', '南山区', '440305', '南山街道', '440305001', '广东省深圳市南山区南山街道', 4, 7, 1), " +
+                        "('广东省', '44', '深圳市', '4403', '福田区', '440304', '福田街道', '440304001', '广东省深圳市福田区福田街道', 4, 8, 1)");
+                
+                log.info("街道示例数据已初始化");
+            }
+        } catch (Exception e) {
+            log.warn("初始化街道数据失败: {}", e.getMessage());
+        }
+
+try {
+  Integer count = jdbcTemplate.queryForObject(
+  "SELECT COUNT(*) FROM admins", Integer.class);
+
+  if (count == null || count == 0) {
+    // 默认管理员: 手机号 13800138000, 密码 admin123
+    String encryptedPassword = org.springframework.util.DigestUtils.md5DigestAsHex("xzyj_admin_salt_admin123".getBytes());
+    jdbcTemplate.execute("INSERT INTO admins (username, password, real_name, phone, email, role, status) VALUES " +
+    "('admin', '" + encryptedPassword + "', '系统管理员', '13800138000', 'admin@xzyj.com', 'admin', 1)");
+    log.info("默认管理员已初始化: 手机号 13800138000, 密码 admin123");
+  }
+} catch (Exception e) {
+  log.warn("初始化任务模板失败: {}", e.getMessage());
+}
+
+// 初始化 users 表测试用户
+try {
+  // 创建默认家庭(如果不存在)
+  Integer familyCount = jdbcTemplate.queryForObject(
+    "SELECT COUNT(*) FROM families WHERE id = 1", Integer.class);
+  if (familyCount == null || familyCount == 0) {
+    jdbcTemplate.execute("INSERT INTO families (id, name, invite_code) VALUES (1, '默认家庭', '888888')");
+    log.info("默认家庭已创建");
+  }
+
+  // 密码加密: MD5("xzyj_salt_" + password)
+  String adminPassword = org.springframework.util.DigestUtils.md5DigestAsHex("xzyj_salt_admin123".getBytes());
+  String teacherPassword = org.springframework.util.DigestUtils.md5DigestAsHex("xzyj_salt_teacher123".getBytes());
+
+  // 创建管理员用户(管理员角色,密码 admin123)
+  Integer adminUserCount = jdbcTemplate.queryForObject(
+    "SELECT COUNT(*) FROM users WHERE phone = '13800138000'", Integer.class);
+  if (adminUserCount == null || adminUserCount == 0) {
+    jdbcTemplate.execute("INSERT INTO users (openid, family_id, role, phone, nickname, real_name, password) VALUES " +
+      "('seed_admin_13800138000', 1, 'admin', '13800138000', '系统管理员', '系统管理员', '" + adminPassword + "')");
+    log.info("测试管理员用户已创建: 13800138000 (admin角色, 密码 admin123)");
+  }
+
+  // 创建测试教师用户
+  Integer teacherUserCount = jdbcTemplate.queryForObject(
+    "SELECT COUNT(*) FROM users WHERE phone = '13800138001'", Integer.class);
+  if (teacherUserCount == null || teacherUserCount == 0) {
+    jdbcTemplate.execute("INSERT INTO users (openid, family_id, role, phone, nickname, real_name, password) VALUES " +
+      "('seed_teacher_13800138001', 1, 'teacher', '13800138001', '测试教师', '测试教师', '" + teacherPassword + "')");
+    log.info("测试教师用户已创建: 13800138001 (教师角色, 密码 teacher123)");
+  }
+} catch (Exception e) {
+  log.warn("初始化测试用户失败: {}", e.getMessage());
+}
+
+        // 初始化成长规划师模块相关表
+        initGuideModuleTables(jdbcTemplate);
+    }
+
+    private void initGuideModuleTables(JdbcTemplate jdbcTemplate) {
+        // 服务类型表
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS service_types (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "name VARCHAR(100) NOT NULL COMMENT '类型名称', " +
+                "icon VARCHAR(50) COMMENT '图标', " +
+                "description VARCHAR(500) COMMENT '描述', " +
+                "sort_order INT DEFAULT 0 COMMENT '排序', " +
+                "status VARCHAR(20) DEFAULT 'ACTIVE' COMMENT '状态:ACTIVE/DISABLED', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
+                ")");
+        } catch (Exception e) {
+            log.warn("创建service_types表失败: {}", e.getMessage());
+        }
+
+        // 服务内容表
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS service_contents (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "type_id BIGINT NOT NULL COMMENT '关联服务类型', " +
+                "title VARCHAR(200) NOT NULL COMMENT '内容标题', " +
+                "content TEXT COMMENT '富文本内容', " +
+                "duration INT COMMENT '建议时长(分钟)', " +
+                "age_range VARCHAR(50) COMMENT '适用年龄', " +
+                "price DECIMAL(10,2) COMMENT '参考价格', " +
+                "status VARCHAR(20) DEFAULT 'DRAFT' COMMENT '状态:DRAFT/ACTIVE', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
+                ")");
+        } catch (Exception e) {
+            log.warn("创建service_contents表失败: {}", e.getMessage());
+        }
+
+        // 套餐模板表
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS guide_package_templates (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "name VARCHAR(100) NOT NULL COMMENT '模板名称', " +
+                "validity_months INT COMMENT '有效期(月)', " +
+                "reference_price DECIMAL(10,2) COMMENT '参考价格', " +
+                "min_price DECIMAL(10,2) COMMENT '最低售价', " +
+                "description VARCHAR(500) COMMENT '说明', " +
+                "status VARCHAR(20) DEFAULT 'ACTIVE' COMMENT '状态:ACTIVE/DISABLED', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
+                ")");
+        } catch (Exception e) {
+            log.warn("创建guide_package_templates表失败: {}", e.getMessage());
+        }
+
+        // 套餐模板服务项表
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS package_template_services (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "template_id BIGINT NOT NULL COMMENT '关联模板', " +
+                "content_id BIGINT NOT NULL COMMENT '关联服务内容', " +
+                "quantity INT DEFAULT 1 COMMENT '数量', " +
+                "unit VARCHAR(20) COMMENT '单位' " +
+                ")");
+        } catch (Exception e) {
+            log.warn("创建package_template_services表失败: {}", e.getMessage());
+        }
+
+        // 成长规划师套餐表
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS guide_packages (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "teacher_id BIGINT NOT NULL COMMENT '成长规划师ID', " +
+                "template_id BIGINT COMMENT '关联模板', " +
+                "name VARCHAR(100) NOT NULL COMMENT '套餐名称', " +
+                "price DECIMAL(10,2) NOT NULL COMMENT '销售价格', " +
+                "validity_months INT COMMENT '有效期(月)', " +
+                "description VARCHAR(500) COMMENT '套餐说明', " +
+                "status VARCHAR(20) DEFAULT 'DRAFT' COMMENT '状态:DRAFT/PUBLISHED/PAUSED', " +
+                "sales_count INT DEFAULT 0 COMMENT '销量', " +
+                "share_code VARCHAR(50) COMMENT '分享码', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
+                ")");
+        } catch (Exception e) {
+            log.warn("创建guide_packages表失败: {}", e.getMessage());
+        }
+
+        // 迁移:guide_packages 表添加缺失列
+        try {
+            jdbcTemplate.execute("ALTER TABLE guide_packages ADD COLUMN guide_id BIGINT COMMENT '成长规划师ID' AFTER id");
+        } catch (Exception e) {
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE guide_packages ADD COLUMN package_id BIGINT COMMENT '关联模板' AFTER guide_id");
+        } catch (Exception e) {
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE guide_packages ADD COLUMN commission_rate DECIMAL(5,2) COMMENT '佣金比例' AFTER package_id");
+        } catch (Exception e) {
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE guide_packages MODIFY COLUMN teacher_id BIGINT NULL");
+        } catch (Exception e) {
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE guide_packages MODIFY COLUMN template_id BIGINT NULL");
+        } catch (Exception e) {
+        }
+
+        // 迁移:guide_packages 表添加 level 列(GuidePackage 实体需要)
+        try {
+            jdbcTemplate.execute("ALTER TABLE guide_packages ADD COLUMN level VARCHAR(20) COMMENT '套餐等级' AFTER name");
+        } catch (Exception e) {
+        }
+
+        // 迁移:guide_packages 表添加 validity_days 列
+        try {
+            jdbcTemplate.execute("ALTER TABLE guide_packages ADD COLUMN validity_days INT DEFAULT 0 COMMENT '有效期天数' AFTER validity_months");
+        } catch (Exception e) {
+        }
+
+        // 迁移:guide_packages 表添加 sort_order 列
+        try {
+            jdbcTemplate.execute("ALTER TABLE guide_packages ADD COLUMN sort_order INT DEFAULT 0 COMMENT '排序' AFTER status");
+        } catch (Exception e) {
+        }
+
+        // 成长规划师套餐服务项表
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS guide_package_services (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "package_id BIGINT NOT NULL COMMENT '关联套餐', " +
+                "content_id BIGINT NOT NULL COMMENT '关联服务内容', " +
+                "quantity INT DEFAULT 1 COMMENT '数量', " +
+                "unit VARCHAR(20) COMMENT '单位' " +
+                ")");
+        } catch (Exception e) {
+            log.warn("创建guide_package_services表失败: {}", e.getMessage());
+        }
+
+        // 订单表
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS guide_orders (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "order_no VARCHAR(50) NOT NULL COMMENT '订单号', " +
+                "teacher_id BIGINT NOT NULL COMMENT '成长规划师ID', " +
+                "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
+                "package_id BIGINT NOT NULL COMMENT '套餐ID', " +
+                "amount DECIMAL(10,2) NOT NULL COMMENT '金额', " +
+                "status VARCHAR(20) DEFAULT 'PENDING' COMMENT '状态:PENDING/PAID/COMPLETED/REFUNDED', " +
+                "pay_time DATETIME COMMENT '支付时间', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
+                ")");
+        } catch (Exception e) {
+            log.warn("创建guide_orders表失败: {}", e.getMessage());
+        }
+
+        // 佣金明细表
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS guide_commissions (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "teacher_id BIGINT NOT NULL COMMENT '成长规划师ID', " +
+                "order_id BIGINT NOT NULL COMMENT '订单ID', " +
+                "amount DECIMAL(10,2) NOT NULL COMMENT '佣金金额', " +
+                "rate DECIMAL(5,2) COMMENT '佣金比例', " +
+                "status VARCHAR(20) DEFAULT 'PENDING' COMMENT '状态:PENDING/COMPLETED', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP" +
+                ")");
+        } catch (Exception e) {
+            log.warn("创建guide_commissions表失败: {}", e.getMessage());
+        }
+
+        // 服务记录表
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS guide_service_logs (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "teacher_id BIGINT NOT NULL COMMENT '成长规划师ID', " +
+                "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
+                "order_id BIGINT COMMENT '订单ID', " +
+                "service_content_id BIGINT NOT NULL COMMENT '服务内容ID', " +
+                "notes VARCHAR(500) COMMENT '服务记录', " +
+                "duration INT COMMENT '服务时长(分钟)', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP" +
+                ")");
+        } catch (Exception e) {
+            log.warn("创建guide_service_logs表失败: {}", e.getMessage());
+        }
+
+        // 成长规划师邀请码表
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS guide_invite_codes (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "guide_id BIGINT NOT NULL COMMENT '成长规划师用户ID', " +
+                "code VARCHAR(50) NOT NULL UNIQUE COMMENT '邀请码', " +
+                "status VARCHAR(20) DEFAULT 'active' COMMENT '状态', " +
+                "used_by_user_id BIGINT COMMENT '使用该邀请码的用户ID', " +
+                "used_at DATETIME COMMENT '使用时间', " +
+                "expire_at DATETIME COMMENT '过期时间', " +
+                "use_count INT DEFAULT 0 COMMENT '使用次数', " +
+                "max_use_count INT COMMENT '最大使用次数', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
+                ")");
+        } catch (Exception e) {
+            log.warn("创建guide_invite_codes表失败: {}", e.getMessage());
+        }
+
+        // 迁移29: assessment_orders表添加appointment_id字段
+        try {
+            jdbcTemplate.execute("ALTER TABLE assessment_orders ADD COLUMN appointment_id BIGINT COMMENT '关联的预约ID'");
+            log.info("已添加appointment_id列到assessment_orders表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        log.info("成长规划师模块数据库表初始化完成");
+    }
+
+    private void insertSysConfigSeed(String key, String value, String desc) {
+        Integer count = jdbcTemplate.queryForObject(
+            "SELECT COUNT(*) FROM sys_config WHERE config_key = ?", Integer.class, key);
+        if (count == null || count == 0) {
+            jdbcTemplate.update(
+                "INSERT INTO sys_config (config_key, config_value, description, updated_at) VALUES (?, ?, ?, NOW())",
+                key, value, desc);
+        }
+    }
+
+    private void insertProductSeed(String name, String desc, String type,
+                                java.math.BigDecimal price,
+                                String domain, String memberEligible) {
+        Integer count = jdbcTemplate.queryForObject(
+            "SELECT COUNT(*) FROM products WHERE name = ? AND product_type = ?", Integer.class, name, type);
+        if (count == null || count == 0) {
+            jdbcTemplate.update(
+                "INSERT INTO products (name, description, price, product_type, vendor_id, vendor_name, status, domain, member_eligible, created_at, updated_at) " +
+                "VALUES (?, ?, ?, ?, NULL, '平台官方', 'on_shelf', ?, ?, NOW(), NOW())",
+                name, desc, price, type, domain, memberEligible);
+        }
+    }
+}

+ 14 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/GlobalExceptionHandler.java

@@ -0,0 +1,14 @@
+package com.etotem.cfc.config;
+
+import com.etotem.cfc.common.Result;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+    @ExceptionHandler(IllegalArgumentException.class)
+    public Result<Void> handleIllegalArgumentException(IllegalArgumentException e) {
+        return Result.error(400, e.getMessage());
+    }
+}

+ 70 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/JwtConfig.java

@@ -0,0 +1,70 @@
+package com.etotem.cfc.config;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import io.jsonwebtoken.security.Keys;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.SecretKey;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+@Component
+public class JwtConfig {
+
+    @Value("${jwt.secret}")
+    private String secret;
+
+    @Value("${jwt.expiration}")
+    private Long expiration;
+
+    private SecretKey getSigningKey() {
+        return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
+    }
+
+    public String generateToken(Long userId, String role) {
+        Map<String, Object> claims = new HashMap<>();
+        claims.put("userId", userId);
+        claims.put("role", role);
+        return createToken(claims, userId.toString());
+    }
+
+    private String createToken(Map<String, Object> claims, String subject) {
+        Date now = new Date();
+        Date expirationDate = new Date(now.getTime() + expiration);
+
+        return Jwts.builder()
+                .setClaims(claims)
+                .setSubject(subject)
+                .setIssuedAt(now)
+                .setExpiration(expirationDate)
+                .signWith(getSigningKey(), SignatureAlgorithm.HS256)
+                .compact();
+    }
+
+    public Claims parseToken(String token) {
+        return Jwts.parserBuilder()
+                .setSigningKey(getSigningKey())
+                .build()
+                .parseClaimsJws(token)
+                .getBody();
+    }
+
+    public boolean isTokenExpired(String token) {
+        try {
+            Claims claims = parseToken(token);
+            return claims.getExpiration().before(new Date());
+        } catch (Exception e) {
+            return true;
+        }
+    }
+
+    public Long getUserIdFromToken(String token) {
+        Claims claims = parseToken(token);
+        return Long.parseLong(claims.getSubject());
+    }
+}

+ 67 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/JwtInterceptor.java

@@ -0,0 +1,67 @@
+package com.etotem.cfc.config;
+
+import io.jsonwebtoken.Claims;
+import lombok.extern.slf4j.Slf4j;
+import javax.annotation.Resource;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Slf4j
+@Component
+public class JwtInterceptor implements HandlerInterceptor {
+
+    @Resource
+    private JwtConfig jwtConfig;
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        // 放 OPTIONS 请求
+        if ("OPTIONS".equals(request.getMethod())) {
+            return true;
+        }
+
+        // 获取token
+        String token = request.getHeader("Authorization");
+        
+        if (!StringUtils.hasText(token)) {
+            // 尝试从参数中获取
+            token = request.getParameter("token");
+        }
+
+        if (!StringUtils.hasText(token)) {
+            log.warn("请求 {} 缺少Token", request.getRequestURI());
+            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+            response.getWriter().write("{\"code\":401,\"message\":\"请先登录\"}");
+            return false;
+        }
+
+        // 去掉Bearer前缀
+        if (token.startsWith("Bearer ")) {
+            token = token.substring(7);
+        }
+
+        try {
+            // 解析token
+            Claims claims = jwtConfig.parseToken(token);
+            Long userId = Long.parseLong(claims.getSubject());
+            String role = (String) claims.get("role");
+
+            // 添加自定义请求头,供下游使用
+            response.setHeader("X-User-Id", userId.toString());
+            response.setHeader("X-User-Role", role);
+            request.setAttribute("userId", userId);
+            request.setAttribute("role", role);
+
+            return true;
+        } catch (Exception e) {
+            log.warn("Token解析失败: {}", e.getMessage());
+            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+            response.getWriter().write("{\"code\":401,\"message\":\"登录已过期,请重新登录\"}");
+            return false;
+        }
+    }
+}

+ 18 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/MyBatisPlusConfig.java

@@ -0,0 +1,18 @@
+package com.etotem.cfc.config;
+
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class MyBatisPlusConfig {
+
+    @Bean
+    public MybatisPlusInterceptor mybatisPlusInterceptor() {
+        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
+        return interceptor;
+    }
+}

+ 154 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/OperationLogInterceptor.java

@@ -0,0 +1,154 @@
+package com.etotem.cfc.config;
+
+import com.etotem.cfc.service.UserOperationLogService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+import org.springframework.web.servlet.ModelAndView;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.BufferedReader;
+
+@Slf4j
+@Component
+public class OperationLogInterceptor implements HandlerInterceptor {
+
+    @Resource
+    private UserOperationLogService logService;
+
+    /**
+     * 不记录日志的路径
+     */
+    private static final String[] EXCLUDE_PATHS = {
+            "/api/auth/send-code",
+            "/api/auth/check-phone",
+            "/api/streets",
+            "/swagger-ui",
+            "/v3/api-docs",
+            "/swagger-resources",
+            "/webjars"
+    };
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        // 检查是否需要排除
+        String path = request.getRequestURI();
+        for (String excludePath : EXCLUDE_PATHS) {
+            if (path.contains(excludePath)) {
+                return true;
+            }
+        }
+
+        return true;
+    }
+
+    @Override
+    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
+                        Object handler, Exception ex) throws Exception {
+        try {
+            // 获取用户ID(从拦截器设置)
+            Object userIdObj = request.getAttribute("userId");
+            if (userIdObj == null) {
+                return;
+            }
+            Long userId = (Long) userIdObj;
+
+            // 获取操作类型
+            String method = request.getMethod();
+            String path = request.getRequestURI();
+            String operationType = getOperationType(method, path);
+            
+            // 获取描述
+            String description = method + " " + path;
+
+            // 获取请求参数
+            String requestParams = getRequestParams(request);
+
+            // 获取客户端IP
+            String clientIp = getClientIp(request);
+
+            // 获取User-Agent
+            String userAgent = request.getHeader("User-Agent");
+
+            // 记录日志
+            logService.logOperation(
+                    userId,
+                    operationType,
+                    description,
+                    path,
+                    method,
+                    requestParams,
+                    response.getStatus(),
+                    clientIp,
+                    userAgent
+            );
+        } catch (Exception e) {
+            log.error("记录操作日志失败: {}", e.getMessage());
+        }
+    }
+
+    /**
+     * 根据请求方法和路径获取操作类型
+     */
+    private String getOperationType(String method, String path) {
+        if (path.contains("/login")) {
+            return "login";
+        } else if (path.contains("/logout")) {
+            return "logout";
+        } else if (path.contains("/create") || path.contains("/add")) {
+            return "create";
+        } else if (path.contains("/update") || path.contains("/edit")) {
+            return "update";
+        } else if (path.contains("/delete")) {
+            return "delete";
+        } else if (path.contains("/query") || path.contains("/list") || path.contains("/get/")) {
+            return "query";
+        }
+        
+        // 根据HTTP方法
+        return method.toLowerCase();
+    }
+
+    /**
+     * 获取请求参数
+     */
+    private String getRequestParams(HttpServletRequest request) {
+        try {
+            if ("POST".equalsIgnoreCase(request.getMethod()) || 
+                "PUT".equalsIgnoreCase(request.getMethod())) {
+                StringBuilder sb = new StringBuilder();
+                BufferedReader reader = request.getReader();
+                String line;
+                while ((line = reader.readLine()) != null) {
+                    sb.append(line);
+                }
+                return sb.toString();
+            }
+            // GET参数
+            return request.getQueryString();
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    /**
+     * 获取客户端真实IP
+     */
+    private String getClientIp(HttpServletRequest request) {
+        String ip = request.getHeader("X-Forwarded-For");
+        if (StringUtils.isBlank(ip)) {
+            ip = request.getHeader("X-Real-IP");
+        }
+        if (StringUtils.isBlank(ip)) {
+            ip = request.getRemoteAddr();
+        }
+        // 多级代理取第一个IP
+        if (ip != null && ip.contains(",")) {
+            ip = ip.split(",")[0].trim();
+        }
+        return ip;
+    }
+}

+ 20 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/RestTemplateConfig.java

@@ -0,0 +1,20 @@
+package com.etotem.cfc.config;
+
+import org.springframework.boot.web.client.RestTemplateBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestTemplate;
+
+import java.time.Duration;
+
+@Configuration
+public class RestTemplateConfig {
+
+    @Bean
+    public RestTemplate restTemplate(RestTemplateBuilder builder) {
+        return builder
+                .setConnectTimeout(Duration.ofSeconds(30))
+                .setReadTimeout(Duration.ofSeconds(30))
+                .build();
+    }
+}

+ 289 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/SchemaSyncHandler.java

@@ -0,0 +1,289 @@
+package com.etotem.cfc.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 运行时数据库结构自动同步处理器
+ * 当SQL报错"字段不存在"时,自动检测并添加字段
+ */
+@Slf4j
+@Component
+public class SchemaSyncHandler {
+
+    @Resource
+    private JdbcTemplate jdbcTemplate;
+
+    // 已处理过的表缓存,避免重复检查
+    private final Set<String> processedTables = ConcurrentHashMap.newKeySet();
+
+    // 字段定义映射表:表名 -> (字段名 -> MySQL类型定义)
+    private static final Map<String, Map<String, String>> SCHEMA_DEFINITIONS;
+    
+    static {
+        // rewards 表字段
+        Map<String, String> rewardsFields = new HashMap<>();
+        rewardsFields.put("title", "VARCHAR(100) NOT NULL DEFAULT ''");
+        rewardsFields.put("points_required", "INT NOT NULL DEFAULT 0");
+        rewardsFields.put("category", "VARCHAR(32)");
+        rewardsFields.put("status", "ENUM('available','pending','exchanged','rejected') DEFAULT 'available'");
+        rewardsFields.put("exchange_approved", "TINYINT");
+        rewardsFields.put("exchanged_at", "DATETIME");
+        rewardsFields.put("is_template", "TINYINT DEFAULT 0");
+        rewardsFields.put("child_id", "BIGINT NULL");
+        rewardsFields.put("family_id", "BIGINT NOT NULL");
+        
+        // tasks 表字段
+        Map<String, String> tasksFields = new HashMap<>();
+        tasksFields.put("title", "VARCHAR(200) NOT NULL");
+        tasksFields.put("description", "VARCHAR(500)");
+        tasksFields.put("category", "VARCHAR(32)");
+        tasksFields.put("points", "TINYINT DEFAULT 2");
+        tasksFields.put("deadline", "DATETIME");
+        tasksFields.put("child_id", "BIGINT NULL");
+        tasksFields.put("family_id", "BIGINT NOT NULL");
+        tasksFields.put("creator_id", "BIGINT NOT NULL");
+        tasksFields.put("status", "ENUM('pending','completed','overdue','cancelled') DEFAULT 'pending'");
+        tasksFields.put("executor_type", "ENUM('child','parent') DEFAULT 'child'");
+        tasksFields.put("executor_id", "BIGINT");
+        tasksFields.put("repeat_type", "ENUM('none','daily','weekly') DEFAULT 'none'");
+        tasksFields.put("need_review", "TINYINT DEFAULT 0");
+        tasksFields.put("bonus_rule", "VARCHAR(500)");
+        tasksFields.put("points_expire_days", "INT DEFAULT 0");
+        tasksFields.put("template_id", "BIGINT");
+        tasksFields.put("is_template", "TINYINT DEFAULT 0");
+        
+        // users 表字段
+        Map<String, String> usersFields = new HashMap<>();
+        usersFields.put("openid", "VARCHAR(64) NOT NULL");
+        usersFields.put("unionid", "VARCHAR(64)");
+        usersFields.put("family_id", "BIGINT NOT NULL");
+        usersFields.put("role", "ENUM('parent','child','teacher') NOT NULL");
+        usersFields.put("nickname", "VARCHAR(50)");
+        usersFields.put("avatar", "VARCHAR(255)");
+        usersFields.put("password", "VARCHAR(128)");
+        usersFields.put("roles", "VARCHAR(100)");
+        usersFields.put("total_points", "INT DEFAULT 0");
+        
+        // children 表字段
+        Map<String, String> childrenFields = new HashMap<>();
+        childrenFields.put("user_id", "BIGINT NOT NULL");
+        childrenFields.put("family_id", "BIGINT NOT NULL");
+        childrenFields.put("nickname", "VARCHAR(50) NOT NULL");
+        childrenFields.put("age", "TINYINT NOT NULL");
+        childrenFields.put("dan_level", "VARCHAR(2)");
+        childrenFields.put("penalty_enabled", "TINYINT DEFAULT 1");
+        childrenFields.put("theme", "VARCHAR(32) DEFAULT 'default'");
+        childrenFields.put("total_points", "INT DEFAULT 0");
+        childrenFields.put("streak_days", "INT DEFAULT 0");
+        childrenFields.put("birthday", "DATE");
+        childrenFields.put("gender", "ENUM('male','female')");
+        
+        // points_log 表字段
+        Map<String, String> pointsLogFields = new HashMap<>();
+        pointsLogFields.put("child_id", "BIGINT NOT NULL");
+        pointsLogFields.put("task_id", "BIGINT");
+        pointsLogFields.put("amount", "INT NOT NULL");
+        pointsLogFields.put("type", "ENUM('earn','spend','bonus','penalty','adjust','reset','refund') NOT NULL");
+        pointsLogFields.put("description", "VARCHAR(255)");
+        pointsLogFields.put("expire_at", "DATE");
+        pointsLogFields.put("expired", "TINYINT DEFAULT 0");
+        
+        SCHEMA_DEFINITIONS = new HashMap<>();
+        SCHEMA_DEFINITIONS.put("rewards", rewardsFields);
+        SCHEMA_DEFINITIONS.put("tasks", tasksFields);
+        SCHEMA_DEFINITIONS.put("users", usersFields);
+        SCHEMA_DEFINITIONS.put("children", childrenFields);
+        SCHEMA_DEFINITIONS.put("points_log", pointsLogFields);
+    }
+
+    /**
+     * 处理字段不存在的异常,自动添加字段后重试
+     * @param exception 原始SQL异常消息
+     * @param operation 要重试的操作
+     * @return 重试结果,如果成功返回null,失败返回异常消息
+     */
+    @Transactional
+    public String handleMissingColumn(String exception, Runnable operation) {
+        // 匹配 "Unknown column 'xxx' in 'field list'" 或 "Field 'xxx' doesn't have a default value"
+        Pattern pattern = Pattern.compile("Unknown column '(\\w+)' in '(?:field list|\\w+)'", Pattern.CASE_INSENSITIVE);
+        Matcher matcher = pattern.matcher(exception);
+        
+        String fieldName = null;
+        if (!matcher.find()) {
+            // 也可能是 "Field 'xxx' doesn't have a default value" 情况
+            pattern = Pattern.compile("Field '(\\w+)' doesn't have a default value", Pattern.CASE_INSENSITIVE);
+            matcher = pattern.matcher(exception);
+            if (!matcher.find()) {
+                log.warn("无法解析字段缺失错误: {}", exception);
+                return "无法解析字段缺失错误";
+            }
+        }
+        fieldName = matcher.group(1);
+        
+        // 尝试提取表名
+        String tableName = extractTableName(exception);
+        if (tableName == null) {
+            log.warn("无法从错误消息中提取表名: {}", exception);
+            return "无法提取表名";
+        }
+        
+        log.info("检测到字段缺失: {}.{}", tableName, fieldName);
+        
+        // 检查并添加字段
+        boolean success = addMissingColumn(tableName, fieldName);
+        
+        if (!success) {
+            return "无法自动添加字段: " + tableName + "." + fieldName;
+        }
+        
+        // 重试操作
+        try {
+            operation.run();
+            log.info("字段自动同步成功,重试操作完成: {}.{}", tableName, fieldName);
+            return null;
+        } catch (Exception e) {
+            log.error("重试操作仍然失败: {}", e.getMessage());
+            return "重试失败: " + e.getMessage();
+        }
+    }
+    
+    /**
+     * 从异常消息中提取表名
+     */
+    private String extractTableName(String message) {
+        // 优先尝试从表名中提取 (tableName.field)
+        Pattern p = Pattern.compile("\\b(\\w+)\\.(\\w+)\\b", Pattern.CASE_INSENSITIVE);
+        Matcher m = p.matcher(message);
+        if (m.find()) {
+            return m.group(1);
+        }
+        
+        // 从 INSERT/UPDATE/INTO tableName 中提取
+        p = Pattern.compile("(?:INSERT\\s+INTO|UPDATE|INTO)\\s+(\\w+)", Pattern.CASE_INSENSITIVE);
+        m = p.matcher(message);
+        if (m.find()) {
+            return m.group(1);
+        }
+        
+        return null;
+    }
+    
+    /**
+     * 添加缺失的列
+     */
+    private boolean addMissingColumn(String tableName, String fieldName) {
+        tableName = tableName.toLowerCase();
+        
+        // 检查是否已经处理过
+        String cacheKey = tableName + "." + fieldName;
+        if (processedTables.contains(cacheKey)) {
+            log.info("字段已在缓存中,跳过: {}", cacheKey);
+            return true;
+        }
+        
+        // 检查表定义中是否存在该字段
+        Map<String, String> fields = SCHEMA_DEFINITIONS.get(tableName);
+        if (fields == null) {
+            log.warn("未找到表 {} 的字段定义", tableName);
+            return false;
+        }
+        
+        String columnDef = fields.get(fieldName.toLowerCase());
+        if (columnDef == null) {
+            log.warn("表 {} 中未定义字段 {}", tableName, fieldName);
+            return false;
+        }
+        
+        try {
+            // 检查字段是否已存在
+            boolean exists = checkColumnExists(tableName, fieldName);
+            if (!exists) {
+                String sql = String.format("ALTER TABLE %s ADD COLUMN %s %s", tableName, fieldName, columnDef);
+                jdbcTemplate.execute(sql);
+                log.info("已自动添加字段: {}.{}", tableName, fieldName);
+            }
+            
+            // 加入缓存
+            processedTables.add(cacheKey);
+            return true;
+        } catch (Exception e) {
+            log.error("自动添加字段失败: {}.{} - {}", tableName, fieldName, e.getMessage());
+            return false;
+        }
+    }
+    
+    /**
+     * 检查字段是否已存在
+     */
+    private boolean checkColumnExists(String tableName, String fieldName) {
+        try {
+            String sql = "SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS " +
+                       "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?";
+            Integer count = jdbcTemplate.queryForObject(sql, Integer.class, tableName, fieldName);
+            return count != null && count > 0;
+        } catch (Exception e) {
+            return false;
+        }
+    }
+    
+    /**
+     * 运行时同步整个表的结构
+     */
+    @Transactional
+    public void syncTableSchema(String tableName) {
+        tableName = tableName.toLowerCase();
+        Map<String, String> fields = SCHEMA_DEFINITIONS.get(tableName);
+        if (fields == null) {
+            log.warn("未找到表 {} 的定义,跳过同步", tableName);
+            return;
+        }
+        
+        log.info("开始同步表 {} 的结构...", tableName);
+        
+        for (Map.Entry<String, String> entry : fields.entrySet()) {
+            String fieldName = entry.getKey();
+            String columnDef = entry.getValue();
+            
+            try {
+                boolean exists = checkColumnExists(tableName, fieldName);
+                if (!exists) {
+                    String sql = String.format("ALTER TABLE %s ADD COLUMN %s %s", tableName, fieldName, columnDef);
+                    jdbcTemplate.execute(sql);
+                    log.info("已添加字段: {}.{}", tableName, fieldName);
+                }
+            } catch (Exception e) {
+                log.warn("同步字段失败: {}.{} - {}", tableName, fieldName, e.getMessage());
+            }
+        }
+        
+        log.info("表 {} 结构同步完成", tableName);
+    }
+    
+    /**
+     * 同步所有已定义的表
+     */
+    public void syncAllTables() {
+        log.info("开始同步所有表结构...");
+        
+        for (String tableName : SCHEMA_DEFINITIONS.keySet()) {
+            try {
+                syncTableSchema(tableName);
+            } catch (Exception e) {
+                log.error("同步表 {} 失败: {}", tableName, e.getMessage());
+            }
+        }
+        
+        log.info("所有表结构同步完成");
+    }
+}

+ 29 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/SecurityHeadersFilter.java

@@ -0,0 +1,29 @@
+package com.etotem.cfc.config;
+
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.*;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+@Component
+@Order(Ordered.HIGHEST_PRECEDENCE)
+public class SecurityHeadersFilter implements Filter {
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+            throws IOException, ServletException {
+        HttpServletResponse httpResponse = (HttpServletResponse) response;
+        HttpServletRequest httpRequest = (HttpServletRequest) request;
+
+        httpResponse.setHeader("Content-Security-Policy",
+                "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' http://localhost:* https://*; font-src 'self' data:;");
+        httpResponse.setHeader("Referrer-Policy", "no-referrer");
+        httpResponse.setHeader("Permissions-Policy", "geolocation=(), microphone=(), camera=()");
+
+        chain.doFilter(request, response);
+    }
+}

+ 39 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/SfmsDataSourceConfig.java

@@ -0,0 +1,39 @@
+package com.etotem.cfc.config;
+
+import com.zaxxer.hikari.HikariDataSource;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.jdbc.DataSourceBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import javax.sql.DataSource;
+
+@Configuration
+public class SfmsDataSourceConfig {
+
+    @Primary
+    @Bean(name = "primaryDataSource")
+    @ConfigurationProperties(prefix = "spring.datasource.hikari")
+    public DataSource primaryDataSource() {
+        return DataSourceBuilder.create().type(HikariDataSource.class).build();
+    }
+
+    @Bean(name = "sfmsDataSource")
+    @ConfigurationProperties(prefix = "sfms.datasource")
+    public DataSource sfmsDataSource() {
+        return DataSourceBuilder.create().build();
+    }
+
+    @Primary
+    @Bean
+    public JdbcTemplate jdbcTemplate() {
+        return new JdbcTemplate(primaryDataSource());
+    }
+
+    @Bean
+    public JdbcTemplate sfmsJdbcTemplate() {
+        return new JdbcTemplate(sfmsDataSource());
+    }
+}

+ 49 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/WebConfig.java

@@ -0,0 +1,49 @@
+package com.etotem.cfc.config;
+
+import javax.annotation.Resource;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class WebConfig implements WebMvcConfigurer {
+
+    @Resource
+    private JwtInterceptor jwtInterceptor;
+
+    @Resource
+    private OperationLogInterceptor operationLogInterceptor;
+
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/**")
+                .allowedOriginPatterns("*")
+                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
+                .allowedHeaders("*")
+                .allowCredentials(true)
+                .maxAge(3600);
+    }
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(jwtInterceptor)
+                .addPathPatterns("/api/**")
+                .excludePathPatterns(
+                        "/api/auth/send-code",
+                        "/api/auth/phone-login",
+                        "/api/auth/wechat-login",
+                        "/api/auth/wechat-phone-login",
+                        "/api/auth/check-phone",
+                        "/api/auth/direct-register",
+                        "/api/auth/register-with-idcard",
+                        "/api/admin-auth/send-code",
+                        "/api/admin-auth/login",
+                        "/api/admin-auth/login-by-password"
+                );
+
+        // 操作日志拦截器(JWT之后)
+        registry.addInterceptor(operationLogInterceptor)
+                .addPathPatterns("/api/**");
+    }
+}

+ 105 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/InviteCardController.java

@@ -0,0 +1,105 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.Family;
+import com.etotem.cfc.entity.InviteCard;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.InviteCardService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+@Tag(name = "邀请卡片", description = "家庭成员与规划师邀请分享")
+@RestController
+@RequestMapping("/api/invite-card")
+public class InviteCardController {
+
+    @Resource
+    private InviteCardService inviteCardService;
+
+    @Resource
+    private ChildMapper childMapper;
+
+    @Resource
+    private FamilyMapper familyMapper;
+
+    @Resource
+    private UserMapper userMapper;
+
+    @PostMapping("/generate")
+    @Operation(summary = "生成邀请码")
+    public Result<Map<String, Object>> generate(@RequestAttribute("userId") Long userId,
+                                                @RequestBody Map<String, Object> params) {
+        String type = params.get("type") != null ? params.get("type").toString() : null;
+        Object refIdObj = params.get("refId");
+        if (type == null || refIdObj == null) {
+            return Result.error("缺少必要参数: type/refId");
+        }
+
+        Long refId = Long.valueOf(refIdObj.toString());
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            return Result.error("用户不存在");
+        }
+
+        if ("child".equals(type)) {
+            Child child = childMapper.selectById(refId);
+            if (child == null) {
+                return Result.error("孩子不存在");
+            }
+            if (!Objects.equals(user.getFamilyId(), child.getFamilyId())) {
+                return Result.error("无权生成该孩子的邀请");
+            }
+        } else if ("family".equals(type) || "guide".equals(type)) {
+            Family family = familyMapper.selectById(refId);
+            if (family == null) {
+                return Result.error("家庭不存在");
+            }
+            if (!Objects.equals(user.getFamilyId(), family.getId())) {
+                return Result.error("无权生成该家庭的邀请");
+            }
+        } else {
+            return Result.error("不支持的邀请码类型");
+        }
+
+        InviteCard card = inviteCardService.generate(type, refId, userId);
+        Map<String, Object> result = new HashMap<>();
+        result.put("code", card.getCode());
+        result.put("expiresAt", card.getExpiresAt());
+        return Result.success(result);
+    }
+
+    @PostMapping("/verify")
+    @Operation(summary = "验证邀请码")
+    public Result<Map<String, Object>> verify(@RequestBody Map<String, Object> params) {
+        String code = params.get("code") != null ? params.get("code").toString() : null;
+        if (code == null || code.trim().isEmpty()) {
+            return Result.error("缺少邀请码");
+        }
+        return inviteCardService.verify(code);
+    }
+
+    @PostMapping("/accept")
+    @Operation(summary = "接受邀请")
+    public Result<Map<String, Object>> accept(@RequestBody Map<String, Object> params) {
+        String code = params.get("code") != null ? params.get("code").toString() : null;
+        String phone = params.get("phone") != null ? params.get("phone").toString() : null;
+        if (code == null || code.trim().isEmpty() || phone == null || phone.trim().isEmpty()) {
+            return Result.error("缺少必要参数: code/phone");
+        }
+        return inviteCardService.accept(code, phone);
+    }
+}

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

@@ -0,0 +1,131 @@
+package com.etotem.cfc.controller;
+
+import javax.annotation.Resource;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.ApplyPackageDTO;
+import com.etotem.cfc.entity.TaskPlanInstance;
+import com.etotem.cfc.entity.TaskPlanItem;
+import com.etotem.cfc.entity.TaskTemplatePackage;
+import com.etotem.cfc.entity.TaskTemplateItem;
+import com.etotem.cfc.service.TaskPlanService;
+import com.etotem.cfc.service.TaskTemplatePackageService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "套餐市场", description = "家长浏览、购买、应用套餐等接口")
+@RestController
+@RequestMapping("/api/market")
+public class MarketController {
+
+    @Resource
+    private TaskTemplatePackageService packageService;
+
+    @Resource
+    private TaskPlanService planService;
+
+    /**
+     * 获取公开套餐列表 (市场)
+     */
+    @Operation(summary = "获取公开套餐列表")
+    @PostMapping("/packages")
+    public Result<List<TaskTemplatePackage>> getPublicPackages(@RequestBody(required = false) Map<String, Object> params) {
+        String category = params != null && params.get("category") != null ? params.get("category").toString() : null;
+        List<TaskTemplatePackage> packages = packageService.getPublicPackages(category);
+        return Result.success(packages);
+    }
+
+    /**
+     * 获取套餐详情
+     */
+    @Operation(summary = "获取套餐详情")
+    @PostMapping("/packages/{id}")
+    public Result<TaskTemplatePackage> getPackageDetail(@PathVariable Long id) {
+        TaskTemplatePackage pkg = packageService.getPackageById(id);
+        return Result.success(pkg);
+    }
+
+    /**
+     * 获取套餐的任务项列表
+     */
+    @Operation(summary = "获取套餐任务项列表")
+    @PostMapping("/packages/{id}/items")
+    public Result<List<TaskTemplateItem>> getPackageItems(@PathVariable Long id) {
+        List<TaskTemplateItem> items = packageService.getPackageItems(id);
+        return Result.success(items);
+    }
+
+    /**
+     * 通过分享码获取套餐
+     */
+    @Operation(summary = "通过分享码获取套餐")
+    @PostMapping("/share/{code}")
+    public Result<TaskTemplatePackage> getPackageByShareCode(@PathVariable String code) {
+        TaskTemplatePackage pkg = packageService.getPackageByShareCode(code);
+        if (pkg == null) {
+            return Result.error("分享码无效");
+        }
+        return Result.success(pkg);
+    }
+
+    /**
+     * 购买并应用套餐
+     */
+    @Operation(summary = "购买并应用套餐")
+    @PostMapping("/packages/{id}/purchase")
+    public Result<TaskPlanInstance> purchasePackage(
+            @PathVariable Long id,
+            @RequestBody ApplyPackageDTO dto,
+            @RequestAttribute("userId") Long userId,
+            @RequestAttribute("familyId") Long familyId) {
+        
+        dto.setPackageId(id);
+        TaskPlanInstance instance = planService.applyPackage(dto, familyId);
+        return Result.success(instance);
+    }
+
+    /**
+     * 获取我的任务计划列表
+     */
+    @Operation(summary = "获取我的任务计划")
+    @PostMapping("/plans")
+    public Result<List<TaskPlanInstance>> getMyPlans(@RequestAttribute("familyId") Long familyId) {
+        List<TaskPlanInstance> plans = planService.getFamilyPlans(familyId);
+        return Result.success(plans);
+    }
+
+    /**
+     * 获取计划详情
+     */
+    @Operation(summary = "获取计划详情")
+    @PostMapping("/plans/{id}")
+    public Result<TaskPlanInstance> getPlanDetail(@PathVariable Long id) {
+        TaskPlanInstance plan = planService.getPlanById(id);
+        return Result.success(plan);
+    }
+
+    /**
+     * 获取计划的任务项
+     */
+    @Operation(summary = "获取计划任务项")
+    @PostMapping("/plans/{id}/items")
+    public Result<List<TaskPlanItem>> getPlanItems(@PathVariable Long id) {
+        List<TaskPlanItem> items = planService.getPlanItems(id);
+        return Result.success(items);
+    }
+
+    /**
+     * 取消计划
+     */
+    @Operation(summary = "取消计划")
+    @PostMapping("/plans/{id}/cancel")
+    public Result<Boolean> cancelPlan(@PathVariable Long id) {
+        boolean success = planService.cancelPlan(id);
+        return Result.success(success);
+    }
+}

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

@@ -0,0 +1,84 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.MediaRecord;
+import com.etotem.cfc.service.MediaRecordService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "媒体管理", description = "图片、视频、音频、文字上传和管理接口")
+@RestController
+@RequestMapping("/api/media")
+public class MediaController {
+
+    @Resource
+    private MediaRecordService mediaRecordService;
+
+    @Operation(summary = "上传媒体文件")
+    @PostMapping("/upload")
+    public Result<MediaRecord> uploadMedia(
+            @RequestParam("file") MultipartFile file,
+            @RequestParam Long taskId,
+            @RequestParam Long creatorId,
+            @RequestParam(required = false) String description) {
+
+        // 验证文件大小
+        if (file.getSize() > getMaxFileSize(file.getContentType())) {
+            return Result.error("文件大小超过限制");
+        }
+
+        MediaRecord record = mediaRecordService.uploadMedia(file, taskId, creatorId, description);
+        return Result.success(record);
+    }
+
+    @Operation(summary = "保存文字记录")
+    @PostMapping("/upload-text")
+    public Result<MediaRecord> uploadText(@RequestBody Map<String, Object> body) {
+        Long taskId = Long.parseLong(body.get("taskId").toString());
+        Long creatorId = Long.parseLong(body.get("creatorId").toString());
+        String content = body.get("content").toString();
+
+        MediaRecord record = mediaRecordService.saveTextRecord(taskId, creatorId, content);
+        return Result.success(record);
+    }
+
+    @Operation(summary = "获取任务的所有媒体")
+    @PostMapping("/task/{taskId}")
+    public Result<List<MediaRecord>> getMediaByTask(@PathVariable Long taskId) {
+        List<MediaRecord> records = mediaRecordService.getMediaByTaskId(taskId);
+        return Result.success(records);
+    }
+
+    @Operation(summary = "删除媒体")
+    @DeleteMapping("/{mediaId}")
+    public Result<Boolean> deleteMedia(
+            @PathVariable Long mediaId,
+            @RequestAttribute("userId") Long userId) {
+
+        boolean success = mediaRecordService.deleteMedia(mediaId, userId);
+        return Result.success(success);
+    }
+
+    @Operation(summary = "批量关联媒体到任务")
+    @PostMapping("/attach")
+    public Result<Boolean> attachMedia(@RequestBody Map<String, Object> body) {
+        Long taskId = Long.parseLong(body.get("taskId").toString());
+        List<Long> mediaIds = (List<Long>) body.get("mediaIds");
+
+        boolean success = mediaRecordService.attachMediaToTask(taskId, mediaIds);
+        return Result.success(success);
+    }
+
+    private long getMaxFileSize(String contentType) {
+        if (contentType == null) return 10 * 1024 * 1024; // 10MB
+        if (contentType.startsWith("video")) return 100 * 1024 * 1024; // 100MB
+        if (contentType.startsWith("audio")) return 50 * 1024 * 1024; // 50MB
+        return 10 * 1024 * 1024; // 10MB for images
+    }
+}

+ 110 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/MembershipController.java

@@ -0,0 +1,110 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.*;
+import com.etotem.cfc.service.MembershipService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "会员管理", description = "会员等级、支付相关接口")
+@RestController
+@RequestMapping("/api/membership")
+public class MembershipController {
+
+    @Resource
+    private MembershipService membershipService;
+
+    /**
+     * 获取所有会员等级
+     */
+    @Operation(summary = "获取会员等级列表")
+    @PostMapping("/levels")
+    public Result<List<MembershipLevelDTO>> getAllLevels() {
+        List<MembershipLevelDTO> levels = membershipService.getAllLevels();
+        return Result.success(levels);
+    }
+
+    /**
+     * 获取家庭当前会员信息
+     */
+    @Operation(summary = "获取家庭会员信息")
+    @PostMapping("/my")
+    public Result<FamilyMembershipDTO> getMyMembership(javax.servlet.http.HttpServletRequest request) {
+        Long userId = getUserId(request);
+        // 获取用户家庭ID
+        // 简化:假设通过其他服务获取
+        Long familyId = 1L; // 临时
+        
+        FamilyMembershipDTO membership = membershipService.getFamilyMembership(familyId);
+        return Result.success(membership);
+    }
+
+    /**
+     * 获取当前会员等级
+     */
+    @Operation(summary = "获取当前会员等级")
+    @PostMapping("/current")
+    public Result<MembershipLevelDTO> getCurrentLevel(javax.servlet.http.HttpServletRequest request) {
+        Long familyId = 1L; // 临时
+        
+        MembershipLevelDTO level = membershipService.getCurrentLevel(familyId);
+        return Result.success(level);
+    }
+
+    /**
+     * 创建支付订单
+     */
+    @Operation(summary = "创建订单")
+    @PostMapping("/orders")
+    public Result<PaymentOrderDTO> createOrder(@RequestBody Map<String, Object> params) {
+        String levelCode = params.get("levelCode").toString();
+        String paymentType = params.get("paymentType").toString();
+        Long familyId = 1L; // 临时
+        
+        PaymentOrderDTO order = membershipService.createOrder(familyId, levelCode, paymentType);
+        return Result.success(order);
+    }
+
+    /**
+     * 处理支付回调
+     */
+    @Operation(summary = "支付回调")
+    @PostMapping("/notify")
+    public Result<Boolean> paymentNotify(@RequestBody Map<String, Object> params) {
+        String orderNo = params.get("orderNo").toString();
+        String transactionId = params.get("transactionId").toString();
+        String payMethod = params.get("payMethod").toString();
+        boolean success = membershipService.processPaymentCallback(orderNo, transactionId, payMethod);
+        return Result.success(success);
+    }
+
+    /**
+     * 检查功能权限
+     */
+    @Operation(summary = "检查功能权限")
+    @PostMapping("/can-use")
+    public Result<Boolean> canUseFeature(@RequestBody Map<String, Object> params) {
+        String feature = params.get("feature").toString();
+        Long familyId = 1L; // 临时
+        
+        boolean canUse = membershipService.canUseFeature(familyId, feature);
+        return Result.success(canUse);
+    }
+
+    private Long getUserId(javax.servlet.http.HttpServletRequest request) {
+        Object userIdObj = request.getAttribute("userId");
+        if (userIdObj != null) {
+            return (Long) userIdObj;
+        }
+        String userIdHeader = request.getHeader("X-User-Id");
+        if (userIdHeader != null) {
+            return Long.parseLong(userIdHeader);
+        }
+        return null;
+    }
+}

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

@@ -0,0 +1,77 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import javax.annotation.Resource;
+import com.etotem.cfc.entity.MiniGame;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.service.MiniGameService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import java.util.List;
+import java.util.Map;
+@Tag(name = "小游戏管理", description = "小游戏相关接口")
+@RestController
+@RequestMapping("/api/mini-game")
+public class MiniGameController {
+    @Resource
+    private MiniGameService miniGameService;
+    private UserMapper userMapper;
+    private ChildMapper childMapper;
+    /**
+     * 获取启用的小游戏列表
+     */
+    @Operation(summary = "获取小游戏列表")
+    @PostMapping("/list")
+    public Result<List<MiniGame>> getGameList() {
+        List<MiniGame> games = miniGameService.getEnabledGames();
+        return Result.success(games);
+    }
+    @Operation(summary = "获取游戏详情")
+    @PostMapping("/{gameCode}")
+    public Result<MiniGame> getGameByCode(@PathVariable String gameCode) {
+        MiniGame game = miniGameService.getGameByCode(gameCode);
+        if (game == null) {
+            return Result.error("游戏不存在");
+        }
+        return Result.success(game);
+    }
+    @Operation(summary = "获取所有小游戏(管理端)")
+    @PostMapping("/all")
+    public Result<List<MiniGame>> getAllGames() {
+        List<MiniGame> games = miniGameService.getAllGames();
+        return Result.success(games);
+    }
+    @Operation(summary = "更新游戏状态")
+    @PutMapping("/{id}/status")
+    public Result<Boolean> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
+        boolean success = miniGameService.updateGameStatus(id, status);
+        return Result.success(success);
+    }
+    @Operation(summary = "游戏完成任务")
+    @PostMapping("/complete")
+    public Result<Map<String, Object>> completeGame(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> body) {
+        Long childId = Long.valueOf(body.get("childId").toString());
+        String gameCode = (String) body.get("gameCode");
+        Integer completionTime = body.get("completionTime") != null ?
+                Integer.valueOf(body.get("completionTime").toString()) : null;
+        Integer score = body.get("score") != null ?
+                Integer.valueOf(body.get("score").toString()) : null;
+        // 权限校验:验证用户是否有权限操作该孩子
+        User user = userMapper.selectById(userId);
+        if (user != null) {
+            Child child = childMapper.selectById(childId);
+            if (child == null) {
+                return Result.error("孩子不存在");
+                // 验证孩子是否属于用户的家庭
+            }
+        }
+        return Result.error("处理不成功");
+    }
+}

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

@@ -0,0 +1,120 @@
+package com.etotem.cfc.controller;
+import javax.annotation.Resource;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.PackageOrder;
+import com.etotem.cfc.entity.TaskPlanInstance;
+import com.etotem.cfc.service.PackagePaymentService;
+import com.etotem.cfc.service.TaskPlanService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+@Tag(name = "套餐支付", description = "套餐购买支付接口")
+@RestController
+@RequestMapping("/api/payment")
+public class PackagePaymentController {
+
+    @Resource
+    private PackagePaymentService packagePaymentService;
+
+    @Resource
+    private TaskPlanService planService;
+
+    /**
+     * 创建支付订单
+     */
+    @Operation(summary = "创建支付订单")
+    @PostMapping("/create")
+    public Result<PackageOrder> createOrder(
+            @RequestAttribute("userId") Long userId,
+            @RequestAttribute("familyId") Long familyId,
+            @RequestBody Map<String, Object> params) {
+        Long packageId = Long.valueOf(params.get("packageId").toString());
+        String payMethod = params.get("payMethod") != null ? params.get("payMethod").toString() : "wechat";
+        
+        PackageOrder order = packagePaymentService.createOrder(userId, familyId, packageId, payMethod);
+        return Result.success(order);
+    }
+
+    /**
+     * 获取支付参数 (微信JSAPI)
+     */
+    @Operation(summary = "获取微信支付参数")
+    @PostMapping("/wechat params")
+    public Result<Map<String, Object>> getWechatPayParams(@RequestBody Map<String, Object> params) {
+        Long orderId = Long.valueOf(params.get("orderId").toString());
+        String openid = params.get("openid").toString();
+        
+        Map<String, Object> payParams = packagePaymentService.createWechatPayOrder(orderId, openid);
+        return Result.success(payParams);
+    }
+
+    /**
+     * 微信支付回调
+     */
+    @Operation(summary = "微信支付回调")
+    @PostMapping("/notify")
+    public String handlePayNotify(@RequestBody String xmlData) {
+        boolean success = packagePaymentService.handlePayNotify(xmlData);
+        
+        if (success) {
+            return "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>";
+        } else {
+            return "<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[ERROR]]></return_msg></xml>";
+        }
+    }
+
+    /**
+     * 查询订单状态
+     */
+    @Operation(summary = "查询订单状态")
+    @PostMapping("/orders/{orderId}")
+    public Result<PackageOrder> getOrderStatus(@PathVariable Long orderId) {
+        PackageOrder order = packagePaymentService.getOrderById(orderId);
+        return Result.success(order);
+    }
+
+    /**
+     * 支付成功后应用套餐
+     */
+    @Operation(summary = "支付成功后应用套餐")
+    @PostMapping("/apply")
+    public Result<TaskPlanInstance> applyAfterPayment(@RequestBody Map<String, Object> params) {
+        Long orderId = Long.valueOf(params.get("orderId").toString());
+        Long childId = Long.valueOf(params.get("childId").toString());
+        java.util.List<Long> selectedItemIds = null;
+        if (params.get("selectedItemIds") instanceof java.util.List) {
+            selectedItemIds = new java.util.ArrayList<>();
+            for (Object itemId : (java.util.List<?>) params.get("selectedItemIds")) {
+                selectedItemIds.add(Long.valueOf(itemId.toString()));
+            }
+        }
+        
+        PackageOrder order = packagePaymentService.getOrderById(orderId);
+        if (order == null || !"paid".equals(order.getStatus())) {
+            return Result.error("订单未支付");
+        }
+
+        // 应用套餐
+        com.etotem.cfc.dto.ApplyPackageDTO dto = new com.etotem.cfc.dto.ApplyPackageDTO();
+        dto.setPackageId(order.getPackageId());
+        dto.setChildId(childId);
+        dto.setSelectedItemIds(selectedItemIds);
+        
+        TaskPlanInstance instance = planService.applyPackage(dto, order.getFamilyId());
+        return Result.success(instance);
+    }
+
+    /**
+     * 取消订单
+     */
+    @Operation(summary = "取消订单")
+    @PostMapping("/orders/{orderId}/cancel")
+    public Result<Boolean> cancelOrder(@PathVariable Long orderId) {
+        boolean success = packagePaymentService.cancelOrder(orderId);
+        return Result.success(success);
+    }
+}

+ 71 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/StreakController.java

@@ -0,0 +1,71 @@
+package com.etotem.cfc.controller;
+import javax.annotation.Resource;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.service.StreakService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+@Tag(name = "打卡管理", description = "连续打卡相关接口")
+@RestController
+@RequestMapping("/api/streak")
+public class StreakController {
+
+    @Resource
+    private StreakService streakService;
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Resource
+    private ChildMapper childMapper;
+
+    /**
+     * 获取孩子的打卡进度信息
+     */
+    @Operation(summary = "获取打卡进度")
+    @PostMapping("/progress/{childId}")
+    public Result<Map<String, Object>> getStreakProgress(
+            @RequestAttribute(value = "userId", required = false) Long userId,
+            @PathVariable Long childId) {
+
+        // 权限校验:验证用户是否有权限查看该孩子
+        if (userId != null) {
+            User user = userMapper.selectById(userId);
+            if (user == null) {
+                return Result.error("用户不存在");
+            }
+
+            Child child = childMapper.selectById(childId);
+            if (child == null) {
+                return Result.error("孩子不存在");
+            }
+
+            // 验证孩子是否属于用户的家庭
+            if (!child.getFamilyId().equals(user.getFamilyId())) {
+                return Result.error("无权限查看该孩子的打卡记录");
+            }
+        }
+
+        Map<String, Object> progress = streakService.getStreakProgress(childId);
+        return Result.success(progress);
+    }
+
+    /**
+     * 重置连续打卡(定时任务接口)
+     * 由管理员或定时任务触发
+     */
+    @Operation(summary = "重置中断的打卡")
+    @PostMapping("/reset")
+    public Result<Boolean> resetStreak() {
+        streakService.resetStreakIfNeeded();
+        return Result.success(true);
+    }
+}

+ 96 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/StreetController.java

@@ -0,0 +1,96 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.Street;
+import com.etotem.cfc.service.StreetService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 街道地址控制器 - 支持四级地址选择和匹配
+ */
+@Tag(name = "街道地址", description = "四级地址选择、区域匹配、统计接口")
+@RestController
+@RequestMapping("/api/streets")
+public class StreetController {
+
+    @Resource
+    private StreetService streetService;
+
+    /**
+     * 获取所有省份
+     */
+    @Operation(summary = "获取所有省份")
+    @PostMapping("/provinces")
+    public Result<List<Street>> getAllProvinces() {
+        List<Street> provinces = streetService.getAllProvinces();
+        return Result.success(provinces);
+    }
+
+    /**
+     * 根据父级ID获取子级地址
+     */
+    @Operation(summary = "根据父级ID获取子级地址")
+    @PostMapping("/children/{parentId}")
+    public Result<List<Street>> getChildren(@PathVariable Long parentId) {
+        List<Street> children = streetService.getChildrenByParentId(parentId);
+        return Result.success(children);
+    }
+
+    /**
+     * 根据ID获取地址详情
+     */
+    @Operation(summary = "根据ID获取地址详情")
+    @PostMapping("/{id}")
+    public Result<Street> getById(@PathVariable Long id) {
+        Street street = streetService.getById(id);
+        return Result.success(street);
+    }
+
+    /**
+     * 获取地址的完整路径
+     */
+    @Operation(summary = "获取地址完整路径")
+    @PostMapping("/{id}/path")
+    public Result<List<Street>> getAddressPath(@PathVariable Long id) {
+        List<Street> path = streetService.getAddressPath(id);
+        return Result.success(path);
+    }
+
+    /**
+     * 搜索地址
+     */
+    @Operation(summary = "搜索地址")
+    @PostMapping("/search")
+    public Result<List<Street>> search(@RequestBody Map<String, Object> params) {
+        String keyword = params.get("keyword") != null ? params.get("keyword").toString() : null;
+        List<Street> results = streetService.search(keyword);
+        return Result.success(results);
+    }
+
+    /**
+     * 区域回退匹配 - 获取匹配范围
+     */
+    @Operation(summary = "区域回退匹配", description = "根据街道ID返回街道级、区县级、城市级的匹配范围")
+    @PostMapping("/{id}/match")
+    public Result<Map<String, Object>> match(@PathVariable Long id) {
+        Map<String, Object> matchResult = streetService.matchByStreetWithFallback(id);
+        return Result.success(matchResult);
+    }
+
+    /**
+     * 按区域统计
+     */
+    @Operation(summary = "按区域统计", description = "按指定层级统计地址数量")
+    @PostMapping("/stats")
+    public Result<Map<String, Object>> getStatistics(@RequestBody(required = false) Map<String, Object> params) {
+        Integer level = params != null && params.get("level") != null ? Integer.valueOf(params.get("level").toString()) : 4;
+        Map<String, Object> stats = streetService.getStatisticsByLevel(level);
+        return Result.success(stats);
+    }
+}

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

@@ -0,0 +1,132 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.CreatePackageDTO;
+import com.etotem.cfc.dto.CreatePackageItemDTO;
+import com.etotem.cfc.entity.TaskTemplatePackage;
+import com.etotem.cfc.entity.TaskTemplateItem;
+import com.etotem.cfc.service.TaskTemplatePackageService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "任务模板套餐", description = "套餐创建、审核、发布等接口")
+@RestController
+@RequestMapping("/api/packages")
+public class TaskTemplatePackageController {
+
+    @Resource
+    private TaskTemplatePackageService packageService;
+
+    /**
+     * 创建套餐
+     */
+    @Operation(summary = "创建套餐")
+    @PostMapping("/create")
+    public Result<Long> createPackage(@RequestAttribute("userId") Long userId,
+                                       @RequestBody CreatePackageDTO dto) {
+        TaskTemplatePackage pkg = packageService.createPackage(dto, userId);
+        return Result.success(pkg.getId());
+    }
+
+    /**
+     * 更新套餐
+     */
+    @Operation(summary = "更新套餐")
+    @PutMapping("/{id}")
+    public Result<Boolean> updatePackage(@PathVariable Long id,
+                                          @RequestBody CreatePackageDTO dto) {
+        boolean success = packageService.updatePackage(id, dto);
+        return Result.success(success);
+    }
+
+    /**
+     * 获取套餐详情
+     */
+    @Operation(summary = "获取套餐详情")
+    @PostMapping("/{id}")
+    public Result<TaskTemplatePackage> getPackage(@PathVariable Long id) {
+        TaskTemplatePackage pkg = packageService.getPackageById(id);
+        return Result.success(pkg);
+    }
+
+    /**
+     * 获取我的套餐列表
+     */
+    @Operation(summary = "获取我的套餐列表")
+    @PostMapping("/my")
+    public Result<List<TaskTemplatePackage>> getMyPackages(@RequestAttribute("userId") Long userId) {
+        List<TaskTemplatePackage> packages = packageService.getMyPackages(userId);
+        return Result.success(packages);
+    }
+
+    /**
+     * 提交审核
+     */
+    @Operation(summary = "提交审核")
+    @PostMapping("/{id}/submit")
+    public Result<Boolean> submitForReview(@PathVariable Long id) {
+        boolean success = packageService.submitForReview(id);
+        return Result.success(success);
+    }
+
+    /**
+     * 发布/下架
+     */
+    @Operation(summary = "发布/下架")
+    @PostMapping("/{id}/publish")
+    public Result<Boolean> publishPackage(@PathVariable Long id,
+                                           @RequestBody Map<String, Object> params) {
+        boolean publish = params.get("publish") != null && Boolean.parseBoolean(params.get("publish").toString());
+        boolean success = packageService.publishPackage(id, publish);
+        return Result.success(success);
+    }
+
+    /**
+     * 添加任务项
+     */
+    @Operation(summary = "添加任务项")
+    @PostMapping("/{id}/items")
+    public Result<Long> addItem(@PathVariable Long id,
+                                  @RequestBody CreatePackageItemDTO dto) {
+        dto.setPackageId(id);
+        TaskTemplateItem item = packageService.addItem(dto);
+        return Result.success(item.getId());
+    }
+
+    /**
+     * 更新任务项
+     */
+    @Operation(summary = "更新任务项")
+    @PutMapping("/items/{itemId}")
+    public Result<Boolean> updateItem(@PathVariable Long itemId,
+                                        @RequestBody CreatePackageItemDTO dto) {
+        boolean success = packageService.updateItem(itemId, dto);
+        return Result.success(success);
+    }
+
+    /**
+     * 删除任务项
+     */
+    @Operation(summary = "删除任务项")
+    @DeleteMapping("/items/{itemId}")
+    public Result<Boolean> deleteItem(@PathVariable Long itemId) {
+        boolean success = packageService.deleteItem(itemId);
+        return Result.success(success);
+    }
+
+    /**
+     * 获取套餐的任务项列表
+     */
+    @Operation(summary = "获取套餐的任务项列表")
+    @PostMapping("/{id}/item-list")
+    public Result<List<TaskTemplateItem>> getPackageItems(@PathVariable Long id) {
+        List<TaskTemplateItem> items = packageService.getPackageItems(id);
+        return Result.success(items);
+    }
+}

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

@@ -0,0 +1,131 @@
+package com.etotem.cfc.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.ChildInfoDTO;
+import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.Family;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.UserService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "用户接口", description = "用户相关接口")
+@RestController
+@RequestMapping("/api/user")
+public class UserController {
+
+    @Resource
+    private UserService userService;
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Resource
+    private FamilyMapper familyMapper;
+
+    @Resource
+    private ChildMapper childMapper;
+
+    @PostMapping("/children/list")
+    @Operation(summary = "获取子女列表")
+    public Result<List<ChildInfoDTO>> getChildren(HttpServletRequest request) {
+        Long userId = (Long) request.getAttribute("userId");
+        List<ChildInfoDTO> children = userService.getChildren(userId);
+        return Result.success(children);
+    }
+
+    @PostMapping("/info")
+    @Operation(summary = "获取用户信息")
+    public Result<User> getUserInfo(HttpServletRequest request) {
+        Long userId = (Long) request.getAttribute("userId");
+        User user = userService.getUserInfo(userId);
+        if (user != null) {
+            user.setPassword(null);
+        }
+        return Result.success(user);
+    }
+
+    @PostMapping("/update")
+    @Operation(summary = "更新用户信息")
+    public Result<Boolean> updateUser(HttpServletRequest request, @RequestBody Map<String, Object> params) {
+        Long userId = (Long) request.getAttribute("userId");
+
+        com.etotem.cfc.dto.UpdateUserDTO dto = new com.etotem.cfc.dto.UpdateUserDTO();
+        if (params.containsKey("nickname")) dto.setNickname((String) params.get("nickname"));
+        if (params.containsKey("avatar")) dto.setAvatar((String) params.get("avatar"));
+        if (params.containsKey("realName")) dto.setRealName((String) params.get("realName"));
+        if (params.containsKey("role")) dto.setRole((String) params.get("role"));
+        if (params.containsKey("gender")) dto.setGender((String) params.get("gender"));
+        if (params.containsKey("birthday")) dto.setBirthday((String) params.get("birthday"));
+        if (params.containsKey("ethnicity")) dto.setEthnicity((String) params.get("ethnicity"));
+        if (params.containsKey("bloodType")) dto.setBloodType((String) params.get("bloodType"));
+        if (params.containsKey("highestEducation")) dto.setHighestEducation((String) params.get("highestEducation"));
+        if (params.containsKey("maritalStatus")) dto.setMaritalStatus((String) params.get("maritalStatus"));
+        if (params.containsKey("familyRole")) dto.setFamilyRole((String) params.get("familyRole"));
+
+        boolean success = userService.updateUserInfo(userId, dto);
+        return success ? Result.success(true) : Result.error("更新失败");
+    }
+
+    @PostMapping("/family-members")
+    @Operation(summary = "获取家庭成员列表")
+    public Result<Map<String, Object>> getFamilyMembers(HttpServletRequest request) {
+        Long userId = (Long) request.getAttribute("userId");
+        User user = userMapper.selectById(userId);
+
+        if (user == null) {
+            return Result.error("用户不存在");
+        }
+
+        Long familyId = user.getFamilyId();
+        if (familyId == null) {
+            return Result.error("用户未加入家庭");
+        }
+
+        Family family = familyMapper.selectById(familyId);
+        if (family == null) {
+            return Result.error("家庭不存在");
+        }
+
+        List<User> parents = userMapper.selectList(
+            new LambdaQueryWrapper<User>()
+                .eq(User::getFamilyId, familyId)
+                .eq(User::getRole, "parent")
+        );
+
+        List<Child> children = childMapper.selectList(
+            new LambdaQueryWrapper<Child>()
+                .eq(Child::getFamilyId, familyId)
+        );
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("familyId", familyId);
+        result.put("familyName", family.getName());
+        result.put("inviteCode", family.getInviteCode());
+        result.put("parents", parents);
+        result.put("children", children);
+
+        return Result.success(result);
+    }
+
+    @PutMapping("/children/{childId}")
+    @Operation(summary = "更新孩子信息")
+    public Result<Boolean> updateChild(HttpServletRequest request,
+                                       @PathVariable Long childId,
+                                       @RequestBody com.etotem.cfc.dto.UpdateChildDTO dto) {
+        Long userId = (Long) request.getAttribute("userId");
+        boolean success = userService.updateChild(userId, childId, dto);
+        return Result.success(success);
+    }
+}

+ 295 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminAuthController.java

@@ -0,0 +1,295 @@
+package com.etotem.cfc.controller.admin;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.config.JwtConfig;
+import com.etotem.cfc.dto.AdminLoginDTO;
+import com.etotem.cfc.dto.SendCodeDTO;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.LoginAttemptService;
+import com.etotem.cfc.service.UserService;
+import com.etotem.cfc.service.VerificationCodeService;
+import lombok.extern.slf4j.Slf4j;
+import javax.annotation.Resource;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+@Slf4j
+@RestController
+@RequestMapping("/api/admin-auth")
+public class AdminAuthController {
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Resource
+    private UserService userService;
+
+    @Resource
+    private VerificationCodeService verificationCodeService;
+
+    @Resource
+    private JwtConfig jwtConfig;
+
+    @Resource
+    private LoginAttemptService loginAttemptService;
+
+    /**
+     * 发送管理员登录验证码
+     */
+    @PostMapping("/send-code")
+    public Result<Boolean> sendCode(@RequestBody SendCodeDTO dto) {
+        if (dto.getPhone() == null || dto.getPhone().isEmpty()) {
+            return Result.error("手机号不能为空");
+        }
+
+        // 验证手机号格式
+        if (!dto.getPhone().matches("^1[3-9]\\d{9}$")) {
+            return Result.error("手机号格式不正确");
+        }
+
+        // 验证该手机号是否已注册且为管理员/规划师
+        User user = userMapper.selectOne(new LambdaQueryWrapper<User>()
+                .eq(User::getPhone, dto.getPhone()));
+
+        if (user == null) {
+            return Result.error("该手机号未注册");
+        }
+
+        // 检查是否有管理员或规划师角色
+        if (!"admin".equals(user.getRole()) && !userService.hasRole(user.getId(), "teacher")) {
+            return Result.error("该账号没有管理权限");
+        }
+
+        // 生成验证码
+        verificationCodeService.generateCode(dto.getPhone(), "admin_login");
+        return Result.success(true);
+    }
+
+    /**
+     * 管理员验证码登录
+     */
+    @PostMapping("/login")
+    public Result<Map<String, Object>> login(@RequestBody AdminLoginDTO dto, HttpServletRequest request) {
+        if (dto.getPhone() == null || dto.getCode() == null) {
+            return Result.error("手机号和验证码不能为空");
+        }
+
+        String ip = getClientIP(request);
+        String lockKey = dto.getPhone() + ":" + ip;
+        if (loginAttemptService.isBlocked(lockKey)) {
+            long remaining = loginAttemptService.getLockRemainingTime(lockKey);
+            return Result.error("登录尝试过多,请" + (remaining / 60000 + 1) + "分钟后重试");
+        }
+
+        boolean valid = verificationCodeService.verifyCode(dto.getPhone(), dto.getCode(), "admin_login");
+        if (!valid) {
+            loginAttemptService.recordFailure(lockKey);
+            return Result.error("验证码错误或已过期");
+        }
+
+        User user = userMapper.selectOne(new LambdaQueryWrapper<User>()
+                .eq(User::getPhone, dto.getPhone()));
+
+        if (user == null) {
+            return Result.error("用户不存在");
+        }
+
+        loginAttemptService.clearSuccess(lockKey);
+
+        // 使用用户的角色,如果是admin或teacher
+        String role = user.getRole() != null ? user.getRole() : "admin";
+
+        String token = jwtConfig.generateToken(user.getId(), role);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("token", token);
+        result.put("adminId", user.getId());
+        result.put("username", user.getNickname() != null ? user.getNickname() : user.getPhone());
+        result.put("realName", user.getRealName() != null ? user.getRealName() : user.getNickname());
+        result.put("role", role);
+
+        return Result.success(result);
+    }
+
+    /**
+     * 获取当前登录管理员信息
+     */
+    @PostMapping("/info")
+    public Result<Map<String, Object>> getAdminInfo(@RequestAttribute("userId") Long userId) {
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            return Result.error("用户不存在");
+        }
+        Map<String, Object> result = new HashMap<>();
+        result.put("id", user.getId());
+        result.put("nickname", user.getNickname());
+        result.put("phone", user.getPhone());
+        result.put("email", "");
+        result.put("username", user.getNickname() != null ? user.getNickname() : user.getPhone());
+        result.put("realName", user.getRealName() != null ? user.getRealName() : user.getNickname());
+        result.put("role", user.getRole());
+        // 清除敏感信息
+        user.setPassword(null);
+        result.put("user", user);
+        return Result.success(result);
+    }
+
+    /**
+     * 管理员修改密码
+     */
+    @PostMapping("/change-password")
+    public Result<Boolean> changePassword(@RequestAttribute("userId") Long userId,
+                                           @RequestBody Map<String, String> params) {
+        String oldPassword = params.get("oldPassword");
+        String newPassword = params.get("newPassword");
+
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            return Result.error("用户不存在");
+        }
+
+        // 验证旧密码
+        if (!userService.verifyPassword(userId, oldPassword)) {
+            return Result.error("原密码错误");
+        }
+
+        // 更新密码
+        userService.setPassword(userId, newPassword);
+
+        return Result.success(true);
+    }
+
+    /**
+     * 更新管理员个人信息
+     */
+    @PostMapping("/update-info")
+    public Result<Map<String, Object>> updateInfo(@RequestAttribute("userId") Long userId,
+                                                     @RequestBody Map<String, Object> params) {
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            return Result.error("用户不存在");
+        }
+
+        if (params.containsKey("nickname")) {
+            user.setNickname((String) params.get("nickname"));
+        }
+        if (params.containsKey("phone")) {
+            user.setPhone((String) params.get("phone"));
+        }
+
+        user.setUpdatedAt(new Date());
+        userMapper.updateById(user);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("nickname", user.getNickname());
+        result.put("phone", user.getPhone());
+        return Result.success(result);
+    }
+
+    /**
+     * 通过验证码重置密码
+     */
+    @PostMapping("/reset-password")
+    public Result<Boolean> resetPassword(@RequestAttribute("userId") Long userId,
+                                           @RequestBody Map<String, String> params) {
+        String phone = params.get("phone");
+        String code = params.get("code");
+        String newPassword = params.get("newPassword");
+
+        if (phone == null || code == null || newPassword == null) {
+            return Result.error("手机号、验证码和新密码不能为空");
+        }
+
+        if (!verificationCodeService.verifyCode(phone, code, "resetpwd")) {
+            return Result.error("验证码错误或已过期");
+        }
+
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            return Result.error("用户不存在");
+        }
+
+        userService.resetPassword(userId, newPassword);
+
+        return Result.success(true);
+    }
+
+    /**
+     * 管理员用户名密码登录
+     */
+    @PostMapping("/login-by-password")
+    public Result<Map<String, Object>> loginByPassword(@RequestBody AdminLoginDTO dto, HttpServletRequest request) {
+        if (dto.getPhone() == null || dto.getPassword() == null) {
+            return Result.error("用户名和密码不能为空");
+        }
+
+        String ip = getClientIP(request);
+        String lockKey = dto.getPhone() + ":" + ip;
+        if (loginAttemptService.isBlocked(lockKey)) {
+            long remaining = loginAttemptService.getLockRemainingTime(lockKey);
+            return Result.error("登录尝试过多,请" + (remaining / 60000 + 1) + "分钟后重试");
+        }
+
+        // 通过手机号查找用户
+        User user = userMapper.selectOne(new LambdaQueryWrapper<User>()
+                .eq(User::getPhone, dto.getPhone()));
+
+        if (user == null) {
+            return Result.error("用户不存在");
+        }
+
+        // 验证密码(兼容两种hash方式)
+        java.nio.charset.Charset utf8 = java.nio.charset.StandardCharsets.UTF_8;
+        String hash1 = org.springframework.util.DigestUtils.md5DigestAsHex(
+                ("xzyj_salt_" + dto.getPassword()).getBytes(utf8));
+        String hash2 = org.springframework.util.DigestUtils.md5DigestAsHex(
+                ("xzyj_admin_salt_" + dto.getPassword()).getBytes(utf8));
+        if (!hash1.equals(user.getPassword()) && !hash2.equals(user.getPassword())) {
+            loginAttemptService.recordFailure(lockKey);
+            int remaining = loginAttemptService.getRemainingAttempts(lockKey);
+            return Result.error("密码错误,剩余" + remaining + "次尝试机会");
+        }
+
+        loginAttemptService.clearSuccess(lockKey);
+
+        // 确定角色
+        String role;
+        if ("admin".equals(user.getRole())) {
+            role = "admin";
+        } else if ("teacher".equals(user.getRole()) || userService.hasRole(user.getId(), "teacher")) {
+            role = "teacher";
+        } else {
+            // 如果用户既不是admin也不是teacher,给个默认角色,但记录警告
+            role = user.getRole() != null ? user.getRole() : "admin";
+            log.warn("用户 {} 登录后台管理,角色为: {}", user.getId(), role);
+        }
+
+        String token = jwtConfig.generateToken(user.getId(), role);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("token", token);
+        result.put("adminId", user.getId());
+        result.put("username", user.getNickname() != null ? user.getNickname() : user.getPhone());
+        result.put("realName", user.getRealName() != null ? user.getRealName() : user.getNickname());
+        result.put("role", role);
+
+        return Result.success(result);
+    }
+
+    private String getClientIP(HttpServletRequest request) {
+        String ip = request.getHeader("X-Forwarded-For");
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("X-Real-IP");
+        }
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getRemoteAddr();
+        }
+        return ip;
+    }
+}

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

@@ -0,0 +1,883 @@
+package com.etotem.cfc.controller.admin;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.*;
+import com.etotem.cfc.mapper.*;
+import com.etotem.cfc.service.UserService;
+import javax.annotation.Resource;
+
+import org.springframework.util.DigestUtils;
+
+import java.util.Date;
+
+import io.swagger.v3.oas.annotations.Operation;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/admin")
+public class AdminController {
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Resource
+    private ChildMapper childMapper;
+
+    @Resource
+    private TaskMapper taskMapper;
+
+    @Resource
+    private RewardMapper rewardMapper;
+
+    @Resource
+    private PointsLogMapper pointsLogMapper;
+
+    @Resource
+    private FamilyMapper familyMapper;
+
+    @Resource
+    private TaskTemplateMapper taskTemplateMapper;
+
+    @Resource
+    private AdminMapper adminMapper;
+
+    @Resource
+    private UserService userService;
+
+    @Resource
+    private GuidePackageMapper guidePackageMapper;
+
+    // 用户管理
+    @PostMapping("/users")
+    public Result<Page<User>> getUsers(@RequestBody Map<String, Object> params) {
+        int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
+        int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 10;
+        Page<User> pageParam = new Page<>(page, size);
+        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
+        if (params.containsKey("role") && params.get("role") != null && !params.get("role").toString().isEmpty()) {
+            wrapper.eq(User::getRole, String.valueOf(params.get("role")));
+        }
+        if (params.containsKey("nickname") && params.get("nickname") != null && !params.get("nickname").toString().isEmpty()) {
+            wrapper.like(User::getNickname, String.valueOf(params.get("nickname")));
+        }
+        if (params.containsKey("phone") && params.get("phone") != null && !params.get("phone").toString().isEmpty()) {
+            wrapper.like(User::getPhone, String.valueOf(params.get("phone")));
+        }
+        Page<User> result = userMapper.selectPage(pageParam, wrapper);
+        return Result.success(result);
+    }
+
+    @PostMapping("/users/create")
+    public Result<User> createUser(@RequestBody Map<String, Object> params) {
+        String phone = (String) params.get("phone");
+        String nickname = (String) params.get("nickname");
+        String role = (String) params.get("role");
+        String realName = (String) params.get("realName");
+        
+        if (phone == null || phone.isEmpty()) {
+            return Result.error("手机号不能为空");
+        }
+        
+        User existingUser = userMapper.selectOne(new LambdaQueryWrapper<User>()
+            .eq(User::getPhone, phone));
+        if (existingUser != null) {
+            return Result.error("手机号已存在");
+        }
+        
+        User user = new User();
+        user.setPhone(phone);
+        user.setNickname(nickname != null ? nickname : "用户" + phone.substring(7));
+        user.setRole(role != null ? role : "parent");
+        user.setRealName(realName);
+        user.setPassword("");
+        user.setCreatedAt(new Date());
+        user.setUpdatedAt(new Date());
+        
+        userMapper.insert(user);
+        return Result.success(user);
+    }
+
+    @PostMapping("/users/{id}")
+    public Result<User> getUser(@PathVariable Long id) {
+        return Result.success(userMapper.selectById(id));
+    }
+
+    @PutMapping("/users/{id}")
+    public Result<Boolean> updateUser(@PathVariable Long id, @RequestBody User user) {
+        user.setId(id);
+        userMapper.updateById(user);
+        return Result.success(true);
+    }
+
+    @PostMapping("/users/{id}/reset-password")
+    public Result<Boolean> resetUserPassword(@PathVariable Long id, @RequestBody Map<String, String> params) {
+        String newPassword = params.get("password");
+        if (newPassword == null || newPassword.length() < 6) {
+            return Result.error("密码长度不能少于6位");
+        }
+        boolean success = userService.resetPassword(id, newPassword);
+        if (success) {
+            return Result.success(true);
+        }
+        return Result.error("重置密码失败");
+    }
+
+    @DeleteMapping("/users/{id}")
+    public Result<Boolean> deleteUser(@PathVariable Long id) {
+        userMapper.deleteById(id);
+        return Result.success(true);
+    }
+
+    // 孩子管理
+    @PostMapping("/children")
+    public Result<Page<Child>> getChildren(@RequestBody Map<String, Object> params) {
+        int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
+        int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 10;
+        Page<Child> pageParam = new Page<>(page, size);
+        Page<Child> result = childMapper.selectPage(pageParam, null);
+        return Result.success(result);
+    }
+
+    @PostMapping("/children/{id}")
+    public Result<Child> getChild(@PathVariable Long id) {
+        return Result.success(childMapper.selectById(id));
+    }
+
+    @PutMapping("/children/{id}")
+    public Result<Boolean> updateChild(@PathVariable Long id, @RequestBody Child child) {
+        child.setId(id);
+        childMapper.updateById(child);
+        return Result.success(true);
+    }
+
+    @PostMapping("/children/{id}/adjust-points")
+    public Result<Map<String, Object>> adjustPoints(
+            @PathVariable Long id,
+            @RequestBody Map<String, Object> params) {
+        Integer amount = params.get("amount") != null ? ((Number) params.get("amount")).intValue() : 0;
+        String reason = (String) params.get("reason");
+
+        Child child = childMapper.selectById(id);
+        if (child == null) {
+            return Result.error("孩子不存在");
+        }
+
+        int newBalance = child.getTotalPoints() + amount;
+        if (newBalance < 0) {
+            return Result.error("积分不能为负");
+        }
+
+        child.setTotalPoints(newBalance);
+        child.setUpdatedAt(new Date());
+        childMapper.updateById(child);
+
+        // 记录积分流水
+        PointsLog log = new PointsLog();
+        log.setChildId(id);
+        log.setAmount(amount);
+        log.setType("adjust");
+        log.setDescription(reason);
+        log.setCreatedAt(new Date());
+        pointsLogMapper.insert(log);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("newBalance", newBalance);
+        return Result.success(result);
+    }
+
+    // 任务管理
+    @PostMapping("/tasks")
+    public Result<Page<Task>> getTasks(@RequestBody Map<String, Object> params) {
+        int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
+        int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 10;
+        String status = (String) params.get("status");
+        Long familyId = params.get("familyId") != null ? ((Number) params.get("familyId")).longValue() : null;
+        
+        Page<Task> pageParam = new Page<>(page, size);
+        LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<>();
+        if (status != null && !status.isEmpty()) {
+            wrapper.eq(Task::getStatus, status);
+        }
+        if (familyId != null) {
+            wrapper.eq(Task::getFamilyId, familyId);
+        }
+        wrapper.orderByDesc(Task::getCreatedAt);
+        Page<Task> result = taskMapper.selectPage(pageParam, wrapper);
+        return Result.success(result);
+    }
+
+    @PostMapping("/tasks/stats")
+    public Result<Map<String, Object>> getTaskStats() {
+        Map<String, Object> stats = new HashMap<>();
+        
+        Long total = taskMapper.selectCount(null);
+        Long today = taskMapper.selectCount(new LambdaQueryWrapper<Task>()
+                .ge(Task::getDeadline, new Date()));
+        
+        stats.put("total", total);
+        stats.put("today", today);
+        
+        return Result.success(stats);
+    }
+
+    // 奖励管理
+    @PostMapping("/rewards")
+    public Result<Page<Reward>> getRewards(@RequestBody Map<String, Object> params) {
+        int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
+        int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 10;
+        String status = (String) params.get("status");
+        Long familyId = params.get("familyId") != null ? ((Number) params.get("familyId")).longValue() : null;
+        
+        Page<Reward> pageParam = new Page<>(page, size);
+        LambdaQueryWrapper<Reward> wrapper = new LambdaQueryWrapper<>();
+        if (status != null && !status.isEmpty()) {
+            wrapper.eq(Reward::getStatus, status);
+        }
+        if (familyId != null) {
+            wrapper.eq(Reward::getFamilyId, familyId);
+        }
+        wrapper.orderByDesc(Reward::getCreatedAt);
+        Page<Reward> result = rewardMapper.selectPage(pageParam, wrapper);
+        return Result.success(result);
+    }
+
+    @PostMapping("/rewards/templates")
+    public Result<Object> getRewardTemplates(@RequestAttribute("userId") Long userId) {
+        // 获取用户的家庭 ID,只返回该家庭的模板
+        com.etotem.cfc.entity.User user = userService.getUserInfo(userId);
+        if (user == null || user.getFamilyId() == null) {
+            return Result.error("用户必须属于某个家庭");
+        }
+        
+        return Result.success(rewardMapper.selectList(new LambdaQueryWrapper<Reward>()
+                .eq(Reward::getIsTemplate, 1)
+                .eq(Reward::getFamilyId, user.getFamilyId()))); // 按家庭 ID 过滤
+    }
+
+    @PostMapping("/rewards/create")
+    public Result<Long> createRewardTemplate(@RequestBody Reward reward, @RequestAttribute("userId") Long userId) {
+        // 强制关联家庭
+        com.etotem.cfc.entity.User user = userService.getUserInfo(userId);
+        if (user == null || user.getFamilyId() == null) {
+            return Result.error("用户必须属于某个家庭");
+        }
+        
+        reward.setFamilyId(user.getFamilyId());
+        reward.setIsTemplate(1);
+        reward.setCreatedAt(new Date());
+        reward.setUpdatedAt(new Date());
+        rewardMapper.insert(reward);
+        return Result.success(reward.getId());
+    }
+
+    @DeleteMapping("/rewards/templates/{id}")
+    public Result<Boolean> deleteRewardTemplate(@PathVariable Long id, @RequestAttribute("userId") Long userId) {
+        // 验证家庭归属
+        com.etotem.cfc.entity.User user = userService.getUserInfo(userId);
+        if (user != null && user.getFamilyId() != null) {
+            Reward reward = rewardMapper.selectById(id);
+            if (reward != null && !reward.getFamilyId().equals(user.getFamilyId())) {
+                return Result.error("无权删除其他家庭的模板");
+            }
+        }
+        rewardMapper.deleteById(id);
+        return Result.success(true);
+    }
+
+    @PutMapping("/rewards/templates/{id}")
+    public Result<Boolean> updateRewardTemplate(@PathVariable Long id, @RequestBody Reward reward, @RequestAttribute("userId") Long userId) {
+        // 验证家庭归属
+        com.etotem.cfc.entity.User user = userService.getUserInfo(userId);
+        if (user == null || user.getFamilyId() == null) {
+            return Result.error("用户必须属于某个家庭");
+        }
+        
+        Reward existingReward = rewardMapper.selectById(id);
+        if (existingReward != null && !existingReward.getFamilyId().equals(user.getFamilyId())) {
+            return Result.error("无权修改其他家庭的模板");
+        }
+        
+        reward.setId(id);
+        reward.setFamilyId(user.getFamilyId()); // 确保更新时保持家庭关联
+        reward.setUpdatedAt(new Date());
+        rewardMapper.updateById(reward);
+        return Result.success(true);
+    }
+
+    @PostMapping("/rewards/{id}/approve")
+    public Result<Boolean> approveReward(@PathVariable Long id, @RequestBody Map<String, Boolean> params) {
+        Boolean approved = params.get("approved");
+        Reward reward = rewardMapper.selectById(id);
+        if (reward == null) {
+            return Result.error("奖励不存在");
+        }
+
+        if (approved) {
+            reward.setStatus("exchanged");
+            reward.setExchangeApproved(1);
+            reward.setExchangedAt(new Date());
+        } else {
+            // 拒绝,返还积分
+            Child child = childMapper.selectById(reward.getChildId());
+            if (child != null) {
+                child.setTotalPoints(child.getTotalPoints() + reward.getPointsRequired());
+                childMapper.updateById(child);
+
+                PointsLog log = new PointsLog();
+                log.setChildId(reward.getChildId());
+                log.setAmount(reward.getPointsRequired());
+                log.setType("refund");
+                log.setDescription("奖励兑换被拒绝,积分退还");
+                log.setCreatedAt(new Date());
+                pointsLogMapper.insert(log);
+            }
+            reward.setStatus("rejected");
+            reward.setExchangeApproved(0);
+        }
+        reward.setUpdatedAt(new Date());
+        rewardMapper.updateById(reward);
+        return Result.success(true);
+    }
+
+    // 积分管理
+    @PostMapping("/points/logs")
+    public Result<Page<PointsLog>> getPointsLogs(@RequestBody Map<String, Object> params) {
+        Integer page = 1;
+        Integer size = 10;
+        Long childId = null;
+        
+        // 安全解析page
+        if (params.get("page") != null) {
+            if (params.get("page") instanceof Integer) {
+                page = ((Number) params.get("page")).intValue();
+            } else if (params.get("page") instanceof Number) {
+                page = ((Number) params.get("page")).intValue();
+            } else {
+                try { page = Integer.parseInt(params.get("page").toString()); } catch (Exception e) {}
+            }
+        }
+        
+        // 安全解析size
+        if (params.get("size") != null) {
+            if (params.get("size") instanceof Integer) {
+                size = ((Number) params.get("size")).intValue();
+            } else if (params.get("size") instanceof Number) {
+                size = ((Number) params.get("size")).intValue();
+            } else {
+                try { size = Integer.parseInt(params.get("size").toString()); } catch (Exception e) {}
+            }
+        }
+        
+        // 安全解析childId
+        if (params.get("childId") != null) {
+            if (params.get("childId") instanceof Number) {
+                childId = ((Number) params.get("childId")).longValue();
+            } else {
+                try { childId = Long.parseLong(params.get("childId").toString()); } catch (Exception e) {}
+            }
+        }
+        
+        String type = (String) params.get("type");
+        
+        Page<PointsLog> pageParam = new Page<>(page, size);
+        LambdaQueryWrapper<PointsLog> wrapper = new LambdaQueryWrapper<>();
+        if (childId != null) {
+            wrapper.eq(PointsLog::getChildId, childId);
+        }
+        if (type != null && !type.isEmpty()) {
+            wrapper.eq(PointsLog::getType, type);
+        }
+        wrapper.orderByDesc(PointsLog::getCreatedAt);
+        Page<PointsLog> result = pointsLogMapper.selectPage(pageParam, wrapper);
+        return Result.success(result);
+    }
+
+    @PostMapping("/points/stats")
+    public Result<Map<String, Object>> getPointsStats() {
+        Map<String, Object> stats = new HashMap<>();
+
+        Long totalChildren = childMapper.selectCount(null);
+
+        stats.put("totalChildren", totalChildren);
+
+        return Result.success(stats);
+    }
+
+    // 家庭管理
+    @PostMapping("/families")
+    public Result<Page<Family>> getFamilies(@RequestBody Map<String, Object> params) {
+        int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
+        int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 10;
+        String name = (String) params.get("name");
+        
+        Page<Family> pageParam = new Page<>(page, size);
+        LambdaQueryWrapper<Family> wrapper = new LambdaQueryWrapper<>();
+        if (name != null && !name.isEmpty()) {
+            wrapper.like(Family::getName, name);
+        }
+        wrapper.orderByDesc(Family::getCreatedAt);
+        Page<Family> result = familyMapper.selectPage(pageParam, wrapper);
+        return Result.success(result);
+    }
+
+    @PostMapping("/families/{id}")
+    public Result<Family> getFamily(@PathVariable Long id) {
+        return Result.success(familyMapper.selectById(id));
+    }
+
+    @PostMapping("/families/{id}/members")
+    public Result<Map<String, Object>> getFamilyMembers(@PathVariable Long id) {
+        Family family = familyMapper.selectById(id);
+        if (family == null) {
+            return Result.error("家庭不存在");
+        }
+
+        // 查询家长
+        LambdaQueryWrapper<User> parentWrapper = new LambdaQueryWrapper<>();
+        parentWrapper.eq(User::getFamilyId, id).eq(User::getRole, "parent");
+        java.util.List<User> parents = userMapper.selectList(parentWrapper);
+
+        // 查询孩子
+        LambdaQueryWrapper<Child> childWrapper = new LambdaQueryWrapper<>();
+        childWrapper.eq(Child::getFamilyId, id);
+        java.util.List<Child> children = childMapper.selectList(childWrapper);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("family", family);
+        result.put("parents", parents);
+        result.put("children", children);
+
+        return Result.success(result);
+    }
+
+    @PutMapping("/families/{id}")
+    public Result<Boolean> updateFamily(@PathVariable Long id, @RequestBody Family family) {
+        family.setId(id);
+        family.setUpdatedAt(new Date());
+        familyMapper.updateById(family);
+        return Result.success(true);
+    }
+
+    @DeleteMapping("/families/{id}")
+    public Result<Boolean> deleteFamily(@PathVariable Long id) {
+        familyMapper.deleteById(id);
+        return Result.success(true);
+    }
+
+    // 任务模板管理
+    @PostMapping("/task-templates")
+    public Result<Page<TaskTemplate>> getTaskTemplates(@RequestBody Map<String, Object> params) {
+        int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
+        int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 10;
+        String category = (String) params.get("category");
+        String difficulty = (String) params.get("difficulty");
+        Integer isActive = params.get("isActive") != null ? ((Number) params.get("isActive")).intValue() : null;
+        
+        Page<TaskTemplate> pageParam = new Page<>(page, size);
+        LambdaQueryWrapper<TaskTemplate> wrapper = new LambdaQueryWrapper<>();
+        if (category != null && !category.isEmpty()) {
+            wrapper.eq(TaskTemplate::getCategory, category);
+        }
+        if (difficulty != null && !difficulty.isEmpty()) {
+            wrapper.eq(TaskTemplate::getDifficulty, difficulty);
+        }
+        if (isActive != null) {
+            wrapper.eq(TaskTemplate::getIsActive, isActive);
+        }
+        wrapper.orderByDesc(TaskTemplate::getCreatedAt);
+        Page<TaskTemplate> result = taskTemplateMapper.selectPage(pageParam, wrapper);
+        return Result.success(result);
+    }
+
+    @PostMapping("/task-templates/{id}")
+    public Result<TaskTemplate> getTaskTemplate(@PathVariable Long id) {
+        return Result.success(taskTemplateMapper.selectById(id));
+    }
+
+    @PostMapping("/task-templates/create")
+    public Result<Long> createTaskTemplate(@RequestBody TaskTemplate template) {
+        template.setCreatedAt(new Date());
+        template.setUpdatedAt(new Date());
+        if (template.getIsActive() == null) {
+            template.setIsActive(1);
+        }
+        taskTemplateMapper.insert(template);
+        return Result.success(template.getId());
+    }
+
+    @PutMapping("/task-templates/{id}")
+    public Result<Boolean> updateTaskTemplate(@PathVariable Long id, @RequestBody TaskTemplate template) {
+        template.setId(id);
+        template.setUpdatedAt(new Date());
+        taskTemplateMapper.updateById(template);
+        return Result.success(true);
+    }
+
+    @DeleteMapping("/task-templates/{id}")
+    public Result<Boolean> deleteTaskTemplate(@PathVariable Long id) {
+        taskTemplateMapper.deleteById(id);
+        return Result.success(true);
+    }
+
+    // ========== 成长规划师审核相关接口 ==========
+
+    /**
+     * 获取待审核的成长规划师申请列表
+     */
+    @Operation(summary = "获取成长规划师审核列表")
+    @PostMapping("/guide/applications")
+    public Result<Page<User>> getGuideApplications(@RequestBody Map<String, Object> params) {
+        String status = params.get("status") != null ? (String) params.get("status") : "pending";
+        int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
+        int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 10;
+
+        Page<User> pageParam = new Page<>(page, size);
+        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
+            .eq(User::getTeacherStatus, status)
+            .isNotNull(User::getRealName)
+            .orderByDesc(User::getUpdatedAt);
+
+        Page<User> result = userMapper.selectPage(pageParam, wrapper);
+        return Result.success(result);
+    }
+
+    /**
+     * 审核成长规划师申请 - 通过
+     */
+    @Operation(summary = "通过成长规划师审核")
+    @PostMapping("/guide/approve/{userId}")
+    public Result<Boolean> approveGuide(@PathVariable Long userId) {
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            return Result.error("用户不存在");
+        }
+
+        user.setTeacherStatus("approved");
+        user.setRole("teacher");
+        user.setUpdatedAt(new Date());
+        userMapper.updateById(user);
+
+        // 同步创建/更新admins表记录,确保规划师可登录Web管理端并显示规划师菜单
+        Admin admin = adminMapper.selectOne(new LambdaQueryWrapper<Admin>()
+                .eq(Admin::getPhone, user.getPhone()));
+        if (admin == null) {
+            admin = new Admin();
+            admin.setPhone(user.getPhone());
+            admin.setUsername(user.getPhone());
+            admin.setRealName(user.getRealName());
+            admin.setNickname(user.getNickname());
+            admin.setRole("teacher");
+            admin.setStatus(1);
+            admin.setPassword(DigestUtils.md5DigestAsHex(("xzyj_admin_salt_" + user.getPhone()).getBytes()));
+            admin.setCreatedAt(new Date());
+            admin.setUpdatedAt(new Date());
+            adminMapper.insert(admin);
+        } else {
+            admin.setRole("teacher");
+            admin.setUpdatedAt(new Date());
+            adminMapper.updateById(admin);
+        }
+
+        return Result.success(true);
+    }
+
+    /**
+     * 审核成长规划师申请 - 拒绝
+     */
+    @Operation(summary = "拒绝成长规划师审核")
+    @PostMapping("/guide/reject/{userId}")
+    public Result<Boolean> rejectGuide(@PathVariable Long userId, @RequestBody Map<String, String> params) {
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            return Result.error("用户不存在");
+        }
+
+        user.setTeacherStatus("rejected");
+        user.setTeacherRejectReason(params.get("reason"));
+        user.setUpdatedAt(new Date());
+        userMapper.updateById(user);
+
+        return Result.success(true);
+    }
+
+    /**
+     * 获取待审核成长规划师数量
+     */
+    @Operation(summary = "获取待审核成长规划师数量")
+    @PostMapping("/guide/pending-count")
+    public Result<Map<String, Object>> getPendingGuideCount() {
+        Long count = userMapper.selectCount(new LambdaQueryWrapper<User>()
+            .eq(User::getTeacherStatus, "pending")
+            .isNotNull(User::getRealName));
+        Map<String, Object> result = new HashMap<>();
+        result.put("count", count);
+        return Result.success(result);
+    }
+
+    /**
+     * 获取待审核套餐数量
+     */
+    @Operation(summary = "获取待审核套餐数量")
+    @PostMapping("/package/pending-count")
+    public Result<Map<String, Object>> getPendingPackageCount() {
+        Long count = guidePackageMapper.selectCount(new LambdaQueryWrapper<com.etotem.cfc.entity.GuidePackage>()
+            .eq(com.etotem.cfc.entity.GuidePackage::getStatus, "pending"));
+        Map<String, Object> result = new HashMap<>();
+        result.put("count", count);
+        return Result.success(result);
+    }
+
+    // ========== 成长规划师相关接口 ==========
+
+    /**
+     * 获取成长规划师关联的家庭列表
+     */
+    @PostMapping("/teacher/families")
+    public Result<Object> getTeacherFamilies(@RequestHeader("X-User-Id") Long userId) {
+        User teacher = userMapper.selectById(userId);
+        if (teacher == null || !"teacher".equals(teacher.getRole())) {
+            return Result.error("非成长规划师账号");
+        }
+
+        String familyIds = teacher.getTeacherFamilyIds();
+        if (familyIds == null || familyIds.isEmpty()) {
+            return Result.success(new java.util.ArrayList<>());
+        }
+
+        // 解析家庭ID列表
+        java.util.List<Long> ids = new java.util.ArrayList<>();
+        for (String id : familyIds.split(",")) {
+            try {
+                ids.add(Long.parseLong(id.trim()));
+            } catch (Exception e) {
+                // 忽略
+            }
+        }
+
+        if (ids.isEmpty()) {
+            return Result.success(new java.util.ArrayList<>());
+        }
+
+        // 查询家庭信息
+        java.util.List<Family> families = familyMapper.selectBatchIds(ids);
+        return Result.success(families);
+    }
+
+    /**
+     * 获取成长规划师发布的任务模板
+     */
+    @PostMapping("/teacher/task-templates")
+    public Result<Object> getTeacherTaskTemplates(@RequestHeader("X-User-Id") Long userId) {
+        User teacher = userMapper.selectById(userId);
+        if (teacher == null || !"teacher".equals(teacher.getRole())) {
+            return Result.error("非成长规划师账号");
+        }
+
+        java.util.List<TaskTemplate> templates = taskTemplateMapper.selectList(
+            new LambdaQueryWrapper<TaskTemplate>()
+                .eq(TaskTemplate::getTeacherId, userId)
+                .orderByDesc(TaskTemplate::getCreatedAt)
+        );
+        return Result.success(templates);
+    }
+
+    /**
+     * 成长规划师创建任务模板
+     */
+    @PostMapping("/teacher/task-templates/create")
+    public Result<Long> createTeacherTaskTemplate(@RequestHeader("X-User-Id") Long userId,
+                                                   @RequestBody TaskTemplate template) {
+        User teacher = userMapper.selectById(userId);
+        if (teacher == null || !"teacher".equals(teacher.getRole())) {
+            return Result.error("非成长规划师账号");
+        }
+
+        template.setTeacherId(userId);
+        template.setCreatedAt(new Date());
+        template.setUpdatedAt(new Date());
+        if (template.getIsActive() == null) {
+            template.setIsActive(1);
+        }
+        taskTemplateMapper.insert(template);
+        return Result.success(template.getId());
+    }
+
+    /**
+     * 成长规划师删除自己的任务模板
+     */
+    @DeleteMapping("/teacher/task-templates/{id}")
+    public Result<Boolean> deleteTeacherTaskTemplate(@RequestHeader("X-User-Id") Long userId,
+                                                     @PathVariable Long id) {
+        User teacher = userMapper.selectById(userId);
+        if (teacher == null || !"teacher".equals(teacher.getRole())) {
+            return Result.error("非成长规划师账号");
+        }
+
+        TaskTemplate template = taskTemplateMapper.selectById(id);
+        if (template == null || !template.getTeacherId().equals(userId)) {
+            return Result.error("模板不存在或无权限删除");
+        }
+
+        taskTemplateMapper.deleteById(id);
+        return Result.success(true);
+    }
+
+    /**
+     * 获取指定家庭的任务列表
+     */
+    @PostMapping("/teacher/families/{familyId}/tasks")
+    public Result<Object> getTeacherFamilyTasks(@RequestHeader("X-User-Id") Long userId,
+                                                @PathVariable Long familyId,
+                                                @RequestBody Map<String, Object> params) {
+        User teacher = userMapper.selectById(userId);
+        if (teacher == null || !"teacher".equals(teacher.getRole())) {
+            return Result.error("非成长规划师账号");
+        }
+
+        // 检查家庭是否在成长规划师关联列表中
+        if (!isTeacherFamily(teacher, familyId)) {
+            return Result.error("无权限查看该家庭的任务");
+        }
+
+        int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
+        int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 10;
+        
+        Page<Task> pageParam = new Page<>(page, size);
+        Page<Task> result = taskMapper.selectPage(pageParam, new LambdaQueryWrapper<Task>()
+            .eq(Task::getFamilyId, familyId)
+            .orderByDesc(Task::getCreatedAt));
+        return Result.success(result);
+    }
+
+    /**
+     * 获取指定家庭的奖励列表
+     */
+    @PostMapping("/teacher/families/{familyId}/rewards")
+    public Result<Object> getTeacherFamilyRewards(@RequestHeader("X-User-Id") Long userId,
+                                                  @PathVariable Long familyId,
+                                                  @RequestBody Map<String, Object> params) {
+        User teacher = userMapper.selectById(userId);
+        if (teacher == null || !"teacher".equals(teacher.getRole())) {
+            return Result.error("非成长规划师账号");
+        }
+
+        // 检查家庭是否在成长规划师关联列表中
+        if (!isTeacherFamily(teacher, familyId)) {
+            return Result.error("无权限查看该家庭的奖励");
+        }
+
+        int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
+        int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 10;
+        
+        Page<Reward> pageParam = new Page<>(page, size);
+        Page<Reward> result = rewardMapper.selectPage(pageParam, new LambdaQueryWrapper<Reward>()
+            .eq(Reward::getFamilyId, familyId)
+            .orderByDesc(Reward::getCreatedAt));
+        return Result.success(result);
+    }
+
+    /**
+     * 检查家庭是否在成长规划师关联列表中
+     */
+    private boolean isTeacherFamily(User teacher, Long familyId) {
+        String familyIds = teacher.getTeacherFamilyIds();
+        if (familyIds == null || familyIds.isEmpty()) {
+            return false;
+        }
+        for (String id : familyIds.split(",")) {
+            try {
+                if (Long.parseLong(id.trim()) == familyId) {
+                    return true;
+                }
+            } catch (Exception e) {
+                // 忽略
+            }
+        }
+        return false;
+    }
+
+    // ========== 溪福俱乐部 - 服务商审核接口 ==========
+
+    @PostMapping("/vendor/list")
+    public Result vendorList(@RequestBody Map<String, Object> params) {
+        Integer page = params.get("page") != null ? Integer.valueOf(params.get("page").toString()) : 1;
+        Integer size = params.get("size") != null ? Integer.valueOf(params.get("size").toString()) : 20;
+        String status = (String) params.get("status");
+        com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<User> wrapper = new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>();
+        wrapper.isNotNull("vendor_type");
+        if (status != null && !status.isEmpty()) {
+            wrapper.eq("vendor_status", status);
+        }
+        wrapper.orderByDesc("updated_at");
+        com.baomidou.mybatisplus.extension.plugins.pagination.Page<User> pageObj = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(page, size);
+        com.baomidou.mybatisplus.core.metadata.IPage<User> result = userMapper.selectPage(pageObj, wrapper);
+        Map<String, Object> data = new HashMap<>();
+        data.put("list", result.getRecords());
+        data.put("total", result.getTotal());
+        data.put("page", page);
+        data.put("size", size);
+        return Result.success(data);
+    }
+
+    @PostMapping("/vendor/review")
+    public Result vendorReview(@RequestBody Map<String, Object> params) {
+        Long userId = Long.valueOf(params.get("userId").toString());
+        String action = (String) params.get("action");
+        String reason = (String) params.getOrDefault("reason", "");
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            return Result.error("用户不存在");
+        }
+        if ("approve".equals(action)) {
+            user.setVendorStatus("approved");
+            user.setVendorRejectReason(null);
+        } else if ("reject".equals(action)) {
+            user.setVendorStatus("rejected");
+            user.setVendorRejectReason(reason);
+        } else {
+            return Result.error("无效的审核操作");
+        }
+        user.setUpdatedAt(new Date());
+        userMapper.updateById(user);
+        return Result.success("审核完成");
+    }
+
+    // ========== 商品/订单管理接口 ==========
+
+    @Resource
+    private com.etotem.cfc.service.ProductService productService;
+
+    @Resource
+    private com.etotem.cfc.service.ProductOrderService productOrderService;
+
+    @PostMapping("/product/pending")
+    public Result<java.util.List<com.etotem.cfc.dto.ProductDTO>> productPending() {
+        return productService.pendingReview();
+    }
+
+    @PostMapping("/product/list")
+    public Result<java.util.List<com.etotem.cfc.dto.ProductDTO>> productList(@RequestBody Map<String, Object> params) {
+        String status = (String) params.get("status");
+        return productService.adminList(status);
+    }
+
+    @PostMapping("/product/review")
+    public Result<String> productReview(@RequestBody Map<String, Object> params) {
+        Long productId = Long.parseLong(params.get("productId").toString());
+        String action = (String) params.get("action");
+        String rejectReason = (String) params.get("rejectReason");
+        return productService.review(productId, action, rejectReason);
+    }
+
+    @PostMapping("/order/list")
+    public Result<java.util.List<com.etotem.cfc.dto.ProductOrderDTO>> orderList(@RequestBody Map<String, Object> params) {
+        String status = (String) params.get("status");
+        return productOrderService.adminList(status);
+    }
+}

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

@@ -0,0 +1,78 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.Guide;
+import com.etotem.cfc.entity.GuideApplication;
+import com.etotem.cfc.entity.GuidePackage;
+import com.etotem.cfc.service.GuideApplicationService;
+import com.etotem.cfc.service.GuidePackageService;
+import com.etotem.cfc.service.GuideService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/admin/guide")
+public class AdminGuideController {
+
+    @Autowired
+    private GuideApplicationService guideApplicationService;
+
+    @Autowired
+    private GuideService guideService;
+
+    @Autowired
+    private GuidePackageService guidePackageService;
+
+    @PostMapping("/applications/pending")
+    public Result<List<GuideApplication>> getPendingApplications() {
+        return Result.success(guideApplicationService.getPendingApplications());
+    }
+
+    @PostMapping("/applications/{id}/approve")
+    public Result<Boolean> approveApplication(@PathVariable Long id, @RequestParam Long adminId) {
+        return Result.success(guideApplicationService.approveApplication(id, adminId));
+    }
+
+    @PostMapping("/applications/{id}/reject")
+    public Result<Boolean> rejectApplication(@PathVariable Long id,
+                                             @RequestParam Long adminId,
+                                             @RequestParam String reason) {
+        return Result.success(guideApplicationService.rejectApplication(id, adminId, reason));
+    }
+
+    @PostMapping("/guides")
+    public Result<List<Guide>> getAllGuides() {
+        return Result.success(guideService.getActiveGuides());
+    }
+
+    @PostMapping("/guides/level/{level}")
+    public Result<List<Guide>> getGuidesByLevel(@PathVariable String level) {
+        return Result.success(guideService.getByLevel(level));
+    }
+
+    @PostMapping("/packages")
+    public Result<List<GuidePackage>> getAllPackages() {
+        return Result.success(guidePackageService.list());
+    }
+
+    @PostMapping("/packages/create")
+    public Result<Boolean> createPackage(@RequestBody GuidePackage pkg) {
+        return Result.success(guidePackageService.save(pkg));
+    }
+
+    @PostMapping("/packages/{id}/update")
+    public Result<Boolean> updatePackage(@PathVariable Long id, @RequestBody GuidePackage pkg) {
+        pkg.setId(id);
+        return Result.success(guidePackageService.updateById(pkg));
+    }
+
+    @PostMapping("/packages/{id}/status")
+    public Result<Boolean> updatePackageStatus(@PathVariable Long id, @RequestParam String status) {
+        GuidePackage pkg = new GuidePackage();
+        pkg.setId(id);
+        pkg.setStatus(status);
+        return Result.success(guidePackageService.updateById(pkg));
+    }
+}

+ 53 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminPackageController.java

@@ -0,0 +1,53 @@
+package com.etotem.cfc.controller.admin;
+import javax.annotation.Resource;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.TaskTemplatePackage;
+import com.etotem.cfc.service.TaskTemplatePackageService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@Tag(name = "管理端-套餐审核", description = "套餐审核管理接口")
+@RestController
+@RequestMapping("/api/admin/packages")
+public class AdminPackageController {
+
+    @Resource
+    private TaskTemplatePackageService packageService;
+
+    /**
+     * 获取待审核的套餐列表
+     */
+    @Operation(summary = "获取待审核套餐列表")
+    @PostMapping("/pending")
+    public Result<List<TaskTemplatePackage>> getPendingPackages() {
+        List<TaskTemplatePackage> packages = packageService.getPendingPackages();
+        return Result.success(packages);
+    }
+
+    /**
+     * 审核通过
+     */
+    @Operation(summary = "审核通过")
+    @PostMapping("/{id}/approve")
+    public Result<Boolean> approvePackage(@PathVariable Long id,
+                                           @RequestAttribute("userId") Long reviewerId) {
+        boolean success = packageService.approvePackage(id, reviewerId);
+        return Result.success(success);
+    }
+
+    /**
+     * 审核拒绝
+     */
+    @Operation(summary = "审核拒绝")
+    @PostMapping("/{id}/reject")
+    public Result<Boolean> rejectPackage(@PathVariable Long id,
+                                          @RequestParam String reason,
+                                          @RequestAttribute("userId") Long reviewerId) {
+        boolean success = packageService.rejectPackage(id, reviewerId, reason);
+        return Result.success(success);
+    }
+}

+ 113 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AssessmentAdminController.java

@@ -0,0 +1,113 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.AssessmentMaterial;
+import com.etotem.cfc.entity.AssessmentPointsConfig;
+import com.etotem.cfc.service.AssessmentService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "测评管理", description = "管理员测评资料与积分配置")
+@RestController
+@RequestMapping("/api/admin/assessment")
+public class AssessmentAdminController {
+
+    @Resource
+    private AssessmentService assessmentService;
+
+    @Operation(summary = "获取测评资料列表")
+    @PostMapping("/materials")
+    public Result<List<AssessmentMaterial>> getMaterials() {
+        List<AssessmentMaterial> list = assessmentService.getMaterialList();
+        return Result.success(list);
+    }
+
+    @Operation(summary = "获取测评资料详情")
+    @PostMapping("/materials/{id}")
+    public Result<AssessmentMaterial> getMaterial(@PathVariable Long id) {
+        AssessmentMaterial material = assessmentService.getById(id);
+        if (material == null) {
+            return Result.error("资料不存在");
+        }
+        return Result.success(material);
+    }
+
+    @Operation(summary = "创建测评资料")
+    @PostMapping("/materials/create")
+    public Result<AssessmentMaterial> createMaterial(@RequestBody Map<String, String> body) {
+        String title = body.get("title");
+        String content = body.get("content");
+        String coverImage = body.get("coverImage");
+
+        if (title == null || title.isEmpty()) {
+            return Result.error("标题不能为空");
+        }
+
+        AssessmentMaterial material = assessmentService.createMaterial(title, content, coverImage);
+        return Result.success(material);
+    }
+
+    @Operation(summary = "更新测评资料")
+    @PostMapping("/materials/{id}/update")
+    public Result<AssessmentMaterial> updateMaterial(@PathVariable Long id, @RequestBody Map<String, String> body) {
+        AssessmentMaterial material = assessmentService.getById(id);
+        if (material == null) {
+            return Result.error("资料不存在");
+        }
+
+        if (body.get("title") != null) {
+            material.setTitle(body.get("title"));
+        }
+        if (body.get("content") != null) {
+            material.setContent(body.get("content"));
+        }
+        if (body.get("coverImage") != null) {
+            material.setCoverImage(body.get("coverImage"));
+        }
+        if (body.get("status") != null) {
+            material.setStatus(body.get("status"));
+        }
+
+        assessmentService.updateById(material);
+        return Result.success(material);
+    }
+
+    @Operation(summary = "删除测评资料")
+    @PostMapping("/materials/{id}/delete")
+    public Result<Boolean> deleteMaterial(@PathVariable Long id) {
+        AssessmentMaterial material = assessmentService.getById(id);
+        if (material == null) {
+            return Result.error("资料不存在");
+        }
+        material.setStatus("inactive");
+        assessmentService.updateById(material);
+        return Result.success(true);
+    }
+
+    @Operation(summary = "获取积分配置")
+    @PostMapping("/config")
+    public Result<AssessmentPointsConfig> getPointsConfig() {
+        AssessmentPointsConfig config = assessmentService.getPointsConfig();
+        return Result.success(config);
+    }
+
+    @Operation(summary = "更新积分配置")
+    @PostMapping("/config/update")
+    public Result<Boolean> updatePointsConfig(@RequestBody Map<String, Object> body) {
+        Integer pointsValue = (Integer) body.get("pointsValue");
+        Double pointsValueYuan = body.get("pointsValueYuan") != null
+            ? Double.valueOf(body.get("pointsValueYuan").toString()) : 0.01;
+
+        if (pointsValue == null || pointsValue <= 0) {
+            return Result.error("积分值必须大于0");
+        }
+
+        boolean success = assessmentService.updatePointsConfig(pointsValue, pointsValueYuan);
+        return Result.success(success);
+    }
+}

+ 32 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/DataMigrationController.java

@@ -0,0 +1,32 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.service.DataMigrationService;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/migration")
+public class DataMigrationController {
+
+    @Resource
+    private DataMigrationService dataMigrationService;
+
+    /**
+     * 从 SFMS 系统迁移数据到当前系统。
+     * 迁移顺序:家庭 → 家长用户 → 孩子用户+详情 → 规划师 → 规划师-家庭关系 → 订单 → 测评结果 → 成长档案 → 成长方案
+     */
+    @PostMapping("/run")
+    public Result<Map<String, Object>> runMigration() {
+        Map<String, Object> result = dataMigrationService.runMigration();
+        if (Boolean.TRUE.equals(result.get("success"))) {
+            return Result.success("迁移完成", result);
+        } else {
+            return Result.success("迁移过程中有部分错误", result);
+        }
+    }
+}

+ 36 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/MigrationController.java

@@ -0,0 +1,36 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+
+@Tag(name = "数据库迁移", description = "数据库结构迁移接口")
+@RestController
+@RequestMapping("/api")
+public class MigrationController {
+
+    @Resource
+    private JdbcTemplate jdbcTemplate;
+
+    @Operation(summary = "迁移guide_packages表结构")
+    @PostMapping("/migrate/guide-packages")
+    public Result<String> migrateGuidePackages() {
+        try {
+            jdbcTemplate.execute("ALTER TABLE guide_packages ADD COLUMN IF NOT EXISTS guide_id BIGINT COMMENT '成长规划师ID'");
+        } catch (Exception ignored) {
+        }
+        try {
+            jdbcTemplate.execute("ALTER TABLE guide_packages ADD COLUMN IF NOT EXISTS package_id BIGINT COMMENT '关联模板'");
+        } catch (Exception ignored) {
+        }
+        try {
+            jdbcTemplate.execute("ALTER TABLE guide_packages ADD COLUMN IF NOT EXISTS commission_rate DECIMAL(5,2) COMMENT '佣金比例'");
+        } catch (Exception ignored) {
+        }
+        return Result.success("迁移完成");
+    }
+}

+ 69 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/OperationLogController.java

@@ -0,0 +1,69 @@
+package com.etotem.cfc.controller.admin;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.UserOperationLog;
+import com.etotem.cfc.service.UserOperationLogService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import javax.annotation.Resource;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "日志管理", description = "操作日志查询接口")
+@RestController
+@RequestMapping("/api/admin/logs")
+public class OperationLogController {
+
+    @Resource
+    private UserOperationLogService logService;
+
+    @Operation(summary = "分页查询操作日志")
+    @PostMapping("/operations")
+    public Result<Page<UserOperationLog>> queryLogs(
+            @RequestAttribute("userId") Long adminId,
+            @RequestBody Map<String, Object> params) {
+        Long userId = params.get("userId") != null ? ((Number) params.get("userId")).longValue() : null;
+        String operationType = (String) params.get("operationType");
+        Integer page = params.get("page") != null ? (Integer) params.get("page") : 1;
+        Integer size = params.get("size") != null ? (Integer) params.get("size") : 20;
+
+        // 解析日期
+        Date startDate = null;
+        Date endDate = null;
+        try {
+            if (params.get("startDate") != null) {
+                java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
+                startDate = sdf.parse((String) params.get("startDate"));
+            }
+            if (params.get("endDate") != null) {
+                java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
+                endDate = sdf.parse((String) params.get("endDate"));
+            }
+        } catch (Exception e) {
+            // 日期解析失败
+        }
+
+        Page<UserOperationLog> result = logService.queryLogs(
+                userId, operationType, startDate, endDate, page, size);
+
+        return Result.success(result);
+    }
+
+    @Operation(summary = "获取用户最近操作记录")
+    @PostMapping("/recent")
+    public Result<List<UserOperationLog>> getRecentLogs(
+            @RequestAttribute("userId") Long adminId,
+            @RequestBody Map<String, Object> params) {
+        Long userId = ((Number) params.get("userId")).longValue();
+        Integer limit = params.get("limit") != null ? (Integer) params.get("limit") : 10;
+
+        List<UserOperationLog> logs = logService.getRecentLogs(userId, limit);
+        return Result.success(logs);
+    }
+}

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

@@ -0,0 +1,68 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.GuidePackageTemplate;
+import com.etotem.cfc.service.GuidePackageTemplateService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+
+@Tag(name = "套餐模板管理", description = "系统套餐模板CRUD")
+@RestController
+@RequestMapping("/api/admin/package-templates")
+public class PackageTemplateController {
+
+    @Resource
+    private GuidePackageTemplateService templateService;
+
+    @PostMapping("/list")
+    @Operation(summary = "获取套餐模板列表")
+    public Result<List<GuidePackageTemplate>> list() {
+        List<GuidePackageTemplate> list = templateService.getActiveTemplates();
+        return Result.success(list);
+    }
+
+    @PostMapping("/all")
+    @Operation(summary = "获取所有套餐模板(含停用)")
+    public Result<List<GuidePackageTemplate>> listAll() {
+        List<GuidePackageTemplate> list = templateService.list();
+        return Result.success(list);
+    }
+
+    @PostMapping("/{id}")
+    @Operation(summary = "获取套餐模板详情")
+    public Result<GuidePackageTemplate> getById(@PathVariable Long id) {
+        GuidePackageTemplate template = templateService.getTemplateById(id);
+        return template != null ? Result.success(template) : Result.error("不存在");
+    }
+
+    @PostMapping("/create")
+    @Operation(summary = "创建套餐模板")
+    public Result<GuidePackageTemplate> create(@RequestBody GuidePackageTemplate template) {
+        template.setStatus("ACTIVE");
+        template.setCreatedAt(new Date());
+        template.setUpdatedAt(new Date());
+        GuidePackageTemplate created = templateService.createTemplate(template);
+        return Result.success(created);
+    }
+
+    @PutMapping("/{id}")
+    @Operation(summary = "更新套餐模板")
+    public Result<GuidePackageTemplate> update(@PathVariable Long id, @RequestBody GuidePackageTemplate template) {
+        template.setId(id);
+        template.setUpdatedAt(new Date());
+        boolean success = templateService.updateTemplate(template);
+        return success ? Result.success(template) : Result.error("更新失败");
+    }
+
+    @DeleteMapping("/{id}")
+    @Operation(summary = "删除套餐模板")
+    public Result<Void> delete(@PathVariable Long id) {
+        boolean success = templateService.deleteTemplate(id);
+        return success ? Result.success(null) : Result.error("删除失败");
+    }
+}

+ 105 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/ServiceContentController.java

@@ -0,0 +1,105 @@
+package com.etotem.cfc.controller.admin;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.ServiceContent;
+import com.etotem.cfc.entity.ServiceType;
+import com.etotem.cfc.service.ServiceContentService;
+import com.etotem.cfc.service.ServiceTypeService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "服务内容管理", description = "系统服务内容CRUD")
+@RestController
+@RequestMapping("/api/admin/service-contents")
+public class ServiceContentController {
+
+    @Resource
+    private ServiceContentService serviceContentService;
+
+    @Resource
+    private ServiceTypeService serviceTypeService;
+
+    @PostMapping("/list")
+    @Operation(summary = "获取服务内容列表")
+    public Result<List<ServiceContent>> list(@RequestBody Map<String, Object> params) {
+        Long typeId = params.get("typeId") != null ? Long.valueOf(params.get("typeId").toString()) : null;
+        String status = params.get("status") != null ? params.get("status").toString() : null;
+
+        LambdaQueryWrapper<ServiceContent> wrapper = new LambdaQueryWrapper<>();
+        if (typeId != null) {
+            wrapper.eq(ServiceContent::getTypeId, typeId);
+        }
+        if (status != null && !status.isEmpty()) {
+            wrapper.eq(ServiceContent::getStatus, status);
+        }
+        wrapper.orderByAsc(ServiceContent::getId);
+        List<ServiceContent> list = serviceContentService.list(wrapper);
+
+        for (ServiceContent content : list) {
+            ServiceType type = serviceTypeService.getById(content.getTypeId());
+            if (type != null) {
+                content.setTypeId(type.getId());
+            }
+        }
+        return Result.success(list);
+    }
+
+    @PostMapping("/active")
+    @Operation(summary = "获取所有启用的服务内容")
+    public Result<List<ServiceContent>> listActive() {
+        List<ServiceContent> list = serviceContentService.getActiveContents();
+        return Result.success(list);
+    }
+
+    @PostMapping("/{id}")
+    @Operation(summary = "获取服务内容详情")
+    public Result<ServiceContent> getById(@RequestBody Map<String, Object> params) {
+        Long id = Long.valueOf(params.get("id").toString());
+        ServiceContent content = serviceContentService.getById(id);
+        return content != null ? Result.success(content) : Result.error("不存在");
+    }
+
+    @PostMapping("/create")
+    @Operation(summary = "创建服务内容")
+    public Result<ServiceContent> create(@RequestBody ServiceContent serviceContent) {
+        serviceContent.setStatus("ACTIVE");
+        serviceContent.setCreatedAt(new Date());
+        serviceContent.setUpdatedAt(new Date());
+        ServiceContent created = serviceContentService.createContent(serviceContent);
+        return Result.success(created);
+    }
+
+    @PostMapping("/update")
+    @Operation(summary = "更新服务内容")
+    public Result<ServiceContent> update(@RequestBody Map<String, Object> params) {
+        Long id = Long.valueOf(params.get("id").toString());
+        ServiceContent serviceContent = new ServiceContent();
+        serviceContent.setId(id);
+        serviceContent.setTitle(params.get("title") != null ? params.get("title").toString() : null);
+        serviceContent.setContent(params.get("content") != null ? params.get("content").toString() : null);
+        serviceContent.setDuration(params.get("duration") != null ? Integer.valueOf(params.get("duration").toString()) : null);
+        serviceContent.setAgeRange(params.get("ageRange") != null ? params.get("ageRange").toString() : null);
+        if (params.get("price") != null) {
+            serviceContent.setPrice(new java.math.BigDecimal(params.get("price").toString()));
+        }
+        serviceContent.setUpdatedAt(new Date());
+        boolean success = serviceContentService.updateContent(serviceContent);
+        return success ? Result.success(serviceContent) : Result.error("更新失败");
+    }
+
+    @PostMapping("/delete")
+    @Operation(summary = "删除服务内容")
+    public Result<Void> delete(@RequestBody Map<String, Object> params) {
+        Long id = Long.valueOf(params.get("id").toString());
+        boolean success = serviceContentService.deleteContent(id);
+        return success ? Result.success(null) : Result.error("删除失败");
+    }
+}

+ 71 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/ServiceTypeController.java

@@ -0,0 +1,71 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.ServiceType;
+import com.etotem.cfc.service.ServiceTypeService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+
+@Tag(name = "服务类型管理", description = "系统服务类型CRUD")
+@RestController
+@RequestMapping("/api/admin/service-types")
+public class ServiceTypeController {
+
+    @Resource
+    private ServiceTypeService serviceTypeService;
+
+    @PostMapping("/list")
+    @Operation(summary = "获取服务类型列表")
+    public Result<List<ServiceType>> list() {
+        List<ServiceType> list = serviceTypeService.getActiveTypes();
+        return Result.success(list);
+    }
+
+    @PostMapping("/all")
+    @Operation(summary = "获取所有服务类型(含停用)")
+    public Result<List<ServiceType>> listAll() {
+        List<ServiceType> list = serviceTypeService.list();
+        return Result.success(list);
+    }
+
+    @PostMapping("/create")
+    @Operation(summary = "创建服务类型")
+    public Result<ServiceType> create(@RequestBody ServiceType serviceType) {
+        serviceType.setStatus("ACTIVE");
+        serviceType.setCreatedAt(new Date());
+        serviceType.setUpdatedAt(new Date());
+        ServiceType created = serviceTypeService.createType(serviceType);
+        return Result.success(created);
+    }
+
+    @PostMapping("/{id}")
+    @Operation(summary = "更新服务类型")
+    public Result<ServiceType> update(@PathVariable Long id, @RequestBody ServiceType serviceType) {
+        serviceType.setId(id);
+        serviceType.setUpdatedAt(new Date());
+        boolean success = serviceTypeService.updateType(serviceType);
+        return success ? Result.success(serviceType) : Result.error("更新失败");
+    }
+
+    @PostMapping("/{id}/delete")
+    @Operation(summary = "删除服务类型")
+    public Result<Void> delete(@PathVariable Long id) {
+        boolean success = serviceTypeService.deleteType(id);
+        return success ? Result.success(null) : Result.error("删除失败");
+    }
+
+    @PostMapping("/{id}/detail")
+    @Operation(summary = "获取服务类型详情")
+    public Result<ServiceType> getById(@PathVariable Long id) {
+        ServiceType serviceType = serviceTypeService.getById(id);
+        if (serviceType == null) {
+            return Result.error("服务类型不存在");
+        }
+        return Result.success(serviceType);
+    }
+}

+ 141 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/assessment/AssessmentAppointmentController.java

@@ -0,0 +1,141 @@
+package com.etotem.cfc.controller.assessment;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.AssessmentAppointment;
+import com.etotem.cfc.entity.AssessmentOrder;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.AssessmentAppointmentService;
+import com.etotem.cfc.service.AssessmentOrderService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Tag(name = "评估预约", description = "评估预约管理")
+@RestController
+@RequestMapping("/api/assessment/appointment")
+public class AssessmentAppointmentController {
+
+    @Resource
+    private AssessmentAppointmentService appointmentService;
+
+    @Resource
+    private AssessmentOrderService orderService;
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Operation(summary = "创建评估预约")
+    @PostMapping("/create")
+    public Result<Map<String, Object>> createAppointment(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> body) {
+
+        Long childId = body.get("childId") != null ? Long.valueOf(body.get("childId").toString()) : null;
+        Long guideId = body.get("guideId") != null ? Long.valueOf(body.get("guideId").toString()) : null;
+        Long packageId = body.get("packageId") != null ? Long.valueOf(body.get("packageId").toString()) : null;
+        String appointmentDate = body.get("appointmentDate") != null ? body.get("appointmentDate").toString() : null;
+        String appointmentTime = body.get("appointmentTime") != null ? body.get("appointmentTime").toString() : null;
+        String notes = body.get("notes") != null ? body.get("notes").toString() : null;
+        Long totalPrice = body.get("totalPrice") != null ? Long.valueOf(body.get("totalPrice").toString()) : 0L;
+        String guideName = body.get("guideName") != null ? body.get("guideName").toString() : null;
+        String packageName = body.get("packageName") != null ? body.get("packageName").toString() : null;
+        Long discountAmount = body.get("discountAmount") != null ? Long.valueOf(body.get("discountAmount").toString()) : 0L;
+
+        User user = userMapper.selectById(userId);
+        if (user == null) return Result.error("用户不存在");
+
+        AssessmentAppointment appointment = appointmentService.createAppointment(
+                userId, childId, guideId, packageId, appointmentDate, appointmentTime, notes);
+
+        Long familyId = user.getFamilyId();
+        AssessmentOrder order = orderService.createOrder(
+                familyId, userId, childId, guideId, packageId,
+                guideName, packageName, totalPrice, discountAmount);
+
+        // 关联预约与订单
+        order.setAppointmentId(appointment.getId());
+        orderService.updateById(order);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("appointmentId", appointment.getId());
+        result.put("orderNo", order.getOrderNo());
+        result.put("actualPrice", order.getActualPrice());
+        result.put("status", order.getStatus());
+
+        return Result.success(result);
+    }
+
+    @Operation(summary = "确认预约")
+    @PostMapping("/confirm/{id}")
+    public Result<Boolean> confirmAppointment(
+            @RequestAttribute("userId") Long userId,
+            @RequestAttribute("role") String role,
+            @PathVariable Long id) {
+        if (!"teacher".equals(role)) {
+            return Result.error("只有成长规划师可以确认预约");
+        }
+        boolean success = appointmentService.confirmAppointment(id, userId);
+        return Result.success(success);
+    }
+
+    @Operation(summary = "取消预约")
+    @PostMapping("/cancel/{id}")
+    public Result<Boolean> cancelAppointment(@PathVariable Long id) {
+        boolean success = appointmentService.cancelAppointment(id);
+        return Result.success(success);
+    }
+
+    @Operation(summary = "我的预约列表(家长视角)")
+    @PostMapping("/my-list")
+    public Result<List<Map<String, Object>>> getMyAppointments(@RequestAttribute("userId") Long userId) {
+        List<AssessmentAppointment> appointments = appointmentService.getByUserId(userId);
+        List<Map<String, Object>> result = appointments.stream().map(this::toMap).collect(Collectors.toList());
+        return Result.success(result);
+    }
+
+    @Operation(summary = "成长规划师的预约列表")
+    @PostMapping("/guide-list")
+    public Result<List<Map<String, Object>>> getGuideAppointments(
+            @RequestAttribute("userId") Long userId,
+            @RequestAttribute("role") String role) {
+        if (!"teacher".equals(role)) {
+            return Result.error("只有成长规划师可以查看此列表");
+        }
+        List<AssessmentAppointment> appointments = appointmentService.getByGuideId(userId);
+        List<Map<String, Object>> result = appointments.stream().map(this::toMap).collect(Collectors.toList());
+        return Result.success(result);
+    }
+
+    private Map<String, Object> toMap(AssessmentAppointment appt) {
+        Map<String, Object> map = new HashMap<>();
+        map.put("id", appt.getId());
+        map.put("childId", appt.getChildId());
+        map.put("guideId", appt.getGuideId());
+        map.put("appointmentDate", appt.getAppointmentDate());
+        map.put("appointmentTime", appt.getAppointmentTime());
+        map.put("status", appt.getStatus());
+        map.put("statusText", getStatusText(appt.getStatus()));
+        map.put("notes", appt.getNotes());
+        map.put("createdAt", appt.getCreatedAt());
+        return map;
+    }
+
+    private String getStatusText(Integer status) {
+        if (status == null) return "未知";
+        switch (status) {
+            case 0: return "待确认";
+            case 1: return "已确认";
+            case 2: return "已完成";
+            case 3: return "已取消";
+            case 4: return "未到";
+            default: return "未知";
+        }
+    }
+}

+ 143 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/assessment/AssessmentOrderController.java

@@ -0,0 +1,143 @@
+package com.etotem.cfc.controller.assessment;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.AssessmentOrder;
+import com.etotem.cfc.entity.AssessmentAppointment;
+import com.etotem.cfc.service.AssessmentOrderService;
+import com.etotem.cfc.service.AssessmentAppointmentService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Tag(name = "评估订单", description = "评估订单与支付")
+@RestController
+@RequestMapping("/api/assessment/order")
+public class AssessmentOrderController {
+
+    @Resource
+    private AssessmentOrderService orderService;
+
+    @Resource
+    private AssessmentAppointmentService appointmentService;
+
+    @Resource
+    private com.etotem.cfc.mapper.FamilyMapper familyMapper;
+
+    @Operation(summary = "创建评估订单")
+    @PostMapping("/create")
+    public Result<Map<String, Object>> createOrder(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> body) {
+        Long familyId = Long.valueOf(body.get("familyId").toString());
+        Long childId = body.get("childId") != null ? Long.valueOf(body.get("childId").toString()) : null;
+        Long guideId = null;
+        if (body.get("guideId") != null) {
+            guideId = Long.valueOf(body.get("guideId").toString());
+        }
+        // 未指定规划师时,尝试使用家庭绑定的规划师
+        if (guideId == null) {
+            com.etotem.cfc.entity.Family family = familyMapper.selectById(familyId);
+            if (family != null && family.getTeacherId() != null) {
+                guideId = family.getTeacherId();
+            }
+        }
+        Long packageId = Long.valueOf(body.get("packageId").toString());
+        String guideName = body.get("guideName") != null ? body.get("guideName").toString() : null;
+        String packageName = body.get("packageName") != null ? body.get("packageName").toString() : null;
+        Long totalPrice = body.get("totalPrice") != null ? Long.valueOf(body.get("totalPrice").toString()) : 0L;
+        Long discountAmount = body.get("discountAmount") != null ? Long.valueOf(body.get("discountAmount").toString()) : 0L;
+
+        AssessmentOrder order = orderService.createOrder(
+                familyId, userId, childId, guideId, packageId,
+                guideName, packageName, totalPrice, discountAmount);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("orderNo", order.getOrderNo());
+        result.put("actualPrice", order.getActualPrice());
+        result.put("status", order.getStatus());
+        return Result.success(result);
+    }
+
+    @Operation(summary = "获取订单详情")
+    @PostMapping("/detail")
+    public Result<AssessmentOrder> getOrderDetail(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> body) {
+        String orderNo = body.get("orderNo") != null ? body.get("orderNo").toString() : null;
+        if (orderNo == null) return Result.error("订单号不能为空");
+        AssessmentOrder order = orderService.getByOrderNo(orderNo);
+        if (order == null) return Result.error("订单不存在");
+        if (!order.getUserId().equals(userId)) return Result.error("无权查看");
+        return Result.success(order);
+    }
+
+    @Operation(summary = "我的订单列表")
+    @PostMapping("/my-orders")
+    public Result<List<AssessmentOrder>> getMyOrders(@RequestAttribute("userId") Long userId) {
+        List<AssessmentOrder> orders = orderService.getByUserId(userId);
+        return Result.success(orders);
+    }
+
+    @Operation(summary = "支付订单")
+    @PostMapping("/pay")
+    public Result<Map<String, Object>> payOrder(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> body) {
+        String orderNo = body.get("orderNo") != null ? body.get("orderNo").toString() : null;
+        String payType = body.get("payType") != null ? body.get("payType").toString() : "wechat";
+        if (orderNo == null) return Result.error("订单号不能为空");
+
+        boolean success = orderService.payOrder(orderNo, payType);
+        if (!success) return Result.error("支付失败");
+
+        // 支付成功后确认预约
+        AssessmentOrder order = orderService.getByOrderNo(orderNo);
+        if (order != null && order.getChildId() != null) {
+            appointmentService.confirmAppointmentByOrder(order);
+        }
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("orderNo", orderNo);
+        result.put("payTime", new Date());
+        result.put("status", "paid");
+        return Result.success(result);
+    }
+
+    @Operation(summary = "取消订单")
+    @PostMapping("/cancel")
+    public Result<Boolean> cancelOrder(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> body) {
+        String orderNo = body.get("orderNo") != null ? body.get("orderNo").toString() : null;
+        if (orderNo == null) return Result.error("订单号不能为空");
+
+        AssessmentOrder order = orderService.getByOrderNo(orderNo);
+        if (order == null) return Result.error("订单不存在");
+        if (!order.getUserId().equals(userId)) return Result.error("无权操作");
+
+        boolean success = orderService.cancelOrder(orderNo);
+        if (!success) return Result.error("取消失败,仅待支付订单可取消");
+        return Result.success(true);
+    }
+
+    @Operation(summary = "退款订单")
+    @PostMapping("/refund")
+    public Result<Boolean> refundOrder(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> body) {
+        String orderNo = body.get("orderNo") != null ? body.get("orderNo").toString() : null;
+        if (orderNo == null) return Result.error("订单号不能为空");
+
+        AssessmentOrder order = orderService.getByOrderNo(orderNo);
+        if (order == null) return Result.error("订单不存在");
+        if (!order.getUserId().equals(userId)) return Result.error("无权操作");
+
+        boolean success = orderService.refundOrder(orderNo);
+        if (!success) return Result.error("退款失败,仅已支付订单可退款");
+        return Result.success(true);
+    }
+}

+ 291 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/auth/AuthController.java

@@ -0,0 +1,291 @@
+package com.etotem.cfc.controller.auth;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.*;
+import com.etotem.cfc.dto.RegisterWithIdCardDTO;
+import com.etotem.cfc.dto.RegisterWithInviteCodeDTO;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.service.UserService;
+import com.etotem.cfc.service.VerificationCodeService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@Tag(name = "用户认证", description = "用户登录、验证码、密码设置等认证接口")
+@RestController
+@RequestMapping("/api/auth")
+public class AuthController {
+
+    @Resource
+    private UserService userService;
+
+    @Resource
+    private VerificationCodeService verificationCodeService;
+
+    /**
+     * 发送验证码
+     */
+    @Operation(summary = "发送验证码")
+    @PostMapping("/send-code")
+    public Result<Boolean> sendCode(@RequestBody SendCodeDTO dto) {
+        if (dto.getPhone() == null || dto.getPhone().isEmpty()) {
+            return Result.error("手机号不能为空");
+        }
+        // 验证手机号格式
+        if (!dto.getPhone().matches("^1[3-9]\\d{9}$")) {
+            return Result.error("手机号格式不正确");
+        }
+        verificationCodeService.generateCode(dto.getPhone(), dto.getType());
+        return Result.success(true);
+    }
+
+    /**
+     * 手机号验证码登录
+     */
+    @Operation(summary = "手机号验证码登录")
+    @PostMapping("/phone-login")
+    public Result<LoginResultDTO> phoneLogin(@RequestBody PhoneLoginDTO dto) {
+        if (dto.getPhone() == null || dto.getCode() == null) {
+            return Result.error("手机号和验证码不能为空");
+        }
+        // 验证验证码
+        boolean valid = verificationCodeService.verifyCode(dto.getPhone(), dto.getCode(), "login");
+        if (!valid) {
+            return Result.error("验证码错误或已过期");
+        }
+        // 执行登录
+        try {
+            LoginResultDTO result = userService.phoneLogin(dto);
+            return Result.success(result);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 微信手机号登录(通过微信开放能力获取手机号)
+     */
+    @Operation(summary = "微信手机号登录")
+    @PostMapping("/wechat-phone-login")
+    public Result<LoginResultDTO> wechatPhoneLogin(@RequestBody WechatPhoneLoginDTO dto) {
+        if (dto.getCode() == null || dto.getPhoneCode() == null) {
+            return Result.error("登录code和手机号code不能为空");
+        }
+        try {
+            LoginResultDTO result = userService.wechatPhoneLogin(dto);
+            return Result.success(result);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 微信静默登录(用户再次打开小程序时自动登录)
+     */
+    @Operation(summary = "微信静默登录")
+    @PostMapping("/silent-login")
+    public Result<LoginResultDTO> silentLogin(@RequestBody WechatLoginDTO dto) {
+        if (dto.getCode() == null) {
+            return Result.error("登录code不能为空");
+        }
+        try {
+            LoginResultDTO result = userService.silentLogin(dto.getCode());
+            return Result.success(result);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 通过openid自动登录
+     */
+    @Operation(summary = "openid自动登录")
+    @PostMapping("/auto-login")
+    public Result<LoginResultDTO> autoLogin(@RequestBody WechatLoginDTO dto) {
+        if (dto.getOpenid() == null) {
+            return Result.error("openid不能为空");
+        }
+        LoginResultDTO result = userService.autoLoginByOpenid(dto.getOpenid());
+        if (result == null) {
+            return Result.error("用户不存在,请先登录");
+        }
+        return Result.success(result);
+    }
+
+    /**
+     * 手机号+身份证注册登录
+     */
+    @Operation(summary = "手机号+身份证注册登录")
+    @PostMapping("/register-with-idcard")
+    public Result<LoginResultDTO> registerWithIdCard(@RequestBody RegisterWithIdCardDTO dto) {
+        if (dto.getPhone() == null || dto.getCode() == null) {
+            return Result.error("手机号和验证码不能为空");
+        }
+        if (dto.getIdCard() == null || dto.getIdCard().isEmpty()) {
+            return Result.error("身份证号不能为空");
+        }
+        if (dto.getRealName() == null || dto.getRealName().isEmpty()) {
+            return Result.error("真实姓名不能为空");
+        }
+        
+        // 验证身份证格式(简单验证,实际应更严格)
+        if (!dto.getIdCard().matches("^\\d{15}$|^\\d{17}[0-9Xx]$")) {
+            return Result.error("身份证号格式不正确");
+        }
+        
+        try {
+            LoginResultDTO result = userService.registerWithIdCard(dto);
+            return Result.success(result);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 微信code登录(原有功能)
+     */
+    @Operation(summary = "微信code登录")
+    @PostMapping("/wechat-login")
+    public Result<LoginResultDTO> wechatLogin(@RequestBody WechatLoginDTO dto) {
+        try {
+            LoginResultDTO result = userService.wechatLogin(dto);
+            return Result.success(result);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    @Operation(summary = "获取当前用户信息")
+    @PostMapping("/info")
+    public Result<User> getInfo(HttpServletRequest request) {
+        Long userId = getUserId(request);
+        if (userId == null) {
+            return Result.error("未登录");
+        }
+        User user = userService.getUserInfo(userId);
+        if (user != null) {
+            user.setPassword(null);
+        }
+        return Result.success(user);
+    }
+
+    @Operation(summary = "设置密码")
+    @PostMapping("/set-password")
+    public Result<Boolean> setPassword(HttpServletRequest request,
+                                           @RequestBody SetPasswordDTO dto) {
+        Long userId = getUserId(request);
+        boolean success = userService.setPassword(userId, dto.getPassword());
+        return Result.success(success);
+    }
+
+    @Operation(summary = "验证密码")
+    @PostMapping("/verify-password")
+    public Result<Boolean> verifyPassword(HttpServletRequest request,
+                                             @RequestBody VerifyPasswordDTO dto) {
+        Long userId = getUserId(request);
+        boolean valid = userService.verifyPassword(userId, dto.getPassword());
+        return Result.success(valid);
+    }
+
+    /**
+     * 检查手机号是否已注册(测试环境用)
+     */
+    @Operation(summary = "检查手机号是否已注册")
+    @PostMapping("/check-phone")
+    public Result<Boolean> checkPhone(@RequestBody CheckPhoneDTO dto) {
+        if (dto.getPhone() == null || dto.getPhone().isEmpty()) {
+            return Result.error("手机号不能为空");
+        }
+        if (!dto.getPhone().matches("^1[3-9]\\d{9}$")) {
+            return Result.error("手机号格式不正确");
+        }
+        boolean registered = userService.isPhoneRegistered(dto.getPhone());
+        return Result.success(registered);
+    }
+
+    /**
+     * 直接注册(测试环境:新用户不通过验证码直接注册)
+     */
+    @Operation(summary = "直接注册用户")
+    @PostMapping("/direct-register")
+    public Result<LoginResultDTO> directRegister(@RequestBody DirectRegisterDTO dto) {
+        if (dto.getPhone() == null || dto.getPhone().isEmpty()) {
+            return Result.error("手机号不能为空");
+        }
+        if (!dto.getPhone().matches("^1[3-9]\\d{9}$")) {
+            return Result.error("手机号格式不正确");
+        }
+        // 检查手机号是否已注册
+        if (userService.isPhoneRegistered(dto.getPhone())) {
+            return Result.error("该手机号已注册,请使用验证码登录");
+        }
+        
+        LoginResultDTO result = userService.directRegister(dto);
+        return Result.success(result);
+    }
+
+    /**
+     * 使用邀请码注册并加入家庭
+     */
+    @Operation(summary = "使用邀请码注册")
+    @PostMapping("/register-with-invite")
+    public Result<LoginResultDTO> registerWithInvite(@RequestBody RegisterWithInviteCodeDTO dto) {
+        if (dto.getPhone() == null || dto.getPhone().isEmpty()) {
+            return Result.error("手机号不能为空");
+        }
+        if (dto.getInviteCode() == null || dto.getInviteCode().isEmpty()) {
+            return Result.error("邀请码不能为空");
+        }
+        if (dto.getCode() == null || dto.getCode().isEmpty()) {
+            return Result.error("验证码不能为空");
+        }
+
+        // 验证验证码
+        if (!verificationCodeService.verifyCode(dto.getPhone(), dto.getCode(), dto.getCodeId())) {
+            return Result.error("验证码错误或已过期");
+        }
+
+        // 检查手机号是否已注册
+        if (userService.isPhoneRegistered(dto.getPhone())) {
+            return Result.error("该手机号已注册,请使用验证码登录");
+        }
+
+        try {
+            LoginResultDTO result = userService.registerWithInviteCode(dto);
+            return Result.success(result);
+        } catch (Exception e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    
+    @PostMapping("/switch-role")
+    public Result<LoginResultDTO> switchRole(HttpServletRequest request, @RequestBody SwitchRoleDTO dto) {
+        Long userId = getUserId(request);
+        if (userId == null) {
+            return Result.error("未登录");
+        }
+        try {
+            LoginResultDTO result = userService.switchRoleForLogin(userId, dto.getRole());
+            return Result.success(result);
+        } catch (Exception e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    private Long getUserId(HttpServletRequest request) {
+        Object userIdObj = request.getAttribute("userId");
+        if (userIdObj != null) {
+            return (Long) userIdObj;
+        }
+        String userIdHeader = request.getHeader("X-User-Id");
+        if (userIdHeader != null) {
+            return Long.parseLong(userIdHeader);
+        }
+        return null;
+    }
+}

+ 38 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/config/SysConfigController.java

@@ -0,0 +1,38 @@
+package com.etotem.cfc.controller.config;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.SysConfigDTO;
+import com.etotem.cfc.service.SysConfigService;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/admin/config")
+public class SysConfigController {
+
+    @Resource
+    private SysConfigService configService;
+
+    @PostMapping("/list")
+    public Result<List<SysConfigDTO>> list() {
+        return configService.listAll();
+    }
+
+    @PostMapping("/update")
+    public Result<String> update(@RequestBody Map<String, Object> params) {
+        String key = (String) params.get("configKey");
+        String value = (String) params.get("configValue");
+        return configService.update(key, value);
+    }
+
+    @PostMapping("/all")
+    public Result<Map<String, String>> getAllAsMap() {
+        return configService.getAllAsMap();
+    }
+}

+ 87 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/family/BindController.java

@@ -0,0 +1,87 @@
+package com.etotem.cfc.controller.family;
+import javax.annotation.Resource;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.BindGuideDTO;
+import com.etotem.cfc.entity.GuideFamily;
+import com.etotem.cfc.service.GuideFamilyService;
+import com.etotem.cfc.service.TeacherMessageService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Tag(name = "家长绑定", description = "家长绑定指导师接口")
+@RestController
+@RequestMapping("/api/bind")
+public class BindController {
+
+    @Resource
+    private GuideFamilyService guideFamilyService;
+
+    @Resource
+    private TeacherMessageService teacherMessageService;
+
+    /**
+     * 家长确认绑定指导师
+     */
+    @Operation(summary = "确认绑定指导师")
+    @PostMapping("/accept")
+    public Result<Boolean> acceptBind(@RequestAttribute("userId") Long userId,
+                                        @RequestAttribute("familyId") Long familyId,
+                                        @RequestBody BindGuideDTO dto) {
+        try {
+            boolean success = guideFamilyService.confirmBind(dto.getGuideId(), familyId,
+                dto.getServiceType(), dto.getServicePrice());
+            return Result.success(success);
+        } catch (RuntimeException e) {
+            return Result.error(400, e.getMessage());
+        }
+    }
+
+    /**
+     * 获取绑定的指导师列表
+     */
+    @Operation(summary = "获取绑定的指导师列表")
+    @PostMapping("/guides")
+    public Result<List<Map<String, Object>>> getBoundGuides(@RequestAttribute("familyId") Long familyId) {
+        List<GuideFamily> bindings = guideFamilyService.getGuidesByFamily(familyId);
+        
+        List<Map<String, Object>> result = bindings.stream().map(binding -> {
+            Map<String, Object> item = new java.util.HashMap<>();
+            item.put("id", binding.getId());
+            item.put("guideId", binding.getGuideId());
+            item.put("serviceType", binding.getServiceType());
+            item.put("servicePrice", binding.getServicePrice());
+            item.put("status", binding.getStatus());
+            item.put("boundAt", binding.getBoundAt());
+            return item;
+        }).collect(Collectors.toList());
+        
+        return Result.success(result);
+    }
+
+    /**
+     * 家长解除绑定规划师(会通知规划师)
+     */
+    @Operation(summary = "解除绑定规划师")
+    @PostMapping("/unbind")
+    public Result<Boolean> unbind(@RequestAttribute("userId") Long userId,
+                                   @RequestAttribute("familyId") Long familyId,
+                                   @RequestBody Map<String, Object> params) {
+        Long id = Long.valueOf(params.get("id").toString());
+        try {
+            GuideFamily gf = guideFamilyService.unbindAndGet(id, familyId);
+            // 发送解绑通知给规划师
+            teacherMessageService.sendMessage(familyId, userId, gf.getGuideId(),
+                "家长已解除与您的绑定关系");
+            return Result.success(true);
+        } catch (RuntimeException e) {
+            return Result.error(400, e.getMessage());
+        }
+    }
+}

+ 42 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/family/BindInviteController.java

@@ -0,0 +1,42 @@
+package com.etotem.cfc.controller.family;
+
+import javax.annotation.Resource;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.service.BindInviteService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+@Tag(name = "指导师绑定邀请", description = "指导师生成邀请、家长确认绑定等接口")
+@RestController
+@RequestMapping("/api/guide/bind")
+public class BindInviteController {
+
+    @Resource
+    private BindInviteService bindInviteService;
+
+    /**
+     * 生成绑定邀请
+     */
+    @Operation(summary = "生成绑定邀请")
+    @PostMapping("/invite")
+    public Result<Map<String, Object>> generateInvite(@RequestAttribute("userId") Long userId) {
+        Map<String, Object> result = bindInviteService.generateInvite(userId);
+        return Result.success(result);
+    }
+
+    /**
+     * 校验邀请令牌
+     */
+    @Operation(summary = "校验邀请令牌")
+    @PostMapping("/validate")
+    public Result<Map<String, Object>> validateToken(@RequestBody Map<String, Object> params) {
+        String token = params.get("token") != null ? params.get("token").toString() : null;
+        Map<String, Object> result = bindInviteService.validateToken(token);
+        return Result.success(result);
+    }
+}

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

@@ -0,0 +1,136 @@
+package com.etotem.cfc.controller.family;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.Family;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.FamilyMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.util.DigestUtils;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * 家庭管理控制器
+ */
+@Tag(name = "家庭管理", description = "家庭相关接口")
+@RestController
+@RequestMapping("/api/family")
+public class FamilyController {
+
+    @Resource
+    private FamilyMapper familyMapper;
+
+    @Resource
+    private UserMapper userMapper;
+
+    /**
+     * 获取当前家庭的邀请码
+     */
+    @Operation(summary = "获取邀请码")
+    @PostMapping("/invite-code")
+    public Result<String> getInviteCode(@RequestHeader("X-User-Id") Long userId) {
+        User user = userMapper.selectById(userId);
+        if (user == null || user.getFamilyId() == null) {
+            return Result.error("用户没有家庭");
+        }
+
+        Family family = familyMapper.selectById(user.getFamilyId());
+        if (family == null) {
+            return Result.error("家庭不存在");
+        }
+
+        // 如果没有邀请码,生成一个
+        if (family.getInviteCode() == null || family.getInviteCode().isEmpty()) {
+            family.setInviteCode(generateInviteCode());
+            family.setUpdatedAt(new Date());
+            familyMapper.updateById(family);
+        }
+
+        return Result.success(family.getInviteCode());
+    }
+
+    /**
+     * 生成/重置邀请码
+     */
+    @Operation(summary = "生成邀请码")
+    @PostMapping("/invite-code/generate")
+    public Result<String> generateInviteCode(@RequestHeader("X-User-Id") Long userId) {
+        User user = userMapper.selectById(userId);
+        if (user == null || user.getFamilyId() == null) {
+            return Result.error("用户没有家庭");
+        }
+
+        Family family = familyMapper.selectById(user.getFamilyId());
+        if (family == null) {
+            return Result.error("家庭不存在");
+        }
+
+        // 生成新的邀请码
+        String newCode = generateInviteCode();
+        family.setInviteCode(newCode);
+        family.setUpdatedAt(new Date());
+        familyMapper.updateById(family);
+
+        return Result.success(newCode);
+    }
+
+    /**
+     * 成长规划师绑定家庭
+     */
+    @Operation(summary = "成长规划师绑定家庭")
+    @PostMapping("/guide-bind")
+    public Result<String> guideBindFamily(@RequestHeader("X-User-Id") Long userId, 
+                                          @RequestBody Map<String, String> params) {
+        String inviteCode = params.get("inviteCode");
+        if (inviteCode == null || inviteCode.isEmpty()) {
+            return Result.error("邀请码不能为空");
+        }
+
+        User guide = userMapper.selectById(userId);
+        if (guide == null || !"teacher".equals(guide.getRole())) {
+            return Result.error("只有成长规划师才能使用此功能");
+        }
+
+        // 查找家庭
+        Family family = familyMapper.selectOne(new LambdaQueryWrapper<Family>()
+                .eq(Family::getInviteCode, inviteCode));
+        
+        if (family == null) {
+            return Result.error("邀请码无效");
+        }
+
+        // 更新家庭的成长规划师ID
+        family.setTeacherId(userId);
+        family.setUpdatedAt(new Date());
+        familyMapper.updateById(family);
+
+        // 更新成长规划师的 teacherFamilyIds
+        String currentIds = guide.getTeacherFamilyIds();
+        String newFamilyId = String.valueOf(family.getId());
+        if (currentIds == null || !currentIds.contains(newFamilyId)) {
+            String updatedIds = currentIds == null ? newFamilyId : currentIds + "," + newFamilyId;
+            guide.setTeacherFamilyIds(updatedIds);
+            guide.setUpdatedAt(new Date());
+            userMapper.updateById(guide);
+        }
+
+        return Result.success("绑定成功");
+    }
+
+    /**
+     * 生成随机邀请码
+     */
+    private String generateInviteCode() {
+        // 生成8位随机邀请码
+        String uuid = UUID.randomUUID().toString().replace("-", "").toUpperCase();
+        return uuid.substring(0, 8);
+    }
+}

+ 252 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyUserController.java

@@ -0,0 +1,252 @@
+package com.etotem.cfc.controller.family;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.*;
+import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.Family;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.FamilyMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.UserService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import javax.annotation.Resource;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "家庭用户管理", description = "家庭用户信息、孩子管理等接口")
+@RestController
+@RequestMapping("/api/family/user")
+public class FamilyUserController {
+
+    @Resource
+    private UserService userService;
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Resource
+    private FamilyMapper familyMapper;
+
+    @Resource
+    private ChildMapper childMapper;
+
+    private Long getUserId(javax.servlet.http.HttpServletRequest request) {
+        // 从请求属性中获取用户ID(由拦截器设置)
+        Object userIdObj = request.getAttribute("userId");
+        if (userIdObj != null) {
+            return (Long) userIdObj;
+        }
+        // 兼容从请求头获取
+        String userIdHeader = request.getHeader("X-User-Id");
+        if (userIdHeader != null) {
+            return Long.parseLong(userIdHeader);
+        }
+        return null;
+    }
+
+    @Operation(summary = "获取用户信息")
+    @PostMapping("/info")
+    public Result<User> getUserInfo(javax.servlet.http.HttpServletRequest request) {
+        Long userId = getUserId(request);
+        User user = userService.getUserInfo(userId);
+        return Result.success(user);
+    }
+
+    @Operation(summary = "更新用户信息")
+    @PostMapping("/update")
+    public Result<Boolean> updateUserInfo(javax.servlet.http.HttpServletRequest request,
+                                          @RequestBody UpdateUserDTO dto) {
+        Long userId = getUserId(request);
+        boolean success = userService.updateUserInfo(userId, dto);
+        return Result.success(success);
+    }
+
+    @Operation(summary = "加入家庭(通过邀请码)")
+    @PostMapping("/join-family")
+    public Result<Boolean> joinFamily(javax.servlet.http.HttpServletRequest request,
+                                      @RequestBody Map<String, String> body) {
+        Long userId = getUserId(request);
+        String inviteCode = body.get("inviteCode");
+        boolean success = userService.joinFamily(userId, inviteCode);
+        return Result.success(success);
+    }
+
+    @Operation(summary = "创建新家庭")
+    @PostMapping("/create-family")
+    public Result<Long> createFamily(javax.servlet.http.HttpServletRequest request,
+                                     @RequestBody Map<String, String> body) {
+        Long userId = getUserId(request);
+        String familyName = body.getOrDefault("name", "我的家庭");
+        Long familyId = userService.createFamilyForUser(userId, familyName);
+        return Result.success(familyId);
+    }
+
+    @Operation(summary = "切换家长/孩子模式")
+    @PostMapping("/switch-mode")
+    public Result<String> switchMode(javax.servlet.http.HttpServletRequest request,
+                                     @RequestBody Map<String, String> body) {
+        Long userId = getUserId(request);
+        String mode = body.get("mode");
+        String result = userService.switchMode(userId, mode);
+        return Result.success(result);
+    }
+
+    @Operation(summary = "切换用户角色 (支持 parent/child/teacher)")
+    @PostMapping("/switch-role")
+    public Result<String> switchRole(javax.servlet.http.HttpServletRequest request,
+                                      @RequestBody Map<String, String> body) {
+        Long userId = getUserId(request);
+        String role = body.get("role");
+        String result = userService.switchRole(userId, role);
+        if (result == null) {
+            return Result.error("切换失败,请确认您拥有该角色");
+        }
+        return Result.success(result);
+    }
+
+    @Operation(summary = "获取用户所有角色列表")
+    @PostMapping("/roles")
+    public Result<List<String>> getUserRoles(javax.servlet.http.HttpServletRequest request) {
+        Long userId = getUserId(request);
+        List<String> roles = userService.getUserRoles(userId);
+        return Result.success(roles);
+    }
+
+    @Operation(summary = "家长切换到指定孩子身份")
+    @PostMapping("/switch-to-child")
+    public Result<Long> switchToChild(javax.servlet.http.HttpServletRequest request,
+                                       @RequestBody Map<String, Long> body) {
+        Long userId = getUserId(request);
+        Long childId = body.get("childId");
+        Long result = userService.switchToChild(userId, childId);
+        return Result.success(result);
+    }
+
+    @Operation(summary = "孩子切换回家长身份")
+    @PostMapping("/switch-back-to-parent")
+    public Result<Boolean> switchBackToParent(javax.servlet.http.HttpServletRequest request) {
+        Long userId = getUserId(request);
+        boolean result = userService.switchBackToParent(userId);
+        return Result.success(result);
+    }
+
+    @Operation(summary = "创建孩子")
+    @PostMapping("/children")
+    public Result<ChildInfoDTO> createChild(javax.servlet.http.HttpServletRequest request,
+                                            @RequestBody CreateChildDTO dto) {
+        Long userId = getUserId(request);
+        ChildInfoDTO child = userService.createChild(userId, dto);
+        return Result.success(child);
+    }
+
+    @Operation(summary = "获取孩子列表")
+    @PostMapping("/children/list")
+    public Result<List<ChildInfoDTO>> getChildren(javax.servlet.http.HttpServletRequest request) {
+        Long userId = getUserId(request);
+        List<ChildInfoDTO> children = userService.getChildren(userId);
+        return Result.success(children);
+    }
+
+    @Operation(summary = "更新孩子信息")
+    @PutMapping("/children/{childId}")
+    public Result<Boolean> updateChild(javax.servlet.http.HttpServletRequest request,
+                                       @PathVariable Long childId,
+                                       @RequestBody UpdateChildDTO dto) {
+        Long userId = getUserId(request);
+        boolean success = userService.updateChild(userId, childId, dto);
+        return Result.success(success);
+    }
+
+    @Operation(summary = "获取家庭成员列表")
+    @PostMapping("/family-members")
+    public Result<Map<String, Object>> getFamilyMembers(javax.servlet.http.HttpServletRequest request) {
+        Long userId = getUserId(request);
+        User user = userMapper.selectById(userId);
+
+        if (user == null) {
+            return Result.error("用户不存在");
+        }
+
+        Long familyId = user.getFamilyId();
+        if (familyId == null) {
+            return Result.error("用户未加入家庭");
+        }
+
+        // 获取家庭信息
+        Family family = familyMapper.selectById(familyId);
+        if (family == null) {
+            return Result.error("家庭不存在");
+        }
+
+        // 查询家长列表
+        List<User> parents = userMapper.selectList(
+            new LambdaQueryWrapper<User>()
+                .eq(User::getFamilyId, familyId)
+                .eq(User::getRole, "parent")
+        );
+
+        // 查询孩子列表
+        List<Child> children = childMapper.selectList(
+            new LambdaQueryWrapper<Child>()
+                .eq(Child::getFamilyId, familyId)
+        );
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("familyId", familyId);
+        result.put("familyName", family.getName());
+        result.put("inviteCode", family.getInviteCode());
+        result.put("creatorId", family.getCreatorId());
+        result.put("parents", parents);
+        result.put("children", children);
+
+        return Result.success(result);
+    }
+
+    @Operation(summary = "踢出家庭成员(仅管理员可操作)")
+    @PostMapping("/kick")
+    public Result<Boolean> kickMember(javax.servlet.http.HttpServletRequest request,
+                                      @RequestBody Map<String, Long> body) {
+        Long userId = getUserId(request);
+        Long targetUserId = body.get("targetUserId");
+        if (targetUserId == null) {
+            return Result.error("缺少目标用户ID");
+        }
+        if (targetUserId.equals(userId)) {
+            return Result.error("不能踢出自己");
+        }
+        User operator = userMapper.selectById(userId);
+        if (operator == null) {
+            return Result.error("用户不存在");
+        }
+        Long familyId = operator.getFamilyId();
+        if (familyId == null) {
+            return Result.error("用户未加入家庭");
+        }
+        Family family = familyMapper.selectById(familyId);
+        if (family == null) {
+            return Result.error("家庭不存在");
+        }
+        if (!Long.valueOf(userId).equals(family.getCreatorId())) {
+            return Result.error("只有家庭管理员才能踢出成员");
+        }
+        User target = userMapper.selectById(targetUserId);
+        if (target == null) {
+            return Result.error("目标用户不存在");
+        }
+        if (!familyId.equals(target.getFamilyId())) {
+            return Result.error("目标用户不属于同一家庭");
+        }
+        // 踢出:清空目标用户的家庭ID
+        target.setFamilyId(null);
+        target.setUpdatedAt(new Date());
+        userMapper.updateById(target);
+        return Result.success(true);
+    }
+}

+ 105 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/family/ParentController.java

@@ -0,0 +1,105 @@
+package com.etotem.cfc.controller.family;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.CreateRewardDTO;
+import com.etotem.cfc.dto.RewardDTO;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.service.RewardService;
+import com.etotem.cfc.service.UserService;
+import javax.annotation.Resource;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/parent")
+public class ParentController {
+
+    @Resource
+    private RewardService rewardService;
+
+    @Resource
+    private UserService userService;
+
+    /**
+     * 家长心愿单 - 创建心愿(直接进入可用列表)
+     */
+    @PostMapping("/wishlist")
+    public Result<Long> createParentWishlist(@RequestAttribute("userId") Long userId,
+                                              @RequestBody CreateRewardDTO dto) {
+        // 家长直接创建奖励,状态为available
+        Long rewardId = rewardService.createReward(userId, dto);
+        return Result.success(rewardId);
+    }
+
+    /**
+     * 家长心愿单 - 获取列表(所有家庭成员的心愿)
+     */
+    @PostMapping("/wishlist/list")
+    public Result<List<RewardDTO>> getParentWishlist(@RequestAttribute("userId") Long userId) {
+        User user = userService.getUserInfo(userId);
+        if (user == null || user.getFamilyId() == null) {
+            return Result.error("用户无家庭信息");
+        }
+        
+        // 获取家庭所有可用的奖励(包括孩子创建的待审批心愿)
+        List<RewardDTO> rewards = rewardService.getFamilyRewardLibrary(user.getFamilyId());
+        return Result.success(rewards);
+    }
+
+    /**
+     * 家长心愿单 - 获取家庭心愿列表(路由兼容)
+     */
+    @PostMapping("/wishlist/family")
+    public Result<List<RewardDTO>> getFamilyWishlist(@RequestAttribute("userId") Long userId) {
+        User user = userService.getUserInfo(userId);
+        if (user == null || user.getFamilyId() == null) {
+            return Result.error("用户无家庭信息");
+        }
+        
+        List<RewardDTO> rewards = rewardService.getFamilyRewardLibrary(user.getFamilyId());
+        return Result.success(rewards);
+    }
+
+    /**
+     * 家长心愿单 - 审批孩子的心愿并填写积分
+     */
+    @PostMapping("/wishlist/{id}/approve")
+    public Result<Boolean> approveWishlist(@PathVariable Long id,
+                                            @RequestAttribute("userId") Long userId,
+                                            @RequestBody Map<String, Object> params) {
+        Integer pointsRequired = params.get("pointsRequired") != null ? (Integer) params.get("pointsRequired") : null;
+        
+        if (pointsRequired == null || pointsRequired <= 0) {
+            return Result.error("请填写有效的积分");
+        }
+        
+        boolean success = rewardService.approveWishlist(id, userId, pointsRequired);
+        return Result.success(success);
+    }
+
+    /**
+     * 家长心愿单 - 兑换家长的心愿
+     */
+    @PostMapping("/wishlist/{id}/exchange")
+    public Result<Map<String, Object>> exchangeParentWishlist(@PathVariable Long id,
+                                                               @RequestAttribute("userId") Long userId) {
+        // 家长兑换自己的心愿,直接扣积分
+        User user = userService.getUserInfo(userId);
+        if (user == null) {
+            return Result.error("用户不存在");
+        }
+        
+        // 家长积分在user表
+        if (user.getTotalPoints() == null) {
+            user.setTotalPoints(0);
+        }
+        
+        // TODO: 实现家长兑换逻辑(暂时返回成功)
+        Map<String, Object> result = new java.util.HashMap<>();
+        result.put("success", true);
+        result.put("newBalance", user.getTotalPoints());
+        return Result.success(result);
+    }
+}

+ 68 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/family/PointsController.java

@@ -0,0 +1,68 @@
+package com.etotem.cfc.controller.family;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.PointsLog;
+import com.etotem.cfc.service.PointsService;
+import javax.annotation.Resource;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/points")
+public class PointsController {
+
+    @Resource
+    private PointsService pointsService;
+
+    @PostMapping("/balance")
+    public Result<Map<String, Object>> getBalance(@RequestBody Map<String, Object> params) {
+        Long childId = params.get("childId") != null ? Long.valueOf(params.get("childId").toString()) : null;
+        Map<String, Object> balance = pointsService.getBalance(childId);
+        return Result.success(balance);
+    }
+
+    @PostMapping("/logs")
+    public Result<Page<PointsLog>> getPointsLogs(@RequestBody Map<String, Object> params) {
+        Long childId = params.get("childId") != null ? Long.valueOf(params.get("childId").toString()) : null;
+        Integer page = params.get("page") != null ? Integer.valueOf(params.get("page").toString()) : 1;
+        Integer size = params.get("size") != null ? Integer.valueOf(params.get("size").toString()) : 10;
+        Page<PointsLog> logs = pointsService.getPointsLogs(childId, page, size);
+        return Result.success(logs);
+    }
+
+    @PostMapping("/adjust")
+    public Result<Map<String, Object>> adjustPoints(@RequestBody Map<String, Object> params,
+                                                    @RequestAttribute("userId") Long userId) {
+        Long childId = Long.valueOf(params.get("childId").toString());
+        Integer amount = Integer.valueOf(params.get("amount").toString());
+        String reason = params.get("reason").toString();
+        String password = params.get("password").toString();
+        Map<String, Object> result = pointsService.adjustPoints(childId, amount, reason, password, userId);
+        return Result.success(result);
+    }
+
+    @PostMapping("/system-balance")
+    public Result<Map<String, Object>> getSystemBalance(@RequestBody Map<String, Object> params) {
+        Long childId = params.get("childId") != null ? Long.valueOf(params.get("childId").toString()) : null;
+        if (childId == null) {
+            return Result.error("childId不能为空");
+        }
+        int balance = pointsService.getSystemPointsBalance(childId);
+        Map<String, Object> result = new HashMap<>();
+        result.put("systemPoints", balance);
+        return Result.success(result);
+    }
+
+    @PostMapping("/logs-by-category")
+    public Result<Page<PointsLog>> getPointsLogsByCategory(@RequestBody Map<String, Object> params) {
+        Long childId = params.get("childId") != null ? Long.valueOf(params.get("childId").toString()) : null;
+        String category = params.get("category") != null ? params.get("category").toString() : null;
+        Integer page = params.get("page") != null ? Integer.valueOf(params.get("page").toString()) : 1;
+        Integer size = params.get("size") != null ? Integer.valueOf(params.get("size").toString()) : 10;
+        Page<PointsLog> logs = pointsService.getPointsLogsByCategory(childId, category, page, size);
+        return Result.success(logs);
+    }
+}

+ 95 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/growth/GrowthPlanController.java

@@ -0,0 +1,95 @@
+package com.etotem.cfc.controller.growth;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.GrowthPlan;
+import com.etotem.cfc.service.GrowthPlanService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "成长方案", description = "成长方案创建、更新、复盘等接口")
+@RestController
+@RequestMapping("/api/growth/plan")
+public class GrowthPlanController {
+
+    @Resource
+    private GrowthPlanService growthPlanService;
+
+    @Operation(summary = "创建成长方案")
+    @PostMapping("/create")
+    public Result<GrowthPlan> createPlan(@RequestBody GrowthPlan plan,
+                                          @RequestAttribute("userId") Long userId) {
+        if (plan.getChildId() == null) {
+            return Result.error("请选择孩子");
+        }
+        plan.setTeacherId(userId);
+        GrowthPlan result = growthPlanService.createPlan(plan);
+        return Result.success(result);
+    }
+
+    @Operation(summary = "获取孩子的成长方案列表")
+    @PostMapping("/child/{childId}")
+    public Result<List<GrowthPlan>> getChildPlans(@PathVariable Long childId) {
+        List<GrowthPlan> plans = growthPlanService.getByChildId(childId);
+        return Result.success(plans);
+    }
+
+    @Operation(summary = "获取孩子当前活跃方案")
+    @PostMapping("/child/{childId}/active")
+    public Result<GrowthPlan> getActivePlan(@PathVariable Long childId) {
+        GrowthPlan plan = growthPlanService.getActivePlanByChildId(childId);
+        if (plan == null) {
+            return Result.error("暂无活跃方案");
+        }
+        return Result.success(plan);
+    }
+
+    @Operation(summary = "获取方案详情")
+    @PostMapping("/{id}")
+    public Result<GrowthPlan> getPlanDetail(@PathVariable Long id) {
+        GrowthPlan plan = growthPlanService.getById(id);
+        if (plan == null) {
+            return Result.error("方案不存在");
+        }
+        return Result.success(plan);
+    }
+
+    @Operation(summary = "更新成长方案")
+    @PostMapping("/update")
+    public Result<Boolean> updatePlan(@RequestBody Map<String, Object> body) {
+        Long id = body.get("id") != null ? Long.valueOf(body.get("id").toString()) : null;
+        if (id == null) return Result.error("方案ID不能为空");
+
+        GrowthPlan plan = growthPlanService.getById(id);
+        if (plan == null) return Result.error("方案不存在");
+
+        if (body.containsKey("planTitle")) plan.setPlanTitle(body.get("planTitle").toString());
+        if (body.containsKey("planContent")) plan.setPlanContent(body.get("planContent").toString());
+        if (body.containsKey("targetDanLevel")) plan.setTargetDanLevel(body.get("targetDanLevel").toString());
+        if (body.containsKey("durationMonths")) plan.setDurationMonths(Integer.valueOf(body.get("durationMonths").toString()));
+        if (body.containsKey("status")) plan.setStatus(body.get("status").toString());
+        if (body.containsKey("startDate") && body.get("startDate") != null) {
+            plan.setStartDate(java.sql.Date.valueOf(body.get("startDate").toString()));
+        }
+        if (body.containsKey("endDate") && body.get("endDate") != null) {
+            plan.setEndDate(java.sql.Date.valueOf(body.get("endDate").toString()));
+        }
+
+        boolean success = growthPlanService.updatePlan(plan);
+        return Result.success(success);
+    }
+
+    @Operation(summary = "复盘成长方案")
+    @PostMapping("/{id}/review")
+    public Result<Boolean> reviewPlan(@PathVariable Long id) {
+        boolean success = growthPlanService.reviewPlan(id);
+        if (!success) {
+            return Result.error("方案不存在");
+        }
+        return Result.success(true);
+    }
+}

+ 167 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/growth/GrowthRecordController.java

@@ -0,0 +1,167 @@
+package com.etotem.cfc.controller.growth;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.Family;
+import com.etotem.cfc.entity.GrowthRecord;
+import com.etotem.cfc.service.AssessmentOrderService;
+import com.etotem.cfc.service.GrowthRecordService;
+import com.etotem.cfc.mapper.FamilyMapper;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "成长档案", description = "成长档案创建、查询、外部同步等接口")
+@RestController
+@RequestMapping("/api/growth")
+public class GrowthRecordController {
+
+    @Resource
+    private GrowthRecordService growthRecordService;
+
+    @Resource
+    private AssessmentOrderService assessmentOrderService;
+
+    @Resource
+    private FamilyMapper familyMapper;
+
+    @Operation(summary = "创建成长档案")
+    @PostMapping("/record/create")
+    public Result<GrowthRecord> createRecord(@RequestBody GrowthRecord record,
+                                              @RequestAttribute("userId") Long userId) {
+        record.setParentId(userId);
+        if (record.getChildId() == null) {
+            return Result.error("请选择孩子");
+        }
+        GrowthRecord result = growthRecordService.createRecord(record);
+        return Result.success(result);
+    }
+
+    @Operation(summary = "创建成长档案并购买测评")
+    @PostMapping("/record/create-with-order")
+    public Result<Map<String, Object>> createRecordWithOrder(
+            @RequestBody Map<String, Object> body,
+            @RequestAttribute("userId") Long userId) {
+        // 解析成长档案字段
+        GrowthRecord record = new GrowthRecord();
+        if (body.get("childId") != null) {
+            record.setChildId(Long.valueOf(body.get("childId").toString()));
+        }
+        if (body.get("school") != null) record.setSchool(body.get("school").toString());
+        if (body.get("grade") != null) record.setGrade(Integer.valueOf(body.get("grade").toString()));
+        if (body.get("academicPerformance") != null) record.setAcademicPerformance(body.get("academicPerformance").toString());
+        if (body.get("schoolProvince") != null) record.setSchoolProvince(body.get("schoolProvince").toString());
+        if (body.get("schoolCity") != null) record.setSchoolCity(body.get("schoolCity").toString());
+        if (body.get("schoolDistrict") != null) record.setSchoolDistrict(body.get("schoolDistrict").toString());
+        if (body.get("schoolStreet") != null) record.setSchoolStreet(body.get("schoolStreet").toString());
+
+        record.setParentId(userId);
+        if (record.getChildId() == null) {
+            return Result.error("请选择孩子");
+        }
+
+        // 创建成长档案
+        GrowthRecord growthResult = growthRecordService.createRecord(record);
+
+        // 检查是否同时购买测评
+        boolean alsoPurchase = body.get("alsoPurchaseAssessment") != null
+                && Boolean.parseBoolean(body.get("alsoPurchaseAssessment").toString());
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("record", growthResult);
+        result.put("order", null);
+
+        if (alsoPurchase) {
+            Long familyId = body.get("familyId") != null
+                    ? Long.valueOf(body.get("familyId").toString()) : null;
+            Long packageId = body.get("packageId") != null
+                    ? Long.valueOf(body.get("packageId").toString()) : null;
+
+            if (familyId == null || packageId == null) {
+                return Result.error("购买测评需要 familyId 和 packageId");
+            }
+
+            // 自动分配规划师
+            Family family = familyMapper.selectById(familyId);
+            Long guideId = (family != null && family.getTeacherId() != null)
+                    ? family.getTeacherId() : null;
+            String guideName = null;
+
+            // 解析价格
+            Long totalPrice = body.get("totalPrice") != null
+                    ? Long.valueOf(body.get("totalPrice").toString()) : 0L;
+            Long discountAmount = body.get("discountAmount") != null
+                    ? Long.valueOf(body.get("discountAmount").toString()) : 0L;
+
+            com.etotem.cfc.entity.AssessmentOrder order = assessmentOrderService.createOrder(
+                    familyId, userId, record.getChildId(), guideId, packageId,
+                    guideName, null, totalPrice, discountAmount);
+
+            Map<String, Object> orderInfo = new HashMap<>();
+            orderInfo.put("orderNo", order.getOrderNo());
+            orderInfo.put("actualPrice", order.getActualPrice());
+            orderInfo.put("status", order.getStatus());
+            result.put("order", orderInfo);
+        }
+
+        return Result.success(result);
+    }
+
+    @Operation(summary = "获取家长的成长档案列表")
+    @PostMapping("/record/list")
+    public Result<List<GrowthRecord>> getRecordList(@RequestAttribute("userId") Long userId) {
+        List<GrowthRecord> records = growthRecordService.getRecordsByParent(userId);
+        return Result.success(records);
+    }
+
+    @Operation(summary = "获取指定孩子的成长档案列表")
+    @PostMapping("/record/child/{childId}")
+    public Result<List<GrowthRecord>> getChildRecords(@PathVariable Long childId) {
+        List<GrowthRecord> records = growthRecordService.getRecordsByChild(childId);
+        return Result.success(records);
+    }
+
+    @Operation(summary = "获取成长档案详情")
+    @PostMapping("/record/{id}")
+    public Result<GrowthRecord> getRecordDetail(@PathVariable Long id) {
+        GrowthRecord record = growthRecordService.getRecordById(id);
+        if (record == null) {
+            return Result.error("档案不存在");
+        }
+        return Result.success(record);
+    }
+
+    @Operation(summary = "删除成长档案")
+    @PostMapping("/record/{id}/delete")
+    public Result<Boolean> deleteRecord(@PathVariable Long id,
+                                         @RequestAttribute("userId") Long userId) {
+        boolean success = growthRecordService.deleteRecord(id, userId);
+        if (!success) {
+            return Result.error("删除失败,档案不存在或无权限");
+        }
+        return Result.success(true);
+    }
+
+    @Operation(summary = "外部系统同步测评结果")
+    @PostMapping("/external/sync")
+    public Result<GrowthRecord> syncExternalResult(@RequestBody Map<String, Object> params) {
+        Long childId = params.get("childId") != null ? Long.valueOf(params.get("childId").toString()) : null;
+        Long parentId = params.get("parentId") != null ? Long.valueOf(params.get("parentId").toString()) : null;
+        Long assessmentId = params.get("assessmentId") != null ? Long.valueOf(params.get("assessmentId").toString()) : null;
+
+        if (childId == null || parentId == null || assessmentId == null) {
+            return Result.error("缺少必要参数: childId, parentId, assessmentId");
+        }
+
+        GrowthRecord record = growthRecordService.syncExternalResult(childId, parentId, assessmentId);
+        if (record == null) {
+            return Result.error("同步失败,测评结果不存在");
+        }
+        return Result.success(record);
+    }
+}

+ 77 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/guide/AssessmentController.java

@@ -0,0 +1,77 @@
+package com.etotem.cfc.controller.guide;
+import javax.annotation.Resource;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.DanAssessmentResult;
+import com.etotem.cfc.service.AssessmentPermissionService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 测评报告控制器
+ */
+@Tag(name = "测评报告", description = "测评报告查询接口(含层级权限控制)")
+@RestController
+@RequestMapping("/api/assessment")
+public class AssessmentController {
+
+    @Resource
+    private AssessmentPermissionService assessmentPermissionService;
+
+    /**
+     * 获取测评报告详情
+     */
+    @Operation(summary = "获取测评报告详情")
+    @PostMapping("/result/{id}")
+    public Result<DanAssessmentResult> getAssessmentResult(@RequestAttribute("userId") Long userId,
+                                                          @RequestAttribute("role") String role,
+                                                          @PathVariable Long id) {
+        // 只有指导师可以查看测评报告
+        if (!"teacher".equals(role)) {
+            return Result.error("只有指导师可以查看测评报告");
+        }
+
+        // 检查权限
+        if (!assessmentPermissionService.canViewAssessment(userId, id)) {
+            return Result.error("无权查看该测评报告");
+        }
+
+        DanAssessmentResult result = assessmentPermissionService.getAssessmentById(id);
+        return Result.success(result);
+    }
+
+    /**
+     * 获取指导师自己的测评报告列表
+     */
+    @Operation(summary = "获取指导师自己的测评报告列表")
+    @PostMapping("/my-results")
+    public Result<List<DanAssessmentResult>> getMyAssessmentResults(@RequestAttribute("userId") Long userId,
+                                                                     @RequestAttribute("role") String role) {
+        // 只有指导师可以查看测评报告
+        if (!"teacher".equals(role)) {
+            return Result.error("只有指导师可以查看测评报告");
+        }
+
+        List<DanAssessmentResult> results = assessmentPermissionService.getOwnAssessments(userId);
+        return Result.success(results);
+    }
+
+    /**
+     * 获取下级指导师的测评报告列表
+     */
+    @Operation(summary = "获取下级指导师的测评报告列表")
+    @PostMapping("/child-results")
+    public Result<List<DanAssessmentResult>> getChildAssessmentResults(@RequestAttribute("userId") Long userId,
+                                                                       @RequestAttribute("role") String role) {
+        // 只有指导师可以查看测评报告
+        if (!"teacher".equals(role)) {
+            return Result.error("只有指导师可以查看测评报告");
+        }
+
+        List<DanAssessmentResult> results = assessmentPermissionService.getAccessibleAssessments(userId);
+        return Result.success(results);
+    }
+}

+ 120 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideAssessmentController.java

@@ -0,0 +1,120 @@
+package com.etotem.cfc.controller.guide;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.AssessmentMaterial;
+import com.etotem.cfc.entity.FamilyAssessmentConfig;
+import com.etotem.cfc.service.AssessmentService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Tag(name = "成长规划师测评", description = "成长规划师管理家庭DAN测评")
+@RestController
+@RequestMapping("/api/guide/assessment")
+public class GuideAssessmentController {
+
+    @Resource
+    private AssessmentService assessmentService;
+
+    @Operation(summary = "获取可用测评资料")
+    @PostMapping("/materials")
+    public Result<List<AssessmentMaterial>> getMaterials() {
+        List<AssessmentMaterial> list = assessmentService.getMaterialList();
+        return Result.success(list);
+    }
+
+    @Operation(summary = "为家庭启用测评")
+    @PostMapping("/family/enable")
+    public Result<Boolean> enableFamilyAssessment(
+            @RequestAttribute("userId") Long teacherId,
+            @RequestBody Map<String, Object> body) {
+
+        Long familyId = Long.valueOf(body.get("familyId").toString());
+        Long materialId = body.get("materialId") != null
+            ? Long.valueOf(body.get("materialId").toString()) : null;
+        Integer remindMonths = body.get("remindMonths") != null
+            ? Integer.valueOf(body.get("remindMonths").toString()) : 12;
+
+        if (familyId == null) {
+            return Result.error("家庭ID不能为空");
+        }
+
+        AssessmentMaterial material = assessmentService.getActiveMaterial();
+        if (material != null && materialId == null) {
+            materialId = material.getId();
+        }
+
+        boolean success = assessmentService.enableFamilyAssessment(familyId, materialId, remindMonths, teacherId);
+        return Result.success(success);
+    }
+
+    @Operation(summary = "关闭家庭测评")
+    @DeleteMapping("/family/{familyId}")
+    public Result<Boolean> disableFamilyAssessment(@PathVariable Long familyId) {
+        boolean success = assessmentService.disableFamilyAssessment(familyId);
+        return Result.success(success);
+    }
+
+    @Operation(summary = "获取所有家庭测评配置")
+    @PostMapping("/families")
+    public Result<List<Map<String, Object>>> getFamilyConfigs() {
+        List<FamilyAssessmentConfig> configs = assessmentService.getFamiliesByTeacher(null);
+
+        List<Map<String, Object>> result = configs.stream().map(config -> {
+            Map<String, Object> map = new HashMap<>();
+            map.put("id", config.getId());
+            map.put("familyId", config.getFamilyId());
+            map.put("materialId", config.getMaterialId());
+            map.put("remindMonths", config.getRemindMonths());
+            map.put("lastRemindAt", config.getLastRemindAt());
+            map.put("nextRemindAt", config.getNextRemindAt());
+            map.put("isEnabled", config.getIsEnabled());
+
+            if (config.getMaterialId() != null) {
+                AssessmentMaterial material = assessmentService.getById(config.getMaterialId());
+                if (material != null) {
+                    map.put("materialTitle", material.getTitle());
+                    map.put("materialContent", material.getContent());
+                }
+            }
+
+            return map;
+        }).collect(Collectors.toList());
+
+        return Result.success(result);
+    }
+
+    @Operation(summary = "获取家庭测评配置")
+    @PostMapping("/family/{familyId}")
+    public Result<Map<String, Object>> getFamilyConfig(@PathVariable Long familyId) {
+        FamilyAssessmentConfig config = assessmentService.getFamilyConfig(familyId);
+        if (config == null) {
+            return Result.success(null);
+        }
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("id", config.getId());
+        result.put("familyId", config.getFamilyId());
+        result.put("materialId", config.getMaterialId());
+        result.put("remindMonths", config.getRemindMonths());
+        result.put("lastRemindAt", config.getLastRemindAt());
+        result.put("nextRemindAt", config.getNextRemindAt());
+        result.put("isEnabled", config.getIsEnabled());
+
+        if (config.getMaterialId() != null) {
+            AssessmentMaterial material = assessmentService.getById(config.getMaterialId());
+            if (material != null) {
+                result.put("materialTitle", material.getTitle());
+                result.put("materialContent", material.getContent());
+            }
+        }
+
+        return Result.success(result);
+    }
+}

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

@@ -0,0 +1,162 @@
+package com.etotem.cfc.controller.guide;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.Guide;
+import com.etotem.cfc.entity.GuideApplication;
+import com.etotem.cfc.entity.GuidePackage;
+import com.etotem.cfc.entity.InviteCard;
+import com.etotem.cfc.service.GuideApplicationService;
+import com.etotem.cfc.service.GuidePackageService;
+import com.etotem.cfc.service.GuideService;
+import com.etotem.cfc.service.InviteCardService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/guide")
+public class GuideController {
+
+    @Resource
+    private GuideApplicationService guideApplicationService;
+
+    @Resource
+    private GuideService guideService;
+
+    @Resource
+    private GuidePackageService guidePackageService;
+
+    @Resource
+    private InviteCardService inviteCardService;
+
+    @PostMapping("/guide-packages")
+    public Result<List<GuidePackage>> getPackages() {
+        return Result.success(guidePackageService.getActivePackages());
+    }
+
+    @PostMapping("/packages/level/{level}")
+    public Result<List<GuidePackage>> getPackagesByLevel(@PathVariable String level) {
+        return Result.success(guidePackageService.getPackagesByLevel(level));
+    }
+
+    @PostMapping("/apply/new")
+    public Result<Boolean> applyNew(HttpServletRequest request,
+                                    @RequestBody Map<String, Object> params) {
+        Long userId = getUserId(request);
+        if (userId == null) {
+            return Result.error("未登录");
+        }
+        String targetLevel = (String) params.get("targetLevel");
+        Long packageId = params.get("packageId") != null ? Long.valueOf(params.get("packageId").toString()) : null;
+        String paymentProof = (String) params.get("paymentProof");
+        return Result.success(guideApplicationService.applyNew(userId, targetLevel, packageId, paymentProof));
+    }
+
+    @PostMapping("/apply/upgrade")
+    public Result<Boolean> applyUpgrade(HttpServletRequest request,
+                                        @RequestBody Map<String, Object> params) {
+        Long userId = getUserId(request);
+        if (userId == null) {
+            return Result.error("未登录");
+        }
+        String fromLevel = (String) params.get("fromLevel");
+        String toLevel = (String) params.get("toLevel");
+        String paymentProof = (String) params.get("paymentProof");
+        return Result.success(guideApplicationService.applyUpgrade(userId, fromLevel, toLevel, paymentProof));
+    }
+
+    @PostMapping("/my")
+    public Result<Guide> getMyGuide(HttpServletRequest request) {
+        Long userId = getUserId(request);
+        if (userId == null) {
+            return Result.error("未登录");
+        }
+        return Result.success(guideService.getByUserId(userId));
+    }
+
+    @PostMapping("/applications/my")
+    public Result<List<GuideApplication>> getMyApplications(HttpServletRequest request) {
+        Long userId = getUserId(request);
+        if (userId == null) {
+            return Result.error("未登录");
+        }
+        LambdaQueryWrapper<GuideApplication> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(GuideApplication::getUserId, userId)
+               .orderByDesc(GuideApplication::getCreatedAt);
+        return Result.success(guideApplicationService.list(wrapper));
+    }
+
+    @PostMapping("/my/packages")
+    public Result<List<GuidePackage>> getMyPackages(HttpServletRequest request) {
+        Long userId = getUserId(request);
+        if (userId == null) {
+            return Result.error("未登录");
+        }
+        Guide guide = guideService.getByUserId(userId);
+        if (guide == null) {
+            return Result.success(java.util.Collections.emptyList());
+        }
+        String level = guide.getLevel();
+        if (level == null) {
+            return Result.success(java.util.Collections.emptyList());
+        }
+        return Result.success(guidePackageService.getPackagesByLevel(level));
+    }
+
+    @PostMapping("/package/templates")
+    public Result<List<GuidePackage>> getPackageTemplates() {
+        return Result.success(guidePackageService.getActivePackages());
+    }
+
+    @PostMapping("/parent-invite/generate")
+    public Result<Map<String, Object>> generateParentInvite(HttpServletRequest request,
+                                                             @RequestBody Map<String, Object> params) {
+        Long userId = getUserId(request);
+        if (userId == null) {
+            return Result.error("未登录");
+        }
+        Long familyId = params.get("familyId") != null ? Long.valueOf(params.get("familyId").toString()) : null;
+        if (familyId == null) {
+            return Result.error("家庭ID不能为空");
+        }
+        InviteCard card = inviteCardService.generate("parent_invite", familyId, userId);
+        Map<String, Object> data = new java.util.HashMap<>();
+        data.put("id", card.getId());
+        data.put("code", card.getCode());
+        data.put("type", card.getType());
+        data.put("refId", card.getRefId());
+        data.put("expiresAt", card.getExpiresAt());
+        return Result.success(data);
+    }
+
+    @PostMapping("/parent-invite/accept")
+    public Result<Map<String, Object>> acceptParentInvite(HttpServletRequest request,
+                                                           @RequestBody Map<String, Object> params) {
+        Long userId = getUserId(request);
+        if (userId == null) {
+            return Result.error("未登录");
+        }
+        String code = (String) params.get("code");
+        if (code == null || code.isEmpty()) {
+            return Result.error("邀请码不能为空");
+        }
+        return inviteCardService.acceptParentInvite(code, userId);
+    }
+
+    private Long getUserId(HttpServletRequest request) {
+        Object userIdObj = request.getAttribute("userId");
+        if (userIdObj != null) {
+            return (Long) userIdObj;
+        }
+        String userIdHeader = request.getHeader("X-User-Id");
+        if (userIdHeader != null) {
+            return Long.parseLong(userIdHeader);
+        }
+        return null;
+    }
+}

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

@@ -0,0 +1,437 @@
+package com.etotem.cfc.controller.guide;
+import javax.annotation.Resource;
+
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.TaskReviewDTO;
+import com.etotem.cfc.entity.Child;
+import com.etotem.cfc.entity.Task;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.TaskMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.TaskService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Tag(name = "指导师-家庭任务管理", description = "指导师查看家庭任务、审核任务等接口")
+@RestController
+@RequestMapping("/api/guide/families")
+public class GuideFamilyTaskController {
+
+    @Resource
+    private TaskService taskService;
+
+    @Resource
+    private ChildMapper childMapper;
+
+    @Resource
+    private TaskMapper taskMapper;
+
+    @Resource
+    private UserMapper userMapper;
+
+    /**
+     * 获取指导师关联的家庭列表(带简要任务统计)
+     */
+    @Operation(summary = "获取关联家庭列表")
+    @ApiResponses({
+        @ApiResponse(responseCode = "200", description = "成功返回家庭列表"),
+        @ApiResponse(responseCode = "403", description = "Access denied - 非教师"),
+        @ApiResponse(responseCode = "401", description = "未登录")
+    })
+    @PostMapping("/bound-families")
+    public Result<List<Map<String, Object>>> getBoundFamilies(@RequestAttribute("userId") Long guideId, @RequestAttribute("role") String role) {
+        if (!"teacher".equals(role)) { return Result.error("Access denied"); }
+
+        // 获取当前指导师信息
+        User guide = userMapper.selectById(guideId);
+        if (guide == null || guide.getTeacherFamilyIds() == null || guide.getTeacherFamilyIds().isEmpty()) {
+            return Result.success(new ArrayList<>());
+        }
+
+        // 解析家庭ID列表(逗号分隔)
+        List<Long> familyIds = Arrays.stream(guide.getTeacherFamilyIds().split(","))
+                .map(String::trim)
+                .filter(s -> !s.isEmpty())
+                .map(Long::parseLong)
+                .collect(Collectors.toList());
+
+        if (familyIds.isEmpty()) {
+            return Result.success(new ArrayList<>());
+        }
+
+        List<Map<String, Object>> result = new ArrayList<>();
+
+        for (Long familyId : familyIds) {
+            // 获取家庭的家长用户
+            List<User> parents = userMapper.selectList(
+                new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<User>()
+                    .eq("family_id", familyId)
+                    .eq("role", "parent")
+            );
+
+            if (parents.isEmpty()) {
+                continue;
+            }
+
+            User parent = parents.get(0);
+            Map<String, Object> item = new HashMap<>();
+            item.put("familyId", familyId);
+            item.put("familyName", "家庭" + familyId);
+            item.put("parentName", parent.getNickname());
+
+            // 获取家庭孩子列表
+            List<Child> children = childMapper.selectList(
+                new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<Child>()
+                    .eq("family_id", familyId)
+            );
+            item.put("childrenCount", children.size());
+
+            // 获取待审核任务数量
+            long pendingCount = 0;
+            for (Child child : children) {
+                List<Task> pendingTasks = taskService.getPendingReviewTasksForChild(child.getId());
+                pendingCount += pendingTasks.size();
+            }
+            item.put("pendingReviewCount", pendingCount);
+
+            result.add(item);
+        }
+
+        return Result.success(result);
+    }
+
+    /**
+     * 获取指定家庭的所有孩子任务概览
+     */
+    @Operation(summary = "获取家庭任务概览")
+    @ApiResponses({
+        @ApiResponse(responseCode = "200", description = "成功返回概览"),
+        @ApiResponse(responseCode = "403", description = "Access denied"),
+        @ApiResponse(responseCode = "401", description = "未登录")
+    })
+    @PostMapping("/{familyId}/overview")
+    public Result<Map<String, Object>> getFamilyTaskOverview(
+            @RequestAttribute("userId") Long guideId,
+            @RequestAttribute("role") String role,
+            @PathVariable Long familyId) {
+        if (!"teacher".equals(role)) { return Result.error("Access denied"); }
+
+        List<Child> children = childMapper.selectList(
+            new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<Child>()
+                .eq("family_id", familyId)
+        );
+
+        List<Map<String, Object>> childTaskList = new ArrayList<>();
+        int totalPending = 0;
+        int totalCompleted = 0;
+        int totalOverdue = 0;
+
+        for (Child child : children) {
+            Map<String, Object> childInfo = new HashMap<>();
+            childInfo.put("childId", child.getId());
+            childInfo.put("childName", child.getNickname());
+            childInfo.put("totalPoints", child.getTotalPoints());
+            childInfo.put("streakDays", child.getStreakDays());
+
+            // 获取今日任务
+            List<Task> todayTasks = taskService.getTodayTasks(child.getId());
+            childInfo.put("todayTaskCount", todayTasks.size());
+            childInfo.put("todayCompletedCount", todayTasks.stream()
+                .filter(t -> "completed".equals(t.getStatus()))
+                .count());
+
+            // 获取待审核任务
+            List<Task> pendingTasks = taskService.getPendingReviewTasksForChild(child.getId());
+            childInfo.put("pendingReviewCount", pendingTasks.size());
+            totalPending += pendingTasks.size();
+
+            // 统计已完成和超时
+            List<Task> allTasks = taskService.getAllTasksByChild(child.getId());
+            totalCompleted += allTasks.stream()
+                .filter(t -> "completed".equals(t.getStatus()))
+                .count();
+            totalOverdue += allTasks.stream()
+                .filter(t -> "overdue".equals(t.getStatus()))
+                .count();
+
+            childTaskList.add(childInfo);
+        }
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("familyId", familyId);
+        result.put("children", childTaskList);
+        result.put("totalChildren", children.size());
+        result.put("totalPendingReview", totalPending);
+        result.put("totalCompleted", totalCompleted);
+        result.put("totalOverdue", totalOverdue);
+
+        return Result.success(result);
+    }
+
+    /**
+     * 获取家庭孩子的任务列表
+     */
+    @Operation(summary = "获取孩子任务列表")
+    @ApiResponses({
+        @ApiResponse(responseCode = "200", description = "成功返回分页任务"),
+        @ApiResponse(responseCode = "403", description = "Access denied"),
+        @ApiResponse(responseCode = "401", description = "未登录")
+    })
+    @PostMapping("/{familyId}/children/{childId}/tasks")
+    public Result<Page<Task>> getChildTasks(
+            @PathVariable Long familyId,
+            @PathVariable Long childId,
+            @RequestAttribute("role") String role,
+            @RequestBody(required = false) Map<String, Object> params) {
+        if (!"teacher".equals(role)) { return Result.error("Access denied"); }
+
+        Integer page = params != null && params.get("page") != null ? Integer.valueOf(params.get("page").toString()) : 1;
+        Integer size = params != null && params.get("size") != null ? Integer.valueOf(params.get("size").toString()) : 10;
+        String status = params != null && params.get("status") != null ? params.get("status").toString() : null;
+
+        Page<Task> tasks = taskService.getTaskHistoryByChild(childId, page, size, status);
+        return Result.success(tasks);
+    }
+
+    /**
+     * 获取家庭孩子的待审核任务
+     */
+    @Operation(summary = "获取待审核任务列表")
+    @ApiResponses({
+        @ApiResponse(responseCode = "200", description = "成功返回待审核任务"),
+        @ApiResponse(responseCode = "403", description = "Access denied"),
+        @ApiResponse(responseCode = "401", description = "未登录")
+    })
+    @PostMapping("/{familyId}/children/{childId}/pending-review")
+    public Result<List<Task>> getPendingReviewTasks(
+            @PathVariable Long familyId,
+            @PathVariable Long childId,
+            @RequestAttribute("role") String role) {
+
+        if (!"teacher".equals(role)) { return Result.error("Access denied"); }
+        List<Task> tasks = taskService.getPendingReviewTasksForChild(childId);
+        return Result.success(tasks);
+    }
+
+    /**
+     * 审核任务(指导师)
+     */
+    @Operation(summary = "审核任务")
+    @ApiResponses({
+        @ApiResponse(responseCode = "200", description = "审核成功返回布尔值"),
+        @ApiResponse(responseCode = "403", description = "Access denied"),
+        @ApiResponse(responseCode = "401", description = "未登录")
+    })
+    @PostMapping("/{familyId}/tasks/{taskId}/review")
+    public Result<Boolean> reviewTask(
+            @PathVariable Long familyId,
+            @PathVariable Long taskId,
+            @RequestAttribute("role") String role,
+            @RequestBody TaskReviewDTO dto) {
+
+        if (!"teacher".equals(role)) { return Result.error("Access denied"); }
+        boolean success = taskService.reviewTask(taskId, dto);
+        return Result.success(success);
+    }
+
+    /**
+     * 批量审核任务
+     */
+    @Operation(summary = "批量审核任务")
+    @ApiResponses({
+        @ApiResponse(responseCode = "200", description = "批量审核成功返回统计"),
+        @ApiResponse(responseCode = "403", description = "Access denied"),
+        @ApiResponse(responseCode = "401", description = "未登录")
+    })
+    @PostMapping("/{familyId}/tasks/batch-review")
+    public Result<Map<String, Object>> batchReviewTasks(
+            @PathVariable Long familyId,
+            @RequestAttribute("role") String role,
+            @RequestBody Map<String, Object> params) {
+
+        @SuppressWarnings("unchecked")
+        List<Object> rawTaskIds = (List<Object>) params.get("taskIds");
+        List<Long> taskIds = new ArrayList<>();
+        if (rawTaskIds != null) {
+            for (Object taskId : rawTaskIds) {
+                taskIds.add(Long.valueOf(taskId.toString()));
+            }
+        }
+        Boolean approved = params.get("approved") != null ? Boolean.valueOf(params.get("approved").toString()) : Boolean.FALSE;
+
+
+        if (!"teacher".equals(role)) { return Result.error("Access denied"); }
+        int successCount = 0;
+        int failCount = 0;
+
+        for (Long taskId : taskIds) {
+            try {
+                TaskReviewDTO dto = new TaskReviewDTO();
+                dto.setApproved(approved);
+                dto.setComment(approved ? "指导师审核通过" : "指导师审核拒绝");
+                if (taskService.reviewTask(taskId, dto)) {
+                    successCount++;
+                } else {
+                    failCount++;
+                }
+            } catch (Exception e) {
+                failCount++;
+            }
+        }
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("successCount", successCount);
+        result.put("failCount", failCount);
+        return Result.success(result);
+    }
+
+    /**
+     * 指导师修改任务
+     */
+    @Operation(summary = "修改任务")
+    @ApiResponses({
+        @ApiResponse(responseCode = "200", description = "修改成功"),
+        @ApiResponse(responseCode = "403", description = "Access denied"),
+        @ApiResponse(responseCode = "404", description = "任务不存在")
+    })
+    @PutMapping("/{familyId}/tasks/{taskId}")
+    public Result<Boolean> updateTask(
+            @PathVariable Long familyId,
+            @PathVariable Long taskId,
+            @RequestAttribute("role") String role,
+            @RequestBody Map<String, Object> taskData) {
+        
+        if (!"teacher".equals(role)) { return Result.error("Access denied"); }
+        
+        Task task = taskMapper.selectById(taskId);
+        if (task == null) {
+            return Result.error("任务不存在");
+        }
+        
+        // 验证任务属于该家庭
+        if (!familyId.equals(task.getFamilyId())) {
+            return Result.error("任务不属于该家庭");
+        }
+        
+        // 更新任务字段
+        if (taskData.containsKey("title")) {
+            task.setTitle((String) taskData.get("title"));
+        }
+        if (taskData.containsKey("description")) {
+            task.setDescription((String) taskData.get("description"));
+        }
+        if (taskData.containsKey("points")) {
+            task.setPoints((Integer) taskData.get("points"));
+        }
+        if (taskData.containsKey("deadline")) {
+            task.setDeadline(new java.util.Date((Long) taskData.get("deadline")));
+        }
+        if (taskData.containsKey("status")) {
+            task.setStatus((String) taskData.get("status"));
+        }
+        
+        task.setUpdatedAt(new java.util.Date());
+        taskMapper.updateById(task);
+        
+        return Result.success(true);
+    }
+
+    /**
+     * 指导师删除任务
+     */
+    @Operation(summary = "删除任务")
+    @ApiResponses({
+        @ApiResponse(responseCode = "200", description = "删除成功"),
+        @ApiResponse(responseCode = "403", description = "Access denied"),
+        @ApiResponse(responseCode = "404", description = "任务不存在")
+    })
+    @DeleteMapping("/{familyId}/tasks/{taskId}")
+    public Result<Boolean> deleteTask(
+            @PathVariable Long familyId,
+            @PathVariable Long taskId,
+            @RequestAttribute("role") String role) {
+        
+        if (!"teacher".equals(role)) { return Result.error("Access denied"); }
+        
+        Task task = taskMapper.selectById(taskId);
+        if (task == null) {
+            return Result.error("任务不存在");
+        }
+        
+        // 验证任务属于该家庭
+        if (!familyId.equals(task.getFamilyId())) {
+            return Result.error("任务不属于该家庭");
+        }
+        
+        // 指导师可以删除任何任务(不只是创建者)
+        task.setStatus("cancelled");
+        task.setUpdatedAt(new java.util.Date());
+        taskMapper.updateById(task);
+        
+        return Result.success(true);
+    }
+
+    /**
+     * 获取规划师首页统计(实际数据)
+     */
+    @Operation(summary = "获取规划师首页统计")
+    @PostMapping("/dashboard-stats")
+    public Result<Map<String, Object>> getDashboardStats(@RequestAttribute("userId") Long guideId, @RequestAttribute("role") String role) {
+        if (!"teacher".equals(role)) { return Result.error("Access denied"); }
+
+        User guide = userMapper.selectById(guideId);
+        long totalFamilies = 0;
+        long todayCompleted = 0;
+        long pendingReview = 0;
+
+        if (guide != null && guide.getTeacherFamilyIds() != null && !guide.getTeacherFamilyIds().isEmpty()) {
+            List<Long> familyIds = Arrays.stream(guide.getTeacherFamilyIds().split(","))
+                    .map(String::trim).filter(s -> !s.isEmpty()).map(Long::parseLong)
+                    .collect(Collectors.toList());
+            totalFamilies = familyIds.size();
+
+            // 今天0点
+            Date todayStart = Date.from(LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant());
+
+            for (Long familyId : familyIds) {
+                List<Child> children = childMapper.selectList(
+                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Child>()
+                        .eq(Child::getFamilyId, familyId));
+                for (Child child : children) {
+                    // 今日完成任务:status=completed 且 completedAt >= todayStart
+                    long completed = taskMapper.selectCount(
+                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Task>()
+                            .eq(Task::getChildId, child.getId())
+                            .eq(Task::getStatus, "completed")
+                            .ge(Task::getCompletedAt, todayStart));
+                    todayCompleted += completed;
+
+                    // 待审核任务
+                    long pending = taskMapper.selectCount(
+                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Task>()
+                            .eq(Task::getChildId, child.getId())
+                            .eq(Task::getNeedReview, 1)
+                            .eq(Task::getStatus, "completed"));
+                    pendingReview += pending;
+                }
+            }
+        }
+
+        Map<String, Object> stats = new HashMap<>();
+        stats.put("totalFamilies", totalFamilies);
+        stats.put("todayCompletedTasks", todayCompleted);
+        stats.put("pendingReviewTasks", pendingReview);
+        return Result.success(stats);
+    }
+}

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

@@ -0,0 +1,319 @@
+package com.etotem.cfc.controller.guide;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.GuideRegisterDTO;
+import com.etotem.cfc.dto.PackagePurchaseDTO;
+import com.etotem.cfc.entity.GuideApplication;
+import com.etotem.cfc.entity.GuideApplicationRecord;
+import com.etotem.cfc.entity.GuideInviteCode;
+import com.etotem.cfc.entity.GuidePackage;
+import com.etotem.cfc.entity.GuideHierarchy;
+import com.etotem.cfc.entity.PackageOrder;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.GuideApplicationService;
+import com.etotem.cfc.service.GuideHierarchyService;
+import com.etotem.cfc.service.GuideInviteCodeService;
+import com.etotem.cfc.service.GuidePackageService;
+import com.etotem.cfc.service.UserService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 成长规划师管理控制器
+ */
+@Tag(name = "成长规划师管理", description = "成长规划师注册、邀请码、层级关系等接口")
+@RestController
+@RequestMapping("/api/guide")
+public class GuideManagementController {
+
+    @Resource
+    private GuideInviteCodeService guideInviteCodeService;
+
+    @Resource
+    private GuideHierarchyService guideHierarchyService;
+
+    @Resource
+    private GuidePackageService guidePackageService;
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Resource
+    private UserService userService;
+
+    @Resource
+    private GuideApplicationService guideApplicationService;
+
+    /**
+     * 成长规划师注册(保存草稿)
+     */
+    @Operation(summary = "成长规划师注册")
+    @PostMapping("/register")
+    public Result<Boolean> registerGuide(@RequestBody GuideRegisterDTO dto) {
+        // 检查用户是否存在
+        User user = userMapper.selectById(dto.getUserId());
+        if (user == null) {
+            return Result.error("用户不存在");
+        }
+
+        // 更新用户信息
+        user.setTeacherNo(dto.getTeacherNo());
+        user.setTeacherPhoto(dto.getTeacherPhoto());
+        user.setCertificateImage(dto.getCertificateImage());
+        user.setRealName(dto.getRealName());
+        user.setPhone(dto.getPhone());
+        user.setIdCard(dto.getIdCard());
+        user.setAddress(dto.getAddress());
+        user.setTeacherStatus("pending");
+        user.setUpdatedAt(new java.util.Date());
+        userMapper.updateById(user);
+
+        // 创建申请记录
+        guideApplicationService.createDraft(
+            dto.getUserId(), "new_apply", "junior", null,
+            dto.getRealName(), dto.getPhone(), dto.getIdCard(),
+            dto.getTeacherNo(), dto.getTeacherPhoto(), dto.getCertificateImage(),
+            dto.getAddress(), dto.getBankCard(), dto.getBankName()
+        );
+
+        // 将teacher角色添加到用户的角色列表中
+        userService.addRole(user.getId(), "teacher");
+
+        return Result.success(true);
+    }
+
+    /**
+     * 提交申请(draft -> pending)
+     */
+    @Operation(summary = "提交成长规划师申请")
+    @PostMapping("/application/submit")
+    public Result<Boolean> submitApplication(@RequestAttribute("userId") Long userId) {
+        // 查找用户的草稿申请
+        List<GuideApplication> apps = guideApplicationService.getUserApplications(userId);
+        GuideApplication draft = null;
+        for (GuideApplication app : apps) {
+            if ("draft".equals(app.getStatus())) {
+                draft = app;
+                break;
+            }
+        }
+        if (draft == null) {
+            return Result.error("未找到草稿申请");
+        }
+        boolean success = guideApplicationService.submitApplication(draft.getId(), userId);
+        return success ? Result.success(true) : Result.error("提交失败");
+    }
+
+    /**
+     * 撤回申请
+     */
+    @Operation(summary = "撤回成长规划师申请")
+    @PostMapping("/application/withdraw")
+    public Result<Boolean> withdrawApplication(@RequestAttribute("userId") Long userId) {
+        List<GuideApplication> apps = guideApplicationService.getUserApplications(userId);
+        GuideApplication pending = null;
+        for (GuideApplication app : apps) {
+            if ("pending".equals(app.getStatus())) {
+                pending = app;
+                break;
+            }
+        }
+        if (pending == null) {
+            return Result.error("未找到待审核的申请");
+        }
+        boolean success = guideApplicationService.withdrawApplication(pending.getId(), userId);
+        return success ? Result.success(true) : Result.error("撤回失败");
+    }
+
+    /**
+     * 重新提交申请
+     */
+    @Operation(summary = "重新提交成长规划师申请")
+    @PostMapping("/application/resubmit")
+    public Result<Boolean> resubmitApplication(@RequestAttribute("userId") Long userId) {
+        List<GuideApplication> apps = guideApplicationService.getUserApplications(userId);
+        GuideApplication target = null;
+        for (GuideApplication app : apps) {
+            if ("withdrawn".equals(app.getStatus()) || "rejected".equals(app.getStatus())) {
+                target = app;
+                break;
+            }
+        }
+        if (target == null) {
+            return Result.error("未找到可重新提交的申请");
+        }
+        boolean success = guideApplicationService.resubmitApplication(target.getId(), userId);
+        return success ? Result.success(true) : Result.error("重新提交失败");
+    }
+
+    /**
+     * 获取用户申请列表
+     */
+    @Operation(summary = "获取用户申请列表")
+    @PostMapping("/applications")
+    public Result<List<GuideApplication>> getUserApplications(@RequestAttribute("userId") Long userId) {
+        return Result.success(guideApplicationService.getUserApplications(userId));
+    }
+
+    /**
+     * 获取申请处理记录
+     */
+    @Operation(summary = "获取申请处理记录")
+    @PostMapping("/application/{id}/records")
+    public Result<List<GuideApplicationRecord>> getApplicationRecords(@PathVariable Long id) {
+        return Result.success(guideApplicationService.getApplicationRecords(id));
+    }
+
+    /**
+     * 如果有邀请码,验证并建立层级关系
+     */
+    private void processInviteCode(String inviteCode, Long userId) {
+        if (inviteCode != null && !inviteCode.isEmpty()) {
+            GuideInviteCode code = guideInviteCodeService.validateInviteCode(inviteCode);
+            if (code != null) {
+                guideHierarchyService.addChildGuide(code.getGuideId(), userId);
+                guideInviteCodeService.useInviteCode(inviteCode, userId);
+            }
+        }
+    }
+
+    /**
+     * 生成邀请码
+     */
+    @Operation(summary = "生成邀请码")
+    @PostMapping("/invite-code/generate")
+    public Result<GuideInviteCode> generateInviteCode(@RequestAttribute("userId") Long userId,
+                                                       @RequestParam(defaultValue = "1") Integer maxUseCount,
+                                                       @RequestParam(defaultValue = "30") Integer expireDays) {
+        GuideInviteCode inviteCode = guideInviteCodeService.generateInviteCode(userId, maxUseCount, expireDays);
+        return Result.success(inviteCode);
+    }
+
+    /**
+     * 获取邀请码列表
+     */
+    @Operation(summary = "获取邀请码列表")
+    @PostMapping("/invite-codes")
+    public Result<List<GuideInviteCode>> getInviteCodes(@RequestAttribute("userId") Long userId) {
+        List<GuideInviteCode> inviteCodes = guideInviteCodeService.getGuideInviteCodes(userId);
+        return Result.success(inviteCodes);
+    }
+
+    /**
+     * 验证邀请码
+     */
+    @Operation(summary = "验证邀请码")
+    @PostMapping("/invite-code/validate")
+    public Result<Map<String, Object>> validateInviteCode(@RequestParam String code) {
+        GuideInviteCode inviteCode = guideInviteCodeService.validateInviteCode(code);
+
+        if (inviteCode == null) {
+            return Result.error("邀请码无效或已过期");
+        }
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("guideId", inviteCode.getGuideId());
+        result.put("code", inviteCode.getCode());
+        result.put("maxUseCount", inviteCode.getMaxUseCount());
+        result.put("useCount", inviteCode.getUseCount());
+        result.put("expireAt", inviteCode.getExpireAt());
+
+        User guide = userMapper.selectById(inviteCode.getGuideId());
+        if (guide != null) {
+            result.put("guideName", guide.getNickname());
+            result.put("guideAvatar", guide.getAvatar());
+        }
+
+        return Result.success(result);
+    }
+
+    /**
+     * 获取下级成长规划师列表
+     */
+    @Operation(summary = "获取下级成长规划师列表")
+    @PostMapping("/child-guides")
+    public Result<List<User>> getChildGuides(@RequestAttribute("userId") Long userId) {
+        List<User> childGuides = guideHierarchyService.getChildGuides(userId);
+        return Result.success(childGuides);
+    }
+
+    /**
+     * 获取所有下级成长规划师(包括间接下级)
+     */
+    @Operation(summary = "获取所有下级成长规划师")
+    @PostMapping("/all-child-guides")
+    public Result<List<User>> getAllChildGuides(@RequestAttribute("userId") Long userId) {
+        List<User> childGuides = guideHierarchyService.getAllChildGuides(userId);
+        return Result.success(childGuides);
+    }
+
+    /**
+     * 获取上级成长规划师
+     */
+    @Operation(summary = "获取上级成长规划师")
+    @PostMapping("/parent-guide")
+    public Result<User> getParentGuide(@RequestAttribute("userId") Long userId) {
+        User parentGuide = guideHierarchyService.getParentGuide(userId);
+        return Result.success(parentGuide);
+    }
+
+    /**
+     * 添加成长规划师套餐
+     */
+    @Operation(summary = "添加成长规划师套餐")
+    @PostMapping("/package/add")
+    public Result<Boolean> addGuidePackage(@RequestAttribute("userId") Long userId,
+                                          @RequestBody Map<String, Object> params) {
+        Long packageId = Long.valueOf(params.get("packageId").toString());
+        java.math.BigDecimal price = new java.math.BigDecimal(params.get("price").toString());
+        java.math.BigDecimal commissionRate = params.containsKey("commissionRate")
+            ? new java.math.BigDecimal(params.get("commissionRate").toString())
+            : new java.math.BigDecimal("0.1");
+
+        boolean success = guidePackageService.addGuidePackage(userId, packageId, price, commissionRate);
+        return Result.success(success);
+    }
+
+    /**
+     * 获取成长规划师对特定套餐的定价
+     */
+    @Operation(summary = "获取成长规划师套餐定价")
+    @PostMapping("/package/{packageId}")
+    public Result<GuidePackage> getGuidePackage(@RequestAttribute("userId") Long userId,
+                                               @PathVariable Long packageId) {
+        GuidePackage guidePackage = guidePackageService.getGuidePackage(userId, packageId);
+        return Result.success(guidePackage);
+    }
+
+    /**
+     * 更新成长规划师套餐价格
+     */
+    @Operation(summary = "更新成长规划师套餐价格")
+    @PutMapping("/package/{packageId}")
+    public Result<Boolean> updateGuidePackage(@RequestAttribute("userId") Long userId,
+                                               @PathVariable Long packageId,
+                                               @RequestBody Map<String, Object> params) {
+        java.math.BigDecimal price = new java.math.BigDecimal(params.get("price").toString());
+        boolean success = guidePackageService.updateGuidePackagePrice(userId, packageId, price);
+        return Result.success(success);
+    }
+
+    /**
+     * 删除成长规划师套餐
+     */
+    @Operation(summary = "删除成长规划师套餐")
+    @DeleteMapping("/package/{packageId}")
+    public Result<Boolean> deleteGuidePackage(@RequestAttribute("userId") Long userId,
+                                               @PathVariable Long packageId) {
+        boolean success = guidePackageService.deleteGuidePackage(userId, packageId);
+        return Result.success(success);
+    }
+}

+ 96 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideOrderController.java

@@ -0,0 +1,96 @@
+package com.etotem.cfc.controller.guide;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.PackageOrder;
+import com.etotem.cfc.mapper.PackageOrderMapper;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+
+@Tag(name = "成长规划师订单", description = "成长规划师订单与佣金接口")
+@RestController
+@RequestMapping("/api/guide/orders")
+public class GuideOrderController {
+
+    @Resource
+    private PackageOrderMapper packageOrderMapper;
+
+    @PostMapping("/list")
+    @Operation(summary = "获取订单列表")
+    public Result<Map<String, Object>> getOrders(@RequestAttribute("userId") Long guideId,
+                                                   @RequestBody(required = false) Map<String, Object> params) {
+        Integer page = (params != null && params.get("page") != null) ? Integer.valueOf(params.get("page").toString()) : 1;
+        Integer size = (params != null && params.get("size") != null) ? Integer.valueOf(params.get("size").toString()) : 10;
+        String status = (params != null && params.get("status") != null) ? params.get("status").toString() : null;
+
+        LambdaQueryWrapper<PackageOrder> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(PackageOrder::getGuideId, guideId);
+        if (status != null && !status.isEmpty()) {
+            wrapper.eq(PackageOrder::getStatus, status);
+        }
+        wrapper.orderByDesc(PackageOrder::getCreatedAt);
+
+        Page<PackageOrder> pagination = new Page<>(page, size);
+        Page<PackageOrder> result = packageOrderMapper.selectPage(pagination, wrapper);
+
+        Map<String, Object> response = new HashMap<>();
+        response.put("records", result.getRecords());
+        response.put("total", result.getTotal());
+        response.put("pages", result.getPages());
+        response.put("current", result.getCurrent());
+
+        return Result.success(response);
+    }
+
+    @PostMapping("/stats")
+    @Operation(summary = "获取订单统计")
+    public Result<Map<String, Object>> getOrderStats(@RequestAttribute("userId") Long guideId) {
+        LambdaQueryWrapper<PackageOrder> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(PackageOrder::getGuideId, guideId);
+
+        Long totalOrders = packageOrderMapper.selectCount(wrapper);
+        Long paidOrders = packageOrderMapper.selectCount(new LambdaQueryWrapper<PackageOrder>()
+                .eq(PackageOrder::getGuideId, guideId)
+                .eq(PackageOrder::getStatus, "paid"));
+
+        BigDecimal totalRevenue = packageOrderMapper.selectList(new LambdaQueryWrapper<PackageOrder>()
+                .eq(PackageOrder::getGuideId, guideId)
+                .eq(PackageOrder::getStatus, "paid"))
+                .stream()
+                .map(PackageOrder::getPrice)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+        Map<String, Object> stats = new HashMap<>();
+        stats.put("totalOrders", totalOrders);
+        stats.put("paidOrders", paidOrders);
+        stats.put("totalRevenue", totalRevenue);
+        stats.put("pendingOrders", totalOrders - paidOrders);
+
+        return Result.success(stats);
+    }
+
+    @PostMapping("/detail")
+    @Operation(summary = "获取订单详情")
+    public Result<PackageOrder> getOrderDetail(@RequestAttribute("userId") Long guideId,
+                                                  @RequestBody Map<String, Object> params) {
+        if (params == null || params.get("id") == null) {
+            return Result.error("缺少订单ID");
+        }
+        Long id = Long.valueOf(params.get("id").toString());
+        PackageOrder order = packageOrderMapper.selectById(id);
+        if (order == null) {
+            return Result.error("订单不存在");
+        }
+        if (!order.getGuideId().equals(guideId)) {
+            return Result.error("无权查看");
+        }
+        return Result.success(order);
+    }
+}

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

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

+ 108 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideRecordController.java

@@ -0,0 +1,108 @@
+package com.etotem.cfc.controller.guide;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.AssessmentPointsConfig;
+import com.etotem.cfc.entity.DanAssessmentResult;
+import com.etotem.cfc.service.AssessmentPermissionService;
+import com.etotem.cfc.service.AssessmentService;
+import com.etotem.cfc.service.PointsService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+
+@Tag(name = "成长规划师记录", description = "成长规划师录入测评结果(含孩子快照)")
+@RestController
+@RequestMapping("/api/guide/record")
+public class GuideRecordController {
+
+    private static final Logger log = LoggerFactory.getLogger(GuideRecordController.class);
+
+    @Resource
+    private AssessmentService assessmentService;
+
+    @Resource
+    private AssessmentPermissionService assessmentPermissionService;
+
+    @Resource
+    private PointsService pointsService;
+
+    @Operation(summary = "录入测评结果(含孩子快照)")
+    @PostMapping("/create")
+    public Result<DanAssessmentResult> createRecord(
+            @RequestAttribute("userId") Long userId,
+            @RequestAttribute("role") String role,
+            @RequestBody DanAssessmentResult result) {
+        if (!"teacher".equals(role)) {
+            return Result.error("只有成长规划师可以录入测评结果");
+        }
+        result.setTeacherId(userId);
+        result.setStatus("completed");
+        result.setCreatedAt(new Date());
+        result.setUpdatedAt(new Date());
+        if (result.getAssessmentDate() == null) {
+            result.setAssessmentDate(new Date());
+        }
+        DanAssessmentResult saved = assessmentService.recordResult(result);
+        // 发放系统积分:完成测评建档奖励
+        if (saved != null && saved.getChildId() != null) {
+            try {
+                AssessmentPointsConfig config = assessmentService.getPointsConfig();
+                int points = (config != null && config.getPointsValue() != null)
+                    ? config.getPointsValue() : 50;
+                pointsService.awardSystemPoints(saved.getChildId(), points,
+                    "完成测评建档奖励");
+            } catch (Exception e) {
+                log.error("发放系统积分失败", e);
+                // 不阻断主流程
+            }
+        }
+        return Result.success(saved);
+    }
+
+    @Operation(summary = "查询我的测评记录列表")
+    @PostMapping("/my-records")
+    public Result<List<DanAssessmentResult>> getMyRecords(@RequestAttribute("userId") Long userId) {
+        List<DanAssessmentResult> results = assessmentService.getResultsByGuide(userId);
+        return Result.success(results);
+    }
+
+    @Operation(summary = "获取测评记录详情")
+    @PostMapping("/detail/{id}")
+    public Result<DanAssessmentResult> getRecordDetail(
+            @RequestAttribute("userId") Long userId,
+            @RequestAttribute("role") String role,
+            @PathVariable Long id) {
+        if (!assessmentPermissionService.canViewAssessment(userId, id)) {
+            return Result.error("无权查看该记录");
+        }
+        DanAssessmentResult danResult = assessmentPermissionService.getAssessmentById(id);
+        if (danResult == null) {
+            return Result.error("记录不存在");
+        }
+        DanAssessmentResult result = new DanAssessmentResult();
+        result.setId(danResult.getId());
+        result.setChildId(danResult.getChildId());
+        result.setTeacherId(danResult.getTeacherId());
+        result.setAppointmentId(danResult.getAppointmentId());
+        result.setDanLevel(danResult.getDanLevel());
+        result.setAttentionScore(danResult.getAttentionScore());
+        result.setFocusScore(danResult.getFocusScore());
+        result.setMemoryScore(danResult.getMemoryScore());
+        result.setLogicScore(danResult.getLogicScore());
+        result.setOverallScore(danResult.getOverallScore());
+        result.setAssessmentDate(danResult.getAssessmentDate());
+        result.setDurationMinutes(danResult.getDurationMinutes());
+        result.setAnalysisReport(danResult.getAnalysisReport());
+        result.setGrowthSuggestions(danResult.getGrowthSuggestions());
+        result.setNextReviewDate(danResult.getNextReviewDate());
+        result.setStatus(danResult.getStatus());
+        result.setCreatedAt(danResult.getCreatedAt());
+        return Result.success(result);
+    }
+}

+ 98 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideTeamController.java

@@ -0,0 +1,98 @@
+package com.etotem.cfc.controller.guide;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.GuideInviteCode;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.GuideInviteCodeService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.*;
+
+@Tag(name = "成长规划师团队", description = "成长规划师团队管理接口")
+@RestController
+@RequestMapping("/api/guide/team")
+public class GuideTeamController {
+
+    @Resource
+    private GuideInviteCodeService guideInviteCodeService;
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Operation(summary = "获取我的团队")
+    @PostMapping("/my-team")
+    public Result<Map<String, Object>> getMyTeam(@RequestAttribute("userId") Long guideId) {
+        GuideInviteCode inviteCode = guideInviteCodeService.getByGuideId(guideId);
+
+        List<User> teamMembers = new ArrayList<>();
+        if (inviteCode != null) {
+            List<User> users = userMapper.selectList(new LambdaQueryWrapper<User>()
+                    .like(User::getTeacherNo, inviteCode.getCode()));
+            teamMembers = users;
+        }
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("inviteCode", inviteCode);
+        result.put("members", teamMembers);
+        result.put("memberCount", teamMembers.size());
+
+        return Result.success(result);
+    }
+
+    @Operation(summary = "获取我的邀请码")
+    @PostMapping("/invite-code")
+    public Result<GuideInviteCode> getMyInviteCode(@RequestAttribute("userId") Long guideId) {
+        GuideInviteCode inviteCode = guideInviteCodeService.getByGuideId(guideId);
+        if (inviteCode == null) {
+            inviteCode = guideInviteCodeService.createInviteCode(guideId);
+        }
+        return Result.success(inviteCode);
+    }
+
+    @Operation(summary = "生成新的邀请码")
+    @PostMapping("/invite-code/generate")
+    public Result<GuideInviteCode> generateInviteCode(@RequestAttribute("userId") Long guideId) {
+        GuideInviteCode inviteCode = guideInviteCodeService.createInviteCode(guideId);
+        return Result.success(inviteCode);
+    }
+
+    @Operation(summary = "获取团队成员列表")
+    @PostMapping("/members")
+    public Result<List<User>> getTeamMembers(@RequestAttribute("userId") Long guideId) {
+        GuideInviteCode inviteCode = guideInviteCodeService.getByGuideId(guideId);
+        if (inviteCode == null) {
+            return Result.success(new ArrayList<>());
+        }
+        List<User> members = userMapper.selectList(new LambdaQueryWrapper<User>()
+                .like(User::getTeacherNo, inviteCode.getCode()));
+        return Result.success(members);
+    }
+
+    @Operation(summary = "获取团队统计")
+    @PostMapping("/stats")
+    public Result<Map<String, Object>> getTeamStats(@RequestAttribute("userId") Long guideId) {
+        GuideInviteCode inviteCode = guideInviteCodeService.getByGuideId(guideId);
+        Map<String, Object> stats = new HashMap<>();
+        
+        if (inviteCode == null) {
+            stats.put("totalMembers", 0);
+            stats.put("totalFamilies", 0);
+            stats.put("totalChildren", 0);
+            return Result.success(stats);
+        }
+        
+        List<User> members = userMapper.selectList(new LambdaQueryWrapper<User>()
+                .like(User::getTeacherNo, inviteCode.getCode()));
+        
+        stats.put("totalMembers", members.size());
+        stats.put("totalFamilies", 0);
+        stats.put("totalChildren", 0);
+        
+        return Result.success(stats);
+    }
+}

+ 82 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/guide/PackagePurchaseController.java

@@ -0,0 +1,82 @@
+package com.etotem.cfc.controller.guide;
+import javax.annotation.Resource;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.PackagePurchaseDTO;
+import com.etotem.cfc.entity.PackageOrder;
+import com.etotem.cfc.service.PackagePurchaseService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 套餐购买控制器
+ */
+@Tag(name = "套餐购买", description = "套餐购买、价格查询等接口")
+@RestController
+@RequestMapping("/api/package")
+public class PackagePurchaseController {
+
+    @Resource
+    private PackagePurchaseService packagePurchaseService;
+
+    /**
+     * 获取套餐价格
+     */
+    @Operation(summary = "获取套餐价格")
+    @PostMapping("/price")
+    public Result<Map<String, Object>> getPackagePrice(@RequestParam Long packageId,
+                                                        @RequestParam(required = false) Long guideId,
+                                                        @RequestParam(required = false) String inviteCode) {
+        Map<String, Object> priceInfo = packagePurchaseService.getPackagePrice(packageId, guideId, inviteCode);
+        
+        if (priceInfo == null) {
+            return Result.error("套餐不存在");
+        }
+        
+        return Result.success(priceInfo);
+    }
+
+    /**
+     * 获取可用的指导师列表
+     */
+    @Operation(summary = "获取可用的指导师列表")
+    @PostMapping("/available-guides")
+    public Result<List<Map<String, Object>>> getAvailableGuides(@RequestParam Long packageId) {
+        List<Map<String, Object>> guides = packagePurchaseService.getAvailableGuides(packageId);
+        return Result.success(guides);
+    }
+
+    /**
+     * 创建套餐订单
+     */
+    @Operation(summary = "创建套餐订单")
+    @PostMapping("/order/create")
+    public Result<PackageOrder> createOrder(@RequestBody PackagePurchaseDTO dto) {
+        PackageOrder order = packagePurchaseService.createOrder(dto);
+        
+        if (order == null) {
+            return Result.error("创建订单失败");
+        }
+        
+        return Result.success(order);
+    }
+
+    /**
+     * 获取订单详情
+     */
+    @Operation(summary = "获取订单详情")
+    @PostMapping("/order/{orderNo}")
+    public Result<PackageOrder> getOrder(@PathVariable String orderNo) {
+        PackageOrder order = packagePurchaseService.getOrder(orderNo);
+        
+        if (order == null) {
+            return Result.error("订单不存在");
+        }
+        
+        return Result.success(order);
+    }
+}

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

@@ -0,0 +1,94 @@
+package com.etotem.cfc.controller.guide;
+
+import javax.annotation.Resource;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.PackageOrder;
+import com.etotem.cfc.service.PaymentService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+/**
+ * 支付控制器
+ */
+@Tag(name = "支付管理", description = "支付相关接口")
+@RestController
+@RequestMapping("/api/payment")
+public class PaymentController {
+
+    @Resource
+    private PaymentService paymentService;
+
+    /**
+     * 创建微信支付订单
+     */
+    @Operation(summary = "创建微信支付订单")
+    @PostMapping("/wechat/create")
+    public Result<Map<String, Object>> createWechatPayOrder(@RequestBody Map<String, Object> params) {
+        String orderNo = (String) params.get("orderNo");
+        String description = (String) params.get("description");
+        Integer amount = (Integer) params.get("amount");
+        
+        Map<String, Object> result = paymentService.createWechatPayOrder(orderNo, description, amount);
+        return Result.success(result);
+    }
+
+    /**
+     * 创建支付宝订单
+     */
+    @Operation(summary = "创建支付宝订单")
+    @PostMapping("/alipay/create")
+    public Result<Map<String, Object>> createAlipayOrder(@RequestBody Map<String, Object> params) {
+        String orderNo = (String) params.get("orderNo");
+        String description = (String) params.get("description");
+        Integer amount = (Integer) params.get("amount");
+        
+        Map<String, Object> result = paymentService.createAlipayOrder(orderNo, description, amount);
+        return Result.success(result);
+    }
+
+    /**
+     * 处理微信支付回调
+     */
+    @Operation(summary = "处理微信支付回调")
+    @PostMapping("/wechat/callback")
+    public Result<Boolean> handleWechatCallback(@RequestBody Map<String, Object> params) {
+        String orderNo = (String) params.get("orderNo");
+        String transactionId = (String) params.get("transactionId");
+        
+        boolean success = paymentService.handlePaymentCallback(orderNo, transactionId, "wechat");
+        return Result.success(success);
+    }
+
+    /**
+     * 处理支付宝回调
+     */
+    @Operation(summary = "处理支付宝回调")
+    @PostMapping("/alipay/callback")
+    public Result<Boolean> handleAlipayCallback(@RequestBody Map<String, Object> params) {
+        String orderNo = (String) params.get("orderNo");
+        String transactionId = (String) params.get("transactionId");
+        
+        boolean success = paymentService.handlePaymentCallback(orderNo, transactionId, "alipay");
+        return Result.success(success);
+    }
+
+    /**
+     * 查询订单状态
+     */
+    @Operation(summary = "查询订单状态")
+    @PostMapping("/order/status/{orderNo}")
+    public Result<PackageOrder> getOrderStatus(@PathVariable String orderNo) {
+        PackageOrder order = paymentService.getOrderStatus(orderNo);
+        
+        if (order == null) {
+            return Result.error("订单不存在");
+        }
+        
+        return Result.success(order);
+    }
+}

+ 152 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/guide/TeacherController.java

@@ -0,0 +1,152 @@
+package com.etotem.cfc.controller.guide;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.*;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.service.TeacherService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import javax.annotation.Resource;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 教师控制器
+ * 处理教师相关的业务操作
+ */
+@Tag(name = "教师管理", description = "教师任务创建、班级管理、学生进度查看等接口")
+@RestController
+@RequestMapping("/api/teacher")
+public class TeacherController {
+
+    @Resource
+    private TeacherService teacherService;
+
+  /**
+   * 获取教师信息
+   */
+    @Operation(summary = "获取教师信息")
+    @PostMapping("/info")
+    public Result<TeacherDTO> getTeacherInfo(@RequestAttribute("userId") Long teacherId) {
+    TeacherDTO info = teacherService.getTeacherInfo(teacherId);
+    return Result.success(info);
+  }
+
+  /**
+   * 创建教学任务
+   */
+    @Operation(summary = "创建教学任务")
+    @PostMapping("/tasks")
+    public Result<Long> createTask(@RequestAttribute("userId") Long teacherId,
+      @RequestBody CreateTeacherTaskDTO dto) {
+    // TODO: Implement createTask in TeacherService
+    // Long taskId = teacherService.createTask(teacherId, dto);
+    // return Result.success(taskId);
+    return Result.error("功能开发中");
+  }
+
+  /**
+   * 获取教师任务列表
+   */
+    @Operation(summary = "获取教师任务列表")
+    @PostMapping("/tasks/list")
+    public Result<List<TaskDTO>> getTaskList(@RequestAttribute("userId") Long teacherId) {
+    // TODO: Implement getTaskList in TeacherService
+    // List<TaskDTO> tasks = teacherService.getTaskList(teacherId);
+    // return Result.success(tasks);
+    return Result.success(null);
+  }
+
+  /**
+   * 创建班级
+   */
+    @Operation(summary = "创建班级")
+    @PostMapping("/classes")
+    public Result<Long> createClass(@RequestAttribute("userId") Long teacherId,
+      @RequestBody CreateClassDTO dto) {
+    // TODO: Implement createClass in TeacherService
+    // Long classId = teacherService.createClass(teacherId, dto);
+    // return Result.success(classId);
+    return Result.error("功能开发中");
+  }
+
+  /**
+   * 获取教师班级列表
+   */
+    @Operation(summary = "获取教师班级列表")
+    @PostMapping("/classes/list")
+    public Result<List<ClassInfoDTO>> getClassList(@RequestAttribute("userId") Long teacherId) {
+    // TODO: Implement getClassList in TeacherService
+    // List<ClassInfoDTO> classes = teacherService.getClassList(teacherId);
+    // return Result.success(classes);
+    return Result.success(null);
+  }
+
+  /**
+   * 获取班级学生列表
+   */
+    @Operation(summary = "获取班级学生列表")
+    @PostMapping("/classes/{classId}/students")
+    public Result<List<StudentInfoDTO>> getClassStudents(@RequestAttribute("userId") Long teacherId,
+      @PathVariable Long classId) {
+    // TODO: Implement getClassStudents in TeacherService
+    // List<StudentInfoDTO> students = teacherService.getClassStudents(teacherId, classId);
+    // return Result.success(students);
+    return Result.success(null);
+  }
+
+  /**
+   * 添加学生到班级
+   */
+    @Operation(summary = "添加学生到班级")
+    @PostMapping("/classes/{classId}/students/add")
+    public Result<Boolean> addStudentToClass(@RequestAttribute("userId") Long teacherId,
+      @PathVariable Long classId,
+      @RequestBody Map<String, Long> body) {
+    // Long studentId = body.get("studentId");
+    // boolean success = teacherService.addStudentToClass(teacherId, classId, studentId);
+    // return Result.success(success);
+    return Result.error("功能开发中");
+  }
+
+  /**
+   * 从班级移除学生
+   */
+    @Operation(summary = "从班级移除学生")
+    @DeleteMapping("/classes/{classId}/students/{studentId}")
+    public Result<Boolean> removeStudentFromClass(@RequestAttribute("userId") Long teacherId,
+      @PathVariable Long classId,
+      @PathVariable Long studentId) {
+    // boolean success = teacherService.removeStudentFromClass(teacherId, classId, studentId);
+    // return Result.success(success);
+    return Result.error("功能开发中");
+  }
+
+  /**
+   * 获取学生学习进度
+   */
+    @Operation(summary = "获取学生学习进度")
+    @PostMapping("/students/{studentId}/progress")
+    public Result<StudentProgressDTO> getStudentProgress(@RequestAttribute("userId") Long teacherId,
+      @PathVariable Long studentId) {
+    // TODO: Implement getStudentProgress in TeacherService
+    // StudentProgressDTO progress = teacherService.getStudentProgress(teacherId, studentId);
+    // return Result.success(progress);
+    return Result.success(null);
+  }
+
+  /**
+   * 获取班级学习报告
+   */
+    @Operation(summary = "获取班级学习报告")
+    @PostMapping("/classes/{classId}/report")
+    public Result<ClassReportDTO> getClassReport(@RequestAttribute("userId") Long teacherId,
+      @PathVariable Long classId) {
+    // TODO: Implement getClassReport in TeacherService
+    // ClassReportDTO report = teacherService.getClassReport(teacherId, classId);
+    // return Result.success(report);
+    return Result.success(null);
+  }
+}

+ 112 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/guide/TeacherMessageController.java

@@ -0,0 +1,112 @@
+package com.etotem.cfc.controller.guide;
+import javax.annotation.Resource;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.TeacherMessage;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.TeacherMessageService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "成长规划师消息", description = "成长规划师接受家长咨询的接口")
+@RestController
+@RequestMapping("/api/guide/messages")
+public class TeacherMessageController {
+
+    @Resource
+    private TeacherMessageService teacherMessageService;
+
+    @Resource
+    private UserMapper userMapper;
+
+    /**
+     * 家长发送咨询消息
+     */
+    @Operation(summary = "家长发送咨询")
+    @PostMapping("/send")
+    public Result<Long> sendMessage(@RequestAttribute("userId") Long parentId,
+                                     @RequestBody Map<String, Object> body) {
+        if (body.get("familyId") == null || body.get("teacherId") == null) {
+            return Result.error("缺少必要参数: familyId/teacherId");
+        }
+        String content = (String) body.get("content");
+        if (content == null || content.trim().isEmpty()) {
+            return Result.error("消息内容不能为空");
+        }
+        Long familyId = Long.valueOf(body.get("familyId").toString());
+        Long teacherId = Long.valueOf(body.get("teacherId").toString());
+
+        Long messageId = teacherMessageService.sendMessage(familyId, parentId, teacherId, content);
+        return Result.success(messageId);
+    }
+
+    /**
+     * 成长规划师回复消息
+     */
+    @Operation(summary = "成长规划师回复咨询")
+    @PostMapping("/{messageId}/reply")
+    public Result<Boolean> replyMessage(@RequestAttribute("userId") Long teacherId,
+                                         @RequestAttribute("role") String role,
+                                         @PathVariable Long messageId,
+                                         @RequestBody Map<String, String> body) {
+        if (!"teacher".equals(role)) {
+            return Result.error("只有成长规划师可以回复");
+        }
+        
+        String replyContent = body.get("replyContent");
+        boolean success = teacherMessageService.replyMessage(messageId, teacherId, replyContent);
+        return Result.success(success);
+    }
+
+    /**
+     * 成长规划师获取咨询消息列表
+     */
+    @Operation(summary = "成长规划师获取咨询列表")
+    @PostMapping("/list")
+    public Result<Map<String, Object>> getMessages(@RequestAttribute("userId") Long teacherId,
+            @RequestAttribute("role") String role,
+            @RequestBody Map<String, Object> params) {
+        Integer page = params.get("page") != null ? Integer.valueOf(params.get("page").toString()) : 1;
+        Integer size = params.get("size") != null ? Integer.valueOf(params.get("size").toString()) : 10;
+        if (!"teacher".equals(role)) {
+            return Result.error("只有成长规划师可以查看");
+        }
+
+        Page<TeacherMessage> messages = teacherMessageService.getMessagesByTeacher(teacherId, page, size);
+
+        // 补充家长信息
+        for (TeacherMessage msg : messages.getRecords()) {
+            if (msg.getParentId() != null) {
+                User parent = userMapper.selectById(msg.getParentId());
+                if (parent != null) {
+                    msg.setContent(msg.getContent() + " [家长:" + parent.getNickname() + "]");
+                }
+            }
+        }
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("records", messages.getRecords());
+        result.put("total", messages.getTotal());
+        result.put("pages", messages.getPages());
+        result.put("current", messages.getCurrent());
+
+        return Result.success(result);
+    }
+
+    /**
+     * 家长获取自己的咨询消息
+     */
+    @Operation(summary = "家长获取咨询记录")
+    @PostMapping("/my")
+    public Result<List<TeacherMessage>> getMyMessages(@RequestAttribute("userId") Long parentId) {
+        List<TeacherMessage> messages = teacherMessageService.getMessagesByParent(parentId);
+        return Result.success(messages);
+    }
+}

+ 129 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/parent/ParentAssessmentController.java

@@ -0,0 +1,129 @@
+package com.etotem.cfc.controller.parent;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.AssessmentMaterial;
+import com.etotem.cfc.entity.AssessmentPointsConfig;
+import com.etotem.cfc.entity.AssessmentRecord;
+import com.etotem.cfc.entity.FamilyAssessmentConfig;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.AssessmentService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "家长测评", description = "家长查看DAN测评任务与历史")
+@RestController
+@RequestMapping("/api/parent/assessment")
+public class ParentAssessmentController {
+
+    @Resource
+    private AssessmentService assessmentService;
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Resource
+    private ChildMapper childMapper;
+
+    @Operation(summary = "获取当前测评任务")
+    @PostMapping("/current")
+    public Result<Map<String, Object>> getCurrentAssessment(@RequestAttribute("userId") Long userId) {
+        Long familyId = getFamilyIdByUserId(userId);
+        if (familyId == null) {
+            return Result.error("未找到家庭信息");
+        }
+
+        FamilyAssessmentConfig config = assessmentService.getFamilyConfig(familyId);
+        if (config == null || config.getIsEnabled() != 1) {
+            return Result.success(null);
+        }
+
+        AssessmentMaterial material = null;
+        if (config.getMaterialId() != null) {
+            material = assessmentService.getById(config.getMaterialId());
+        } else {
+            material = assessmentService.getActiveMaterial();
+        }
+
+        AssessmentPointsConfig pointsConfig = assessmentService.getPointsConfig();
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("configId", config.getId());
+        result.put("remindMonths", config.getRemindMonths());
+        result.put("lastRemindAt", config.getLastRemindAt());
+        result.put("nextRemindAt", config.getNextRemindAt());
+
+        if (material != null) {
+            result.put("title", material.getTitle());
+            result.put("content", material.getContent());
+            result.put("coverImage", material.getCoverImage());
+        }
+
+        if (pointsConfig != null) {
+            result.put("pointsValue", pointsConfig.getPointsValue());
+            result.put("pointsValueYuan", pointsConfig.getPointsValueYuan());
+        }
+
+        return Result.success(result);
+    }
+
+    @Operation(summary = "获取测评历史记录")
+    @PostMapping("/records")
+    public Result<List<AssessmentRecord>> getRecords(@RequestAttribute("userId") Long userId) {
+        Long familyId = getFamilyIdByUserId(userId);
+        if (familyId == null) {
+            return Result.success(null);
+        }
+
+        List<AssessmentRecord> records = assessmentService.getFamilyRecords(familyId);
+        return Result.success(records);
+    }
+
+    @Operation(summary = "家长主动创建测评任务")
+    @PostMapping("/create")
+    public Result<AssessmentRecord> createAssessment(@RequestAttribute("userId") Long userId) {
+        Long familyId = getFamilyIdByUserId(userId);
+        if (familyId == null) {
+            return Result.error("未找到家庭信息");
+        }
+
+        AssessmentMaterial material = assessmentService.getActiveMaterial();
+        if (material == null) {
+            return Result.error("暂无可用测评资料");
+        }
+
+        Long childId = getChildIdByUserId(userId);
+        AssessmentRecord record = assessmentService.createRecord(familyId, childId, material.getId());
+
+        return Result.success(record);
+    }
+
+    @Operation(summary = "确认完成测评(由外部系统调用)")
+    @PostMapping("/complete/{recordId}")
+    public Result<Boolean> completeAssessment(@PathVariable Long recordId) {
+        boolean success = assessmentService.completeRecord(recordId);
+        return Result.success(success);
+    }
+
+    private Long getFamilyIdByUserId(Long userId) {
+        User user = userMapper.selectById(userId);
+        return user != null ? user.getFamilyId() : null;
+    }
+
+    private Long getChildIdByUserId(Long userId) {
+        User user = userMapper.selectById(userId);
+        if (user == null) return null;
+        LambdaQueryWrapper<com.etotem.cfc.entity.Child> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(com.etotem.cfc.entity.Child::getFamilyId, user.getFamilyId());
+        com.etotem.cfc.entity.Child child = childMapper.selectOne(wrapper);
+        return child != null ? child.getId() : null;
+    }
+}

+ 75 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/parent/ParentDashboardController.java

@@ -0,0 +1,75 @@
+package com.etotem.cfc.controller.parent;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.ChildInfoDTO;
+import com.etotem.cfc.service.PointsService;
+import com.etotem.cfc.service.TaskService;
+import com.etotem.cfc.service.UserService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "家长首页", description = "家长首页数据接口")
+@RestController
+@RequestMapping("/api/parent")
+public class ParentDashboardController {
+
+    @Resource
+    private UserService userService;
+
+    @Resource
+    private TaskService taskService;
+
+    @Resource
+    private PointsService pointsService;
+
+    @Operation(summary = "获取家长首页仪表盘数据")
+    @PostMapping("/dashboard")
+    public Result<Map<String, Object>> getDashboard(@RequestAttribute("userId") Long userId) {
+        List<ChildInfoDTO> children = userService.getChildren(userId);
+
+        List<Map<String, Object>> childStatsList = new ArrayList<>();
+        int totalCompletionRate = 0;
+        int totalHabitCount = 0;
+        int childCount = children == null ? 0 : children.size();
+
+        if (children != null) {
+            for (ChildInfoDTO child : children) {
+                Map<String, Object> stats = new HashMap<>();
+                stats.put("id", child.getId());
+                stats.put("nickname", child.getNickname());
+                stats.put("totalPoints", child.getTotalPoints());
+                stats.put("systemPoints", child.getSystemPoints());
+                stats.put("streakDays", child.getStreakDays());
+                stats.put("danLevel", child.getDanLevel());
+                stats.put("age", child.getAge());
+
+                Map<String, Object> taskStats = taskService.getChildTaskStats(child.getId());
+                stats.put("completionRate", taskStats.getOrDefault("completionRate", 0));
+                stats.put("pendingReviewCount", taskStats.getOrDefault("pendingReviewCount", 0));
+                stats.put("habitCount", taskStats.getOrDefault("habitCount", 0));
+
+                totalCompletionRate += (Integer) taskStats.getOrDefault("completionRate", 0);
+                totalHabitCount += (Integer) taskStats.getOrDefault("habitCount", 0);
+
+                childStatsList.add(stats);
+            }
+        }
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("children", childStatsList);
+        result.put("avgCompletionRate", childCount > 0 ? totalCompletionRate / childCount : 0);
+        result.put("totalHabitCount", totalHabitCount);
+
+        return Result.success(result);
+    }
+}

+ 60 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductController.java

@@ -0,0 +1,60 @@
+package com.etotem.cfc.controller.product;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.ProductDTO;
+import com.etotem.cfc.dto.ProductListQueryDTO;
+import com.etotem.cfc.entity.Product;
+import com.etotem.cfc.service.ProductService;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/product")
+public class ProductController {
+
+    @Resource
+    private ProductService productService;
+
+    @PostMapping("/list")
+    public Result<Map<String, Object>> list(@RequestBody ProductListQueryDTO query) {
+        return productService.list(query);
+    }
+
+    @PostMapping("/detail")
+    public Result<ProductDTO> detail(@RequestBody Map<String, Object> params) {
+        Long id = Long.parseLong(params.get("id").toString());
+        return productService.detail(id);
+    }
+
+    @PostMapping("/create")
+    public Result<ProductDTO> create(@RequestBody Product product,
+                                    @RequestHeader(value = "X-User-Id", required = false) Long userId) {
+        return productService.create(product, userId);
+    }
+
+    @PostMapping("/update")
+    public Result<ProductDTO> update(@RequestBody Product product,
+                                    @RequestHeader(value = "X-User-Id", required = false) Long userId) {
+        return productService.update(product, userId);
+    }
+
+    @PostMapping("/my")
+    public Result<List<ProductDTO>> myProducts(@RequestHeader(value = "X-User-Id", required = false) Long userId) {
+        return productService.myProducts(userId);
+    }
+
+    @PostMapping("/shelve")
+    public Result<String> shelve(@RequestBody Map<String, Object> params,
+                                @RequestHeader(value = "X-User-Id", required = false) Long userId) {
+        Long productId = Long.parseLong(params.get("productId").toString());
+        String action = (String) params.get("action");
+        return productService.shelve(productId, userId, action);
+    }
+}

+ 71 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductOrderController.java

@@ -0,0 +1,71 @@
+package com.etotem.cfc.controller.product;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.CreateProductOrderDTO;
+import com.etotem.cfc.dto.ProductOrderDTO;
+import com.etotem.cfc.service.ProductOrderService;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.RequestAttribute;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/product/order")
+public class ProductOrderController {
+
+    @Resource
+    private ProductOrderService orderService;
+
+    @PostMapping("/create")
+    public Result<ProductOrderDTO> create(@RequestBody CreateProductOrderDTO dto,
+                                         @RequestAttribute("userId") Long userId) {
+        return orderService.create(dto, userId);
+    }
+
+    @PostMapping("/pay")
+    public Result<ProductOrderDTO> pay(@RequestBody Map<String, Object> params,
+                                      @RequestAttribute("userId") Long userId) {
+        String orderNo = (String) params.get("orderNo");
+        return orderService.pay(orderNo, userId);
+    }
+
+    @PostMapping("/cancel")
+    public Result<String> cancel(@RequestBody Map<String, Object> params,
+                                @RequestAttribute("userId") Long userId) {
+        String orderNo = (String) params.get("orderNo");
+        return orderService.cancel(orderNo, userId);
+    }
+
+    @PostMapping("/my")
+    public Result<List<ProductOrderDTO>> myOrders(@RequestBody Map<String, Object> params,
+                                                 @RequestAttribute("userId") Long userId) {
+        String status = (String) params.get("status");
+        return orderService.myOrders(userId, status);
+    }
+
+    @PostMapping("/detail")
+    public Result<ProductOrderDTO> detail(@RequestBody Map<String, Object> params,
+                                        @RequestAttribute("userId") Long userId) {
+        String orderNo = (String) params.get("orderNo");
+        return orderService.detail(orderNo, userId);
+    }
+
+    @PostMapping("/vendor")
+    public Result<List<ProductOrderDTO>> vendorOrders(@RequestBody Map<String, Object> params,
+                                                     @RequestAttribute("userId") Long userId) {
+        String status = (String) params.get("status");
+        return orderService.vendorOrders(userId, status);
+    }
+
+    @PostMapping("/confirm")
+    public Result<String> confirm(@RequestBody Map<String, Object> params,
+                                 @RequestAttribute("userId") Long userId) {
+        String orderNo = (String) params.get("orderNo");
+        return orderService.confirm(orderNo, userId);
+    }
+}

+ 66 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/reward/RewardController.java

@@ -0,0 +1,66 @@
+package com.etotem.cfc.controller.reward;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.CreateRewardDTO;
+import com.etotem.cfc.dto.RewardDTO;
+import com.etotem.cfc.entity.Reward;
+import com.etotem.cfc.service.RewardService;
+import javax.annotation.Resource;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/rewards")
+public class RewardController {
+
+    @Resource
+    private RewardService rewardService;
+
+    @PostMapping("/create")
+    public Result<Long> createReward(@RequestAttribute("userId") Long userId,
+                                     @RequestBody CreateRewardDTO dto) {
+        Long rewardId = rewardService.createReward(userId, dto);
+        return Result.success(rewardId);
+    }
+
+    @PostMapping("/wishlist")
+    public Result<List<RewardDTO>> getWishlist(@RequestBody Map<String, Object> params) {
+        Long childId = params.get("childId") != null ? ((Number) params.get("childId")).longValue() : null;
+        List<RewardDTO> wishlist = rewardService.getWishlist(childId);
+        return Result.success(wishlist);
+    }
+
+    @PostMapping("/{id}/exchange")
+    public Result<Map<String, Object>> exchangeReward(@PathVariable Long id,
+                                                     @RequestBody Map<String, Object> params) {
+        Long childId = Long.valueOf(params.get("childId").toString());
+        Map<String, Object> result = rewardService.exchangeReward(id, childId);
+        return Result.success(result);
+    }
+
+    @PostMapping("/{id}/approve")
+    public Result<Boolean> approveReward(@PathVariable Long id,
+                                        @RequestBody java.util.Map<String, Boolean> body) {
+        Boolean approved = body.get("approved");
+        boolean success = rewardService.approveReward(id, approved);
+        return Result.success(success);
+    }
+
+    @PostMapping("/templates")
+    public Result<List<Reward>> getTemplates() {
+        List<Reward> templates = rewardService.getTemplates();
+        return Result.success(templates);
+    }
+
+    @PostMapping("/exchange-history")
+    public Result<Page<Reward>> getExchangeHistory(@RequestBody Map<String, Object> params) {
+        Long childId = params.get("childId") != null ? ((Number) params.get("childId")).longValue() : null;
+        Integer page = params.get("page") != null ? (Integer) params.get("page") : 1;
+        Integer size = params.get("size") != null ? (Integer) params.get("size") : 10;
+        Page<Reward> history = rewardService.getExchangeHistory(childId, page, size);
+        return Result.success(history);
+    }
+}

+ 70 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/reward/RewardWishlistController.java

@@ -0,0 +1,70 @@
+package com.etotem.cfc.controller.reward;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.CreateRewardWishlistDTO;
+import com.etotem.cfc.entity.RewardWishlist;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.service.RewardWishlistService;
+import com.etotem.cfc.service.UserService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "家长心愿单管理")
+@RestController
+@RequestMapping("/api/reward-wishlist")
+public class RewardWishlistController {
+
+    @Resource
+    private RewardWishlistService rewardWishlistService;
+
+    @Resource
+    private UserService userService;
+
+    @Operation(summary = "创建心愿")
+    @PostMapping("/create")
+    public Result<Long> create(@RequestAttribute("userId") Long userId,
+                                @RequestBody CreateRewardWishlistDTO dto) {
+        Long id = rewardWishlistService.create(userId, dto);
+        return Result.success(id);
+    }
+
+    @Operation(summary = "获取我的心愿单")
+    @PostMapping("/my")
+    public Result<List<RewardWishlist>> getMyWishlist(@RequestAttribute("userId") Long userId) {
+        List<RewardWishlist> list = rewardWishlistService.getByUserId(userId);
+        return Result.success(list);
+    }
+
+    @Operation(summary = "获取家庭心愿单")
+    @PostMapping("/family")
+    public Result<List<RewardWishlist>> getFamilyWishlist(@RequestAttribute("userId") Long userId) {
+        User user = userService.getUserInfo(userId);
+        if (user == null) {
+            return Result.error("用户不存在");
+        }
+        List<RewardWishlist> list = rewardWishlistService.getByFamilyId(user.getFamilyId());
+        return Result.success(list);
+    }
+
+    @Operation(summary = "兑换心愿")
+    @PostMapping("/{id}/exchange")
+    public Result<Boolean> exchange(@PathVariable Long id,
+                                     @RequestAttribute("userId") Long userId) {
+        boolean success = rewardWishlistService.exchange(id, userId);
+        return Result.success(success);
+    }
+
+    @Operation(summary = "审核心愿")
+    @PostMapping("/{id}/approve")
+    public Result<Boolean> approve(@PathVariable Long id,
+                                   @RequestBody Map<String, Boolean> body) {
+        boolean approved = body.get("approved");
+        boolean success = rewardWishlistService.approve(id, approved);
+        return Result.success(success);
+    }
+}

+ 38 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/sncp/ZkxzSyncController.java

@@ -0,0 +1,38 @@
+package com.etotem.cfc.controller.sncp;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.service.ZkxzSyncService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+
+@Tag(name = "zkxz同步", description = "与zkxz外部平台同步")
+@RestController
+@RequestMapping("/api/sncp")
+public class ZkxzSyncController {
+
+    @Resource
+    private ZkxzSyncService zkxzSyncService;
+
+    @Operation(summary = "同步客户到zkxz")
+    @PostMapping("/addClient/{clientId}")
+    public Result<String> syncClient(@PathVariable String clientId) {
+        if (!zkxzSyncService.isEnabled()) {
+            return Result.error("zkxz同步未启用");
+        }
+        String result = zkxzSyncService.syncClient(new HashMap<>());
+        return Result.success(result);
+    }
+
+    @Operation(summary = "获取通知")
+    @PostMapping("/notice/{userLogin}")
+    public Result<String> getNotice(@PathVariable String userLogin) {
+        if (!zkxzSyncService.isEnabled()) {
+            return Result.success("zkxz未启用");
+        }
+        return Result.success("zkxz未启用");
+    }
+}

+ 72 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/stats/StatsController.java

@@ -0,0 +1,72 @@
+package com.etotem.cfc.controller.stats;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.ChildMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.TaskStatsService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Map;
+
+@Tag(name = "任务统计", description = "任务完成率分析")
+@RestController
+@RequestMapping("/api/stats")
+public class StatsController {
+
+    @Resource
+    private TaskStatsService taskStatsService;
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Resource
+    private ChildMapper childMapper;
+
+    @Operation(summary = "孩子任务完成率")
+    @PostMapping("/child-completion")
+    public Result<Map<String, Object>> childCompletion(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> params) {
+        User user = userMapper.selectById(userId);
+        if (user == null) return Result.error("用户不存在");
+
+        Long childId = params.get("childId") != null ? Long.valueOf(params.get("childId").toString()) : null;
+        Date startDate = parseDate(params.get("startDate"));
+        Date endDate = parseDate(params.get("endDate"));
+
+        Map<String, Object> stats = taskStatsService.getChildStats(childId, startDate, endDate);
+        return Result.success(stats);
+    }
+
+    @Operation(summary = "家庭任务完成率")
+    @PostMapping("/family-completion")
+    public Result<Map<String, Object>> familyCompletion(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> params) {
+        User user = userMapper.selectById(userId);
+        if (user == null) return Result.error("用户不存在");
+        if (user.getFamilyId() == null) return Result.error("未关联家庭");
+
+        Date startDate = parseDate(params.get("startDate"));
+        Date endDate = parseDate(params.get("endDate"));
+
+        Map<String, Object> stats = taskStatsService.getFamilyStats(user.getFamilyId(), startDate, endDate);
+        return Result.success(stats);
+    }
+
+    private Date parseDate(Object value) {
+        if (value == null) return null;
+        try {
+            return new SimpleDateFormat("yyyy-MM-dd").parse(value.toString());
+        } catch (ParseException e) {
+            return null;
+        }
+    }
+}

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

@@ -0,0 +1,161 @@
+package com.etotem.cfc.controller.task;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.CompleteTaskDTO;
+import com.etotem.cfc.dto.CreateTaskDTO;
+import com.etotem.cfc.dto.TaskReviewDTO;
+import com.etotem.cfc.entity.MiniGame;
+import com.etotem.cfc.entity.Task;
+import com.etotem.cfc.service.MiniGameService;
+import com.etotem.cfc.service.TaskService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import javax.annotation.Resource;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "任务管理", description = "任务的创建、完成、审核、历史查询等接口")
+@RestController
+@RequestMapping("/api/tasks")
+public class TaskController {
+
+    @Resource
+    private TaskService taskService;
+
+    @Resource
+    private MiniGameService miniGameService;
+
+    @Operation(summary = "获取可选择的小游戏列表")
+    @PostMapping("/minigame-options")
+    public Result<List<MiniGame>> getMinigameOptions() {
+        List<MiniGame> games = miniGameService.getEnabledGames();
+        return Result.success(games);
+    }
+
+    @Operation(summary = "创建任务")
+    @PostMapping("/create")
+    public Result<Long> createTask(@RequestAttribute("userId") Long userId,
+                                   @RequestBody CreateTaskDTO dto) {
+        Long taskId = taskService.createTask(userId, dto);
+        return Result.success(taskId);
+    }
+
+    @Operation(summary = "获取今日任务")
+    @PostMapping("/today")
+    public Result<List<Task>> getTodayTasks(@RequestBody Map<String, Object> params) {
+        Long childId = Long.valueOf(params.get("childId").toString());
+        List<Task> tasks = taskService.getTodayTasks(childId);
+        return Result.success(tasks);
+    }
+
+    @Operation(summary = "完成任务")
+    @PostMapping("/{id}/complete")
+    public Result<Map<String, Object>> completeTask(@PathVariable Long id,
+                                                    @RequestBody CompleteTaskDTO dto) {
+        Long childId = dto.getChildId();
+        Map<String, Object> result = taskService.completeTask(id, childId, dto.getPhotoUrl());
+        return Result.success(result);
+    }
+
+    @Operation(summary = "审核任务")
+    @PostMapping("/{id}/review")
+    public Result<Boolean> reviewTask(@PathVariable Long id,
+                                      @RequestBody TaskReviewDTO dto) {
+        boolean success = taskService.reviewTask(id, dto);
+        return Result.success(success);
+    }
+
+  @Operation(summary = "获取任务历史")
+  @PostMapping("/history")
+  public Result<Page<Task>> getTaskHistory(@RequestBody Map<String, Object> params) {
+    Long childId = Long.valueOf(params.get("childId").toString());
+    Integer page = params.get("page") != null ? Integer.valueOf(params.get("page").toString()) : 1;
+    Integer size = params.get("size") != null ? Integer.valueOf(params.get("size").toString()) : 10;
+    Page<Task> history = taskService.getTaskHistory(childId, page, size);
+    return Result.success(history);
+  }
+
+    @Operation(summary = "获取待审核任务")
+    @PostMapping("/pending-review")
+    public Result<List<Task>> getPendingReviewTasks(@RequestAttribute("userId") Long userId) {
+    List<Task> tasks = taskService.getPendingReviewTasks(userId);
+    return Result.success(tasks);
+  }
+
+    @Operation(summary = "获取家长今日任务")
+    @PostMapping("/today-parent")
+    public Result<List<Task>> getTodayParentTasks(@RequestAttribute("userId") Long userId) {
+        List<Task> tasks = taskService.getTodayParentTasks(userId);
+        return Result.success(tasks);
+    }
+
+    @Operation(summary = "家长完成任务")
+    @PostMapping("/{id}/complete-parent")
+    public Result<Map<String, Object>> completeParentTask(@PathVariable Long id,
+                                                        @RequestAttribute("userId") Long userId) {
+        Map<String, Object> result = taskService.completeParentTask(id, userId);
+        return Result.success(result);
+    }
+
+    @Operation(summary = "删除任务")
+    @DeleteMapping("/{id}")
+    public Result<Boolean> deleteTask(@PathVariable Long id,
+            @RequestAttribute("userId") Long userId) {
+        boolean success = taskService.deleteTask(id, userId);
+        return Result.success(success);
+    }
+
+    @Operation(summary = "完成小游戏任务")
+    @PostMapping("/{id}/complete-minigame")
+    public Result<Map<String, Object>> completeMinigameTask(
+            @PathVariable Long id,
+            @RequestBody Map<String, Object> body) {
+        Long childId = Long.valueOf(body.get("childId").toString());
+        Integer completionTime = body.get("completionTime") != null ?
+            Integer.valueOf(body.get("completionTime").toString()) : null;
+        Integer score = body.get("score") != null ?
+            Integer.valueOf(body.get("score").toString()) : null;
+
+        try {
+            Map<String, Object> result = taskService.completeMinigameTask(id, childId, completionTime, score);
+            return Result.success(result);
+        } catch (Exception e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    @Operation(summary = "批量完成任务")
+    @PostMapping("/batch-complete")
+    public Result<Boolean> batchComplete(@RequestBody Map<String, Object> params,
+                                         @RequestAttribute("userId") Long userId) {
+        List<Long> taskIds = (List<Long>) params.get("taskIds");
+        if (taskIds == null || taskIds.isEmpty()) {
+            return Result.error("taskIds不能为空");
+        }
+        try {
+            taskService.batchComplete(taskIds, userId);
+            return Result.success(true);
+        } catch (Exception e) {
+            return Result.error("批量操作失败: " + e.getMessage());
+        }
+    }
+
+    @Operation(summary = "批量删除任务")
+    @PostMapping("/batch-delete")
+    public Result<Boolean> batchDelete(@RequestBody Map<String, Object> params,
+                                       @RequestAttribute("userId") Long userId) {
+        List<Long> taskIds = (List<Long>) params.get("taskIds");
+        if (taskIds == null || taskIds.isEmpty()) {
+            return Result.error("taskIds不能为空");
+        }
+        try {
+            taskService.batchDelete(taskIds, userId);
+            return Result.success(true);
+        } catch (Exception e) {
+            return Result.error("批量操作失败: " + e.getMessage());
+        }
+    }
+}

+ 145 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/task/TaskReminderController.java

@@ -0,0 +1,145 @@
+package com.etotem.cfc.controller.task;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.Task;
+import com.etotem.cfc.service.TaskReminderService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * TASK-013~015: 任务提醒和统计接口
+ */
+@Tag(name = "任务提醒统计", description = "任务提醒、历史查询、统计分析接口")
+@RestController
+@RequestMapping("/api/task-reminders")
+public class TaskReminderController {
+
+    @Resource
+    private TaskReminderService taskReminderService;
+
+    /**
+     * TASK-013: 获取需要提醒的任务(30分钟内到期)
+     */
+    @Operation(summary = "获取需要提醒的任务")
+    @PostMapping("/reminder/{childId}")
+    public Result<List<Task>> getTasksNeedReminder(@PathVariable Long childId) {
+        List<Task> tasks = taskReminderService.getTasksNeedReminder(childId);
+        return Result.success(tasks);
+    }
+
+    /**
+     * TASK-013: 获取即将到期的任务(可指定时间范围)
+     */
+    @Operation(summary = "获取即将到期的任务")
+    @PostMapping("/upcoming/{childId}")
+    public Result<List<Task>> getUpcomingTasks(@PathVariable Long childId,
+                                                @RequestBody(required = false) Map<String, Object> params) {
+        Integer minutes = params != null && params.get("minutes") != null ? Integer.valueOf(params.get("minutes").toString()) : 60;
+        List<Task> tasks = taskReminderService.getUpcomingTasks(childId, minutes);
+        return Result.success(tasks);
+    }
+
+    /**
+     * TASK-013: 获取已超时任务
+     */
+    @Operation(summary = "获取已超时任务")
+    @PostMapping("/overdue/{childId}")
+    public Result<List<Task>> getOverdueTasks(@PathVariable Long childId) {
+        List<Task> tasks = taskReminderService.getOverdueTasks(childId);
+        return Result.success(tasks);
+    }
+
+    /**
+     * TASK-014: 获取任务历史(带筛选)
+     */
+    @Operation(summary = "获取任务历史(带筛选)")
+    @PostMapping("/history/filter/{childId}")
+    public Result<Map<String, Object>> getTaskHistoryWithFilter(
+            @PathVariable Long childId,
+            @RequestBody(required = false) Map<String, Object> params) {
+        String status = params != null && params.get("status") != null ? params.get("status").toString() : null;
+        Date startDate = null;
+        Date endDate = null;
+        Integer page = params != null && params.get("page") != null ? Integer.valueOf(params.get("page").toString()) : 1;
+        Integer size = params != null && params.get("size") != null ? Integer.valueOf(params.get("size").toString()) : 10;
+        try {
+            if (params != null && params.get("startDate") != null) {
+                startDate = new java.text.SimpleDateFormat("yyyy-MM-dd").parse(params.get("startDate").toString());
+            }
+            if (params != null && params.get("endDate") != null) {
+                endDate = new java.text.SimpleDateFormat("yyyy-MM-dd").parse(params.get("endDate").toString());
+            }
+        } catch (Exception e) {
+            startDate = null;
+            endDate = null;
+        }
+        Map<String, Object> result = taskReminderService.getTaskHistoryWithFilter(
+            childId, status, startDate, endDate, page, size);
+        return Result.success(result);
+    }
+
+    /**
+     * TASK-015: 获取任务统计概览
+     */
+    @Operation(summary = "获取任务统计概览")
+    @PostMapping("/statistics/{childId}")
+    public Result<Map<String, Object>> getTaskStatistics(
+            @PathVariable Long childId,
+            @RequestBody(required = false) Map<String, Object> params) {
+        Date startDate = null;
+        Date endDate = null;
+        try {
+            if (params != null && params.get("startDate") != null) {
+                startDate = new java.text.SimpleDateFormat("yyyy-MM-dd").parse(params.get("startDate").toString());
+            }
+            if (params != null && params.get("endDate") != null) {
+                endDate = new java.text.SimpleDateFormat("yyyy-MM-dd").parse(params.get("endDate").toString());
+            }
+        } catch (Exception e) {
+            startDate = null;
+            endDate = null;
+        }
+        Map<String, Object> stats = taskReminderService.getTaskStatistics(childId, startDate, endDate);
+        return Result.success(stats);
+    }
+
+    /**
+     * TASK-015: 获取按分类统计
+     */
+    @Operation(summary = "获取按分类统计")
+    @PostMapping("/statistics/category/{childId}")
+    public Result<List<Map<String, Object>>> getTaskStatsByCategory(@PathVariable Long childId) {
+        List<Map<String, Object>> stats = taskReminderService.getTaskStatsByCategory(childId);
+        return Result.success(stats);
+    }
+
+    /**
+     * TASK-015: 获取按日期统计
+     */
+    @Operation(summary = "获取按日期统计")
+    @PostMapping("/statistics/date/{childId}")
+    public Result<List<Map<String, Object>>> getTaskStatsByDate(
+            @PathVariable Long childId,
+            @RequestBody(required = false) Map<String, Object> params) {
+        Integer days = params != null && params.get("days") != null ? Integer.valueOf(params.get("days").toString()) : 7;
+        List<Map<String, Object>> stats = taskReminderService.getTaskStatsByDate(childId, days);
+        return Result.success(stats);
+    }
+
+    /**
+     * TASK-015: 获取家庭任务统计
+     */
+    @Operation(summary = "获取家庭任务统计")
+    @PostMapping("/statistics/family/{familyId}")
+    public Result<Map<String, Object>> getFamilyTaskStatistics(@PathVariable Long familyId) {
+        Map<String, Object> stats = taskReminderService.getFamilyTaskStatistics(familyId);
+        return Result.success(stats);
+    }
+}

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

@@ -0,0 +1,38 @@
+package com.etotem.cfc.controller.vendor;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.service.VendorService;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/vendor")
+public class VendorController {
+
+    @Resource
+    private VendorService vendorService;
+
+    @PostMapping("/apply")
+    public Result apply(@RequestBody Map<String, Object> params,
+                        @RequestHeader("X-User-Id") Long userId) {
+        String vendorType = (String) params.get("vendorType");
+        String vendorInfo = (String) params.getOrDefault("vendorInfo", "");
+        return vendorService.apply(userId, vendorType, vendorInfo);
+    }
+
+    @PostMapping("/status")
+    public Result getStatus(@RequestHeader("X-User-Id") Long userId) {
+        return vendorService.getStatus(userId);
+    }
+
+    @PostMapping("/info")
+    public Result getVendorInfo(@RequestHeader("X-User-Id") Long userId) {
+        return vendorService.getVendorInfo(userId);
+    }
+}

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

@@ -0,0 +1,255 @@
+package com.etotem.cfc.controller.wish;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.CreateWishDTO;
+import com.etotem.cfc.dto.WishDTO;
+import com.etotem.cfc.service.WishService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 心愿控制器
+ */
+@Slf4j
+@Tag(name = "心愿管理", description = "心愿创建、定价、兑换、兑现等接口")
+@RestController
+@RequestMapping("/api/wishes")
+public class WishController {
+
+    @Resource
+    private WishService wishService;
+
+    // ========== 创建心愿 ==========
+
+    @Operation(summary = "创建心愿")
+    @PostMapping("/create")
+    public Result<Long> createWish(
+            @RequestAttribute("userId") Long userId,
+            @RequestAttribute("role") String role,
+            @RequestBody CreateWishDTO dto) {
+        try {
+            Long wishId;
+            if ("child".equals(role)) {
+                wishId = wishService.createByChild(userId, dto);
+            } else {
+                wishId = wishService.createByParent(userId, dto);
+            }
+            return Result.success(wishId);
+        } catch (RuntimeException e) {
+            log.error("创建心愿失败", e);
+            return Result.error(e.getMessage());
+        }
+    }
+
+    // ========== 定价流程 ==========
+
+    @Operation(summary = "家长定价")
+    @PutMapping("/{id}/set-price")
+    public Result<Boolean> setPrice(
+            @PathVariable Long id,
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> body) {
+        try {
+            Integer pointsRequired = (Integer) body.get("pointsRequired");
+            if (pointsRequired == null || pointsRequired <= 0) {
+                return Result.error("请输入有效的积分值");
+            }
+            boolean success = wishService.setPrice(id, userId, pointsRequired);
+            if (!success) {
+                return Result.error("定价失败,请检查心愿状态");
+            }
+            return Result.success(true);
+        } catch (Exception e) {
+            log.error("定价失败", e);
+            return Result.error("定价失败: " + e.getMessage());
+        }
+    }
+
+    @Operation(summary = "家长拒绝心愿")
+    @PutMapping("/{id}/reject")
+    public Result<Boolean> rejectWish(
+            @PathVariable Long id,
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, String> body) {
+        try {
+            String reason = body.get("reason");
+            boolean success = wishService.rejectWish(id, userId, reason);
+            if (!success) {
+                return Result.error("拒绝失败,请检查心愿状态");
+            }
+            return Result.success(true);
+        } catch (Exception e) {
+            log.error("拒绝心愿失败", e);
+            return Result.error("拒绝失败: " + e.getMessage());
+        }
+    }
+
+    // ========== 兑换流程 ==========
+
+    @Operation(summary = "孩子申请兑换")
+    @PostMapping("/{id}/exchange")
+    public Result<Boolean> requestExchange(
+            @PathVariable Long id,
+            @RequestAttribute("userId") Long userId) {
+        try {
+            boolean success = wishService.requestExchange(id, userId);
+            if (!success) {
+                return Result.error("申请兑换失败,请检查心愿状态");
+            }
+            return Result.success(true);
+        } catch (RuntimeException e) {
+            log.error("申请兑换失败", e);
+            return Result.error(e.getMessage());
+        }
+    }
+
+    @Operation(summary = "家长审批兑换")
+    @PutMapping("/{id}/approve-exchange")
+    public Result<Boolean> approveExchange(
+            @PathVariable Long id,
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> body) {
+        try {
+            Boolean approved = (Boolean) body.get("approved");
+            String reason = (String) body.get("reason"); // 拒绝原因
+            if (approved == null) {
+                return Result.error("请指定审批结果");
+            }
+            boolean success = wishService.approveExchange(id, userId, approved);
+            if (!success) {
+                return Result.error("审批失败,请检查心愿状态");
+            }
+            return Result.success(true);
+        } catch (RuntimeException e) {
+            log.error("审批兑换失败", e);
+            return Result.error(e.getMessage());
+        }
+    }
+
+    // ========== 兑现流程 ==========
+
+    @Operation(summary = "家长标记已购买")
+    @PutMapping("/{id}/fulfill")
+    public Result<Boolean> markFulfilled(
+            @PathVariable Long id,
+            @RequestAttribute("userId") Long userId) {
+        try {
+            boolean success = wishService.markFulfilled(id, userId);
+            if (!success) {
+                return Result.error("标记失败,请检查心愿状态");
+            }
+            return Result.success(true);
+        } catch (Exception e) {
+            log.error("标记已购买失败", e);
+            return Result.error("标记失败: " + e.getMessage());
+        }
+    }
+
+    @Operation(summary = "孩子确认收货")
+    @PutMapping("/{id}/confirm")
+    public Result<Boolean> confirmReceived(
+            @PathVariable Long id,
+            @RequestAttribute("userId") Long userId) {
+        try {
+            boolean success = wishService.confirmReceived(id, userId);
+            if (!success) {
+                return Result.error("确认失败,请检查心愿状态");
+            }
+            return Result.success(true);
+        } catch (Exception e) {
+            log.error("确认收货失败", e);
+            return Result.error("确认失败: " + e.getMessage());
+        }
+    }
+
+    // ========== 取消心愿 ==========
+
+    @Operation(summary = "取消心愿")
+    @DeleteMapping("/{id}")
+    public Result<Boolean> cancelWish(
+            @PathVariable Long id,
+            @RequestAttribute("userId") Long userId,
+            @RequestBody(required = false) Map<String, String> body) {
+        try {
+            String reason = body != null ? body.get("reason") : null;
+            boolean success = wishService.cancelWish(id, userId, reason);
+            if (!success) {
+                return Result.error("取消失败,请检查心愿状态");
+            }
+            return Result.success(true);
+        } catch (Exception e) {
+            log.error("取消心愿失败", e);
+            return Result.error("取消失败: " + e.getMessage());
+        }
+    }
+
+    // ========== 查询接口 ==========
+
+    @Operation(summary = "获取心愿列表")
+    @PostMapping("/list")
+    public Result<List<WishDTO>> getWishes(
+            @RequestAttribute("userId") Long userId,
+            @RequestAttribute("role") String role,
+            @RequestParam(required = false) Long childId,
+            @RequestParam(required = false) Long familyId,
+            @RequestParam(required = false) String status) {
+        try {
+            List<WishDTO> wishes;
+            if ("child".equals(role)) {
+                // 孩子只能看自己的心愿
+                wishes = wishService.getChildWishes(userId);
+            } else if (childId != null) {
+                // 家长查看指定孩子的心愿
+                wishes = wishService.getChildWishes(childId);
+            } else if (familyId != null) {
+                // 家长查看家庭所有心愿
+                wishes = wishService.getFamilyWishes(familyId);
+            } else {
+                return Result.error("请指定查询条件(childId或familyId)");
+            }
+            return Result.success(wishes);
+        } catch (Exception e) {
+            log.error("获取心愿列表失败", e);
+            return Result.error("获取失败: " + e.getMessage());
+        }
+    }
+
+    @Operation(summary = "获取待处理列表(家长)")
+    @PostMapping("/pending")
+    public Result<List<WishDTO>> getPendingWishes(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> body) {
+        try {
+            Long familyId = body.get("familyId") != null ? Long.valueOf(body.get("familyId").toString()) : null;
+            if (familyId == null) {
+                return Result.error("请指定家庭ID");
+            }
+            List<WishDTO> wishes = wishService.getParentPendingWishes(familyId);
+            return Result.success(wishes);
+        } catch (Exception e) {
+            log.error("获取待处理列表失败", e);
+            return Result.error("获取失败: " + e.getMessage());
+        }
+    }
+
+    @Operation(summary = "获取心愿详情")
+    @PostMapping("/{id}")
+    public Result<WishDTO> getWishById(@PathVariable Long id) {
+        try {
+            WishDTO wish = wishService.getWishById(id);
+            if (wish == null) {
+                return Result.error("心愿不存在");
+            }
+            return Result.success(wish);
+        } catch (Exception e) {
+            log.error("获取心愿详情失败", e);
+            return Result.error("获取失败: " + e.getMessage());
+        }
+    }
+}

+ 17 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/AddGuidePackageDTO.java

@@ -0,0 +1,17 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+
+@Data
+public class AddGuidePackageDTO {
+    @NotNull(message = "套餐ID不能为空")
+    private Long packageId;
+
+    @NotNull(message = "价格不能为空")
+    private BigDecimal price;
+
+    private BigDecimal commissionRate;
+}

+ 25 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/AdjustPointsDTO.java

@@ -0,0 +1,25 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+/**
+ * 调整积分 DTO
+ */
+@Data
+public class AdjustPointsDTO {
+
+    @NotNull(message = "孩子ID不能为空")
+    private Long childId;
+
+    @NotNull(message = "调整金额不能为空")
+    private Integer amount;
+
+    @NotBlank(message = "调整原因不能为空")
+    private String reason;
+
+    @NotBlank(message = "密码不能为空")
+    private String password;
+}

+ 17 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/AdminAdjustPointsDTO.java

@@ -0,0 +1,17 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 管理员调整孩子积分 DTO(路径含childId,故不含childId)
+ */
+@Data
+public class AdminAdjustPointsDTO {
+
+    @NotNull(message = "调整金额不能为空")
+    private Integer amount;
+
+    private String reason;
+}

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

@@ -0,0 +1,15 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 管理员审批奖励 DTO
+ */
+@Data
+public class AdminApproveRewardDTO {
+
+    @NotNull(message = "审批结果不能为空")
+    private Boolean approved;
+}

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

@@ -0,0 +1,15 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+@Data
+public class AdminChangePasswordDTO {
+
+    @NotBlank(message = "原密码不能为空")
+    private String oldPassword;
+
+    @NotBlank(message = "新密码不能为空")
+    private String newPassword;
+}

+ 32 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/AdminCreateGuidePackageDTO.java

@@ -0,0 +1,32 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+@Data
+public class AdminCreateGuidePackageDTO {
+
+    @NotBlank(message = "套餐名称不能为空")
+    private String name;
+
+    private String level;
+
+    private String description;
+
+    @NotNull(message = "价格不能为空")
+    private BigDecimal price;
+
+    private Integer validityDays;
+
+    private Long guideId;
+
+    private Long packageId;
+
+    private BigDecimal commissionRate;
+
+    private Integer sortOrder;
+}

+ 18 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/AdminCreateRewardDTO.java

@@ -0,0 +1,18 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+@Data
+public class AdminCreateRewardDTO {
+
+    @NotBlank(message = "奖励名称不能为空")
+    private String title;
+
+    @NotNull(message = "所需积分不能为空")
+    private Integer pointsRequired;
+
+    private String category;
+}

+ 42 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/AdminCreateTaskTemplateDTO.java

@@ -0,0 +1,42 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+@Data
+public class AdminCreateTaskTemplateDTO {
+
+    @NotBlank(message = "任务标题不能为空")
+    private String title;
+
+    private String description;
+
+    @NotNull(message = "积分不能为空")
+    private Integer points;
+
+    private String category;
+
+    private String difficulty;
+
+    private Integer isActive;
+
+    private Integer needReview;
+
+    private Integer reviewByCategory;
+
+    private Integer recommendedAge;
+
+    private String completeTypes;
+
+    private Integer duration;
+
+    private String reviewType;
+
+    private String taskType;
+
+    private String frequency;
+
+    private Integer maxFrequency;
+}

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

@@ -0,0 +1,21 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * 管理员创建用户 DTO
+ */
+@Data
+public class AdminCreateUserDTO {
+
+    @NotBlank(message = "手机号不能为空")
+    private String phone;
+
+    private String nickname;
+
+    private String role;
+
+    private String realName;
+}

+ 10 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/AdminLoginDTO.java

@@ -0,0 +1,10 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+@Data
+public class AdminLoginDTO {
+    private String phone;
+    private String code;
+    private String password;
+}

+ 17 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/AdminResetPasswordDTO.java

@@ -0,0 +1,17 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Size;
+
+/**
+ * 管理员重置用户密码 DTO
+ */
+@Data
+public class AdminResetPasswordDTO {
+
+    @NotBlank(message = "密码不能为空")
+    @Size(min = 6, message = "密码长度不能少于6位")
+    private String password;
+}

+ 18 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/AdminResetPwdDTO.java

@@ -0,0 +1,18 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+@Data
+public class AdminResetPwdDTO {
+
+    @NotBlank(message = "手机号不能为空")
+    private String phone;
+
+    @NotBlank(message = "验证码不能为空")
+    private String code;
+
+    @NotBlank(message = "新密码不能为空")
+    private String newPassword;
+}

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

@@ -0,0 +1,21 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+@Data
+public class AdminUpdateChildDTO {
+
+    private String nickname;
+
+    private Integer age;
+
+    private String gender;
+
+    private String birthday;
+
+    private String phone;
+
+    private Integer penaltyEnabled;
+
+    private String theme;
+}

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

@@ -0,0 +1,21 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+@Data
+public class AdminUpdateFamilyDTO {
+
+    private String name;
+
+    private String province;
+
+    private String city;
+
+    private String district;
+
+    private String street;
+
+    private Long streetId;
+
+    private Long teacherId;
+}

+ 29 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/AdminUpdateGuidePackageDTO.java

@@ -0,0 +1,29 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class AdminUpdateGuidePackageDTO {
+
+    private String name;
+
+    private String level;
+
+    private String description;
+
+    private BigDecimal price;
+
+    private Integer validityDays;
+
+    private Long guideId;
+
+    private Long packageId;
+
+    private BigDecimal commissionRate;
+
+    private String status;
+
+    private Integer sortOrder;
+}

Неке датотеке нису приказане због велике количине промена