feat: 更新目标管理模块,优化数据库表结构和API逻辑
- 修改目标表和目标完成记录表的字段类型,增强数据一致性和查询性能。 - 移除不必要的搜索字段,简化目标查询DTO,提升查询效率。 - 引入目标状态枚举,增强代码可读性和维护性。 - 添加复合索引以优化查询性能,提升系统响应速度。 - 更新目标管理控制器和服务逻辑,确保与新数据库结构的兼容性。
This commit is contained in:
@@ -59,12 +59,21 @@ export class CreateGoalDto {
|
||||
customRepeatRule?: CustomRepeatRuleDto;
|
||||
|
||||
@IsDateString({}, { message: '开始日期格式无效' })
|
||||
startDate: string;
|
||||
@IsOptional()
|
||||
startDate?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString({}, { message: '结束日期格式无效' })
|
||||
endDate?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
startTime: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
endTime: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1, { message: '目标总次数必须大于0' })
|
||||
|
||||
@@ -12,9 +12,9 @@ export class GoalQueryDto {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(100)
|
||||
@Max(500)
|
||||
@Transform(({ value }) => parseInt(value))
|
||||
pageSize?: number = 20;
|
||||
pageSize?: number = 50;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(GoalStatus)
|
||||
@@ -28,10 +28,6 @@ export class GoalQueryDto {
|
||||
@IsString()
|
||||
category?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
search?: string; // 搜索标题和描述
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
startDate?: string; // 开始日期范围
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
Request,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
@@ -19,21 +18,24 @@ import { GoalQueryDto } from './dto/goal-query.dto';
|
||||
import { CreateGoalCompletionDto } from './dto/goal-completion.dto';
|
||||
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
|
||||
import { BaseResponseDto, ResponseCode } from '../base.dto';
|
||||
import { GoalStatus } from './models/goal.model';
|
||||
import { CurrentUser } from 'src/common/decorators/current-user.decorator';
|
||||
import { AccessTokenPayload } from 'src/users/services/apple-auth.service';
|
||||
|
||||
@Controller('goals')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class GoalsController {
|
||||
constructor(private readonly goalsService: GoalsService) {}
|
||||
constructor(private readonly goalsService: GoalsService) { }
|
||||
|
||||
/**
|
||||
* 创建目标
|
||||
*/
|
||||
@Post()
|
||||
async createGoal(
|
||||
@Request() req,
|
||||
@Body() createGoalDto: CreateGoalDto,
|
||||
@CurrentUser() user: AccessTokenPayload,
|
||||
): Promise<BaseResponseDto<any>> {
|
||||
const goal = await this.goalsService.createGoal(req.user.id, createGoalDto);
|
||||
const goal = await this.goalsService.createGoal(user.sub, createGoalDto);
|
||||
return {
|
||||
code: ResponseCode.SUCCESS,
|
||||
message: '目标创建成功',
|
||||
@@ -46,10 +48,10 @@ export class GoalsController {
|
||||
*/
|
||||
@Get()
|
||||
async getGoals(
|
||||
@Request() req,
|
||||
@Query() query: GoalQueryDto,
|
||||
@CurrentUser() user: AccessTokenPayload,
|
||||
): Promise<BaseResponseDto<any>> {
|
||||
const result = await this.goalsService.getGoals(req.user.id, query);
|
||||
const result = await this.goalsService.getGoals(user.sub, query);
|
||||
return {
|
||||
code: ResponseCode.SUCCESS,
|
||||
message: '获取目标列表成功',
|
||||
@@ -62,10 +64,10 @@ export class GoalsController {
|
||||
*/
|
||||
@Get(':id')
|
||||
async getGoal(
|
||||
@Request() req,
|
||||
@Param('id') id: string,
|
||||
@CurrentUser() user: AccessTokenPayload,
|
||||
): Promise<BaseResponseDto<any>> {
|
||||
const goal = await this.goalsService.getGoal(req.user.id, id);
|
||||
const goal = await this.goalsService.getGoal(user.sub, id);
|
||||
return {
|
||||
code: ResponseCode.SUCCESS,
|
||||
message: '获取目标详情成功',
|
||||
@@ -78,11 +80,11 @@ export class GoalsController {
|
||||
*/
|
||||
@Put(':id')
|
||||
async updateGoal(
|
||||
@Request() req,
|
||||
@Param('id') id: string,
|
||||
@Body() updateGoalDto: UpdateGoalDto,
|
||||
@CurrentUser() user: AccessTokenPayload,
|
||||
): Promise<BaseResponseDto<any>> {
|
||||
const goal = await this.goalsService.updateGoal(req.user.id, id, updateGoalDto);
|
||||
const goal = await this.goalsService.updateGoal(user.sub, id, updateGoalDto);
|
||||
return {
|
||||
code: ResponseCode.SUCCESS,
|
||||
message: '目标更新成功',
|
||||
@@ -96,10 +98,10 @@ export class GoalsController {
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async deleteGoal(
|
||||
@Request() req,
|
||||
@Param('id') id: string,
|
||||
@CurrentUser() user: AccessTokenPayload,
|
||||
): Promise<BaseResponseDto<boolean>> {
|
||||
const result = await this.goalsService.deleteGoal(req.user.id, id);
|
||||
const result = await this.goalsService.deleteGoal(user.sub, id);
|
||||
return {
|
||||
code: ResponseCode.SUCCESS,
|
||||
message: '目标删除成功',
|
||||
@@ -112,13 +114,13 @@ export class GoalsController {
|
||||
*/
|
||||
@Post(':id/complete')
|
||||
async completeGoal(
|
||||
@Request() req,
|
||||
@Param('id') id: string,
|
||||
@Body() createCompletionDto: CreateGoalCompletionDto,
|
||||
@CurrentUser() user: AccessTokenPayload,
|
||||
): Promise<BaseResponseDto<any>> {
|
||||
// 确保完成记录的目标ID与路径参数一致
|
||||
createCompletionDto.goalId = id;
|
||||
const completion = await this.goalsService.completeGoal(req.user.id, createCompletionDto);
|
||||
const completion = await this.goalsService.completeGoal(user.sub, createCompletionDto);
|
||||
return {
|
||||
code: ResponseCode.SUCCESS,
|
||||
message: '目标完成记录成功',
|
||||
@@ -131,11 +133,11 @@ export class GoalsController {
|
||||
*/
|
||||
@Get(':id/completions')
|
||||
async getGoalCompletions(
|
||||
@Request() req,
|
||||
@Param('id') id: string,
|
||||
@Query() query: any,
|
||||
@CurrentUser() user: AccessTokenPayload,
|
||||
): Promise<BaseResponseDto<any>> {
|
||||
const result = await this.goalsService.getGoalCompletions(req.user.id, id, query);
|
||||
const result = await this.goalsService.getGoalCompletions(user.sub, id, query);
|
||||
return {
|
||||
code: ResponseCode.SUCCESS,
|
||||
message: '获取目标完成记录成功',
|
||||
@@ -147,8 +149,10 @@ export class GoalsController {
|
||||
* 获取目标统计信息
|
||||
*/
|
||||
@Get('stats/overview')
|
||||
async getGoalStats(@Request() req): Promise<BaseResponseDto<any>> {
|
||||
const stats = await this.goalsService.getGoalStats(req.user.id);
|
||||
async getGoalStats(
|
||||
@CurrentUser() user: AccessTokenPayload,
|
||||
): Promise<BaseResponseDto<any>> {
|
||||
const stats = await this.goalsService.getGoalStats(user.sub);
|
||||
return {
|
||||
code: ResponseCode.SUCCESS,
|
||||
message: '获取目标统计成功',
|
||||
@@ -161,30 +165,30 @@ export class GoalsController {
|
||||
*/
|
||||
@Post('batch')
|
||||
async batchUpdateGoals(
|
||||
@Request() req,
|
||||
@Body() body: {
|
||||
goalIds: string[];
|
||||
action: 'pause' | 'resume' | 'complete' | 'delete';
|
||||
data?: any;
|
||||
},
|
||||
@CurrentUser() user: AccessTokenPayload,
|
||||
): Promise<BaseResponseDto<any>> {
|
||||
const { goalIds, action, data } = body;
|
||||
const results = [];
|
||||
const results: { goalId: string; success: boolean; error?: string }[] = [];
|
||||
|
||||
for (const goalId of goalIds) {
|
||||
try {
|
||||
switch (action) {
|
||||
case 'pause':
|
||||
await this.goalsService.updateGoal(req.user.id, goalId, { status: 'paused' });
|
||||
await this.goalsService.updateGoal(user.sub, goalId, { status: GoalStatus.PAUSED });
|
||||
break;
|
||||
case 'resume':
|
||||
await this.goalsService.updateGoal(req.user.id, goalId, { status: 'active' });
|
||||
await this.goalsService.updateGoal(user.sub, goalId, { status: GoalStatus.ACTIVE });
|
||||
break;
|
||||
case 'complete':
|
||||
await this.goalsService.updateGoal(req.user.id, goalId, { status: 'completed' });
|
||||
await this.goalsService.updateGoal(user.sub, goalId, { status: GoalStatus.COMPLETED });
|
||||
break;
|
||||
case 'delete':
|
||||
await this.goalsService.deleteGoal(req.user.id, goalId);
|
||||
await this.goalsService.deleteGoal(user.sub, goalId);
|
||||
break;
|
||||
}
|
||||
results.push({ goalId, success: true });
|
||||
|
||||
@@ -4,10 +4,12 @@ import { GoalsController } from './goals.controller';
|
||||
import { GoalsService } from './goals.service';
|
||||
import { Goal } from './models/goal.model';
|
||||
import { GoalCompletion } from './models/goal-completion.model';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
SequelizeModule.forFeature([Goal, GoalCompletion]),
|
||||
UsersModule,
|
||||
],
|
||||
controllers: [GoalsController],
|
||||
providers: [GoalsService],
|
||||
|
||||
@@ -32,6 +32,8 @@ export class GoalsService {
|
||||
...createGoalDto,
|
||||
startDate: new Date(createGoalDto.startDate),
|
||||
endDate: createGoalDto.endDate ? new Date(createGoalDto.endDate) : null,
|
||||
startTime: createGoalDto.startTime,
|
||||
endTime: createGoalDto.endTime,
|
||||
});
|
||||
|
||||
this.logger.log(`用户 ${userId} 创建了目标: ${goal.title}`);
|
||||
@@ -47,7 +49,7 @@ export class GoalsService {
|
||||
*/
|
||||
async getGoals(userId: string, query: GoalQueryDto) {
|
||||
try {
|
||||
const { page = 1, pageSize = 20, status, repeatType, category, search, startDate, endDate, sortBy = 'createdAt', sortOrder = 'desc' } = query;
|
||||
const { page = 1, pageSize = 20, status, repeatType, category, startDate, endDate, sortBy = 'createdAt', sortOrder = 'desc' } = query;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
// 构建查询条件
|
||||
@@ -68,13 +70,6 @@ export class GoalsService {
|
||||
where.category = category;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where[Op.or] = [
|
||||
{ title: { [Op.like]: `%${search}%` } },
|
||||
{ description: { [Op.like]: `%${search}%` } },
|
||||
];
|
||||
}
|
||||
|
||||
if (startDate || endDate) {
|
||||
where.startDate = {};
|
||||
if (startDate) {
|
||||
@@ -85,6 +80,8 @@ export class GoalsService {
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(`查询条件: ${JSON.stringify(where)}`);
|
||||
|
||||
// 构建排序条件
|
||||
const order: Order = [[sortBy, sortOrder.toUpperCase()]];
|
||||
|
||||
@@ -107,7 +104,7 @@ export class GoalsService {
|
||||
page,
|
||||
pageSize,
|
||||
total: count,
|
||||
items: goals.map(goal => this.formatGoalResponse(goal)),
|
||||
list: goals.map(goal => this.formatGoalResponse(goal)),
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`获取目标列表失败: ${error.message}`);
|
||||
@@ -390,7 +387,7 @@ export class GoalsService {
|
||||
*/
|
||||
private formatGoalResponse(goal: Goal) {
|
||||
const goalData = goal.toJSON();
|
||||
|
||||
|
||||
// 计算进度百分比
|
||||
if (goalData.targetCount) {
|
||||
goalData.progressPercentage = Math.min(100, Math.round((goalData.completedCount / goalData.targetCount) * 100));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Column, DataType, Model, Table, Index, ForeignKey, BelongsTo } from 'sequelize-typescript';
|
||||
import { Column, DataType, Model, Table, ForeignKey, BelongsTo } from 'sequelize-typescript';
|
||||
import { Goal } from './goal.model';
|
||||
|
||||
@Table({
|
||||
@@ -7,16 +7,15 @@ import { Goal } from './goal.model';
|
||||
})
|
||||
export class GoalCompletion extends Model {
|
||||
@Column({
|
||||
type: DataType.UUID,
|
||||
type: DataType.CHAR(36),
|
||||
defaultValue: DataType.UUIDV4,
|
||||
primaryKey: true,
|
||||
})
|
||||
declare id: string;
|
||||
|
||||
@Index
|
||||
@ForeignKey(() => Goal)
|
||||
@Column({
|
||||
type: DataType.UUID,
|
||||
type: DataType.CHAR(36),
|
||||
allowNull: false,
|
||||
comment: '目标ID',
|
||||
})
|
||||
@@ -25,9 +24,8 @@ export class GoalCompletion extends Model {
|
||||
@BelongsTo(() => Goal)
|
||||
declare goal: Goal;
|
||||
|
||||
@Index
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
type: DataType.STRING(255),
|
||||
allowNull: false,
|
||||
comment: '用户ID',
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Column, DataType, Model, Table, Index, HasMany } from 'sequelize-typescript';
|
||||
import { Column, DataType, Model, Table, HasMany } from 'sequelize-typescript';
|
||||
import { GoalCompletion } from './goal-completion.model';
|
||||
|
||||
export enum GoalRepeatType {
|
||||
@@ -21,22 +21,21 @@ export enum GoalStatus {
|
||||
})
|
||||
export class Goal extends Model {
|
||||
@Column({
|
||||
type: DataType.UUID,
|
||||
type: DataType.CHAR(36),
|
||||
defaultValue: DataType.UUIDV4,
|
||||
primaryKey: true,
|
||||
})
|
||||
declare id: string;
|
||||
|
||||
@Index
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
type: DataType.STRING(255),
|
||||
allowNull: false,
|
||||
comment: '用户ID',
|
||||
})
|
||||
declare userId: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
type: DataType.STRING(255),
|
||||
allowNull: false,
|
||||
comment: '目标标题',
|
||||
})
|
||||
@@ -73,19 +72,35 @@ export class Goal extends Model {
|
||||
declare customRepeatRule: any;
|
||||
|
||||
@Column({
|
||||
type: DataType.DATE,
|
||||
type: DataType.DATEONLY,
|
||||
allowNull: false,
|
||||
comment: '目标开始日期',
|
||||
})
|
||||
declare startDate: Date;
|
||||
|
||||
@Column({
|
||||
type: DataType.DATE,
|
||||
type: DataType.DATEONLY,
|
||||
allowNull: true,
|
||||
comment: '目标结束日期',
|
||||
})
|
||||
declare endDate: Date;
|
||||
|
||||
// 开始时间,分钟
|
||||
@Column({
|
||||
type: DataType.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '开始时间,分钟',
|
||||
})
|
||||
declare startTime: number;
|
||||
|
||||
// 结束时间,分钟
|
||||
@Column({
|
||||
type: DataType.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '结束时间,分钟',
|
||||
})
|
||||
declare endTime: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.ENUM('active', 'paused', 'completed', 'cancelled'),
|
||||
allowNull: false,
|
||||
@@ -110,7 +125,7 @@ export class Goal extends Model {
|
||||
declare targetCount: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
type: DataType.STRING(100),
|
||||
allowNull: true,
|
||||
comment: '目标分类标签',
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user