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

docs: 梳理任务管理需求文档

- 创建task-requirements.md详细需求文档
- 整理TASK-001~016需求清单和状态
- 定义Task实体字段和数据模型
- 汇总API接口和前端页面
- 标注待完善功能
User 6 месяцев назад
Родитель
Сommit
f9c5dfa61b

+ 275 - 0
docs/task-requirements.md

@@ -0,0 +1,275 @@
+# 任务管理模块需求分析
+
+**版本**: V1.2
+**更新时间**: 2026-04-13
+
+---
+
+## 一、需求清单总览
+
+| 需求ID | 描述 | 优先级 | 状态 |
+|--------|------|--------|------|
+| TASK-001 | 家长可创建任务,设置任务名称 | P0 | ✅ 已实现 |
+| TASK-002 | 设置任务积分值(1-10分,默认2分) | P0 | ✅ 已实现 |
+| TASK-003 | 设置任务截止时间(精确到分钟) | P0 | ✅ 已实现 |
+| TASK-004 | 设置任务重复(每日/每周) | P0 | ✅ 已实现 |
+| TASK-005 | 设置任务分类(学习类/生活类/运动类/其他) | P0 | ✅ 已实现 |
+| TASK-006 | 任务模板功能 | P1 | ⚠️ 部分实现 |
+| TASK-007 | 设置审核方式 | P1 | ⚠️ 部分实现 |
+| TASK-008 | 孩子可查看今日任务列表 | P0 | ✅ 已实现 |
+| TASK-009 | 孩子点击"完成"按钮提交任务 | P0 | ✅ 已实现 |
+| TASK-010 | 任务完成方式设置 | P2 | ⚠️ 待完善 |
+| TASK-011 | 任务时长设置 | P2 | ⚠️ 待完善 |
+| TASK-012A | 打卡支持照片上传 | P0 | ✅ 已实现 |
+| TASK-012B | 打卡支持视频上传 | P0 | ✅ 已实现 |
+| TASK-012C | 打卡支持录音上传 | P0 | ✅ 已实现 |
+| TASK-012D | 打卡支持文字描述 | P0 | ✅ 已实现 |
+| TASK-012E | 打卡支持倒计时功能 | P0 | ✅ 已实现 |
+| TASK-012F | 倒计时结束时语音提醒 | P0 | ✅ 已实现 |
+| TASK-013 | 任务提醒通知 | P1 | ✅ 已实现 |
+| TASK-014 | 任务历史记录查询 | P1 | ✅ 已实现 |
+| TASK-015 | 任务统计分析报表 | P1 | ✅ 已实现 |
+| TASK-016 | 批量操作任务 | P2 | ❌ 未实现 |
+
+---
+
+## 二、需求详细说明
+
+### 2.1 任务创建(TASK-001~005)
+
+#### TASK-001: 创建任务,设置任务名称
+- **描述**: 家长可以创建任务,设置任务名称
+- **必填**: 是
+- **字段**: `title` (String, 最大100字符)
+- **验证**: 不能为空
+- **实现位置**: `TaskService.createTask()`
+
+#### TASK-002: 设置任务积分值
+- **描述**: 积分值范围1-10分,默认2分
+- **必填**: 是
+- **字段**: `points` (Integer)
+- **默认值**: 2
+- **验证**: 1-10分
+
+#### TASK-003: 设置任务截止时间
+- **描述**: 精确到分钟,支持多孩子任务和家长任务
+- **必填**: 是
+- **字段**: `deadline` (DateTime)
+- **前端**: 多级日期时间选择器(年/月/日/时/分)
+
+#### TASK-004: 设置任务重复
+- **描述**: 支持每日重复、每周重复
+- **字段**: `repeatType` (String)
+- **可选值**: `none` / `daily` / `weekly`
+- **定时任务**: `RepeatTaskGenerator` 每天05:00执行
+
+#### TASK-005: 设置任务分类
+- **描述**: 学习类/生活类/运动类/其他
+- **字段**: `category` (String)
+- **可选值**: `学习类` / `生活类` / `运动类` / `其他`
+
+---
+
+### 2.2 任务模板和审核(TASK-006~007)
+
+#### TASK-006: 任务模板功能
+- **描述**: 使用任务模板快速创建
+- **状态**: ⚠️ 部分实现
+- **待完善**: 模板库管理、模板选择UI
+
+#### TASK-007: 设置审核方式
+- **描述**: 选择审核人类型
+- **字段**: `reviewType` (String)
+- **可选值**: 
+  - `creator` - 创建者审核
+  - `parent` - 家长审核
+  - `teacher` - 指导师审核
+  - `ai` - AI自动审核
+
+---
+
+### 2.3 任务查看和完成(TASK-008~011)
+
+#### TASK-008: 孩子查看今日任务
+- **描述**: 孩子可以查看今日需要完成的任务列表
+- **实现**: `GET /api/tasks/today?childId={id}`
+- **返回**: 当日所有pending/completed状态的任务
+
+#### TASK-009: 孩子完成任务
+- **描述**: 孩子点击完成按钮提交任务
+- **实现**: `POST /api/tasks/{id}/complete`
+- **参数**: childId, photoUrl(可选)
+
+#### TASK-010: 任务完成方式设置
+- **描述**: 支持多种完成方式
+- **字段**: `completeTypes` (JSON数组)
+- **可选值**: 
+  - `checkin` - 打卡
+  - `text` - 文本
+  - `audio` - 语音
+  - `image` - 图片
+  - `video` - 视频
+
+#### TASK-011: 任务时长设置
+- **字段**: `duration` (Integer, 分钟)
+- **描述**: 任务预计耗时
+
+---
+
+### 2.4 打卡功能(TASK-012A~F)
+
+| 需求ID | 描述 | 实现 |
+|--------|------|------|
+| TASK-012A | 照片上传 | ✅ 文件上传API |
+| TASK-012B | 视频上传 | ✅ 文件上传API |
+| TASK-012C | 录音上传 | ✅ 文件上传API |
+| TASK-012D | 文字描述 | ✅ CompleteTaskDTO.text |
+| TASK-012E | 倒计时功能 | ✅ 前端组件 |
+| TASK-012F | 语音提醒 | ✅ 前端TTS |
+
+---
+
+### 2.5 任务提醒和统计(TASK-013~016)
+
+#### TASK-013: 任务提醒通知
+- **描述**: 任务截止前发送提醒
+- **API**:
+  - `GET /api/tasks/reminder/{childId}` - 30分钟内到期
+  - `GET /api/tasks/upcoming/{childId}?minutes=60` - 自定义时间
+  - `GET /api/tasks/overdue/{childId}` - 已超时任务
+
+#### TASK-014: 任务历史记录查询
+- **描述**: 查看历史任务,支持筛选
+- **API**: `GET /api/tasks/history/filter/{childId}`
+- **筛选条件**: 状态、时间范围
+
+#### TASK-015: 任务统计分析报表
+- **描述**: 统计任务完成情况
+- **API**:
+  - `GET /api/tasks/statistics/{childId}` - 概览统计
+  - `GET /api/tasks/statistics/category/{childId}` - 分类统计
+  - `GET /api/tasks/statistics/date/{childId}?days=7` - 日期统计
+  - `GET /api/tasks/statistics/family/{familyId}` - 家庭统计
+
+#### TASK-016: 批量操作任务
+- **描述**: 批量删除、批量审核
+- **状态**: ❌ 未实现
+
+---
+
+## 三、任务数据模型
+
+### 3.1 Task 实体字段
+
+```java
+// 基础字段
+private Long id;
+private Long familyId;       // 家庭ID
+private Long creatorId;      // 创建者ID
+private String title;        // 任务名称
+private String description;  // 任务描述
+
+// 执行者
+private String executorType; // child/parent
+private Long executorId;     // 执行者ID
+private Long childId;        // 孩子ID(兼容)
+
+// 积分和时间
+private Integer points;      // 积分值
+private Date deadline;      // 截止时间
+
+// 分类和审核
+private String category;     // 分类
+private Integer needReview; // 是否需要审核
+private String reviewType;   // 审核人类型
+private String status;       // 任务状态
+private Date completedAt;    // 完成时间
+
+// 重复设置 (TASK-004)
+private String repeatType;  // none/daily/weekly
+private Long templateId;    // 模板ID
+private Integer isTemplate; // 是否模板
+
+// POINT-002: 超额积分
+private String bonusRule;   // JSON格式规则
+
+// POINT-009: 积分有效期
+private Integer pointsExpireDays; // 有效期天数
+
+// 扩展字段
+private String taskType;    // onetime/recurring
+private String frequency;   // daily/weekly/monthly
+private Integer maxFrequency; // 周期内最多次数
+private String completeTypes; // 完成方式JSON
+private Date earliestStart;  // 最早开始
+private Date latestEnd;      // 最晚结束
+```
+
+### 3.2 状态流转
+
+```
+pending → completed (孩子完成)
+pending → completed (审核通过)
+completed → pending (审核拒绝)
+pending → cancelled (创建者删除)
+```
+
+---
+
+## 四、API接口汇总
+
+### 4.1 任务管理
+
+| 方法 | 路径 | 功能 | 权限 |
+|------|------|------|------|
+| POST | /api/tasks | 创建任务 | 家长/指导师 |
+| GET | /api/tasks/today | 今日任务 | 孩子 |
+| POST | /api/tasks/{id}/complete | 完成任务 | 孩子 |
+| POST | /api/tasks/{id}/review | 审核任务 | 家长/指导师 |
+| GET | /api/tasks/history | 历史记录 | 孩子/家长 |
+| GET | /api/tasks/pending-review | 待审核任务 | 家长/指导师 |
+| DELETE | /api/tasks/{id} | 删除任务 | 创建者 |
+
+### 4.2 任务提醒统计
+
+| 方法 | 路径 | 功能 |
+|------|------|------|
+| GET | /api/tasks/reminder/{childId} | 需要提醒的任务 |
+| GET | /api/tasks/upcoming/{childId} | 即将到期任务 |
+| GET | /api/tasks/overdue/{childId} | 已超时任务 |
+| GET | /api/tasks/history/filter/{childId} | 带筛选历史 |
+| GET | /api/tasks/statistics/{childId} | 统计概览 |
+| GET | /api/tasks/statistics/category/{childId} | 分类统计 |
+| GET | /api/tasks/statistics/date/{childId} | 日期统计 |
+| GET | /api/tasks/statistics/family/{familyId} | 家庭统计 |
+
+---
+
+## 五、前端页面
+
+| 页面 | 路径 | 功能 |
+|------|------|------|
+| 创建任务 | `/pages/tasks/create-task` | 任务表单 |
+| 任务列表 | `/pages/tasks/tasks` | 任务查看/管理 |
+| 今日任务 | 首页嵌入 | 孩子查看任务 |
+
+---
+
+## 六、待完善功能
+
+| 功能 | 优先级 | 说明 |
+|------|--------|------|
+| 任务模板管理 | P1 | 模板库CRUD |
+| 批量操作 | P2 | 批量删除/审核 |
+| 任务时长显示 | P2 | 前端展示 |
+| AI审核 | P1 | 调用AI服务自动审核 |
+| 消息推送 | P1 | 微信模板消息提醒 |
+
+---
+
+## 七、数据库迁移
+
+| 迁移 | 字段 | 说明 |
+|------|------|------|
+| 迁移20 | template_id | 重复任务模板追踪 |
+| 迁移21 | is_template | 模板任务标记 |

+ 127 - 12
zxyj-backend/src/main/java/com/zxyj/config/DatabaseInitializer.java

@@ -102,22 +102,29 @@ public class DatabaseInitializer implements CommandLineRunner {
             "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
             "family_id BIGINT NOT NULL, " +
             "creator_id BIGINT NOT NULL, " +
-            "child_id BIGINT NOT NULL, " +
+            "child_id BIGINT COMMENT '执行者ID(child任务时必填,parent任务时可空)', " +
+            "executor_type VARCHAR(20) COMMENT '执行者类型: child/parent', " +
+            "executor_id BIGINT COMMENT '执行者ID', " +
             "title VARCHAR(200) NOT NULL, " +
             "description VARCHAR(500), " +
             "points TINYINT DEFAULT 2, " +
-            "deadline DATETIME NOT NULL, " +
+            "deadline DATETIME, " +
             "repeat_type ENUM('none','daily','weekly') DEFAULT 'none', " +
             "category VARCHAR(32), " +
             "need_review TINYINT DEFAULT 0, " +
             "review_by_category TINYINT DEFAULT 0, " +
             "status ENUM('pending','completed','overdue','cancelled') DEFAULT 'pending', " +
             "completed_at DATETIME, " +
+            "bonus_rule VARCHAR(500), " +
+            "points_expire_days INT DEFAULT 0, " +
+            "template_id BIGINT, " +
+            "is_template TINYINT DEFAULT 0, " +
             "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
             "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
             "INDEX idx_child_id_deadline (child_id, deadline), " +
             "INDEX idx_status (status), " +
-            "INDEX idx_category (category)" +
+            "INDEX idx_category (category), " +
+            "INDEX idx_executor (executor_type, executor_id)" +
             ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
             
             // 积分流水表
@@ -635,12 +642,55 @@ public class DatabaseInitializer implements CommandLineRunner {
 
 // 迁移13: admin_task_templates表添加teacher_id字段
 try {
-jdbcTemplate.execute("ALTER TABLE admin_task_templates ADD COLUMN teacher_id BIGINT COMMENT \'指导师ID\'");
+jdbcTemplate.execute("ALTER TABLE admin_task_templates ADD COLUMN teacher_id BIGINT COMMENT '指导师ID'");
 log.info("已添加teacher_id列到admin_task_templates表");
 } catch (Exception e) {
 // 列已存在,忽略错误
 }
 
+// 迁移13b: admin_task_templates表添加新字段
+try {
+jdbcTemplate.execute("ALTER TABLE admin_task_templates ADD COLUMN complete_types VARCHAR(100) COMMENT '完成方式JSON'");
+log.info("已添加complete_types列到admin_task_templates表");
+} catch (Exception e) {
+// 列已存在,忽略错误
+}
+
+try {
+jdbcTemplate.execute("ALTER TABLE admin_task_templates ADD COLUMN duration INT COMMENT '预计完成时长(分钟)'");
+log.info("已添加duration列到admin_task_templates表");
+} catch (Exception e) {
+// 列已存在,忽略错误
+}
+
+try {
+jdbcTemplate.execute("ALTER TABLE admin_task_templates ADD COLUMN review_type VARCHAR(20) COMMENT '审核人类型: creator/parent/teacher/ai'");
+log.info("已添加review_type列到admin_task_templates表");
+} catch (Exception e) {
+// 列已存在,忽略错误
+}
+
+try {
+jdbcTemplate.execute("ALTER TABLE admin_task_templates ADD COLUMN task_type VARCHAR(20) COMMENT '任务性质: onetime/recurring'");
+log.info("已添加task_type列到admin_task_templates表");
+} catch (Exception e) {
+// 列已存在,忽略错误
+}
+
+try {
+jdbcTemplate.execute("ALTER TABLE admin_task_templates ADD COLUMN frequency VARCHAR(20) COMMENT '频率: daily/weekly/monthly'");
+log.info("已添加frequency列到admin_task_templates表");
+} catch (Exception e) {
+// 列已存在,忽略错误
+}
+
+try {
+jdbcTemplate.execute("ALTER TABLE admin_task_templates ADD COLUMN max_frequency INT COMMENT '周期内最多完成次数'");
+log.info("已添加max_frequency列到admin_task_templates表");
+} catch (Exception e) {
+// 列已存在,忽略错误
+}
+
 // 迁移14: rewards表child_id字段允许NULL(模板奖励不需要child_id)
 try {
 jdbcTemplate.execute("ALTER TABLE rewards MODIFY COLUMN child_id BIGINT NULL COMMENT \'孩子ID,模板奖励为NULL\'");
@@ -713,15 +763,80 @@ log.info("已添加template_id列到tasks表");
 // 列已存在,忽略错误
 }
 
-// 迁移21: tasks表添加is_template字段(TASK-004: 标记模板任务)
-try {
-jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN is_template TINYINT DEFAULT 0 COMMENT \'是否为模板任务\'");
-log.info("已添加is_template列到tasks表");
-} catch (Exception e) {
-// 列已存在,忽略错误
-}
+        // 迁移21: tasks表添加is_template字段(TASK-004: 标记模板任务)
+        try {
+            jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN is_template TINYINT DEFAULT 0 COMMENT '是否为模板任务'");
+            log.info("已添加is_template列到tasks表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        // 迁移22: 修复tasks表child_id和deadline允许NULL(支持多孩子和家长任务)
+        try {
+            jdbcTemplate.execute("ALTER TABLE tasks MODIFY COLUMN child_id BIGINT NULL COMMENT '执行者ID'");
+            log.info("已修改tasks.child_id允许NULL");
+        } catch (Exception e) {
+            // 修改失败,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE tasks MODIFY COLUMN deadline DATETIME NULL COMMENT '截止时间'");
+            log.info("已修改tasks.deadline允许NULL");
+        } catch (Exception e) {
+            // 修改失败,忽略错误
+        }
+
+        // 迁移23: tasks表添加新字段
+        try {
+            jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN task_type VARCHAR(20) COMMENT '任务性质: onetime/recurring'");
+            log.info("已添加task_type列到tasks表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN frequency VARCHAR(20) COMMENT '频率: daily/weekly/monthly'");
+            log.info("已添加frequency列到tasks表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN max_frequency INT COMMENT '周期内最多完成次数'");
+            log.info("已添加max_frequency列到tasks表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN complete_types VARCHAR(100) COMMENT '完成方式JSON'");
+            log.info("已添加complete_types列到tasks表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN review_type VARCHAR(20) COMMENT '审核人类型: creator/parent/teacher/ai'");
+            log.info("已添加review_type列到tasks表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN earliest_start DATETIME COMMENT '最早开始时间'");
+            log.info("已添加earliest_start列到tasks表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN latest_end DATETIME COMMENT '最晚结束时间'");
+            log.info("已添加latest_end列到tasks表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
 
-log.info("数据库迁移完成");
+        log.info("数据库迁移完成");
     }
 
     private void initializeDefaultData() {

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

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

+ 22 - 0
zxyj-backend/src/main/java/com/zxyj/entity/Task.java

@@ -34,6 +34,7 @@ private String description;
 
 private Integer points;
 
+// 截止时间(允许NULL,支持多孩子任务和家长任务)
 private Date deadline;
 
 // TASK-004: 重复类型 (none/daily/weekly)
@@ -45,6 +46,9 @@ private Integer needReview;
 
 private Integer reviewByCategory;
 
+// 审核人类型: creator/parent/teacher/ai
+private String reviewType;
+
 private String status;
 
 private Date completedAt;
@@ -62,6 +66,24 @@ private Long templateId;
 // TASK-004: 是否为模板任务(模板任务不会被完成,只用于生成实例)
 private Integer isTemplate;
 
+// 任务性质: onetime(一次性) / recurring(循环)
+private String taskType;
+
+// 频率: daily/weekly/monthly
+private String frequency;
+
+// 周期内最多完成次数
+private Integer maxFrequency;
+
+// 完成方式(多选,JSON格式: ["checkin","text","image"])
+private String completeTypes;
+
+// 最早开始时间
+private Date earliestStart;
+
+// 最晚结束时间
+private Date latestEnd;
+
 private Date createdAt;
 
 private Date updatedAt;

+ 17 - 0
zxyj-backend/src/main/java/com/zxyj/service/TaskService.java

@@ -120,6 +120,23 @@ public class TaskService {
         task.setRepeatType(dto.getRepeatType() != null ? dto.getRepeatType() : "none");
         task.setCategory(dto.getCategory());
 
+        // 任务性质
+        task.setTaskType(dto.getTaskType());
+        task.setFrequency(dto.getFrequency());
+        task.setMaxFrequency(dto.getMaxFrequency());
+
+        // 完成方式(转为JSON存储)
+        if (dto.getCompleteTypes() != null && !dto.getCompleteTypes().isEmpty()) {
+            task.setCompleteTypes(String.join(",", dto.getCompleteTypes()));
+        }
+
+        // 审核设置
+        task.setReviewType(dto.getReviewType());
+        
+        // 时间设置
+        task.setEarliestStart(dto.getEarliestStart());
+        task.setLatestEnd(dto.getLatestEnd());
+
         // 如果是重复任务,标记为模板
         if (!"none".equals(task.getRepeatType())) {
             task.setIsTemplate(1); // 模板任务

+ 15 - 4
zxyj-frontend/pages/rewards/rewards.vue

@@ -35,13 +35,24 @@
     <!-- 添加心愿弹窗 -->
     <view class="modal-mask" v-if="showAddModal" @click="showAddModal = false">
       <view class="modal" @click.stop>
-        <view class="modal-title">添加心愿</view>
+        <view class="modal-title">{{ currentRole === 'child' ? '添加心愿' : '添加心愿' }}</view>
+        
         <view class="form-item">
-          <input v-model="newReward.title" placeholder="心愿名称" />
+          <view class="form-label">心愿名称 <text class="required">*</text></view>
+          <input v-model="newReward.title" placeholder="请输入心愿名称" />
         </view>
-        <view class="form-item">
-          <input v-model="newReward.pointsRequired" type="number" placeholder="所需积分" />
+        
+        <!-- 家长模式可以填写积分,孩子模式不需要填写积分 -->
+        <view class="form-item" v-if="currentRole === 'parent'">
+          <view class="form-label">所需积分 <text class="required">*</text></view>
+          <input v-model="newReward.pointsRequired" type="number" placeholder="请输入所需积分" />
+        </view>
+        
+        <!-- 孩子模式提示 -->
+        <view class="form-tip" v-if="currentRole === 'child'">
+          <text>孩子创建心愿后,需要家长填写所需积分才能生效</text>
         </view>
+        
         <view class="modal-btns">
           <button @click="showAddModal = false">取消</button>
           <button class="btn-primary" @click="createReward">确定</button>

+ 106 - 25
zxyj-web/src/views/TaskTemplates.vue

@@ -81,13 +81,25 @@
             </el-tag>
           </template>
         </el-table-column>
-        <el-table-column prop="recommendedAge" label="推荐年龄" width="100">
-          <template slot-scope="scope">
-            <span v-if="scope.row.recommendedAge">{{ scope.row.recommendedAge }}岁+</span>
-            <span v-else>-</span>
-          </template>
-        </el-table-column>
-        <el-table-column prop="needReview" label="需审核" width="80">
+<el-table-column prop="recommendedAge" label="推荐年龄" width="100">
+      <template slot-scope="scope">
+        <span v-if="scope.row.recommendedAge">{{ scope.row.recommendedAge }}岁+</span>
+        <span v-else>-</span>
+      </template>
+    </el-table-column>
+    <el-table-column prop="duration" label="时长" width="80">
+      <template slot-scope="scope">
+        <span v-if="scope.row.duration">{{ scope.row.duration }}分钟</span>
+        <span v-else>-</span>
+      </template>
+    </el-table-column>
+    <el-table-column prop="taskType" label="性质" width="80">
+      <template slot-scope="scope">
+        <el-tag v-if="scope.row.taskType === 'recurring'" type="warning" size="small">循环</el-tag>
+        <el-tag v-else type="info" size="small">一次性</el-tag>
+      </template>
+    </el-table-column>
+    <el-table-column prop="needReview" label="需审核" width="80">
           <template slot-scope="scope">
             <el-tag v-if="scope.row.needReview === 1" type="warning" size="small">是</el-tag>
             <el-tag v-else type="info" size="small">否</el-tag>
@@ -181,22 +193,59 @@
             <el-option label="困难" value="hard"></el-option>
           </el-select>
         </el-form-item>
-        <el-form-item label="推荐年龄" prop="recommendedAge">
-          <el-input-number v-model="editForm.recommendedAge" :min="0" :max="18" placeholder="推荐年龄"></el-input-number>
-          <span style="margin-left: 10px; color: #909399">岁及以上</span>
-        </el-form-item>
-        <el-form-item label="需要审核" prop="needReview">
-          <el-switch v-model="editForm.needReview" :active-value="1" :inactive-value="0"></el-switch>
-          <span style="margin-left: 10px; color: #909399; font-size: 12px">
-            开启后,孩子完成任务需要家长审核
-          </span>
-        </el-form-item>
-        <el-form-item label="按分类审核" prop="reviewByCategory" v-if="editForm.needReview === 1">
-          <el-switch v-model="editForm.reviewByCategory" :active-value="1" :inactive-value="0"></el-switch>
-          <span style="margin-left: 10px; color: #909399; font-size: 12px">
-            开启后,将按照分类自动分配审核人
-          </span>
-        </el-form-item>
+    <el-form-item label="推荐年龄" prop="recommendedAge">
+      <el-input-number v-model="editForm.recommendedAge" :min="0" :max="18" placeholder="推荐年龄"></el-input-number>
+      <span style="margin-left: 10px; color: #909399">岁及以上</span>
+    </el-form-item>
+    <el-form-item label="预计时长" prop="duration">
+      <el-input-number v-model="editForm.duration" :min="1" :max="480" placeholder="预计时长"></el-input-number>
+      <span style="margin-left: 10px; color: #909399">分钟</span>
+    </el-form-item>
+    <el-form-item label="完成方式" prop="completeTypes">
+      <el-checkbox-group v-model="editForm.completeTypes">
+        <el-checkbox label="checkin">打卡</el-checkbox>
+        <el-checkbox label="text">文本</el-checkbox>
+        <el-checkbox label="voice">语音</el-checkbox>
+        <el-checkbox label="image">图片</el-checkbox>
+        <el-checkbox label="video">视频</el-checkbox>
+      </el-checkbox-group>
+    </el-form-item>
+    <el-form-item label="任务性质" prop="taskType">
+      <el-radio-group v-model="editForm.taskType">
+        <el-radio label="onetime">一次性任务</el-radio>
+        <el-radio label="recurring">循环任务</el-radio>
+      </el-radio-group>
+    </el-form-item>
+    <el-form-item label="频率" prop="frequency" v-if="editForm.taskType === 'recurring'">
+      <el-select v-model="editForm.frequency" placeholder="请选择频率">
+        <el-option label="每天" value="daily"></el-option>
+        <el-option label="每周" value="weekly"></el-option>
+        <el-option label="每月" value="monthly"></el-option>
+      </el-select>
+    </el-form-item>
+    <el-form-item label="周期次数" prop="maxFrequency" v-if="editForm.taskType === 'recurring'">
+      <el-input-number v-model="editForm.maxFrequency" :min="1" :max="100" placeholder="周期内最多完成次数"></el-input-number>
+      <span style="margin-left: 10px; color: #909399; font-size: 12px">
+        超过此次数不计积分
+      </span>
+    </el-form-item>
+    <el-form-item label="需要审核" prop="needReview">
+      <el-switch v-model="editForm.needReview" :active-value="1" :inactive-value="0"></el-switch>
+    </el-form-item>
+    <el-form-item label="审核方式" prop="reviewType" v-if="editForm.needReview === 1">
+      <el-select v-model="editForm.reviewType" placeholder="请选择审核方式">
+        <el-option label="创建人审核" value="creator"></el-option>
+        <el-option label="家长审核" value="parent"></el-option>
+        <el-option label="指导师审核" value="teacher"></el-option>
+        <el-option label="AI自动审核" value="ai"></el-option>
+      </el-select>
+    </el-form-item>
+    <el-form-item label="按分类审核" prop="reviewByCategory" v-if="editForm.needReview === 1 && editForm.reviewType === 'parent'">
+      <el-switch v-model="editForm.reviewByCategory" :active-value="1" :inactive-value="0"></el-switch>
+      <span style="margin-left: 10px; color: #909399; font-size: 12px">
+        开启后,将按照分类自动分配审核人
+      </span>
+    </el-form-item>
       </el-form>
       <div slot="footer">
         <el-button @click="editDialogVisible = false">取消</el-button>
@@ -236,7 +285,13 @@ export default {
         points: 10,
         difficulty: 'medium',
         recommendedAge: null,
+        duration: null,
+        completeTypes: ['checkin'],
+        taskType: 'onetime',
+        frequency: null,
+        maxFrequency: null,
         needReview: 1,
+        reviewType: 'creator',
         reviewByCategory: 0,
         isActive: 1
       },
@@ -315,7 +370,13 @@ export default {
         points: 10,
         difficulty: 'medium',
         recommendedAge: null,
+        duration: null,
+        completeTypes: ['checkin'],
+        taskType: 'onetime',
+        frequency: null,
+        maxFrequency: null,
         needReview: 1,
+        reviewType: 'creator',
         reviewByCategory: 0,
         isActive: 1
       }
@@ -323,6 +384,15 @@ export default {
     },
     handleEdit(row) {
       this.isCreate = false
+      // 解析 completeTypes(如果是字符串则转为数组)
+      let completeTypes = row.completeTypes
+      if (typeof completeTypes === 'string') {
+        try {
+          completeTypes = JSON.parse(completeTypes)
+        } catch (e) {
+          completeTypes = ['checkin']
+        }
+      }
       this.editForm = {
         id: row.id,
         title: row.title,
@@ -331,7 +401,13 @@ export default {
         points: row.points,
         difficulty: row.difficulty,
         recommendedAge: row.recommendedAge,
+        duration: row.duration,
+        completeTypes: completeTypes || ['checkin'],
+        taskType: row.taskType || 'onetime',
+        frequency: row.frequency,
+        maxFrequency: row.maxFrequency,
         needReview: row.needReview,
+        reviewType: row.reviewType || 'creator',
         reviewByCategory: row.reviewByCategory || 0,
         isActive: row.isActive
       }
@@ -345,11 +421,16 @@ export default {
         if (valid) {
           this.submitting = true
           try {
+            // 准备提交数据,确保 completeTypes 为 JSON 字符串
+            const submitData = {
+              ...this.editForm,
+              completeTypes: JSON.stringify(this.editForm.completeTypes)
+            }
             if (this.isCreate) {
-              await createTaskTemplate(this.editForm)
+              await createTaskTemplate(submitData)
               this.$message.success('创建成功')
             } else {
-              await updateTaskTemplate(this.editForm.id, this.editForm)
+              await updateTaskTemplate(this.editForm.id, submitData)
               this.$message.success('更新成功')
             }
             this.editDialogVisible = false