- 在updateGoal方法中添加日志记录,便于调试和监控目标更新过程。 - 优化目标更新时的日期处理逻辑,确保endDate字段在未提供时设置为undefined,提升数据的灵活性。
442 lines
13 KiB
TypeScript
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;
|
|
}
|
|
}
|