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

- 创建普拉提分类和动作数据的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

@@ -1,22 +1,38 @@
export interface ExerciseLibraryItem {
key: string;
name: string;
description: string;
description?: string;
category: string; // 中文分类名
targetMuscleGroups: string;
equipmentName?: string;
beginnerReps?: number;
beginnerSets?: number;
breathingCycles?: number;
holdDuration?: number;
specialInstructions?: string;
}
export interface ExerciseCategoryDto {
key: string; // 英文 key
name: string; // 中文名
type: 'mat_pilates' | 'equipment_pilates';
equipmentName?: string;
sortOrder?: number;
}
export interface ExerciseDto {
key: string;
name: string;
description: string;
description?: string;
categoryKey: string;
categoryName: string;
targetMuscleGroups: string;
equipmentName?: string;
beginnerReps?: number;
beginnerSets?: number;
breathingCycles?: number;
holdDuration?: number;
specialInstructions?: string;
sortOrder?: number;
}

View File

@@ -2,14 +2,14 @@ import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { ExerciseCategory } from './models/exercise-category.model';
import { Exercise } from './models/exercise.model';
import { ExerciseConfigResponse, ExerciseLibraryItem } from './dto/exercise.dto';
import { ExerciseConfigResponse } from './dto/exercise.dto';
@Injectable()
export class ExercisesService {
constructor(
@InjectModel(ExerciseCategory) private readonly categoryModel: typeof ExerciseCategory,
@InjectModel(Exercise) private readonly exerciseModel: typeof Exercise,
) {}
) { }
async getConfig(): Promise<ExerciseConfigResponse> {
const [categories, exercises] = await Promise.all([
@@ -18,65 +18,11 @@ export class ExercisesService {
]);
return {
categories: categories.map((c) => ({ key: c.key, name: c.name, sortOrder: c.sortOrder })),
exercises: exercises.map((e) => ({
key: e.key,
name: e.name,
description: e.description,
categoryKey: e.categoryKey,
categoryName: e.categoryName,
sortOrder: e.sortOrder,
})),
categories: categories,
exercises,
};
}
async seedFromLibrary(library: ExerciseLibraryItem[]): Promise<void> {
const categoryNameToKey: Record<string, string> = {};
const uniqueCategoryNames = Array.from(new Set(library.map((i) => i.category)));
uniqueCategoryNames.forEach((name) => {
const key = this.slugifyCategory(name);
categoryNameToKey[name] = key;
});
await this.categoryModel.bulkCreate(
uniqueCategoryNames.map((name, index) => ({
key: categoryNameToKey[name],
name,
sortOrder: index,
})),
{ ignoreDuplicates: true },
);
await this.exerciseModel.bulkCreate(
library.map((item, index) => ({
key: item.key,
name: item.name,
description: item.description,
categoryKey: categoryNameToKey[item.category],
categoryName: item.category,
sortOrder: index,
})),
{ ignoreDuplicates: true },
);
}
private slugifyCategory(name: string): string {
const mapping: Record<string, string> = {
'核心与腹部': 'core',
'脊柱与后链': 'spine_posterior_chain',
'侧链与髋': 'lateral_hip',
'平衡与支撑': 'balance_support',
'进阶控制': 'advanced_control',
'柔韧与拉伸': 'mobility_stretch',
};
return mapping[name] || name
.normalize('NFKD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[^a-zA-Z0-9]+/g, '_')
.replace(/^_+|_+$/g, '')
.toLowerCase();
}
}

View File

@@ -20,6 +20,20 @@ export class ExerciseCategory extends Model {
})
declare name: string;
@Column({
type: DataType.ENUM('mat_pilates', 'equipment_pilates'),
allowNull: false,
comment: '普拉提类型:垫上普拉提或器械普拉提',
})
declare type: 'mat_pilates' | 'equipment_pilates';
@Column({
type: DataType.STRING,
allowNull: true,
comment: '器械名称(仅器械普拉提需要)',
})
declare equipmentName: string;
@Column({
type: DataType.INTEGER,
allowNull: false,

View File

@@ -13,15 +13,36 @@ export class Exercise extends Model {
})
declare key: string;
@Column({ type: DataType.STRING, allowNull: false, comment: '名称(含中英文)' })
@Column({ type: DataType.STRING, allowNull: false, comment: '动作名称' })
declare name: string;
@Column({ type: DataType.STRING, allowNull: false, comment: '中文分类名(冗余,便于展示)' })
declare categoryName: string;
@Column({ type: DataType.TEXT, allowNull: false, comment: '描述' })
@Column({ type: DataType.TEXT, allowNull: true, comment: '动作描述' })
declare description: string;
@Column({ type: DataType.TEXT, allowNull: false, comment: '主要锻炼肌肉群' })
declare targetMuscleGroups: string;
@Column({ type: DataType.STRING, allowNull: true, comment: '器械名称(器械普拉提专用)' })
declare equipmentName: string;
@Column({ type: DataType.INTEGER, allowNull: true, comment: '入门级别建议练习次数' })
declare beginnerReps: number;
@Column({ type: DataType.INTEGER, allowNull: true, comment: '入门级别建议组数' })
declare beginnerSets: number;
@Column({ type: DataType.INTEGER, allowNull: true, comment: '呼吸循环次数(替代普通次数)' })
declare breathingCycles: number;
@Column({ type: DataType.INTEGER, allowNull: true, comment: '保持时间(秒)' })
declare holdDuration: number;
@Column({ type: DataType.STRING, allowNull: true, comment: '特殊说明(如每侧、前后各等)' })
declare specialInstructions: string;
@ForeignKey(() => ExerciseCategory)
@Column({ type: DataType.STRING, allowNull: false, comment: '分类键' })
declare categoryKey: string;