新增训练计划模块,包括控制器、服务、模型及数据传输对象,更新应用模块以引入新模块,同时在AI教练模块中添加体态评估功能,支持体重识别与更新,优化用户体重历史记录管理。
This commit is contained in:
46
src/training-plans/dto/training-plan.dto.ts
Normal file
46
src/training-plans/dto/training-plan.dto.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsArray, IsDateString, IsEnum, IsInt, IsNotEmpty, IsNumber, IsOptional, IsString, Max, Min, ValidateIf } from 'class-validator';
|
||||
import { PlanGoal, PlanMode } from '../models/training-plan.model';
|
||||
|
||||
export class CreateTrainingPlanDto {
|
||||
@ApiProperty({ description: '开始日期(ISO)' })
|
||||
@IsDateString()
|
||||
startDate: string;
|
||||
|
||||
@ApiProperty({ enum: ['daysOfWeek', 'sessionsPerWeek'] })
|
||||
@IsEnum(['daysOfWeek', 'sessionsPerWeek'])
|
||||
mode: PlanMode;
|
||||
|
||||
@ApiProperty({ type: [Number], description: '按周几训练(0-6)', required: true })
|
||||
@IsArray()
|
||||
daysOfWeek: number[];
|
||||
|
||||
@ApiProperty({ description: '每周训练次数(1-7)' })
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(7)
|
||||
sessionsPerWeek: number;
|
||||
|
||||
@ApiProperty({ enum: ['postpartum_recovery', 'fat_loss', 'posture_correction', 'core_strength', 'flexibility', 'rehab', 'stress_relief', ''] })
|
||||
@IsString()
|
||||
goal: PlanGoal;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
startWeightKg?: number;
|
||||
|
||||
@ApiProperty({ enum: ['morning', 'noon', 'evening', ''], required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
preferredTimeOfDay?: 'morning' | 'noon' | 'evening' | '';
|
||||
}
|
||||
|
||||
export class TrainingPlanSummaryDto {
|
||||
@ApiProperty() id: string;
|
||||
@ApiProperty() createdAt: string;
|
||||
@ApiProperty() startDate: string;
|
||||
@ApiProperty() goal: string;
|
||||
}
|
||||
|
||||
|
||||
49
src/training-plans/models/training-plan.model.ts
Normal file
49
src/training-plans/models/training-plan.model.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Column, DataType, Index, Model, PrimaryKey, Table } from 'sequelize-typescript';
|
||||
|
||||
export type PlanMode = 'daysOfWeek' | 'sessionsPerWeek';
|
||||
export type PlanGoal = 'postpartum_recovery' | 'fat_loss' | 'posture_correction' | 'core_strength' | 'flexibility' | 'rehab' | 'stress_relief' | '';
|
||||
|
||||
@Table({
|
||||
tableName: 't_training_plans',
|
||||
underscored: true,
|
||||
})
|
||||
export class TrainingPlan extends Model {
|
||||
@PrimaryKey
|
||||
@Column({ type: DataType.STRING })
|
||||
declare id: string;
|
||||
|
||||
@Column({ type: DataType.STRING, allowNull: false })
|
||||
declare userId: string;
|
||||
|
||||
@Column({ type: DataType.DATE, allowNull: false })
|
||||
declare createdAt: Date;
|
||||
|
||||
@Column({ type: DataType.DATE, allowNull: false })
|
||||
declare startDate: Date;
|
||||
|
||||
@Column({ type: DataType.ENUM('daysOfWeek', 'sessionsPerWeek'), allowNull: false })
|
||||
declare mode: PlanMode;
|
||||
|
||||
@Column({ type: DataType.JSON, allowNull: false, comment: '0-6' })
|
||||
declare daysOfWeek: number[];
|
||||
|
||||
@Column({ type: DataType.INTEGER, allowNull: false })
|
||||
declare sessionsPerWeek: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.ENUM('postpartum_recovery', 'fat_loss', 'posture_correction', 'core_strength', 'flexibility', 'rehab', 'stress_relief', ''),
|
||||
allowNull: false,
|
||||
})
|
||||
declare goal: PlanGoal;
|
||||
|
||||
@Column({ type: DataType.FLOAT, allowNull: true })
|
||||
declare startWeightKg: number | null;
|
||||
|
||||
@Column({ type: DataType.ENUM('morning', 'noon', 'evening', ''), allowNull: false, defaultValue: '' })
|
||||
declare preferredTimeOfDay: 'morning' | 'noon' | 'evening' | '';
|
||||
|
||||
@Column({ type: DataType.DATE, defaultValue: DataType.NOW })
|
||||
declare updatedAt: Date;
|
||||
}
|
||||
|
||||
|
||||
43
src/training-plans/training-plans.controller.ts
Normal file
43
src/training-plans/training-plans.controller.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Body, Controller, Delete, Get, Param, Post, UseGuards } from '@nestjs/common';
|
||||
import { ApiBody, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
|
||||
import { TrainingPlansService } from './training-plans.service';
|
||||
import { CreateTrainingPlanDto } from './dto/training-plan.dto';
|
||||
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
|
||||
import { CurrentUser } from '../common/decorators/current-user.decorator';
|
||||
import { AccessTokenPayload } from '../users/services/apple-auth.service';
|
||||
|
||||
@ApiTags('training-plans')
|
||||
@Controller('training-plans')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class TrainingPlansController {
|
||||
constructor(private readonly service: TrainingPlansService) { }
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: '新增训练计划' })
|
||||
@ApiBody({ type: CreateTrainingPlanDto })
|
||||
async create(@CurrentUser() user: AccessTokenPayload, @Body() dto: CreateTrainingPlanDto) {
|
||||
return this.service.create(user.sub, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: '删除训练计划' })
|
||||
@ApiParam({ name: 'id' })
|
||||
async remove(@CurrentUser() user: AccessTokenPayload, @Param('id') id: string) {
|
||||
return this.service.remove(user.sub, id);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: '训练计划列表' })
|
||||
async list(@CurrentUser() user: AccessTokenPayload) {
|
||||
return this.service.list(user.sub);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: '训练计划详情' })
|
||||
@ApiParam({ name: 'id' })
|
||||
async detail(@CurrentUser() user: AccessTokenPayload, @Param('id') id: string) {
|
||||
return this.service.detail(user.sub, id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
19
src/training-plans/training-plans.module.ts
Normal file
19
src/training-plans/training-plans.module.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SequelizeModule } from '@nestjs/sequelize';
|
||||
import { TrainingPlansService } from './training-plans.service';
|
||||
import { TrainingPlansController } from './training-plans.controller';
|
||||
import { TrainingPlan } from './models/training-plan.model';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
UsersModule,
|
||||
SequelizeModule.forFeature([TrainingPlan]),
|
||||
],
|
||||
controllers: [TrainingPlansController],
|
||||
providers: [TrainingPlansService],
|
||||
exports: [TrainingPlansService],
|
||||
})
|
||||
export class TrainingPlansModule { }
|
||||
|
||||
|
||||
53
src/training-plans/training-plans.service.ts
Normal file
53
src/training-plans/training-plans.service.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/sequelize';
|
||||
import { TrainingPlan } from './models/training-plan.model';
|
||||
import { CreateTrainingPlanDto } from './dto/training-plan.dto';
|
||||
|
||||
@Injectable()
|
||||
export class TrainingPlansService {
|
||||
constructor(
|
||||
@InjectModel(TrainingPlan)
|
||||
private trainingPlanModel: typeof TrainingPlan,
|
||||
) { }
|
||||
|
||||
async create(userId: string, dto: CreateTrainingPlanDto) {
|
||||
const id = `plan_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
const createdAt = new Date();
|
||||
const plan = await this.trainingPlanModel.create({
|
||||
id,
|
||||
userId,
|
||||
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 ?? '',
|
||||
});
|
||||
return plan.toJSON();
|
||||
}
|
||||
|
||||
async remove(userId: string, id: string) {
|
||||
const count = await this.trainingPlanModel.destroy({ where: { id, userId } });
|
||||
return { success: count > 0 };
|
||||
}
|
||||
|
||||
async list(userId: string) {
|
||||
const rows = await this.trainingPlanModel.findAll({ where: { userId }, order: [['created_at', 'DESC']] });
|
||||
return rows.map(r => ({
|
||||
id: r.id,
|
||||
createdAt: r.createdAt,
|
||||
startDate: r.startDate,
|
||||
goal: r.goal,
|
||||
}));
|
||||
}
|
||||
|
||||
async detail(userId: string, id: string) {
|
||||
const plan = await this.trainingPlanModel.findOne({ where: { id, userId } });
|
||||
if (!plan) throw new NotFoundException('训练计划不存在');
|
||||
return plan.toJSON();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user