import { Inject, Injectable, NotFoundException } from '@nestjs/common'; import { InjectModel } from '@nestjs/sequelize'; import { TrainingPlan } from './models/training-plan.model'; import { CreateTrainingPlanDto, UpdateTrainingPlanDto } from './dto/training-plan.dto'; import { ActivityLogsService } from '../activity-logs/activity-logs.service'; import { ActivityActionType, ActivityEntityType } from '../activity-logs/models/activity-log.model'; import { Logger as WinstonLogger } from 'winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; @Injectable() export class TrainingPlansService { @Inject(WINSTON_MODULE_PROVIDER) private readonly winstonLogger: WinstonLogger; constructor( @InjectModel(TrainingPlan) private trainingPlanModel: typeof TrainingPlan, private readonly activityLogsService: ActivityLogsService, ) { } async create(userId: string, dto: CreateTrainingPlanDto) { const createdAt = new Date(); // 检查用户是否有其他激活的训练计划 const activePlans = await this.trainingPlanModel.findAll({ where: { userId, isActive: true, deleted: false } }); const plan = await this.trainingPlanModel.create({ userId, name: dto.name ?? '', createdAt, startDate: new Date(dto.startDate), mode: dto.mode, daysOfWeek: dto.daysOfWeek, sessionsPerWeek: dto.sessionsPerWeek, goal: dto.goal, startWeightKg: dto.startWeightKg ?? null, preferredTimeOfDay: dto.preferredTimeOfDay ?? '', isActive: activePlans.length === 0, }); this.winstonLogger.info(`create plan ${plan.id} for user ${userId} success`, { context: 'TrainingPlansService', userId, planId: plan.id, }); await this.activityLogsService.record({ userId, entityType: ActivityEntityType.TRAINING_PLAN, action: ActivityActionType.CREATE, entityId: plan.id, changes: plan.toJSON(), }); this.winstonLogger.info(`create plan ${plan.id} for user ${userId} success`, { context: 'TrainingPlansService', userId, planId: plan.id, }); return plan.toJSON(); } async remove(userId: string, id: string) { const [count] = await this.trainingPlanModel.update( { deleted: true }, { where: { id, userId, deleted: false } } ); this.winstonLogger.info(`remove plan ${id} for user ${userId}`, { context: 'TrainingPlansService', userId, id, }); if (count > 0) { await this.activityLogsService.record({ userId, entityType: ActivityEntityType.TRAINING_PLAN, action: ActivityActionType.DELETE, entityId: id, changes: null, }); this.winstonLogger.info(`remove plan ${id} for user ${userId} success`, { context: 'TrainingPlansService', userId, id, }); } return { success: count > 0 }; } async list(userId: string, page: number = 1, limit: number = 10) { const offset = (page - 1) * limit; const { rows, count } = await this.trainingPlanModel.findAndCountAll({ where: { userId, deleted: false }, order: [['created_at', 'DESC']], limit, offset, }); return { data: { list: rows, total: count, page, limit, }, }; } async detail(userId: string, id: string) { const plan = await this.trainingPlanModel.findOne({ where: { id, userId, deleted: false } }); if (!plan) throw new NotFoundException('训练计划不存在'); return plan.toJSON(); } async update(userId: string, id: string, dto: UpdateTrainingPlanDto) { const plan = await this.trainingPlanModel.findOne({ where: { id, userId, deleted: false } }); if (!plan) throw new NotFoundException('训练计划不存在'); this.winstonLogger.info(`update plan ${id} for user ${userId}`, { context: 'TrainingPlansService', userId, id, }); const before = plan.toJSON(); if (typeof dto.name !== 'undefined') plan.name = dto.name ?? ''; if (typeof dto.startDate !== 'undefined') plan.startDate = new Date(dto.startDate); if (typeof dto.mode !== 'undefined') plan.mode = dto.mode; if (typeof dto.daysOfWeek !== 'undefined') plan.daysOfWeek = dto.daysOfWeek as number[]; if (typeof dto.sessionsPerWeek !== 'undefined') plan.sessionsPerWeek = dto.sessionsPerWeek as number; if (typeof dto.goal !== 'undefined') plan.goal = dto.goal; if (typeof dto.startWeightKg !== 'undefined') plan.startWeightKg = dto.startWeightKg ?? null; if (typeof dto.preferredTimeOfDay !== 'undefined') plan.preferredTimeOfDay = dto.preferredTimeOfDay ?? ''; await plan.save(); const after = plan.toJSON(); const changedKeys = Object.keys(after).filter((key) => (before as any)[key] !== (after as any)[key]); const changes: Record = {}; for (const key of changedKeys) { changes[key] = { before: (before as any)[key], after: (after as any)[key] }; } await this.activityLogsService.record({ userId, entityType: ActivityEntityType.TRAINING_PLAN, action: ActivityActionType.UPDATE, entityId: id, changes, }); this.winstonLogger.info(`update plan ${id} for user ${userId} success`, { context: 'TrainingPlansService', userId, id, }); return after; } async activate(userId: string, id: string) { const plan = await this.trainingPlanModel.findOne({ where: { id, userId, deleted: false } }); if (!plan) throw new NotFoundException('训练计划不存在'); this.winstonLogger.info(`activate plan ${id} for user ${userId}`, { context: 'TrainingPlansService', userId, id, }); const transaction = await this.trainingPlanModel.sequelize?.transaction(); if (!transaction) throw new Error('Failed to start transaction'); try { // 用户是否有其他激活的训练计划 const activePlan = await this.trainingPlanModel.findAll({ where: { userId, isActive: true, deleted: false }, transaction }); if (activePlan.length > 0) { for (const p of activePlan) { p.isActive = false; await p.save({ transaction }); } } plan.isActive = true; await plan.save({ transaction }); await transaction.commit(); this.winstonLogger.info(`activate plan ${id} for user ${userId} success`, { context: 'TrainingPlansService', userId, id, }); return plan.toJSON(); } catch (error) { await transaction.rollback(); throw error; } } }