Files
plates-server/src/goals/goals.service.ts
richarjiang 8aca29e2b3 feat: 更新目标修改逻辑,增加日志记录和日期处理优化
- 在updateGoal方法中添加日志记录,便于调试和监控目标更新过程。
- 优化目标更新时的日期处理逻辑,确保endDate字段在未提供时设置为undefined,提升数据的灵活性。
2025-08-26 22:12:46 +08:00

442 lines
13 KiB
TypeScript

import { Injectable, Logger, NotFoundException, BadRequestException } from '@nestjs/common';
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';
import { CreateGoalDto } from './dto/create-goal.dto';
import { UpdateGoalDto } from './dto/update-goal.dto';
import { GoalQueryDto } from './dto/goal-query.dto';
import { CreateGoalCompletionDto } from './dto/goal-completion.dto';
import { GoalTaskService } from './services/goal-task.service';
import * as dayjs from 'dayjs';
@Injectable()
export class GoalsService {
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,
@InjectConnection()
private readonly sequelize: Sequelize,
private readonly goalTaskService: GoalTaskService,
) { }
/**
* 创建目标
*/
async createGoal(userId: string, createGoalDto: CreateGoalDto): Promise<Goal> {
try {
this.logger.log(`createGoal: ${JSON.stringify(createGoalDto, null, 2)}`);
// 验证自定义重复规则
if (createGoalDto.repeatType === GoalRepeatType.CUSTOM && !createGoalDto.customRepeatRule) {
throw new BadRequestException('自定义重复类型必须提供自定义重复规则');
}
// 验证日期逻辑
if (createGoalDto.endDate && dayjs(createGoalDto.endDate).isBefore(createGoalDto.startDate)) {
throw new BadRequestException('结束日期不能早于开始日期');
}
const goal = await this.goalModel.create({
userId,
...createGoalDto,
startDate: createGoalDto.startDate ? new Date(createGoalDto.startDate) : undefined,
endDate: createGoalDto.endDate ? new Date(createGoalDto.endDate) : undefined,
startTime: createGoalDto.startTime ? createGoalDto.startTime : undefined,
endTime: createGoalDto.endTime ? createGoalDto.endTime : undefined,
});
this.logger.log(`用户 ${userId} 创建了目标: ${goal.title}`);
return goal;
} catch (error) {
this.logger.error(`创建目标失败: ${error.message}`);
throw error;
}
}
/**
* 获取用户的目标列表
*/
async getGoals(userId: string, query: GoalQueryDto) {
try {
// 惰性生成任务
await this.goalTaskService.generateTasksLazily(userId);
const { page = 1, pageSize = 20, status, repeatType, category, startDate, endDate, sortBy = 'createdAt', sortOrder = 'desc' } = query;
const offset = (page - 1) * pageSize;
// 构建查询条件
const where: WhereOptions = {
userId,
deleted: false,
};
if (status) {
where.status = status;
}
if (repeatType) {
where.repeatType = repeatType;
}
if (category) {
where.category = category;
}
if (startDate || endDate) {
where.startDate = {};
if (startDate) {
where.startDate[Op.gte] = new Date(startDate);
}
if (endDate) {
where.startDate[Op.lte] = new Date(endDate);
}
}
this.logger.log(`查询条件: ${JSON.stringify(where)}`);
// 构建排序条件
const order: Order = [[sortBy, sortOrder.toUpperCase()]];
const { rows: goals, count } = await this.goalModel.findAndCountAll({
where,
order,
offset,
limit: pageSize,
include: [
{
model: GoalCompletion,
as: 'completions',
where: { deleted: false },
required: false,
},
{
model: GoalTask,
as: 'tasks',
where: { deleted: false },
required: false,
limit: 5, // 只显示最近5个任务
order: [['startDate', 'DESC']],
},
],
});
return {
page,
pageSize,
total: count,
list: goals.map(goal => this.formatGoalResponse(goal)),
};
} catch (error) {
this.logger.error(`获取目标列表失败: ${error.message}`);
throw error;
}
}
/**
* 更新目标
*/
async updateGoal(userId: string, goalId: string, updateGoalDto: UpdateGoalDto): Promise<Goal> {
try {
this.logger.log(`updateGoal updateGoalDto: ${JSON.stringify(updateGoalDto, null, 2)}`);
const goal = await this.goalModel.findOne({
where: { id: goalId, userId, deleted: false },
});
if (!goal) {
throw new NotFoundException('目标不存在');
}
// 验证日期逻辑
if (updateGoalDto.endDate && updateGoalDto.startDate) {
if (dayjs(updateGoalDto.endDate).isBefore(updateGoalDto.startDate)) {
throw new BadRequestException('结束日期不能早于开始日期');
}
}
// 如果目标已完成,不允许修改
if (goal.status === GoalStatus.COMPLETED && updateGoalDto.status !== GoalStatus.COMPLETED) {
throw new BadRequestException('已完成的目标不能修改状态');
}
await goal.update({
...updateGoalDto,
endDate: updateGoalDto.endDate ? new Date(updateGoalDto.endDate) : undefined,
});
this.logger.log(`用户 ${userId} 更新了目标: ${goal.title}`);
return this.formatGoalResponse(goal);
} catch (error) {
this.logger.error(`更新目标失败: ${error.message}`);
throw error;
}
}
/**
* 删除目标
*/
async deleteGoal(userId: string, goalId: string): Promise<boolean> {
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 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 transaction.commit();
this.logger.log(`用户 ${userId} 删除了目标: ${goal.title}`);
return true;
} catch (error) {
// 回滚事务
await transaction.rollback();
this.logger.error(`删除目标失败: ${error.message}`);
throw error;
}
}
/**
* 记录目标完成
*/
async completeGoal(userId: string, createCompletionDto: CreateGoalCompletionDto): Promise<GoalCompletion> {
try {
const goal = await this.goalModel.findOne({
where: { id: createCompletionDto.goalId, userId, deleted: false },
});
if (!goal) {
throw new NotFoundException('目标不存在');
}
if (goal.status !== GoalStatus.ACTIVE) {
throw new BadRequestException('只有激活状态的目标才能记录完成');
}
const completionCount = createCompletionDto.completionCount || 1;
const completedAt = createCompletionDto.completedAt ? new Date(createCompletionDto.completedAt) : new Date();
// 创建完成记录
const completion = await this.goalCompletionModel.create({
goalId: createCompletionDto.goalId,
userId,
completedAt,
completionCount,
notes: createCompletionDto.notes,
});
// 更新目标的完成次数
const newCompletedCount = goal.completedCount + completionCount;
await goal.update({ completedCount: newCompletedCount });
// 检查是否达到目标总次数
if (goal.targetCount && newCompletedCount >= goal.targetCount) {
await goal.update({ status: GoalStatus.COMPLETED });
}
this.logger.log(`用户 ${userId} 完成了目标: ${goal.title}`);
return completion;
} catch (error) {
this.logger.error(`记录目标完成失败: ${error.message}`);
throw error;
}
}
/**
* 获取目标完成记录
*/
async getGoalCompletions(userId: string, goalId: string, query: any = {}) {
try {
const { page = 1, pageSize = 20, startDate, endDate } = query;
const offset = (page - 1) * pageSize;
// 验证目标存在
const goal = await this.goalModel.findOne({
where: { id: goalId, userId, deleted: false },
});
if (!goal) {
throw new NotFoundException('目标不存在');
}
// 构建查询条件
const where: WhereOptions = {
goalId,
userId,
deleted: false,
};
if (startDate || endDate) {
where.completedAt = {};
if (startDate) {
where.completedAt[Op.gte] = new Date(startDate);
}
if (endDate) {
where.completedAt[Op.lte] = new Date(endDate);
}
}
const { rows: completions, count } = await this.goalCompletionModel.findAndCountAll({
where,
order: [['completedAt', 'DESC']],
offset,
limit: pageSize,
include: [
{
model: Goal,
as: 'goal',
attributes: ['id', 'title', 'repeatType', 'frequency'],
},
],
});
return {
page,
pageSize,
total: count,
items: completions,
};
} catch (error) {
this.logger.error(`获取目标完成记录失败: ${error.message}`);
throw error;
}
}
/**
* 获取目标统计信息
*/
async getGoalStats(userId: string) {
try {
const goals = await this.goalModel.findAll({
where: { userId, deleted: false },
include: [
{
model: GoalCompletion,
as: 'completions',
where: { deleted: false },
required: false,
},
],
});
const stats = {
total: goals.length,
active: goals.filter(g => g.status === GoalStatus.ACTIVE).length,
completed: goals.filter(g => g.status === GoalStatus.COMPLETED).length,
paused: goals.filter(g => g.status === GoalStatus.PAUSED).length,
cancelled: goals.filter(g => g.status === GoalStatus.CANCELLED).length,
byCategory: {},
byRepeatType: {},
totalCompletions: 0,
thisWeekCompletions: 0,
thisMonthCompletions: 0,
};
const now = dayjs();
const weekStart = now.startOf('week');
const monthStart = now.startOf('month');
goals.forEach(goal => {
// 按分类统计
if (goal.category) {
stats.byCategory[goal.category] = (stats.byCategory[goal.category] || 0) + 1;
}
// 按重复类型统计
stats.byRepeatType[goal.repeatType] = (stats.byRepeatType[goal.repeatType] || 0) + 1;
// 统计完成次数
stats.totalCompletions += goal.completedCount;
// 统计本周和本月的完成次数
goal.completions?.forEach(completion => {
const completionDate = dayjs(completion.completedAt);
if (completionDate.isAfter(weekStart)) {
stats.thisWeekCompletions += completion.completionCount;
}
if (completionDate.isAfter(monthStart)) {
stats.thisMonthCompletions += completion.completionCount;
}
});
});
return stats;
} catch (error) {
this.logger.error(`获取目标统计失败: ${error.message}`);
throw error;
}
}
/**
* 格式化目标响应
*/
private formatGoalResponse(goal: Goal) {
const goalData = goal.toJSON();
// 计算进度百分比
if (goalData.targetCount) {
goalData.progressPercentage = Math.min(100, Math.round((goalData.completedCount / goalData.targetCount) * 100));
} else {
goalData.progressPercentage = 0;
}
// 计算剩余天数
if (goalData.endDate) {
const endDate = dayjs(goalData.endDate);
const now = dayjs();
goalData.daysRemaining = Math.max(0, endDate.diff(now, 'day'));
} else {
goalData.daysRemaining = null;
}
return goalData;
}
}