feat: 删除心情打卡和目标子任务API测试文件

- 移除test-goal-tasks.http和test-mood-checkins.http文件,清理不再使用的测试文件。
- 更新GoalsService中的目标删除逻辑,增加事务处理以确保数据一致性。
- 优化GoalTaskService中的任务生成逻辑,增加日志记录以便于调试和监控。
This commit is contained in:
2025-08-23 14:31:15 +08:00
parent f6b4c99e75
commit cba56021de
5 changed files with 99 additions and 300 deletions

View File

@@ -1,6 +1,6 @@
import { IsString, IsNotEmpty, IsOptional, IsEnum, IsInt, IsDateString, IsBoolean, Min, Max, IsArray, ValidateNested } from 'class-validator'; import { IsString, IsNotEmpty, IsOptional, IsEnum, IsInt, IsDateString, IsBoolean, Min, Max, IsArray, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { GoalRepeatType, GoalStatus } from '../models/goal.model'; import { GoalRepeatType } from '../models/goal.model';
export class CustomRepeatRuleDto { export class CustomRepeatRuleDto {
@IsOptional() @IsOptional()

View File

@@ -1,6 +1,7 @@
import { Injectable, Logger, NotFoundException, BadRequestException } from '@nestjs/common'; import { Injectable, Logger, NotFoundException, BadRequestException } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize'; import { InjectModel, InjectConnection } from '@nestjs/sequelize';
import { Op, WhereOptions, Order } from 'sequelize'; import { Op, WhereOptions, Order, Transaction } from 'sequelize';
import { Sequelize } from 'sequelize-typescript';
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 { GoalTask } from './models/goal-task.model';
@@ -22,6 +23,8 @@ export class GoalsService {
private readonly goalCompletionModel: typeof GoalCompletion, private readonly goalCompletionModel: typeof GoalCompletion,
@InjectModel(GoalTask) @InjectModel(GoalTask)
private readonly goalTaskModel: typeof GoalTask, private readonly goalTaskModel: typeof GoalTask,
@InjectConnection()
private readonly sequelize: Sequelize,
private readonly goalTaskService: GoalTaskService, private readonly goalTaskService: GoalTaskService,
) { } ) { }
@@ -180,27 +183,58 @@ export class GoalsService {
* 删除目标 * 删除目标
*/ */
async deleteGoal(userId: string, goalId: string): Promise<boolean> { async deleteGoal(userId: string, goalId: string): Promise<boolean> {
const transaction = await this.sequelize.transaction();
try { try {
// 验证目标存在
const goal = await this.goalModel.findOne({ const goal = await this.goalModel.findOne({
where: { id: goalId, userId, deleted: false }, where: { id: goalId, userId, deleted: false },
transaction,
}); });
if (!goal) { if (!goal) {
await transaction.rollback();
throw new NotFoundException('目标不存在'); throw new NotFoundException('目标不存在');
} }
// 软删除目标 // 使用事务删除目标及其相关数据
await goal.update({ deleted: true }); await Promise.all([
// 软删除目标本身
this.goalModel.update(
{ deleted: true },
{
where: { id: goalId, userId, deleted: false },
transaction
}
),
// 软删除目标完成记录
this.goalCompletionModel.update(
{ deleted: true },
{
where: { goalId, userId, deleted: false },
transaction
}
),
// 软删除与目标关联的任务
this.goalTaskModel.update(
{ deleted: true },
{
where: { goalId, userId, deleted: false },
transaction
}
),
]);
// 软删除相关的完成记录 // 提交事务
await this.goalCompletionModel.update( await transaction.commit();
{ deleted: true },
{ where: { goalId, userId } }
);
this.logger.log(`用户 ${userId} 删除了目标: ${goal.title}`); this.logger.log(`用户 ${userId} 删除了目标: ${goal.title}`);
return true; return true;
} catch (error) { } catch (error) {
// 回滚事务
await transaction.rollback();
this.logger.error(`删除目标失败: ${error.message}`); this.logger.error(`删除目标失败: ${error.message}`);
throw error; throw error;
} }

View File

@@ -42,8 +42,11 @@ export class GoalTaskService {
} }
const goals = await this.goalModel.findAll({ where }); const goals = await this.goalModel.findAll({ where });
this.logger.log(`为用户 ${userId} 找到 ${goals.length} 个活跃目标`);
for (const goal of goals) { for (const goal of goals) {
this.logger.log(`开始为目标 ${goal.title} (${goal.repeatType}) 生成任务`);
await this.generateTasksForGoal(goal); await this.generateTasksForGoal(goal);
} }
} catch (error) { } catch (error) {
@@ -83,6 +86,9 @@ export class GoalTaskService {
case GoalRepeatType.CUSTOM: case GoalRepeatType.CUSTOM:
await this.generateCustomTasks(goal, startDate, endDate, existingTasks); await this.generateCustomTasks(goal, startDate, endDate, existingTasks);
break; break;
default:
this.logger.warn(`未知的重复类型: ${goal.repeatType}`);
break;
} }
// 更新过期任务状态 // 更新过期任务状态
@@ -191,41 +197,66 @@ export class GoalTaskService {
existingTasks: GoalTask[] existingTasks: GoalTask[]
): Promise<void> { ): Promise<void> {
const today = dayjs(); const today = dayjs();
const generateUntil = today.add(3, 'month'); // 提前生成3个月的任务 const generateUntil = today.add(6, 'month'); // 提前生成6个月的任务
const actualEndDate = endDate.isBefore(generateUntil) ? endDate : generateUntil; const actualEndDate = endDate.isBefore(generateUntil) ? endDate : generateUntil;
// 检查是否有自定义重复规则指定每月第几天
let targetDayOfMonth = 1; // 默认每月1号
if (goal.customRepeatRule && goal.customRepeatRule.dayOfMonth) {
targetDayOfMonth = goal.customRepeatRule.dayOfMonth;
this.logger.log(`目标 ${goal.title} 设置为每月第 ${targetDayOfMonth}`);
}
// 从开始日期开始,逐月生成任务
let current = startDate.startOf('month'); let current = startDate.startOf('month');
let generatedCount = 0;
this.logger.log(`开始生成每月任务,目标日期:每月第 ${targetDayOfMonth}`);
while (current.isSameOrBefore(actualEndDate)) { while (current.isSameOrBefore(actualEndDate)) {
const monthStart = current.startOf('month'); // 计算该月的目标日期
const monthEnd = current.endOf('month'); const targetDate = current.date(targetDayOfMonth);
// 如果目标日期超出了该月的天数,则使用该月的最后一天
const daysInMonth = current.daysInMonth();
const actualTargetDate = targetDayOfMonth > daysInMonth ? current.date(daysInMonth) : targetDate;
// 检查是否已经过了该月的目标日期
if (actualTargetDate.isBefore(today)) {
this.logger.log(`跳过 ${current.format('YYYY年MM月')},目标日期 ${actualTargetDate.format('MM-DD')} 已过期`);
current = current.add(1, 'month');
continue;
}
// 检查是否已存在该月的任务 // 检查是否已存在该月的任务
const existingTask = existingTasks.find(task => { const existingTask = existingTasks.find(task => {
const taskMonthStart = dayjs(task.startDate).startOf('month'); const taskDate = dayjs(task.startDate);
return taskMonthStart.isSame(monthStart); return taskDate.isSame(actualTargetDate, 'day');
}); });
if (!existingTask && monthStart.isSameOrAfter(startDate)) { if (!existingTask && actualTargetDate.isSameOrAfter(startDate)) {
const taskTitle = `${goal.title} - ${current.format('YYYY年MM月')}`; const taskTitle = `${goal.title} - ${actualTargetDate.format('YYYY年MM月DD日')}`;
await this.goalTaskModel.create({ await this.goalTaskModel.create({
goalId: goal.id, goalId: goal.id,
userId: goal.userId, userId: goal.userId,
title: taskTitle, title: taskTitle,
description: `每月目标:完成${goal.frequency}`, description: `每月目标:完成${goal.frequency}`,
startDate: monthStart.toDate(), startDate: actualTargetDate.toDate(),
endDate: monthEnd.toDate(), endDate: actualTargetDate.toDate(), // 任务在当天完成
targetCount: goal.frequency, targetCount: goal.frequency,
currentCount: 0, currentCount: 0,
status: TaskStatus.PENDING, status: TaskStatus.PENDING,
}); });
generatedCount++;
this.logger.log(`为目标 ${goal.title} 生成每月任务: ${taskTitle}`); this.logger.log(`为目标 ${goal.title} 生成每月任务: ${taskTitle}`);
} }
current = current.add(1, 'month'); current = current.add(1, 'month');
} }
this.logger.log(`为目标 ${goal.title} 生成了 ${generatedCount} 个每月任务`);
} }
/** /**
@@ -238,18 +269,26 @@ export class GoalTaskService {
existingTasks: GoalTask[] existingTasks: GoalTask[]
): Promise<void> { ): Promise<void> {
if (!goal.customRepeatRule) { if (!goal.customRepeatRule) {
this.logger.warn(`目标 ${goal.title} 缺少自定义重复规则`);
return; return;
} }
const { weekdays } = goal.customRepeatRule; const { weekdays } = goal.customRepeatRule;
this.logger.log(`为目标 ${goal.title} 生成自定义任务,重复规则: ${JSON.stringify(goal.customRepeatRule)}`);
if (weekdays && weekdays.length > 0) { if (weekdays && weekdays.length > 0) {
// 按指定星期几生成任务 // 按指定星期几生成任务
const today = dayjs(); const today = dayjs();
const generateUntil = today.add(7, 'day'); const generateUntil = today.add(4, 'week'); // 提前生成4周的任务确保有足够的任务
const actualEndDate = endDate.isBefore(generateUntil) ? endDate : generateUntil; const actualEndDate = endDate.isBefore(generateUntil) ? endDate : generateUntil;
let current = startDate; // 从今天开始生成,如果开始日期晚于今天则从开始日期开始
let current = startDate.isBefore(today) ? today : startDate;
let generatedCount = 0;
this.logger.log(`开始生成自定义任务,日期范围: ${current.format('YYYY-MM-DD')}${actualEndDate.format('YYYY-MM-DD')}`);
while (current.isSameOrBefore(actualEndDate)) { while (current.isSameOrBefore(actualEndDate)) {
const dayOfWeek = current.day(); // 0=周日, 6=周六 const dayOfWeek = current.day(); // 0=周日, 6=周六
@@ -277,12 +316,17 @@ export class GoalTaskService {
status: TaskStatus.PENDING, status: TaskStatus.PENDING,
}); });
generatedCount++;
this.logger.log(`为目标 ${goal.title} 生成自定义任务: ${taskTitle}`); this.logger.log(`为目标 ${goal.title} 生成自定义任务: ${taskTitle}`);
} }
} }
current = current.add(1, 'day'); current = current.add(1, 'day');
} }
this.logger.log(`为目标 ${goal.title} 生成了 ${generatedCount} 个自定义任务`);
} else {
this.logger.warn(`目标 ${goal.title} 的自定义重复规则中没有指定星期几`);
} }
} }

View File

@@ -1,185 +0,0 @@
### 目标子任务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需要先创建目标和任务后才能测试

View File

@@ -1,94 +0,0 @@
### 心情打卡功能测试
### 1. 创建心情打卡 - 开心
POST http://localhost:3000/mood-checkins
Authorization: Bearer YOUR_JWT_TOKEN
Content-Type: application/json
{
"moodType": "happy",
"intensity": 8,
"description": "今天完成了重要的项目,心情很好!",
"checkinDate": "2025-08-21",
"metadata": {
"tags": ["工作", "成就感"],
"trigger": "完成项目里程碑"
}
}
### 2. 创建心情打卡 - 焦虑
POST http://localhost:3000/mood-checkins
Authorization: Bearer YOUR_JWT_TOKEN
Content-Type: application/json
{
"moodType": "anxious",
"intensity": 6,
"description": "明天有重要的会议,感到有些紧张",
"checkinDate": "2025-08-21",
"metadata": {
"tags": ["工作", "压力"],
"trigger": "重要会议前的紧张"
}
}
### 3. 创建心情打卡 - 心动
POST http://localhost:3000/mood-checkins
Authorization: Bearer YOUR_JWT_TOKEN
Content-Type: application/json
{
"moodType": "excited",
"intensity": 9,
"description": "看到喜欢的人发来消息,心跳加速",
"checkinDate": "2025-08-21",
"metadata": {
"tags": ["感情", "甜蜜"],
"trigger": "收到心仪对象的消息"
}
}
### 3. 获取今日心情打卡
GET http://localhost:3000/mood-checkins/daily
Authorization: Bearer YOUR_JWT_TOKEN
### 4. 获取指定日期心情打卡
GET http://localhost:3000/mood-checkins/daily?date=2025-08-21
Authorization: Bearer YOUR_JWT_TOKEN
### 5. 获取心情打卡历史
GET http://localhost:3000/mood-checkins/history?startDate=2025-08-01&endDate=2025-08-31
Authorization: Bearer YOUR_JWT_TOKEN
### 6. 获取心情打卡历史(按类型过滤)
GET http://localhost:3000/mood-checkins/history?startDate=2025-08-01&endDate=2025-08-31&moodType=happy
Authorization: Bearer YOUR_JWT_TOKEN
### 7. 获取心情统计数据
GET http://localhost:3000/mood-checkins/statistics?startDate=2025-08-01&endDate=2025-08-31
Authorization: Bearer YOUR_JWT_TOKEN
### 8. 更新心情打卡
PUT http://localhost:3000/mood-checkins
Authorization: Bearer YOUR_JWT_TOKEN
Content-Type: application/json
{
"id": "MOOD_CHECKIN_ID",
"moodType": "thrilled",
"intensity": 9,
"description": "更新后的心情描述 - 非常兴奋!",
"metadata": {
"tags": ["更新", "兴奋"],
"trigger": "收到好消息"
}
}
### 9. 删除心情打卡
DELETE http://localhost:3000/mood-checkins
Authorization: Bearer YOUR_JWT_TOKEN
Content-Type: application/json
{
"id": "MOOD_CHECKIN_ID"
}