feat:更新依赖项的源地址,将所有依赖的镜像地址更改为官方的Yarn注册表地址,并在应用模块中引入新的Exercises模块。
This commit is contained in:
28
src/exercises/dto/exercise.dto.ts
Normal file
28
src/exercises/dto/exercise.dto.ts
Normal 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[];
|
||||
}
|
||||
|
||||
|
||||
24
src/exercises/exercises.controller.ts
Normal file
24
src/exercises/exercises.controller.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
17
src/exercises/exercises.module.ts
Normal file
17
src/exercises/exercises.module.ts
Normal 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 {}
|
||||
|
||||
|
||||
82
src/exercises/exercises.service.ts
Normal file
82
src/exercises/exercises.service.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
41
src/exercises/models/exercise-category.model.ts
Normal file
41
src/exercises/models/exercise-category.model.ts
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
42
src/exercises/models/exercise.model.ts
Normal file
42
src/exercises/models/exercise.model.ts
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user