更新训练会话API文档和服务逻辑
- 修改训练会话文档,增加三种训练创建方式的详细说明,包括自动获取、基于计划手动创建和完全自定义创建。 - 在控制器中新增创建训练会话和向训练会话添加自定义动作的API,支持基于训练计划或自定义动作创建训练会话。 - 更新服务逻辑,支持创建自定义训练会话并添加自定义动作,增强训练会话管理的灵活性和用户体验。
This commit is contained in:
@@ -45,17 +45,69 @@ POST /training-plans/{planId}/exercises
|
|||||||
POST /training-plans/{planId}/activate
|
POST /training-plans/{planId}/activate
|
||||||
```
|
```
|
||||||
|
|
||||||
### 第二步:开始每日训练
|
### 第二步:开始训练(三种方式)
|
||||||
|
|
||||||
|
#### 方式一:自动获取今日训练(推荐)
|
||||||
```bash
|
```bash
|
||||||
# 1. 获取今日训练会话(如不存在则自动创建)
|
# 获取今日训练会话(如不存在则自动创建)
|
||||||
GET /workouts/today
|
GET /workouts/today
|
||||||
# 系统会自动基于激活的训练计划创建今日训练会话
|
# 系统会自动基于激活的训练计划创建今日训练会话
|
||||||
|
```
|
||||||
|
|
||||||
# 2. 开始训练会话(可选)
|
#### 方式二:基于训练计划手动创建
|
||||||
POST /workouts/sessions/{sessionId}/start
|
```bash
|
||||||
|
POST /workouts/sessions
|
||||||
{
|
{
|
||||||
"startedAt": "2024-01-15T09:00:00.000Z"
|
"trainingPlanId": "{planId}",
|
||||||
|
"name": "晚间训练",
|
||||||
|
"scheduledDate": "2024-01-15T19:00:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 方式三:创建完全自定义训练
|
||||||
|
```bash
|
||||||
|
POST /workouts/sessions
|
||||||
|
{
|
||||||
|
"name": "自定义核心训练",
|
||||||
|
"scheduledDate": "2024-01-15T15:00:00.000Z",
|
||||||
|
"customExercises": [
|
||||||
|
{
|
||||||
|
"exerciseKey": "plank",
|
||||||
|
"name": "平板支撑",
|
||||||
|
"plannedDurationSec": 60,
|
||||||
|
"itemType": "exercise",
|
||||||
|
"sortOrder": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "休息",
|
||||||
|
"plannedDurationSec": 30,
|
||||||
|
"itemType": "rest",
|
||||||
|
"sortOrder": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "自定义深蹲变式",
|
||||||
|
"plannedSets": 3,
|
||||||
|
"plannedReps": 20,
|
||||||
|
"note": "脚距离肩膀更宽",
|
||||||
|
"itemType": "exercise",
|
||||||
|
"sortOrder": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第三步:执行训练
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 开始训练会话(可选)
|
||||||
|
POST /workouts/sessions/{sessionId}/start
|
||||||
|
|
||||||
|
# 2. 动态添加动作(如果需要)
|
||||||
|
POST /workouts/sessions/{sessionId}/exercises
|
||||||
|
{
|
||||||
|
"name": "额外的拉伸",
|
||||||
|
"plannedDurationSec": 120,
|
||||||
|
"itemType": "exercise"
|
||||||
}
|
}
|
||||||
|
|
||||||
# 3. 开始特定动作
|
# 3. 开始特定动作
|
||||||
@@ -87,25 +139,27 @@ POST /workouts/sessions/{sessionId}/exercises/{exerciseId}/complete
|
|||||||
- 每日训练是独立的实例
|
- 每日训练是独立的实例
|
||||||
- 修改计划不影响历史训练记录
|
- 修改计划不影响历史训练记录
|
||||||
|
|
||||||
### 2. 自动化管理
|
### 2. 灵活的创建方式
|
||||||
- 客户端直接获取今日训练,系统自动创建
|
- 自动创建:基于激活计划的今日训练
|
||||||
- 所有动作完成后自动完成训练会话
|
- 计划创建:基于指定训练计划创建
|
||||||
- 无需手动管理会话生命周期
|
- 自定义创建:完全自定义的训练动作
|
||||||
|
|
||||||
### 3. 进度追踪
|
### 3. 进度追踪
|
||||||
- 每个训练会话都有完整的状态跟踪
|
- 每个训练会话都有完整的状态跟踪
|
||||||
- 支持详细的性能数据记录
|
- 支持详细的性能数据记录
|
||||||
- 可以分析训练趋势和进步情况
|
- 可以分析训练趋势和进步情况
|
||||||
|
|
||||||
### 4. 灵活性
|
### 4. 动态调整能力
|
||||||
- 支持训练中的临时调整
|
- 支持训练中动态添加动作
|
||||||
- 支持跳过或修改特定动作
|
- 支持跳过或修改特定动作
|
||||||
|
- 自动完成会话管理
|
||||||
- 自动计算训练统计数据
|
- 自动计算训练统计数据
|
||||||
|
|
||||||
## API 端点总览
|
## API 端点总览
|
||||||
|
|
||||||
### 训练会话管理
|
### 训练会话管理
|
||||||
- `GET /workouts/today` - 获取/自动创建今日训练会话 ⭐
|
- `GET /workouts/today` - 获取/自动创建今日训练会话 ⭐
|
||||||
|
- `POST /workouts/sessions` - 手动创建训练会话(支持基于计划或自定义动作)
|
||||||
- `GET /workouts/sessions` - 获取训练会话列表
|
- `GET /workouts/sessions` - 获取训练会话列表
|
||||||
- `GET /workouts/sessions/{id}` - 获取训练会话详情
|
- `GET /workouts/sessions/{id}` - 获取训练会话详情
|
||||||
- `POST /workouts/sessions/{id}/start` - 开始训练(可选)
|
- `POST /workouts/sessions/{id}/start` - 开始训练(可选)
|
||||||
@@ -113,6 +167,7 @@ POST /workouts/sessions/{sessionId}/exercises/{exerciseId}/complete
|
|||||||
- 注意:训练会话在所有动作完成后自动完成
|
- 注意:训练会话在所有动作完成后自动完成
|
||||||
|
|
||||||
### 训练动作管理
|
### 训练动作管理
|
||||||
|
- `POST /workouts/sessions/{id}/exercises` - 向训练会话添加自定义动作 ⭐
|
||||||
- `GET /workouts/sessions/{id}/exercises` - 获取训练动作列表
|
- `GET /workouts/sessions/{id}/exercises` - 获取训练动作列表
|
||||||
- `GET /workouts/sessions/{id}/exercises/{exerciseId}` - 获取动作详情
|
- `GET /workouts/sessions/{id}/exercises/{exerciseId}` - 获取动作详情
|
||||||
- `POST /workouts/sessions/{id}/exercises/{exerciseId}/start` - 开始动作
|
- `POST /workouts/sessions/{id}/exercises/{exerciseId}/start` - 开始动作
|
||||||
|
|||||||
@@ -2,7 +2,37 @@ import { ApiProperty, PartialType } from '@nestjs/swagger';
|
|||||||
import { IsArray, IsBoolean, IsDateString, IsEnum, IsInt, IsNotEmpty, IsOptional, IsString, IsUUID, Min } from 'class-validator';
|
import { IsArray, IsBoolean, IsDateString, IsEnum, IsInt, IsNotEmpty, IsOptional, IsString, IsUUID, Min } from 'class-validator';
|
||||||
import { WorkoutStatus } from '../models/workout-session.model';
|
import { WorkoutStatus } from '../models/workout-session.model';
|
||||||
|
|
||||||
// 注意:训练会话由系统自动创建,不需要手动创建DTO
|
export class CreateWorkoutSessionDto {
|
||||||
|
@ApiProperty({ description: '训练计划ID(基于训练计划创建时必填)', required: false })
|
||||||
|
@IsUUID()
|
||||||
|
@IsOptional()
|
||||||
|
trainingPlanId?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '训练会话名称' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '计划训练日期', required: false })
|
||||||
|
@IsDateString()
|
||||||
|
@IsOptional()
|
||||||
|
scheduledDate?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '自定义训练动作列表(自定义训练时使用)', required: false, type: 'array' })
|
||||||
|
@IsArray()
|
||||||
|
@IsOptional()
|
||||||
|
customExercises?: Array<{
|
||||||
|
exerciseKey?: string;
|
||||||
|
name: string;
|
||||||
|
plannedSets?: number;
|
||||||
|
plannedReps?: number;
|
||||||
|
plannedDurationSec?: number;
|
||||||
|
restSec?: number;
|
||||||
|
note?: string;
|
||||||
|
itemType?: 'exercise' | 'rest' | 'note';
|
||||||
|
sortOrder: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
export class StartWorkoutDto {
|
export class StartWorkoutDto {
|
||||||
@ApiProperty({ description: '实际开始时间', required: false })
|
@ApiProperty({ description: '实际开始时间', required: false })
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Body, Controller, Delete, Get, Param, Post, Query, UseGuards, Put } fro
|
|||||||
import { ApiBody, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
|
import { ApiBody, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
|
||||||
import { WorkoutsService } from './workouts.service';
|
import { WorkoutsService } from './workouts.service';
|
||||||
import {
|
import {
|
||||||
|
CreateWorkoutSessionDto,
|
||||||
StartWorkoutDto,
|
StartWorkoutDto,
|
||||||
UpdateWorkoutSessionDto,
|
UpdateWorkoutSessionDto,
|
||||||
WorkoutSessionResponseDto
|
WorkoutSessionResponseDto
|
||||||
@@ -26,7 +27,12 @@ export class WorkoutsController {
|
|||||||
|
|
||||||
// ==================== 训练会话管理 ====================
|
// ==================== 训练会话管理 ====================
|
||||||
|
|
||||||
// 注意:不提供手动创建会话接口,客户端应使用 GET /workouts/today 自动获取/创建
|
@Post('sessions')
|
||||||
|
@ApiOperation({ summary: '创建训练会话(支持基于训练计划或自定义动作)' })
|
||||||
|
@ApiBody({ type: CreateWorkoutSessionDto })
|
||||||
|
async createSession(@CurrentUser() user: AccessTokenPayload, @Body() dto: CreateWorkoutSessionDto) {
|
||||||
|
return this.workoutsService.createWorkoutSession(user.sub, dto);
|
||||||
|
}
|
||||||
|
|
||||||
@Get('sessions')
|
@Get('sessions')
|
||||||
@ApiOperation({ summary: '获取训练会话列表' })
|
@ApiOperation({ summary: '获取训练会话列表' })
|
||||||
@@ -69,6 +75,18 @@ export class WorkoutsController {
|
|||||||
|
|
||||||
// ==================== 训练动作管理 ====================
|
// ==================== 训练动作管理 ====================
|
||||||
|
|
||||||
|
@Post('sessions/:id/exercises')
|
||||||
|
@ApiOperation({ summary: '向训练会话添加自定义动作' })
|
||||||
|
@ApiParam({ name: 'id', description: '训练会话ID' })
|
||||||
|
@ApiBody({ type: CreateWorkoutExerciseDto })
|
||||||
|
async addExerciseToSession(
|
||||||
|
@CurrentUser() user: AccessTokenPayload,
|
||||||
|
@Param('id') sessionId: string,
|
||||||
|
@Body() dto: CreateWorkoutExerciseDto,
|
||||||
|
) {
|
||||||
|
return this.workoutsService.addExerciseToSession(user.sub, sessionId, dto);
|
||||||
|
}
|
||||||
|
|
||||||
@Get('sessions/:id/exercises')
|
@Get('sessions/:id/exercises')
|
||||||
@ApiOperation({ summary: '获取训练会话的所有动作' })
|
@ApiOperation({ summary: '获取训练会话的所有动作' })
|
||||||
@ApiParam({ name: 'id', description: '训练会话ID' })
|
@ApiParam({ name: 'id', description: '训练会话ID' })
|
||||||
|
|||||||
@@ -6,21 +6,18 @@ import { TrainingPlan } from '../training-plans/models/training-plan.model';
|
|||||||
import { ScheduleExercise } from '../training-plans/models/schedule-exercise.model';
|
import { ScheduleExercise } from '../training-plans/models/schedule-exercise.model';
|
||||||
import { Exercise } from '../exercises/models/exercise.model';
|
import { Exercise } from '../exercises/models/exercise.model';
|
||||||
import {
|
import {
|
||||||
|
CreateWorkoutSessionDto,
|
||||||
StartWorkoutDto,
|
StartWorkoutDto,
|
||||||
UpdateWorkoutSessionDto,
|
|
||||||
} from './dto/workout-session.dto';
|
} from './dto/workout-session.dto';
|
||||||
import {
|
import {
|
||||||
CreateWorkoutExerciseDto,
|
CreateWorkoutExerciseDto,
|
||||||
UpdateWorkoutExerciseDto,
|
UpdateWorkoutExerciseDto,
|
||||||
StartWorkoutExerciseDto,
|
StartWorkoutExerciseDto,
|
||||||
CompleteWorkoutExerciseDto,
|
CompleteWorkoutExerciseDto,
|
||||||
UpdateWorkoutExerciseOrderDto,
|
|
||||||
} from './dto/workout-exercise.dto';
|
} from './dto/workout-exercise.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 { Logger as WinstonLogger } from 'winston';
|
||||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||||
import { Op, Transaction } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WorkoutsService {
|
export class WorkoutsService {
|
||||||
@@ -37,11 +34,30 @@ export class WorkoutsService {
|
|||||||
private scheduleExerciseModel: typeof ScheduleExercise,
|
private scheduleExerciseModel: typeof ScheduleExercise,
|
||||||
@InjectModel(Exercise)
|
@InjectModel(Exercise)
|
||||||
private exerciseModel: typeof Exercise,
|
private exerciseModel: typeof Exercise,
|
||||||
private readonly activityLogsService: ActivityLogsService,
|
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
// ==================== 训练会话管理 ====================
|
// ==================== 训练会话管理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建训练会话(支持基于训练计划或自定义动作)
|
||||||
|
*/
|
||||||
|
async createWorkoutSession(userId: string, dto: CreateWorkoutSessionDto) {
|
||||||
|
if (dto.trainingPlanId) {
|
||||||
|
// 基于训练计划创建
|
||||||
|
return this.createWorkoutSessionFromPlan(
|
||||||
|
userId,
|
||||||
|
dto.trainingPlanId,
|
||||||
|
dto.scheduledDate ? new Date(dto.scheduledDate) : new Date(),
|
||||||
|
dto.name
|
||||||
|
);
|
||||||
|
} else if (dto.customExercises && dto.customExercises.length > 0) {
|
||||||
|
// 基于自定义动作创建
|
||||||
|
return this.createCustomWorkoutSession(userId, dto);
|
||||||
|
} else {
|
||||||
|
throw new BadRequestException('必须提供训练计划ID或自定义动作列表');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取今日训练会话,如果不存在则自动创建
|
* 获取今日训练会话,如果不存在则自动创建
|
||||||
*/
|
*/
|
||||||
@@ -100,10 +116,69 @@ export class WorkoutsService {
|
|||||||
return this.createWorkoutSessionFromPlan(userId, activeTrainingPlan.id, today);
|
return this.createWorkoutSessionFromPlan(userId, activeTrainingPlan.id, today);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建自定义训练会话
|
||||||
|
*/
|
||||||
|
private async createCustomWorkoutSession(userId: string, dto: CreateWorkoutSessionDto) {
|
||||||
|
const transaction = await this.workoutSessionModel.sequelize?.transaction();
|
||||||
|
if (!transaction) throw new Error('Failed to start transaction');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 创建训练会话
|
||||||
|
const workoutSession = await this.workoutSessionModel.create({
|
||||||
|
userId,
|
||||||
|
trainingPlanId: null, // 自定义训练不关联训练计划
|
||||||
|
name: dto.name,
|
||||||
|
scheduledDate: dto.scheduledDate ? new Date(dto.scheduledDate) : new Date(),
|
||||||
|
status: 'planned',
|
||||||
|
}, { transaction });
|
||||||
|
|
||||||
|
// 2. 创建自定义动作
|
||||||
|
for (const customExercise of dto.customExercises!) {
|
||||||
|
// 如果有exerciseKey,验证动作是否存在
|
||||||
|
if (customExercise.exerciseKey) {
|
||||||
|
const exercise = await this.exerciseModel.findByPk(customExercise.exerciseKey);
|
||||||
|
if (!exercise) {
|
||||||
|
throw new NotFoundException(`动作 "${customExercise.exerciseKey}" 不存在`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.workoutExerciseModel.create({
|
||||||
|
workoutSessionId: workoutSession.id,
|
||||||
|
userId,
|
||||||
|
exerciseKey: customExercise.exerciseKey,
|
||||||
|
name: customExercise.name,
|
||||||
|
plannedSets: customExercise.plannedSets,
|
||||||
|
plannedReps: customExercise.plannedReps,
|
||||||
|
plannedDurationSec: customExercise.plannedDurationSec,
|
||||||
|
restSec: customExercise.restSec,
|
||||||
|
note: customExercise.note || '',
|
||||||
|
itemType: customExercise.itemType || 'exercise',
|
||||||
|
status: 'pending',
|
||||||
|
sortOrder: customExercise.sortOrder,
|
||||||
|
}, { transaction });
|
||||||
|
}
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
|
||||||
|
this.winstonLogger.info(`创建自定义训练会话 ${workoutSession.id}`, {
|
||||||
|
context: 'WorkoutsService',
|
||||||
|
userId,
|
||||||
|
workoutSessionId: workoutSession.id,
|
||||||
|
exerciseCount: dto.customExercises!.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.getWorkoutSessionDetail(userId, workoutSession.id);
|
||||||
|
} catch (error) {
|
||||||
|
await transaction.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从训练计划创建训练会话(内部方法)
|
* 从训练计划创建训练会话(内部方法)
|
||||||
*/
|
*/
|
||||||
private async createWorkoutSessionFromPlan(userId: string, trainingPlanId: string, scheduledDate: Date) {
|
private async createWorkoutSessionFromPlan(userId: string, trainingPlanId: string, scheduledDate: Date, name?: string) {
|
||||||
const trainingPlan = await this.trainingPlanModel.findOne({
|
const trainingPlan = await this.trainingPlanModel.findOne({
|
||||||
where: { id: trainingPlanId, userId, deleted: false }
|
where: { id: trainingPlanId, userId, deleted: false }
|
||||||
});
|
});
|
||||||
@@ -120,7 +195,7 @@ export class WorkoutsService {
|
|||||||
const workoutSession = await this.workoutSessionModel.create({
|
const workoutSession = await this.workoutSessionModel.create({
|
||||||
userId,
|
userId,
|
||||||
trainingPlanId,
|
trainingPlanId,
|
||||||
name: trainingPlan.name || '今日训练',
|
name: name || trainingPlan.name || '今日训练',
|
||||||
scheduledDate,
|
scheduledDate,
|
||||||
status: 'planned',
|
status: 'planned',
|
||||||
}, { transaction });
|
}, { transaction });
|
||||||
@@ -158,14 +233,6 @@ export class WorkoutsService {
|
|||||||
workoutSessionId: workoutSession.id,
|
workoutSessionId: workoutSession.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.activityLogsService.record({
|
|
||||||
userId,
|
|
||||||
entityType: ActivityEntityType.WORKOUT,
|
|
||||||
action: ActivityActionType.CREATE,
|
|
||||||
entityId: workoutSession.id,
|
|
||||||
changes: workoutSession.toJSON(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.getWorkoutSessionDetail(userId, workoutSession.id);
|
return this.getWorkoutSessionDetail(userId, workoutSession.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
@@ -177,38 +244,41 @@ export class WorkoutsService {
|
|||||||
* 开始训练会话
|
* 开始训练会话
|
||||||
*/
|
*/
|
||||||
async startWorkoutSession(userId: string, sessionId: string, dto: StartWorkoutDto = {}) {
|
async startWorkoutSession(userId: string, sessionId: string, dto: StartWorkoutDto = {}) {
|
||||||
const session = await this.workoutSessionModel.findOne({
|
try {
|
||||||
where: { id: sessionId, userId, deleted: false }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!session) {
|
const session = await this.workoutSessionModel.findOne({
|
||||||
throw new NotFoundException('训练会话不存在');
|
where: { id: sessionId, userId, deleted: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
throw new NotFoundException('训练会话不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.status !== 'planned') {
|
||||||
|
throw new BadRequestException('只能开始计划中的训练会话');
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTime = dto.startedAt ? new Date(dto.startedAt) : new Date();
|
||||||
|
session.startedAt = startTime;
|
||||||
|
session.status = 'in_progress';
|
||||||
|
await session.save();
|
||||||
|
|
||||||
|
this.winstonLogger.info(`开始训练会话 ${sessionId}`, {
|
||||||
|
context: 'WorkoutsService',
|
||||||
|
userId,
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return session.toJSON();
|
||||||
|
} catch (error) {
|
||||||
|
this.winstonLogger.error(`开始训练会话失败 ${sessionId}`, {
|
||||||
|
context: 'WorkoutsService',
|
||||||
|
userId,
|
||||||
|
sessionId,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.status !== 'planned') {
|
|
||||||
throw new BadRequestException('只能开始计划中的训练会话');
|
|
||||||
}
|
|
||||||
|
|
||||||
const startTime = dto.startedAt ? new Date(dto.startedAt) : new Date();
|
|
||||||
session.startedAt = startTime;
|
|
||||||
session.status = 'in_progress';
|
|
||||||
await session.save();
|
|
||||||
|
|
||||||
this.winstonLogger.info(`开始训练会话 ${sessionId}`, {
|
|
||||||
context: 'WorkoutsService',
|
|
||||||
userId,
|
|
||||||
sessionId,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.activityLogsService.record({
|
|
||||||
userId,
|
|
||||||
entityType: ActivityEntityType.WORKOUT,
|
|
||||||
action: ActivityActionType.UPDATE,
|
|
||||||
entityId: sessionId,
|
|
||||||
changes: { status: { before: 'planned', after: 'in_progress' } },
|
|
||||||
});
|
|
||||||
|
|
||||||
return session.toJSON();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注意:训练会话现在自动完成,不需要手动完成方法
|
// 注意:训练会话现在自动完成,不需要手动完成方法
|
||||||
@@ -303,19 +373,61 @@ export class WorkoutsService {
|
|||||||
sessionId,
|
sessionId,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.activityLogsService.record({
|
|
||||||
userId,
|
|
||||||
entityType: ActivityEntityType.WORKOUT,
|
|
||||||
action: ActivityActionType.DELETE,
|
|
||||||
entityId: sessionId,
|
|
||||||
changes: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 训练动作管理 ====================
|
// ==================== 训练动作管理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向训练会话添加自定义动作
|
||||||
|
*/
|
||||||
|
async addExerciseToSession(userId: string, sessionId: string, dto: CreateWorkoutExerciseDto) {
|
||||||
|
const session = await this.validateWorkoutSession(userId, sessionId);
|
||||||
|
|
||||||
|
if (session.status === 'completed') {
|
||||||
|
throw new BadRequestException('已完成的训练会话无法添加动作');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有exerciseKey,验证动作是否存在
|
||||||
|
if (dto.exerciseKey) {
|
||||||
|
const exercise = await this.exerciseModel.findByPk(dto.exerciseKey);
|
||||||
|
if (!exercise) {
|
||||||
|
throw new NotFoundException(`动作 "${dto.exerciseKey}" 不存在`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取下一个排序顺序
|
||||||
|
const lastExercise = await this.workoutExerciseModel.findOne({
|
||||||
|
where: { workoutSessionId: sessionId, deleted: false },
|
||||||
|
order: [['sortOrder', 'DESC']],
|
||||||
|
});
|
||||||
|
const sortOrder = lastExercise ? lastExercise.sortOrder + 1 : 1;
|
||||||
|
|
||||||
|
const exercise = await this.workoutExerciseModel.create({
|
||||||
|
workoutSessionId: sessionId,
|
||||||
|
userId,
|
||||||
|
exerciseKey: dto.exerciseKey,
|
||||||
|
name: dto.name,
|
||||||
|
plannedSets: dto.plannedSets,
|
||||||
|
plannedReps: dto.plannedReps,
|
||||||
|
plannedDurationSec: dto.plannedDurationSec,
|
||||||
|
restSec: dto.restSec,
|
||||||
|
note: dto.note || '',
|
||||||
|
itemType: dto.itemType || 'exercise',
|
||||||
|
status: 'pending',
|
||||||
|
sortOrder,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.winstonLogger.info(`向训练会话添加动作 ${exercise.id}`, {
|
||||||
|
context: 'WorkoutsService',
|
||||||
|
userId,
|
||||||
|
sessionId,
|
||||||
|
exerciseId: exercise.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return exercise.toJSON();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 开始训练动作
|
* 开始训练动作
|
||||||
*/
|
*/
|
||||||
@@ -411,29 +523,10 @@ export class WorkoutsService {
|
|||||||
async updateWorkoutExercise(userId: string, sessionId: string, exerciseId: string, dto: UpdateWorkoutExerciseDto) {
|
async updateWorkoutExercise(userId: string, sessionId: string, exerciseId: string, dto: UpdateWorkoutExerciseDto) {
|
||||||
const exercise = await this.validateWorkoutExercise(userId, sessionId, exerciseId);
|
const exercise = await this.validateWorkoutExercise(userId, sessionId, exerciseId);
|
||||||
|
|
||||||
const before = exercise.toJSON();
|
|
||||||
|
|
||||||
// 更新字段
|
// 更新字段
|
||||||
Object.assign(exercise, dto);
|
Object.assign(exercise, dto);
|
||||||
await exercise.save();
|
await exercise.save();
|
||||||
|
|
||||||
const after = exercise.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] };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(changes).length > 0) {
|
|
||||||
await this.activityLogsService.record({
|
|
||||||
userId,
|
|
||||||
entityType: ActivityEntityType.WORKOUT,
|
|
||||||
action: ActivityActionType.UPDATE,
|
|
||||||
entityId: exerciseId,
|
|
||||||
changes,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.winstonLogger.info(`更新训练动作 ${exerciseId}`, {
|
this.winstonLogger.info(`更新训练动作 ${exerciseId}`, {
|
||||||
context: 'WorkoutsService',
|
context: 'WorkoutsService',
|
||||||
userId,
|
userId,
|
||||||
@@ -441,7 +534,7 @@ export class WorkoutsService {
|
|||||||
exerciseId,
|
exerciseId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return after;
|
return exercise.toJSON();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -575,17 +668,6 @@ export class WorkoutsService {
|
|||||||
sessionId,
|
sessionId,
|
||||||
duration: session.totalDurationSec,
|
duration: session.totalDurationSec,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.activityLogsService.record({
|
|
||||||
userId,
|
|
||||||
entityType: ActivityEntityType.WORKOUT,
|
|
||||||
action: ActivityActionType.UPDATE,
|
|
||||||
entityId: sessionId,
|
|
||||||
changes: {
|
|
||||||
status: { before: 'in_progress', after: 'completed' },
|
|
||||||
autoCompleted: true
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user