增强文章控制器的安全性,添加JWT身份验证守卫;优化训练计划服务,简化日志记录逻辑,确保使用计划创建训练会话时的准确性;更新训练会话模型,允许训练计划ID为可空字段。

This commit is contained in:
2025-08-16 13:42:36 +08:00
parent fafb618c32
commit 477f5b4b79
4 changed files with 47 additions and 39 deletions

View File

@@ -6,7 +6,7 @@ import { CreateArticleDto, QueryArticlesDto, CreateArticleResponseDto, QueryArti
@ApiTags('articles') @ApiTags('articles')
@Controller('articles') @Controller('articles')
@UseGuards(JwtAuthGuard)
export class ArticlesController { export class ArticlesController {
constructor(private readonly articlesService: ArticlesService) { } constructor(private readonly articlesService: ArticlesService) { }
@@ -15,12 +15,14 @@ export class ArticlesController {
@ApiOperation({ summary: '创建文章' }) @ApiOperation({ summary: '创建文章' })
@ApiBody({ type: CreateArticleDto }) @ApiBody({ type: CreateArticleDto })
@ApiResponse({ status: 200 }) @ApiResponse({ status: 200 })
@UseGuards(JwtAuthGuard)
async create(@Body() dto: CreateArticleDto): Promise<CreateArticleResponseDto> { async create(@Body() dto: CreateArticleDto): Promise<CreateArticleResponseDto> {
return this.articlesService.create(dto); return this.articlesService.create(dto);
} }
@Get('list') @Get('list')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@UseGuards(JwtAuthGuard)
@ApiOperation({ summary: '查询文章列表(分页)' }) @ApiOperation({ summary: '查询文章列表(分页)' })
async list(@Query() query: QueryArticlesDto): Promise<QueryArticlesResponseDto> { async list(@Query() query: QueryArticlesDto): Promise<QueryArticlesResponseDto> {
return this.articlesService.query(query); return this.articlesService.query(query);
@@ -36,6 +38,7 @@ export class ArticlesController {
// 增加阅读数 // 增加阅读数
@Post(':id/read-count') @Post(':id/read-count')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@UseGuards(JwtAuthGuard)
@ApiOperation({ summary: '增加文章阅读数' }) @ApiOperation({ summary: '增加文章阅读数' })
async increaseReadCount(@Param('id') id: string): Promise<CreateArticleResponseDto> { async increaseReadCount(@Param('id') id: string): Promise<CreateArticleResponseDto> {
return this.articlesService.increaseReadCount(id); return this.articlesService.increaseReadCount(id);

View File

@@ -17,14 +17,12 @@ export class TrainingPlansService {
) { } ) { }
async create(userId: string, dto: CreateTrainingPlanDto) { async create(userId: string, dto: CreateTrainingPlanDto) {
const id = `plan_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
const createdAt = new Date(); const createdAt = new Date();
// 检查用户是否有其他激活的训练计划 // 检查用户是否有其他激活的训练计划
const activePlans = await this.trainingPlanModel.findAll({ where: { userId, isActive: true, deleted: false } }); const activePlans = await this.trainingPlanModel.findAll({ where: { userId, isActive: true, deleted: false } });
const plan = await this.trainingPlanModel.create({ const plan = await this.trainingPlanModel.create({
id,
userId, userId,
name: dto.name ?? '', name: dto.name ?? '',
createdAt, createdAt,
@@ -37,10 +35,10 @@ export class TrainingPlansService {
preferredTimeOfDay: dto.preferredTimeOfDay ?? '', preferredTimeOfDay: dto.preferredTimeOfDay ?? '',
isActive: activePlans.length === 0, isActive: activePlans.length === 0,
}); });
this.winstonLogger.info(`create plan ${id} for user ${userId} success`, { this.winstonLogger.info(`create plan ${plan.id} for user ${userId} success`, {
context: 'TrainingPlansService', context: 'TrainingPlansService',
userId, userId,
id, planId: plan.id,
}); });
await this.activityLogsService.record({ await this.activityLogsService.record({
userId, userId,
@@ -49,10 +47,10 @@ export class TrainingPlansService {
entityId: plan.id, entityId: plan.id,
changes: plan.toJSON(), changes: plan.toJSON(),
}); });
this.winstonLogger.info(`create plan ${id} for user ${userId} success`, { this.winstonLogger.info(`create plan ${plan.id} for user ${userId} success`, {
context: 'TrainingPlansService', context: 'TrainingPlansService',
userId, userId,
id, planId: plan.id,
}); });
return plan.toJSON(); return plan.toJSON();
} }

View File

@@ -20,7 +20,7 @@ export class WorkoutSession extends Model {
declare userId: string; declare userId: string;
@ForeignKey(() => TrainingPlan) @ForeignKey(() => TrainingPlan)
@Column({ type: DataType.UUID, allowNull: false, comment: '关联的训练计划模板' }) @Column({ type: DataType.UUID, allowNull: true, comment: '关联的训练计划模板' })
declare trainingPlanId: string; declare trainingPlanId: string;
@BelongsTo(() => TrainingPlan) @BelongsTo(() => TrainingPlan)

View File

@@ -50,12 +50,10 @@ export class WorkoutsService {
dto.scheduledDate ? new Date(dto.scheduledDate) : new Date(), dto.scheduledDate ? new Date(dto.scheduledDate) : new Date(),
dto.name dto.name
); );
} else if (dto.customExercises && dto.customExercises.length > 0) { } else {
// 基于自定义动作创建 // 基于自定义动作创建
return this.createCustomWorkoutSession(userId, dto); return this.createCustomWorkoutSession(userId, dto);
} else { }
throw new BadRequestException('必须提供训练计划ID或自定义动作列表');
}
} }
/** /**
@@ -138,29 +136,31 @@ export class WorkoutsService {
}, { transaction }); }, { transaction });
// 2. 创建自定义动作 // 2. 创建自定义动作
for (const customExercise of dto.customExercises!) { if (dto.customExercises && dto.customExercises.length > 0) {
// 如果有exerciseKey验证动作是否存在 for (const customExercise of dto.customExercises!) {
if (customExercise.exerciseKey) { // 如果有exerciseKey,验证动作是否存在
const exercise = await this.exerciseModel.findByPk(customExercise.exerciseKey); if (customExercise.exerciseKey) {
if (!exercise) { const exercise = await this.exerciseModel.findByPk(customExercise.exerciseKey);
throw new NotFoundException(`动作 "${customExercise.exerciseKey}" 不存在`); if (!exercise) {
throw new NotFoundException(`动作 "${customExercise.exerciseKey}" 不存在`);
}
} }
}
await this.workoutExerciseModel.create({ await this.workoutExerciseModel.create({
workoutSessionId: workoutSession.id, workoutSessionId: workoutSession.id,
userId, userId,
exerciseKey: customExercise.exerciseKey, exerciseKey: customExercise.exerciseKey,
name: customExercise.name, name: customExercise.name,
plannedSets: customExercise.plannedSets, plannedSets: customExercise.plannedSets,
plannedReps: customExercise.plannedReps, plannedReps: customExercise.plannedReps,
plannedDurationSec: customExercise.plannedDurationSec, plannedDurationSec: customExercise.plannedDurationSec,
restSec: customExercise.restSec, restSec: customExercise.restSec,
note: customExercise.note || '', note: customExercise.note || '',
itemType: customExercise.itemType || 'exercise', itemType: customExercise.itemType || 'exercise',
status: 'pending', status: 'pending',
sortOrder: customExercise.sortOrder, sortOrder: customExercise.sortOrder,
}, { transaction }); }, { transaction });
}
} }
await transaction.commit(); await transaction.commit();
@@ -169,10 +169,10 @@ export class WorkoutsService {
context: 'WorkoutsService', context: 'WorkoutsService',
userId, userId,
workoutSessionId: workoutSession.id, workoutSessionId: workoutSession.id,
exerciseCount: dto.customExercises!.length, exerciseCount: dto.customExercises!.length,
}); });
return this.getWorkoutSessionDetail(userId, workoutSession.id); return workoutSession.toJSON();
} catch (error) { } catch (error) {
await transaction.rollback(); await transaction.rollback();
throw error; throw error;
@@ -262,6 +262,14 @@ export class WorkoutsService {
throw new BadRequestException('只能开始计划中的训练会话'); throw new BadRequestException('只能开始计划中的训练会话');
} }
// 是否有训练动作,没有的话提示添加
const exercises = await this.workoutExerciseModel.findAll({
where: { workoutSessionId: sessionId, deleted: false }
});
if (exercises.length === 0) {
throw new BadRequestException('请先添加训练动作');
}
const startTime = dto.startedAt ? new Date(dto.startedAt) : new Date(); const startTime = dto.startedAt ? new Date(dto.startedAt) : new Date();
session.startedAt = startTime; session.startedAt = startTime;
session.status = 'in_progress'; session.status = 'in_progress';
@@ -298,7 +306,6 @@ export class WorkoutsService {
include: [ include: [
{ {
model: TrainingPlan, model: TrainingPlan,
required: false,
attributes: ['id', 'name', 'goal'] attributes: ['id', 'name', 'goal']
} }
], ],
@@ -308,7 +315,7 @@ export class WorkoutsService {
}); });
return { return {
sessions: sessions.map(s => s.toJSON()), sessions,
pagination: { pagination: {
page, page,
limit, limit,