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

实现目标管理的完整功能,包括数据库表设计、API接口、业务逻辑和文档说明。支持用户创建、管理和跟踪个人目标,提供增删改查操作及目标完成记录功能。
This commit is contained in:
2025-08-21 22:50:30 +08:00
parent f26d8e64c6
commit 270b59c599
12 changed files with 1555 additions and 0 deletions

412
src/goals/goals.service.ts Normal file
View File

@@ -0,0 +1,412 @@
import { Injectable, Logger, NotFoundException, BadRequestException } from '@nestjs/common';
import { Op, WhereOptions, Order } from 'sequelize';
import { Goal, GoalStatus, GoalRepeatType } from './models/goal.model';
import { GoalCompletion } from './models/goal-completion.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 * as dayjs from 'dayjs';
@Injectable()
export class GoalsService {
private readonly logger = new Logger(GoalsService.name);
/**
* 创建目标
*/
async createGoal(userId: string, createGoalDto: CreateGoalDto): Promise<Goal> {
try {
// 验证自定义重复规则
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 Goal.create({
userId,
...createGoalDto,
startDate: new Date(createGoalDto.startDate),
endDate: createGoalDto.endDate ? new Date(createGoalDto.endDate) : null,
});
this.logger.log(`用户 ${userId} 创建了目标: ${goal.title}`);
return goal;
} catch (error) {
this.logger.error(`创建目标失败: ${error.message}`);
throw error;
}
}
/**
* 获取用户的目标列表
*/
async getGoals(userId: string, query: GoalQueryDto) {
try {
const { page = 1, pageSize = 20, status, repeatType, category, search, 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 (search) {
where[Op.or] = [
{ title: { [Op.like]: `%${search}%` } },
{ description: { [Op.like]: `%${search}%` } },
];
}
if (startDate || endDate) {
where.startDate = {};
if (startDate) {
where.startDate[Op.gte] = new Date(startDate);
}
if (endDate) {
where.startDate[Op.lte] = new Date(endDate);
}
}
// 构建排序条件
const order: Order = [[sortBy, sortOrder.toUpperCase()]];
const { rows: goals, count } = await Goal.findAndCountAll({
where,
order,
offset,
limit: pageSize,
include: [
{
model: GoalCompletion,
as: 'completions',
where: { deleted: false },
required: false,
},
],
});
return {
page,
pageSize,
total: count,
items: goals.map(goal => this.formatGoalResponse(goal)),
};
} catch (error) {
this.logger.error(`获取目标列表失败: ${error.message}`);
throw error;
}
}
/**
* 获取单个目标详情
*/
async getGoal(userId: string, goalId: string): Promise<Goal> {
try {
const goal = await Goal.findOne({
where: { id: goalId, userId, deleted: false },
include: [
{
model: GoalCompletion,
as: 'completions',
where: { deleted: false },
required: false,
order: [['completedAt', 'DESC']],
limit: 10, // 只显示最近10次完成记录
},
],
});
if (!goal) {
throw new NotFoundException('目标不存在');
}
return this.formatGoalResponse(goal);
} catch (error) {
this.logger.error(`获取目标详情失败: ${error.message}`);
throw error;
}
}
/**
* 更新目标
*/
async updateGoal(userId: string, goalId: string, updateGoalDto: UpdateGoalDto): Promise<Goal> {
try {
const goal = await Goal.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,
startDate: updateGoalDto.startDate ? new Date(updateGoalDto.startDate) : goal.startDate,
endDate: updateGoalDto.endDate ? new Date(updateGoalDto.endDate) : goal.endDate,
});
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> {
try {
const goal = await Goal.findOne({
where: { id: goalId, userId, deleted: false },
});
if (!goal) {
throw new NotFoundException('目标不存在');
}
// 软删除目标
await goal.update({ deleted: true });
// 软删除相关的完成记录
await GoalCompletion.update(
{ deleted: true },
{ where: { goalId, userId } }
);
this.logger.log(`用户 ${userId} 删除了目标: ${goal.title}`);
return true;
} catch (error) {
this.logger.error(`删除目标失败: ${error.message}`);
throw error;
}
}
/**
* 记录目标完成
*/
async completeGoal(userId: string, createCompletionDto: CreateGoalCompletionDto): Promise<GoalCompletion> {
try {
const goal = await Goal.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 GoalCompletion.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 Goal.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 GoalCompletion.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 Goal.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;
}
}