diff --git a/sql-scripts/goals-tables-create.sql b/sql-scripts/goals-tables-create.sql index 9d3e604..656ca1f 100644 --- a/sql-scripts/goals-tables-create.sql +++ b/sql-scripts/goals-tables-create.sql @@ -1,83 +1,60 @@ -- 创建目标表 -CREATE TABLE IF NOT EXISTS t_goals ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id VARCHAR(255) NOT NULL, - title VARCHAR(255) NOT NULL, - description TEXT, - repeat_type VARCHAR(20) NOT NULL DEFAULT 'daily' CHECK (repeat_type IN ('daily', 'weekly', 'monthly', 'custom')), - frequency INTEGER NOT NULL DEFAULT 1 CHECK (frequency > 0 AND frequency <= 100), - custom_repeat_rule JSONB, - start_date DATE NOT NULL, - end_date DATE, - status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'paused', 'completed', 'cancelled')), - completed_count INTEGER NOT NULL DEFAULT 0, - target_count INTEGER CHECK (target_count > 0), - category VARCHAR(100), - priority INTEGER NOT NULL DEFAULT 0 CHECK (priority >= 0 AND priority <= 10), - has_reminder BOOLEAN NOT NULL DEFAULT false, - reminder_time TIME, - reminder_settings JSONB, - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - deleted BOOLEAN NOT NULL DEFAULT false -); +CREATE TABLE IF NOT EXISTS `t_goals` ( + `id` char(36) NOT NULL COMMENT '主键ID', + `user_id` varchar(255) NOT NULL COMMENT '用户ID', + `title` varchar(255) NOT NULL COMMENT '目标标题', + `description` text COMMENT '目标描述', + `repeat_type` enum('daily','weekly','monthly','custom') NOT NULL DEFAULT 'daily' COMMENT '重复周期类型:daily-每日,weekly-每周,monthly-每月,custom-自定义', + `frequency` int NOT NULL DEFAULT 1 COMMENT '频率(每天/每周/每月多少次)', + `custom_repeat_rule` json DEFAULT NULL COMMENT '自定义重复规则(如每周几)', + `start_date` date NOT NULL COMMENT '目标开始日期', + `end_date` date DEFAULT NULL COMMENT '目标结束日期', + `status` enum('active','paused','completed','cancelled') NOT NULL DEFAULT 'active' COMMENT '目标状态:active-激活,paused-暂停,completed-已完成,cancelled-已取消', + `completed_count` int NOT NULL DEFAULT 0 COMMENT '已完成次数', + `target_count` int DEFAULT NULL COMMENT '目标总次数(null表示无限制)', + `category` varchar(100) DEFAULT NULL COMMENT '目标分类标签', + `priority` int NOT NULL DEFAULT 0 COMMENT '优先级(数字越大优先级越高)', + `has_reminder` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否提醒', + `reminder_time` time DEFAULT NULL COMMENT '提醒时间', + `reminder_settings` json DEFAULT NULL COMMENT '提醒设置(如每周几提醒)', + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否已删除', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_status` (`status`), + KEY `idx_repeat_type` (`repeat_type`), + KEY `idx_category` (`category`), + KEY `idx_start_date` (`start_date`), + KEY `idx_deleted` (`deleted`), + KEY `idx_user_status` (`user_id`, `status`, `deleted`), + CONSTRAINT `chk_frequency` CHECK (`frequency` > 0 AND `frequency` <= 100), + CONSTRAINT `chk_priority` CHECK (`priority` >= 0 AND `priority` <= 10), + CONSTRAINT `chk_target_count` CHECK (`target_count` IS NULL OR `target_count` > 0) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户目标表'; -- 创建目标完成记录表 -CREATE TABLE IF NOT EXISTS t_goal_completions ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - goal_id UUID NOT NULL REFERENCES t_goals(id) ON DELETE CASCADE, - user_id VARCHAR(255) NOT NULL, - completed_at TIMESTAMP WITH TIME ZONE NOT NULL, - completion_count INTEGER NOT NULL DEFAULT 1 CHECK (completion_count > 0), - notes TEXT, - metadata JSONB, - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - deleted BOOLEAN NOT NULL DEFAULT false -); +CREATE TABLE IF NOT EXISTS `t_goal_completions` ( + `id` char(36) NOT NULL COMMENT '主键ID', + `goal_id` char(36) NOT NULL COMMENT '目标ID', + `user_id` varchar(255) NOT NULL COMMENT '用户ID', + `completed_at` datetime NOT NULL COMMENT '完成日期', + `completion_count` int NOT NULL DEFAULT 1 COMMENT '完成次数', + `notes` text COMMENT '完成备注', + `metadata` json DEFAULT NULL COMMENT '完成时的额外数据', + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否已删除', + PRIMARY KEY (`id`), + KEY `idx_goal_id` (`goal_id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_completed_at` (`completed_at`), + KEY `idx_deleted` (`deleted`), + KEY `idx_goal_completed` (`goal_id`, `completed_at`, `deleted`), + CONSTRAINT `fk_goal_completions_goal` FOREIGN KEY (`goal_id`) REFERENCES `t_goals` (`id`) ON DELETE CASCADE, + CONSTRAINT `chk_completion_count` CHECK (`completion_count` > 0) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='目标完成记录表'; --- 创建索引 -CREATE INDEX IF NOT EXISTS idx_goals_user_id ON t_goals(user_id); -CREATE INDEX IF NOT EXISTS idx_goals_status ON t_goals(status); -CREATE INDEX IF NOT EXISTS idx_goals_repeat_type ON t_goals(repeat_type); -CREATE INDEX IF NOT EXISTS idx_goals_category ON t_goals(category); -CREATE INDEX IF NOT EXISTS idx_goals_start_date ON t_goals(start_date); -CREATE INDEX IF NOT EXISTS idx_goals_deleted ON t_goals(deleted); - -CREATE INDEX IF NOT EXISTS idx_goal_completions_goal_id ON t_goal_completions(goal_id); -CREATE INDEX IF NOT EXISTS idx_goal_completions_user_id ON t_goal_completions(user_id); -CREATE INDEX IF NOT EXISTS idx_goal_completions_completed_at ON t_goal_completions(completed_at); -CREATE INDEX IF NOT EXISTS idx_goal_completions_deleted ON t_goal_completions(deleted); - --- 创建复合索引 -CREATE INDEX IF NOT EXISTS idx_goals_user_status ON t_goals(user_id, status); -CREATE INDEX IF NOT EXISTS idx_goal_completions_goal_completed ON t_goal_completions(goal_id, completed_at); - --- 添加注释 -COMMENT ON TABLE t_goals IS '用户目标表'; -COMMENT ON COLUMN t_goals.id IS '目标ID'; -COMMENT ON COLUMN t_goals.user_id IS '用户ID'; -COMMENT ON COLUMN t_goals.title IS '目标标题'; -COMMENT ON COLUMN t_goals.description IS '目标描述'; -COMMENT ON COLUMN t_goals.repeat_type IS '重复周期类型:daily-每日,weekly-每周,monthly-每月,custom-自定义'; -COMMENT ON COLUMN t_goals.frequency IS '频率(每天/每周/每月多少次)'; -COMMENT ON COLUMN t_goals.custom_repeat_rule IS '自定义重复规则(如每周几)'; -COMMENT ON COLUMN t_goals.start_date IS '目标开始日期'; -COMMENT ON COLUMN t_goals.end_date IS '目标结束日期'; -COMMENT ON COLUMN t_goals.status IS '目标状态:active-激活,paused-暂停,completed-已完成,cancelled-已取消'; -COMMENT ON COLUMN t_goals.completed_count IS '已完成次数'; -COMMENT ON COLUMN t_goals.target_count IS '目标总次数(null表示无限制)'; -COMMENT ON COLUMN t_goals.category IS '目标分类标签'; -COMMENT ON COLUMN t_goals.priority IS '优先级(数字越大优先级越高)'; -COMMENT ON COLUMN t_goals.has_reminder IS '是否提醒'; -COMMENT ON COLUMN t_goals.reminder_time IS '提醒时间'; -COMMENT ON COLUMN t_goals.reminder_settings IS '提醒设置(如每周几提醒)'; - -COMMENT ON TABLE t_goal_completions IS '目标完成记录表'; -COMMENT ON COLUMN t_goal_completions.id IS '完成记录ID'; -COMMENT ON COLUMN t_goal_completions.goal_id IS '目标ID'; -COMMENT ON COLUMN t_goal_completions.user_id IS '用户ID'; -COMMENT ON COLUMN t_goal_completions.completed_at IS '完成日期'; -COMMENT ON COLUMN t_goal_completions.completion_count IS '完成次数'; -COMMENT ON COLUMN t_goal_completions.notes IS '完成备注'; -COMMENT ON COLUMN t_goal_completions.metadata IS '完成时的额外数据'; +-- 创建额外的复合索引以优化查询性能 +CREATE INDEX IF NOT EXISTS `idx_goals_user_date` ON `t_goals` (`user_id`, `start_date`, `deleted`); +CREATE INDEX IF NOT EXISTS `idx_goal_completions_user_date` ON `t_goal_completions` (`user_id`, `completed_at`, `deleted`); \ No newline at end of file diff --git a/src/goals/dto/create-goal.dto.ts b/src/goals/dto/create-goal.dto.ts index bc72387..c674244 100644 --- a/src/goals/dto/create-goal.dto.ts +++ b/src/goals/dto/create-goal.dto.ts @@ -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' }) diff --git a/src/goals/dto/goal-query.dto.ts b/src/goals/dto/goal-query.dto.ts index 8c98fd0..9560560 100644 --- a/src/goals/dto/goal-query.dto.ts +++ b/src/goals/dto/goal-query.dto.ts @@ -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; // 开始日期范围 diff --git a/src/goals/goals.controller.ts b/src/goals/goals.controller.ts index 72c73e8..963c738 100644 --- a/src/goals/goals.controller.ts +++ b/src/goals/goals.controller.ts @@ -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> { - 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> { - 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> { - 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> { - 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> { - 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> { // 确保完成记录的目标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> { - 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> { - const stats = await this.goalsService.getGoalStats(req.user.id); + async getGoalStats( + @CurrentUser() user: AccessTokenPayload, + ): Promise> { + 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> { 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 }); diff --git a/src/goals/goals.module.ts b/src/goals/goals.module.ts index 0a8b9a3..0c34f1f 100644 --- a/src/goals/goals.module.ts +++ b/src/goals/goals.module.ts @@ -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], diff --git a/src/goals/goals.service.ts b/src/goals/goals.service.ts index a57ccf0..3a6b62b 100644 --- a/src/goals/goals.service.ts +++ b/src/goals/goals.service.ts @@ -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)); diff --git a/src/goals/models/goal-completion.model.ts b/src/goals/models/goal-completion.model.ts index fb7e1ee..912decc 100644 --- a/src/goals/models/goal-completion.model.ts +++ b/src/goals/models/goal-completion.model.ts @@ -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', }) diff --git a/src/goals/models/goal.model.ts b/src/goals/models/goal.model.ts index b070f60..cfc6dd0 100644 --- a/src/goals/models/goal.model.ts +++ b/src/goals/models/goal.model.ts @@ -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: '目标分类标签', })