新增训练计划模块,包括控制器、服务、模型及数据传输对象,更新应用模块以引入新模块,同时在AI教练模块中添加体态评估功能,支持体重识别与更新,优化用户体重历史记录管理。

This commit is contained in:
richarjiang
2025-08-14 12:57:03 +08:00
parent 8c358a21f7
commit 24924e5d81
26 changed files with 935 additions and 5 deletions

View 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;
}

View 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;
}

View 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);
}
}

View 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 { }

View 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();
}
}