Kaynağa Gözat

feat: 实现心愿功能(wishes)替代rewards/reward_wishlists

- 新增wishes表:个人实际心愿,与rewards心愿库区分
- 新增Wish实体/Mapper/Service/Controller完整后端API
- 新增小程序孩子端心愿页面(列表/创建/详情)
- 新增小程序家长端心愿管理页
- 新增Web管理端心愿管理页
- 删除错误的migration_wishes.sql迁移脚本
- 新增schema-wishes.sql建表脚本

设计明确:
- rewards: 心愿库(家长预设奖励模板)
- wishes: 个人心愿(每个人的实际心愿)

其他修复:
- 修复pages.json路由配置
- 修复API调用和页面跳转
User 5 ay önce
ebeveyn
işleme
35a70e6722

+ 306 - 0
docs/badge-requirements.md

@@ -0,0 +1,306 @@
+# 勋章系统模块需求分析
+
+**版本**: V1.2
+**更新时间**: 2026-04-15
+
+---
+
+## 一、需求清单概览
+
+| 需求ID | 描述 | 优先级 | 状态 |
+|--------|------|--------|------|
+| BADGE-001 | 勋章定义与分类 | P1 | ❌ 未实现 |
+| BADGE-002 | 勋章解锁条件设置 | P1 | ❌ 未实现 |
+| BADGE-003 | 勋章自动授予逻辑 | P1 | ❌ 未实现 |
+| BADGE-004 | 勋章展示与收藏 | P1 | ❌ 未实现 |
+| BADGE-005 | 勋章有效期与过期 | P1 | ❌ 未实现 |
+| BADGE-006 | 勋章统计与排行 | P1 | ❌ 未实现 |
+
+---
+
+## 二、需求详细描述
+
+### 2.1 勋章定义与分类(BADGE-001)
+
+#### BADGE-001: 勋章定义与分类
+- **描述**:系统支持定义多种勋章类型,每种勋章有唯一的标识、名称、图标、等级和分类
+- **字段**:
+  - `badgeId`:勋章唯一标识
+  - `name`:勋章名称
+  - `description`:勋章描述
+  - `icon`:勋章图标
+  - `category`:勋章分类(学习类/运动类/家务类/打卡类/综合类)
+  - `level`:勋章等级(铜/银/金/钻石)
+  - `rarity`:稀有度(普通/稀有/史诗/传说)
+- **可选值**:
+  - 分类:`学习` / `运动` / `家务` / `阅读` / `打卡` / `综合`
+  - 等级:`铜牌` / `银牌` / `金牌` / `钻石`
+  - 稀有度:`普通` / `稀有` / `史诗` / `传说`
+
+---
+
+### 2.2 勋章解锁条件设置(BADGE-002)
+
+#### BADGE-002: 勋章解锁条件设置
+- **描述**:每种勋章可以设置解锁条件,达到条件后自动或手动解锁
+- **字段**:
+  - `triggerType`:触发类型(任务完成次数 / 连续打卡天数 / 积分累计 / 兑换次数)
+  - `threshold`:阈值
+  - `childOnly`:是否仅孩子可解锁
+- **可选值**:
+  - 触发类型:`task_count` / `streak_days` / `points_total` / `reward_count` / `custom`
+  - 阈值:整数,根据类型定义
+
+---
+
+### 2.3 勋章自动授予逻辑(BADGE-003)
+
+#### BADGE-003: 勋章自动授予逻辑
+- **描述**:系统根据解锁条件自动判断是否授予勋章,支持即时授予和家长审批后授予
+- **实现**:
+  - 定时任务扫描孩子的解锁条件
+  - 或在关键动作(任务完成、打卡)时触发检查
+  - 支持自动授予或需要审批
+- **状态记录**:
+  - `badgeId`:勋章ID
+  - `childId`:获得者
+  - `earnedAt`:获得时间
+  - `status`:有效/已过期/已撤销
+
+---
+
+### 2.4 勋章展示与收藏(BADGE-004)
+
+#### BADGE-004: 勋章展示与收藏
+- **描述**:孩子在个人主页或勋章墙展示已获得勋章,支持收藏功能
+- **前端页面**:
+  - 勋章墙:展示所有已获得勋章,按分类/时间排列
+  - 勋章详情:查看勋章名称、描述、获得时间、稀有度
+  - 收藏功能:将喜欢的勋章置顶
+- **展示规则**:
+  - 未获得的勋章显示为锁定状态
+  - 已获得的勋章显示正常图标和名称
+  - 收藏的勋章显示在最前
+
+---
+
+### 2.5 勋章有效期与过期(BADGE-005)
+
+#### BADGE-005: 勋章有效期与过期
+- **描述**:部分勋章可以设置有效期,过期后变为过期状态,但仍保留展示
+- **字段**:
+  - `expireDays`:有效期天数(-1 表示永久有效)
+  - `expireAt`:过期时间
+- **逻辑**:
+  - 定时任务每日检查勋章有效期
+  - 过期后勋章仍保留在展示页,但状态标记为“已过期”
+  - 可设置续期条件,重新激活勋章
+
+---
+
+### 2.6 勋章统计与排行(BADGE-006)
+
+#### BADGE-006: 勋章统计与排行
+- **描述**:提供勋章统计功能,包括个人勋章数量、分类统计、全家排行等
+- **统计维度**:
+  - 个人获得勋章总数
+  - 各分类勋章数量
+  - 各等级勋章数量
+  - 稀有度分布
+- **排行功能**:
+  - 家庭内排行:按勋章数量或积分排序
+  - 街道/区域排行(可选)
+
+---
+
+## 三、勋章数据模型
+
+### 3.1 Badge 勋章定义实体
+
+```java
+// 基础字段
+private Long id;
+private String badgeId;      // 勋章唯一标识
+private String name;         // 勋章名称
+private String description;  // 勋章描述
+private String icon;         // 勋章图标URL
+private String category;     // 勋章分类
+private String level;        // 勋章等级
+private String rarity;       // 稀有度
+
+// 解锁条件
+private String triggerType;  // 触发类型
+private Integer threshold;    // 阈值
+private Boolean needApproval; // 是否需要审批
+private Integer expireDays;  // 有效期天数
+
+// 系统字段
+private Integer sortOrder;   // 排序
+private Integer isActive;    // 是否启用
+private Date createdAt;
+private Date updatedAt;
+```
+
+### 3.2 ChildBadge 儿童勋章关联实体
+
+```java
+// 关联字段
+private Long id;
+private Long childId;        // 孩子ID
+private Long badgeId;        // 勋章ID(对应Badge.id)
+
+// 获得信息
+private Date earnedAt;       // 获得时间
+private Date expireAt;       // 过期时间
+private Integer status;      // 状态:1-有效 0-过期 -1-已撤销
+private Boolean isFavorite;  // 是否收藏
+
+// 系统字段
+private Date createdAt;
+private Date updatedAt;
+```
+
+### 3.3 状态说明
+
+| 状态码 | 含义 | 说明 |
+|--------|------|------|
+| 1 | 有效 | 勋章正常使用 |
+| 0 | 已过期 | 已超过有效期 |
+| -1 | 已撤销 | 被管理员撤销 |
+
+---
+
+## 四、勋章流程说明
+
+### 4.1 勋章生命周期
+
+```
+定义勋章 → 设置解锁条件 → 孩子达成条件 → 系统判断 → 授予勋章 → 展示/收藏 → 过期/续期
+```
+
+### 4.2 阶段与角色
+
+| 阶段 | 家长/管理员 | 孩子 | 系统 |
+|------|------------|------|------|
+| 定义阶段 | 创建/编辑勋章模板 | 不参与 | 校验字段 |
+| 解锁阶段 | 设置解锁条件 | 执行任务/打卡 | 检查触发条件 |
+| 授予阶段 | 审批(如需要) | 等待/获得 | 写入获得记录 |
+| 展示阶段 | 查看全家勋章 | 查看/收藏个人勋章 | 渲染勋章墙 |
+| 过期阶段 | 查看统计 | 查看过期勋章 | 检查并标记过期 |
+
+---
+
+## 五、API接口设计
+
+### 5.1 勋章管理
+
+| 方法 | 路径 | 功能 | 权限 |
+|------|------|------|------|
+| POST | /api/badges | 创建勋章定义 | 管理员 |
+| GET | /api/badges | 获取勋章列表 | 公开 |
+| GET | /api/badges/{id} | 获取勋章详情 | 公开 |
+| PUT | /api/badges/{id} | 编辑勋章定义 | 管理员 |
+| DELETE | /api/badges/{id} | 删除勋章 | 管理员 |
+
+### 5.2 勋章授予
+
+| 方法 | 路径 | 功能 | 权限 |
+|------|------|------|------|
+| POST | /api/badges/grant | 手动授予勋章 | 家长/管理员 |
+| POST | /api/badges/check | 检查并授予勋章 | 系统 |
+| GET | /api/badges/child/{childId} | 获取孩子的勋章 | 孩子/家长 |
+| PUT | /api/badges/{id}/favorite | 收藏/取消收藏 | 孩子 |
+
+### 5.3 勋章统计
+
+| 方法 | 路径 | 功能 | 权限 |
+|------|------|------|------|
+| GET | /api/badges/statistics/child/{childId} | 孩子勋章统计 | 孩子/家长 |
+| GET | /api/badges/statistics/family/{familyId} | 家庭勋章统计 | 家长 |
+| GET | /api/badges/ranking/family/{familyId} | 家庭内排行 | 家长 |
+
+---
+
+## 六、前端页面设计
+
+| 页面 | 路径 | 功能 |
+|------|------|------|
+| 勋章墙 | `/pages/badges/badges` | 展示已获得勋章 |
+| 勋章详情 | `/pages/badges/detail` | 勋章详细信息 |
+| 勋章库 | `/pages/badges/library` | 浏览所有勋章定义 |
+| 勋章统计 | `/pages/badges/statistics` | 统计与排行 |
+
+---
+
+## 七、待完善事项
+
+| 功能 | 优先级 | 说明 |
+|------|--------|------|
+| 勋章定义CRUD | P1 | 后端实体与API |
+| 解锁条件引擎 | P1 | 定时任务+事件触发 |
+| 勋章墙前端 | P1 | 小程序/WEB展示 |
+| 勋章过期逻辑 | P1 | 定时任务 |
+| 勋章统计排行 | P1 | 统计API |
+
+---
+
+## 八、数据库设计建议
+
+### 8.1 勋章定义表
+
+```sql
+CREATE TABLE badge (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    badge_id VARCHAR(50) UNIQUE NOT NULL,
+    name VARCHAR(100) NOT NULL,
+    description TEXT,
+    icon VARCHAR(255),
+    category VARCHAR(20),
+    level VARCHAR(20),
+    rarity VARCHAR(20),
+    trigger_type VARCHAR(20),
+    threshold INT DEFAULT 0,
+    need_approval TINYINT DEFAULT 0,
+    expire_days INT DEFAULT -1,
+    sort_order INT DEFAULT 0,
+    is_active TINYINT DEFAULT 1,
+    created_at DATETIME,
+    updated_at DATETIME
+);
+```
+
+### 8.2 儿童勋章关联表
+
+```sql
+CREATE TABLE child_badge (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    child_id BIGINT NOT NULL,
+    badge_id BIGINT NOT NULL,
+    earned_at DATETIME,
+    expire_at DATETIME,
+    status TINYINT DEFAULT 1,
+    is_favorite TINYINT DEFAULT 0,
+    created_at DATETIME,
+    updated_at DATETIME,
+    UNIQUE KEY uk_child_badge (child_id, badge_id)
+);
+```
+
+---
+
+## 九、后续开发建议
+
+### 9.1 第一阶段:基础功能
+1. Badge 实体与 CRUD API
+2. ChildBadge 关联表与授予逻辑
+3. 勋章墙前端展示
+
+### 9.2 第二阶段:解锁与过期
+1. 解锁条件引擎(定时任务)
+2. 勋章过期检查定时任务
+3. 收藏功能
+
+### 9.3 第三阶段:统计与扩展
+1. 勋章统计 API
+2. 家庭内排行
+3. 勋章续期与活动勋章

+ 792 - 0
docs/dan-requirements.md

@@ -0,0 +1,792 @@
+# DAN测评模块需求分析
+
+**版本**: V1.4
+**更新时间**: 2026-04-16
+
+---
+
+## 一、需求背景与现状
+
+### 1.1 业务背景
+
+DAN测评是一个针对儿童注意力、专注力、记忆力、逻辑思维等多维能力的综合评估系统。测评结果用于:
+- 评估孩子当前的能力水平
+- 生成针对性的成长建议
+- 追踪能力发展趋势
+- 为家长提供教育参考
+- 支持任务模板智能推荐
+
+### 1.2 核心业务变化(V1.3 → V1.4)
+
+| 变化项 | V1.3(旧) | V1.4(新) |
+|--------|-----------|-----------|
+| 等级概念 | DAN等级 A/B/C/D | **取消**,改为测评包 |
+| 能力维度 | 固定五维(注意力/专注力/记忆力/逻辑/综合) | **指标体系**,可配置 |
+| 任务推荐 | 无 | 自动推荐+手动确认 |
+| 收费方式 | 无 | P2阶段与外部系统集成 |
+
+### 1.3 测评包定义
+
+| 测评包 | 适用年龄 | 类型 | 说明 |
+|--------|----------|------|------|
+| A1 | 7-9岁 | 儿童认知能力发展 | 儿童认知能力综合评估 |
+| A2 | 10岁以上 | 青少年核心素养 | 青少年核心素养综合评估 |
+| B1 | 7-9岁 | 儿童家庭环境 | 家庭环境评估 |
+| B2 | 10岁以上 | 青少年家庭环境 | 家庭环境评估 |
+| B3 | 7-9岁 | 儿童学习习惯 | 学习习惯评估 |
+| B4 | 10岁以上 | 青少年学习习惯 | 学习习惯评估 |
+| B5 | 7-9岁 | 儿童心理状态 | 心理状态评估 |
+| B6 | 10岁以上 | 职业发展 | 职业发展评估 |
+
+### 1.4 现有实现状态
+
+| 组件 | 状态 | 说明 |
+|------|------|------|
+| `DanAssessmentResult` 实体 | ⚠️ 已定义 | 存在于 `entity/DanAssessmentResult.java` |
+| `AssessmentAppointment` 实体 | ⚠️ 已定义 | 存在于 `entity/AssessmentAppointment.java` |
+| DAN测评 API | ❌ 不存在 | 无对应 Controller |
+| 预约 API | ⚠️ 部分实现 | 依赖 Appointment 实体 |
+| 前端页面 | ❌ 不存在 | 无对应页面 |
+
+---
+
+## 二、指标体系设计
+
+### 2.1 指标定义
+
+指标是测评的最小评估单元,每个指标有唯一的指标编码、所属能力维度、等级阈值。
+
+#### 指标属性
+
+| 属性 | 类型 | 说明 |
+|------|------|------|
+| indicatorCode | String | 指标编码,如 COG001(认知能力)、EMI001(情绪心理) |
+| indicatorName | String | 指标名称 |
+| dimension | String | 能力维度(COG/EMI/SOC/PHY) |
+| description | String | 指标说明 |
+| levelThresholds | JSON | 等级阈值,如 {"高":80,"中":60,"低":0} |
+| sortOrder | Integer | 排序 |
+
+#### 指标维度
+
+| 维度编码 | 维度名称 |
+|----------|----------|
+| COG | 认知能力 |
+| EMI | 情绪心理 |
+| SOC | 社会关系 |
+| PHY | 身体健康 |
+
+### 2.2 测评包-指标映射(dan_package_indicator)
+
+每个测评包包含一组指标,定义见下表:
+
+| 测评包 | 包含指标 |
+|--------|----------|
+| A1(儿童认知能力发展-7-9岁) | COG001, COG002, COG003, COG004, COG005 |
+| A2(青少年核心素养-10岁以上) | COG001, COG002, COG003, COG004, COG005, COG006 |
+| B1(儿童家庭环境-7-9岁) | EMI001, EMI002, SOC001 |
+| B2(青少年家庭环境-10岁以上) | EMI001, EMI002, EMI003, SOC001, SOC002 |
+| B3(儿童学习习惯-7-9岁) | SOC001, SOC002, PHY001 |
+| B4(青少年学习习惯-10岁以上) | SOC001, SOC002, SOC003, PHY001, PHY002 |
+| B5(儿童心理状态-7-9岁) | EMI001, EMI002, PHY001 |
+| B6(职业发展-10岁以上) | EMI001, EMI002, EMI003, PHY001, PHY002 |
+
+### 2.3 指标-任务模板映射(dan_indicator_template)
+
+每个指标可关联多个任务模板,用于测评后的任务推荐。
+
+| 指标 | 推荐任务模板类型 |
+|------|------------------|
+| COG001(认知灵活) | 认知灵活性训练、多任务切换 |
+| COG002(工作记忆) | 工作记忆训练、数字记忆 |
+| COG003(注意力广度) | 舒尔特方格、视觉搜索 |
+| COG004(注意力稳定性) | 专注力训练、静态任务 |
+| COG005(逻辑推理) | 逻辑思维训练、 puzzles |
+| COG006(问题解决) | 策略游戏、方案规划 |
+| EMI001(情绪识别) | 情绪日记、情绪识别训练 |
+| EMI002(情绪调节) | 情绪调节练习、放松训练 |
+| EMI003(抗压能力) | 压力应对训练、心理韧性 |
+| SOC001(社交技能) | 社交互动练习、沟通训练 |
+| SOC002(合作能力) | 团队合作任务、小组活动 |
+| PHY001(运动协调) | 运动游戏、协调性训练 |
+| PHY002(身体耐力) | 体能训练、耐力运动 |
+
+---
+
+## 三、需求清单
+
+### 3.1 需求列表
+
+| 需求ID | 描述 | 优先级 | 状态 |
+|--------|------|--------|------|
+| DAN-001 | 测评预约创建与管理 | P0 | ⚠️ 部分实现 |
+| DAN-002 | 测评结果录入与存储(按指标) | P0 | ⚠️ 已定义实体 |
+| DAN-003 | 测评结果列表查询 | P0 | ❌ 未实现 |
+| DAN-004 | 测评结果详情查看 | P0 | ❌ 未实现 |
+| DAN-005 | 指标得分展示 | P0 | ⚠️ 已定义字段 |
+| DAN-006 | 指标等级计算 | P0 | ❌ 未实现 |
+| DAN-007 | 历史趋势对比 | P1 | ❌ 未实现 |
+| DAN-008 | 成长建议生成(按指标) | P1 | ❌ 未实现 |
+| DAN-009 | 下次复测提醒 | P1 | ❌ 未实现 |
+| DAN-010 | 测评报告导出 | P1 | ❌ 未实现 |
+| DAN-011 | 测评统计报表 | P1 | ❌ 未实现 |
+| DAN-012 | 测评包管理 | P1 | ❌ 未实现 |
+| DAN-013 | 指标库管理 | P1 | ❌ 未实现 |
+| DAN-014 | 指标-模板映射管理 | P1 | ❌ 未实现 |
+| DAN-015 | 任务自动推荐 | P1 | ❌ 未实现 |
+| DAN-016 | 收费集成预留 | P2 | ❌ 未实现 |
+| DAN-017 | 指导师录入权限 | P0 | ⚠️ 已定义 |
+| DAN-018 | 家长查看权限 | P0 | ⚠️ 已定义 |
+| DAN-019 | 测评数据脱敏 | P2 | ❌ 未实现 |
+
+---
+
+## 四、需求详细说明
+
+### 4.1 测评包管理(DAN-012)
+
+#### 功能描述
+管理测评包的定义,包括测评包编码、名称、适用年龄范围、包含的指标列表。
+
+#### 字段定义
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| id | Long | 是 | 主键 |
+| packageCode | String | 是 | 测评包编码(A1/A2/B1-B6) |
+| packageName | String | 是 | 测评包名称 |
+| ageRangeMin | Integer | 是 | 适用最小年龄 |
+| ageRangeMax | Integer | 是 | 适用最大年龄 |
+| category | String | 是 | 分类(认知能力/核心能力/家庭环境/行为表现/心理状态) |
+| indicators | JSON | 是 | 指标编码数组,如 ["ATT001","MEM001"] |
+| description | String | 否 | 说明 |
+| isActive | Integer | 是 | 是否启用(1启用/0禁用) |
+| createdAt | Date | 是 | 创建时间 |
+| updatedAt | Date | 是 | 更新时间 |
+
+---
+
+### 4.2 指标库管理(DAN-013)
+
+#### 功能描述
+管理指标的定义,支持指标的增删改查。
+
+#### 字段定义
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| id | Long | 是 | 主键 |
+| indicatorCode | String | 是 | 指标编码(如 ATT001) |
+| indicatorName | String | 是 | 指标名称 |
+| dimension | String | 是 | 能力维度 |
+| description | String | 否 | 指标说明 |
+| levelThresholds | JSON | 是 | 等级阈值(如 {"高":80,"中":60,"低":0}) |
+| sortOrder | Integer | 否 | 排序 |
+| isActive | Integer | 是 | 是否启用 |
+| createdAt | Date | 是 | 创建时间 |
+| updatedAt | Date | 是 | 更新时间 |
+
+---
+
+### 4.3 指标-任务模板映射管理(DAN-014)
+
+#### 功能描述
+管理指标与任务模板的关联关系,支持自动推荐逻辑。
+
+#### 字段定义
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| id | Long | 是 | 主键 |
+| indicatorCode | String | 是 | 指标编码 |
+| templateId | Long | 是 | 任务模板ID |
+| weight | Integer | 否 | 推荐权重(1-10) |
+| isActive | Integer | 是 | 是否启用 |
+| createdAt | Date | 是 | 创建时间 |
+| updatedAt | Date | 是 | 更新时间 |
+
+---
+
+### 4.4 预约管理(DAN-001)
+
+#### 功能描述
+家长或指导师为孩子预约DAN测评,选择测评时间、指导师和测评包。
+
+#### 业务流程
+```
+家长/指导师 → 选择测评包 → 选择时间 → 预约记录生成 → 指导师确认 → 测评执行 → 标记完成
+```
+
+#### 字段定义
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| childId | Long | 是 | 预约的孩子 |
+| teacherId | Long | 是 | 指导师 |
+| packageCode | String | 是 | 测评包编码 |
+| appointmentDate | Date | 是 | 预约日期 |
+| appointmentTime | String | 是 | 预约时间(HH:mm) |
+| notes | String | 否 | 备注 |
+| status | String | 是 | pending/completed/cancelled |
+
+---
+
+### 4.5 测评结果录入与存储(DAN-002)
+
+#### 功能描述
+指导师完成测评后,录入测评结果并存储。测评结果按指标存储得分。
+
+#### 字段定义
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| id | Long | 是 | 主键 |
+| childId | Long | 是 | 测评的孩子 |
+| teacherId | Long | 是 | 指导师ID |
+| appointmentId | Long | 否 | 关联的预约ID |
+| packageCode | String | 是 | 测评包编码 |
+| indicatorScores | JSON | 是 | 指标得分数组 |
+
+```json
+// indicatorScores 示例
+[
+  {"code": "ATT001", "name": "注意力广度", "score": 85, "level": "高"},
+  {"code": "MEM001", "name": "短时记忆", "score": 72, "level": "中"},
+  {"code": "LOG001", "name": "逻辑推理", "score": 90, "level": "高"}
+]
+```
+
+#### 其他字段
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| assessmentDate | Date | 是 | 测评日期 |
+| durationMinutes | Integer | 否 | 测评时长 |
+| gameRecords | String | 否 | 游戏记录(JSON) |
+| analysisReport | String | 否 | 分析报告 |
+| growthSuggestions | String | 否 | 成长建议(按指标生成) |
+| nextReviewDate | Date | 否 | 下次复测日期 |
+| status | String | 是 | draft/completed/archived |
+| createdAt | Date | 是 | 创建时间 |
+| updatedAt | Date | 是 | 更新时间 |
+
+#### 状态流转
+- `draft`:草稿(指导师录入中)
+- `completed`:已完成(正式结果)
+- `archived`:已归档(历史记录)
+
+---
+
+### 4.6 测评结果列表查询(DAN-003)
+
+#### 功能描述
+根据孩子ID查询测评记录列表,支持筛选和分页。
+
+#### 筛选条件
+
+| 条件 | 类型 | 说明 |
+|------|------|------|
+| childId | Long | 孩子ID(必填) |
+| timeRange | String | 时间范围(recent_month/recent_quarter/all) |
+| packageCode | String | 测评包筛选 |
+| status | String | 状态筛选 |
+
+#### 返回字段
+- 列表展示:不包含详细分析报告和成长建议
+- 包含字段:id、assessmentDate、packageCode、indicatorScores 摘要、status
+
+---
+
+### 4.7 测评结果详情查看(DAN-004)
+
+#### 功能描述
+查看单次测评的完整结果。
+
+#### 展示内容
+
+| 模块 | 内容 |
+|------|------|
+| 基本信息 | 测评日期、指导师、测评包、测评时长 |
+| 指标得分 | indicatorScores 完整展示,按维度分组 |
+| 指标等级 | 每个指标的等级(高/中/低) |
+| 分析报告 | analysisReport 全文 |
+| 成长建议 | growthSuggestions 全文(按指标维度) |
+| 下次复测 | nextReviewDate |
+
+---
+
+### 4.8 指标等级计算(DAN-006)
+
+#### 功能描述
+根据指标得分和等级阈值计算指标等级。
+
+#### 计算规则
+1. 查询指标定义的 levelThresholds
+2. 根据得分判断等级:
+   - score >= 高阈值 → "高"
+   - score >= 中阈值 → "中"
+   - score < 中阈值 → "低"
+
+#### 示例
+```json
+// 指标定义
+{"indicatorCode": "ATT001", "levelThresholds": {"高":80,"中":60,"低":0}}
+
+// 得分85 → 等级"高"
+// 得分65 → 等级"中"  
+// 得分55 → 等级"低"
+```
+
+---
+
+### 4.9 历史趋势对比(DAN-007)
+
+#### 功能描述
+展示孩子历次测评各指标的趋势变化。
+
+#### 展示内容
+- 折线图:各指标得分趋势
+- 表格:历史测评记录对比
+- 维度汇总:按能力维度的平均得分
+
+#### 时间范围
+- 最近三月
+- 最近半年
+- 全部历史
+
+---
+
+### 4.10 成长建议生成(DAN-008)
+
+#### 功能描述
+根据测评结果(按指标)生成针对性的成长建议。
+
+#### 生成规则
+1. 遍历所有指标,筛选出等级为"低"或"中"的指标
+2. 根据指标维度生成对应建议:
+   - ATT(注意力):专注力训练游戏、日常注意力培养建议
+   - MEM(记忆力):记忆方法教学、记忆训练游戏
+   - LOG(逻辑思维):逻辑思维训练、问题解决技巧
+   - EMI(情绪调节):情绪管理方法、情绪调节练习
+   - BEH(行为习惯):行为养成计划、自控力培养
+   - FAM(家庭环境):家庭支持建议、亲子互动改善
+
+#### 建议类型
+- 日常生活建议
+- 游戏训练建议
+- 专业干预建议(如需)
+
+---
+
+### 4.11 任务自动推荐(DAN-015)
+
+#### 功能描述
+测评完成后,根据指标得分自动推荐相关任务模板,家长可手动选择确认。
+
+#### 业务流程
+```
+测评完成 → 系统分析指标得分 → 筛选得分低的指标 
+→ 查找对应任务模板 → 按权重排序 → 展示推荐列表 
+→ 家长选择确认 → 创建任务
+```
+
+#### 推荐算法
+1. **筛选低分指标**:score < 中阈值的指标
+2. **查找关联模板**:从 dan_indicator_template 查询关联的任务模板
+3. **权重排序**:按 weight 降序排列
+4. **去重**:同一模板只推荐一次
+
+#### 展示内容
+- 推荐任务列表:templateName、推荐理由(关联的指标)
+- 家长操作:选择确认 / 忽略 / 自定义选择
+
+---
+
+### 4.12 下次复测提醒(DAN-009)
+
+#### 功能描述
+根据 nextReviewDate 发送复测提醒。
+
+#### 提醒机制
+- 提前7天/3天/1天提醒
+- 提醒方式:系统消息推送
+- 触发:定时任务每日扫描
+
+---
+
+### 4.13 测评报告导出(DAN-010)
+
+#### 功能描述
+将测评结果导出为PDF格式。
+
+#### 导出内容
+- 封面:孩子信息、测评日期、测评包
+- 总体评价:各维度得分汇总
+- 指标雷达图
+- 各指标详细分析(得分+等级)
+- 历史趋势对比
+- 成长建议(按维度)
+- 下次复测计划
+
+---
+
+### 4.14 测评统计报表(DAN-011)
+
+#### 功能描述
+提供测评统计功能。
+
+#### 统计维度
+- 测评总次数
+- 各测评包分布
+- 各指标平均分
+- 各等级分布(高/中/低)
+- 测评完成率
+
+---
+
+### 4.15 收费集成预留(DAN-016)
+
+#### 功能描述
+P2阶段与外部收费系统集成,目前预留接口。
+
+#### 预留内容
+1. 测评包定价字段(dan_package.price)
+2. 支付状态字段(dan_appointment.paymentStatus)
+3. 回调通知接口(/api/dan/payment/callback)
+4. 订单创建接口(预留)
+
+---
+
+### 4.16 指导师录入权限(DAN-017)
+
+#### 权限范围
+- 创建测评结果
+- 编辑自己录入的测评结果
+- 查看自己负责家庭的测评记录
+- 管理测评包、指标、模板映射
+
+---
+
+### 4.17 家长查看权限(DAN-018)
+
+#### 权限范围
+- 查看自己孩子的所有测评记录
+- 查看测评详情
+- 查看历史趋势
+- 确认任务推荐
+
+---
+
+### 4.18 测评数据脱敏(DAN-019)
+
+#### 脱敏规则
+- 街道级汇总展示
+- 匿名化处理
+- 仅展示统计趋势
+
+---
+
+## 五、API接口设计
+
+### 5.1 测评包管理
+
+| 方法 | 路径 | 功能 | 权限 |
+|------|------|------|------|
+| GET | /api/dan/packages | 测评包列表 | 公开 |
+| GET | /api/dan/packages/{code} | 测评包详情 | 公开 |
+| POST | /api/dan/packages | 创建测评包 | 管理员 |
+| PUT | /api/dan/packages/{id} | 更新测评包 | 管理员 |
+| DELETE | /api/dan/packages/{id} | 删除测评包 | 管理员 |
+
+### 5.2 指标管理
+
+| 方法 | 路径 | 功能 | 权限 |
+|------|------|------|------|
+| GET | /api/dan/indicators | 指标列表 | 公开 |
+| GET | /api/dan/indicators/{code} | 指标详情 | 公开 |
+| POST | /api/dan/indicators | 创建指标 | 管理员 |
+| PUT | /api/dan/indicators/{id} | 更新指标 | 管理员 |
+| DELETE | /api/dan/indicators/{id} | 删除指标 | 管理员 |
+
+### 5.3 指标-模板映射
+
+| 方法 | 路径 | 功能 | 权限 |
+|------|------|------|------|
+| GET | /api/dan/indicator-template | 映射列表 | 指导师 |
+| POST | /api/dan/indicator-template | 创建映射 | 管理员 |
+| PUT | /api/dan/indicator-template/{id} | 更新映射 | 管理员 |
+| DELETE | /api/dan/indicator-template/{id} | 删除映射 | 管理员 |
+
+### 5.4 预约管理
+
+| 方法 | 路径 | 功能 | 权限 |
+|------|------|------|------|
+| POST | /api/dan/appointments | 创建预约 | 家长/指导师 |
+| GET | /api/dan/appointments | 预约列表 | 家长/指导师 |
+| GET | /api/dan/appointments/{id} | 预约详情 | 家长/指导师 |
+| PUT | /api/dan/appointments/{id} | 修改预约 | 家长/指导师 |
+| DELETE | /api/dan/appointments/{id} | 取消预约 | 家长/指导师 |
+
+### 5.5 测评结果
+
+| 方法 | 路径 | 功能 | 权限 |
+|------|------|------|------|
+| POST | /api/dan/results | 创建测评记录 | 指导师 |
+| GET | /api/dan/results/child/{childId} | 测评列表 | 家长/指导师 |
+| GET | /api/dan/results/{id} | 测评详情 | 家长/指导师 |
+| PUT | /api/dan/results/{id} | 更新测评记录 | 指导师 |
+| DELETE | /api/dan/results/{id} | 删除测评记录 | 指导师 |
+
+### 5.6 任务推荐
+
+| 方法 | 路径 | 功能 | 权限 |
+|------|------|------|------|
+| GET | /api/dan/results/{id}/recommend-tasks | 获取推荐任务 | 家长 |
+| POST | /api/dan/results/{id}/confirm-tasks | 确认推荐任务 | 家长 |
+
+### 5.7 分析与统计
+
+| 方法 | 路径 | 功能 | 权限 |
+|------|------|------|------|
+| GET | /api/dan/analysis/{id} | 能力分析 | 家长/指导师 |
+| GET | /api/dan/trend/{childId} | 历史趋势 | 家长/指导师 |
+| GET | /api/dan/statistics/child/{childId} | 测评统计 | 家长/指导师 |
+| GET | /api/dan/statistics/family/{familyId} | 家庭统计 | 家长 |
+
+### 5.8 导出
+
+| 方法 | 路径 | 功能 | 权限 |
+|------|------|------|------|
+| GET | /api/dan/export/{id} | 导出PDF报告 | 家长/指导师 |
+
+### 5.9 收费集成(P2)
+
+| 方法 | 路径 | 功能 | 权限 |
+|------|------|------|------|
+| POST | /api/dan/payment/create | 创建支付订单 | 家长 |
+| POST | /api/dan/payment/callback | 支付回调 | 外部系统 |
+
+---
+
+## 六、前端页面
+
+| 页面 | 路径 | 功能 |
+|------|------|------|
+| 测评包选择 | `/pages/dan/packages` | 选择测评包 |
+| 预约页面 | `/pages/dan/appointment` | 预约创建与管理 |
+| 测评列表 | `/pages/dan/list` | 测评历史记录 |
+| 测评详情 | `/pages/dan/detail` | 测评结果详情 |
+| 指标雷达图 | `/pages/dan/radar` | 能力维度雷达图 |
+| 趋势对比 | `/pages/dan/trend` | 历史趋势图表 |
+| 任务推荐 | `/pages/dan/recommend` | 任务推荐确认 |
+| 统计报表 | `/pages/dan/statistics` | 测评统计 |
+| 管理后台 | `/pages/admin/dan` | 测评包/指标/映射管理 |
+
+---
+
+## 七、数据库设计
+
+### 7.1 测评包表(dan_package)
+
+```sql
+CREATE TABLE dan_package (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    package_code VARCHAR(10) NOT NULL UNIQUE,
+    package_name VARCHAR(50) NOT NULL,
+    age_range_min INT NOT NULL,
+    age_range_max INT NOT NULL,
+    category VARCHAR(20) NOT NULL,
+    indicators JSON NOT NULL,
+    description TEXT,
+    price DECIMAL(10,2) DEFAULT 0,
+    is_active TINYINT DEFAULT 1,
+    created_at DATETIME,
+    updated_at DATETIME,
+    INDEX idx_code (package_code),
+    INDEX idx_category (category)
+);
+```
+
+### 7.2 指标表(dan_indicator)
+
+```sql
+CREATE TABLE dan_indicator (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    indicator_code VARCHAR(20) NOT NULL UNIQUE,
+    indicator_name VARCHAR(50) NOT NULL,
+    dimension VARCHAR(20) NOT NULL,
+    description TEXT,
+    level_thresholds JSON NOT NULL,
+    sort_order INT DEFAULT 0,
+    is_active TINYINT DEFAULT 1,
+    created_at DATETIME,
+    updated_at DATETIME,
+    INDEX idx_code (indicator_code),
+    INDEX idx_dimension (dimension)
+);
+```
+
+### 7.3 指标-任务模板映射表(dan_indicator_template)
+
+```sql
+CREATE TABLE dan_indicator_template (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    indicator_code VARCHAR(20) NOT NULL,
+    template_id BIGINT NOT NULL,
+    weight INT DEFAULT 5,
+    is_active TINYINT DEFAULT 1,
+    created_at DATETIME,
+    updated_at DATETIME,
+    INDEX idx_indicator (indicator_code),
+    INDEX idx_template (template_id)
+);
+```
+
+### 7.4 测评预约表(dan_appointment)
+
+```sql
+CREATE TABLE dan_appointment (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    child_id BIGINT NOT NULL,
+    teacher_id BIGINT,
+    package_code VARCHAR(10) NOT NULL,
+    appointment_date DATE NOT NULL,
+    appointment_time VARCHAR(10) NOT NULL,
+    notes TEXT,
+    payment_status VARCHAR(20) DEFAULT 'unpaid',
+    status VARCHAR(20) DEFAULT 'pending',
+    created_at DATETIME,
+    updated_at DATETIME,
+    INDEX idx_child (child_id),
+    INDEX idx_teacher (teacher_id),
+    INDEX idx_date (appointment_date)
+);
+```
+
+### 7.5 测评结果表(dan_assessment_result)
+
+```sql
+CREATE TABLE dan_assessment_result (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    child_id BIGINT NOT NULL,
+    teacher_id BIGINT NOT NULL,
+    appointment_id BIGINT,
+    package_code VARCHAR(10) NOT NULL,
+    indicator_scores JSON NOT NULL,
+    assessment_date DATE NOT NULL,
+    duration_minutes INT,
+    game_records JSON,
+    analysis_report TEXT,
+    growth_suggestions TEXT,
+    next_review_date DATE,
+    status VARCHAR(20) DEFAULT 'draft',
+    created_at DATETIME,
+    updated_at DATETIME,
+    INDEX idx_child (child_id),
+    INDEX idx_teacher (teacher_id),
+    INDEX idx_date (assessment_date),
+    INDEX idx_package (package_code)
+);
+```
+
+---
+
+## 八、开发优先级
+
+### P0(紧急)
+
+1. **DAN-012 测评包管理**
+   - 后端 CRUD API
+   - 管理后台页面
+
+2. **DAN-013 指标库管理**
+   - 后端 CRUD API
+   - 管理后台页面
+
+3. **DAN-014 指标-模板映射**
+   - 后端 CRUD API
+   - 管理后台页面
+
+4. **DAN-001 预约管理**
+   - 后端 CRUD API
+   - 前端预约页面
+
+5. **DAN-002 测评结果录入**
+   - 后端结果录入 API
+   - 指标等级自动计算
+
+6. **DAN-003 测评列表查询**
+   - 后端列表 API
+   - 前端列表页面
+
+7. **DAN-004 测评详情**
+   - 后端详情 API
+   - 前端详情页面(按指标展示)
+
+### P1(重要)
+
+8. **DAN-006 指标等级计算**
+   - 自动计算逻辑
+   - 等级展示
+
+9. **DAN-007 历史趋势**
+   - 趋势 API
+   - 趋势图表前端
+
+10. **DAN-008 成长建议**
+    - 建议生成规则引擎
+    - 建议展示
+
+11. **DAN-015 任务自动推荐**
+    - 推荐算法实现
+    - 家长确认流程
+
+12. **DAN-009 复测提醒**
+    - 定时任务
+
+### P2(扩展)
+
+13. **DAN-010 报告导出**
+14. **DAN-011 统计报表**
+15. **DAN-016 收费集成**
+16. **DAN-019 数据脱敏**
+
+---
+
+## 九、现有代码参考
+
+### 9.1 实体类(需更新)
+- `zxyj-backend/src/main/java/com/zxyj/entity/DanAssessmentResult.java` - 需增加 packageCode 和 indicatorScores
+- `zxyj-backend/src/main/java/com/zxyj/entity/AssessmentAppointment.java` - 需增加 packageCode
+
+### 9.2 需创建
+- `zxyj-backend/src/main/java/com/zxyj/entity/DanPackage.java`
+- `zxyj-backend/src/main/java/com/zxyj/entity/DanIndicator.java`
+- `zxyj-backend/src/main/java/com/zxyj/entity/DanIndicatorTemplate.java`
+- `zxyj-backend/src/main/java/com/zxyj/controller/dan/DanPackageController.java`
+- `zxyj-backend/src/main/java/com/zxyj/controller/dan/DanIndicatorController.java`
+- `zxyj-backend/src/main/java/com/zxyj/controller/dan/DanAssessmentController.java`
+- `zxyj-backend/src/main/java/com/zxyj/controller/dan/DanRecommendController.java`
+- `zxyj-frontend/pages/dan/` 目录及页面
+
+---
+
+## 十、待完善事项
+
+| 功能 | 优先级 | 说明 |
+|------|--------|------|
+| 测评包CRUD | P0 | 后端API + 管理后台 |
+| 指标库CRUD | P0 | 后端API + 管理后台 |
+| 映射管理 | P0 | 后端API + 管理后台 |
+| 预约CRUD | P0 | 后端API |
+| 结果录入 | P0 | 后端API + 指标等级计算 |
+| 列表查询 | P0 | 后端API + 前端 |
+| 详情展示 | P0 | 前端页面(按指标) |
+| 等级计算 | P1 | 自动计算逻辑 |
+| 趋势分析 | P1 | 后端API + 前端图表 |
+| 成长建议 | P1 | 规则引擎 |
+| 任务推荐 | P1 | 推荐算法 + 家长确认 |
+| 复测提醒 | P1 | 定时任务 |
+| 报告导出 | P2 | PDF生成 |
+| 统计报表 | P2 | 统计API |
+| 收费集成 | P2 | P2阶段讨论 |

+ 327 - 0
docs/focus-requirements.md

@@ -0,0 +1,327 @@
+# 专注训练模块需求分析
+
+**版本**: V1.2
+**更新时间**: 2026-04-15
+
+---
+
+## 一、需求清单概览
+
+| 需求ID | 描述 | 优先级 | 状态 |
+|--------|------|--------|------|
+| FOCUS-001 | 舒尔特方格游戏 | P1 | ✅ 已实现 |
+| FOCUS-002 | 1a2b猜数字游戏 | P1 | ✅ 已实现 |
+| FOCUS-003 | 数独游戏 | P1 | ✅ 已实现 |
+| FOCUS-004 | 专注力训练记录 | P1 | ⚠️ 部分实现 |
+| FOCUS-005 | 训练数据分析 | P1 | ❌ 未实现 |
+| FOCUS-006 | 训练成果展示 | P1 | ❌ 未实现 |
+| FOCUS-007 | 训练提醒与激励 | P1 | ❌ 未实现 |
+| FOCUS-008 | 训练积分奖励 | P1 | ⚠️ 部分实现 |
+
+---
+
+## 二、需求详细描述
+
+### 2.1 舒尔特方格游戏(FOCUS-001)
+
+#### FOCUS-001: 舒尔特方格游戏
+- **描述**:经典的专注力训练游戏,玩家按顺序点击1-25的数字正方形
+- **游戏规则**:
+  - 5x5 网格,随机分布 1-25 数字
+  - 按顺序点击,速度越快分数越高
+  - 可选难度:简单(3x3)/中等(4x4)/困难(5x5)
+- **现有字段**(基于 `MiniGame` 实体):
+  - `gameCode`:schulte
+  - `gameName`:舒尔特方格
+  - `defaultDuration`:默认时长
+  - `defaultPoints`:默认积分
+- **状态**:✅ 已实现
+
+---
+
+### 2.2 1a2b猜数字游戏(FOCUS-002)
+
+#### FOCUS-002: 1a2b猜数字游戏
+- **描述**:猜测指定位数的数字,A表示位置和数字都正确,B表示数字正确但位置错误
+- **游戏规则**:
+  - 4位不重复数字
+  - 输入猜测后返回 "xAyB" 格式结果
+  - 可选难度:3位数/4位数/5位数
+- **现有字段**:
+  - `gameCode`:1a2b
+  - `gameName`:猜数字
+  - `defaultDuration`:默认时长
+  - `defaultPoints`:默认积分
+- **状态**:✅ 已实现
+
+---
+
+### 2.3 数独游戏(FOCUS-003)
+
+#### FOCUS-003: 数独游戏
+- **描述**:经典数独填充游戏,每行每列每个3x3宫格填入1-9不重复数字
+- **游戏规则**:
+  - 9x9 宫格,部分数字预先填充
+  - 玩家填充剩余空格
+  - 可选难度:简单/中等/困难
+- **现有字段**:
+  - `gameCode`:sudoku
+  - `gameName`:数独
+  - `defaultDuration`:默认时长
+  - `defaultPoints`:默认积分
+- **状态**:✅ 已实现
+
+---
+
+### 2.4 专注力训练记录(FOCUS-004)
+
+#### FOCUS-004: 专注力训练记录
+- **描述**:记录每次训练的详细数据,包括时长、得分、排名等
+- **记录字段**:
+  - `childId`:训练的孩子
+  - `gameCode`:游戏类型
+  - `score`:得分
+  - `duration`:用时(秒)
+  - `level`:难度等级
+  - `rank`:本次排名
+  - `completedAt`:完成时间
+- **存储方式**:可复用 `MediaRecord` 或新建 `GameRecord` 表
+- **状态**:⚠️ 部分实现(现有后端记录得分和用时)
+
+---
+
+### 2.5 训练数据分析(FOCUS-005)
+
+#### FOCUS-005: 训练数据分析
+- **描述**:对训练记录进行数据分析,生成专注力报告
+- **分析维度**:
+  - 各游戏平均得分
+  - 各游戏最佳成绩
+  - 训练频率统计
+  - 能力趋势分析
+- **展示形式**:数据卡片 + 趋势图表
+
+---
+
+### 2.6 训练成果展示(FOCUS-006)
+
+#### FOCUS-006: 训练成果展示
+- **描述**:在个人主页或专门页面展示训练成果
+- **展示内容**:
+  - 累计训练次数
+  - 获得积分总数
+  - 最佳成绩榜单
+  - 连续训练天数
+- **展示位置**:
+  - 个人主页训练入口
+  - 专门的训练成果页面
+  - 勋章墙(可关联)
+
+---
+
+### 2.7 训练提醒与激励(FOCUS-007)
+
+#### FOCUS-007: 训练提醒与激励
+- **描述**:通过系统消息或激励机制鼓励孩子坚持训练
+- **提醒功能**:
+  - 每日训练提醒
+  - 连续训练激励
+  - 里程碑达成庆祝
+- **激励机制**:
+  - 连续训练徽章
+  - 积分加成
+  - 排行榜激励
+
+---
+
+### 2.8 训练积分奖励(FOCUS-008)
+
+#### FOCUS-008: 训练积分奖励
+- **描述**:完成游戏后获得积分奖励,积分可累积使用
+- **奖励规则**:
+  - 基础积分:游戏默认积分
+  - 表现加成:根据用时/正确率增加积分
+  -连胜加成:连续完成游戏获得额外积分
+- **积分用途**:
+  - 可用于心愿兑换
+  - 可与任务积分合并统计
+- **状态**:⚠️ 部分实现(现有小游戏积分已接入)
+
+---
+
+## 三、现有小游戏数据模型
+
+### 3.1 MiniGame 小游戏定义实体
+
+```java
+// 现有字段
+private Long id;
+private String gameCode;          // 游戏代码: schulte, 1a2b, sudoku
+private String gameName;          // 游戏名称
+private String description;       // 游戏描述
+private String icon;              // 图标emoji
+private Integer defaultDuration; // 默认时长(分钟)
+private Integer defaultPoints;   // 默认奖励积分
+private String gameUrl;           // 游戏H5页面URL
+private Integer sortOrder;       // 排序
+private Integer status;          // 状态: 0禁用 1启用
+private Date createdAt;
+private Date updatedAt;
+```
+
+### 3.2 游戏记录实体(建议新增)
+
+```java
+private Long id;
+private Long childId;           // 孩子ID
+private String gameCode;        // 游戏代码
+private Integer score;           // 得分
+private Integer duration;       // 用时(秒)
+private String level;            // 难度:easy/medium/hard
+private Integer rank;            // 本次排名
+private Integer pointsEarned;    // 获得积分
+private Date completedAt;        // 完成时间
+private String deviceInfo;       // 设备信息
+private Date createdAt;
+```
+
+---
+
+## 四、API接口设计
+
+### 4.1 游戏管理
+
+| 方法 | 路径 | 功能 | 权限 |
+|------|------|------|------|
+| GET | /api/games | 获取游戏列表 | 公开 |
+| GET | /api/games/{code} | 获取游戏详情 | 公开 |
+| POST | /api/games | 创建游戏定义 | 管理员 |
+| PUT | /api/games/{code} | 更新游戏配置 | 管理员 |
+
+### 4.2 游戏记录
+
+| 方法 | 路径 | 功能 | 权限 |
+|------|------|------|------|
+| POST | /api/games/record | 记录游戏成绩 | 孩子 |
+| GET | /api/games/records/{childId} | 游戏记录列表 | 孩子/家长 |
+| GET | /api/games/best/{childId}/{gameCode} | 最佳成绩 | 孩子/家长 |
+| GET | /api/games/statistics/{childId} | 训练统计 | 孩子/家长 |
+
+### 4.3 排行榜
+
+| 方法 | 路径 | 功能 | 权限 |
+|------|------|------|------|
+| GET | /api/games/ranking/{gameCode} | 全服排行榜 | 公开 |
+| GET | /api/games/ranking/family/{familyId} | 家庭排行榜 | 家长 |
+
+---
+
+## 五、前端页面设计
+
+| 页面 | 路径 | 功能 |
+|------|------|------|
+| 游戏列表 | `/pages/games/index` | 展示可玩游戏 |
+| 舒尔特方格 | `/pages/games/schulte` | 舒尔特方格游戏 |
+| 猜数字 | `/pages/games/1a2b` | 1a2b猜数字游戏 |
+| 数独 | `/pages/games/sudoku` | 数独游戏 |
+| 训练记录 | `/pages/games/records` | 历史训练记录 |
+| 训练统计 | `/pages/games/statistics` | 训练数据分析 |
+| 排行榜 | `/pages/games/ranking` | 排行榜展示 |
+
+---
+
+## 六、待完善事项
+
+| 功能 | 优先级 | 说明 |
+|------|--------|------|
+| 游戏记录存储 | P1 | 新增 game_record 表 |
+| 训练数据分析 | P1 | 统计 API |
+| 训练成果展示 | P1 | 前端页面 |
+| 训练提醒 | P2 | 定时任务 |
+| 激励机制 | P2 | 徽章/积分加成 |
+| 排行榜功能 | P1 | 排行榜 API |
+| 数据导出 | P2 | 训练报告导出 |
+
+---
+
+## 七、数据库设计建议
+
+### 7.1 游戏记录表
+
+```sql
+CREATE TABLE game_record (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    child_id BIGINT NOT NULL,
+    game_code VARCHAR(20) NOT NULL,
+    score INT NOT NULL,
+    duration INT NOT NULL COMMENT '用时(秒)',
+    level VARCHAR(10) DEFAULT 'medium',
+    rank INT DEFAULT 0,
+    points_earned INT DEFAULT 0,
+    completed_at DATETIME,
+    device_info VARCHAR(255),
+    created_at DATETIME,
+    INDEX idx_child (child_id),
+    INDEX idx_game (game_code),
+    INDEX idx_completed (completed_at)
+);
+```
+
+### 7.2 排行榜缓存表(可选)
+
+```sql
+CREATE TABLE game_ranking (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    game_code VARCHAR(20) NOT NULL,
+    child_id BIGINT NOT NULL,
+    best_score INT DEFAULT 0,
+    best_duration INT DEFAULT 0,
+    total_games INT DEFAULT 0,
+    total_points INT DEFAULT 0,
+    ranking INT DEFAULT 0,
+    updated_at DATETIME,
+    UNIQUE KEY uk_game_child (game_code, child_id)
+);
+```
+
+---
+
+## 八、现有代码参考
+
+### 8.1 实体类位置
+`zxyj-backend/src/main/java/com/zxyj/entity/MiniGame.java`
+
+### 8.2 现有三款游戏
+| 游戏 | gameCode | 状态 |
+|------|----------|------|
+| 舒尔特方格 | schulte | ✅ 已实现 |
+| 猜数字 | 1a2b | ✅ 已实现 |
+| 数独 | sudoku | ✅ 已实现 |
+
+### 8.3 前端页面
+- `zxyj-frontend/pages/games/` 目录下已有对应页面
+
+### 8.4 待补充
+- 游戏记录存储(需新增表)
+- 排行榜逻辑(需完善)
+- 训练数据分析(需开发)
+
+---
+
+## 九、后续开发建议
+
+### 9.1 第一阶段:完善记录
+1. 新增 game_record 表
+2. 完善游戏记录 API
+3. 训练记录页面
+
+### 9.2 第二阶段:数据分析
+1. 训练统计 API
+2. 最佳成绩展示
+3. 训练趋势图表
+
+### 9.3 第三阶段:激励与社交
+1. 排行榜功能
+2. 训练提醒定时任务
+3. 连续训练激励机制
+4. 与勋章系统关联

+ 209 - 0
docs/report-requirements.md

@@ -0,0 +1,209 @@
+# 报表统计模块需求分析
+
+**版本**: V1.2
+**更新时间**: 2026-04-15
+
+---
+
+## 一、需求清单概览
+
+| 需求ID | 描述 | 优先级 | 状态 |
+|--------|------|--------|------|
+| REPORT-001 | 个人任务完成统计 | P1 | ❌ 未实现 |
+| REPORT-002 | 家庭任务完成统计 | P1 | ❌ 未实现 |
+| REPORT-003 | 积分收支明细 | P1 | ❌ 未实现 |
+| REPORT-004 | 成长趋势图表 | P1 | ❌ 未实现 |
+
+---
+
+## 二、需求详细描述
+
+### 2.1 个人任务完成统计(REPORT-001)
+
+#### REPORT-001: 个人任务完成统计
+- **描述**:统计指定孩子在指定时间范围内的任务完成情况,包括完成任务数、超时数、驳回数、完成率等
+- **筛选条件**:
+  - 时间范围:今日/本周/本月/自定义
+  - 任务分类:学习类/生活类/运动类/其他
+  - 任务状态:已完成/已超时/已驳回
+- **统计维度**:
+  - 任务总数
+  - 完成任务数
+  - 超时任务数
+  - 驳回任务数
+  - 完成率
+  - 平均完成耗时
+- **展示形式**:数字卡片 + 列表 + 筛选条件
+
+---
+
+### 2.2 家庭任务完成统计(REPORT-002)
+
+#### REPORT-002: 家庭任务完成统计
+- **描述**:统计整个家庭在指定时间范围内的任务完成情况,支持按孩子分组
+- **筛选条件**:
+  - 时间范围:今日/本周/本月/自定义
+  - 家庭成员:全部/指定孩子
+  - 任务分类:学习类/生活类/运动类/其他
+- **统计维度**:
+  - 家庭任务总数
+  - 各孩子完成任务数
+  - 各孩子完成率对比
+  - 任务完成趋势
+  - 分类分布
+- **展示形式**:家庭概览卡片 + 孩子对比表格 + 趋势图表
+
+---
+
+### 2.3 积分收支明细(REPORT-003)
+
+#### REPORT-003: 积分收支明细
+- **描述**:记录并展示指定孩子或家庭成员的积分变动明细,包括获得积分和扣除积分
+- **筛选条件**:
+  - 时间范围:今日/本周/本月/自定义
+  - 积分类型:获得/扣除/全部
+  - 来源类型:任务奖励/打卡奖励/兑换扣除/超时扣分/系统赠送
+- **统计维度**:
+  - 当前积分余额
+  - 积分获得总数
+  - 积分扣除总数
+  - 积分净变化
+  - 各来源占比
+- **展示形式**:余额卡片 + 收支明细列表 + 饼图/趋势图
+
+---
+
+### 2.4 成长趋势图表(REPORT-004)
+
+#### REPORT-004: 成长趋势图表
+- **描述**:以图表形式展示孩子在一段时间内的成长趋势,包括任务完成、积分变化、打卡连续性等
+- **图表类型**:
+  - 折线图:任务完成趋势
+  - 柱状图:积分变化
+  - 热力图:打卡连续性
+  - 雷达图:能力维度分布
+- **时间粒度**:日/周/月
+- **对比功能**:
+  - 与自身历史对比
+  - 与家庭成员对比
+  - 与同龄孩子对比(可选)
+- **展示形式**:图表Dashboard + 数据导出
+
+---
+
+## 三、统计维度汇总
+
+### 3.1 任务统计维度
+
+| 维度 | 说明 | 计算方式 |
+|------|------|----------|
+| 任务总数 | 统计周期内创建的任务数 | COUNT(task_id) |
+| 完成任务数 | 状态为已完成的任务数 | COUNT WHERE status='completed' |
+| 超时任务数 | 超过截止时间的任務数 | COUNT WHERE status='overdue' |
+| 驳回任务数 | 审核被驳回的任务数 | COUNT WHERE reviewResult='rejected' |
+| 完成率 | 完成任务数/任务总数 | 完成任务数/任务总数 |
+| 平均完成耗时 | 从创建到完成平均耗时 | AVG(completedAt - createdAt) |
+
+### 3.2 积分统计维度
+
+| 维度 | 说明 | 计算方式 |
+|------|------|----------|
+| 当前积分 | 孩子当前持有的积分余额 | SUM(获得) - SUM(扣除) |
+| 获得积分 | 通过任务/打卡获得的积分 | SUM WHERE type='earned' |
+| 扣除积分 | 通过兑换/超时扣除的积分 | SUM WHERE type='deducted' |
+| 净变化 | 获得积分 - 扣除积分 | 获得积分 - 扣除积分 |
+
+### 3.3 时间粒度说明
+
+| 粒度 | 说明 | 适用场景 |
+|------|------|----------|
+| 今日 | 当天 00:00 ~ 23:59:59 | 实时查看 |
+| 本周 | 周一 ~ 周日 | 周报 |
+| 本月 | 1日 ~ 月末 | 月报 |
+| 自定义 | 用户选择起止日期 | 专题分析 |
+
+---
+
+## 四、API接口设计
+
+### 4.1 个人统计
+
+| 方法 | 路径 | 功能 | 权限 |
+|------|------|------|------|
+| GET | /api/reports/child/{childId}/tasks | 个人任务统计 | 孩子/家长 |
+| GET | /api/reports/child/{childId}/points | 个人积分统计 | 孩子/家长 |
+| GET | /api/reports/child/{childId}/trend | 个人成长趋势 | 孩子/家长 |
+
+### 4.2 家庭统计
+
+| 方法 | 路径 | 功能 | 权限 |
+|------|------|------|------|
+| GET | /api/reports/family/{familyId}/tasks | 家庭任务统计 | 家长 |
+| GET | /api/reports/family/{childId}/points | 家庭积分统计 | 家长 |
+| GET | /api/reports/family/{familyId}/ranking | 家庭排行 | 家长 |
+
+### 4.3 导出功能
+
+| 方法 | 路径 | 功能 | 权限 |
+|------|------|------|------|
+| GET | /api/reports/export/child/{childId} | 导出个人报表 | 孩子/家长 |
+| GET | /api/reports/export/family/{familyId} | 导出家庭报表 | 家长 |
+
+---
+
+## 五、前端页面设计
+
+| 页面 | 路径 | 功能 |
+|------|------|------|
+| 个人统计 | `/pages/report/child` | 个人任务与积分统计 |
+| 家庭统计 | `/pages/report/family` | 家庭整体统计与对比 |
+| 成长趋势 | `/pages/report/trend` | 图表化成长趋势 |
+| 数据导出 | `/pages/report/export` | 报表导出 |
+
+---
+
+## 六、待完善事项
+
+| 功能 | 优先级 | 说明 |
+|------|--------|------|
+| 个人任务统计API | P1 | 后端统计接口 |
+| 家庭任务统计API | P1 | 后端统计接口 |
+| 积分明细API | P1 | 后端明细接口 |
+| 趋势图表前端 | P1 | 图表组件集成 |
+| 数据导出功能 | P2 | PDF/Excel导出 |
+| 同龄对比 | P2 | 需要匿名汇总数据 |
+
+---
+
+## 七、后续开发建议
+
+### 7.1 第一阶段:基础统计
+1. 个人任务统计 API
+2. 个人积分统计 API
+3. 基础统计页面
+
+### 7.2 第二阶段:家庭统计
+1. 家庭任务统计 API
+2. 家庭排行 API
+3. 家庭统计页面
+
+### 7.3 第三阶段:可视化
+1. 成长趋势图表
+2. 打卡热力图
+3. 数据导出功能
+
+---
+
+## 八、技术实现建议
+
+### 8.1 统计缓存
+- 每日凌晨生成前一日统计快照
+- 使用缓存提高查询性能
+
+### 8.2 数据聚合
+- MySQL GROUP BY 实现基础统计
+- 复杂趋势计算可考虑预计算或定时任务
+
+### 8.3 图表方案
+- Web端:ECharts / AntV
+- 小程序端:uni-app 图表组件 / uCharts

+ 7 - 7
docs/task-requirements.md

@@ -422,14 +422,14 @@ pending → cancelled (创建者删除)
 
 - **任务列表页**: `id`、`title`、`points`、`category`、`deadline`、`status`、`needReview`、`childId`、`createdAt`
 - **筛选条件**: 任务标题、状态、家庭
-- **统计信息**: 总任务数、今日任务数
-- **适用场景**: 后台查看、检索、按家庭定位任务
+- **统计信息**总任务数、今日任务数
+- **适用场景**后台查看、检索、按家庭定位任务
 
 #### 3.6.3 Web端任务模板页显示重点
 
-- **列表字段**: 模板名称、分类、积分、难度、推荐年龄、时长、任务性质、是否需要审核、启用状态、创建时间
-- **编辑字段**: 标题、描述、分类、积分、难度、推荐年龄、预计时长、完成方式、任务性质、频率、周期次数、是否审核、审核方式、按分类审核
-- **适用场景**: 统一模板配置,供任务创建复用
+- **列表字段**模板名称、分类、积分、难度、推荐年龄、时长、任务性质、是否需要审核、启用状态、创建时间
+- **编辑字段**标题、描述、分类、积分、难度、推荐年龄、预计时长、完成方式、任务性质、频率、周期次数、是否审核、审核方式、按分类审核
+- **适用场景**统一模板配置,供任务创建复用
 
 #### 3.6.4 小程序任务列表页显示重点
 
@@ -505,7 +505,7 @@ pending → cancelled (创建者删除)
 | category | 任务分类 | 家长/创建人 | 列表显示 / 模板编辑 | 列表显示 / 创建页填写 | 否 | 创建 |
 | points | 奖励积分 | 家长/创建人 | 列表显示 / 模板编辑 | 列表显示 / 创建页填写 | 不建议改 | 创建 |
 | deadline | 截止时间 | 家长/创建人 | 列表显示 | 创建页填写 | 否 | 创建 |
-| childId / childIds | 执行孩子 | 家长/创建人 | 列表显示 childId | 创建页选择孩子 | 否 | 创建 |
+| childId / childIds | 执行孩子(单选/多选) | 家长/创建人 | 列表显示 childId | 创建页选择孩子 | 否 | 创建 |
 | repeatType | 重复规则 | 家长/创建人 | 模板编辑 | 创建页可设 | 否 | 创建 |
 | completeTypes | 完成方式 | 家长/创建人 | 模板编辑 | 创建页可设 | 否 | 创建 |
 | needReview | 是否审核 | 家长/创建人 | 列表显示 / 模板编辑 | 创建页可设 | 否 | 创建 |
@@ -565,7 +565,7 @@ pending → cancelled (创建者删除)
 | 字段 | 是否必填 | 说明 | 责任主体 |
 |------|----------|------|----------|
 | title | 是 | 任务名称 | 家长/创建人 |
-| childId / childIds | 是 | 执行孩子 | 家长/创建人 |
+| childId / childIds | 是 | 执行孩子(单选/多选) | 家长/创建人 |
 | points | 是 | 奖励积分 | 家长/创建人 |
 | category | 是 | 任务分类 | 家长/创建人 |
 | deadline | 是 | 截止时间 | 家长/创建人 |

+ 0 - 144
zxyj-backend/src/main/resources/migration_wishes.sql

@@ -1,144 +0,0 @@
--- 心愿功能重构迁移脚本
--- 执行时间: 预计 5 分钟
--- 影响范围: rewards, reward_wishlists 表数据
--- 执行前请备份数据!
-
--- 1. 创建新表 wishes
-CREATE TABLE IF NOT EXISTS wishes (
-    id BIGINT AUTO_INCREMENT PRIMARY KEY,
-    
-    -- 归属信息
-    family_id BIGINT NOT NULL COMMENT '家庭ID',
-    child_id BIGINT NOT NULL COMMENT '目标孩子ID',
-    created_by BIGINT NOT NULL COMMENT '创建者用户ID',
-    creator_type VARCHAR(10) NOT NULL COMMENT '创建者类型: child/parent',
-    
-    -- 心愿内容
-    title VARCHAR(200) NOT NULL COMMENT '心愿名称',
-    description TEXT COMMENT '详细描述',
-    category VARCHAR(50) COMMENT '分类',
-    
-    -- 积分设置
-    points_required INT COMMENT '所需积分',
-    points_set_by BIGINT COMMENT '定价者ID',
-    points_set_at DATETIME COMMENT '定价时间',
-    
-    -- 状态
-    status VARCHAR(20) DEFAULT 'draft' COMMENT '状态: draft/active/pending_exchange/approved/exchanging/fulfilled/cancelled/rejected',
-    
-    -- 兑换流程
-    exchange_requested_at DATETIME COMMENT '申请兑换时间',
-    exchange_approved_by BIGINT COMMENT '批准者ID',
-    exchange_approved_at DATETIME COMMENT '批准时间',
-    points_deducted_at DATETIME COMMENT '扣积分时间',
-    
-    -- 兑现流程
-    fulfilled_by BIGINT COMMENT '兑现者ID',
-    fulfilled_at DATETIME COMMENT '兑现时间',
-    child_confirmed_at DATETIME COMMENT '孩子确认收货时间',
-    
-    -- 拒绝记录
-    rejected_by BIGINT COMMENT '拒绝者ID',
-    rejected_at DATETIME COMMENT '拒绝时间',
-    reject_reason VARCHAR(500) COMMENT '拒绝原因',
-    reject_count INT DEFAULT 0 COMMENT '被拒绝次数',
-    
-    -- 取消记录
-    cancelled_by BIGINT COMMENT '取消者ID',
-    cancelled_at DATETIME COMMENT '取消时间',
-    cancel_reason VARCHAR(500) COMMENT '取消原因',
-    
-    -- 时间戳
-    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    
-    -- 索引
-    INDEX idx_family (family_id),
-    INDEX idx_child (child_id),
-    INDEX idx_status (status),
-    INDEX idx_creator (created_by)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='心愿表';
-
--- 2. 迁移 rewards 表数据(孩子创建的心愿)
--- 注意:这里需要根据实际数据调整
-INSERT INTO wishes (
-    id, family_id, child_id, created_by, creator_type,
-    title, description, category, points_required, points_set_by,
-    status, created_at, updated_at
-)
-SELECT 
-    r.id,
-    r.family_id,
-    r.child_id,
-    c.user_id as created_by,  -- 创建者为孩子关联的用户
-    'child' as creator_type,
-    r.title,
-    NULL as description,
-    r.category,
-    CASE WHEN r.status = 'pending_approval' THEN NULL ELSE r.points_required END,
-    CASE WHEN r.status = 'pending_approval' THEN NULL ELSE 1 END,  -- 需要后续修正
-    CASE 
-        WHEN r.status = 'pending_approval' THEN 'draft'
-        WHEN r.status = 'available' THEN 'active'
-        WHEN r.status = 'pending' THEN 'approved'
-        WHEN r.status = 'exchanged' THEN 'fulfilled'
-        WHEN r.status = 'rejected' THEN 'rejected'
-        ELSE r.status
-    END,
-    r.created_at,
-    r.updated_at
-FROM rewards r
-LEFT JOIN children c ON r.child_id = c.id
-WHERE r.is_template = 0 OR r.is_template IS NULL;
-
--- 3. 迁移 reward_wishlists 表数据(家长创建的心愿)
--- 注意:家长创建的心愿需要指定目标孩子,这里暂时设为0,需要后续修正
-INSERT INTO wishes (
-    family_id, child_id, created_by, creator_type,
-    title, description, category,
-    points_required, points_set_by, points_set_at,
-    status, created_at, updated_at
-)
-SELECT 
-    rw.family_id,
-    0 as child_id,  -- 需要后续修正
-    rw.user_id as created_by,
-    'parent' as creator_type,
-    rw.title,
-    rw.description,
-    rw.category,
-    rw.points_required,
-    rw.user_id as points_set_by,
-    rw.created_at as points_set_at,
-    CASE 
-        WHEN rw.status = 'pending' THEN 'active'
-        WHEN rw.status = 'exchanged' THEN 'fulfilled'
-        WHEN rw.status = 'rejected' THEN 'rejected'
-        ELSE rw.status
-    END,
-    rw.created_at,
-    rw.updated_at
-FROM reward_wishlists rw;
-
--- 4. 修正 points_set_by 字段(根据家庭关系更新)
--- 找到同一家庭的家长作为定价者
-UPDATE wishes w
-JOIN children c ON w.child_id = c.id
-JOIN users u ON c.family_id = u.family_id AND u.role = 'parent'
-SET w.points_set_by = u.id
-WHERE w.creator_type = 'child' 
-  AND w.points_set_by IS NULL 
-  AND w.points_required IS NOT NULL
-  AND w.status != 'draft';
-
--- 5. 备份旧表(不删除,保留备份)
--- 如果需要可以手动执行以下语句:
--- RENAME TABLE rewards TO rewards_backup_20260414;
--- RENAME TABLE reward_wishlists TO reward_wishlists_backup_20260414;
-
--- 6. 验证迁移结果
-SELECT 'wishes表记录数' as info, COUNT(*) as count FROM wishes
-UNION ALL
-SELECT 'rewards表记录数', COUNT(*) FROM rewards
-UNION ALL
-SELECT 'reward_wishlists表记录数', COUNT(*) FROM reward_wishlists;

+ 58 - 0
zxyj-backend/src/main/resources/schema-wishes.sql

@@ -0,0 +1,58 @@
+-- 心愿表 (wishes)
+-- 用途:每个人的实际心愿,与心愿库(rewards)区分
+-- 创建时间: 2026-04-15
+
+CREATE TABLE IF NOT EXISTS wishes (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    
+    -- 归属信息
+    family_id BIGINT NOT NULL COMMENT '家庭ID',
+    child_id BIGINT NOT NULL COMMENT '目标孩子ID',
+    created_by BIGINT NOT NULL COMMENT '创建者用户ID',
+    creator_type VARCHAR(10) NOT NULL COMMENT '创建者类型: child/parent',
+    
+    -- 心愿内容
+    title VARCHAR(200) NOT NULL COMMENT '心愿名称',
+    description TEXT COMMENT '详细描述',
+    category VARCHAR(50) COMMENT '分类',
+    
+    -- 积分设置
+    points_required INT COMMENT '所需积分',
+    points_set_by BIGINT COMMENT '定价者ID',
+    points_set_at DATETIME COMMENT '定价时间',
+    
+    -- 状态 (个人心愿状态机)
+    status VARCHAR(20) DEFAULT 'draft' COMMENT '状态: draft/active/pending_exchange/approved/exchanging/fulfilled/cancelled/rejected',
+    
+    -- 兑换流程
+    exchange_requested_at DATETIME COMMENT '申请兑换时间',
+    exchange_approved_by BIGINT COMMENT '批准者ID',
+    exchange_approved_at DATETIME COMMENT '批准时间',
+    points_deducted_at DATETIME COMMENT '扣积分时间',
+    
+    -- 兑现流程
+    fulfilled_by BIGINT COMMENT '兑现者ID',
+    fulfilled_at DATETIME COMMENT '兑现时间',
+    child_confirmed_at DATETIME COMMENT '孩子确认收货时间',
+    
+    -- 拒绝记录
+    rejected_by BIGINT COMMENT '拒绝者ID',
+    rejected_at DATETIME COMMENT '拒绝时间',
+    reject_reason VARCHAR(500) COMMENT '拒绝原因',
+    reject_count INT DEFAULT 0 COMMENT '被拒绝次数',
+    
+    -- 取消记录
+    cancelled_by BIGINT COMMENT '取消者ID',
+    cancelled_at DATETIME COMMENT '取消时间',
+    cancel_reason VARCHAR(500) COMMENT '取消原因',
+    
+    -- 时间戳
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    
+    -- 索引
+    INDEX idx_family (family_id),
+    INDEX idx_child (child_id),
+    INDEX idx_status (status),
+    INDEX idx_creator (created_by)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='个人心愿表';

+ 1 - 0
zxyj-frontend/AGENTS.md

@@ -52,6 +52,7 @@ pages.json 中定义 4 个 TabBar 页面:
 - **DO NOT** 在前端存储敏感信息 (密码、密钥)
 - **NEVER** 跳过 JWT 认证直接调用需登录接口
 - **NEVER** 在本地存储中保存敏感用户信息
+- **NEVER** 在 WXML/Vue 模板中使用可选链 `?.`,微信小程序不支持 → 使用 `&&` 代替(如 `currentWish?.title` 改为 `currentWish && currentWish.title`)
 
 ## UNIQUE FEATURES
 

+ 6 - 0
zxyj-frontend/pages.json

@@ -198,6 +198,12 @@
 "style": {
 "navigationBarTitleText": "心愿详情"
 }
+},
+{
+"path": "pages/parent/wishes/index",
+"style": {
+"navigationBarTitleText": "心愿管理"
+}
 }
 ],
   "globalStyle": {

+ 45 - 2
zxyj-frontend/pages/child/rewards.vue

@@ -1,5 +1,10 @@
 <template>
   <view class="container">
+    <!-- 退出切换按钮 -->
+    <view class="exit-switch" v-if="isSwitchedChild" @click="exitSwitch">
+      <text>🔄 退出切换</text>
+    </view>
+    
     <view class="reward-list" v-if="rewards.length > 0">
       <view v-for="reward in rewards" :key="reward.id" class="reward-item">
         <view class="reward-info">
@@ -38,7 +43,7 @@
 </template>
 
 <script>
-import { getWishlist, createReward as createRewardApi, getPointsBalance, getChildren } from '../../utils/api.js'
+import { getWishlist, createReward as createRewardApi, getPointsBalance, getChildren, switchBackToParent, verifyPassword } from '../../utils/api.js'
 
 export default {
   data() {
@@ -47,10 +52,12 @@ export default {
       currentPoints: 0,
       currentChildId: '',
       showAddModal: false,
-      newReward: { title: '' }
+      newReward: { title: '' },
+      isSwitchedChild: false // 家长切换到孩子状态的标记
     }
   },
   onShow() {
+    this.isSwitchedChild = uni.getStorageSync('isSwitchedChild') || false
     this.loadRewards()
   },
   methods: {
@@ -88,6 +95,41 @@ export default {
       } catch (e) {
         console.error('添加心愿失败', e)
       }
+    },
+    exitSwitch() {
+      if (!this.isSwitchedChild) return
+      
+      uni.showModal({
+        title: '退出切换',
+        content: '请输入家长密码以确认退出',
+        editable: true,
+        success: async (res) => {
+          if (res.confirm && res.content) {
+            try {
+              const verifyResult = await verifyPassword(res.content)
+              if (verifyResult.code === 200) {
+                await this.switchBackToParentMode()
+              } else {
+                uni.showToast({ title: '密码错误', icon: 'none' })
+              }
+            } catch (e) {
+              uni.showToast({ title: '密码验证失败', icon: 'none' })
+            }
+          }
+        }
+      })
+    },
+    async switchBackToParentMode() {
+      try {
+        const result = await switchBackToParent()
+        if (result.data) {
+          this.$store.commit('switchBackToParent')
+          uni.showToast({ title: '已切换回家长模式', icon: 'success' })
+          uni.reLaunch({ url: '/pages/index/parent-index' })
+        }
+      } catch (e) { 
+        uni.showToast({ title: '切换失败', icon: 'none' }) 
+      }
     }
   }
 }
@@ -95,6 +137,7 @@ export default {
 
 <style scoped>
 .container { padding: 30rpx; padding-bottom: 120rpx; }
+.exit-switch { background: #f5f5f5; padding: 20rpx; border-radius: 10rpx; margin-bottom: 20rpx; text-align: center; color: #666; font-size: 28rpx; }
 .reward-item { background: #fff; border-radius: 16rpx; padding: 30rpx; margin-bottom: 20rpx; }
 .reward-title { font-size: 30rpx; color: #333; margin-bottom: 15rpx; }
 .reward-progress { width: 100%; }

+ 46 - 2
zxyj-frontend/pages/child/tasks.vue

@@ -1,6 +1,11 @@
 <template>
   <PlayfulCard elevation="1">
     <view class="container">
+      <!-- 退出切换按钮 -->
+      <view class="exit-switch" v-if="isSwitchedChild" @click="exitSwitch">
+        <text>🔄 退出切换</text>
+      </view>
+      
       <view class="task-list" v-if="tasks.length > 0">
         <view v-for="task in tasks" :key="task.id" class="task-item" :class="{ completed: task.status === 'completed' }">
           <view class="task-left">
@@ -26,17 +31,19 @@
 
 <script>
 import PlayfulCard from '@/components/PlayfulCard.vue'
-import { getTodayTasks, completeTask as completeTaskApi, getChildren } from '../../utils/api.js'
+import { getTodayTasks, completeTask as completeTaskApi, getChildren, switchBackToParent, verifyPassword } from '../../utils/api.js'
 
 export default {
   components: { PlayfulCard },
   data() {
     return {
       tasks: [],
-      currentChildId: ''
+      currentChildId: '',
+      isSwitchedChild: false // 家长切换到孩子状态的标记
     }
   },
   onShow() {
+    this.isSwitchedChild = uni.getStorageSync('isSwitchedChild') || false
     this.loadData()
   },
   methods: {
@@ -71,6 +78,42 @@ export default {
       if (!dateStr) return ''
       const date = new Date(dateStr)
       return `${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`
+    },
+    exitSwitch() {
+      // 只有家长切换到孩子状态时才显示退出切换功能
+      if (!this.isSwitchedChild) return
+      
+      uni.showModal({
+        title: '退出切换',
+        content: '请输入家长密码以确认退出',
+        editable: true,
+        success: async (res) => {
+          if (res.confirm && res.content) {
+            try {
+              const verifyResult = await verifyPassword(res.content)
+              if (verifyResult.code === 200) {
+                await this.switchBackToParentMode()
+              } else {
+                uni.showToast({ title: '密码错误', icon: 'none' })
+              }
+            } catch (e) {
+              uni.showToast({ title: '密码验证失败', icon: 'none' })
+            }
+          }
+        }
+      })
+    },
+    async switchBackToParentMode() {
+      try {
+        const result = await switchBackToParent()
+        if (result.data) {
+          this.$store.commit('switchBackToParent')
+          uni.showToast({ title: '已切换回家长模式', icon: 'success' })
+          uni.reLaunch({ url: '/pages/index/parent-index' })
+        }
+      } catch (e) { 
+        uni.showToast({ title: '切换失败', icon: 'none' }) 
+      }
     }
   }
 }
@@ -78,6 +121,7 @@ export default {
 
 <style scoped>
 .container { padding: 30rpx; }
+.exit-switch { background: #f5f5f5; padding: 20rpx; border-radius: 10rpx; margin-bottom: 20rpx; text-align: center; color: #666; font-size: 28rpx; }
 .task-item { background: #fff; border-radius: 16rpx; padding: 30rpx; margin-bottom: 20rpx; display: flex; justify-content: space-between; align-items: center; }
 .task-item.completed { opacity: 0.6; }
 .task-title { font-size: 30rpx; color: #333; margin-bottom: 10rpx; }

+ 13 - 7
zxyj-frontend/pages/index/child-index.vue

@@ -152,7 +152,8 @@ export default {
       currentChildId: '',
       showConfetti: false,
       level: '',
-      tip: ''
+      tip: '',
+      isSwitchedChild: false // 家长切换到孩子状态的标记
     }
   },
   computed: {
@@ -162,13 +163,15 @@ export default {
       return (this.completedCount / this.totalTaskCount) * 100
     }
   },
-  onLoad() { this.checkLogin() },
+onLoad() { this.checkLogin() },
   onShow() {
     const currentRole = uni.getStorageSync('currentRole') || 'parent'
     if (currentRole !== 'child') {
       uni.reLaunch({ url: '/pages/index/parent-index' })
       return
     }
+    // 获取切换标记
+    this.isSwitchedChild = uni.getStorageSync('isSwitchedChild') || false
     if (uni.getStorageSync('token')) this.loadData()
   },
   methods: {
@@ -288,10 +291,14 @@ export default {
       uni.showModal({ title: '🔥 连续打卡', content: content, showCancel: false })
     },
     switchMode() {
+      // 只有家长切换到孩子状态时才显示退出切换功能
+      if (!this.isSwitchedChild) {
+        return // 孩子正常登录不显示切换按钮
+      }
       // 弹出密码输入框
       uni.showModal({
-        title: '切换模式',
-        content: '请输入家长密码以确认切换',
+        title: '退出切换',
+        content: '请输入家长密码以确认退出',
         editable: true,
         success: async (res) => {
           if (res.confirm && res.content) {
@@ -314,9 +321,8 @@ async switchBackToParentMode() {
     try {
       const result = await switchBackToParent()
       if (result.data) {
-        // 切换回家长视图模式
-        uni.setStorageSync('currentRole', 'parent')
-        uni.removeStorageSync('currentChildId')
+        // 使用store的mutation清除切换标记
+        this.$store.commit('switchBackToParent')
         uni.showToast({ title: '已切换回家长模式', icon: 'success' })
         uni.reLaunch({ url: '/pages/index/parent-index' })
       }

+ 4 - 10
zxyj-frontend/pages/index/parent-index.vue

@@ -184,12 +184,8 @@ export default {
             const result = await switchToChild(child.id)
             if (result.data) {
               this.currentChildId = child.id
-              // 只切换视图模式,不修改实际角色
-              this.$store.commit('switchViewMode', 'child')
-              this.$store.commit('setCurrentChild', child.id)
-              // 保存当前孩子ID
-              uni.setStorageSync('currentChildId', child.id)
-              uni.setStorageSync('currentRole', 'child')
+              // 使用新的切换到孩子模式mutation,设置isSwitchedChild标记
+              this.$store.commit('switchToChild', child.id)
               uni.showToast({
                 title: `已切换到 ${child.nickname} 模式`,
                 icon: 'success'
@@ -215,10 +211,8 @@ export default {
       const result = await switchBackToParent()
       if (result.data) {
         this.currentChildId = ''
-        // 切换回家长视图模式
-        this.$store.commit('switchViewMode', 'parent')
-        uni.setStorageSync('currentRole', 'parent')
-        uni.removeStorageSync('currentChildId')
+        // 使用新的切换回家长模式mutation,清除isSwitchedChild标记
+        this.$store.commit('switchBackToParent')
         uni.showToast({
           title: '已切换回家长模式',
           icon: 'success'

+ 540 - 0
zxyj-frontend/pages/parent/wishes/index.vue

@@ -0,0 +1,540 @@
+<template>
+  <view class="container">
+    <!-- 标签页切换 -->
+    <view class="tabs">
+      <view 
+        class="tab-item" 
+        :class="{ active: activeTab === 'pending' }"
+        @click="activeTab = 'pending'"
+      >
+        待处理
+        <text class="badge" v-if="pendingCount > 0">{{ pendingCount }}</text>
+      </view>
+      <view 
+        class="tab-item" 
+        :class="{ active: activeTab === 'all' }"
+        @click="activeTab = 'all'"
+      >
+        全部心愿
+      </view>
+    </view>
+
+    <!-- 待处理列表 -->
+    <view class="wish-list" v-if="activeTab === 'pending'">
+      <!-- 待定价 -->
+      <view class="section" v-if="draftWishes.length > 0">
+        <view class="section-title">📝 待定价</view>
+        <view 
+          v-for="wish in draftWishes" 
+          :key="wish.id" 
+          class="wish-card"
+        >
+          <view class="wish-info">
+            <text class="wish-child">{{ wish.childName }}</text>
+            <text class="wish-title">{{ wish.title }}</text>
+            <text class="wish-category" v-if="wish.category">{{ wish.category }}</text>
+          </view>
+          <view class="wish-actions">
+            <button class="btn-price" @click="showPriceDialog(wish)">定价</button>
+            <button class="btn-reject" @click="rejectWish(wish)">拒绝</button>
+          </view>
+        </view>
+      </view>
+
+      <!-- 待审批兑换 -->
+      <view class="section" v-if="pendingExchangeWishes.length > 0">
+        <view class="section-title">⏳ 待审批兑换</view>
+        <view 
+          v-for="wish in pendingExchangeWishes" 
+          :key="wish.id" 
+          class="wish-card"
+        >
+          <view class="wish-info">
+            <text class="wish-child">{{ wish.childName }}</text>
+            <text class="wish-title">{{ wish.title }}</text>
+            <text class="wish-points">{{ wish.pointsRequired }}积分</text>
+          </view>
+          <view class="wish-actions">
+            <button class="btn-approve" @click="approveExchange(wish, true)">批准</button>
+            <button class="btn-reject" @click="approveExchange(wish, false)">拒绝</button>
+          </view>
+        </view>
+      </view>
+
+      <!-- 待兑现 -->
+      <view class="section" v-if="approvedWishes.length > 0">
+        <view class="section-title">📦 待兑现</view>
+        <view 
+          v-for="wish in approvedWishes" 
+          :key="wish.id" 
+          class="wish-card"
+        >
+          <view class="wish-info">
+            <text class="wish-child">{{ wish.childName }}</text>
+            <text class="wish-title">{{ wish.title }}</text>
+            <text class="wish-points">已扣{{ wish.pointsRequired }}积分</text>
+          </view>
+          <view class="wish-actions">
+            <button class="btn-fulfill" @click="markFulfilled(wish)">已购买</button>
+          </view>
+        </view>
+      </view>
+
+      <!-- 空状态 -->
+      <view class="empty" v-if="pendingWishes.length === 0">
+        <text class="empty-icon">✅</text>
+        <text class="empty-text">暂无待处理心愿</text>
+      </view>
+    </view>
+
+    <!-- 全部心愿列表 -->
+    <view class="wish-list" v-if="activeTab === 'all'">
+      <view 
+        v-for="wish in allWishes" 
+        :key="wish.id" 
+        class="wish-card"
+        :class="wish.status"
+      >
+        <view class="wish-header">
+          <view class="wish-info-left">
+            <text class="wish-child">{{ wish.childName }}</text>
+            <text class="wish-title">{{ wish.title }}</text>
+          </view>
+          <text class="wish-status" :class="wish.status">{{ wish.statusText }}</text>
+        </view>
+        <view class="wish-meta">
+          <text class="wish-points" v-if="wish.pointsRequired">{{ wish.pointsRequired }}积分</text>
+          <text class="wish-time">{{ wish.createdAt }}</text>
+        </view>
+      </view>
+
+      <!-- 空状态 -->
+      <view class="empty" v-if="allWishes.length === 0">
+        <text class="empty-icon">🎁</text>
+        <text class="empty-text">暂无心愿</text>
+      </view>
+    </view>
+
+    <!-- 定价弹窗 -->
+    <view class="modal-mask" v-if="priceDialogVisible" @click="priceDialogVisible = false">
+      <view class="modal" @click.stop>
+        <view class="modal-title">设置积分</view>
+        <view class="modal-content">
+          <text class="modal-wish-title">{{ currentWish && currentWish.title }}</text>
+          <view class="input-row">
+            <text class="inputLabel">所需积分:</text>
+            <input 
+              type="number" 
+              v-model="priceValue" 
+              placeholder="请输入积分"
+              class="price-input"
+            />
+          </view>
+        </view>
+        <view class="modal-btns">
+          <button class="btn-cancel" @click="priceDialogVisible = false">取消</button>
+          <button class="btn-confirm" @click="submitPrice">确定</button>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { 
+  getPendingWishes, 
+  getWishes, 
+  setWishPrice, 
+  rejectWish,
+  approveWishExchange,
+  markWishFulfilled
+} from '../../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      activeTab: 'pending',
+      familyId: '',
+      pendingWishes: [],
+      allWishes: [],
+      priceDialogVisible: false,
+      currentWish: null,
+      priceValue: ''
+    }
+  },
+  computed: {
+    pendingCount() {
+      return this.pendingWishes.length
+    },
+    draftWishes() {
+      return this.pendingWishes.filter(w => w.status === 'draft')
+    },
+    pendingExchangeWishes() {
+      return this.pendingWishes.filter(w => w.status === 'pending_exchange')
+    },
+    approvedWishes() {
+      return this.pendingWishes.filter(w => w.status === 'approved')
+    }
+  },
+  onShow() {
+    this.familyId = uni.getStorageSync('familyId')
+    this.loadData()
+  },
+  methods: {
+    async loadData() {
+      try {
+        // 获取待处理列表
+        const pendingRes = await getPendingWishes(this.familyId)
+        this.pendingWishes = pendingRes.data || []
+        
+        // 获取全部心愿
+        const allRes = await getWishes({ familyId: this.familyId })
+        this.allWishes = allRes.data || []
+      } catch (e) {
+        console.error('加载数据失败', e)
+        uni.showToast({ title: '加载失败', icon: 'none' })
+      }
+    },
+    
+    showPriceDialog(wish) {
+      this.currentWish = wish
+      this.priceValue = ''
+      this.priceDialogVisible = true
+    },
+    
+    async submitPrice() {
+      const points = parseInt(this.priceValue)
+      if (!points || points <= 0) {
+        uni.showToast({ title: '请输入有效积分', icon: 'none' })
+        return
+      }
+      
+      try {
+        await setWishPrice(this.currentWish.id, points)
+        uni.showToast({ title: '定价成功', icon: 'success' })
+        this.priceDialogVisible = false
+        this.loadData()
+      } catch (e) {
+        uni.showToast({ title: e.message || '定价失败', icon: 'none' })
+      }
+    },
+    
+    async rejectWish(wish) {
+      uni.showModal({
+        title: '拒绝心愿',
+        content: `确定要拒绝"${wish.title}"吗?`,
+        success: async (res) => {
+          if (res.confirm) {
+            try {
+              await rejectWish(wish.id, '不合适')
+              uni.showToast({ title: '已拒绝', icon: 'success' })
+              this.loadData()
+            } catch (e) {
+              uni.showToast({ title: '操作失败', icon: 'none' })
+            }
+          }
+        }
+      })
+    },
+    
+    async approveExchange(wish, approved) {
+      const action = approved ? '批准' : '拒绝'
+      uni.showModal({
+        title: `${action}兑换`,
+        content: `确定要${action}"${wish.title}"的兑换申请吗?`,
+        success: async (res) => {
+          if (res.confirm) {
+            try {
+              await approveWishExchange(wish.id, approved)
+              uni.showToast({ title: `已${action}`, icon: 'success' })
+              this.loadData()
+            } catch (e) {
+              uni.showToast({ title: e.message || '操作失败', icon: 'none' })
+            }
+          }
+        }
+      })
+    },
+    
+    async markFulfilled(wish) {
+      uni.showModal({
+        title: '标记已购买',
+        content: `确定已购买"${wish.title}"吗?`,
+        success: async (res) => {
+          if (res.confirm) {
+            try {
+              await markWishFulfilled(wish.id)
+              uni.showToast({ title: '已标记', icon: 'success' })
+              this.loadData()
+            } catch (e) {
+              uni.showToast({ title: '操作失败', icon: 'none' })
+            }
+          }
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  padding: 30rpx;
+  min-height: 100vh;
+  background: #f5f5f5;
+}
+
+/* 标签页 */
+.tabs {
+  display: flex;
+  background: #fff;
+  border-radius: 50rpx;
+  padding: 8rpx;
+  margin-bottom: 30rpx;
+}
+
+.tab-item {
+  flex: 1;
+  text-align: center;
+  padding: 16rpx;
+  border-radius: 40rpx;
+  font-size: 28rpx;
+  color: #666;
+  position: relative;
+}
+
+.tab-item.active {
+  background: linear-gradient(135deg, #FF6B6B, #FF8E53);
+  color: #fff;
+}
+
+.badge {
+  position: absolute;
+  top: 4rpx;
+  right: 20rpx;
+  background: #FF6B6B;
+  color: #fff;
+  font-size: 20rpx;
+  padding: 2rpx 10rpx;
+  border-radius: 20rpx;
+  min-width: 30rpx;
+  text-align: center;
+}
+
+.tab-item.active .badge {
+  background: #fff;
+  color: #FF6B6B;
+}
+
+/* 分组 */
+.section {
+  margin-bottom: 30rpx;
+}
+
+.section-title {
+  font-size: 28rpx;
+  color: #666;
+  margin-bottom: 15rpx;
+  padding-left: 10rpx;
+}
+
+/* 心愿卡片 */
+.wish-card {
+  background: #fff;
+  border-radius: 15rpx;
+  padding: 25rpx;
+  margin-bottom: 15rpx;
+}
+
+.wish-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+}
+
+.wish-info {
+  flex: 1;
+}
+
+.wish-info-left {
+  flex: 1;
+}
+
+.wish-child {
+  font-size: 24rpx;
+  color: #FF6B6B;
+  background: #FFF5F5;
+  padding: 4rpx 12rpx;
+  border-radius: 10rpx;
+  margin-right: 10rpx;
+}
+
+.wish-title {
+  font-size: 30rpx;
+  color: #333;
+  font-weight: bold;
+  margin-left: 10rpx;
+}
+
+.wish-category {
+  font-size: 24rpx;
+  color: #666;
+  background: #f5f5f5;
+  padding: 4rpx 12rpx;
+  border-radius: 10rpx;
+  margin-left: 10rpx;
+}
+
+.wish-meta {
+  display: flex;
+  justify-content: space-between;
+  margin-top: 15rpx;
+}
+
+.wish-points {
+  font-size: 26rpx;
+  color: #FF6B6B;
+  font-weight: bold;
+}
+
+.wish-time {
+  font-size: 24rpx;
+  color: #999;
+}
+
+.wish-status {
+  font-size: 24rpx;
+  padding: 6rpx 16rpx;
+  border-radius: 20rpx;
+  background: #f0f0f0;
+  color: #666;
+}
+
+.wish-status.active { background: #D1FAE5; color: #059669; }
+.wish-status.pending_exchange { background: #FEF3C7; color: #D97706; }
+.wish-status.approved { background: #DBEAFE; color: #2563EB; }
+.wish-status.exchanging { background: #DBEAFE; color: #2563EB; }
+.wish-status.fulfilled { background: #EDE9FE; color: #7C3AED; }
+
+.wish-actions {
+  display: flex;
+  gap: 15rpx;
+  margin-top: 20rpx;
+}
+
+.btn-price, .btn-approve, .btn-fulfill {
+  background: linear-gradient(135deg, #FF6B6B, #FF8E53);
+  color: #fff;
+  font-size: 26rpx;
+  padding: 12rpx 24rpx;
+  border-radius: 30rpx;
+  margin: 0;
+}
+
+.btn-reject {
+  background: #fff;
+  color: #999;
+  border: 1rpx solid #ddd;
+  font-size: 26rpx;
+  padding: 12rpx 24rpx;
+  border-radius: 30rpx;
+  margin: 0;
+}
+
+/* 空状态 */
+.empty {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 80rpx;
+}
+
+.empty-icon {
+  font-size: 80rpx;
+  margin-bottom: 20rpx;
+}
+
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+}
+
+/* 弹窗 */
+.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;
+}
+
+.modal {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 40rpx;
+  width: 80%;
+}
+
+.modal-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  text-align: center;
+  margin-bottom: 30rpx;
+}
+
+.modal-content {
+  margin-bottom: 30rpx;
+}
+
+.modal-wish-title {
+  font-size: 28rpx;
+  color: #333;
+  display: block;
+  text-align: center;
+  margin-bottom: 30rpx;
+}
+
+.input-row {
+  display: flex;
+  align-items: center;
+}
+
+.inputLabel {
+  font-size: 28rpx;
+  color: #333;
+}
+
+.price-input {
+  flex: 1;
+  background: #f5f5f5;
+  border-radius: 15rpx;
+  padding: 20rpx;
+  font-size: 28rpx;
+}
+
+.modal-btns {
+  display: flex;
+  gap: 20rpx;
+}
+
+.btn-cancel, .btn-confirm {
+  flex: 1;
+  padding: 20rpx;
+  border-radius: 40rpx;
+  font-size: 28rpx;
+}
+
+.btn-cancel {
+  background: #f5f5f5;
+  color: #666;
+}
+
+.btn-confirm {
+  background: linear-gradient(135deg, #FF6B6B, #FF8E53);
+  color: #fff;
+}
+</style>

+ 41 - 6
zxyj-frontend/pages/profile/profile.vue

@@ -1,12 +1,18 @@
 <template>
   <view class="container">
+    <!-- 退出切换按钮(仅家长切换到孩子状态时显示) -->
+    <view class="exit-switch" v-if="isSwitchedChild" @click="exitSwitch">
+      <text>🔄 退出切换</text>
+    </view>
+    
     <view class="user-card">
       <view class="avatar">👤</view>
       <view class="user-info">
         <view class="nickname">{{ nickname || '未设置昵称' }}</view>
         <view class="role">{{ role === 'parent' ? '家长模式' : (role === 'child' ? '孩子模式' : '指导师模式') }}</view>
       </view>
-      <button class="btn-switch" @click="switchMode">切换</button>
+      <!-- 切换按钮仅在非切换状态下显示 -->
+      <button class="btn-switch" v-if="!isSwitchedChild" @click="switchMode">切换</button>
     </view>
 
     <view class="menu-list">
@@ -70,13 +76,16 @@ export default {
       oldPassword: '',
       newPassword: '',
       confirmPassword: '',
-      children: []
+      children: [],
+      isSwitchedChild: false // 家长切换到孩子状态的标记
     }
   },
   onShow() {
     this.nickname = this.$store.state.nickname || uni.getStorageSync('userInfo')?.nickname || ''
     this.role = this.$store.state.role || 'parent'
     this.hasPassword = !!uni.getStorageSync('passwordSet')
+    // 获取切换标记
+    this.isSwitchedChild = uni.getStorageSync('isSwitchedChild') || false
     this.loadChildren()
   },
   methods: {
@@ -125,7 +134,7 @@ export default {
           return
         }
         if (this.children.length === 1) {
-          this.$store.commit('switchRole', newRole)
+          this.$store.commit('switchToChild', this.children[0].id)
           this.role = newRole
           uni.showToast({ title: '已切换为孩子模式', icon: 'success' })
           return
@@ -134,15 +143,14 @@ export default {
           itemList: this.children.map(c => c.nickname),
           success: (res) => {
             const selectedChild = this.children[res.tapIndex]
-            uni.setStorageSync('currentChildId', selectedChild.id)
-            this.$store.commit('switchRole', newRole)
+            this.$store.commit('switchToChild', selectedChild.id)
             this.role = newRole
             uni.showToast({ title: `已切换为${selectedChild.nickname}模式`, icon: 'success' })
           }
         })
         return
       }
-      this.$store.commit('switchRole', newRole)
+      this.$store.commit('switchBackToParent')
       this.role = newRole
       uni.showToast({ title: `已切换为${newRole === 'parent' ? '家长' : '孩子'}模式`, icon: 'success' })
     },
@@ -211,6 +219,33 @@ export default {
           }
         }
       })
+    },
+    exitSwitch() {
+      if (!this.isSwitchedChild) return
+      uni.showModal({
+        title: '退出切换',
+        content: '请输入家长密码以确认退出',
+        editable: true,
+        success: async (res) => {
+          if (res.confirm && res.content) {
+            try {
+              const verifyResult = await verifyPassword(res.content)
+              if (verifyResult.code === 200) {
+                this.$store.commit('switchBackToParent')
+                this.isSwitchedChild = false
+                this.role = 'parent'
+                uni.showToast({ title: '已退出切换', icon: 'success' })
+                // 刷新首页
+                uni.reLaunch({ url: '/pages/index/index' })
+              } else {
+                uni.showToast({ title: '密码错误', icon: 'none' })
+              }
+            } catch (e) {
+              uni.showToast({ title: '验证失败', icon: 'none' })
+            }
+          }
+        }
+      })
     }
   }
 }

+ 25 - 1
zxyj-frontend/store/index.js

@@ -12,7 +12,8 @@ const store = new Vuex.Store({
     familyId: '',
     nickname: '',
     currentChildId: '',
-    children: []
+    children: [],
+    isSwitchedChild: false // 家长切换到孩子状态的标记
   },
   mutations: {
     setToken(state, token) {
@@ -30,6 +31,9 @@ const store = new Vuex.Store({
       uni.setStorageSync('role', userInfo.role)
       uni.setStorageSync('currentRole', userInfo.role)
       uni.setStorageSync('familyId', userInfo.familyId)
+      // 登录时清除切换标记
+      state.isSwitchedChild = false
+      uni.setStorageSync('isSwitchedChild', false)
     },
     setChildren(state, children) {
       state.children = children
@@ -41,6 +45,24 @@ const store = new Vuex.Store({
       state.currentChildId = childId
       uni.setStorageSync('currentChildId', childId)
     },
+    // 切换到孩子模式(家长切换)
+    switchToChild(state, childId) {
+      state.currentRole = 'child'
+      state.currentChildId = childId
+      state.isSwitchedChild = true
+      uni.setStorageSync('currentRole', 'child')
+      uni.setStorageSync('currentChildId', childId)
+      uni.setStorageSync('isSwitchedChild', true)
+    },
+    // 切换回家长模式
+    switchBackToParent(state) {
+      state.currentRole = 'parent'
+      state.currentChildId = ''
+      state.isSwitchedChild = false
+      uni.setStorageSync('currentRole', 'parent')
+      uni.removeStorageSync('currentChildId')
+      uni.setStorageSync('isSwitchedChild', false)
+    },
     // 切换视图模式(不修改数据库中的实际角色)
     switchViewMode(state, role) {
       state.currentRole = role
@@ -60,8 +82,10 @@ const store = new Vuex.Store({
       state.nickname = ''
       state.currentChildId = ''
       state.children = []
+      state.isSwitchedChild = false
       uni.removeStorageSync('token')
       uni.removeStorageSync('currentRole')
+      uni.removeStorageSync('isSwitchedChild')
     }
   },
   actions: {

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

@@ -184,7 +184,7 @@ export const getAuthorizations = () => {
 }
 
 export const getChildren = () => {
-  return request('/api/user/children', 'POST')
+  return request('/api/user/children/list', 'POST')
 }
 
 export const createChild = (data) => {
@@ -305,6 +305,33 @@ export const approveParentWishlist = (id, pointsRequired) => {
   return request(`/api/parent/wishlist/${id}/approve`, 'POST', { pointsRequired })
 }
 
+// =============================================
+// 家长心愿管理模块
+// =============================================
+export const getPendingWishes = (familyId) => {
+  return request('/api/wishes/pending', 'POST', { familyId })
+}
+
+export const getWishes = (params) => {
+  return request('/api/wishes', 'POST', params)
+}
+
+export const setWishPrice = (wishId, points) => {
+  return request(`/api/wishes/${wishId}/price`, 'POST', { pointsRequired: points })
+}
+
+export const rejectWish = (wishId, reason) => {
+  return request(`/api/wishes/${wishId}/reject`, 'POST', { reason })
+}
+
+export const approveWishExchange = (wishId, approved) => {
+  return request(`/api/wishes/${wishId}/approve-exchange`, 'POST', { approved })
+}
+
+export const markWishFulfilled = (wishId) => {
+  return request(`/api/wishes/${wishId}/fulfill`, 'POST', {})
+}
+
 // =============================================
 // 指导师-套餐模块
 // =============================================

+ 72 - 0
zxyj-web/src/api/admin.js

@@ -219,6 +219,78 @@ export function getPointsStats() {
   })
 }
 
+// ========== 心愿管理 API(新版) ==========
+
+// 获取心愿列表
+export function getWishesList(params) {
+  return request({
+    url: '/api/wishes',
+    method: 'get',
+    params
+  })
+}
+
+// 获取待处理列表(家长)
+export function getPendingWishes(familyId) {
+  return request({
+    url: '/api/wishes/pending',
+    method: 'get',
+    params: { familyId }
+  })
+}
+
+// 获取心愿详情
+export function getWishDetail(id) {
+  return request({
+    url: `/api/wishes/${id}`,
+    method: 'get'
+  })
+}
+
+// 家长定价
+export function setWishPrice(id, pointsRequired) {
+  return request({
+    url: `/api/wishes/${id}/set-price`,
+    method: 'put',
+    data: { pointsRequired }
+  })
+}
+
+// 家长拒绝心愿
+export function rejectWish(id, reason) {
+  return request({
+    url: `/api/wishes/${id}/reject`,
+    method: 'put',
+    data: { reason }
+  })
+}
+
+// 家长审批兑换
+export function approveWishExchange(id, approved, reason) {
+  return request({
+    url: `/api/wishes/${id}/approve-exchange`,
+    method: 'put',
+    data: { approved, reason }
+  })
+}
+
+// 家长标记已购买
+export function markWishFulfilled(id) {
+  return request({
+    url: `/api/wishes/${id}/fulfill`,
+    method: 'put'
+  })
+}
+
+// 取消心愿
+export function deleteWish(id, reason) {
+  return request({
+    url: `/api/wishes/${id}`,
+    method: 'delete',
+    data: { reason }
+  })
+}
+
 // ========== 指导师相关 API ==========
 
 // 获取指导师关联的家庭列表

+ 553 - 0
zxyj-web/src/views/Wishes.vue

@@ -0,0 +1,553 @@
+<template>
+  <div class="page-container">
+    <el-card>
+      <div slot="header">
+        <span>心愿管理</span>
+        <el-button
+          type="primary"
+          size="small"
+          style="float: right"
+          @click="handleRefresh"
+          :loading="loading"
+        >
+          刷新
+        </el-button>
+      </div>
+
+      <el-tabs v-model="activeTab" @tab-click="handleTabClick">
+        <!-- 待处理列表 -->
+        <el-tab-pane label="待处理" name="pending">
+          <!-- 统计卡片 -->
+          <el-row :gutter="20" style="margin-bottom: 20px">
+            <el-col :span="6">
+              <el-card shadow="hover" class="stat-card draft">
+                <div class="stat-num">{{ draftCount }}</div>
+                <div class="stat-label">待定价</div>
+              </el-card>
+            </el-col>
+            <el-col :span="6">
+              <el-card shadow="hover" class="stat-card pending">
+                <div class="stat-num">{{ pendingCount }}</div>
+                <div class="stat-label">待审批</div>
+              </el-card>
+            </el-col>
+            <el-col :span="6">
+              <el-card shadow="hover" class="stat-card approved">
+                <div class="stat-num">{{ approvedCount }}</div>
+                <div class="stat-label">待兑现</div>
+              </el-card>
+            </el-col>
+            <el-col :span="6">
+              <el-card shadow="hover" class="stat-card exchanging">
+                <div class="stat-num">{{ exchangingCount }}</div>
+                <div class="stat-label">待收货</div>
+              </el-card>
+            </el-col>
+          </el-row>
+
+          <!-- 待定价列表 -->
+          <div v-if="draftWishes.length > 0" class="wish-section">
+            <div class="section-title">📝 待定价</div>
+            <el-table :data="draftWishes" border v-loading="loading" style="width: 100%">
+              <el-table-column prop="id" label="ID" width="80"></el-table-column>
+              <el-table-column prop="childName" label="孩子" width="100"></el-table-column>
+              <el-table-column prop="title" label="心愿名称" min-width="200"></el-table-column>
+              <el-table-column prop="category" label="分类" width="100"></el-table-column>
+              <el-table-column prop="createdAt" label="创建时间" width="180">
+                <template slot-scope="scope">
+                  {{ formatDate(scope.row.createdAt) }}
+                </template>
+              </el-table-column>
+              <el-table-column label="操作" width="200" fixed="right">
+                <template slot-scope="scope">
+                  <el-button type="text" size="small" @click="showPriceDialog(scope.row)">定价</el-button>
+                  <el-button type="text" size="small" style="color: #F56C6C" @click="handleReject(scope.row)">拒绝</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+
+          <!-- 待审批兑换列表 -->
+          <div v-if="pendingExchangeWishes.length > 0" class="wish-section">
+            <div class="section-title">⏳ 待审批兑换</div>
+            <el-table :data="pendingExchangeWishes" border v-loading="loading" style="width: 100%">
+              <el-table-column prop="id" label="ID" width="80"></el-table-column>
+              <el-table-column prop="childName" label="孩子" width="100"></el-table-column>
+              <el-table-column prop="title" label="心愿名称" min-width="200"></el-table-column>
+              <el-table-column prop="pointsRequired" label="所需积分" width="120">
+                <template slot-scope="scope">
+                  <el-tag type="warning">{{ scope.row.pointsRequired }}</el-tag>
+                </template>
+              </el-table-column>
+              <el-table-column prop="exchangeRequestedAt" label="申请时间" width="180">
+                <template slot-scope="scope">
+                  {{ formatDate(scope.row.exchangeRequestedAt) }}
+                </template>
+              </el-table-column>
+              <el-table-column label="操作" width="200" fixed="right">
+                <template slot-scope="scope">
+                  <el-button type="text" size="small" @click="handleApproveExchange(scope.row, true)">批准</el-button>
+                  <el-button type="text" size="small" style="color: #F56C6C" @click="handleApproveExchange(scope.row, false)">拒绝</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+
+          <!-- 待兑现列表 -->
+          <div v-if="approvedWishes.length > 0" class="wish-section">
+            <div class="section-title">📦 待兑现</div>
+            <el-table :data="approvedWishes" border v-loading="loading" style="width: 100%">
+              <el-table-column prop="id" label="ID" width="80"></el-table-column>
+              <el-table-column prop="childName" label="孩子" width="100"></el-table-column>
+              <el-table-column prop="title" label="心愿名称" min-width="200"></el-table-column>
+              <el-table-column prop="pointsRequired" label="已扣积分" width="120">
+                <template slot-scope="scope">
+                  <el-tag type="success">{{ scope.row.pointsRequired }}</el-tag>
+                </template>
+              </el-table-column>
+              <el-table-column prop="exchangeApprovedAt" label="批准时间" width="180">
+                <template slot-scope="scope">
+                  {{ formatDate(scope.row.exchangeApprovedAt) }}
+                </template>
+              </el-table-column>
+              <el-table-column label="操作" width="120" fixed="right">
+                <template slot-scope="scope">
+                  <el-button type="text" size="small" @click="handleFulfill(scope.row)">已购买</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+
+          <!-- 待收货列表 -->
+          <div v-if="exchangingWishes.length > 0" class="wish-section">
+            <div class="section-title">🎁 待收货确认</div>
+            <el-table :data="exchangingWishes" border v-loading="loading" style="width: 100%">
+              <el-table-column prop="id" label="ID" width="80"></el-table-column>
+              <el-table-column prop="childName" label="孩子" width="100"></el-table-column>
+              <el-table-column prop="title" label="心愿名称" min-width="200"></el-table-column>
+              <el-table-column prop="fulfilledAt" label="购买时间" width="180">
+                <template slot-scope="scope">
+                  {{ formatDate(scope.row.fulfilledAt) }}
+                </template>
+              </el-table-column>
+              <el-table-column label="状态" width="100">
+                <template slot-scope="scope">
+                  <el-tag type="info">等待确认</el-tag>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+
+          <!-- 空状态 -->
+          <el-empty v-if="pendingWishes.length === 0" description="暂无待处理心愿"></el-empty>
+        </el-tab-pane>
+
+        <!-- 全部心愿 -->
+        <el-tab-pane label="全部心愿" name="all">
+          <!-- 搜索筛选 -->
+          <el-form :inline="true" :model="searchForm" style="margin-bottom: 20px">
+            <el-form-item label="家庭">
+              <el-input
+                v-model="searchForm.familyName"
+                placeholder="点击选择家庭"
+                readonly
+                @click.native="showFamilySelector"
+              >
+                <el-button
+                  slot="append"
+                  icon="el-icon-close"
+                  v-if="searchForm.familyId"
+                  @click.stop="clearFamily"
+                  style="padding: 8px"
+                ></el-button>
+                <i slot="suffix" class="el-input__icon el-icon-arrow-down" @click.stop="showFamilySelector"></i>
+              </el-input>
+            </el-form-item>
+            <el-form-item label="状态">
+              <el-select v-model="searchForm.status" placeholder="全部状态" clearable @change="handleSearch">
+                <el-option label="待定价" value="draft"></el-option>
+                <el-option label="积攒中" value="active"></el-option>
+                <el-option label="待审批" value="pending_exchange"></el-option>
+                <el-option label="已批准" value="approved"></el-option>
+                <el-option label="待收货" value="exchanging"></el-option>
+                <el-option label="已完成" value="fulfilled"></el-option>
+                <el-option label="已拒绝" value="rejected"></el-option>
+                <el-option label="已取消" value="cancelled"></el-option>
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" @click="handleSearch">搜索</el-button>
+              <el-button @click="handleReset">重置</el-button>
+            </el-form-item>
+          </el-form>
+
+          <!-- 家庭选择对话框 -->
+          <el-dialog title="选择家庭" :visible.sync="familyDialogVisible" width="600px">
+            <el-table :data="familyList" border v-loading="familyLoading" @row-click="selectFamily" style="cursor: pointer">
+              <el-table-column prop="id" label="ID" width="80"></el-table-column>
+              <el-table-column prop="name" label="家庭名称" min-width="150"></el-table-column>
+              <el-table-column prop="inviteCode" label="邀请码" width="120"></el-table-column>
+            </el-table>
+            <el-pagination
+              style="margin-top: 15px; text-align: right"
+              @current-change="loadFamilies"
+              :current-page="familyPagination.current"
+              :page-size="familyPagination.size"
+              :total="familyPagination.total"
+              layout="total, prev, pager, next"
+            ></el-pagination>
+          </el-dialog>
+
+          <el-table :data="tableData" border v-loading="loading" style="width: 100%">
+            <el-table-column prop="id" label="ID" width="80"></el-table-column>
+            <el-table-column prop="childName" label="孩子" width="100"></el-table-column>
+            <el-table-column prop="title" label="心愿名称" min-width="200"></el-table-column>
+            <el-table-column prop="pointsRequired" label="所需积分" width="120">
+              <template slot-scope="scope">
+                <el-tag v-if="scope.row.pointsRequired" type="warning">{{ scope.row.pointsRequired }}</el-tag>
+                <span v-else class="no-price">未定价</span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="category" label="分类" width="100"></el-table-column>
+            <el-table-column prop="status" label="状态" width="100">
+              <template slot-scope="scope">
+                <el-tag :type="getStatusType(scope.row.status)" size="small">
+                  {{ scope.row.statusText }}
+                </el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column prop="creatorType" label="创建者" width="80">
+              <template slot-scope="scope">
+                <el-tag :type="scope.row.creatorType === 'parent' ? 'primary' : 'success'" size="small">
+                  {{ scope.row.creatorType === 'parent' ? '家长' : '孩子' }}
+                </el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column prop="createdAt" label="创建时间" width="180">
+              <template slot-scope="scope">
+                {{ formatDate(scope.row.createdAt) }}
+              </template>
+            </el-table-column>
+          </el-table>
+
+          <el-pagination
+            style="margin-top: 20px; text-align: right"
+            @size-change="handleSizeChange"
+            @current-change="handleCurrentChange"
+            :current-page="pagination.current"
+            :page-sizes="[10, 20, 50, 100]"
+            :page-size="pagination.size"
+            :total="pagination.total"
+            layout="total, sizes, prev, pager, next, jumper"
+          ></el-pagination>
+        </el-tab-pane>
+      </el-tabs>
+    </el-card>
+
+    <!-- 定价对话框 -->
+    <el-dialog title="设置积分" :visible.sync="priceDialogVisible" width="400px">
+      <el-form label-width="100px">
+        <el-form-item label="心愿名称">
+          <span>{{ currentWish?.title }}</span>
+        </el-form-item>
+        <el-form-item label="所需积分">
+          <el-input-number v-model="priceValue" :min="1" :max="10000"></el-input-number>
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="priceDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="submitPrice" :loading="submitting">确定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { 
+  getWishesList, 
+  getPendingWishes, 
+  setWishPrice, 
+  rejectWish, 
+  approveWishExchange,
+  markWishFulfilled,
+  getFamilyList 
+} from '@/api/admin'
+
+export default {
+  name: 'Wishes',
+  data() {
+    return {
+      activeTab: 'pending',
+      loading: false,
+      submitting: false,
+      tableData: [],
+      pendingWishes: [],
+      searchForm: {
+        status: '',
+        familyId: '',
+        familyName: ''
+      },
+      pagination: {
+        current: 1,
+        size: 10,
+        total: 0
+      },
+      // 家庭选择对话框
+      familyDialogVisible: false,
+      familyLoading: false,
+      familyList: [],
+      familyPagination: {
+        current: 1,
+        size: 10,
+        total: 0
+      },
+      // 定价对话框
+      priceDialogVisible: false,
+      currentWish: null,
+      priceValue: 100
+    }
+  },
+  computed: {
+    draftWishes() {
+      return this.pendingWishes.filter(w => w.status === 'draft')
+    },
+    pendingExchangeWishes() {
+      return this.pendingWishes.filter(w => w.status === 'pending_exchange')
+    },
+    approvedWishes() {
+      return this.pendingWishes.filter(w => w.status === 'approved')
+    },
+    exchangingWishes() {
+      return this.pendingWishes.filter(w => w.status === 'exchanging')
+    },
+    draftCount() { return this.draftWishes.length },
+    pendingCount() { return this.pendingExchangeWishes.length },
+    approvedCount() { return this.approvedWishes.length },
+    exchangingCount() { return this.exchangingWishes.length }
+  },
+  mounted() {
+    this.loadPendingWishes()
+  },
+  methods: {
+    async loadPendingWishes() {
+      this.loading = true
+      try {
+        // 获取当前用户家庭ID
+        const familyId = localStorage.getItem('familyId')
+        if (familyId) {
+          const res = await getPendingWishes(familyId)
+          this.pendingWishes = res.data || []
+        }
+      } catch (error) {
+        this.$message.error(error.message || '加载待处理列表失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    async loadWishes() {
+      this.loading = true
+      try {
+        const params = {
+          page: this.pagination.current,
+          size: this.pagination.size,
+          familyId: this.searchForm.familyId || undefined,
+          status: this.searchForm.status || undefined
+        }
+        const res = await getWishesList(params)
+        this.tableData = res.data.records || res.data || []
+        this.pagination.total = res.data.total || this.tableData.length
+      } catch (error) {
+        this.$message.error(error.message || '加载心愿列表失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    async loadFamilies(page = 1) {
+      this.familyLoading = true
+      try {
+        const res = await getFamilyList({ page, size: this.familyPagination.size })
+        this.familyList = res.data.records || res.data || []
+        this.familyPagination.total = res.data.total || this.familyList.length
+        this.familyPagination.current = page
+      } catch (error) {
+        this.$message.error(error.message || '加载家庭列表失败')
+      } finally {
+        this.familyLoading = false
+      }
+    },
+    selectFamily(row) {
+      this.searchForm.familyId = row.id
+      this.searchForm.familyName = row.name
+      this.familyDialogVisible = false
+      this.handleSearch()
+    },
+    showFamilySelector() {
+      this.familyDialogVisible = true
+      this.loadFamilies()
+    },
+    clearFamily() {
+      this.searchForm.familyId = ''
+      this.searchForm.familyName = ''
+      this.handleSearch()
+    },
+    handleTabClick(tab) {
+      if (tab.name === 'all' && this.tableData.length === 0) {
+        this.loadWishes()
+      }
+    },
+    handleSearch() {
+      this.pagination.current = 1
+      this.loadWishes()
+    },
+    handleReset() {
+      this.searchForm = { status: '', familyId: '', familyName: '' }
+      this.handleSearch()
+    },
+    handleRefresh() {
+      if (this.activeTab === 'pending') {
+        this.loadPendingWishes()
+      } else {
+        this.loadWishes()
+      }
+    },
+    handleSizeChange(val) {
+      this.pagination.size = val
+      this.loadWishes()
+    },
+    handleCurrentChange(val) {
+      this.pagination.current = val
+      this.loadWishes()
+    },
+    showPriceDialog(wish) {
+      this.currentWish = wish
+      this.priceValue = wish.pointsRequired || 100
+      this.priceDialogVisible = true
+    },
+    async submitPrice() {
+      if (!this.priceValue || this.priceValue <= 0) {
+        this.$message.warning('请输入有效积分')
+        return
+      }
+      this.submitting = true
+      try {
+        await setWishPrice(this.currentWish.id, this.priceValue)
+        this.$message.success('定价成功')
+        this.priceDialogVisible = false
+        this.loadPendingWishes()
+      } catch (error) {
+        this.$message.error(error.message || '定价失败')
+      } finally {
+        this.submitting = false
+      }
+    },
+    async handleReject(row) {
+      this.$confirm(`确定要拒绝 "${row.title}" 吗?`, '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        try {
+          await rejectWish(row.id, '不符合要求')
+          this.$message.success('已拒绝')
+          this.loadPendingWishes()
+        } catch (error) {
+          this.$message.error(error.message || '操作失败')
+        }
+      }).catch(() => {})
+    },
+    async handleApproveExchange(row, approved) {
+      const action = approved ? '批准' : '拒绝'
+      this.$confirm(`确定要${action} "${row.title}" 的兑换申请吗?`, '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        try {
+          await approveWishExchange(row.id, approved)
+          this.$message.success(`已${action}`)
+          this.loadPendingWishes()
+        } catch (error) {
+          this.$message.error(error.message || '操作失败')
+        }
+      }).catch(() => {})
+    },
+    async handleFulfill(row) {
+      this.$confirm(`确定已购买 "${row.title}" 吗?`, '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        try {
+          await markWishFulfilled(row.id)
+          this.$message.success('已标记')
+          this.loadPendingWishes()
+        } catch (error) {
+          this.$message.error(error.message || '操作失败')
+        }
+      }).catch(() => {})
+    },
+    getStatusType(status) {
+      const types = {
+        draft: 'info',
+        active: 'success',
+        pending_exchange: 'warning',
+        approved: 'primary',
+        exchanging: 'primary',
+        fulfilled: 'success',
+        cancelled: 'info',
+        rejected: 'danger'
+      }
+      return types[status] || 'info'
+    },
+    formatDate(dateStr) {
+      if (!dateStr) return '-'
+      const date = new Date(dateStr)
+      return date.toLocaleString('zh-CN')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page-container {
+  padding: 20px;
+}
+
+.stat-card {
+  text-align: center;
+  cursor: pointer;
+}
+
+.stat-card.draft { border-top: 3px solid #909399; }
+.stat-card.pending { border-top: 3px solid #E6A23C; }
+.stat-card.approved { border-top: 3px solid #409EFF; }
+.stat-card.exchanging { border-top: 3px solid #409EFF; }
+
+.stat-num {
+  font-size: 32px;
+  font-weight: bold;
+  color: #333;
+}
+
+.stat-label {
+  font-size: 14px;
+  color: #666;
+  margin-top: 5px;
+}
+
+.wish-section {
+  margin-bottom: 30px;
+}
+
+.section-title {
+  font-size: 16px;
+  font-weight: bold;
+  margin-bottom: 15px;
+  color: #333;
+}
+
+.no-price {
+  color: #999;
+  font-size: 12px;
+}
+</style>