新增普拉提训练系统的数据库结构和数据导入功能

- 创建普拉提分类和动作数据的SQL导入脚本,支持垫上普拉提和器械普拉提的分类管理
- 实现数据库结构迁移脚本,添加新字段以支持普拉提类型和器械名称
- 更新数据库升级总结文档,详细说明数据库结构变更和数据导入步骤
- 创建训练会话相关表,支持每日训练实例功能
- 引入训练会话管理模块,整合训练计划与实际训练会话的关系
This commit is contained in:
richarjiang
2025-08-15 15:34:11 +08:00
parent bea71af5d3
commit 0edcfdcae9
28 changed files with 2528 additions and 164 deletions

View File

@@ -17,7 +17,7 @@ export class ScheduleExercise extends Model {
declare id: string;
@ForeignKey(() => TrainingPlan)
@Column({ type: DataType.STRING, allowNull: false })
@Column({ type: DataType.UUID, allowNull: false })
declare trainingPlanId: string;
@BelongsTo(() => TrainingPlan)

View File

@@ -6,8 +6,7 @@ import { Exercise } from '../exercises/models/exercise.model';
import {
CreateScheduleExerciseDto,
UpdateScheduleExerciseDto,
UpdateScheduleExerciseOrderDto,
CompleteScheduleExerciseDto
UpdateScheduleExerciseOrderDto
} from './dto/schedule-exercise.dto';
import { ActivityLogsService } from '../activity-logs/activity-logs.service';
import { ActivityActionType, ActivityEntityType } from '../activity-logs/models/activity-log.model';
@@ -299,60 +298,7 @@ export class ScheduleExerciseService {
}
}
// 标记完成状态
async markComplete(userId: string, trainingPlanId: string, exerciseId: string, dto: CompleteScheduleExerciseDto) {
await this.validateTrainingPlan(userId, trainingPlanId);
const exercise = await this.scheduleExerciseModel.findOne({
where: { id: exerciseId, trainingPlanId, userId, deleted: false }
});
if (!exercise) {
throw new NotFoundException('训练项目不存在');
}
const before = exercise.completed;
exercise.completed = dto.completed;
await exercise.save();
await this.activityLogsService.record({
userId,
entityType: ActivityEntityType.TRAINING_PLAN,
action: ActivityActionType.UPDATE,
entityId: exerciseId,
changes: {
completed: { before, after: dto.completed }
},
});
this.winstonLogger.info(`标记训练项目完成状态 ${exerciseId}: ${dto.completed}`, {
context: 'ScheduleExerciseService',
userId,
trainingPlanId,
exerciseId,
completed: dto.completed,
});
return exercise.toJSON();
}
// 获取训练计划的完成统计
async getCompletionStats(userId: string, trainingPlanId: string) {
await this.validateTrainingPlan(userId, trainingPlanId);
const [total, completed] = await Promise.all([
this.scheduleExerciseModel.count({
where: { trainingPlanId, userId, deleted: false, itemType: 'exercise' }
}),
this.scheduleExerciseModel.count({
where: { trainingPlanId, userId, deleted: false, itemType: 'exercise', completed: true }
})
]);
return {
total,
completed,
percentage: total > 0 ? Math.round((completed / total) * 100) : 0
};
}
// 注意:训练计划是模板,不应该有完成状态
// 训练完成状态应该在 WorkoutSession 和 WorkoutExercise 中管理
// 如需标记完成状态,请使用 WorkoutsService
}

View File

@@ -7,7 +7,6 @@ import {
CreateScheduleExerciseDto,
UpdateScheduleExerciseDto,
UpdateScheduleExerciseOrderDto,
CompleteScheduleExerciseDto,
ScheduleExerciseResponseDto
} from './dto/schedule-exercise.dto';
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
@@ -153,29 +152,9 @@ export class TrainingPlansController {
return this.scheduleExerciseService.updateOrder(user.sub, trainingPlanId, dto);
}
@Put(':id/exercises/:exerciseId/complete')
@ApiOperation({ summary: '标记训练项目完成状态' })
@ApiParam({ name: 'id', description: '训练计划ID' })
@ApiParam({ name: 'exerciseId', description: '训练项目ID' })
@ApiBody({ type: CompleteScheduleExerciseDto })
async markExerciseComplete(
@CurrentUser() user: AccessTokenPayload,
@Param('id') trainingPlanId: string,
@Param('exerciseId') exerciseId: string,
@Body() dto: CompleteScheduleExerciseDto,
) {
return this.scheduleExerciseService.markComplete(user.sub, trainingPlanId, exerciseId, dto);
}
@Get(':id/exercises/stats/completion')
@ApiOperation({ summary: '获取训练计划完成统计' })
@ApiParam({ name: 'id', description: '训练计划ID' })
async getExerciseCompletionStats(
@CurrentUser() user: AccessTokenPayload,
@Param('id') trainingPlanId: string,
) {
return this.scheduleExerciseService.getCompletionStats(user.sub, trainingPlanId);
}
// 注意:训练计划是模板,不应该有完成状态
// 实际的训练完成状态应该在 WorkoutSession 中管理
// 如需完成训练,请使用 /workouts/sessions 相关接口
}