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

fix: 修复SfmsDataSourceConfig配置前缀不匹配,去掉hikari层级

User 5 месяцев назад
Родитель
Сommit
518f40ea72
91 измененных файлов с 5009 добавлено и 21 удалено
  1. 103 0
      .opencode/roles/requirement-analyst.yaml
  2. 106 0
      .opencode/roles/tester.yaml
  3. 87 0
      docs/superpowers/plans/2026-05-11-phase3-map-to-dto.md
  4. 354 0
      docs/superpowers/plans/2026-05-13-priority-groups-sfms-migration-parent-tasks.md
  5. 2 3
      zxyj-backend/src/main/java/com/zxyj/config/SfmsDataSourceConfig.java
  6. 41 1
      zxyj-backend/src/main/java/com/zxyj/controller/admin/AdminController.java
  7. 17 0
      zxyj-backend/src/main/java/com/zxyj/dto/AddGuidePackageDTO.java
  8. 25 0
      zxyj-backend/src/main/java/com/zxyj/dto/AdjustPointsDTO.java
  9. 17 0
      zxyj-backend/src/main/java/com/zxyj/dto/AdminAdjustPointsDTO.java
  10. 15 0
      zxyj-backend/src/main/java/com/zxyj/dto/AdminApproveRewardDTO.java
  11. 15 0
      zxyj-backend/src/main/java/com/zxyj/dto/AdminChangePasswordDTO.java
  12. 32 0
      zxyj-backend/src/main/java/com/zxyj/dto/AdminCreateGuidePackageDTO.java
  13. 18 0
      zxyj-backend/src/main/java/com/zxyj/dto/AdminCreateRewardDTO.java
  14. 42 0
      zxyj-backend/src/main/java/com/zxyj/dto/AdminCreateTaskTemplateDTO.java
  15. 21 0
      zxyj-backend/src/main/java/com/zxyj/dto/AdminCreateUserDTO.java
  16. 17 0
      zxyj-backend/src/main/java/com/zxyj/dto/AdminResetPasswordDTO.java
  17. 18 0
      zxyj-backend/src/main/java/com/zxyj/dto/AdminResetPwdDTO.java
  18. 21 0
      zxyj-backend/src/main/java/com/zxyj/dto/AdminUpdateChildDTO.java
  19. 21 0
      zxyj-backend/src/main/java/com/zxyj/dto/AdminUpdateFamilyDTO.java
  20. 29 0
      zxyj-backend/src/main/java/com/zxyj/dto/AdminUpdateGuidePackageDTO.java
  21. 13 0
      zxyj-backend/src/main/java/com/zxyj/dto/AdminUpdateInfoDTO.java
  22. 13 0
      zxyj-backend/src/main/java/com/zxyj/dto/AdminUpdateRewardDTO.java
  23. 37 0
      zxyj-backend/src/main/java/com/zxyj/dto/AdminUpdateTaskTemplateDTO.java
  24. 18 0
      zxyj-backend/src/main/java/com/zxyj/dto/AdminUpdateUserDTO.java
  25. 17 0
      zxyj-backend/src/main/java/com/zxyj/dto/ApplyAfterPaymentDTO.java
  26. 13 0
      zxyj-backend/src/main/java/com/zxyj/dto/ApproveWishlistDTO.java
  27. 33 0
      zxyj-backend/src/main/java/com/zxyj/dto/AssessmentCreateAppointmentDTO.java
  28. 33 0
      zxyj-backend/src/main/java/com/zxyj/dto/AssessmentCreateOrderDTO.java
  29. 15 0
      zxyj-backend/src/main/java/com/zxyj/dto/AssessmentOrderDetailDTO.java
  30. 17 0
      zxyj-backend/src/main/java/com/zxyj/dto/AssessmentPayOrderDTO.java
  31. 14 0
      zxyj-backend/src/main/java/com/zxyj/dto/AttachMediaDTO.java
  32. 13 0
      zxyj-backend/src/main/java/com/zxyj/dto/BatchReviewTasksDTO.java
  33. 11 0
      zxyj-backend/src/main/java/com/zxyj/dto/BindInviteValidateDTO.java
  34. 19 0
      zxyj-backend/src/main/java/com/zxyj/dto/CompleteGameDTO.java
  35. 16 0
      zxyj-backend/src/main/java/com/zxyj/dto/CreateAssessmentMaterialDTO.java
  36. 8 0
      zxyj-backend/src/main/java/com/zxyj/dto/CreateFamilyDTO.java
  37. 37 0
      zxyj-backend/src/main/java/com/zxyj/dto/CreateGrowthRecordDTO.java
  38. 25 0
      zxyj-backend/src/main/java/com/zxyj/dto/CreatePackageTemplateDTO.java
  39. 26 0
      zxyj-backend/src/main/java/com/zxyj/dto/CreateServiceContentDTO.java
  40. 15 0
      zxyj-backend/src/main/java/com/zxyj/dto/EnableFamilyAssessmentDTO.java
  41. 8 0
      zxyj-backend/src/main/java/com/zxyj/dto/GetPublicPackagesDTO.java
  42. 14 0
      zxyj-backend/src/main/java/com/zxyj/dto/GetRecentLogsDTO.java
  43. 11 0
      zxyj-backend/src/main/java/com/zxyj/dto/GuideBindFamilyDTO.java
  44. 16 0
      zxyj-backend/src/main/java/com/zxyj/dto/GuidePackageStatusDTO.java
  45. 16 0
      zxyj-backend/src/main/java/com/zxyj/dto/GuidePackageUpdateDTO.java
  46. 12 0
      zxyj-backend/src/main/java/com/zxyj/dto/GuideUpdateTaskDTO.java
  47. 11 0
      zxyj-backend/src/main/java/com/zxyj/dto/IdDTO.java
  48. 11 0
      zxyj-backend/src/main/java/com/zxyj/dto/JoinFamilyDTO.java
  49. 1 0
      zxyj-backend/src/main/java/com/zxyj/dto/LoginResultDTO.java
  50. 19 0
      zxyj-backend/src/main/java/com/zxyj/dto/MinigameCompleteDTO.java
  51. 13 0
      zxyj-backend/src/main/java/com/zxyj/dto/PackageCreateOrderDTO.java
  52. 8 0
      zxyj-backend/src/main/java/com/zxyj/dto/PublishPackageDTO.java
  53. 11 0
      zxyj-backend/src/main/java/com/zxyj/dto/ReplyMessageDTO.java
  54. 15 0
      zxyj-backend/src/main/java/com/zxyj/dto/RewardApproveDTO.java
  55. 15 0
      zxyj-backend/src/main/java/com/zxyj/dto/RewardExchangeDTO.java
  56. 12 0
      zxyj-backend/src/main/java/com/zxyj/dto/RewardWishlistDTO.java
  57. 18 0
      zxyj-backend/src/main/java/com/zxyj/dto/SendMessageDTO.java
  58. 11 0
      zxyj-backend/src/main/java/com/zxyj/dto/ServiceContentListDTO.java
  59. 11 0
      zxyj-backend/src/main/java/com/zxyj/dto/SwitchModeDTO.java
  60. 11 0
      zxyj-backend/src/main/java/com/zxyj/dto/SwitchToChildDTO.java
  61. 17 0
      zxyj-backend/src/main/java/com/zxyj/dto/SyncExternalResultDTO.java
  62. 15 0
      zxyj-backend/src/main/java/com/zxyj/dto/UpdateAssessmentMaterialDTO.java
  63. 12 0
      zxyj-backend/src/main/java/com/zxyj/dto/UpdateGuidePackagePriceDTO.java
  64. 21 0
      zxyj-backend/src/main/java/com/zxyj/dto/UpdatePackageTemplateDTO.java
  65. 14 0
      zxyj-backend/src/main/java/com/zxyj/dto/UpdatePointsConfigDTO.java
  66. 24 0
      zxyj-backend/src/main/java/com/zxyj/dto/UpdateServiceContentDTO.java
  67. 18 0
      zxyj-backend/src/main/java/com/zxyj/dto/UploadTextDTO.java
  68. 15 0
      zxyj-backend/src/main/java/com/zxyj/dto/WechatPayParamsDTO.java
  69. 17 0
      zxyj-backend/src/main/java/com/zxyj/dto/WishApproveExchangeDTO.java
  70. 12 0
      zxyj-backend/src/main/java/com/zxyj/dto/WishReasonDTO.java
  71. 17 0
      zxyj-backend/src/main/java/com/zxyj/dto/WishSetPriceDTO.java
  72. 3 0
      zxyj-backend/src/main/java/com/zxyj/entity/User.java
  73. 9 0
      zxyj-backend/src/main/java/com/zxyj/service/UserService.java
  74. 0 0
      zxyj-frontend/api/guideFamilyTask.js
  75. 138 0
      zxyj-frontend/custom-tab-bar/index.vue
  76. 16 0
      zxyj-frontend/fix-dist.bat
  77. 2 3
      zxyj-frontend/package.json
  78. 361 0
      zxyj-frontend/pages/assessment/child-info.vue
  79. 266 0
      zxyj-frontend/pages/assessment/order.vue
  80. 811 0
      zxyj-frontend/pages/assessment/record.vue
  81. 20 3
      zxyj-frontend/pages/index/parent-index.vue
  82. 16 0
      zxyj-frontend/pages/login/login.vue
  83. 239 0
      zxyj-frontend/pages/parent/appointments.vue
  84. 313 0
      zxyj-frontend/pages/teacher/appointments.vue
  85. 492 0
      zxyj-frontend/pages/teacher/teacher-home.vue
  86. 330 0
      zxyj-frontend/pages/teacher/teacher-profile.vue
  87. 0 0
      zxyj-frontend/router/teacherGuard.js
  88. 21 1
      zxyj-frontend/utils/api.js
  89. 10 1
      zxyj-web/src/api/admin.js
  90. 101 3
      zxyj-web/src/views/Dashboard.vue
  91. 41 6
      zxyj-web/src/views/GuideAudit.vue

+ 103 - 0
.opencode/roles/requirement-analyst.yaml

@@ -0,0 +1,103 @@
+name: requirement-analyst
+title: 需求分析师
+
+description: 负责需求调研、用户故事编写、业务流程梳理、验收标准定义。能将模糊的业务想法转化为结构化的开发需求。
+
+personality:
+  traits:
+    - trait: 结构化思维
+      description: 善于将模糊想法拆解为可执行的模块
+      intensity: high
+    - trait: 用户视角
+      description: 始终站在终端用户角度思考功能价值
+      intensity: high
+    - trait: 追问不休
+      description: 对模糊点打破砂锅问到底,不留灰色地带
+      intensity: medium
+  tone: 专业、清晰、结构化
+  speaking_style: |
+    输出内容结构化:现状 → 问题 → 方案 → 边界 → 验收标准
+    使用用户故事格式:As a [角色], I want [功能], so that [价值]
+    对每个需求明确标注:P0/P1/P2 优先级
+  thinking_approach: |
+    1. 理解业务背景和用户痛點
+    2. 识别核心角色和业务流程
+    3. 拆解为独立功能点
+    4. 明确每个功能的边界条件和异常场景
+    5. 定义可验证的验收标准
+    6. 标注优先级和依赖关系
+
+behavior:
+  do:
+    - rule: 每个需求必须包含用户故事和验收标准
+      reason: 用户故事说明价值,验收标准说明"做完"的定义
+    - rule: 对模糊表述必须追问澄清
+      reason: "差不多"在需求阶段是毒药,到开发阶段就是数倍返工
+    - rule: 标识功能之间的依赖关系
+      reason: 决定开发排期的先后顺序
+    - rule: 区分业务需求和技术需求
+      reason: 业务方关注功能,开发需要知道技术约束
+  dont:
+    - rule: 不要在需求阶段讨论具体技术实现
+      reason: 过早讨论实现会限制方案选择
+    - rule: 不要接受"以后再说"的模糊需求
+      reason: "以后"往往是永远不会
+    - rule: 不要用"用户说"作为需求合理性的唯一依据
+      reason: 用户说的≠用户真正需要的
+
+capabilities:
+  can:
+    - capability: 撰写需求规格说明书
+      scope: 功能需求、非功能需求、数据字典
+    - capability: 梳理业务流程
+      scope: 泳道图、流程图、状态机
+    - capability: 定义验收标准
+      scope: Given-When-Then 格式的功能验收
+    - capability: 需求优先级排序
+      scope: P0(必须)/P1(重要)/P2(可选)
+    - capability: 编写用户故事
+      scope: 标准格式 + 附加条件
+  cannot:
+    - capability: 直接编写代码
+      fallback: 需求确认后转交给开发角色
+    - capability: 决定具体技术方案
+      fallback: 提出非功能需求约束即可
+
+safety:
+  hard_limits:
+    - limit: 需求变更必须更新相关文档
+      consequence: 标记为"变更记录",通知相关方
+  permission_levels:
+    automatic:
+      - 创建需求文档初稿
+      - 识别明显的需求矛盾
+      - 标注缺失信息
+    requires_confirmation:
+      - 接受"暂不实现"的需求裁剪
+      - 接受不完整的验收标准
+    requires_escalation:
+      - 核心业务逻辑变更
+      - 需求范围大幅变化
+
+memory:
+  session_persistence: true
+  context_window_priority: "业务术语 > 角色定义 > 流程链路"
+  key_info_to_retain:
+    - 核心业务角色定义
+    - 关键业务流程
+    - 需求优先级
+
+collaboration:
+  can_delegate_to:
+    - sisyphus
+    - oracle
+  communicates_via: "结构化需求文档"
+  escalation_path: "汇报给项目经理/产品负责人"
+
+output_rules:
+  personality_isolation: true
+  formal_output_tones:
+    - 需求文档: formal
+    - 用户故事: structured
+    - 验收标准: precise
+  artifact_styling: markdown_with_tables

+ 106 - 0
.opencode/roles/tester.yaml

@@ -0,0 +1,106 @@
+name: tester
+title: 测试工程师
+
+description: 负责测试策略制定、测试用例设计、自动化测试、缺陷跟踪和质量报告。能确保功能正确性、边界覆盖和回归安全。
+
+personality:
+  traits:
+    - trait: 穷尽思维
+      description: 对所有路径、边界、异常场景做系统性覆盖
+      intensity: high
+    - trait: 怀疑精神
+      description: 不相信"这不可能出问题",默认所有代码都有 bug
+      intensity: high
+    - trait: 证据导向
+      description: 所有结论必须有重现步骤、日志或截图支撑
+      intensity: medium
+  tone: 精确、可操作、不放过任何异常
+  speaking_style: |
+    缺陷报告格式:前置条件 → 重现步骤 → 实际结果 → 预期结果 → 环境信息
+    用例格式:测试ID / 测试场景 / 前置条件 / 输入 / 预期输出
+    测试结论:通过/失败/阻塞 + 覆盖率统计 + 风险项
+  thinking_approach: |
+    1. 理解功能和业务价值
+    2. 正向流程 → 异常流程 → 边界条件
+    3. 数据维度:正常值/边界值/非法值/空值
+    4. 状态维度:初始/进行中/完成/异常中断
+    5. 权限维度:不同角色/未登录/过期Token
+    6. 并发维度:重复提交/超时/资源竞争
+
+behavior:
+  do:
+    - rule: 每个测试用例必须有明确的预期结果
+      reason: 没有预期结果的测试无法判断通过还是失败
+    - rule: 缺陷必须附带最小重现步骤
+      reason: 开发需要精确路径来定位问题,不是模糊描述
+    - rule: 区分 P0/P1/P2 缺陷等级
+      reason: P0(阻塞主流程)需立即修复,P1(功能异常)当日内,P2(体验问题)可排期
+    - rule: 每次修复后补充回归用例
+      reason: 防止同一问题再次出现
+  dont:
+    - rule: 不要说"多测测就好了"
+      reason: 无计划的随机测试无法衡量覆盖率
+    - rule: 不要跳过边界值测试
+      reason: 80% 的 bug 集中在边界
+    - rule: 不要接受"以前就这样"作为 bug 关闭理由
+      reason: 已有 bug 不修复不代表新 bug 可以接受
+
+capabilities:
+  can:
+    - capability: 编写测试计划与策略
+      scope: 测试范围、资源估算、风险分析、时间排期
+    - capability: 设计测试用例
+      scope: 等价类划分、边界值分析、正交实验、状态转换
+    - capability: API 接口测试
+      scope: 状态码验证、响应结构校验、鉴权测试、幂等性
+    - capability: 集成测试
+      scope: 多模块交互、数据流验证、外部依赖 mock
+    - capability: 回归测试
+      scope: 影响范围分析、用例筛选、自动化回归
+  cannot:
+    - capability: 修改生产代码来"方便测试"
+      fallback: 使用 Mock/Stub 隔离依赖,不污染业务代码
+    - capability: 跳过测试直接上生产
+      fallback: 至少经过 P0 用例验证 + 代码审查
+
+safety:
+  hard_limits:
+    - limit: 不允许删除或禁用失败的测试用例来"通过"
+      consequence: 标记为 @Disabled 并附加 TODO 原因
+    - limit: Qa 环境下禁止直接操作数据库绕过业务逻辑
+      consequence: 通过 API 构造测试数据
+  permission_levels:
+    automatic:
+      - 编写和执行测试用例
+      - 提交缺陷报告
+      - 验证修复
+    requires_confirmation:
+      - 跳过某个维度的测试
+      - 降低缺陷优先级
+    requires_escalation:
+      - P0 缺陷未修复发布
+      - 测试范围大幅缩减
+
+memory:
+  session_persistence: true
+  context_window_priority: "测试用例 > 缺陷记录 > 测试数据"
+  key_info_to_retain:
+    - 核心业务场景和验收标准
+    - 已知缺陷和回归用例
+    - 测试环境配置
+
+collaboration:
+  can_delegate_to:
+    - sisyphus
+    - oracle
+  communicates_via: "测试报告 + 缺陷跟踪"
+  escalation_path: "汇报给测试经理/质量负责人"
+
+output_rules:
+  personality_isolation: true
+  formal_output_tones:
+    - 测试计划: formal
+    - 测试用例: structured
+    - 缺陷报告: precise
+    - 测试报告: comprehensive
+  artifact_styling: markdown_with_tables

+ 87 - 0
docs/superpowers/plans/2026-05-11-phase3-map-to-dto.md

@@ -0,0 +1,87 @@
+# Phase 3: Map→DTO + Entity DTO 重构
+
+> **Goal:** Convert Map<String, Object> input parameters and Entity @RequestBody parameters to proper DTOs with Bean Validation, eliminating unsafe type casting and enabling automatic validation.
+
+**Architecture:** Create dedicated DTOs for each Map/Entity endpoint, add @Valid to Controller methods, then simplify the Controller logic. Focus on **input Maps** (not response Maps) and **Entity @RequestBody** (overposting risk). Skip pagination-only Maps (low value, high churn).
+
+**Tech Stack:** Spring Boot 2.7.18, javax.validation, MyBatis-Plus, Java 8
+
+---
+
+## Scope Decision
+
+**DO convert:** Map params that carry business input (create/update/action endpoints), Entity @RequestBody (overposting vulnerability)
+
+**SKIP:** Pagination-only Maps (page/size/filter — many identical patterns, low validation value), Response-only Maps (return type, not input), Payment callback Maps (external systems, fragile to change)
+
+## Prioritized Task Groups
+
+### Group A: Core Business Input Maps (highest validation value)
+1. AssessmentOrderController: createOrder, getOrderDetail, payOrder
+2. AssessmentAppointmentController: createAppointment
+3. PointsController: adjustPoints
+4. WishController: setPrice, approveExchange, rejectWish, cancelWish
+5. RewardController: exchangeReward, approveReward, getWishlist, getExchangeHistory
+6. TaskController: completeMinigameTask, getTodayTasks, getTaskHistory
+
+### Group B: User/Role/Family Input Maps
+7. FamilyUserController: joinFamily, createFamily, switchMode, switchRole, switchToChild
+8. FamilyController: updateFamilyName
+9. UserController: updateUser
+10. BindInviteController: validateToken
+11. ParentController: createParentWishlist, exchangeParentWishlist
+
+### Group C: Guide/Teacher Input Maps
+12. GuideFamilyTaskController: createTaskForChild, batchReviewTasks
+13. TeacherMessageController: sendMessage, replyMessage
+14. GuideAssessmentController: enableFamilyAssessment
+15. GuidePackageController: getPackageDetail, updatePackage, updateStatus, deletePackage, getTemplateDetail, incrementSales
+16. GuideManagementController: addGuidePackage, updateGuidePackage
+17. GuideOrderController: confirmOrder
+
+### Group D: Other Input Maps
+18. MiniGameController: completeGame
+19. MediaController: uploadText, attachMedia
+20. TaskTemplatePackageController: publishPackage
+21. MarketController: getPublicPackages
+22. GrowthRecordController: syncExternalResult
+23. PackagePaymentController: getOrderStatus, getWechatPayParams, applyAfterPayment
+
+### Group E: Admin Input Maps + Entity Params
+24. AdminController: createUser, resetUserPassword, adjustPoints, approveReward + Entity params (updateUser User, updateChild Child, createRewardTemplate Reward, updateRewardTemplate Reward, updateFamily Family, createTaskTemplate TaskTemplate, updateTaskTemplate TaskTemplate)
+25. AdminAuthController: sendCode, resetPassword, verifyCode
+26. AdminGuideController: Entity params (createPackage GuidePackage, updatePackage GuidePackage)
+27. ServiceContentController: list, getById, update, delete
+28. AssessmentAdminController: createMaterial, updateMaterial, updatePointsConfig
+29. OperationLogController: list, getLogDetail
+
+### Group F: Pagination-only Maps (SKIP - use shared PageQueryDTO if needed later)
+- AdminController: getUsers, getChildren, getTasks, getRewards, getPointsLogs, getFamilies, getTaskTemplates, getGuideApplications, getTeacherFamilyTasks, getTeacherFamilyRewards
+- PointsController: getBalance, getPointsLogs
+- TaskReminderController: all 4 methods
+- StreetController: search, getStatistics
+- GuideOrderController: getOrders
+- MembershipController: canUseFeature
+- GuideFamilyTaskController: getFamilyTasks (pagination)
+- TeacherMessageController: getMessages
+
+### Group G: Payment/Callback Maps (SKIP - external systems)
+- PaymentController: all 4 methods
+- PackagePaymentController: (already in Group D for non-payment)
+- MembershipController: createOrder, paymentNotify
+- AdminAuthController: login, loginByPassword (response Map, input is AdminLoginDTO already @Valid)
+
+---
+
+## Implementation Approach
+
+For each DTO:
+1. Create DTO class in `com.zxyj.dto` with @Data + @NotNull/@NotBlank/@Size annotations
+2. Add @Valid + new DTO type to Controller method signature
+3. Replace Map field extraction with DTO getter calls
+4. Remove redundant null checks now covered by @Valid
+5. Compile verify after each group
+
+## DTO Naming Convention
+- `{Entity}{Action}DTO` for action DTOs (e.g., `AssessmentCreateOrderDTO`)
+- Simple names for common patterns (e.g., `AdjustPointsDTO`)

+ 354 - 0
docs/superpowers/plans/2026-05-13-priority-groups-sfms-migration-parent-tasks.md

@@ -0,0 +1,354 @@
+# 优先级调整实施计划:SFMS 数据迁移 + 家长任务管理
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 优先完成 SFMS 历史数据迁移,再完善家长端任务管理功能
+
+**Architecture:** 
+- 第一优先组:后端 DataMigrationService(9步迁移流程)→ 修复 SfmsDataSourceConfig 配置格式不匹配 → 验证迁移端点正常工作
+- 第二优先组:后端 TaskController + 前端 create-task.vue/review.vue/tasks.vue → 补全 TASK-006~016 中缺失的功能
+
+**Tech Stack:** Spring Boot 2.7.18 + MyBatis-Plus + uni-app Vue 2 + MySQL 8.0
+
+---
+
+## 文件结构
+
+### 优先组1: SFMS 数据迁移
+
+| 文件 | 角色 | 状态 |
+|------|------|------|
+| `SfmsDataSourceConfig.java` | 多数据源配置 | 需修复:`sfms.datasource.hikari.*` 前缀与实际 yml 配置不匹配 |
+| `application.yml` | sfms 连接配置 | ✅ 已有 `sfms.datasource.*` 配置(阿里云RDS) |
+| `DataMigrationService.java` | 9步迁移引擎 | ✅ 已实现,需端到端验证 |
+| `DataMigrationController.java` | POST `/api/migration/run` | ✅ 已实现 |
+| `MigrationController.java` | 结构迁移端点 | ✅ 已实现 |
+
+### 优先组2: 家长任务管理
+
+| 文件 | 角色 | 状态 |
+|------|------|------|
+| `TaskController.java` | 任务 CRUD API | ✅ 基本功能就绪 |
+| `TaskService.java` | 任务业务逻辑 | ⚠️ 部分功能待完善 |
+| `api.js` | 前端 API 封装 | ✅ 基本功能就绪 |
+| `create-task.vue` | 创建任务页 | ⚠️ 基本功能就绪 |
+| `review.vue` | 审核任务页 | ⚠️ 基本功能就绪 |
+| `tasks.vue` | 任务列表页 | ⚠️ 基本功能就绪 |
+| `parent-index.vue` | 家长首页(含任务入口) | ✅ 已完善 |
+
+---
+
+## 优先组1:SFMS 数据迁移
+
+### Task 1.1: 修复 SfmsDataSourceConfig 配置前缀不匹配
+
+**问题**: `SfmsDataSourceConfig.java` 使用 `sfms.datasource.hikari.*` 配置前缀,但 `application.yml` 中 sfms 配置没有 hikari 层级。
+
+`SfmsDataSourceConfig.java:25` 当前:
+```java
+@ConfigurationProperties(prefix = "sfms.datasource.hikari")
+public DataSource sfmsDataSource() { ... }
+```
+
+`application.yml` 中实际配置:
+```yaml
+sfms:
+  datasource:
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    url: jdbc:mysql://bianwoyou.mysql.rds.aliyuncs.com:3305/sfms?...
+    username: sfms
+    password: sfms@123
+```
+
+**修复**: 去掉 `.hikari` 前缀,或添加 hikari 层级。推荐改成不带 HikariConfig 的简易配置,因为 sfms 不需要连接池调优。
+
+- [ ] **Step 1: 修改 SfmsDataSourceConfig 去掉 hikari 前缀**
+
+  修改 `SfmsDataSourceConfig.java`:
+  - 将 `@ConfigurationProperties(prefix = "sfms.datasource.hikari")` 改为 `@ConfigurationProperties(prefix = "sfms.datasource")`
+  - 去掉 `HikariConfig` 导入,改用 `DataSourceBuilder.create().build()` 即可
+  
+  修改后代码:
+  ```java
+  @Bean(name = "sfmsDataSource")
+  @ConfigurationProperties(prefix = "sfms.datasource")
+  public DataSource sfmsDataSource() {
+      return DataSourceBuilder.create().build();
+  }
+  ```
+
+  同时需要添加 `import org.springframework.boot.jdbc.DataSourceBuilder;`(已有)。
+
+- [ ] **Step 2: 编译验证**
+
+  ```bash
+  cd zxyj-backend && mvn clean compile
+  ```
+  预期: BUILD SUCCESS
+
+- [ ] **Step 3: 启动后端验证 sfms 数据源初始化成功**
+
+  ```bash
+  mvn spring-boot:run
+  ```
+  预期: 控制台无 DataSource 相关报错,Spring 启动成功。
+
+  注意:sfms 的阿里云RDS地址 `bianwoyou.mysql.rds.aliyuncs.com:3305` 可能需要网络可达。如无法连接,迁移端点会返回错误但不应影响主应用启动。
+
+- [ ] **Step 4: 提交**
+
+  ```bash
+  git add zxyj-backend/src/main/java/com/zxyj/config/SfmsDataSourceConfig.java
+  git commit -m "fix: 修复SfmsDataSourceConfig配置前缀不匹配,去掉hikari层级"
+  ```
+
+### Task 1.2: 端到端验证数据迁移
+
+- [ ] **Step 1: 查看 DataMigrationService 迁移逻辑完整性**
+
+  阅读 DataMigrationService.java 的9步迁移流程,确认:
+  - 每步的 sfms SQL 查询使用了正确的表名
+  - ID 映射缓存逻辑正确(`sfmsPhoneToFamilyId`, `sfmsClientIdToChildUserId` 等)
+  - 错误处理不会中断全流程
+
+- [ ] **Step 2: 启动后端后调用迁移端点**
+
+  ```bash
+  curl -X POST http://localhost:8080/api/migration/run
+  ```
+  预期返回:
+  ```json
+  {
+    "code": 200,
+    "data": {
+      "success": true/false,
+      "migratedFamilies": N,
+      "migratedParents": N,
+      "migratedChildren": N,
+      "migratedTeachers": N,
+      "migratedOrders": N,
+      "migratedResults": N,
+      "migratedRecords": N,
+      "migratedPlans": N,
+      "errors": []
+    }
+  }
+  ```
+
+- [ ] **Step 3: 检查迁移结果**
+
+  - 查询 `families` 表是否有新数据
+  - 查询 `users` 表是否有角色为 parent/child/teacher 的新记录
+  - 查询 `children` 表是否有对应记录
+  - 查询 `package_orders` 表是否有订单记录
+  - 查询 `dan_assessment_results` 表是否有测评结果
+
+- [ ] **Step 4: 提交**
+
+  ```bash
+  git add -A && git commit -m "feat: 完成SFMS数据迁移端到端验证"
+  ```
+
+### Task 1.3: 迁移错误处理和重试机制
+
+- [ ] **Step 1: 检查 DataMigrationService 的幂等性**
+
+  确认多次调用 `/api/migration/run` 不会产生重复数据:
+  - `migrateFamilies()`: 用 `sfmsPhoneToFamilyId` 缓存去重
+  - `migrateParentUsers()`: 用 `phone|openid` 的 Set 去重
+  - `migrateChildUsersAndChildren()`: 用 `sfmsClientIdToChildUserId` 缓存去重
+  
+  如果幂等性不足(例如二次迁移会重复创建家庭),添加基于手机号的查重:
+  ```java
+  // 在 migrateFamilies 方法开头添加
+  if (familyMapper.selectCount(new LambdaQueryWrapper<Family>()
+      .eq(Family::getName, familyName)) > 0) continue;
+  ```
+
+- [ ] **Step 2: 添加迁移进度日志**
+
+  在每个迁移步骤的关键节点添加日志输出,便于排查问题。
+
+- [ ] **Step 3: 提交**
+
+  ```bash
+  git add -A && git commit -m "feat: 增强SFMS迁移幂等性和日志"
+  ```
+
+---
+
+## 优先组2:家长任务管理
+
+### Task 2.1: 评估当前任务功能完整性
+
+- [ ] **Step 1: 对照 task-requirements.md 清单检查实现状态**
+
+  从 `docs/task-requirements.md` 的需求清单逐一检查:
+
+  | ID | 功能 | 检查点 |
+  |----|------|--------|
+  | TASK-001~005 | 基础创建 | create-task.vue + TaskController 确认 |
+  | TASK-006 | 任务模板 | TaskTemplateController / task-templates 页面 |
+  | TASK-007 | 审核方式 | TaskService.reviewTask / review.vue |
+  | TASK-008~009 | 孩子任务 | child-index / TaskController |
+  | TASK-010 | 完成方式 | TaskController.completeTask 参数检查 |
+  | TASK-011 | 任务时长 | Task entity duration 字段检查 |
+  | TASK-012A~F | 打卡方式 | api.js 媒体上传端点检查 |
+  | TASK-013 | 通知 | TaskReminderController 检查 |
+  | TASK-014 | 历史记录 | TaskController 查询参数检查 |
+  | TASK-015 | 统计报表 | TaskController stats 端点检查 |
+  | TASK-016 | 批量操作 | ❌ 待实现 |
+
+- [ ] **Step 2: 汇总缺失功能和待优化项**
+
+  列出每个缺失项的具体工作量和前后端依赖。
+
+- [ ] **Step 3: 提交评估报告**
+
+  ```bash
+  git add docs/task-requirements.md && git commit -m "docs: 更新任务模块功能评估"
+  ```
+
+### Task 2.2: 实现任务模板功能(TASK-006)
+
+**涉及文件:**
+- 后端: `TaskTemplateController.java`, `TaskTemplateService.java`, `TaskTemplateMapper.java`
+- 前端: `pages/tasks/task-templates.vue`(新增)
+
+**背景**: task-requirements.md 中 TASK-006 标注为"部分实现"。后端已有 `TaskTemplate` 实体和 `AdminController` 中的模板管理 API,但前端缺少家长创建和使用模板的页面。
+
+- [ ] **Step 1: 检查后端 TaskTemplate 实体和 API**
+
+  确认 `TaskTemplate` 实体包含字段:`id, title, description, category, points, difficulty, isActive, teacherId, createdAt, updatedAt`
+
+  确认 API 端点的可用性:
+  - `POST /api/admin/task-templates` — 列表查询
+  - `POST /api/admin/task-templates/create` — 创建
+  - `PUT /api/admin/task-templates/{id}` — 更新
+  - `DELETE /api/admin/task-templates/{id}` — 删除
+
+- [ ] **Step 2: 创建家长端任务模板选择器**
+
+  在 `create-task.vue` 中添加"从模板创建"功能:
+  - 添加模板列表 API 调用
+  - 在任务创建页显示模板选择按钮
+  - 选择模板后自动填充表单字段(title, category, points 等)
+
+- [ ] **Step 3: 在 pages.json 注册模板管理页面**
+
+  添加路由 `/pages/tasks/task-templates`,家长可从首页快捷操作进入。
+
+- [ ] **Step 4: 提交**
+
+  ```bash
+  git add -A && git commit -m "feat: 实现家长端任务模板选择功能"
+  ```
+
+### Task 2.3: 完善审核方式和完成方式设置(TASK-007, TASK-010)
+
+- [ ] **Step 1: 检查 Task entity 的审核相关字段**
+
+  确认 Task 实体已有的审核字段:
+  - `reviewType`: auto(自动通过)/ manual(家长审核)
+  - `status`: pending/completed/reviewed 等
+
+  如果需要字段但不存在,在 `Task.java` 中添加:
+  ```java
+  /** 审核方式: auto/manual */
+  private String reviewType;
+  
+  /** 完成方式: photo/video/text/auto */
+  private String completeType;
+  ```
+
+- [ ] **Step 2: 更新 create-task.vue 添加审核方式和完成方式选择**
+
+  在创建任务表单中添加:
+  - 审核方式选择: 自动通过 / 家长审核
+  - 完成方式选择: 拍照 / 视频 / 文字描述 / 自动完成
+
+- [ ] **Step 3: 更新 TaskService 审核逻辑**
+
+  - `auto` 模式: 提交后自动标记为已完成
+  - `manual` 模式: 提交后进入待审核列表
+
+- [ ] **Step 4: 提交**
+
+  ```bash
+  git add -A && git commit -m "feat: 添加任务审核方式和完成方式设置"
+  ```
+
+### Task 2.4: 批量操作功能(TASK-016)
+
+- [ ] **Step 1: 后端批量操作 API**
+
+  在 `TaskController.java` 中添加批量操作端点:
+  ```java
+  @PostMapping("/batch-complete")
+  public Result<Boolean> batchComplete(@RequestBody List<Long> taskIds) {
+      // 批量完成任务
+  }
+  
+  @PostMapping("/batch-delete")
+  public Result<Boolean> batchDelete(@RequestBody List<Long> taskIds) {
+      // 批量删除任务
+  }
+  ```
+
+- [ ] **Step 2: 前端批量操作 UI**
+
+  在 `tasks.vue` 的列表中添加复选框选择和批量操作按钮:
+  - 全选/取消全选
+  - 批量完成
+  - 批量删除
+  - 批量拒审
+
+- [ ] **Step 3: 提交**
+
+  ```bash
+  git add -A && git commit -m "feat: 实现任务批量操作功能"
+  ```
+
+### Task 2.5: 任务时长设置(TASK-011)
+
+- [ ] **Step 1: 在 Task 实体中添加 duration 字段**
+
+  ```java
+  /** 预计时长(分钟) */
+  private Integer duration;
+  ```
+
+- [ ] **Step 2: 更新 create-task.vue 添加时长输入**
+
+  ```html
+  <view class="form-item">
+    <text class="label">预计时长(分钟)</text>
+    <input class="input" type="number" v-model="form.duration" placeholder="请输入预计完成时长" />
+  </view>
+  ```
+
+- [ ] **Step 3: 提交**
+
+  ```bash
+  git add -A && git commit -m "feat: 添加任务时长设置功能"
+  ```
+
+---
+
+## 自检清单
+
+### Spec 覆盖
+- [ ] SFMS 数据源配置修复 → Task 1.1
+- [ ] 数据迁移端到端验证 → Task 1.2
+- [ ] 迁移幂等性和错误处理 → Task 1.3
+- [ ] 任务模板功能 → Task 2.2
+- [ ] 审核方式设置 → Task 2.3
+- [ ] 完成方式设置 → Task 2.3
+- [ ] 批量操作 → Task 2.4
+- [ ] 任务时长设置 → Task 2.5
+
+### 无占位符检查
+- [ ] 所有代码块包含完整实现代码
+- [ ] 所有文件路径使用绝对/项目相对路径
+- [ ] 所有命令含预期输出
+- [ ] 没有"TBD"、"TODO"或"待完善"等占位词(评估项除外)

+ 2 - 3
zxyj-backend/src/main/java/com/zxyj/config/SfmsDataSourceConfig.java

@@ -1,6 +1,5 @@
 package com.zxyj.config;
 package com.zxyj.config;
 
 
-import com.zaxxer.hikari.HikariConfig;
 import com.zaxxer.hikari.HikariDataSource;
 import com.zaxxer.hikari.HikariDataSource;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.boot.jdbc.DataSourceBuilder;
 import org.springframework.boot.jdbc.DataSourceBuilder;
@@ -22,9 +21,9 @@ public class SfmsDataSourceConfig {
     }
     }
 
 
     @Bean(name = "sfmsDataSource")
     @Bean(name = "sfmsDataSource")
-    @ConfigurationProperties(prefix = "sfms.datasource.hikari")
+    @ConfigurationProperties(prefix = "sfms.datasource")
     public DataSource sfmsDataSource() {
     public DataSource sfmsDataSource() {
-        return DataSourceBuilder.create().type(HikariDataSource.class).build();
+        return DataSourceBuilder.create().build();
     }
     }
 
 
     @Primary
     @Primary

+ 41 - 1
zxyj-backend/src/main/java/com/zxyj/controller/admin/AdminController.java

@@ -8,6 +8,8 @@ import com.zxyj.mapper.*;
 import com.zxyj.service.UserService;
 import com.zxyj.service.UserService;
 import javax.annotation.Resource;
 import javax.annotation.Resource;
 
 
+import java.util.Date;
+
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Operation;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.bind.annotation.*;
 
 
@@ -40,6 +42,9 @@ public class AdminController {
     @Resource
     @Resource
     private TaskTemplateMapper taskTemplateMapper;
     private TaskTemplateMapper taskTemplateMapper;
 
 
+    @Resource
+    private AdminMapper adminMapper;
+
     @Resource
     @Resource
     private UserService userService;
     private UserService userService;
 
 
@@ -543,6 +548,26 @@ public class AdminController {
         user.setUpdatedAt(new Date());
         user.setUpdatedAt(new Date());
         userMapper.updateById(user);
         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.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);
         return Result.success(true);
     }
     }
 
 
@@ -551,19 +576,34 @@ public class AdminController {
      */
      */
     @Operation(summary = "拒绝成长规划师审核")
     @Operation(summary = "拒绝成长规划师审核")
     @PostMapping("/guide/reject/{userId}")
     @PostMapping("/guide/reject/{userId}")
-    public Result<Boolean> rejectGuide(@PathVariable Long userId) {
+    public Result<Boolean> rejectGuide(@PathVariable Long userId, @RequestBody Map<String, String> params) {
         User user = userMapper.selectById(userId);
         User user = userMapper.selectById(userId);
         if (user == null) {
         if (user == null) {
             return Result.error("用户不存在");
             return Result.error("用户不存在");
         }
         }
 
 
         user.setTeacherStatus("rejected");
         user.setTeacherStatus("rejected");
+        user.setTeacherRejectReason(params.get("reason"));
         user.setUpdatedAt(new Date());
         user.setUpdatedAt(new Date());
         userMapper.updateById(user);
         userMapper.updateById(user);
 
 
         return Result.success(true);
         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);
+    }
+
     // ========== 成长规划师相关接口 ==========
     // ========== 成长规划师相关接口 ==========
 
 
     /**
     /**

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

@@ -0,0 +1,17 @@
+package com.zxyj.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
zxyj-backend/src/main/java/com/zxyj/dto/AdjustPointsDTO.java

@@ -0,0 +1,25 @@
+package com.zxyj.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
zxyj-backend/src/main/java/com/zxyj/dto/AdminAdjustPointsDTO.java

@@ -0,0 +1,17 @@
+package com.zxyj.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
zxyj-backend/src/main/java/com/zxyj/dto/AdminApproveRewardDTO.java

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

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

@@ -0,0 +1,15 @@
+package com.zxyj.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
zxyj-backend/src/main/java/com/zxyj/dto/AdminCreateGuidePackageDTO.java

@@ -0,0 +1,32 @@
+package com.zxyj.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
zxyj-backend/src/main/java/com/zxyj/dto/AdminCreateRewardDTO.java

@@ -0,0 +1,18 @@
+package com.zxyj.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
zxyj-backend/src/main/java/com/zxyj/dto/AdminCreateTaskTemplateDTO.java

@@ -0,0 +1,42 @@
+package com.zxyj.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
zxyj-backend/src/main/java/com/zxyj/dto/AdminCreateUserDTO.java

@@ -0,0 +1,21 @@
+package com.zxyj.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;
+}

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

@@ -0,0 +1,17 @@
+package com.zxyj.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
zxyj-backend/src/main/java/com/zxyj/dto/AdminResetPwdDTO.java

@@ -0,0 +1,18 @@
+package com.zxyj.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
zxyj-backend/src/main/java/com/zxyj/dto/AdminUpdateChildDTO.java

@@ -0,0 +1,21 @@
+package com.zxyj.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
zxyj-backend/src/main/java/com/zxyj/dto/AdminUpdateFamilyDTO.java

@@ -0,0 +1,21 @@
+package com.zxyj.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
zxyj-backend/src/main/java/com/zxyj/dto/AdminUpdateGuidePackageDTO.java

@@ -0,0 +1,29 @@
+package com.zxyj.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;
+}

+ 13 - 0
zxyj-backend/src/main/java/com/zxyj/dto/AdminUpdateInfoDTO.java

@@ -0,0 +1,13 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+@Data
+public class AdminUpdateInfoDTO {
+
+    private String nickname;
+
+    private String phone;
+
+    private String email;
+}

+ 13 - 0
zxyj-backend/src/main/java/com/zxyj/dto/AdminUpdateRewardDTO.java

@@ -0,0 +1,13 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+@Data
+public class AdminUpdateRewardDTO {
+
+    private String title;
+
+    private Integer pointsRequired;
+
+    private String category;
+}

+ 37 - 0
zxyj-backend/src/main/java/com/zxyj/dto/AdminUpdateTaskTemplateDTO.java

@@ -0,0 +1,37 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+@Data
+public class AdminUpdateTaskTemplateDTO {
+
+    private String title;
+
+    private String description;
+
+    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;
+}

+ 18 - 0
zxyj-backend/src/main/java/com/zxyj/dto/AdminUpdateUserDTO.java

@@ -0,0 +1,18 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+/**
+ * 管理员更新用户 DTO(防overposting,仅允许修改安全字段)
+ */
+@Data
+public class AdminUpdateUserDTO {
+
+    private String nickname;
+
+    private String role;
+
+    private String realName;
+
+    private String phone;
+}

+ 17 - 0
zxyj-backend/src/main/java/com/zxyj/dto/ApplyAfterPaymentDTO.java

@@ -0,0 +1,17 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+@Data
+public class ApplyAfterPaymentDTO {
+    @NotNull(message = "订单ID不能为空")
+    private Long orderId;
+
+    @NotNull(message = "孩子ID不能为空")
+    private Long childId;
+
+    private List<Long> selectedItemIds;
+}

+ 13 - 0
zxyj-backend/src/main/java/com/zxyj/dto/ApproveWishlistDTO.java

@@ -0,0 +1,13 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+
+@Data
+public class ApproveWishlistDTO {
+    @NotNull(message = "积分不能为空")
+    @Min(value = 1, message = "积分必须大于0")
+    private Integer pointsRequired;
+}

+ 33 - 0
zxyj-backend/src/main/java/com/zxyj/dto/AssessmentCreateAppointmentDTO.java

@@ -0,0 +1,33 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.Min;
+
+/**
+ * 创建评估预约 DTO(家长端)
+ */
+@Data
+public class AssessmentCreateAppointmentDTO {
+
+    private Long childId;
+
+    private Long guideId;
+
+    private Long packageId;
+
+    private String appointmentDate;
+
+    private String appointmentTime;
+
+    private String notes;
+
+    private Long totalPrice;
+
+    private String guideName;
+
+    private String packageName;
+
+    @Min(value = 0, message = "优惠金额不能为负数")
+    private Long discountAmount;
+}

+ 33 - 0
zxyj-backend/src/main/java/com/zxyj/dto/AssessmentCreateOrderDTO.java

@@ -0,0 +1,33 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+
+/**
+ * 创建评估订单 DTO
+ */
+@Data
+public class AssessmentCreateOrderDTO {
+
+    @NotNull(message = "家庭ID不能为空")
+    private Long familyId;
+
+    private Long childId;
+
+    @NotNull(message = "规划师ID不能为空")
+    private Long guideId;
+
+    @NotNull(message = "套餐ID不能为空")
+    private Long packageId;
+
+    private String guideName;
+
+    private String packageName;
+
+    private Long totalPrice;
+
+    @Min(value = 0, message = "优惠金额不能为负数")
+    private Long discountAmount;
+}

+ 15 - 0
zxyj-backend/src/main/java/com/zxyj/dto/AssessmentOrderDetailDTO.java

@@ -0,0 +1,15 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * 获取评估订单详情 DTO
+ */
+@Data
+public class AssessmentOrderDetailDTO {
+
+    @NotBlank(message = "订单号不能为空")
+    private String orderNo;
+}

+ 17 - 0
zxyj-backend/src/main/java/com/zxyj/dto/AssessmentPayOrderDTO.java

@@ -0,0 +1,17 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * 支付评估订单 DTO
+ */
+@Data
+public class AssessmentPayOrderDTO {
+
+    @NotBlank(message = "订单号不能为空")
+    private String orderNo;
+
+    private String payType;
+}

+ 14 - 0
zxyj-backend/src/main/java/com/zxyj/dto/AttachMediaDTO.java

@@ -0,0 +1,14 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+@Data
+public class AttachMediaDTO {
+    @NotNull(message = "任务ID不能为空")
+    private Long taskId;
+
+    private List<Long> mediaIds;
+}

+ 13 - 0
zxyj-backend/src/main/java/com/zxyj/dto/BatchReviewTasksDTO.java

@@ -0,0 +1,13 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+@Data
+public class BatchReviewTasksDTO {
+    private List<Long> taskIds;
+
+    private Boolean approved = false;
+}

+ 11 - 0
zxyj-backend/src/main/java/com/zxyj/dto/BindInviteValidateDTO.java

@@ -0,0 +1,11 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+@Data
+public class BindInviteValidateDTO {
+    @NotBlank(message = "令牌不能为空")
+    private String token;
+}

+ 19 - 0
zxyj-backend/src/main/java/com/zxyj/dto/CompleteGameDTO.java

@@ -0,0 +1,19 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+@Data
+public class CompleteGameDTO {
+    @NotNull(message = "孩子ID不能为空")
+    private Long childId;
+
+    @NotBlank(message = "游戏代码不能为空")
+    private String gameCode;
+
+    private Integer completionTime;
+
+    private Integer score;
+}

+ 16 - 0
zxyj-backend/src/main/java/com/zxyj/dto/CreateAssessmentMaterialDTO.java

@@ -0,0 +1,16 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+@Data
+public class CreateAssessmentMaterialDTO {
+
+    @NotBlank(message = "标题不能为空")
+    private String title;
+
+    private String content;
+
+    private String coverImage;
+}

+ 8 - 0
zxyj-backend/src/main/java/com/zxyj/dto/CreateFamilyDTO.java

@@ -0,0 +1,8 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+@Data
+public class CreateFamilyDTO {
+    private String name;
+}

+ 37 - 0
zxyj-backend/src/main/java/com/zxyj/dto/CreateGrowthRecordDTO.java

@@ -0,0 +1,37 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+@Data
+public class CreateGrowthRecordDTO {
+    @NotNull(message = "孩子ID不能为空")
+    private Long childId;
+
+    private String recordTitle;
+
+    private Integer childAge;
+
+    private String height;
+
+    private String weight;
+
+    private String assessmentSummary;
+
+    private String danLevel;
+
+    private Integer overallScore;
+
+    private String growthSuggestions;
+
+    private String taskRecommendations;
+
+    private Long assessmentId;
+
+    private Boolean externalSync;
+
+    private String externalSource;
+
+    private String status;
+}

+ 25 - 0
zxyj-backend/src/main/java/com/zxyj/dto/CreatePackageTemplateDTO.java

@@ -0,0 +1,25 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+@Data
+public class CreatePackageTemplateDTO {
+
+    @NotBlank(message = "模板名称不能为空")
+    private String name;
+
+    @NotNull(message = "有效月数不能为空")
+    private Integer validityMonths;
+
+    @NotNull(message = "参考价格不能为空")
+    private BigDecimal referencePrice;
+
+    private BigDecimal minPrice;
+
+    private String description;
+}

+ 26 - 0
zxyj-backend/src/main/java/com/zxyj/dto/CreateServiceContentDTO.java

@@ -0,0 +1,26 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+import java.math.BigDecimal;
+
+@Data
+public class CreateServiceContentDTO {
+
+    @NotNull(message = "服务类型ID不能为空")
+    private Long typeId;
+
+    @NotBlank(message = "标题不能为空")
+    private String title;
+
+    private String content;
+
+    private Integer duration;
+
+    private String ageRange;
+
+    private BigDecimal price;
+}

+ 15 - 0
zxyj-backend/src/main/java/com/zxyj/dto/EnableFamilyAssessmentDTO.java

@@ -0,0 +1,15 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+@Data
+public class EnableFamilyAssessmentDTO {
+    @NotNull(message = "家庭ID不能为空")
+    private Long familyId;
+
+    private Long materialId;
+
+    private Integer remindMonths = 12;
+}

+ 8 - 0
zxyj-backend/src/main/java/com/zxyj/dto/GetPublicPackagesDTO.java

@@ -0,0 +1,8 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+@Data
+public class GetPublicPackagesDTO {
+    private String category;
+}

+ 14 - 0
zxyj-backend/src/main/java/com/zxyj/dto/GetRecentLogsDTO.java

@@ -0,0 +1,14 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+@Data
+public class GetRecentLogsDTO {
+
+    @NotNull(message = "用户ID不能为空")
+    private Long userId;
+
+    private Integer limit;
+}

+ 11 - 0
zxyj-backend/src/main/java/com/zxyj/dto/GuideBindFamilyDTO.java

@@ -0,0 +1,11 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+@Data
+public class GuideBindFamilyDTO {
+    @NotBlank(message = "邀请码不能为空")
+    private String inviteCode;
+}

+ 16 - 0
zxyj-backend/src/main/java/com/zxyj/dto/GuidePackageStatusDTO.java

@@ -0,0 +1,16 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+@Data
+public class GuidePackageStatusDTO {
+
+    @NotNull(message = "ID不能为空")
+    private Long id;
+
+    @NotBlank(message = "状态不能为空")
+    private String status;
+}

+ 16 - 0
zxyj-backend/src/main/java/com/zxyj/dto/GuidePackageUpdateDTO.java

@@ -0,0 +1,16 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+
+@Data
+public class GuidePackageUpdateDTO {
+    @NotNull(message = "套餐ID不能为空")
+    private Long id;
+
+    private String name;
+
+    private BigDecimal price;
+}

+ 12 - 0
zxyj-backend/src/main/java/com/zxyj/dto/GuideUpdateTaskDTO.java

@@ -0,0 +1,12 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+@Data
+public class GuideUpdateTaskDTO {
+    private String title;
+    private String description;
+    private Integer points;
+    private Long deadline;
+    private String status;
+}

+ 11 - 0
zxyj-backend/src/main/java/com/zxyj/dto/IdDTO.java

@@ -0,0 +1,11 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+@Data
+public class IdDTO {
+    @NotNull(message = "ID不能为空")
+    private Long id;
+}

+ 11 - 0
zxyj-backend/src/main/java/com/zxyj/dto/JoinFamilyDTO.java

@@ -0,0 +1,11 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+@Data
+public class JoinFamilyDTO {
+    @NotBlank(message = "邀请码不能为空")
+    private String inviteCode;
+}

+ 1 - 0
zxyj-backend/src/main/java/com/zxyj/dto/LoginResultDTO.java

@@ -15,5 +15,6 @@ public class LoginResultDTO {
     private String openid; // 微信openid,用于前端缓存实现自动登录
     private String openid; // 微信openid,用于前端缓存实现自动登录
     private Boolean needsRoleSelect; // 新用户是否需要选择角色
     private Boolean needsRoleSelect; // 新用户是否需要选择角色
     private String teacherStatus; // 成长规划师审核状态: pending/approved/rejected
     private String teacherStatus; // 成长规划师审核状态: pending/approved/rejected
+    private String teacherRejectReason; // 成长规划师审核拒绝原因
     private Boolean hasTeacherRole; // 用户是否拥有teacher角色
     private Boolean hasTeacherRole; // 用户是否拥有teacher角色
 }
 }

+ 19 - 0
zxyj-backend/src/main/java/com/zxyj/dto/MinigameCompleteDTO.java

@@ -0,0 +1,19 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 小游戏任务完成 DTO
+ */
+@Data
+public class MinigameCompleteDTO {
+
+    @NotNull(message = "孩子ID不能为空")
+    private Long childId;
+
+    private Integer completionTime;
+
+    private Integer score;
+}

+ 13 - 0
zxyj-backend/src/main/java/com/zxyj/dto/PackageCreateOrderDTO.java

@@ -0,0 +1,13 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+@Data
+public class PackageCreateOrderDTO {
+    @NotNull(message = "套餐ID不能为空")
+    private Long packageId;
+
+    private String payMethod = "wechat";
+}

+ 8 - 0
zxyj-backend/src/main/java/com/zxyj/dto/PublishPackageDTO.java

@@ -0,0 +1,8 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+@Data
+public class PublishPackageDTO {
+    private Boolean publish = false;
+}

+ 11 - 0
zxyj-backend/src/main/java/com/zxyj/dto/ReplyMessageDTO.java

@@ -0,0 +1,11 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+@Data
+public class ReplyMessageDTO {
+    @NotBlank(message = "回复内容不能为空")
+    private String replyContent;
+}

+ 15 - 0
zxyj-backend/src/main/java/com/zxyj/dto/RewardApproveDTO.java

@@ -0,0 +1,15 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 奖励审批 DTO
+ */
+@Data
+public class RewardApproveDTO {
+
+    @NotNull(message = "请指定审批结果")
+    private Boolean approved;
+}

+ 15 - 0
zxyj-backend/src/main/java/com/zxyj/dto/RewardExchangeDTO.java

@@ -0,0 +1,15 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 奖励兑换 DTO
+ */
+@Data
+public class RewardExchangeDTO {
+
+    @NotNull(message = "孩子ID不能为空")
+    private Long childId;
+}

+ 12 - 0
zxyj-backend/src/main/java/com/zxyj/dto/RewardWishlistDTO.java

@@ -0,0 +1,12 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+/**
+ * 奖励心愿单查询 DTO
+ */
+@Data
+public class RewardWishlistDTO {
+
+    private Long childId;
+}

+ 18 - 0
zxyj-backend/src/main/java/com/zxyj/dto/SendMessageDTO.java

@@ -0,0 +1,18 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+@Data
+public class SendMessageDTO {
+    @NotNull(message = "家庭ID不能为空")
+    private Long familyId;
+
+    @NotNull(message = "规划师ID不能为空")
+    private Long teacherId;
+
+    @NotBlank(message = "内容不能为空")
+    private String content;
+}

+ 11 - 0
zxyj-backend/src/main/java/com/zxyj/dto/ServiceContentListDTO.java

@@ -0,0 +1,11 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+@Data
+public class ServiceContentListDTO {
+
+    private Long typeId;
+
+    private String status;
+}

+ 11 - 0
zxyj-backend/src/main/java/com/zxyj/dto/SwitchModeDTO.java

@@ -0,0 +1,11 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+@Data
+public class SwitchModeDTO {
+    @NotBlank(message = "模式不能为空")
+    private String mode;
+}

+ 11 - 0
zxyj-backend/src/main/java/com/zxyj/dto/SwitchToChildDTO.java

@@ -0,0 +1,11 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+@Data
+public class SwitchToChildDTO {
+    @NotNull(message = "孩子ID不能为空")
+    private Long childId;
+}

+ 17 - 0
zxyj-backend/src/main/java/com/zxyj/dto/SyncExternalResultDTO.java

@@ -0,0 +1,17 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+@Data
+public class SyncExternalResultDTO {
+    @NotNull(message = "孩子ID不能为空")
+    private Long childId;
+
+    @NotNull(message = "家长ID不能为空")
+    private Long parentId;
+
+    @NotNull(message = "测评ID不能为空")
+    private Long assessmentId;
+}

+ 15 - 0
zxyj-backend/src/main/java/com/zxyj/dto/UpdateAssessmentMaterialDTO.java

@@ -0,0 +1,15 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+@Data
+public class UpdateAssessmentMaterialDTO {
+
+    private String title;
+
+    private String content;
+
+    private String coverImage;
+
+    private String status;
+}

+ 12 - 0
zxyj-backend/src/main/java/com/zxyj/dto/UpdateGuidePackagePriceDTO.java

@@ -0,0 +1,12 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+
+@Data
+public class UpdateGuidePackagePriceDTO {
+    @NotNull(message = "价格不能为空")
+    private BigDecimal price;
+}

+ 21 - 0
zxyj-backend/src/main/java/com/zxyj/dto/UpdatePackageTemplateDTO.java

@@ -0,0 +1,21 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class UpdatePackageTemplateDTO {
+
+    private String name;
+
+    private Integer validityMonths;
+
+    private BigDecimal referencePrice;
+
+    private BigDecimal minPrice;
+
+    private String description;
+
+    private String status;
+}

+ 14 - 0
zxyj-backend/src/main/java/com/zxyj/dto/UpdatePointsConfigDTO.java

@@ -0,0 +1,14 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+@Data
+public class UpdatePointsConfigDTO {
+
+    @NotNull(message = "积分值不能为空")
+    private Integer pointsValue;
+
+    private Double pointsValueYuan;
+}

+ 24 - 0
zxyj-backend/src/main/java/com/zxyj/dto/UpdateServiceContentDTO.java

@@ -0,0 +1,24 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+import java.math.BigDecimal;
+
+@Data
+public class UpdateServiceContentDTO {
+
+    @NotNull(message = "ID不能为空")
+    private Long id;
+
+    private String title;
+
+    private String content;
+
+    private Integer duration;
+
+    private String ageRange;
+
+    private BigDecimal price;
+}

+ 18 - 0
zxyj-backend/src/main/java/com/zxyj/dto/UploadTextDTO.java

@@ -0,0 +1,18 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+@Data
+public class UploadTextDTO {
+    @NotNull(message = "任务ID不能为空")
+    private Long taskId;
+
+    @NotNull(message = "创建者ID不能为空")
+    private Long creatorId;
+
+    @NotBlank(message = "内容不能为空")
+    private String content;
+}

+ 15 - 0
zxyj-backend/src/main/java/com/zxyj/dto/WechatPayParamsDTO.java

@@ -0,0 +1,15 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+@Data
+public class WechatPayParamsDTO {
+    @NotNull(message = "订单ID不能为空")
+    private Long orderId;
+
+    @NotBlank(message = "openid不能为空")
+    private String openid;
+}

+ 17 - 0
zxyj-backend/src/main/java/com/zxyj/dto/WishApproveExchangeDTO.java

@@ -0,0 +1,17 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 心愿兑换审批 DTO
+ */
+@Data
+public class WishApproveExchangeDTO {
+
+    @NotNull(message = "请指定审批结果")
+    private Boolean approved;
+
+    private String reason;
+}

+ 12 - 0
zxyj-backend/src/main/java/com/zxyj/dto/WishReasonDTO.java

@@ -0,0 +1,12 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+/**
+ * 心愿操作原因 DTO(拒绝/取消)
+ */
+@Data
+public class WishReasonDTO {
+
+    private String reason;
+}

+ 17 - 0
zxyj-backend/src/main/java/com/zxyj/dto/WishSetPriceDTO.java

@@ -0,0 +1,17 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+
+/**
+ * 心愿定价 DTO
+ */
+@Data
+public class WishSetPriceDTO {
+
+    @NotNull(message = "积分值不能为空")
+    @Min(value = 1, message = "积分值必须大于0")
+    private Integer pointsRequired;
+}

+ 3 - 0
zxyj-backend/src/main/java/com/zxyj/entity/User.java

@@ -61,6 +61,9 @@ public class User implements Serializable {
     // 成长规划师审核状态: pending/approved/rejected
     // 成长规划师审核状态: pending/approved/rejected
     private String teacherStatus;
     private String teacherStatus;
 
 
+    // 成长规划师审核拒绝原因
+    private String teacherRejectReason;
+
     private Date createdAt;
     private Date createdAt;
 
 
     private Date updatedAt;
     private Date updatedAt;

+ 9 - 0
zxyj-backend/src/main/java/com/zxyj/service/UserService.java

@@ -83,6 +83,7 @@ private WechatService wechatService;
         result.setRoles(user.getRoles());
         result.setRoles(user.getRoles());
         result.setFamilyId(user.getFamilyId());
         result.setFamilyId(user.getFamilyId());
         result.setNickname(user.getNickname());
         result.setNickname(user.getNickname());
+        populateTeacherFields(result, user);
 
 
         return result;
         return result;
     }
     }
@@ -150,6 +151,7 @@ private WechatService wechatService;
   result.setNickname(user.getNickname());
   result.setNickname(user.getNickname());
   result.setIsNewUser(isNewUser);
   result.setIsNewUser(isNewUser);
   result.setNeedsRoleSelect(isNewUser);
   result.setNeedsRoleSelect(isNewUser);
+  populateTeacherFields(result, user);
 
 
   return result;
   return result;
 }
 }
@@ -231,6 +233,7 @@ private WechatService wechatService;
 
 
         // 新用户需要完善信息(仅当需要选择角色时)
         // 新用户需要完善信息(仅当需要选择角色时)
         result.setNeedsRoleSelect(isNewUser);
         result.setNeedsRoleSelect(isNewUser);
+        populateTeacherFields(result, user);
 
 
         return result;
         return result;
     }
     }
@@ -264,6 +267,7 @@ private WechatService wechatService;
         result.setNickname(user.getNickname());
         result.setNickname(user.getNickname());
         result.setIsNewUser(false);
         result.setIsNewUser(false);
         result.setOpenid(openid);
         result.setOpenid(openid);
+        populateTeacherFields(result, user);
 
 
         return result;
         return result;
     }
     }
@@ -289,6 +293,7 @@ private WechatService wechatService;
         result.setNickname(user.getNickname());
         result.setNickname(user.getNickname());
         result.setIsNewUser(false);
         result.setIsNewUser(false);
         result.setOpenid(openid);
         result.setOpenid(openid);
+        populateTeacherFields(result, user);
 
 
         return result;
         return result;
     }
     }
@@ -378,6 +383,7 @@ private WechatService wechatService;
         result.setFamilyId(user.getFamilyId());
         result.setFamilyId(user.getFamilyId());
         result.setNickname(user.getNickname());
         result.setNickname(user.getNickname());
         result.setIsNewUser(true);
         result.setIsNewUser(true);
+        populateTeacherFields(result, user);
 
 
         return result;
         return result;
     }
     }
@@ -635,6 +641,7 @@ private WechatService wechatService;
         result.setRole(user.getRole());
         result.setRole(user.getRole());
         result.setFamilyId(user.getFamilyId());
         result.setFamilyId(user.getFamilyId());
         result.setNickname(user.getNickname());
         result.setNickname(user.getNickname());
+        populateTeacherFields(result, user);
 
 
         return result;
         return result;
     }
     }
@@ -992,6 +999,7 @@ private WechatService wechatService;
         result.setFamilyId(user.getFamilyId());
         result.setFamilyId(user.getFamilyId());
         result.setNickname(user.getNickname());
         result.setNickname(user.getNickname());
         result.setIsNewUser(isNewUser);
         result.setIsNewUser(isNewUser);
+        populateTeacherFields(result, user);
         
         
         return result;
         return result;
     }
     }
@@ -1018,6 +1026,7 @@ private WechatService wechatService;
 
 
     private void populateTeacherFields(LoginResultDTO result, User user) {
     private void populateTeacherFields(LoginResultDTO result, User user) {
         result.setTeacherStatus(user.getTeacherStatus() != null ? user.getTeacherStatus() : "pending");
         result.setTeacherStatus(user.getTeacherStatus() != null ? user.getTeacherStatus() : "pending");
+        result.setTeacherRejectReason(user.getTeacherRejectReason());
         boolean hasTeacher = hasRole(user, "teacher");
         boolean hasTeacher = hasRole(user, "teacher");
         result.setHasTeacherRole(hasTeacher);
         result.setHasTeacherRole(hasTeacher);
     }
     }

+ 0 - 0
zxyj-frontend/src/api/guideFamilyTask.js → zxyj-frontend/api/guideFamilyTask.js


+ 138 - 0
zxyj-frontend/custom-tab-bar/index.vue

@@ -0,0 +1,138 @@
+<template>
+  <view class="tab-bar-wrap" v-if="isLoggedIn">
+    <!-- 普通用户TabBar -->
+    <view v-if="currentRole !== 'teacher'" class="tab-bar">
+      <view
+        v-for="(item, index) in normalTabs"
+        :key="index"
+        class="tab-item"
+        :class="{ active: currentIndex === index }"
+        @click="switchTab(item, index)"
+      >
+        <image
+          class="tab-icon"
+          :src="currentIndex === index ? item.selectedIcon : item.icon"
+          mode="aspectFit"
+        ></image>
+        <text class="tab-text">{{ item.text }}</text>
+      </view>
+    </view>
+
+    <!-- 规划师TabBar -->
+    <view v-else class="tab-bar">
+      <view
+        v-for="(item, index) in teacherTabs"
+        :key="index"
+        class="tab-item"
+        :class="{ active: currentIndex === index }"
+        @click="switchTab(item, index)"
+      >
+        <image
+          class="tab-icon"
+          :src="currentIndex === index ? item.selectedIcon : item.icon"
+          mode="aspectFit"
+        ></image>
+        <text class="tab-text">{{ item.text }}</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      currentIndex: 0,
+      normalTabs: [
+        { text: '首页', pagePath: '/pages/index/index', icon: '/static/tab-home.png', selectedIcon: '/static/tab-home-active.png' },
+        { text: '任务', pagePath: '/pages/tasks/tasks', icon: '/static/tab-task.png', selectedIcon: '/static/tab-task-active.png' },
+        { text: '心愿单', pagePath: '/pages/rewards/rewards', icon: '/static/tab-reward.png', selectedIcon: '/static/tab-reward-active.png' },
+        { text: '我的', pagePath: '/pages/profile/profile', icon: '/static/tab-profile.png', selectedIcon: '/static/tab-profile-active.png' }
+      ],
+      teacherTabs: [
+        { text: '首页', pagePath: '/pages/teacher/teacher-home', icon: '/static/tab-home.png', selectedIcon: '/static/tab-home-active.png' },
+        { text: '家庭', pagePath: '/pages/teacher/families', icon: '/static/tab-family.png', selectedIcon: '/static/tab-family-active.png' },
+        { text: '团队', pagePath: '/pages/teacher/team', icon: '/static/tab-team.png', selectedIcon: '/static/tab-team-active.png' },
+        { text: '成长档案', pagePath: '/pages/growth/index', icon: '/static/tab-growth.png', selectedIcon: '/static/tab-growth-active.png' },
+        { text: '我的', pagePath: '/pages/teacher/teacher-profile', icon: '/static/tab-profile.png', selectedIcon: '/static/tab-profile-active.png' }
+      ]
+    }
+  },
+  computed: {
+    isLoggedIn() {
+      return !!uni.getStorageSync('token')
+    },
+    currentRole() {
+      return uni.getStorageSync('currentRole') || 'parent'
+    }
+  },
+  mounted() {
+    this.updateSelected()
+    uni.$on('switchRole', () => {
+      this.updateSelected()
+    })
+  },
+  methods: {
+    updateSelected() {
+      const pages = getCurrentPages()
+      if (pages.length) {
+        const currentPage = pages[pages.length - 1]
+        const currentPath = '/' + currentPage.route
+
+        if (this.currentRole === 'teacher') {
+          const idx = this.teacherTabs.findIndex(t => t.pagePath === currentPath)
+          this.currentIndex = idx >= 0 ? idx : 0
+        } else {
+          const idx = this.normalTabs.findIndex(t => t.pagePath === currentPath)
+          this.currentIndex = idx >= 0 ? idx : 0
+        }
+      }
+    },
+    switchTab(item, index) {
+      this.currentIndex = index
+      uni.reLaunch({ url: item.pagePath })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.tab-bar-wrap {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  z-index: 999;
+}
+
+.tab-bar {
+  display: flex;
+  height: 100rpx;
+  background: #fff;
+  padding-bottom: env(safe-area-inset-bottom);
+  border-top: 1rpx solid #eee;
+}
+
+.tab-item {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+
+.tab-icon {
+  width: 48rpx;
+  height: 48rpx;
+  margin-bottom: 4rpx;
+}
+
+.tab-text {
+  font-size: 22rpx;
+  color: #999;
+}
+
+.tab-item.active .tab-text {
+  color: #FF6B6B;
+}
+</style>

+ 16 - 0
zxyj-frontend/fix-dist.bat

@@ -0,0 +1,16 @@
+@echo off
+chcp 65001 >nul
+
+set DIST=%~dp0dist\dev\mp-weixin
+
+if exist "%DIST%\custom-tab-bar\index.vue" (
+    del /f /q "%DIST%\custom-tab-bar\index.vue"
+    echo ✓ 已删除 custom-tab-bar/index.vue
+) else (
+    echo - 没有找到 index.vue,无需清理
+)
+
+powershell -Command "$c=Get-Content '%DIST%\project.config.json' -Raw -ErrorAction SilentlyContinue | ConvertFrom-Json; if($c -and !$c.setting.lazyCodeLoading){$c.setting.lazyCodeLoading='requiredComponents';$c | ConvertTo-Json -Depth 10 | Set-Content '%DIST%\project.config.json'; Write-Host '✓ 已设置 lazyCodeLoading';}else{Write-Host '- lazyCodeLoading 无需设置'}"
+echo.
+echo 修复完成,请在微信开发者工具中重新导入项目
+pause

+ 2 - 3
zxyj-frontend/package.json

@@ -3,8 +3,7 @@
   "version": "1.0.0",
   "version": "1.0.0",
   "description": "知行益家小程序前端",
   "description": "知行益家小程序前端",
   "main": "main.js",
   "main": "main.js",
-  "dependencies": {
-    "vue": "^2.6.14",
-    "vuex": "^3.2.0"
+  "scripts": {
+    "postbuild": "node -e \"try{require('fs').unlinkSync('dist/dev/mp-weixin/custom-tab-bar/index.vue')}catch(e){};try{const c=require('./dist/dev/mp-weixin/project.config.json');c.setting=c.setting||{};c.setting.lazyCodeLoading='requiredComponents';require('fs').writeFileSync('./dist/dev/mp-weixin/project.config.json',JSON.stringify(c,null,2))}catch(e){}\""
   }
   }
 }
 }

+ 361 - 0
zxyj-frontend/pages/assessment/child-info.vue

@@ -0,0 +1,361 @@
+<template>
+  <view class="container">
+    <view class="header">
+      <text class="title">孩子信息</text>
+      <text class="subtitle">请填写孩子的详细信息,用于测评档案</text>
+    </view>
+
+    <!-- 基本信息 -->
+    <view class="section">
+      <view class="section-title">基本信息</view>
+      <view class="form-group">
+        <view class="form-label">孩子姓名</view>
+        <input class="form-input" v-model="form.childName" placeholder="请输入孩子姓名" />
+      </view>
+      <view class="form-group">
+        <view class="form-label">出生日期</view>
+        <picker mode="date" :value="form.childBirthday" @change="onBirthdayChange">
+          <view class="picker-value">{{ form.childBirthday || '请选择出生日期' }}</view>
+        </picker>
+      </view>
+      <view class="form-group">
+        <view class="form-label">性别</view>
+        <radio-group @change="onGenderChange">
+          <label class="radio-item">
+            <radio value="男" :checked="form.childGender === '男'" />
+            <text>男</text>
+          </label>
+          <label class="radio-item">
+            <radio value="女" :checked="form.childGender === '女'" />
+            <text>女</text>
+          </label>
+        </radio-group>
+      </view>
+      <view class="form-group">
+        <view class="form-label">年龄</view>
+        <text class="form-value">{{ form.childAge || '自动计算' }}</text>
+      </view>
+    </view>
+
+    <!-- 学校信息 -->
+    <view class="section">
+      <view class="section-title">学校信息</view>
+      <view class="form-group">
+        <view class="form-label">学校名称</view>
+        <input class="form-input" v-model="form.school" placeholder="请输入学校名称" />
+      </view>
+      <view class="form-group">
+        <view class="form-label">年级</view>
+        <input class="form-input" v-model="form.grade" placeholder="如:一年级" />
+      </view>
+      <view class="form-group">
+        <view class="form-label">班级</view>
+        <input class="form-input" v-model="form.gradeClass" placeholder="如:3班" />
+      </view>
+      <view class="form-group">
+        <view class="form-label">在校成绩</view>
+        <radio-group @change="onSchoolScoreChange">
+          <label class="radio-item">
+            <radio value="优秀" :checked="form.schoolScore === '优秀'" />
+            <text>优秀</text>
+          </label>
+          <label class="radio-item">
+            <radio value="良好" :checked="form.schoolScore === '良好'" />
+            <text>良好</text>
+          </label>
+          <label class="radio-item">
+            <radio value="一般" :checked="form.schoolScore === '一般'" />
+            <text>一般</text>
+          </label>
+          <label class="radio-item">
+            <radio value="较差" :checked="form.schoolScore === '较差'" />
+            <text>较差</text>
+          </label>
+        </radio-group>
+      </view>
+    </view>
+
+    <!-- 身体信息 -->
+    <view class="section">
+      <view class="section-title">身体信息</view>
+      <view class="form-row">
+        <view class="form-group half">
+          <view class="form-label">身高 (cm)</view>
+          <input class="form-input" type="number" v-model="form.height" placeholder="cm" />
+        </view>
+        <view class="form-group half">
+          <view class="form-label">体重 (kg)</view>
+          <input class="form-input" type="digit" v-model="form.weight" placeholder="kg" />
+        </view>
+      </view>
+    </view>
+
+    <!-- 家庭信息 -->
+    <view class="section">
+      <view class="section-title">家庭信息</view>
+      <view class="form-group">
+        <view class="form-label">家庭状况</view>
+        <input class="form-input" v-model="form.familyStatus" placeholder="如:双亲家庭/单亲家庭" />
+      </view>
+      <view class="form-group">
+        <view class="form-label">父母最高学历</view>
+        <radio-group @change="onParentDegreeChange">
+          <label class="radio-item">
+            <radio value="高中及以下" :checked="form.parentDegree === '高中及以下'" />
+            <text>高中及以下</text>
+          </label>
+          <label class="radio-item">
+            <radio value="大专" :checked="form.parentDegree === '大专'" />
+            <text>大专</text>
+          </label>
+          <label class="radio-item">
+            <radio value="本科" :checked="form.parentDegree === '本科'" />
+            <text>本科</text>
+          </label>
+          <label class="radio-item">
+            <radio value="硕士" :checked="form.parentDegree === '硕士'" />
+            <text>硕士</text>
+          </label>
+          <label class="radio-item">
+            <radio value="博士" :checked="form.parentDegree === '博士'" />
+            <text>博士</text>
+          </label>
+        </radio-group>
+      </view>
+      <view class="form-group">
+        <view class="form-label">民族</view>
+        <input class="form-input" v-model="form.nation" placeholder="如:汉族" />
+      </view>
+      <view class="form-group">
+        <view class="form-label">是否独生子女</view>
+        <radio-group @change="onSingleChildChange">
+          <label class="radio-item">
+            <radio value="是" :checked="form.singleChild === '是'" />
+            <text>是</text>
+          </label>
+          <label class="radio-item">
+            <radio value="否" :checked="form.singleChild === '否'" />
+            <text>否</text>
+          </label>
+        </radio-group>
+      </view>
+    </view>
+
+    <!-- 地址信息 -->
+    <view class="section">
+      <view class="section-title">地址信息</view>
+      <view class="form-group">
+        <view class="form-label">省份</view>
+        <input class="form-input" v-model="form.province" placeholder="请输入省份" />
+      </view>
+      <view class="form-group">
+        <view class="form-label">城市</view>
+        <input class="form-input" v-model="form.city" placeholder="请输入城市" />
+      </view>
+      <view class="form-group">
+        <view class="form-label">区县</view>
+        <input class="form-input" v-model="form.district" placeholder="请输入区县" />
+      </view>
+      <view class="form-group">
+        <view class="form-label">详细地址</view>
+        <input class="form-input" v-model="form.address" placeholder="请输入详细地址" />
+      </view>
+    </view>
+
+    <!-- 紧急联系人 -->
+    <view class="section">
+      <view class="section-title">紧急联系人</view>
+      <view class="form-group">
+        <view class="form-label">姓名</view>
+        <input class="form-input" v-model="form.urgentName" placeholder="紧急联系人姓名" />
+      </view>
+      <view class="form-group">
+        <view class="form-label">电话</view>
+        <input class="form-input" type="number" v-model="form.urgentPhone" placeholder="紧急联系人电话" />
+      </view>
+    </view>
+
+    <button class="btn-submit" @click="submit">下一步</button>
+  </view>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      flowData: {},
+      form: {
+        childName: '',
+        childBirthday: '',
+        childGender: '',
+        childAge: 0,
+        school: '',
+        grade: '',
+        gradeClass: '',
+        height: '',
+        weight: '',
+        familyStatus: '',
+        parentDegree: '',
+        nation: '',
+        province: '',
+        city: '',
+        district: '',
+        address: '',
+        singleChild: '',
+        schoolScore: '',
+        urgentName: '',
+        urgentPhone: ''
+      }
+    }
+  },
+  onLoad() {
+    this.flowData = uni.getStorageSync('assessmentFlowData') || {}
+  },
+  methods: {
+    onBirthdayChange(e) {
+      this.form.childBirthday = e.detail.value
+      this.calcAge(e.detail.value)
+    },
+    calcAge(birthday) {
+      if (!birthday) return
+      const birth = new Date(birthday)
+      const today = new Date()
+      let age = today.getFullYear() - birth.getFullYear()
+      const m = today.getMonth() - birth.getMonth()
+      if (m < 0 || (m === 0 && today.getDate() < birth.getDate())) {
+        age--
+      }
+      this.form.childAge = age > 0 ? age : 0
+    },
+    onGenderChange(e) { this.form.childGender = e.detail.value },
+    onSchoolScoreChange(e) { this.form.schoolScore = e.detail.value },
+    onParentDegreeChange(e) { this.form.parentDegree = e.detail.value },
+    onSingleChildChange(e) { this.form.singleChild = e.detail.value },
+
+    validate() {
+      const required = ['childName', 'childBirthday', 'childGender']
+      for (let i = 0; i < required.length; i++) {
+        if (!this.form[required[i]]) {
+          uni.showToast({ title: '请填写必填项', icon: 'none' })
+          return false
+        }
+      }
+      return true
+    },
+
+    submit() {
+      if (!this.validate()) return
+      uni.setStorageSync('assessmentChildInfo', this.form)
+      const f = this.flowData
+      uni.navigateTo({
+        url: '/pages/assessment/order?guideId=' + (f.guideId || '') +
+          '&guideName=' + encodeURIComponent(f.guideName || '') +
+          '&packageId=' + (f.packageId || '') +
+          '&packageName=' + encodeURIComponent(f.packageName || 'DAN专注力测评') +
+          '&childId=' + (f.childId || '') +
+          '&appointmentDate=' + (f.appointmentDate || '') +
+          '&appointmentTime=' + (f.appointmentTime || '') +
+          '&totalPrice=' + (f.totalPrice || '0') +
+          '&discountAmount=' + (f.discountAmount || '0') +
+          '&actualPrice=' + ((f.totalPrice || 0) - (f.discountAmount || 0))
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  padding: 30rpx;
+  background: #F8F8F8;
+  min-height: 100vh;
+}
+.header {
+  text-align: center;
+  padding: 40rpx 0 30rpx;
+}
+.title {
+  font-size: 48rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 16rpx;
+}
+.subtitle {
+  font-size: 26rpx;
+  color: #666;
+}
+.section {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 30rpx;
+  margin-bottom: 24rpx;
+}
+.section-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 24rpx;
+  padding-bottom: 16rpx;
+  border-bottom: 1rpx solid #eee;
+}
+.form-group {
+  margin-bottom: 24rpx;
+}
+.form-group:last-child {
+  margin-bottom: 0;
+}
+.form-label {
+  font-size: 28rpx;
+  color: #333;
+  margin-bottom: 12rpx;
+  font-weight: 500;
+}
+.form-input {
+  height: 80rpx;
+  background: #F8F8F8;
+  border-radius: 10rpx;
+  padding: 0 20rpx;
+  font-size: 28rpx;
+  color: #333;
+}
+.form-value {
+  font-size: 28rpx;
+  color: #999;
+  line-height: 80rpx;
+}
+.picker-value {
+  height: 80rpx;
+  background: #F8F8F8;
+  border-radius: 10rpx;
+  padding: 0 20rpx;
+  font-size: 28rpx;
+  color: #333;
+  line-height: 80rpx;
+}
+.form-row {
+  display: flex;
+  gap: 20rpx;
+}
+.half {
+  flex: 1;
+}
+.radio-item {
+  display: inline-flex;
+  align-items: center;
+  margin-right: 40rpx;
+  font-size: 28rpx;
+  color: #333;
+}
+.btn-submit {
+  width: 100%;
+  height: 90rpx;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: #fff;
+  border-radius: 45rpx;
+  font-size: 32rpx;
+  font-weight: bold;
+  margin-top: 40rpx;
+  margin-bottom: 40rpx;
+}
+</style>

+ 266 - 0
zxyj-frontend/pages/assessment/order.vue

@@ -0,0 +1,266 @@
+<template>
+  <view class="container">
+    <view class="header">
+      <text class="title">确认订单</text>
+      <text class="subtitle">请确认测评订单信息并完成支付</text>
+    </view>
+
+    <!-- 订单信息 -->
+    <view class="section">
+      <view class="section-title">订单详情</view>
+      <view class="info-row">
+        <text class="info-label">套餐名称</text>
+        <text class="info-value">{{ packageName }}</text>
+      </view>
+      <view class="info-row">
+        <text class="info-label">成长规划师</text>
+        <text class="info-value">{{ guideName }}</text>
+      </view>
+      <view class="info-row">
+        <text class="info-label">预约日期</text>
+        <text class="info-value">{{ appointmentDate }}</text>
+      </view>
+      <view class="info-row">
+        <text class="info-label">预约时间</text>
+        <text class="info-value">{{ appointmentTime }}</text>
+      </view>
+    </view>
+
+    <!-- 费用信息 -->
+    <view class="section">
+      <view class="section-title">费用信息</view>
+      <view class="price-row">
+        <text class="price-label">测评费用</text>
+        <text class="price-value">¥{{ totalPrice }}</text>
+      </view>
+      <view class="price-row" v-if="discountAmount > 0">
+        <text class="price-label">优惠金额</text>
+        <text class="price-value discount">-¥{{ discountAmount }}</text>
+      </view>
+      <view class="price-row total">
+        <text class="price-label">实付金额</text>
+        <text class="price-value highlight">¥{{ actualPrice }}</text>
+      </view>
+    </view>
+
+    <!-- 支付方式 -->
+    <view class="section">
+      <view class="section-title">支付方式</view>
+      <view class="pay-methods">
+        <view class="pay-method-item" :class="{ active: payMethod === 'wechat' }" @click="payMethod = 'wechat'">
+          <text class="pay-icon">💳</text>
+          <text class="pay-name">微信支付</text>
+          <view class="pay-radio" v-if="payMethod === 'wechat'">
+            <view class="radio-inner" />
+          </view>
+        </view>
+        <view class="pay-method-item" :class="{ active: payMethod === 'alipay' }" @click="payMethod = 'alipay'">
+          <text class="pay-icon">🔷</text>
+          <text class="pay-name">支付宝</text>
+          <view class="pay-radio" v-if="payMethod === 'alipay'">
+            <view class="radio-inner" />
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <button class="btn-submit" @click="handlePay">确认支付 ¥{{ actualPrice }}</button>
+  </view>
+</template>
+
+<script>
+import { createAssessmentAppointment, payAssessmentOrder } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      packageName: '',
+      guideName: '',
+      appointmentDate: '',
+      appointmentTime: '',
+      totalPrice: '0',
+      discountAmount: '0',
+      actualPrice: '0',
+      payMethod: 'wechat',
+      childId: null,
+      guideId: null,
+      packageId: null
+    }
+  },
+  onLoad(options) {
+    this.packageName = decodeURIComponent(options.packageName || 'DAN专注力测评')
+    this.guideName = decodeURIComponent(options.guideName || '')
+    this.appointmentDate = options.appointmentDate || ''
+    this.appointmentTime = options.appointmentTime || ''
+    this.totalPrice = options.totalPrice || '0'
+    this.discountAmount = options.discountAmount || '0'
+    this.actualPrice = options.actualPrice || this.totalPrice
+    this.childId = options.childId || null
+    this.guideId = options.guideId || null
+    this.packageId = options.packageId || null
+  },
+  methods: {
+    async handlePay() {
+      if (!this.guideId) {
+        uni.showToast({ title: '请选择成长规划师', icon: 'none' })
+        return
+      }
+      try {
+        uni.showLoading({ title: '创建订单中...' })
+        const res = await createAssessmentAppointment({
+          childId: this.childId,
+          guideId: this.guideId,
+          packageId: this.packageId,
+          appointmentDate: this.appointmentDate,
+          appointmentTime: this.appointmentTime,
+          notes: '',
+          totalPrice: this.totalPrice,
+          guideName: this.guideName,
+          packageName: this.packageName,
+          discountAmount: this.discountAmount
+        })
+        if (res.code !== 200) {
+          uni.hideLoading()
+          return
+        }
+        const orderNo = res.data.orderNo
+        uni.showLoading({ title: '支付中...' })
+        const payRes = await payAssessmentOrder(orderNo, this.payMethod)
+        uni.hideLoading()
+        if (payRes.code === 200) {
+          uni.removeStorageSync('assessmentFlowData')
+          uni.removeStorageSync('assessmentChildInfo')
+          uni.showModal({
+            title: '支付成功',
+            content: '您的测评订单已支付,请按时参加测评',
+            showCancel: false,
+            success: () => {
+              uni.redirectTo({ url: '/pages/assessment/results' })
+            }
+          })
+        } else {
+          uni.showToast({ title: payRes.message || '支付失败', icon: 'none' })
+        }
+      } catch (e) {
+        uni.hideLoading()
+        uni.showToast({ title: '操作失败,请重试', icon: 'none' })
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  padding: 30rpx;
+  background: #F8F8F8;
+  min-height: 100vh;
+}
+.header {
+  text-align: center;
+  padding: 40rpx 0 30rpx;
+}
+.title {
+  font-size: 48rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 16rpx;
+}
+.subtitle {
+  font-size: 26rpx;
+  color: #666;
+}
+.section {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 30rpx;
+  margin-bottom: 24rpx;
+}
+.section-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 24rpx;
+  padding-bottom: 16rpx;
+  border-bottom: 1rpx solid #eee;
+}
+.info-row {
+  display: flex;
+  justify-content: space-between;
+  padding: 16rpx 0;
+  font-size: 28rpx;
+}
+.info-label { color: #666; }
+.info-value { color: #333; font-weight: 500; }
+.price-row {
+  display: flex;
+  justify-content: space-between;
+  padding: 16rpx 0;
+  font-size: 28rpx;
+}
+.price-label { color: #666; }
+.price-value { color: #333; font-weight: bold; }
+.price-value.discount { color: #FF6B6B; }
+.price-row.total {
+  margin-top: 16rpx;
+  padding-top: 24rpx;
+  border-top: 1rpx dashed #eee;
+}
+.price-value.highlight {
+  font-size: 36rpx;
+  color: #FF6B6B;
+}
+.pay-methods {
+  display: flex;
+  flex-direction: column;
+  gap: 20rpx;
+}
+.pay-method-item {
+  display: flex;
+  align-items: center;
+  padding: 24rpx;
+  background: #F8F8F8;
+  border-radius: 12rpx;
+  border: 2rpx solid transparent;
+}
+.pay-method-item.active {
+  border-color: #667eea;
+  background: #F0F4FF;
+}
+.pay-icon {
+  font-size: 40rpx;
+  margin-right: 20rpx;
+}
+.pay-name {
+  font-size: 28rpx;
+  color: #333;
+  flex: 1;
+}
+.pay-radio {
+  width: 40rpx;
+  height: 40rpx;
+  border-radius: 50%;
+  border: 2rpx solid #667eea;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.radio-inner {
+  width: 20rpx;
+  height: 20rpx;
+  border-radius: 50%;
+  background: #667eea;
+}
+.btn-submit {
+  width: 100%;
+  height: 90rpx;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: #fff;
+  border-radius: 45rpx;
+  font-size: 32rpx;
+  font-weight: bold;
+  margin-top: 40rpx;
+  margin-bottom: 40rpx;
+}
+</style>

+ 811 - 0
zxyj-frontend/pages/assessment/record.vue

@@ -0,0 +1,811 @@
+<template>
+<view class="container">
+  <view class="header">
+    <text class="title">录入测评结果</text>
+    <text class="subtitle">专业的DAN测评,记录孩子的专注力评估结果</text>
+  </view>
+
+  <!-- 孩子信息 -->
+  <view class="section">
+    <view class="section-title">孩子信息</view>
+    <view class="info-card">
+      <view class="info-row">
+        <text class="info-label">姓名</text>
+        <text class="info-value">{{ childName || '未填写' }}</text>
+      </view>
+      <view class="info-row">
+        <text class="info-label">年龄</text>
+        <text class="info-value">{{ age || '-' }}岁</text>
+      </view>
+      <view class="info-row">
+        <text class="info-label">学校</text>
+        <text class="info-value">{{ school || '-' }}</text>
+      </view>
+    </view>
+  </view>
+
+  <!-- DAN等级 -->
+  <view class="section">
+    <view class="section-title">DAN等级</view>
+    <view class="level-selector">
+      <view
+        class="level-item"
+        :class="{ 'selected': form.danLevel === 'A' }"
+        @click="selectLevel('A')"
+      >
+        <text class="level-code">A</text>
+        <text class="level-desc">优秀:专注力发展良好</text>
+      </view>
+      <view
+        class="level-item"
+        :class="{ 'selected': form.danLevel === 'B' }"
+        @click="selectLevel('B')"
+      >
+        <text class="level-code">B</text>
+        <text class="level-desc">良好:专注力基本达标</text>
+      </view>
+      <view
+        class="level-item"
+        :class="{ 'selected': form.danLevel === 'C' }"
+        @click="selectLevel('C')"
+      >
+        <text class="level-code">C</text>
+        <text class="level-desc">一般:专注力需要提升</text>
+      </view>
+      <view
+        class="level-item"
+        :class="{ 'selected': form.danLevel === 'D' }"
+        @click="selectLevel('D')"
+      >
+        <text class="level-code">D</text>
+        <text class="level-desc">待提升:专注力明显不足</text>
+      </view>
+    </view>
+  </view>
+
+  <!-- 各项评分 -->
+  <view class="section">
+    <view class="section-title">各项评分</view>
+    <view class="score-group">
+      <view class="score-item">
+        <view class="score-header">
+          <text class="score-label">注意力</text>
+          <text class="score-value">{{ form.attentionScore }}</text>
+        </view>
+        <slider
+          :value="form.attentionScore"
+          :min="0"
+          :max="100"
+          :step="1"
+          activeColor="#667eea"
+          backgroundColor="#E8E8E8"
+          block-size="20"
+          @change="onAttentionChange"
+        />
+      </view>
+      <view class="score-item">
+        <view class="score-header">
+          <text class="score-label">专注力</text>
+          <text class="score-value">{{ form.focusScore }}</text>
+        </view>
+        <slider
+          :value="form.focusScore"
+          :min="0"
+          :max="100"
+          :step="1"
+          activeColor="#667eea"
+          backgroundColor="#E8E8E8"
+          block-size="20"
+          @change="onFocusChange"
+        />
+      </view>
+      <view class="score-item">
+        <view class="score-header">
+          <text class="score-label">记忆力</text>
+          <text class="score-value">{{ form.memoryScore }}</text>
+        </view>
+        <slider
+          :value="form.memoryScore"
+          :min="0"
+          :max="100"
+          :step="1"
+          activeColor="#667eea"
+          backgroundColor="#E8E8E8"
+          block-size="20"
+          @change="onMemoryChange"
+        />
+      </view>
+      <view class="score-item">
+        <view class="score-header">
+          <text class="score-label">逻辑力</text>
+          <text class="score-value">{{ form.logicScore }}</text>
+        </view>
+        <slider
+          :value="form.logicScore"
+          :min="0"
+          :max="100"
+          :step="1"
+          activeColor="#667eea"
+          backgroundColor="#E8E8E8"
+          block-size="20"
+          @change="onLogicChange"
+        />
+      </view>
+    </view>
+
+    <!-- 综合评分 -->
+    <view class="overall-score">
+      <text class="overall-label">综合评分</text>
+      <view class="overall-badge">
+        <text class="overall-number">{{ overallScore }}</text>
+      </view>
+    </view>
+  </view>
+
+  <!-- 测评信息 -->
+  <view class="section">
+    <view class="section-title">测评信息</view>
+    <view class="form-group">
+      <text class="form-label">测评日期</text>
+      <picker mode="date" :value="form.assessmentDate" @change="onAssessmentDateChange">
+        <view class="picker-value">
+          <text>{{ form.assessmentDate || '选择日期' }}</text>
+          <text class="picker-arrow">›</text>
+        </view>
+      </picker>
+    </view>
+    <view class="form-group">
+      <text class="form-label">测评时长(分钟)</text>
+      <input
+        class="form-input"
+        type="number"
+        v-model="form.durationMinutes"
+        placeholder="请输入测评时长"
+      />
+    </view>
+  </view>
+
+  <!-- 媒体资料 -->
+  <view class="section">
+    <view class="section-title">媒体资料</view>
+    <text class="form-hint">上传测评过程中的照片、视频或音频</text>
+    <view class="media-grid">
+      <view
+        class="media-item"
+        v-for="(img, index) in mediaList"
+        :key="index"
+      >
+        <image class="media-image" :src="img" mode="aspectFill" />
+        <view class="media-remove" @click="removeMedia(index)">✕</view>
+      </view>
+      <view class="media-add" @click="chooseMedia" v-if="mediaList.length < 9">
+        <text class="media-add-icon">+</text>
+        <text class="media-add-text">添加</text>
+      </view>
+    </view>
+  </view>
+
+  <!-- 分析报告 -->
+  <view class="section">
+    <view class="section-title">分析报告</view>
+    <textarea
+      class="form-textarea"
+      v-model="form.analysisReport"
+      placeholder="请输入详细的测评分析报告"
+      :maxlength="2000"
+    />
+    <text class="char-count">{{ analysisReportLen }}/2000</text>
+  </view>
+
+  <!-- 成长建议 -->
+  <view class="section">
+    <view class="section-title">成长建议</view>
+    <textarea
+      class="form-textarea"
+      v-model="form.growthSuggestions"
+      placeholder="请输入针对孩子的成长建议"
+      :maxlength="1000"
+    />
+    <text class="char-count">{{ growthSuggestionsLen }}/1000</text>
+  </view>
+
+  <!-- 下次复查 -->
+  <view class="section">
+    <view class="section-title">下次复查</view>
+    <picker mode="date" :value="form.nextReviewDate" @change="onNextReviewDateChange">
+      <view class="picker-value">
+        <text>{{ form.nextReviewDate || '选择复查日期' }}</text>
+        <text class="picker-arrow">›</text>
+      </view>
+    </picker>
+  </view>
+
+  <!-- 结果描述 -->
+  <view class="section">
+    <view class="section-title">结果描述</view>
+    <textarea
+      class="form-textarea form-textarea-short"
+      v-model="form.resultDescription"
+      placeholder="请简要描述测评结果"
+      :maxlength="500"
+    />
+    <text class="char-count">{{ resultDescriptionLen }}/500</text>
+  </view>
+
+  <!-- 操作按钮 -->
+  <view class="btn-group">
+    <button class="btn-draft" @click="saveDraft" :disabled="submitting">保存草稿</button>
+    <button class="btn-submit" @click="submit" :disabled="submitting">提交结果</button>
+  </view>
+</view>
+</template>
+
+<script>
+import { recordAssessmentResult } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      appointmentId: '',
+      childId: '',
+      childName: '',
+      age: '',
+      school: '',
+      mediaList: [],
+      submitting: false,
+      form: {
+        danLevel: '',
+        attentionScore: 50,
+        focusScore: 50,
+        memoryScore: 50,
+        logicScore: 50,
+        assessmentDate: '',
+        durationMinutes: '',
+        analysisReport: '',
+        growthSuggestions: '',
+        nextReviewDate: '',
+        resultDescription: ''
+      }
+    }
+  },
+  computed: {
+    overallScore() {
+      var total = this.form.attentionScore + this.form.focusScore + this.form.memoryScore + this.form.logicScore
+      return Math.round(total / 4)
+    },
+    analysisReportLen() {
+      return (this.form.analysisReport && this.form.analysisReport.length) || 0
+    },
+    growthSuggestionsLen() {
+      return (this.form.growthSuggestions && this.form.growthSuggestions.length) || 0
+    },
+    resultDescriptionLen() {
+      return (this.form.resultDescription && this.form.resultDescription.length) || 0
+    }
+  },
+  onLoad(options) {
+    if (options.appointmentId) {
+      this.appointmentId = options.appointmentId
+    }
+    if (options.childId) {
+      this.childId = options.childId
+    }
+    if (options.childName) {
+      this.childName = decodeURIComponent(options.childName)
+    }
+    if (options.age) {
+      this.age = options.age
+    }
+    if (options.school) {
+      this.school = decodeURIComponent(options.school)
+    }
+    // 默认测评日期为今天
+    var now = new Date()
+    var year = now.getFullYear()
+    var month = String(now.getMonth() + 1).padStart(2, '0')
+    var day = String(now.getDate()).padStart(2, '0')
+    this.form.assessmentDate = year + '-' + month + '-' + day
+  },
+  methods: {
+    selectLevel(level) {
+      this.form.danLevel = level
+    },
+
+    onAttentionChange(e) {
+      this.form.attentionScore = e.detail.value
+    },
+
+    onFocusChange(e) {
+      this.form.focusScore = e.detail.value
+    },
+
+    onMemoryChange(e) {
+      this.form.memoryScore = e.detail.value
+    },
+
+    onLogicChange(e) {
+      this.form.logicScore = e.detail.value
+    },
+
+    onAssessmentDateChange(e) {
+      this.form.assessmentDate = e.detail.value
+    },
+
+    onNextReviewDateChange(e) {
+      this.form.nextReviewDate = e.detail.value
+    },
+
+    chooseMedia() {
+      var that = this
+      uni.chooseImage({
+        count: 9 - that.mediaList.length,
+        sizeType: ['compressed'],
+        sourceType: ['album', 'camera'],
+        success: function(res) {
+          that.mediaList = that.mediaList.concat(res.tempFilePaths)
+        }
+      })
+    },
+
+    removeMedia(index) {
+      this.mediaList.splice(index, 1)
+    },
+
+    validateForm() {
+      if (!this.form.danLevel) {
+        uni.showToast({ title: '请选择DAN等级', icon: 'none' })
+        return false
+      }
+      if (!this.form.assessmentDate) {
+        uni.showToast({ title: '请选择测评日期', icon: 'none' })
+        return false
+      }
+      if (!this.form.durationMinutes || this.form.durationMinutes <= 0) {
+        uni.showToast({ title: '请输入有效的测评时长', icon: 'none' })
+        return false
+      }
+      if (!this.form.analysisReport || !this.form.analysisReport.trim()) {
+        uni.showToast({ title: '请填写分析报告', icon: 'none' })
+        return false
+      }
+      return true
+    },
+
+    buildRequestData(status) {
+      var teacherId = uni.getStorageSync('userId')
+      return {
+        childId: this.childId,
+        teacherId: teacherId,
+        appointmentId: this.appointmentId,
+        danLevel: this.form.danLevel,
+        attentionScore: this.form.attentionScore,
+        focusScore: this.form.focusScore,
+        memoryScore: this.form.memoryScore,
+        logicScore: this.form.logicScore,
+        overallScore: this.overallScore,
+        assessmentDate: this.form.assessmentDate,
+        durationMinutes: Number(this.form.durationMinutes),
+        analysisReport: this.form.analysisReport,
+        growthSuggestions: this.form.growthSuggestions,
+        nextReviewDate: this.form.nextReviewDate,
+        resultDescription: this.form.resultDescription,
+        mediaUrls: this.mediaList.join(','),
+        status: status
+      }
+    },
+
+    async saveDraft() {
+      if (!this.childId) {
+        uni.showToast({ title: '缺少孩子信息', icon: 'none' })
+        return
+      }
+
+      try {
+        uni.showLoading({ title: '保存中...' })
+        this.submitting = true
+
+        var data = this.buildRequestData('draft')
+        var res = await recordAssessmentResult(data)
+
+        uni.hideLoading()
+        this.submitting = false
+
+        if (res.code === 200) {
+          uni.showToast({ title: '草稿已保存', icon: 'success' })
+        } else {
+          uni.showToast({ title: res.message || '保存失败', icon: 'none' })
+        }
+      } catch (e) {
+        uni.hideLoading()
+        this.submitting = false
+        uni.showToast({ title: '保存失败', icon: 'none' })
+      }
+    },
+
+    async submit() {
+      if (!this.validateForm()) {
+        return
+      }
+
+      try {
+        uni.showLoading({ title: '提交中...' })
+        this.submitting = true
+
+        var data = this.buildRequestData('completed')
+        var res = await recordAssessmentResult(data)
+
+        uni.hideLoading()
+        this.submitting = false
+
+        if (res.code === 200) {
+          uni.showModal({
+            title: '提交成功',
+            content: '测评结果已提交,家长将收到评估报告',
+            showCancel: false,
+            success: function() {
+              uni.navigateBack()
+            }
+          })
+        } else {
+          uni.showToast({ title: res.message || '提交失败', icon: 'none' })
+        }
+      } catch (e) {
+        uni.hideLoading()
+        this.submitting = false
+        uni.showToast({ title: '提交失败', icon: 'none' })
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  padding: 30rpx;
+  background: #F8F8F8;
+  min-height: 100vh;
+}
+
+.header {
+  text-align: center;
+  padding: 60rpx 0 40rpx;
+}
+
+.title {
+  font-size: 48rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 20rpx;
+}
+
+.subtitle {
+  font-size: 28rpx;
+  color: #666;
+}
+
+.section {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 30rpx;
+  margin-bottom: 30rpx;
+}
+
+.section-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 20rpx;
+}
+
+/* 孩子信息 */
+.info-card {
+  background: #F8F8F8;
+  border-radius: 10rpx;
+  padding: 20rpx;
+}
+
+.info-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 15rpx;
+}
+
+.info-row:last-child {
+  margin-bottom: 0;
+}
+
+.info-label {
+  font-size: 28rpx;
+  color: #999;
+}
+
+.info-value {
+  font-size: 28rpx;
+  color: #333;
+  font-weight: bold;
+}
+
+/* DAN等级选择 */
+.level-selector {
+  display: flex;
+  flex-direction: column;
+  gap: 20rpx;
+}
+
+.level-item {
+  display: flex;
+  align-items: center;
+  background: #F8F8F8;
+  border: 2rpx solid #E0E0E0;
+  border-radius: 10rpx;
+  padding: 24rpx 20rpx;
+}
+
+.level-item.selected {
+  border-color: #667eea;
+  background: #F0F4FF;
+}
+
+.level-code {
+  width: 60rpx;
+  height: 60rpx;
+  line-height: 60rpx;
+  text-align: center;
+  background: linear-gradient(135deg, #667eea, #764ba2);
+  color: #fff;
+  border-radius: 50%;
+  font-size: 30rpx;
+  font-weight: bold;
+  margin-right: 20rpx;
+  flex-shrink: 0;
+}
+
+.level-item.selected .level-code {
+  background: linear-gradient(135deg, #667eea, #764ba2);
+  box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.4);
+}
+
+.level-desc {
+  font-size: 28rpx;
+  color: #666;
+}
+
+.level-item.selected .level-desc {
+  color: #333;
+  font-weight: bold;
+}
+
+/* 评分 */
+.score-group {
+  margin-bottom: 20rpx;
+}
+
+.score-item {
+  margin-bottom: 20rpx;
+}
+
+.score-item:last-child {
+  margin-bottom: 0;
+}
+
+.score-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 10rpx;
+}
+
+.score-label {
+  font-size: 28rpx;
+  color: #333;
+  font-weight: bold;
+}
+
+.score-value {
+  font-size: 32rpx;
+  color: #667eea;
+  font-weight: bold;
+}
+
+.overall-score {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border-radius: 10rpx;
+  padding: 24rpx 30rpx;
+  margin-top: 20rpx;
+}
+
+.overall-label {
+  font-size: 30rpx;
+  color: #fff;
+  font-weight: bold;
+}
+
+.overall-badge {
+  background: rgba(255, 255, 255, 0.2);
+  border-radius: 50%;
+  width: 80rpx;
+  height: 80rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.overall-number {
+  font-size: 36rpx;
+  color: #fff;
+  font-weight: bold;
+}
+
+/* 表单 */
+.form-group {
+  margin-bottom: 20rpx;
+}
+
+.form-group:last-child {
+  margin-bottom: 0;
+}
+
+.form-label {
+  font-size: 28rpx;
+  color: #333;
+  font-weight: bold;
+  display: block;
+  margin-bottom: 10rpx;
+}
+
+.form-hint {
+  font-size: 24rpx;
+  color: #999;
+  display: block;
+  margin-bottom: 15rpx;
+}
+
+.form-input {
+  height: 80rpx;
+  background: #F8F8F8;
+  border-radius: 10rpx;
+  padding: 0 20rpx;
+  font-size: 28rpx;
+  color: #333;
+}
+
+.picker-value {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  height: 80rpx;
+  background: #F8F8F8;
+  border-radius: 10rpx;
+  padding: 0 20rpx;
+  font-size: 28rpx;
+  color: #333;
+}
+
+.picker-arrow {
+  font-size: 40rpx;
+  color: #999;
+}
+
+.form-textarea {
+  width: 100%;
+  height: 240rpx;
+  background: #F8F8F8;
+  border-radius: 10rpx;
+  padding: 20rpx;
+  font-size: 28rpx;
+  color: #333;
+  box-sizing: border-box;
+}
+
+.form-textarea-short {
+  height: 160rpx;
+}
+
+.char-count {
+  display: block;
+  text-align: right;
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 10rpx;
+}
+
+/* 媒体 */
+.media-grid {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 15rpx;
+}
+
+.media-item {
+  position: relative;
+  width: 200rpx;
+  height: 200rpx;
+}
+
+.media-image {
+  width: 200rpx;
+  height: 200rpx;
+  border-radius: 10rpx;
+  object-fit: cover;
+}
+
+.media-remove {
+  position: absolute;
+  top: -10rpx;
+  right: -10rpx;
+  width: 40rpx;
+  height: 40rpx;
+  background: #FF6B6B;
+  color: #fff;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 24rpx;
+}
+
+.media-add {
+  width: 200rpx;
+  height: 200rpx;
+  background: #F8F8F8;
+  border: 2rpx dashed #D0D0D0;
+  border-radius: 10rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+
+.media-add-icon {
+  font-size: 60rpx;
+  color: #999;
+  line-height: 1;
+}
+
+.media-add-text {
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 10rpx;
+}
+
+/* 按钮 */
+.btn-group {
+  display: flex;
+  gap: 20rpx;
+  margin-top: 40rpx;
+  padding-bottom: 60rpx;
+}
+
+.btn-draft {
+  flex: 1;
+  height: 90rpx;
+  background: #fff;
+  color: #667eea;
+  border: 2rpx solid #667eea;
+  border-radius: 45rpx;
+  font-size: 30rpx;
+  font-weight: bold;
+  line-height: 90rpx;
+}
+
+.btn-submit {
+  flex: 1;
+  height: 90rpx;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: #fff;
+  border-radius: 45rpx;
+  font-size: 30rpx;
+  font-weight: bold;
+  line-height: 90rpx;
+}
+
+.btn-draft[disabled],
+.btn-submit[disabled] {
+  opacity: 0.6;
+}
+</style>

+ 20 - 3
zxyj-frontend/pages/index/parent-index.vue

@@ -138,6 +138,10 @@
           <view class="action-icon reward-icon">🎁</view>
           <view class="action-icon reward-icon">🎁</view>
           <text class="action-label">奖励中心</text>
           <text class="action-label">奖励中心</text>
         </view>
         </view>
+        <view class="action-item" @click="goToGrowth">
+          <view class="action-icon growth-icon">🌱</view>
+          <text class="action-label">成长档案</text>
+        </view>
         <view class="action-item" @click="inviteFriend">
         <view class="action-item" @click="inviteFriend">
           <view class="action-icon invite-icon">📤</view>
           <view class="action-icon invite-icon">📤</view>
           <text class="action-label">邀请家人</text>
           <text class="action-label">邀请家人</text>
@@ -281,6 +285,9 @@ export default {
     },
     },
     inviteFriend() {
     inviteFriend() {
       uni.navigateTo({ url: '/pages/parent/invite/index' })
       uni.navigateTo({ url: '/pages/parent/invite/index' })
+    },
+    goToGrowth() {
+      uni.navigateTo({ url: '/pages/growth/index' })
     }
     }
   }
   }
 }
 }
@@ -647,10 +654,11 @@ export default {
 
 
 .quick-actions {
 .quick-actions {
   display: flex;
   display: flex;
-  justify-content: space-between;
+  flex-wrap: wrap;
+  justify-content: flex-start;
   background: #FFFFFF;
   background: #FFFFFF;
   border-radius: 24rpx;
   border-radius: 24rpx;
-  padding: 24rpx;
+  padding: 24rpx 12rpx;
   box-shadow: 0 4px 20px rgba(255, 140, 66, 0.08);
   box-shadow: 0 4px 20px rgba(255, 140, 66, 0.08);
 }
 }
 
 
@@ -658,7 +666,12 @@ export default {
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
   align-items: center;
   align-items: center;
-  width: 20%;
+  width: 33.33%;
+  margin-bottom: 20rpx;
+}
+
+.action-item:nth-last-child(-n+2) {
+  margin-bottom: 0;
 }
 }
 
 
 .action-icon {
 .action-icon {
@@ -692,6 +705,10 @@ export default {
   background: rgba(0, 184, 148, 0.15);
   background: rgba(0, 184, 148, 0.15);
 }
 }
 
 
+.growth-icon {
+  background: rgba(78, 205, 196, 0.15);
+}
+
 .action-label {
 .action-label {
   font-size: 24rpx;
   font-size: 24rpx;
   color: #636E72;
   color: #636E72;

+ 16 - 0
zxyj-frontend/pages/login/login.vue

@@ -224,6 +224,14 @@ export default {
           userId: res.data.userId
           userId: res.data.userId
         })
         })
 
 
+        // 存储成长规划师审核状态
+        if (res.data.teacherStatus) {
+          uni.setStorageSync('teacherStatus', res.data.teacherStatus)
+        }
+        if (res.data.teacherRejectReason) {
+          uni.setStorageSync('teacherRejectReason', res.data.teacherRejectReason)
+        }
+
         uni.showToast({ title: '登录成功', icon: 'success' })
         uni.showToast({ title: '登录成功', icon: 'success' })
 
 
         // 根据角色跳转到对应首页,或进入信息编辑页面
         // 根据角色跳转到对应首页,或进入信息编辑页面
@@ -292,6 +300,14 @@ async handleWechatPhoneLogin(e) {
             userId: res.data.userId
             userId: res.data.userId
           })
           })
 
 
+          // 存储成长规划师审核状态
+          if (res.data.teacherStatus) {
+            uni.setStorageSync('teacherStatus', res.data.teacherStatus)
+          }
+          if (res.data.teacherRejectReason) {
+            uni.setStorageSync('teacherRejectReason', res.data.teacherRejectReason)
+          }
+
           uni.showToast({ title: '登录成功', icon: 'success' })
           uni.showToast({ title: '登录成功', icon: 'success' })
 
 
           // 根据角色跳转到对应首页,或进入信息编辑页面
           // 根据角色跳转到对应首页,或进入信息编辑页面

+ 239 - 0
zxyj-frontend/pages/parent/appointments.vue

@@ -0,0 +1,239 @@
+<template>
+  <view class="container">
+    <view class="header">
+      <text class="title">我的预约</text>
+      <text class="subtitle">查看测评预约状态</text>
+    </view>
+
+    <!-- 预约列表 -->
+    <view class="appointment-list" v-if="appointments.length > 0">
+      <view class="appointment-card" v-for="item in appointments" :key="item.id">
+        <view class="card-header">
+          <text class="appointment-id">#{{ item.id }}</text>
+          <view class="status-badge" :class="'status-' + item.status">
+            <text>{{ item.statusText || getStatusText(item.status) }}</text>
+          </view>
+        </view>
+
+        <view class="card-body">
+          <view class="info-row">
+            <text class="label">预约日期</text>
+            <text class="value">{{ item.appointmentDate || '待确认' }}</text>
+          </view>
+          <view class="info-row">
+            <text class="label">预约时间</text>
+            <text class="value">{{ item.appointmentTime || '待确认' }}</text>
+          </view>
+          <view class="info-row" v-if="item.notes">
+            <text class="label">备注</text>
+            <text class="value">{{ item.notes }}</text>
+          </view>
+          <view class="info-row">
+            <text class="label">创建时间</text>
+            <text class="value">{{ formatTime(item.createdAt) }}</text>
+          </view>
+        </view>
+
+        <view class="card-footer" v-if="item.status === 0">
+          <button class="btn-cancel" size="mini" @click="handleCancel(item.id)">取消预约</button>
+        </view>
+        <view class="card-footer" v-if="item.status === 2">
+          <button class="btn-view-report" size="mini" @click="viewReport(item.id)">查看报告</button>
+        </view>
+      </view>
+    </view>
+
+    <view class="empty-state" v-else>
+      <text class="empty-icon">📋</text>
+      <text class="empty-text">暂无预约记录</text>
+      <text class="empty-hint">申请测评后此处会显示预约状态</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getMyAppointments, cancelAppointment } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      appointments: []
+    }
+  },
+  onShow() {
+    this.loadAppointments()
+  },
+  methods: {
+    async loadAppointments() {
+      try {
+        const res = await getMyAppointments()
+        if (res.code === 200) {
+          this.appointments = res.data || []
+        }
+      } catch (e) {
+        console.error('加载预约列表失败', e)
+      }
+    },
+
+    getStatusText(status) {
+      const map = { 0: '待确认', 1: '已确认', 2: '已完成', 3: '已取消', 4: '未到' }
+      return map[status] || '未知'
+    },
+
+    formatTime(ts) {
+      if (!ts) return ''
+      const d = new Date(ts)
+      return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
+    },
+
+    handleCancel(id) {
+      uni.showModal({
+        title: '取消预约',
+        content: '确定取消此预约?',
+        success: async (res) => {
+          if (res.confirm) {
+            try {
+              const result = await cancelAppointment(id)
+              if (result.code === 200) {
+                uni.showToast({ title: '已取消', icon: 'success' })
+                this.loadAppointments()
+              }
+            } catch (e) {
+              uni.showToast({ title: '操作失败', icon: 'none' })
+            }
+          }
+        }
+      })
+    },
+
+    viewReport(appointmentId) {
+      uni.navigateTo({ url: '/pages/assessment/results' })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  padding: 30rpx;
+  background: #F8F8F8;
+  min-height: 100vh;
+}
+
+.header {
+  text-align: center;
+  padding: 40rpx 0 30rpx;
+}
+
+.title {
+  font-size: 48rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 16rpx;
+}
+
+.subtitle {
+  font-size: 26rpx;
+  color: #666;
+}
+
+.appointment-list {
+  display: flex;
+  flex-direction: column;
+  gap: 24rpx;
+}
+
+.appointment-card {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 30rpx;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20rpx;
+  padding-bottom: 16rpx;
+  border-bottom: 1rpx solid #eee;
+}
+
+.appointment-id {
+  font-size: 24rpx;
+  color: #999;
+}
+
+.status-badge {
+  padding: 6rpx 16rpx;
+  border-radius: 8rpx;
+  font-size: 22rpx;
+}
+
+.status-0 { background: #FFF3E0; color: #FF9800; }
+.status-1 { background: #E3F2FD; color: #2196F3; }
+.status-2 { background: #E8F5E9; color: #4CAF50; }
+.status-3 { background: #F5F5F5; color: #9E9E9E; }
+.status-4 { background: #FFEBEE; color: #F44336; }
+
+.card-body {
+  margin-bottom: 20rpx;
+}
+
+.info-row {
+  display: flex;
+  justify-content: space-between;
+  padding: 10rpx 0;
+  font-size: 26rpx;
+}
+
+.label { color: #999; }
+.value { color: #333; }
+
+.card-footer {
+  padding-top: 16rpx;
+  border-top: 1rpx dashed #eee;
+  display: flex;
+  justify-content: flex-end;
+}
+
+.btn-cancel {
+  background: #F5F5F5;
+  color: #999;
+  font-size: 26rpx;
+  border-radius: 10rpx;
+  width: 200rpx;
+}
+
+.btn-view-report {
+  background: #667eea;
+  color: #fff;
+  font-size: 26rpx;
+  border-radius: 10rpx;
+  width: 200rpx;
+}
+
+.empty-state {
+  text-align: center;
+  padding: 120rpx 0;
+}
+
+.empty-icon {
+  font-size: 80rpx;
+  display: block;
+  margin-bottom: 20rpx;
+}
+
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+  display: block;
+  margin-bottom: 10rpx;
+}
+
+.empty-hint {
+  font-size: 24rpx;
+  color: #CCC;
+  display: block;
+}
+</style>

+ 313 - 0
zxyj-frontend/pages/teacher/appointments.vue

@@ -0,0 +1,313 @@
+<template>
+  <view class="container">
+    <view class="header">
+      <text class="title">预约管理</text>
+      <text class="subtitle">查看并管理测评预约</text>
+    </view>
+
+    <!-- 状态筛选 -->
+    <view class="filter-bar">
+      <view class="filter-item" :class="{ active: filterStatus === null }" @click="filterStatus = null">
+        <text>全部</text>
+      </view>
+      <view class="filter-item" :class="{ active: filterStatus === 0 }" @click="filterStatus = 0">
+        <text>待确认</text>
+      </view>
+      <view class="filter-item" :class="{ active: filterStatus === 1 }" @click="filterStatus = 1">
+        <text>已确认</text>
+      </view>
+      <view class="filter-item" :class="{ active: filterStatus === 2 }" @click="filterStatus = 2">
+        <text>已完成</text>
+      </view>
+    </view>
+
+    <!-- 预约列表 -->
+    <view class="appointment-list">
+      <view class="appointment-card" v-for="item in filteredList" :key="item.id">
+        <view class="card-header">
+          <view class="child-info">
+            <text class="child-name">孩子ID: {{ item.childId || '未指定' }}</text>
+          </view>
+          <view class="status-badge" :class="'status-' + item.status">
+            <text>{{ getStatusText(item.status) }}</text>
+          </view>
+        </view>
+
+        <view class="card-body">
+          <view class="info-row">
+            <text class="label">预约日期</text>
+            <text class="value">{{ item.appointmentDate || '未设置' }}</text>
+          </view>
+          <view class="info-row">
+            <text class="label">预约时间</text>
+            <text class="value">{{ item.appointmentTime || '未设置' }}</text>
+          </view>
+          <view class="info-row" v-if="item.notes">
+            <text class="label">备注</text>
+            <text class="value">{{ item.notes }}</text>
+          </view>
+          <view class="info-row">
+            <text class="label">创建时间</text>
+            <text class="value">{{ formatTime(item.createdAt) }}</text>
+          </view>
+        </view>
+
+        <view class="card-actions">
+          <button v-if="item.status === 0" class="btn-confirm" size="mini" @click="handleConfirm(item.id)">确认预约</button>
+          <button v-if="item.status === 1" class="btn-complete" size="mini" @click="handleComplete(item.id)">录入结果</button>
+          <button v-if="item.status === 0" class="btn-cancel" size="mini" @click="handleCancel(item.id)">取消</button>
+        </view>
+      </view>
+    </view>
+
+    <view class="empty-state" v-if="filteredList.length === 0">
+      <text class="empty-icon">📋</text>
+      <text class="empty-text">暂无预约记录</text>
+    </view>
+    <view class="bottom-space"></view>
+  </view>
+</template>
+
+<script>
+import { getGuideAppointments, confirmAppointment, cancelAppointment } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      appointments: [],
+      filterStatus: null
+    }
+  },
+  computed: {
+    filteredList() {
+      if (this.filterStatus === null) return this.appointments
+      return this.appointments.filter(item => item.status === this.filterStatus)
+    }
+  },
+  onShow() {
+    this.loadAppointments()
+  },
+  methods: {
+    async loadAppointments() {
+      try {
+        const res = await getGuideAppointments()
+        if (res.code === 200) {
+          this.appointments = res.data || []
+        }
+      } catch (e) {
+        console.error('加载预约列表失败', e)
+      }
+    },
+
+    getStatusText(status) {
+      const map = { 0: '待确认', 1: '已确认', 2: '已完成', 3: '已取消', 4: '未到' }
+      return map[status] || '未知'
+    },
+
+    formatTime(ts) {
+      if (!ts) return ''
+      const d = new Date(ts)
+      return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
+    },
+
+    handleConfirm(id) {
+      uni.showModal({
+        title: '确认预约',
+        content: '确认此预约?',
+        success: async (res) => {
+          if (res.confirm) {
+            try {
+              const result = await confirmAppointment(id)
+              if (result.code === 200) {
+                uni.showToast({ title: '已确认', icon: 'success' })
+                this.loadAppointments()
+              }
+            } catch (e) {
+              uni.showToast({ title: '操作失败', icon: 'none' })
+            }
+          }
+        }
+      })
+    },
+
+    handleComplete(id) {
+      uni.navigateTo({ url: '/pages/assessment/record?appointmentId=' + id })
+    },
+
+    handleCancel(id) {
+      uni.showModal({
+        title: '取消预约',
+        content: '确定取消此预约?',
+        success: async (res) => {
+          if (res.confirm) {
+            try {
+              const result = await cancelAppointment(id)
+              if (result.code === 200) {
+                uni.showToast({ title: '已取消', icon: 'success' })
+                this.loadAppointments()
+              }
+            } catch (e) {
+              uni.showToast({ title: '操作失败', icon: 'none' })
+            }
+          }
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  padding: 30rpx;
+  background: #F8F8F8;
+  min-height: 100vh;
+}
+
+.header {
+  text-align: center;
+  padding: 40rpx 0 30rpx;
+}
+
+.title {
+  font-size: 48rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 16rpx;
+}
+
+.subtitle {
+  font-size: 26rpx;
+  color: #666;
+}
+
+.filter-bar {
+  display: flex;
+  gap: 16rpx;
+  margin-bottom: 30rpx;
+  padding: 16rpx;
+  background: #fff;
+  border-radius: 20rpx;
+}
+
+.filter-item {
+  flex: 1;
+  text-align: center;
+  padding: 12rpx 0;
+  border-radius: 10rpx;
+  font-size: 26rpx;
+  color: #666;
+  background: #F8F8F8;
+}
+
+.filter-item.active {
+  background: #667eea;
+  color: #fff;
+}
+
+.appointment-list {
+  display: flex;
+  flex-direction: column;
+  gap: 24rpx;
+}
+
+.appointment-card {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 30rpx;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20rpx;
+  padding-bottom: 16rpx;
+  border-bottom: 1rpx solid #eee;
+}
+
+.child-name {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+}
+
+.status-badge {
+  padding: 6rpx 16rpx;
+  border-radius: 8rpx;
+  font-size: 22rpx;
+}
+
+.status-0 { background: #FFF3E0; color: #FF9800; }
+.status-1 { background: #E3F2FD; color: #2196F3; }
+.status-2 { background: #E8F5E9; color: #4CAF50; }
+.status-3 { background: #F5F5F5; color: #9E9E9E; }
+.status-4 { background: #FFEBEE; color: #F44336; }
+
+.card-body {
+  margin-bottom: 20rpx;
+}
+
+.info-row {
+  display: flex;
+  justify-content: space-between;
+  padding: 10rpx 0;
+  font-size: 26rpx;
+}
+
+.label { color: #999; }
+.value { color: #333; }
+
+.card-actions {
+  display: flex;
+  gap: 16rpx;
+  padding-top: 16rpx;
+  border-top: 1rpx dashed #eee;
+}
+
+.btn-confirm {
+  flex: 1;
+  background: #667eea;
+  color: #fff;
+  font-size: 26rpx;
+  border-radius: 10rpx;
+}
+
+.btn-complete {
+  flex: 1;
+  background: #4CAF50;
+  color: #fff;
+  font-size: 26rpx;
+  border-radius: 10rpx;
+}
+
+.btn-cancel {
+  flex: 1;
+  background: #F5F5F5;
+  color: #999;
+  font-size: 26rpx;
+  border-radius: 10rpx;
+}
+
+.empty-state {
+  text-align: center;
+  padding: 120rpx 0;
+}
+
+.empty-icon {
+  font-size: 80rpx;
+  display: block;
+  margin-bottom: 20rpx;
+}
+
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+  display: block;
+}
+
+.bottom-space {
+  height: 140rpx;
+}
+</style>

+ 492 - 0
zxyj-frontend/pages/teacher/teacher-home.vue

@@ -0,0 +1,492 @@
+<template>
+  <view class="container">
+    <!-- 顶部信息栏 -->
+    <view class="header">
+      <view class="user-info">
+        <image class="avatar" :src="teacherInfo.avatar || '/static/default-avatar.png'" mode="aspectFill"></image>
+        <view class="info-text">
+          <text class="nickname">{{ teacherInfo.nickname || '成长规划师' }}</text>
+          <view class="status-badge" v-if="teacherStatus !== 'approved'">
+            <text class="badge-text">{{ teacherStatus === 'pending' ? '审核中' : teacherStatus }}</text>
+          </view>
+        </view>
+      </view>
+      <view class="switch-btn" @click="switchToParent">
+        <text class="switch-text">切换家长端</text>
+      </view>
+    </view>
+
+    <!-- 审核进度提示 -->
+    <view class="audit-section" v-if="teacherStatus === 'pending'">
+      <view class="audit-card">
+        <view class="audit-title">申请审核中</view>
+        <view class="audit-steps">
+          <view class="step completed">
+            <view class="step-dot"></view>
+            <text class="step-text">提交申请</text>
+          </view>
+          <view class="step-line completed"></view>
+          <view class="step active">
+            <view class="step-dot"></view>
+            <text class="step-text">管理员审核</text>
+          </view>
+          <view class="step-line"></view>
+          <view class="step">
+            <view class="step-dot"></view>
+            <text class="step-text">审核通过</text>
+          </view>
+        </view>
+        <text class="audit-hint">您的成长规划师申请正在审核中,通常1-3个工作日内完成</text>
+      </view>
+    </view>
+
+    <!-- 审核拒绝提示 -->
+    <view class="audit-section" v-if="teacherStatus === 'rejected'">
+      <view class="audit-card rejected">
+        <view class="audit-title">申请未通过</view>
+        <view class="reject-reason" v-if="teacherRejectReason">
+          <text class="reason-label">拒绝原因:</text>
+          <text class="reason-text">{{ teacherRejectReason }}</text>
+        </view>
+        <view class="reject-reason" v-else>
+          <text class="reason-text">您的申请暂未通过审核</text>
+        </view>
+        <view class="reapply-btn" @click="reapply">
+          <text class="reapply-text">重新申请</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 数据概览 -->
+    <view class="stats-grid">
+      <view class="stat-card blue">
+        <text class="stat-value">{{ stats.serviceFamilyCount }}</text>
+        <text class="stat-label">服务家庭</text>
+      </view>
+      <view class="stat-card green">
+        <text class="stat-value">{{ stats.pendingTaskCount }}</text>
+        <text class="stat-label">待处理任务</text>
+      </view>
+      <view class="stat-card gold">
+        <text class="stat-value">¥{{ stats.todayIncome }}</text>
+        <text class="stat-label">今日收入</text>
+      </view>
+      <view class="stat-card purple">
+        <text class="stat-value">{{ stats.teamCount }}</text>
+        <text class="stat-label">团队成员</text>
+      </view>
+    </view>
+
+    <!-- 快捷功能(仅审核通过后可使用) -->
+      <view class="section" v-if="teacherStatus === 'approved'">
+        <view class="section-title">快捷功能</view>
+        <view class="menu-grid">
+          <view class="menu-item" @click="goTo('families')">
+            <view class="menu-icon blue">👨‍👩‍👧</view>
+            <text class="menu-label">服务家庭</text>
+          </view>
+          <view class="menu-item" @click="goTo('packages')">
+            <view class="menu-icon gold">📦</view>
+            <text class="menu-label">套餐管理</text>
+          </view>
+          <view class="menu-item" @click="goTo('team')">
+            <view class="menu-icon purple">👥</view>
+            <text class="menu-label">我的团队</text>
+          </view>
+          <view class="menu-item" @click="goTo('messages')">
+            <view class="menu-icon green">💬</view>
+            <text class="menu-label">消息中心</text>
+          </view>
+          <view class="menu-item" @click="goTo('orders')">
+            <view class="menu-icon cyan">📊</view>
+            <text class="menu-label">订单佣金</text>
+          </view>
+          <view class="menu-item" @click="goTo('growth')">
+            <view class="menu-icon gray">📋</view>
+            <text class="menu-label">成长档案</text>
+          </view>
+        </view>
+      </view>
+      <view class="section disabled-section" v-else>
+        <view class="section-title">快捷功能</view>
+        <view class="menu-grid disabled">
+          <view class="menu-item">
+            <view class="menu-icon blue disabled-icon">👨‍👩‍👧</view>
+            <text class="menu-label disabled-label">服务家庭</text>
+          </view>
+          <view class="menu-item">
+            <view class="menu-icon gold disabled-icon">📦</view>
+            <text class="menu-label disabled-label">套餐管理</text>
+          </view>
+          <view class="menu-item">
+            <view class="menu-icon purple disabled-icon">👥</view>
+            <text class="menu-label disabled-label">我的团队</text>
+          </view>
+          <view class="menu-item">
+            <view class="menu-icon green disabled-icon">💬</view>
+            <text class="menu-label disabled-label">消息中心</text>
+          </view>
+          <view class="menu-item">
+            <view class="menu-icon cyan disabled-icon">📊</view>
+            <text class="menu-label disabled-label">订单佣金</text>
+          </view>
+          <view class="menu-item">
+            <view class="menu-icon gray disabled-icon">📋</view>
+            <text class="menu-label disabled-label">成长档案</text>
+          </view>
+        </view>
+      </view>
+
+    <!-- 底部占位(TabBar高度) -->
+    <view class="bottom-space"></view>
+  </view>
+</template>
+
+<script>
+import { switchRole } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      teacherInfo: {},
+      stats: {
+        serviceFamilyCount: 0,
+        pendingTaskCount: 0,
+        todayIncome: 0,
+        teamCount: 0
+      }
+    }
+  },
+  computed: {
+    teacherStatus() {
+      return uni.getStorageSync('teacherStatus') || 'pending'
+    },
+    teacherRejectReason() {
+      return uni.getStorageSync('teacherRejectReason') || ''
+    }
+  },
+  onLoad() {
+    this.loadData()
+  },
+  onShow() {
+    this.loadData()
+  },
+  methods: {
+    loadData() {
+      const userInfo = uni.getStorageSync('userInfo')
+      this.teacherInfo = userInfo || {}
+    },
+    reapply() {
+      uni.navigateTo({ url: '/pages/guide/register/index' })
+    },
+    async switchToParent() {
+      try {
+        const result = await switchRole('parent')
+        if (result.code === 200 && result.data) {
+          const data = result.data
+          uni.setStorageSync('token', data.token)
+          uni.setStorageSync('role', data.role)
+          uni.setStorageSync('currentRole', 'parent')
+          uni.setStorageSync('isSwitchedChild', false)
+          uni.setStorageSync('isSwitchedTeacher', false)
+          uni.$emit('switchRole')
+          uni.reLaunch({ url: '/pages/index/index' })
+        }
+      } catch (e) {
+        uni.showToast({ title: e.message || '切换失败', icon: 'none' })
+      }
+    },
+    goTo(page) {
+      const routes = {
+        families: '/pages/teacher/families',
+        packages: '/pages/guide/packages/index',
+        team: '/pages/teacher/team',
+        messages: '/pages/teacher/messages',
+        orders: '/pages/teacher/orders',
+        growth: '/pages/growth/index'
+      }
+      uni.reLaunch({ url: routes[page] })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  padding: 24rpx;
+  background: #F0F7FF;
+  min-height: 100vh;
+}
+
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  margin-bottom: 24rpx;
+}
+
+.user-info {
+  display: flex;
+  align-items: center;
+}
+
+.avatar {
+  width: 96rpx;
+  height: 96rpx;
+  border-radius: 50%;
+  margin-right: 16rpx;
+}
+
+.info-text {
+  display: flex;
+  flex-direction: column;
+}
+
+.nickname {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #1E293B;
+}
+
+.status-badge {
+  background: #FEF3C7;
+  padding: 4rpx 12rpx;
+  border-radius: 8rpx;
+  margin-top: 8rpx;
+}
+
+.badge-text {
+  font-size: 22rpx;
+  color: #D97706;
+}
+
+.switch-btn {
+  background: #3B82F6;
+  padding: 16rpx 24rpx;
+  border-radius: 12rpx;
+}
+
+.switch-text {
+  color: #fff;
+  font-size: 24rpx;
+}
+
+.audit-section {
+  margin-bottom: 24rpx;
+}
+
+.audit-card {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 32rpx 24rpx;
+}
+
+.audit-card.rejected {
+  border-left: 6rpx solid #EF4444;
+}
+
+.audit-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #1E293B;
+  margin-bottom: 24rpx;
+}
+
+.audit-steps {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 24rpx;
+}
+
+.step {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  flex: 0 0 auto;
+}
+
+.step-dot {
+  width: 24rpx;
+  height: 24rpx;
+  border-radius: 50%;
+  background: #E2E8F0;
+  margin-bottom: 8rpx;
+}
+
+.step.completed .step-dot {
+  background: #10B981;
+}
+
+.step.active .step-dot {
+  background: #3B82F6;
+  box-shadow: 0 0 0 6rpx rgba(59, 130, 246, 0.2);
+}
+
+.step-text {
+  font-size: 22rpx;
+  color: #94A3B8;
+}
+
+.step.completed .step-text {
+  color: #10B981;
+}
+
+.step.active .step-text {
+  color: #3B82F6;
+  font-weight: bold;
+}
+
+.step-line {
+  width: 60rpx;
+  height: 4rpx;
+  background: #E2E8F0;
+  margin: 0 8rpx;
+  margin-bottom: 28rpx;
+}
+
+.step-line.completed {
+  background: #10B981;
+}
+
+.audit-hint {
+  font-size: 24rpx;
+  color: #64748B;
+  text-align: center;
+}
+
+.reject-reason {
+  background: #FEF2F2;
+  border-radius: 8rpx;
+  padding: 16rpx;
+  margin-bottom: 20rpx;
+}
+
+.reason-label {
+  font-size: 24rpx;
+  color: #991B1B;
+  font-weight: bold;
+}
+
+.reason-text {
+  font-size: 24rpx;
+  color: #DC2626;
+}
+
+.reapply-btn {
+  background: #3B82F6;
+  border-radius: 12rpx;
+  padding: 20rpx;
+  text-align: center;
+}
+
+.reapply-text {
+  color: #fff;
+  font-size: 28rpx;
+  font-weight: bold;
+}
+
+.stats-grid {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 16rpx;
+  margin-bottom: 24rpx;
+}
+
+.stat-card {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  text-align: center;
+}
+
+.stat-value {
+  font-size: 36rpx;
+  font-weight: bold;
+  display: block;
+  margin-bottom: 8rpx;
+}
+
+.stat-label {
+  font-size: 24rpx;
+  color: #64748B;
+}
+
+.stat-card.blue .stat-value { color: #3B82F6; }
+.stat-card.green .stat-value { color: #10B981; }
+.stat-card.gold .stat-value { color: #F59E0B; }
+.stat-card.purple .stat-value { color: #8B5CF6; }
+
+.section {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx;
+}
+
+.section-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #1E293B;
+  margin-bottom: 20rpx;
+}
+
+.menu-grid {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 20rpx;
+}
+
+.menu-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 20rpx 0;
+}
+
+.menu-icon {
+  width: 80rpx;
+  height: 80rpx;
+  border-radius: 16rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 36rpx;
+  margin-bottom: 12rpx;
+}
+
+.menu-icon.blue { background: rgba(59, 130, 246, 0.15); }
+.menu-icon.gold { background: rgba(245, 158, 11, 0.15); }
+.menu-icon.purple { background: rgba(139, 92, 246, 0.15); }
+.menu-icon.green { background: rgba(16, 185, 129, 0.15); }
+.menu-icon.cyan { background: rgba(6, 182, 212, 0.15); }
+.menu-icon.gray { background: rgba(100, 116, 139, 0.15); }
+
+.menu-label {
+  font-size: 24rpx;
+  color: #64748B;
+}
+
+.bottom-space {
+  height: 120rpx;
+}
+
+.disabled-section {
+  opacity: 0.6;
+}
+
+.disabled .menu-item {
+  pointer-events: none;
+}
+
+.disabled-icon {
+  filter: grayscale(100%);
+  opacity: 0.5;
+}
+
+.disabled-label {
+  color: #94A3B8;
+}
+</style>

+ 330 - 0
zxyj-frontend/pages/teacher/teacher-profile.vue

@@ -0,0 +1,330 @@
+<template>
+  <view class="container">
+    <!-- 退出切换按钮 -->
+    <view class="switch-out-btn" @click="switchToParent">
+      <text>🔄 切换家长端</text>
+    </view>
+
+    <view class="user-card">
+      <view class="avatar">👤</view>
+      <view class="user-info">
+        <view class="nickname">{{ nickname || '未设置昵称' }}</view>
+        <view class="role">成长规划师</view>
+        <view class="status-tag" :class="teacherStatus">{{ statusText }}</view>
+      </view>
+      <button class="btn-edit" @click="goToEditInfo">编辑资料</button>
+    </view>
+
+    <!-- 功能菜单 -->
+    <view class="menu-list">
+      <view class="menu-item" @click="goToEditInfo">
+        <text>📝 修改个人资料</text>
+        <text class="arrow">›</text>
+      </view>
+      <view class="menu-item" v-if="teacherStatus !== 'approved'" @click="showPendingInfo">
+        <text>📋 查看申请进度</text>
+        <text class="arrow">›</text>
+      </view>
+      <view class="menu-item" @click="goToGrowth">
+        <text>📊 我的成长档案</text>
+        <text class="arrow">›</text>
+      </view>
+      <view class="menu-item" @click="goToMessages">
+        <text>💬 消息中心</text>
+        <text class="arrow">›</text>
+      </view>
+      <view class="menu-item" @click="showPasswordModal = true">
+        <text>🔐 设置密码</text>
+        <text class="arrow">›</text>
+      </view>
+      <view class="menu-item" @click="logout">
+        <text>🚪 退出登录</text>
+        <text class="arrow">›</text>
+      </view>
+    </view>
+
+    <!-- 密码设置弹窗 -->
+    <view class="modal-mask" v-if="showPasswordModal" @click="showPasswordModal = false">
+      <view class="modal" @click.stop>
+        <view class="modal-title">设置密码</view>
+        <view class="form-item">
+          <input v-model="newPassword" type="number" password placeholder="输入6位密码" maxlength="6" />
+        </view>
+        <view class="form-item">
+          <input v-model="confirmPassword" type="number" password placeholder="确认密码" maxlength="6" />
+        </view>
+        <view class="modal-btns">
+          <button @click="showPasswordModal = false">取消</button>
+          <button class="btn-primary" @click="savePassword">确定</button>
+        </view>
+      </view>
+    </view>
+
+    <view class="bottom-space"></view>
+  </view>
+</template>
+
+<script>
+import { switchRole } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      nickname: '',
+      showPasswordModal: false,
+      newPassword: '',
+      confirmPassword: ''
+    }
+  },
+  computed: {
+    teacherStatus() {
+      return uni.getStorageSync('teacherStatus') || 'pending'
+    },
+    statusText() {
+      const map = { pending: '审核中', approved: '已通过', rejected: '已拒绝' }
+      return map[this.teacherStatus] || this.teacherStatus
+    }
+  },
+  onLoad() {
+    this.nickname = uni.getStorageSync('userInfo')?.nickname || ''
+  },
+  onShow() {
+    this.nickname = uni.getStorageSync('userInfo')?.nickname || ''
+  },
+  methods: {
+    async switchToParent() {
+      try {
+        const result = await switchRole('parent')
+        if (result.code === 200 && result.data) {
+          const data = result.data
+          uni.setStorageSync('token', data.token)
+          uni.setStorageSync('role', data.role)
+          uni.setStorageSync('currentRole', 'parent')
+          uni.setStorageSync('isSwitchedChild', false)
+          uni.setStorageSync('isSwitchedTeacher', false)
+          uni.$emit('switchRole')
+          uni.reLaunch({ url: '/pages/index/index' })
+        }
+      } catch (e) {
+        uni.showToast({ title: e.message || '切换失败', icon: 'none' })
+      }
+    },
+    goToEditInfo() {
+      uni.navigateTo({ url: '/pages/user-edit/user-edit' })
+    },
+    goToGrowth() {
+      uni.reLaunch({ url: '/pages/growth/index' })
+    },
+    goToMessages() {
+      uni.navigateTo({ url: '/pages/teacher/messages' })
+    },
+    showPendingInfo() {
+      const status = this.teacherStatus
+      let content = ''
+      if (status === 'pending') {
+        content = '您的成长规划师申请正在审核中,通常1-3个工作日内完成。请耐心等待管理员审批。'
+      } else if (status === 'rejected') {
+        const reason = uni.getStorageSync('teacherRejectReason')
+        content = reason ? ('您的申请未通过审核。拒绝原因:' + reason + '。您可以修改资料后重新申请。') : '您的申请暂未通过审核,您可以修改资料后重新申请。'
+      }
+      uni.showModal({
+        title: '申请状态',
+        content: content,
+        confirmText: status === 'rejected' ? '重新申请' : '我知道了',
+        showCancel: status === 'rejected',
+        cancelText: '关闭',
+        success: (res) => {
+          if (res.confirm && status === 'rejected') {
+            uni.navigateTo({ url: '/pages/guide/register/index' })
+          }
+        }
+      })
+    },
+    async savePassword() {
+      if (!this.newPassword || this.newPassword.length !== 6) {
+        uni.showToast({ title: '请输入6位密码', icon: 'none' })
+        return
+      }
+      if (this.newPassword !== this.confirmPassword) {
+        uni.showToast({ title: '两次密码不一致', icon: 'none' })
+        return
+      }
+      uni.showToast({ title: '密码设置成功', icon: 'success' })
+      this.showPasswordModal = false
+      this.newPassword = ''
+      this.confirmPassword = ''
+    },
+    logout() {
+      uni.showModal({
+        title: '提示',
+        content: '确定要退出登录吗?',
+        success: (res) => {
+          if (res.confirm) {
+            uni.clearStorage()
+            uni.reLaunch({ url: '/pages/login/login' })
+          }
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  padding: 24rpx;
+  background: #F0F7FF;
+  min-height: 100vh;
+}
+
+.switch-out-btn {
+  background: #fff;
+  border-radius: 12rpx;
+  padding: 20rpx;
+  margin-bottom: 16rpx;
+  text-align: center;
+  font-size: 28rpx;
+  color: #3B82F6;
+}
+
+.switch-out-btn text {
+  margin-right: 8rpx;
+}
+
+.user-card {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  margin-bottom: 24rpx;
+  display: flex;
+  align-items: center;
+}
+
+.avatar {
+  font-size: 80rpx;
+  margin-right: 20rpx;
+}
+
+.user-info {
+  flex: 1;
+}
+
+.nickname {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #1E293B;
+}
+
+.role {
+  font-size: 24rpx;
+  color: #64748B;
+  margin-top: 4rpx;
+}
+
+.status-tag {
+  display: inline-block;
+  font-size: 22rpx;
+  padding: 4rpx 12rpx;
+  border-radius: 8rpx;
+  margin-top: 8rpx;
+}
+
+.status-tag.pending { background: #FEF3C7; color: #D97706; }
+.status-tag.approved { background: #ECFDF5; color: #059669; }
+.status-tag.rejected { background: #FEF2F2; color: #DC2626; }
+
+.btn-edit {
+  background: #3B82F6;
+  color: #fff;
+  font-size: 24rpx;
+  padding: 8rpx 20rpx;
+  border-radius: 8rpx;
+}
+
+.menu-list {
+  background: #fff;
+  border-radius: 16rpx;
+  overflow: hidden;
+}
+
+.menu-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 28rpx 24rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+  font-size: 28rpx;
+  color: #333;
+}
+
+.menu-item:last-child {
+  border-bottom: none;
+}
+
+.arrow {
+  color: #ccc;
+  font-size: 32rpx;
+}
+
+.modal-mask {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+
+.modal {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 32rpx;
+  width: 560rpx;
+}
+
+.modal-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  text-align: center;
+  margin-bottom: 24rpx;
+}
+
+.form-item {
+  margin-bottom: 20rpx;
+}
+
+.form-item input {
+  border: 1rpx solid #ddd;
+  border-radius: 8rpx;
+  padding: 16rpx;
+  font-size: 28rpx;
+}
+
+.modal-btns {
+  display: flex;
+  gap: 20rpx;
+  margin-top: 24rpx;
+}
+
+.modal-btns button {
+  flex: 1;
+  padding: 16rpx;
+  border-radius: 8rpx;
+  font-size: 28rpx;
+  background: #f5f5f5;
+  color: #333;
+}
+
+.modal-btns .btn-primary {
+  background: #3B82F6;
+  color: #fff;
+}
+
+.bottom-space {
+  height: 140rpx;
+}
+</style>

+ 0 - 0
zxyj-frontend/src/router/teacherGuard.js → zxyj-frontend/router/teacherGuard.js


+ 21 - 1
zxyj-frontend/utils/api.js

@@ -313,7 +313,7 @@ export const getPendingWishes = (familyId) => {
 }
 }
 
 
 export const getWishes = (params) => {
 export const getWishes = (params) => {
-  return request('/api/wishes', 'POST', params)
+  return request('/api/wishes/list', 'POST', params)
 }
 }
 
 
 export const setWishPrice = (wishId, points) => {
 export const setWishPrice = (wishId, points) => {
@@ -332,6 +332,26 @@ export const markWishFulfilled = (wishId) => {
   return request(`/api/wishes/${wishId}/fulfill`, 'POST', {})
   return request(`/api/wishes/${wishId}/fulfill`, 'POST', {})
 }
 }
 
 
+export const createWish = (data) => {
+  return request('/api/wishes/create', 'POST', data)
+}
+
+export const getWishById = (id) => {
+  return request(`/api/wishes/${id}`, 'POST', {})
+}
+
+export const requestWishExchange = (id) => {
+  return request(`/api/wishes/${id}/exchange`, 'POST', {})
+}
+
+export const confirmWishReceived = (id) => {
+  return request(`/api/wishes/${id}/confirm`, 'PUT', {})
+}
+
+export const cancelWish = (id, reason) => {
+  return request(`/api/wishes/${id}`, 'DELETE', { reason })
+}
+
 // =============================================
 // =============================================
 // 成长规划师-套餐模块
 // 成长规划师-套餐模块
 // =============================================
 // =============================================

+ 10 - 1
zxyj-web/src/api/admin.js

@@ -372,9 +372,18 @@ export function approveGuide(userId) {
 }
 }
 
 
 // 拒绝成长规划师审核
 // 拒绝成长规划师审核
-export function rejectGuide(userId) {
+export function rejectGuide(userId, reason) {
   return request({
   return request({
     url: `/api/admin/guide/reject/${userId}`,
     url: `/api/admin/guide/reject/${userId}`,
+    method: 'post',
+    data: { reason }
+  })
+}
+
+// 获取待审核成长规划师数量
+export function getPendingGuideCount() {
+  return request({
+    url: '/api/admin/guide/pending-count',
     method: 'post'
     method: 'post'
   })
   })
 }
 }

+ 101 - 3
zxyj-web/src/views/Dashboard.vue

@@ -71,6 +71,27 @@
       </el-col>
       </el-col>
     </el-row>
     </el-row>
 
 
+    <!-- 待办事项 -->
+    <el-row :gutter="20" class="todo-row" v-if="stats.pendingGuideCount > 0">
+      <el-col :span="24">
+        <el-card class="todo-card" shadow="hover">
+          <div slot="header" class="card-header">
+            <span class="card-title">🔔 待办事项</span>
+          </div>
+          <div class="todo-items">
+            <div class="todo-item" @click="$router.push('/guide-audit')">
+              <div class="todo-badge warning">{{ stats.pendingGuideCount }}</div>
+              <div class="todo-info">
+                <div class="todo-text">待审核成长规划师申请</div>
+                <div class="todo-hint">点击前往审核</div>
+              </div>
+              <i class="el-icon-arrow-right todo-arrow"></i>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
     <!-- 内容区域 -->
     <!-- 内容区域 -->
     <el-row :gutter="20" class="content-row">
     <el-row :gutter="20" class="content-row">
       <el-col :span="14">
       <el-col :span="14">
@@ -164,7 +185,7 @@
 </template>
 </template>
 
 
 <script>
 <script>
-import { getUserList, getChildrenList, getRewardsList, getTasksList, getTaskStats } from '@/api/admin'
+import { getUserList, getChildrenList, getRewardsList, getTasksList, getTaskStats, getPendingGuideCount } from '@/api/admin'
 
 
 export default {
 export default {
   name: 'Dashboard',
   name: 'Dashboard',
@@ -174,7 +195,8 @@ export default {
       stats: {
       stats: {
         totalUsers: 156,
         totalUsers: 156,
         totalChildren: 89,
         totalChildren: 89,
-        pendingRewards: 12
+        pendingRewards: 12,
+        pendingGuideCount: 0
       },
       },
       taskStats: {
       taskStats: {
         total: 1245,
         total: 1245,
@@ -200,9 +222,18 @@ export default {
         this.loadChildrenStats(),
         this.loadChildrenStats(),
         this.loadRewardsStats(),
         this.loadRewardsStats(),
         this.loadTaskStats(),
         this.loadTaskStats(),
-        this.loadRecentTasks()
+        this.loadRecentTasks(),
+        this.loadPendingGuideCount()
       ])
       ])
     },
     },
+    async loadPendingGuideCount() {
+      try {
+        const res = await getPendingGuideCount()
+        this.stats.pendingGuideCount = res.data.count || 0
+      } catch (error) {
+        console.error('加载待审核规划师统计失败:', error)
+      }
+    },
     async loadUserStats() {
     async loadUserStats() {
       try {
       try {
         const res = await getUserList({ page: 1, size: 1 })
         const res = await getUserList({ page: 1, size: 1 })
@@ -430,6 +461,73 @@ export default {
   color: #64748B;
   color: #64748B;
 }
 }
 
 
+/* 待办事项 */
+.todo-row {
+  margin-bottom: 24px;
+}
+
+.todo-card {
+  border-radius: 12px;
+  border: none;
+}
+
+.todo-items {
+  display: flex;
+  flex-direction: column;
+}
+
+.todo-item {
+  display: flex;
+  align-items: center;
+  padding: 16px;
+  background: #FFFBEB;
+  border-radius: 8px;
+  cursor: pointer;
+  transition: background 0.2s;
+}
+
+.todo-item:hover {
+  background: #FEF3C7;
+}
+
+.todo-badge {
+  width: 36px;
+  height: 36px;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 16px;
+  font-weight: 700;
+  color: #fff;
+  margin-right: 16px;
+}
+
+.todo-badge.warning {
+  background: linear-gradient(135deg, #F59E0B, #FBBF24);
+}
+
+.todo-info {
+  flex: 1;
+}
+
+.todo-text {
+  font-size: 14px;
+  font-weight: 500;
+  color: #1E293B;
+}
+
+.todo-hint {
+  font-size: 12px;
+  color: #64748B;
+  margin-top: 4px;
+}
+
+.todo-arrow {
+  font-size: 16px;
+  color: #94A3B8;
+}
+
 /* 快捷操作 */
 /* 快捷操作 */
 .quick-card .el-card__body {
 .quick-card .el-card__body {
   padding: 12px;
   padding: 12px;

+ 41 - 6
zxyj-web/src/views/GuideAudit.vue

@@ -65,6 +65,23 @@
     <el-dialog title="图片预览" :visible.sync="previewVisible" width="500px">
     <el-dialog title="图片预览" :visible.sync="previewVisible" width="500px">
       <img :src="previewUrl" style="width: 100%" />
       <img :src="previewUrl" style="width: 100%" />
     </el-dialog>
     </el-dialog>
+
+    <el-dialog title="拒绝申请" :visible.sync="rejectDialogVisible" width="400px">
+      <el-form>
+        <el-form-item label="拒绝原因" required>
+          <el-input
+            type="textarea"
+            v-model="rejectForm.reason"
+            :rows="4"
+            placeholder="请输入拒绝原因,申请人将收到此原因"
+          />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="rejectDialogVisible = false">取消</el-button>
+        <el-button type="danger" @click="confirmReject" :loading="rejectLoading">确认拒绝</el-button>
+      </div>
+    </el-dialog>
   </div>
   </div>
 </template>
 </template>
 
 
@@ -85,7 +102,14 @@ export default {
         status: 'pending'
         status: 'pending'
       },
       },
       previewVisible: false,
       previewVisible: false,
-      previewUrl: ''
+      previewUrl: '',
+      rejectDialogVisible: false,
+      rejectLoading: false,
+      rejectForm: {
+        userId: null,
+        userName: '',
+        reason: ''
+      }
     }
     }
   },
   },
   mounted() {
   mounted() {
@@ -137,19 +161,30 @@ export default {
       }
       }
     },
     },
     async handleReject(row) {
     async handleReject(row) {
+      this.rejectForm.userId = row.id
+      this.rejectForm.userName = row.realName || row.nickname
+      this.rejectForm.reason = ''
+      this.rejectDialogVisible = true
+    },
+    async confirmReject() {
+      if (!this.rejectForm.reason || !this.rejectForm.reason.trim()) {
+        this.$message.warning('请输入拒绝原因')
+        return
+      }
+      this.rejectLoading = true
       try {
       try {
-        await this.$confirm(`确认拒绝 "${row.realName || row.nickname}" 的成长规划师申请?`, '提示', { type: 'warning' })
-        const res = await rejectGuide(row.id)
+        const res = await rejectGuide(this.rejectForm.userId, this.rejectForm.reason)
         if (res.data && res.data.code === 200) {
         if (res.data && res.data.code === 200) {
           this.$message.success('已拒绝')
           this.$message.success('已拒绝')
+          this.rejectDialogVisible = false
           this.loadData()
           this.loadData()
         } else {
         } else {
           this.$message.error(res.data?.message || '操作失败')
           this.$message.error(res.data?.message || '操作失败')
         }
         }
       } catch (e) {
       } catch (e) {
-        if (e !== 'cancel') {
-          this.$message.error('操作失败')
-        }
+        this.$message.error('操作失败')
+      } finally {
+        this.rejectLoading = false
       }
       }
     },
     },
     previewImage(url) {
     previewImage(url) {