Files
plates-server/src/training-plans/training-plans.service.ts

198 lines
6.5 KiB
TypeScript

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<string, any> = {};
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;
}
}
}