198 lines
6.5 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
|