feat: 新增目标子任务管理功能模块

- 实现目标子任务的完整功能,包括数据库表设计、API接口、业务逻辑和文档说明。
- 支持用户创建、管理和跟踪目标子任务,提供增删改查操作及任务完成记录功能。
- 引入惰性任务生成机制,优化任务管理体验,提升系统性能和用户交互。
This commit is contained in:
richarjiang
2025-08-22 16:01:12 +08:00
parent 062a78a839
commit 3530d123fc
10 changed files with 1593 additions and 21 deletions

View File

@@ -0,0 +1,304 @@
# 目标子任务API使用指南
## 概述
目标子任务系统是对目标管理功能的扩展,它支持用户为每个目标自动生成对应的子任务,用户可以根据目标的频率设置(每天、每周、每月)来完成这些子任务。系统采用惰性加载的方式,每次获取任务列表时才生成新的任务。
## 核心功能
### 1. 惰性任务生成
- **触发时机**: 当用户调用获取任务列表API时
- **生成策略**:
- 每日任务提前生成7天的任务
- 每周任务提前生成4周的任务
- 每月任务提前生成3个月的任务
- 自定义任务:根据自定义规则生成
### 2. 任务状态管理
- `pending`: 待开始
- `in_progress`: 进行中(部分完成)
- `completed`: 已完成
- `overdue`: 已过期
- `skipped`: 已跳过
### 3. 进度追踪
- 支持分步完成如一天要喝8杯水可以分8次上报
- 自动计算完成进度百分比
- 当完成次数达到目标次数时自动标记为完成
## API接口详解
### 1. 获取任务列表
**请求方式**: `GET /goals/tasks`
**查询参数**:
```typescript
{
goalId?: string; // 目标ID可选
status?: TaskStatus; // 任务状态(可选)
startDate?: string; // 开始日期(可选)
endDate?: string; // 结束日期(可选)
page?: number; // 页码默认1
pageSize?: number; // 每页数量默认20
}
```
**响应示例**:
```json
{
"code": 200,
"message": "获取任务列表成功",
"data": {
"page": 1,
"pageSize": 20,
"total": 5,
"list": [
{
"id": "task-uuid",
"goalId": "goal-uuid",
"userId": "user-123",
"title": "每日喝水 - 2024年01月15日",
"description": "每日目标完成8次",
"startDate": "2024-01-15",
"endDate": "2024-01-15",
"targetCount": 8,
"currentCount": 3,
"status": "in_progress",
"progressPercentage": 37,
"completedAt": null,
"notes": null,
"metadata": null,
"daysRemaining": 0,
"isToday": true,
"goal": {
"id": "goal-uuid",
"title": "每日喝水",
"repeatType": "daily",
"frequency": 8,
"category": "健康"
}
}
]
}
}
```
### 2. 完成任务
**请求方式**: `POST /goals/tasks/:taskId/complete`
**请求体**:
```typescript
{
count?: number; // 完成次数默认1
notes?: string; // 备注(可选)
completedAt?: string; // 完成时间(可选)
}
```
**使用示例**:
```bash
# 喝水1次
curl -X POST "http://localhost:3000/goals/tasks/task-uuid/complete" \
-H "Authorization: Bearer your-token" \
-H "Content-Type: application/json" \
-d '{"count": 1, "notes": "午饭后喝水"}'
# 一次性完成多次
curl -X POST "http://localhost:3000/goals/tasks/task-uuid/complete" \
-H "Authorization: Bearer your-token" \
-H "Content-Type: application/json" \
-d '{"count": 3, "notes": "连续喝了3杯水"}'
```
**响应示例**:
```json
{
"code": 200,
"message": "任务完成成功",
"data": {
"id": "task-uuid",
"currentCount": 4,
"progressPercentage": 50,
"status": "in_progress",
"notes": "午饭后喝水"
}
}
```
### 3. 获取特定目标的任务列表
**请求方式**: `GET /goals/:goalId/tasks`
**使用示例**:
```bash
curl -X GET "http://localhost:3000/goals/goal-uuid/tasks?page=1&pageSize=10" \
-H "Authorization: Bearer your-token"
```
### 4. 跳过任务
**请求方式**: `POST /goals/tasks/:taskId/skip`
**请求体**:
```typescript
{
reason?: string; // 跳过原因(可选)
}
```
**使用示例**:
```bash
curl -X POST "http://localhost:3000/goals/tasks/task-uuid/skip" \
-H "Authorization: Bearer your-token" \
-H "Content-Type: application/json" \
-d '{"reason": "今天身体不舒服"}'
```
### 5. 获取任务统计
**请求方式**: `GET /goals/tasks/stats/overview`
**查询参数**:
```typescript
{
goalId?: string; // 目标ID可选不传则统计所有目标的任务
}
```
**响应示例**:
```json
{
"code": 200,
"message": "获取任务统计成功",
"data": {
"total": 15,
"pending": 5,
"inProgress": 3,
"completed": 6,
"overdue": 1,
"skipped": 0,
"totalProgress": 68,
"todayTasks": 3,
"weekTasks": 8,
"monthTasks": 15
}
}
```
## 典型使用场景
### 场景1每日喝水目标
1. **创建目标**:
```json
{
"title": "每日喝水",
"description": "每天喝8杯水保持健康",
"repeatType": "daily",
"frequency": 8,
"category": "健康",
"startDate": "2024-01-01"
}
```
2. **系统自动生成任务**: 当用户第一次获取任务列表时系统会自动生成当天及未来7天的每日任务
3. **用户完成任务**: 每次喝水后调用完成API直到当天任务完成
### 场景2每周运动目标
1. **创建目标**:
```json
{
"title": "每周运动",
"description": "每周运动3次",
"repeatType": "weekly",
"frequency": 3,
"category": "运动",
"startDate": "2024-01-01"
}
```
2. **系统生成周任务**: 按周生成任务,每个任务的开始时间是周一,结束时间是周日
3. **灵活完成**: 用户可以在一周内的任何时间完成3次运动每次完成后调用API更新进度
### 场景3自定义周期目标
1. **创建目标**:
```json
{
"title": "周末阅读",
"description": "每个周末阅读1小时",
"repeatType": "custom",
"frequency": 1,
"customRepeatRule": {
"weekdays": [0, 6] // 周日和周六
},
"category": "学习"
}
```
2. **系统按规则生成**: 只在周六和周日生成任务
## 最佳实践
### 1. 任务列表获取
- 建议按日期范围获取任务,避免一次性加载过多数据
- 可以按状态筛选任务,如只显示今天的待完成任务
### 2. 进度上报
- 鼓励用户及时上报完成情况,保持任务状态的实时性
- 对于可分步完成的目标,支持分次上报更有利于习惯养成
### 3. 错误处理
- 当任务不存在或已完成时API会返回相应错误信息
- 客户端应该处理网络异常情况,支持离线记录后同步
### 4. 性能优化
- 惰性生成机制确保只在需要时生成任务
- 建议客户端缓存任务列表减少不必要的API调用
## 数据库表结构
### t_goal_tasks 表字段说明
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | CHAR(36) | 任务ID主键 |
| goal_id | CHAR(36) | 关联的目标ID |
| user_id | VARCHAR(255) | 用户ID |
| title | VARCHAR(255) | 任务标题 |
| description | TEXT | 任务描述 |
| start_date | DATE | 任务开始日期 |
| end_date | DATE | 任务结束日期 |
| target_count | INT | 目标完成次数 |
| current_count | INT | 当前完成次数 |
| status | ENUM | 任务状态 |
| progress_percentage | INT | 完成进度(0-100) |
| completed_at | DATETIME | 完成时间 |
| notes | TEXT | 备注 |
| metadata | JSON | 扩展数据 |
## 注意事项
1. **时区处理**: 所有日期时间都使用服务器时区,客户端需要进行相应转换
2. **并发安全**: 多次快速调用完成API可能导致计数不准确建议客户端控制调用频率
3. **数据一致性**: 目标删除时会级联删除相关任务
4. **性能考虑**: 大量历史任务可能影响查询性能,建议定期清理过期数据
## 常见问题
**Q: 如何修改已生成的任务?**
A: 可以使用更新任务API (PUT /goals/tasks/:taskId) 修改任务的基本信息,但不建议频繁修改以保持数据一致性。
**Q: 任务过期后还能完成吗?**
A: 过期任务状态会自动更新为'overdue',但仍然可以完成,完成后状态会变为'completed'。
**Q: 如何处理用户时区问题?**
A: 客户端应该将用户本地时间转换为服务器时区后发送请求,显示时再转换回用户时区。
**Q: 能否批量完成多个任务?**
A: 目前API设计为单个任务操作如需批量操作可以在客户端并发调用多个API。

View File

@@ -0,0 +1,49 @@
-- 创建目标子任务表
CREATE TABLE IF NOT EXISTS `t_goal_tasks` (
`id` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT '任务ID',
`goal_id` CHAR(36) NOT NULL COMMENT '目标ID',
`user_id` VARCHAR(255) NOT NULL COMMENT '用户ID',
`title` VARCHAR(255) NOT NULL COMMENT '任务标题',
`description` TEXT COMMENT '任务描述',
`start_date` DATE NOT NULL COMMENT '任务开始日期',
`end_date` DATE NOT NULL COMMENT '任务结束日期',
`target_count` INT NOT NULL DEFAULT 1 COMMENT '任务目标次数如喝水8次',
`current_count` INT NOT NULL DEFAULT 0 COMMENT '任务当前完成次数',
`status` ENUM('pending', 'in_progress', 'completed', 'overdue', 'skipped') NOT NULL DEFAULT 'pending' COMMENT '任务状态',
`progress_percentage` INT NOT NULL DEFAULT 0 COMMENT '完成进度百分比 (0-100)',
`completed_at` DATETIME COMMENT '任务完成时间',
`notes` TEXT COMMENT '任务备注',
`metadata` JSON COMMENT '任务额外数据',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否删除',
PRIMARY KEY (`id`),
INDEX `idx_goal_id` (`goal_id`),
INDEX `idx_user_id` (`user_id`),
INDEX `idx_status` (`status`),
INDEX `idx_start_date` (`start_date`),
INDEX `idx_end_date` (`end_date`),
INDEX `idx_deleted` (`deleted`),
INDEX `idx_user_goal` (`user_id`, `goal_id`),
INDEX `idx_user_status` (`user_id`, `status`),
INDEX `idx_user_date_range` (`user_id`, `start_date`, `end_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='目标子任务表';
-- 添加外键约束
ALTER TABLE `t_goal_tasks`
ADD CONSTRAINT `fk_goal_tasks_goal_id`
FOREIGN KEY (`goal_id`) REFERENCES `t_goals` (`id`)
ON DELETE CASCADE ON UPDATE CASCADE;
-- 添加检查约束MySQL 8.0+
-- ALTER TABLE `t_goal_tasks`
-- ADD CONSTRAINT `chk_target_count_positive` CHECK (`target_count` > 0);
--
-- ALTER TABLE `t_goal_tasks`
-- ADD CONSTRAINT `chk_current_count_non_negative` CHECK (`current_count` >= 0);
--
-- ALTER TABLE `t_goal_tasks`
-- ADD CONSTRAINT `chk_progress_percentage_range` CHECK (`progress_percentage` >= 0 AND `progress_percentage` <= 100);
--
-- ALTER TABLE `t_goal_tasks`
-- ADD CONSTRAINT `chk_date_range` CHECK (`end_date` >= `start_date`);

View File

@@ -0,0 +1,122 @@
import { IsString, IsOptional, IsInt, IsDateString, Min, IsUUID, IsEnum, Max } from 'class-validator';
import { TaskStatus } from '../models/goal-task.model';
export class CreateGoalTaskDto {
@IsUUID()
goalId: string;
@IsString()
title: string;
@IsOptional()
@IsString()
description?: string;
@IsDateString({}, { message: '开始日期格式无效' })
startDate: string;
@IsDateString({}, { message: '结束日期格式无效' })
endDate: string;
@IsInt()
@Min(1, { message: '目标次数必须大于0' })
targetCount: number;
@IsOptional()
@IsString()
notes?: string;
@IsOptional()
metadata?: any;
}
export class UpdateGoalTaskDto {
@IsOptional()
@IsString()
title?: string;
@IsOptional()
@IsString()
description?: string;
@IsOptional()
@IsDateString({}, { message: '开始日期格式无效' })
startDate?: string;
@IsOptional()
@IsDateString({}, { message: '结束日期格式无效' })
endDate?: string;
@IsOptional()
@IsInt()
@Min(1, { message: '目标次数必须大于0' })
targetCount?: number;
@IsOptional()
@IsEnum(TaskStatus, { message: '任务状态无效' })
status?: TaskStatus;
@IsOptional()
@IsString()
notes?: string;
@IsOptional()
metadata?: any;
}
export class GoalTaskQueryDto {
@IsOptional()
@IsUUID()
goalId?: string;
@IsOptional()
@IsEnum(TaskStatus, { message: '任务状态无效' })
status?: TaskStatus;
@IsOptional()
@IsDateString({}, { message: '开始日期格式无效' })
startDate?: string;
@IsOptional()
@IsDateString({}, { message: '结束日期格式无效' })
endDate?: string;
@IsOptional()
@IsInt()
@Min(1, { message: '页码必须大于0' })
page?: number = 1;
@IsOptional()
@IsInt()
@Min(1, { message: '每页数量必须大于0' })
@Max(100, { message: '每页数量不能超过100' })
pageSize?: number = 20;
}
export class CompleteGoalTaskDto {
@IsOptional()
@IsInt()
@Min(1, { message: '完成次数必须大于0' })
count?: number = 1;
@IsOptional()
@IsString()
notes?: string;
@IsOptional()
@IsDateString({}, { message: '完成时间格式无效' })
completedAt?: string;
}
export class GoalTaskStatsDto {
total: number;
pending: number;
inProgress: number;
completed: number;
overdue: number;
skipped: number;
totalProgress: number; // 总体完成进度
todayTasks: number;
weekTasks: number;
monthTasks: number;
}

View File

@@ -16,6 +16,8 @@ import { CreateGoalDto } from './dto/create-goal.dto';
import { UpdateGoalDto } from './dto/update-goal.dto'; import { UpdateGoalDto } from './dto/update-goal.dto';
import { GoalQueryDto } from './dto/goal-query.dto'; import { GoalQueryDto } from './dto/goal-query.dto';
import { CreateGoalCompletionDto } from './dto/goal-completion.dto'; import { CreateGoalCompletionDto } from './dto/goal-completion.dto';
import { GoalTaskService } from './services/goal-task.service';
import { GoalTaskQueryDto, CompleteGoalTaskDto, UpdateGoalTaskDto } from './dto/goal-task.dto';
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
import { BaseResponseDto, ResponseCode } from '../base.dto'; import { BaseResponseDto, ResponseCode } from '../base.dto';
import { GoalStatus } from './models/goal.model'; import { GoalStatus } from './models/goal.model';
@@ -25,7 +27,10 @@ import { AccessTokenPayload } from 'src/users/services/apple-auth.service';
@Controller('goals') @Controller('goals')
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
export class GoalsController { export class GoalsController {
constructor(private readonly goalsService: GoalsService) { } constructor(
private readonly goalsService: GoalsService,
private readonly goalTaskService: GoalTaskService,
) { }
/** /**
* 创建目标 * 创建目标
@@ -203,4 +208,123 @@ export class GoalsController {
data: results, data: results,
}; };
} }
// ==================== 子任务相关API ====================
/**
* 获取任务列表
*/
@Get('tasks')
async getTasks(
@Query() query: GoalTaskQueryDto,
@CurrentUser() user: AccessTokenPayload,
): Promise<BaseResponseDto<any>> {
const result = await this.goalTaskService.getTasks(user.sub, query);
return {
code: ResponseCode.SUCCESS,
message: '获取任务列表成功',
data: result,
};
}
/**
* 获取单个任务详情
*/
@Get('tasks/:taskId')
async getTask(
@Param('taskId') taskId: string,
@CurrentUser() user: AccessTokenPayload,
): Promise<BaseResponseDto<any>> {
const task = await this.goalTaskService.getTask(user.sub, taskId);
return {
code: ResponseCode.SUCCESS,
message: '获取任务详情成功',
data: task,
};
}
/**
* 完成任务
*/
@Post('tasks/:taskId/complete')
async completeTask(
@Param('taskId') taskId: string,
@Body() completeDto: CompleteGoalTaskDto,
@CurrentUser() user: AccessTokenPayload,
): Promise<BaseResponseDto<any>> {
const task = await this.goalTaskService.completeTask(user.sub, taskId, completeDto);
return {
code: ResponseCode.SUCCESS,
message: '任务完成成功',
data: task,
};
}
/**
* 更新任务
*/
@Put('tasks/:taskId')
async updateTask(
@Param('taskId') taskId: string,
@Body() updateDto: UpdateGoalTaskDto,
@CurrentUser() user: AccessTokenPayload,
): Promise<BaseResponseDto<any>> {
const task = await this.goalTaskService.updateTask(user.sub, taskId, updateDto);
return {
code: ResponseCode.SUCCESS,
message: '任务更新成功',
data: task,
};
}
/**
* 跳过任务
*/
@Post('tasks/:taskId/skip')
async skipTask(
@Param('taskId') taskId: string,
@Body() body: { reason?: string },
@CurrentUser() user: AccessTokenPayload,
): Promise<BaseResponseDto<any>> {
const task = await this.goalTaskService.skipTask(user.sub, taskId, body.reason);
return {
code: ResponseCode.SUCCESS,
message: '任务跳过成功',
data: task,
};
}
/**
* 获取任务统计
*/
@Get('tasks/stats/overview')
async getTaskStats(
@CurrentUser() user: AccessTokenPayload,
@Query('goalId') goalId?: string,
): Promise<BaseResponseDto<any>> {
const stats = await this.goalTaskService.getTaskStats(user.sub, goalId);
return {
code: ResponseCode.SUCCESS,
message: '获取任务统计成功',
data: stats,
};
}
/**
* 获取特定目标的任务列表
*/
@Get(':id/tasks')
async getGoalTasks(
@Param('id') goalId: string,
@Query() query: Omit<GoalTaskQueryDto, 'goalId'>,
@CurrentUser() user: AccessTokenPayload,
): Promise<BaseResponseDto<any>> {
const taskQuery = { ...query, goalId };
const result = await this.goalTaskService.getTasks(user.sub, taskQuery);
return {
code: ResponseCode.SUCCESS,
message: '获取目标任务列表成功',
data: result,
};
}
} }

View File

@@ -2,17 +2,19 @@ import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize'; import { SequelizeModule } from '@nestjs/sequelize';
import { GoalsController } from './goals.controller'; import { GoalsController } from './goals.controller';
import { GoalsService } from './goals.service'; import { GoalsService } from './goals.service';
import { GoalTaskService } from './services/goal-task.service';
import { Goal } from './models/goal.model'; import { Goal } from './models/goal.model';
import { GoalCompletion } from './models/goal-completion.model'; import { GoalCompletion } from './models/goal-completion.model';
import { GoalTask } from './models/goal-task.model';
import { UsersModule } from '../users/users.module'; import { UsersModule } from '../users/users.module';
@Module({ @Module({
imports: [ imports: [
SequelizeModule.forFeature([Goal, GoalCompletion]), SequelizeModule.forFeature([Goal, GoalCompletion, GoalTask]),
UsersModule, UsersModule,
], ],
controllers: [GoalsController], controllers: [GoalsController],
providers: [GoalsService], providers: [GoalsService, GoalTaskService],
exports: [GoalsService], exports: [GoalsService, GoalTaskService],
}) })
export class GoalsModule {} export class GoalsModule { }

View File

@@ -1,17 +1,30 @@
import { Injectable, Logger, NotFoundException, BadRequestException } from '@nestjs/common'; import { Injectable, Logger, NotFoundException, BadRequestException } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { Op, WhereOptions, Order } from 'sequelize'; import { Op, WhereOptions, Order } from 'sequelize';
import { Goal, GoalStatus, GoalRepeatType } from './models/goal.model'; import { Goal, GoalStatus, GoalRepeatType } from './models/goal.model';
import { GoalCompletion } from './models/goal-completion.model'; import { GoalCompletion } from './models/goal-completion.model';
import { GoalTask } from './models/goal-task.model';
import { CreateGoalDto } from './dto/create-goal.dto'; import { CreateGoalDto } from './dto/create-goal.dto';
import { UpdateGoalDto } from './dto/update-goal.dto'; import { UpdateGoalDto } from './dto/update-goal.dto';
import { GoalQueryDto } from './dto/goal-query.dto'; import { GoalQueryDto } from './dto/goal-query.dto';
import { CreateGoalCompletionDto } from './dto/goal-completion.dto'; import { CreateGoalCompletionDto } from './dto/goal-completion.dto';
import { GoalTaskService } from './services/goal-task.service';
import * as dayjs from 'dayjs'; import * as dayjs from 'dayjs';
@Injectable() @Injectable()
export class GoalsService { export class GoalsService {
private readonly logger = new Logger(GoalsService.name); private readonly logger = new Logger(GoalsService.name);
constructor(
@InjectModel(Goal)
private readonly goalModel: typeof Goal,
@InjectModel(GoalCompletion)
private readonly goalCompletionModel: typeof GoalCompletion,
@InjectModel(GoalTask)
private readonly goalTaskModel: typeof GoalTask,
private readonly goalTaskService: GoalTaskService,
) { }
/** /**
* 创建目标 * 创建目标
*/ */
@@ -27,13 +40,13 @@ export class GoalsService {
throw new BadRequestException('结束日期不能早于开始日期'); throw new BadRequestException('结束日期不能早于开始日期');
} }
const goal = await Goal.create({ const goal = await this.goalModel.create({
userId, userId,
...createGoalDto, ...createGoalDto,
startDate: createGoalDto.startDate ? new Date(createGoalDto.startDate) : null, startDate: createGoalDto.startDate ? new Date(createGoalDto.startDate) : undefined,
endDate: createGoalDto.endDate ? new Date(createGoalDto.endDate) : null, endDate: createGoalDto.endDate ? new Date(createGoalDto.endDate) : undefined,
startTime: createGoalDto.startTime ? createGoalDto.startTime : null, startTime: createGoalDto.startTime ? createGoalDto.startTime : undefined,
endTime: createGoalDto.endTime ? createGoalDto.endTime : null, endTime: createGoalDto.endTime ? createGoalDto.endTime : undefined,
}); });
this.logger.log(`用户 ${userId} 创建了目标: ${goal.title}`); this.logger.log(`用户 ${userId} 创建了目标: ${goal.title}`);
@@ -49,6 +62,9 @@ export class GoalsService {
*/ */
async getGoals(userId: string, query: GoalQueryDto) { async getGoals(userId: string, query: GoalQueryDto) {
try { try {
// 惰性生成任务
await this.goalTaskService.generateTasksLazily(userId);
const { page = 1, pageSize = 20, status, repeatType, category, startDate, endDate, sortBy = 'createdAt', sortOrder = 'desc' } = query; const { page = 1, pageSize = 20, status, repeatType, category, startDate, endDate, sortBy = 'createdAt', sortOrder = 'desc' } = query;
const offset = (page - 1) * pageSize; const offset = (page - 1) * pageSize;
@@ -85,7 +101,7 @@ export class GoalsService {
// 构建排序条件 // 构建排序条件
const order: Order = [[sortBy, sortOrder.toUpperCase()]]; const order: Order = [[sortBy, sortOrder.toUpperCase()]];
const { rows: goals, count } = await Goal.findAndCountAll({ const { rows: goals, count } = await this.goalModel.findAndCountAll({
where, where,
order, order,
offset, offset,
@@ -97,6 +113,14 @@ export class GoalsService {
where: { deleted: false }, where: { deleted: false },
required: false, required: false,
}, },
{
model: GoalTask,
as: 'tasks',
where: { deleted: false },
required: false,
limit: 5, // 只显示最近5个任务
order: [['startDate', 'DESC']],
},
], ],
}); });
@@ -117,7 +141,10 @@ export class GoalsService {
*/ */
async getGoal(userId: string, goalId: string): Promise<Goal> { async getGoal(userId: string, goalId: string): Promise<Goal> {
try { try {
const goal = await Goal.findOne({ // 惰性生成任务
await this.goalTaskService.generateTasksLazily(userId, goalId);
const goal = await this.goalModel.findOne({
where: { id: goalId, userId, deleted: false }, where: { id: goalId, userId, deleted: false },
include: [ include: [
{ {
@@ -128,6 +155,14 @@ export class GoalsService {
order: [['completedAt', 'DESC']], order: [['completedAt', 'DESC']],
limit: 10, // 只显示最近10次完成记录 limit: 10, // 只显示最近10次完成记录
}, },
{
model: GoalTask,
as: 'tasks',
where: { deleted: false },
required: false,
order: [['startDate', 'ASC']],
limit: 20, // 显示最近20个任务
},
], ],
}); });
@@ -147,7 +182,7 @@ export class GoalsService {
*/ */
async updateGoal(userId: string, goalId: string, updateGoalDto: UpdateGoalDto): Promise<Goal> { async updateGoal(userId: string, goalId: string, updateGoalDto: UpdateGoalDto): Promise<Goal> {
try { try {
const goal = await Goal.findOne({ const goal = await this.goalModel.findOne({
where: { id: goalId, userId, deleted: false }, where: { id: goalId, userId, deleted: false },
}); });
@@ -186,7 +221,7 @@ export class GoalsService {
*/ */
async deleteGoal(userId: string, goalId: string): Promise<boolean> { async deleteGoal(userId: string, goalId: string): Promise<boolean> {
try { try {
const goal = await Goal.findOne({ const goal = await this.goalModel.findOne({
where: { id: goalId, userId, deleted: false }, where: { id: goalId, userId, deleted: false },
}); });
@@ -198,7 +233,7 @@ export class GoalsService {
await goal.update({ deleted: true }); await goal.update({ deleted: true });
// 软删除相关的完成记录 // 软删除相关的完成记录
await GoalCompletion.update( await this.goalCompletionModel.update(
{ deleted: true }, { deleted: true },
{ where: { goalId, userId } } { where: { goalId, userId } }
); );
@@ -216,7 +251,7 @@ export class GoalsService {
*/ */
async completeGoal(userId: string, createCompletionDto: CreateGoalCompletionDto): Promise<GoalCompletion> { async completeGoal(userId: string, createCompletionDto: CreateGoalCompletionDto): Promise<GoalCompletion> {
try { try {
const goal = await Goal.findOne({ const goal = await this.goalModel.findOne({
where: { id: createCompletionDto.goalId, userId, deleted: false }, where: { id: createCompletionDto.goalId, userId, deleted: false },
}); });
@@ -232,7 +267,7 @@ export class GoalsService {
const completedAt = createCompletionDto.completedAt ? new Date(createCompletionDto.completedAt) : new Date(); const completedAt = createCompletionDto.completedAt ? new Date(createCompletionDto.completedAt) : new Date();
// 创建完成记录 // 创建完成记录
const completion = await GoalCompletion.create({ const completion = await this.goalCompletionModel.create({
goalId: createCompletionDto.goalId, goalId: createCompletionDto.goalId,
userId, userId,
completedAt, completedAt,
@@ -266,7 +301,7 @@ export class GoalsService {
const offset = (page - 1) * pageSize; const offset = (page - 1) * pageSize;
// 验证目标存在 // 验证目标存在
const goal = await Goal.findOne({ const goal = await this.goalModel.findOne({
where: { id: goalId, userId, deleted: false }, where: { id: goalId, userId, deleted: false },
}); });
@@ -291,7 +326,7 @@ export class GoalsService {
} }
} }
const { rows: completions, count } = await GoalCompletion.findAndCountAll({ const { rows: completions, count } = await this.goalCompletionModel.findAndCountAll({
where, where,
order: [['completedAt', 'DESC']], order: [['completedAt', 'DESC']],
offset, offset,
@@ -322,7 +357,7 @@ export class GoalsService {
*/ */
async getGoalStats(userId: string) { async getGoalStats(userId: string) {
try { try {
const goals = await Goal.findAll({ const goals = await this.goalModel.findAll({
where: { userId, deleted: false }, where: { userId, deleted: false },
include: [ include: [
{ {

View File

@@ -0,0 +1,166 @@
import { Column, DataType, Model, Table, ForeignKey, BelongsTo } from 'sequelize-typescript';
import { Goal } from './goal.model';
export enum TaskStatus {
PENDING = 'pending',
IN_PROGRESS = 'in_progress',
COMPLETED = 'completed',
OVERDUE = 'overdue',
SKIPPED = 'skipped'
}
@Table({
tableName: 't_goal_tasks',
underscored: true,
})
export class GoalTask extends Model {
@Column({
type: DataType.CHAR(36),
defaultValue: DataType.UUIDV4,
primaryKey: true,
})
declare id: string;
@ForeignKey(() => Goal)
@Column({
type: DataType.CHAR(36),
allowNull: false,
comment: '目标ID',
})
declare goalId: string;
@BelongsTo(() => Goal)
declare goal: Goal;
@Column({
type: DataType.STRING(255),
allowNull: false,
comment: '用户ID',
})
declare userId: string;
@Column({
type: DataType.STRING(255),
allowNull: false,
comment: '任务标题',
})
declare title: string;
@Column({
type: DataType.TEXT,
allowNull: true,
comment: '任务描述',
})
declare description: string;
@Column({
type: DataType.DATEONLY,
allowNull: false,
comment: '任务开始日期',
})
declare startDate: Date;
@Column({
type: DataType.DATEONLY,
allowNull: false,
comment: '任务结束日期',
})
declare endDate: Date;
@Column({
type: DataType.INTEGER,
allowNull: false,
defaultValue: 1,
comment: '任务目标次数如喝水8次',
})
declare targetCount: number;
@Column({
type: DataType.INTEGER,
allowNull: false,
defaultValue: 0,
comment: '任务当前完成次数',
})
declare currentCount: number;
@Column({
type: DataType.ENUM('pending', 'in_progress', 'completed', 'overdue', 'skipped'),
allowNull: false,
defaultValue: TaskStatus.PENDING,
comment: '任务状态',
})
declare status: TaskStatus;
@Column({
type: DataType.INTEGER,
allowNull: false,
defaultValue: 0,
comment: '完成进度百分比 (0-100)',
})
declare progressPercentage: number;
@Column({
type: DataType.DATE,
allowNull: true,
comment: '任务完成时间',
})
declare completedAt: Date;
@Column({
type: DataType.TEXT,
allowNull: true,
comment: '任务备注',
})
declare notes: string;
@Column({
type: DataType.JSON,
allowNull: true,
comment: '任务额外数据',
})
declare metadata: any;
@Column({
type: DataType.DATE,
defaultValue: DataType.NOW,
})
declare createdAt: Date;
@Column({
type: DataType.DATE,
defaultValue: DataType.NOW,
})
declare updatedAt: Date;
@Column({
type: DataType.BOOLEAN,
defaultValue: false,
comment: '是否删除',
})
declare deleted: boolean;
// 计算完成进度
updateProgress(): void {
if (this.targetCount > 0) {
this.progressPercentage = Math.min(100, Math.round((this.currentCount / this.targetCount) * 100));
// 更新状态
if (this.currentCount >= this.targetCount) {
this.status = TaskStatus.COMPLETED;
this.completedAt = new Date();
} else if (this.currentCount > 0) {
this.status = TaskStatus.IN_PROGRESS;
}
}
}
// 检查是否过期
checkOverdue(): void {
const now = new Date();
const endDate = new Date(this.endDate);
if (now > endDate && this.status !== TaskStatus.COMPLETED && this.status !== TaskStatus.SKIPPED) {
this.status = TaskStatus.OVERDUE;
}
}
}

View File

@@ -1,5 +1,6 @@
import { Column, DataType, Model, Table, HasMany } from 'sequelize-typescript'; import { Column, DataType, Model, Table, HasMany } from 'sequelize-typescript';
import { GoalCompletion } from './goal-completion.model'; import { GoalCompletion } from './goal-completion.model';
import { GoalTask } from './goal-task.model';
export enum GoalRepeatType { export enum GoalRepeatType {
DAILY = 'daily', DAILY = 'daily',
@@ -73,7 +74,7 @@ export class Goal extends Model {
@Column({ @Column({
type: DataType.DATEONLY, type: DataType.DATEONLY,
allowNull: false, allowNull: true,
comment: '目标开始日期', comment: '目标开始日期',
}) })
declare startDate: Date; declare startDate: Date;
@@ -182,4 +183,7 @@ export class Goal extends Model {
@HasMany(() => GoalCompletion, 'goalId') @HasMany(() => GoalCompletion, 'goalId')
declare completions: GoalCompletion[]; declare completions: GoalCompletion[];
@HasMany(() => GoalTask, 'goalId')
declare tasks: GoalTask[];
} }

View File

@@ -0,0 +1,581 @@
import { Injectable, Logger, NotFoundException, BadRequestException } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { Op, WhereOptions } from 'sequelize';
import { Goal, GoalRepeatType, GoalStatus } from '../models/goal.model';
import { GoalTask, TaskStatus } from '../models/goal-task.model';
import { CreateGoalTaskDto, UpdateGoalTaskDto, GoalTaskQueryDto, CompleteGoalTaskDto } from '../dto/goal-task.dto';
import * as dayjs from 'dayjs';
import * as weekOfYear from 'dayjs/plugin/weekOfYear';
import * as isoWeek from 'dayjs/plugin/isoWeek';
import * as isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
import * as isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
dayjs.extend(weekOfYear);
dayjs.extend(isoWeek);
dayjs.extend(isSameOrBefore);
dayjs.extend(isSameOrAfter);
@Injectable()
export class GoalTaskService {
private readonly logger = new Logger(GoalTaskService.name);
constructor(
@InjectModel(Goal)
private readonly goalModel: typeof Goal,
@InjectModel(GoalTask)
private readonly goalTaskModel: typeof GoalTask,
) { }
/**
* 惰性生成任务 - 每次获取任务列表时调用
*/
async generateTasksLazily(userId: string, goalId?: string): Promise<void> {
try {
const where: WhereOptions = {
userId,
deleted: false,
status: GoalStatus.ACTIVE,
};
if (goalId) {
where.id = goalId;
}
const goals = await this.goalModel.findAll({ where });
for (const goal of goals) {
await this.generateTasksForGoal(goal);
}
} catch (error) {
this.logger.error(`惰性生成任务失败: ${error.message}`);
throw error;
}
}
/**
* 为单个目标生成任务
*/
private async generateTasksForGoal(goal: Goal): Promise<void> {
const now = dayjs();
const startDate = goal.startDate ? dayjs(goal.startDate) : now;
const endDate = goal.endDate ? dayjs(goal.endDate) : now.add(1, 'year');
// 获取已存在的任务
const existingTasks = await this.goalTaskModel.findAll({
where: {
goalId: goal.id,
userId: goal.userId,
deleted: false,
},
});
// 根据重复类型生成任务
switch (goal.repeatType) {
case GoalRepeatType.DAILY:
await this.generateDailyTasks(goal, startDate, endDate, existingTasks);
break;
case GoalRepeatType.WEEKLY:
await this.generateWeeklyTasks(goal, startDate, endDate, existingTasks);
break;
case GoalRepeatType.MONTHLY:
await this.generateMonthlyTasks(goal, startDate, endDate, existingTasks);
break;
case GoalRepeatType.CUSTOM:
await this.generateCustomTasks(goal, startDate, endDate, existingTasks);
break;
}
// 更新过期任务状态
await this.updateOverdueTasks(goal.id, goal.userId);
}
/**
* 生成每日任务
*/
private async generateDailyTasks(
goal: Goal,
startDate: dayjs.Dayjs,
endDate: dayjs.Dayjs,
existingTasks: GoalTask[]
): Promise<void> {
const today = dayjs();
const generateUntil = today.add(7, 'day'); // 提前生成7天的任务
const actualEndDate = endDate.isBefore(generateUntil) ? endDate : generateUntil;
let current = startDate.isBefore(today) ? today : startDate;
while (current.isSameOrBefore(actualEndDate)) {
const taskDate = current.format('YYYY-MM-DD');
// 检查是否已存在该日期的任务
const existingTask = existingTasks.find(task =>
dayjs(task.startDate).format('YYYY-MM-DD') === taskDate
);
if (!existingTask) {
const taskTitle = goal.title;
await this.goalTaskModel.create({
goalId: goal.id,
userId: goal.userId,
title: taskTitle,
description: `每日目标:完成${goal.frequency}`,
startDate: current.toDate(),
endDate: current.toDate(),
targetCount: goal.frequency,
currentCount: 0,
status: TaskStatus.PENDING,
});
this.logger.log(`为目标 ${goal.title} 生成每日任务: ${taskTitle}`);
}
current = current.add(1, 'day');
}
}
/**
* 生成每周任务
*/
private async generateWeeklyTasks(
goal: Goal,
startDate: dayjs.Dayjs,
endDate: dayjs.Dayjs,
existingTasks: GoalTask[]
): Promise<void> {
const today = dayjs();
const generateUntil = today.add(4, 'week'); // 提前生成4周的任务
const actualEndDate = endDate.isBefore(generateUntil) ? endDate : generateUntil;
let current = startDate.startOf('isoWeek');
while (current.isSameOrBefore(actualEndDate)) {
const weekStart = current.startOf('isoWeek');
const weekEnd = current.endOf('isoWeek');
// 检查是否已存在该周的任务
const existingTask = existingTasks.find(task => {
const taskWeekStart = dayjs(task.startDate).startOf('isoWeek');
return taskWeekStart.isSame(weekStart);
});
if (!existingTask && weekStart.isSameOrAfter(startDate)) {
const taskTitle = `${goal.title} - 第${current.isoWeek()}周 (${weekStart.format('MM-DD')}${weekEnd.format('MM-DD')})`;
await this.goalTaskModel.create({
goalId: goal.id,
userId: goal.userId,
title: taskTitle,
description: `每周目标:完成${goal.frequency}`,
startDate: weekStart.toDate(),
endDate: weekEnd.toDate(),
targetCount: goal.frequency,
currentCount: 0,
status: TaskStatus.PENDING,
});
this.logger.log(`为目标 ${goal.title} 生成每周任务: ${taskTitle}`);
}
current = current.add(1, 'week');
}
}
/**
* 生成每月任务
*/
private async generateMonthlyTasks(
goal: Goal,
startDate: dayjs.Dayjs,
endDate: dayjs.Dayjs,
existingTasks: GoalTask[]
): Promise<void> {
const today = dayjs();
const generateUntil = today.add(3, 'month'); // 提前生成3个月的任务
const actualEndDate = endDate.isBefore(generateUntil) ? endDate : generateUntil;
let current = startDate.startOf('month');
while (current.isSameOrBefore(actualEndDate)) {
const monthStart = current.startOf('month');
const monthEnd = current.endOf('month');
// 检查是否已存在该月的任务
const existingTask = existingTasks.find(task => {
const taskMonthStart = dayjs(task.startDate).startOf('month');
return taskMonthStart.isSame(monthStart);
});
if (!existingTask && monthStart.isSameOrAfter(startDate)) {
const taskTitle = `${goal.title} - ${current.format('YYYY年MM月')}`;
await this.goalTaskModel.create({
goalId: goal.id,
userId: goal.userId,
title: taskTitle,
description: `每月目标:完成${goal.frequency}`,
startDate: monthStart.toDate(),
endDate: monthEnd.toDate(),
targetCount: goal.frequency,
currentCount: 0,
status: TaskStatus.PENDING,
});
this.logger.log(`为目标 ${goal.title} 生成每月任务: ${taskTitle}`);
}
current = current.add(1, 'month');
}
}
/**
* 生成自定义周期任务
*/
private async generateCustomTasks(
goal: Goal,
startDate: dayjs.Dayjs,
endDate: dayjs.Dayjs,
existingTasks: GoalTask[]
): Promise<void> {
if (!goal.customRepeatRule) {
return;
}
const { weekdays } = goal.customRepeatRule;
if (weekdays && weekdays.length > 0) {
// 按指定星期几生成任务
const today = dayjs();
const generateUntil = today.add(7, 'day');
const actualEndDate = endDate.isBefore(generateUntil) ? endDate : generateUntil;
let current = startDate;
while (current.isSameOrBefore(actualEndDate)) {
const dayOfWeek = current.day(); // 0=周日, 6=周六
if (weekdays.includes(dayOfWeek)) {
const taskDate = current.format('YYYY-MM-DD');
const existingTask = existingTasks.find(task =>
dayjs(task.startDate).format('YYYY-MM-DD') === taskDate
);
if (!existingTask) {
const weekDayNames = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const taskTitle = `${goal.title} - ${current.format('YYYY年MM月DD日')} ${weekDayNames[dayOfWeek]}`;
await this.goalTaskModel.create({
goalId: goal.id,
userId: goal.userId,
title: taskTitle,
description: `自定义目标:完成${goal.frequency}`,
startDate: current.toDate(),
endDate: current.toDate(),
targetCount: goal.frequency,
currentCount: 0,
status: TaskStatus.PENDING,
});
this.logger.log(`为目标 ${goal.title} 生成自定义任务: ${taskTitle}`);
}
}
current = current.add(1, 'day');
}
}
}
/**
* 更新过期任务状态
*/
private async updateOverdueTasks(goalId: string, userId: string): Promise<void> {
const now = new Date();
await this.goalTaskModel.update(
{ status: TaskStatus.OVERDUE },
{
where: {
goalId,
userId,
deleted: false,
endDate: { [Op.lt]: now },
status: { [Op.notIn]: [TaskStatus.COMPLETED, TaskStatus.SKIPPED, TaskStatus.OVERDUE] },
},
}
);
}
/**
* 获取任务列表
*/
async getTasks(userId: string, query: GoalTaskQueryDto) {
try {
// 先进行惰性生成
await this.generateTasksLazily(userId, query.goalId);
const { page = 1, pageSize = 20, goalId, status, startDate, endDate } = query;
const offset = (page - 1) * pageSize;
const where: WhereOptions = {
userId,
deleted: false,
};
if (goalId) {
where.goalId = goalId;
}
if (status) {
where.status = status;
}
if (startDate || endDate) {
where.startDate = {};
if (startDate) {
where.startDate[Op.gte] = new Date(startDate);
}
if (endDate) {
where.startDate[Op.lte] = new Date(endDate);
}
}
const { rows: tasks, count } = await this.goalTaskModel.findAndCountAll({
where,
order: [['startDate', 'ASC'], ['createdAt', 'DESC']],
offset,
limit: pageSize,
include: [
{
model: Goal,
as: 'goal',
attributes: ['id', 'title', 'repeatType', 'frequency', 'category'],
},
],
});
return {
page,
pageSize,
total: count,
list: tasks.map(task => this.formatTaskResponse(task)),
};
} catch (error) {
this.logger.error(`获取任务列表失败: ${error.message}`);
throw error;
}
}
/**
* 获取单个任务详情
*/
async getTask(userId: string, taskId: string): Promise<GoalTask> {
try {
const task = await this.goalTaskModel.findOne({
where: { id: taskId, userId, deleted: false },
include: [
{
model: Goal,
as: 'goal',
attributes: ['id', 'title', 'repeatType', 'frequency', 'category'],
},
],
});
if (!task) {
throw new NotFoundException('任务不存在');
}
return this.formatTaskResponse(task);
} catch (error) {
this.logger.error(`获取任务详情失败: ${error.message}`);
throw error;
}
}
/**
* 完成任务
*/
async completeTask(userId: string, taskId: string, completeDto: CompleteGoalTaskDto): Promise<GoalTask> {
try {
const task = await this.goalTaskModel.findOne({
where: { id: taskId, userId, deleted: false },
});
if (!task) {
throw new NotFoundException('任务不存在');
}
if (task.status === TaskStatus.COMPLETED) {
throw new BadRequestException('任务已完成');
}
const { count = 1, notes, completedAt } = completeDto;
// 更新完成次数
task.currentCount = Math.min(task.currentCount + count, task.targetCount);
task.notes = notes || task.notes;
if (completedAt) {
task.completedAt = new Date(completedAt);
}
// 更新进度和状态
task.updateProgress();
await task.save();
this.logger.log(`用户 ${userId} 完成任务: ${task.title}, 当前进度: ${task.currentCount}/${task.targetCount}`);
return this.formatTaskResponse(task);
} catch (error) {
this.logger.error(`完成任务失败: ${error.message}`);
throw error;
}
}
/**
* 更新任务
*/
async updateTask(userId: string, taskId: string, updateDto: UpdateGoalTaskDto): Promise<GoalTask> {
try {
const task = await this.goalTaskModel.findOne({
where: { id: taskId, userId, deleted: false },
});
if (!task) {
throw new NotFoundException('任务不存在');
}
await task.update({
...updateDto,
startDate: updateDto.startDate ? new Date(updateDto.startDate) : task.startDate,
endDate: updateDto.endDate ? new Date(updateDto.endDate) : task.endDate,
});
// 如果更新了目标次数,重新计算进度
if (updateDto.targetCount !== undefined) {
task.updateProgress();
await task.save();
}
this.logger.log(`用户 ${userId} 更新任务: ${task.title}`);
return this.formatTaskResponse(task);
} catch (error) {
this.logger.error(`更新任务失败: ${error.message}`);
throw error;
}
}
/**
* 跳过任务
*/
async skipTask(userId: string, taskId: string, reason?: string): Promise<GoalTask> {
try {
const task = await this.goalTaskModel.findOne({
where: { id: taskId, userId, deleted: false },
});
if (!task) {
throw new NotFoundException('任务不存在');
}
await task.update({
status: TaskStatus.SKIPPED,
notes: reason || '用户主动跳过',
});
this.logger.log(`用户 ${userId} 跳过任务: ${task.title}`);
return this.formatTaskResponse(task);
} catch (error) {
this.logger.error(`跳过任务失败: ${error.message}`);
throw error;
}
}
/**
* 获取任务统计
*/
async getTaskStats(userId: string, goalId?: string) {
try {
const where: WhereOptions = {
userId,
deleted: false,
};
if (goalId) {
where.goalId = goalId;
}
const tasks = await this.goalTaskModel.findAll({ where });
const now = dayjs();
const todayStart = now.startOf('day');
const weekStart = now.startOf('isoWeek');
const monthStart = now.startOf('month');
const stats = {
total: tasks.length,
pending: tasks.filter(t => t.status === TaskStatus.PENDING).length,
inProgress: tasks.filter(t => t.status === TaskStatus.IN_PROGRESS).length,
completed: tasks.filter(t => t.status === TaskStatus.COMPLETED).length,
overdue: tasks.filter(t => t.status === TaskStatus.OVERDUE).length,
skipped: tasks.filter(t => t.status === TaskStatus.SKIPPED).length,
totalProgress: 0,
todayTasks: 0,
weekTasks: 0,
monthTasks: 0,
};
// 计算总体进度
if (tasks.length > 0) {
const totalProgress = tasks.reduce((sum, task) => sum + task.progressPercentage, 0);
stats.totalProgress = Math.round(totalProgress / tasks.length);
}
// 统计时间范围内的任务
tasks.forEach(task => {
const taskDate = dayjs(task.startDate);
if (taskDate.isSame(todayStart, 'day')) {
stats.todayTasks++;
}
if (taskDate.isSameOrAfter(weekStart)) {
stats.weekTasks++;
}
if (taskDate.isSameOrAfter(monthStart)) {
stats.monthTasks++;
}
});
return stats;
} catch (error) {
this.logger.error(`获取任务统计失败: ${error.message}`);
throw error;
}
}
/**
* 格式化任务响应
*/
private formatTaskResponse(task: GoalTask) {
const taskData = task.toJSON();
// 检查是否过期
task.checkOverdue();
// 计算剩余天数
const endDate = dayjs(taskData.endDate);
const now = dayjs();
taskData.daysRemaining = Math.max(0, endDate.diff(now, 'day'));
// 计算是否为今日任务
taskData.isToday = dayjs(taskData.startDate).isSame(now, 'day');
return taskData;
}
}

185
test-goal-tasks.http Normal file
View File

@@ -0,0 +1,185 @@
### 目标子任务API测试文件
@baseUrl = http://localhost:3000
@token = your-auth-token-here
### 1. 创建每日喝水目标
POST {{baseUrl}}/goals
Authorization: Bearer {{token}}
Content-Type: application/json
{
"title": "每日喝水",
"description": "每天喝8杯水保持健康",
"repeatType": "daily",
"frequency": 8,
"category": "健康",
"startDate": "2024-01-01",
"hasReminder": true,
"reminderTime": "09:00"
}
### 2. 创建每周运动目标
POST {{baseUrl}}/goals
Authorization: Bearer {{token}}
Content-Type: application/json
{
"title": "每周运动",
"description": "每周运动3次每次至少30分钟",
"repeatType": "weekly",
"frequency": 3,
"category": "运动",
"startDate": "2024-01-01",
"targetCount": 52
}
### 3. 创建自定义周期目标(周末阅读)
POST {{baseUrl}}/goals
Authorization: Bearer {{token}}
Content-Type: application/json
{
"title": "周末阅读",
"description": "每个周末阅读1小时",
"repeatType": "custom",
"frequency": 1,
"customRepeatRule": {
"weekdays": [0, 6]
},
"category": "学习",
"startDate": "2024-01-01"
}
### 4. 获取目标列表(触发任务生成)
GET {{baseUrl}}/goals?page=1&pageSize=10
Authorization: Bearer {{token}}
### 5. 获取所有任务列表
GET {{baseUrl}}/goals/tasks?page=1&pageSize=20
Authorization: Bearer {{token}}
### 6. 获取今天的任务
GET {{baseUrl}}/goals/tasks?startDate=2024-01-15&endDate=2024-01-15
Authorization: Bearer {{token}}
### 7. 获取进行中的任务
GET {{baseUrl}}/goals/tasks?status=pending
Authorization: Bearer {{token}}
### 8. 获取特定目标的任务列表
# 需要替换为实际的goalId
GET {{baseUrl}}/goals/{goalId}/tasks
Authorization: Bearer {{token}}
### 9. 完成任务 - 单次完成
# 需要替换为实际的taskId
POST {{baseUrl}}/goals/tasks/{taskId}/complete
Authorization: Bearer {{token}}
Content-Type: application/json
{
"count": 1,
"notes": "早上喝了第一杯水"
}
### 10. 完成任务 - 多次完成
# 需要替换为实际的taskId
POST {{baseUrl}}/goals/tasks/{taskId}/complete
Authorization: Bearer {{token}}
Content-Type: application/json
{
"count": 3,
"notes": "连续喝了3杯水",
"completedAt": "2024-01-15T14:30:00Z"
}
### 11. 获取单个任务详情
# 需要替换为实际的taskId
GET {{baseUrl}}/goals/tasks/{taskId}
Authorization: Bearer {{token}}
### 12. 更新任务
# 需要替换为实际的taskId
PUT {{baseUrl}}/goals/tasks/{taskId}
Authorization: Bearer {{token}}
Content-Type: application/json
{
"notes": "修改了任务备注",
"targetCount": 10
}
### 13. 跳过任务
# 需要替换为实际的taskId
POST {{baseUrl}}/goals/tasks/{taskId}/skip
Authorization: Bearer {{token}}
Content-Type: application/json
{
"reason": "今天身体不舒服,暂时跳过"
}
### 14. 获取任务统计(所有目标)
GET {{baseUrl}}/goals/tasks/stats/overview
Authorization: Bearer {{token}}
### 15. 获取特定目标的任务统计
# 需要替换为实际的goalId
GET {{baseUrl}}/goals/tasks/stats/overview?goalId={goalId}
Authorization: Bearer {{token}}
### 16. 获取目标详情(包含任务)
# 需要替换为实际的goalId
GET {{baseUrl}}/goals/{goalId}
Authorization: Bearer {{token}}
### 17. 批量操作目标
POST {{baseUrl}}/goals/batch
Authorization: Bearer {{token}}
Content-Type: application/json
{
"goalIds": ["goal-id-1", "goal-id-2"],
"action": "pause"
}
### 18. 获取过期任务
GET {{baseUrl}}/goals/tasks?status=overdue
Authorization: Bearer {{token}}
### 19. 获取已完成任务
GET {{baseUrl}}/goals/tasks?status=completed&page=1&pageSize=10
Authorization: Bearer {{token}}
### 20. 获取本周任务
GET {{baseUrl}}/goals/tasks?startDate=2024-01-08&endDate=2024-01-14
Authorization: Bearer {{token}}
### 测试场景说明
# 场景1每日喝水目标测试流程
# 1. 创建每日喝水目标接口1
# 2. 获取目标列表触发任务生成接口4
# 3. 查看今天的任务接口6
# 4. 分多次完成喝水任务接口9、10
# 5. 查看任务统计接口14
# 场景2每周运动目标测试流程
# 1. 创建每周运动目标接口2
# 2. 获取本周任务接口20
# 3. 完成运动任务接口9
# 4. 查看特定目标的任务列表接口8
# 场景3任务管理测试流程
# 1. 查看所有待完成任务接口7
# 2. 跳过某个任务接口13
# 3. 更新任务信息接口12
# 4. 查看过期任务接口18
### 注意事项
# 1. 替换所有的 {goalId} 和 {taskId} 为实际的ID值
# 2. 替换 {{token}} 为有效的认证令牌
# 3. 根据实际情况调整日期参数
# 4. 某些API需要先创建目标和任务后才能测试