From cba56021de5279f500c7b58d881ad49972c3f594 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Sat, 23 Aug 2025 14:31:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=A0=E9=99=A4=E5=BF=83=E6=83=85?= =?UTF-8?q?=E6=89=93=E5=8D=A1=E5=92=8C=E7=9B=AE=E6=A0=87=E5=AD=90=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1API=E6=B5=8B=E8=AF=95=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除test-goal-tasks.http和test-mood-checkins.http文件,清理不再使用的测试文件。 - 更新GoalsService中的目标删除逻辑,增加事务处理以确保数据一致性。 - 优化GoalTaskService中的任务生成逻辑,增加日志记录以便于调试和监控。 --- src/goals/dto/create-goal.dto.ts | 2 +- src/goals/goals.service.ts | 52 +++++-- src/goals/services/goal-task.service.ts | 66 +++++++-- test-goal-tasks.http | 185 ------------------------ test-mood-checkins.http | 94 ------------ 5 files changed, 99 insertions(+), 300 deletions(-) delete mode 100644 test-goal-tasks.http delete mode 100644 test-mood-checkins.http diff --git a/src/goals/dto/create-goal.dto.ts b/src/goals/dto/create-goal.dto.ts index c674244..70dbf56 100644 --- a/src/goals/dto/create-goal.dto.ts +++ b/src/goals/dto/create-goal.dto.ts @@ -1,6 +1,6 @@ import { IsString, IsNotEmpty, IsOptional, IsEnum, IsInt, IsDateString, IsBoolean, Min, Max, IsArray, ValidateNested } from 'class-validator'; import { Type } from 'class-transformer'; -import { GoalRepeatType, GoalStatus } from '../models/goal.model'; +import { GoalRepeatType } from '../models/goal.model'; export class CustomRepeatRuleDto { @IsOptional() diff --git a/src/goals/goals.service.ts b/src/goals/goals.service.ts index 291d981..01a3809 100644 --- a/src/goals/goals.service.ts +++ b/src/goals/goals.service.ts @@ -1,6 +1,7 @@ import { Injectable, Logger, NotFoundException, BadRequestException } from '@nestjs/common'; -import { InjectModel } from '@nestjs/sequelize'; -import { Op, WhereOptions, Order } from 'sequelize'; +import { InjectModel, InjectConnection } from '@nestjs/sequelize'; +import { Op, WhereOptions, Order, Transaction } from 'sequelize'; +import { Sequelize } from 'sequelize-typescript'; import { Goal, GoalStatus, GoalRepeatType } from './models/goal.model'; import { GoalCompletion } from './models/goal-completion.model'; import { GoalTask } from './models/goal-task.model'; @@ -22,6 +23,8 @@ export class GoalsService { private readonly goalCompletionModel: typeof GoalCompletion, @InjectModel(GoalTask) private readonly goalTaskModel: typeof GoalTask, + @InjectConnection() + private readonly sequelize: Sequelize, private readonly goalTaskService: GoalTaskService, ) { } @@ -180,27 +183,58 @@ export class GoalsService { * 删除目标 */ async deleteGoal(userId: string, goalId: string): Promise { + const transaction = await this.sequelize.transaction(); + try { + // 验证目标存在 const goal = await this.goalModel.findOne({ where: { id: goalId, userId, deleted: false }, + transaction, }); if (!goal) { + await transaction.rollback(); 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( - { deleted: true }, - { where: { goalId, userId } } - ); + // 提交事务 + await transaction.commit(); this.logger.log(`用户 ${userId} 删除了目标: ${goal.title}`); return true; } catch (error) { + // 回滚事务 + await transaction.rollback(); this.logger.error(`删除目标失败: ${error.message}`); throw error; } diff --git a/src/goals/services/goal-task.service.ts b/src/goals/services/goal-task.service.ts index e14ecdf..06cc50e 100644 --- a/src/goals/services/goal-task.service.ts +++ b/src/goals/services/goal-task.service.ts @@ -42,8 +42,11 @@ export class GoalTaskService { } const goals = await this.goalModel.findAll({ where }); + + this.logger.log(`为用户 ${userId} 找到 ${goals.length} 个活跃目标`); for (const goal of goals) { + this.logger.log(`开始为目标 ${goal.title} (${goal.repeatType}) 生成任务`); await this.generateTasksForGoal(goal); } } catch (error) { @@ -83,6 +86,9 @@ export class GoalTaskService { case GoalRepeatType.CUSTOM: await this.generateCustomTasks(goal, startDate, endDate, existingTasks); break; + default: + this.logger.warn(`未知的重复类型: ${goal.repeatType}`); + break; } // 更新过期任务状态 @@ -191,41 +197,66 @@ export class GoalTaskService { existingTasks: GoalTask[] ): Promise { const today = dayjs(); - const generateUntil = today.add(3, 'month'); // 提前生成3个月的任务 + const generateUntil = today.add(6, 'month'); // 提前生成6个月的任务 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 generatedCount = 0; + + this.logger.log(`开始生成每月任务,目标日期:每月第 ${targetDayOfMonth} 天`); 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 taskMonthStart = dayjs(task.startDate).startOf('month'); - return taskMonthStart.isSame(monthStart); + const taskDate = dayjs(task.startDate); + return taskDate.isSame(actualTargetDate, 'day'); }); - if (!existingTask && monthStart.isSameOrAfter(startDate)) { - const taskTitle = `${goal.title} - ${current.format('YYYY年MM月')}`; + if (!existingTask && actualTargetDate.isSameOrAfter(startDate)) { + const taskTitle = `${goal.title} - ${actualTargetDate.format('YYYY年MM月DD日')}`; await this.goalTaskModel.create({ goalId: goal.id, userId: goal.userId, title: taskTitle, description: `每月目标:完成${goal.frequency}次`, - startDate: monthStart.toDate(), - endDate: monthEnd.toDate(), + startDate: actualTargetDate.toDate(), + endDate: actualTargetDate.toDate(), // 任务在当天完成 targetCount: goal.frequency, currentCount: 0, status: TaskStatus.PENDING, }); + generatedCount++; this.logger.log(`为目标 ${goal.title} 生成每月任务: ${taskTitle}`); } current = current.add(1, 'month'); } + + this.logger.log(`为目标 ${goal.title} 生成了 ${generatedCount} 个每月任务`); } /** @@ -238,18 +269,26 @@ export class GoalTaskService { existingTasks: GoalTask[] ): Promise { if (!goal.customRepeatRule) { + this.logger.warn(`目标 ${goal.title} 缺少自定义重复规则`); return; } const { weekdays } = goal.customRepeatRule; + + this.logger.log(`为目标 ${goal.title} 生成自定义任务,重复规则: ${JSON.stringify(goal.customRepeatRule)}`); if (weekdays && weekdays.length > 0) { // 按指定星期几生成任务 const today = dayjs(); - const generateUntil = today.add(7, 'day'); + const generateUntil = today.add(4, 'week'); // 提前生成4周的任务,确保有足够的任务 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)) { const dayOfWeek = current.day(); // 0=周日, 6=周六 @@ -277,12 +316,17 @@ export class GoalTaskService { status: TaskStatus.PENDING, }); + generatedCount++; this.logger.log(`为目标 ${goal.title} 生成自定义任务: ${taskTitle}`); } } current = current.add(1, 'day'); } + + this.logger.log(`为目标 ${goal.title} 生成了 ${generatedCount} 个自定义任务`); + } else { + this.logger.warn(`目标 ${goal.title} 的自定义重复规则中没有指定星期几`); } } diff --git a/test-goal-tasks.http b/test-goal-tasks.http deleted file mode 100644 index c977ae4..0000000 --- a/test-goal-tasks.http +++ /dev/null @@ -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需要先创建目标和任务后才能测试 diff --git a/test-mood-checkins.http b/test-mood-checkins.http deleted file mode 100644 index 18000d5..0000000 --- a/test-mood-checkins.http +++ /dev/null @@ -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" -} \ No newline at end of file