新增训练计划模块,包括控制器、服务、模型及数据传输对象,更新应用模块以引入新模块,同时在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,37 @@
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Query, UseGuards } from '@nestjs/common';
import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
import { ArticlesService } from './articles.service';
import { CreateArticleDto, QueryArticlesDto, CreateArticleResponseDto, QueryArticlesResponseDto } from './dto/article.dto';
@ApiTags('articles')
@Controller('articles')
@UseGuards(JwtAuthGuard)
export class ArticlesController {
constructor(private readonly articlesService: ArticlesService) { }
@Post('create')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '创建文章' })
@ApiBody({ type: CreateArticleDto })
@ApiResponse({ status: 200 })
async create(@Body() dto: CreateArticleDto): Promise<CreateArticleResponseDto> {
return this.articlesService.create(dto);
}
@Get('list')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '查询文章列表(分页)' })
async list(@Query() query: QueryArticlesDto): Promise<QueryArticlesResponseDto> {
return this.articlesService.query(query);
}
@Get(':id')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '获取文章详情并增加阅读数' })
async getOne(@Param('id') id: string): Promise<CreateArticleResponseDto> {
return this.articlesService.getAndIncreaseReadCount(id);
}
}

View File

@@ -0,0 +1,16 @@
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { ArticlesService } from './articles.service';
import { ArticlesController } from './articles.controller';
import { Article } from './models/article.model';
import { UsersModule } from '../users/users.module';
@Module({
imports: [SequelizeModule.forFeature([Article]), UsersModule],
providers: [ArticlesService],
controllers: [ArticlesController],
exports: [ArticlesService],
})
export class ArticlesModule { }

View File

@@ -0,0 +1,61 @@
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { Op } from 'sequelize';
import { Article } from './models/article.model';
import { CreateArticleDto, QueryArticlesDto, ArticleVo } from './dto/article.dto';
import { ResponseCode } from '../base.dto';
@Injectable()
export class ArticlesService {
private readonly logger = new Logger(ArticlesService.name);
constructor(
@InjectModel(Article)
private readonly articleModel: typeof Article,
) { }
async create(dto: CreateArticleDto) {
const article = await this.articleModel.create({
title: dto.title,
publishedDate: dto.publishedDate as any,
htmlContent: dto.htmlContent,
});
return { code: ResponseCode.SUCCESS, message: 'success', data: article.toJSON() as ArticleVo };
}
async query(params: QueryArticlesDto) {
const page = Math.max(1, Number(params.page || 1));
const pageSize = Math.min(100, Math.max(1, Number(params.pageSize || 10)));
const where: any = {};
if (params.keyword) {
where.title = { [Op.like]: `%${params.keyword}%` };
}
if (params.startDate || params.endDate) {
where.publishedDate = {} as any;
if (params.startDate) (where.publishedDate as any)[Op.gte] = params.startDate as any;
if (params.endDate) (where.publishedDate as any)[Op.lte] = params.endDate as any;
}
const { rows, count } = await this.articleModel.findAndCountAll({
where,
order: [['publishedDate', 'DESC'], ['createdAt', 'DESC']],
offset: (page - 1) * pageSize,
limit: pageSize,
});
return {
code: ResponseCode.SUCCESS,
message: 'success',
data: { list: rows.map(r => r.toJSON() as ArticleVo), total: count, page, pageSize },
};
}
async getAndIncreaseReadCount(id: string) {
const article = await this.articleModel.findByPk(id);
if (!article) throw new NotFoundException('文章不存在');
article.readCount += 1;
await article.save();
return { code: ResponseCode.SUCCESS, message: 'success', data: article.toJSON() as ArticleVo };
}
}

View File

@@ -0,0 +1,43 @@
import { ApiProperty } from '@nestjs/swagger';
import { BaseResponseDto } from '../../base.dto';
export class CreateArticleDto {
@ApiProperty({ description: '标题' })
title!: string;
@ApiProperty({ description: '发布日期 YYYY-MM-DD' })
publishedDate!: string;
@ApiProperty({ description: 'HTML 富文本内容' })
htmlContent!: string;
}
export class QueryArticlesDto {
@ApiProperty({ required: false, description: '关键词(匹配标题)' })
keyword?: string;
@ApiProperty({ required: false, description: '起始日期 YYYY-MM-DD' })
startDate?: string;
@ApiProperty({ required: false, description: '结束日期 YYYY-MM-DD' })
endDate?: string;
@ApiProperty({ required: false, description: '分页页码从1开始', default: 1 })
page?: number = 1;
@ApiProperty({ required: false, description: '分页大小', default: 10 })
pageSize?: number = 10;
}
export interface ArticleVo {
id: string;
title: string;
publishedDate: string;
readCount: number;
htmlContent: string;
}
export type CreateArticleResponseDto = BaseResponseDto<ArticleVo>;
export type QueryArticlesResponseDto = BaseResponseDto<{ list: ArticleVo[]; total: number; page: number; pageSize: number }>;

View File

@@ -0,0 +1,57 @@
import { Column, Model, Table, DataType, Index } from 'sequelize-typescript';
@Table({
tableName: 't_articles',
underscored: true,
})
export class Article extends Model {
@Column({
type: DataType.UUID,
defaultValue: DataType.UUIDV4,
primaryKey: true,
})
declare id: string;
@Column({
type: DataType.STRING(200),
allowNull: false,
comment: '标题',
})
declare title: string;
@Column({
type: DataType.DATEONLY,
allowNull: false,
comment: '发布日期(仅日期)',
})
declare publishedDate: string; // YYYY-MM-DD
@Column({
type: DataType.INTEGER,
allowNull: false,
defaultValue: 0,
comment: '阅读数',
})
declare readCount: number;
@Column({
type: DataType.TEXT('long'),
allowNull: false,
comment: 'HTML 富文本内容',
})
declare htmlContent: string;
@Column({
type: DataType.DATE,
defaultValue: DataType.NOW,
})
declare createdAt: Date;
@Column({
type: DataType.DATE,
defaultValue: DataType.NOW,
})
declare updatedAt: Date;
}