feat:更新依赖项的源地址,将所有依赖的镜像地址更改为官方的Yarn注册表地址,并在应用模块中引入新的Exercises模块。

This commit is contained in:
2025-08-14 21:14:18 +08:00
parent 366debf13a
commit 4a77dc1b88
10 changed files with 326 additions and 25 deletions

View File

@@ -0,0 +1,28 @@
export interface ExerciseLibraryItem {
key: string;
name: string;
description: string;
category: string; // 中文分类名
}
export interface ExerciseCategoryDto {
key: string; // 英文 key
name: string; // 中文名
sortOrder?: number;
}
export interface ExerciseDto {
key: string;
name: string;
description: string;
categoryKey: string;
categoryName: string;
sortOrder?: number;
}
export interface ExerciseConfigResponse {
categories: ExerciseCategoryDto[];
exercises: ExerciseDto[];
}

View File

@@ -0,0 +1,24 @@
import { Controller, Get, HttpCode, HttpStatus, UseGuards } from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { Public } from '../common/decorators/public.decorator';
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
import { ExercisesService } from './exercises.service';
import { ExerciseConfigResponse } from './dto/exercise.dto';
@ApiTags('exercises')
@Controller('exercises')
@UseGuards(JwtAuthGuard)
export class ExercisesController {
constructor(private readonly exercisesService: ExercisesService) {}
@Get('config')
@Public()
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '获取动作分类与动作配置(公开)' })
@ApiResponse({ status: 200 })
async getConfig(): Promise<ExerciseConfigResponse> {
return this.exercisesService.getConfig();
}
}

View File

@@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { ExercisesController } from './exercises.controller';
import { ExercisesService } from './exercises.service';
import { ExerciseCategory } from './models/exercise-category.model';
import { Exercise } from './models/exercise.model';
import { UsersModule } from '../users/users.module';
@Module({
imports: [SequelizeModule.forFeature([ExerciseCategory, Exercise]), UsersModule],
controllers: [ExercisesController],
providers: [ExercisesService],
exports: [ExercisesService],
})
export class ExercisesModule {}

View File

@@ -0,0 +1,82 @@
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';
@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([
this.categoryModel.findAll({ order: [['sort_order', 'ASC']] }),
this.exerciseModel.findAll({ order: [['category_key', 'ASC'], ['sort_order', 'ASC']] }),
]);
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,
})),
};
}
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

@@ -0,0 +1,41 @@
import { Column, DataType, HasMany, Model, Table } from 'sequelize-typescript';
import { Exercise } from './exercise.model';
@Table({
tableName: 't_exercise_categories',
underscored: true,
})
export class ExerciseCategory extends Model {
@Column({
type: DataType.STRING,
primaryKey: true,
comment: '分类唯一键(英文/下划线)',
})
declare key: string;
@Column({
type: DataType.STRING,
allowNull: false,
comment: '分类中文名称',
})
declare name: string;
@Column({
type: DataType.INTEGER,
allowNull: false,
defaultValue: 0,
comment: '排序(升序)',
})
declare sortOrder: number;
@HasMany(() => Exercise, { foreignKey: 'categoryKey', sourceKey: 'key' })
declare exercises: Exercise[];
@Column({ type: DataType.DATE, defaultValue: DataType.NOW })
declare createdAt: Date;
@Column({ type: DataType.DATE, defaultValue: DataType.NOW })
declare updatedAt: Date;
}

View File

@@ -0,0 +1,42 @@
import { BelongsTo, Column, DataType, ForeignKey, Model, Table } from 'sequelize-typescript';
import { ExerciseCategory } from './exercise-category.model';
@Table({
tableName: 't_exercises',
underscored: true,
})
export class Exercise extends Model {
@Column({
type: DataType.STRING,
primaryKey: true,
comment: '动作唯一键(英文/下划线)',
})
declare key: string;
@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: '描述' })
declare description: string;
@ForeignKey(() => ExerciseCategory)
@Column({ type: DataType.STRING, allowNull: false, comment: '分类键' })
declare categoryKey: string;
@BelongsTo(() => ExerciseCategory, { foreignKey: 'categoryKey', targetKey: 'key' })
declare category: ExerciseCategory;
@Column({ type: DataType.INTEGER, allowNull: false, defaultValue: 0, comment: '排序(分类内)' })
declare sortOrder: number;
@Column({ type: DataType.DATE, defaultValue: DataType.NOW })
declare createdAt: Date;
@Column({ type: DataType.DATE, defaultValue: DataType.NOW })
declare updatedAt: Date;
}