feat: 新增饮食记录和分析功能
- 创建饮食记录相关的数据库模型、DTO和API接口,支持用户手动添加和AI视觉识别记录饮食。 - 实现饮食分析服务,提供营养分析和健康建议,优化AI教练服务以集成饮食分析功能。 - 更新用户控制器,添加饮食记录的增删查改接口,增强用户饮食管理体验。 - 提供详细的API使用指南和数据库创建脚本,确保功能的完整性和可用性。
This commit is contained in:
@@ -7,10 +7,13 @@ import {
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Put,
|
||||
Delete,
|
||||
Query,
|
||||
Logger,
|
||||
UseGuards,
|
||||
Inject,
|
||||
Req,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
@@ -22,6 +25,7 @@ import { ApiOperation, ApiBody, ApiResponse, ApiTags, ApiQuery } from '@nestjs/s
|
||||
import { UpdateUserDto, UpdateUserResponseDto } from './dto/update-user.dto';
|
||||
import { AppleLoginDto, AppleLoginResponseDto, RefreshTokenDto, RefreshTokenResponseDto } from './dto/apple-login.dto';
|
||||
import { DeleteAccountDto, DeleteAccountResponseDto } from './dto/delete-account.dto';
|
||||
import { CreateDietRecordDto, UpdateDietRecordDto, GetDietHistoryQueryDto, DietRecordResponseDto, DietHistoryResponseDto, DietAnalysisResponseDto } from './dto/diet-record.dto';
|
||||
import { GuestLoginDto, GuestLoginResponseDto, RefreshGuestTokenDto, RefreshGuestTokenResponseDto } from './dto/guest-login.dto';
|
||||
import { AppStoreServerNotificationDto, ProcessNotificationResponseDto } from './dto/app-store-notification.dto';
|
||||
import { RestorePurchaseDto, RestorePurchaseResponseDto } from './dto/restore-purchase.dto';
|
||||
@@ -235,4 +239,177 @@ export class UsersController {
|
||||
|
||||
return this.usersService.restorePurchase(restorePurchaseDto, user.sub, clientIp, userAgent);
|
||||
}
|
||||
|
||||
// ==================== 饮食记录相关接口 ====================
|
||||
|
||||
/**
|
||||
* 添加饮食记录
|
||||
*/
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post('diet-records')
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({ summary: '添加饮食记录' })
|
||||
@ApiBody({ type: CreateDietRecordDto })
|
||||
@ApiResponse({ status: 201, description: '成功添加饮食记录', type: DietRecordResponseDto })
|
||||
async addDietRecord(
|
||||
@Body() createDto: CreateDietRecordDto,
|
||||
@CurrentUser() user: AccessTokenPayload,
|
||||
): Promise<DietRecordResponseDto> {
|
||||
this.logger.log(`添加饮食记录 - 用户ID: ${user.sub}, 食物: ${createDto.foodName}`);
|
||||
return this.usersService.addDietRecord(user.sub, createDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取饮食记录历史
|
||||
*/
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('diet-records')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '获取饮食记录历史' })
|
||||
@ApiQuery({ name: 'startDate', required: false, description: '开始日期' })
|
||||
@ApiQuery({ name: 'endDate', required: false, description: '结束日期' })
|
||||
@ApiQuery({ name: 'mealType', required: false, description: '餐次类型' })
|
||||
@ApiQuery({ name: 'page', required: false, description: '页码' })
|
||||
@ApiQuery({ name: 'limit', required: false, description: '每页数量' })
|
||||
@ApiResponse({ status: 200, description: '成功获取饮食记录', type: DietHistoryResponseDto })
|
||||
async getDietHistory(
|
||||
@Query() query: GetDietHistoryQueryDto,
|
||||
@CurrentUser() user: AccessTokenPayload,
|
||||
): Promise<DietHistoryResponseDto> {
|
||||
this.logger.log(`获取饮食记录 - 用户ID: ${user.sub}`);
|
||||
return this.usersService.getDietHistory(user.sub, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新饮食记录
|
||||
*/
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Put('diet-records/:id')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '更新饮食记录' })
|
||||
@ApiBody({ type: UpdateDietRecordDto })
|
||||
@ApiResponse({ status: 200, description: '成功更新饮食记录', type: DietRecordResponseDto })
|
||||
async updateDietRecord(
|
||||
@Param('id') recordId: string,
|
||||
@Body() updateDto: UpdateDietRecordDto,
|
||||
@CurrentUser() user: AccessTokenPayload,
|
||||
): Promise<DietRecordResponseDto> {
|
||||
this.logger.log(`更新饮食记录 - 用户ID: ${user.sub}, 记录ID: ${recordId}`);
|
||||
return this.usersService.updateDietRecord(user.sub, parseInt(recordId), updateDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除饮食记录
|
||||
*/
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Delete('diet-records/:id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@ApiOperation({ summary: '删除饮食记录' })
|
||||
@ApiResponse({ status: 204, description: '成功删除饮食记录' })
|
||||
async deleteDietRecord(
|
||||
@Param('id') recordId: string,
|
||||
@CurrentUser() user: AccessTokenPayload,
|
||||
): Promise<void> {
|
||||
this.logger.log(`删除饮食记录 - 用户ID: ${user.sub}, 记录ID: ${recordId}`);
|
||||
const success = await this.usersService.deleteDietRecord(user.sub, parseInt(recordId));
|
||||
if (!success) {
|
||||
throw new NotFoundException('饮食记录不存在');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取营养汇总分析
|
||||
*/
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('nutrition-summary')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '获取最近饮食的营养汇总分析' })
|
||||
@ApiQuery({ name: 'mealCount', required: false, description: '分析最近几顿饮食,默认10顿' })
|
||||
@ApiResponse({ status: 200, description: '成功获取营养汇总', type: DietAnalysisResponseDto })
|
||||
async getNutritionSummary(
|
||||
@Query('mealCount') mealCount: string = '10',
|
||||
@CurrentUser() user: AccessTokenPayload,
|
||||
): Promise<DietAnalysisResponseDto> {
|
||||
this.logger.log(`获取营养汇总 - 用户ID: ${user.sub}, 分析${mealCount}顿饮食`);
|
||||
|
||||
const count = Math.min(20, Math.max(1, parseInt(mealCount) || 10));
|
||||
const nutritionSummary = await this.usersService.getRecentNutritionSummary(user.sub, count);
|
||||
|
||||
// 获取最近的饮食记录用于分析
|
||||
const recentRecords = await this.usersService.getDietHistory(user.sub, { limit: count });
|
||||
|
||||
// 简单的营养评分算法(可以后续优化)
|
||||
const nutritionScore = this.calculateNutritionScore(nutritionSummary);
|
||||
|
||||
// 生成基础建议(后续可以接入AI分析)
|
||||
const recommendations = this.generateBasicRecommendations(nutritionSummary);
|
||||
|
||||
return {
|
||||
nutritionSummary,
|
||||
recentRecords: recentRecords.records,
|
||||
healthAnalysis: '基于您最近的饮食记录,我将为您提供个性化的营养分析和健康建议。',
|
||||
nutritionScore,
|
||||
recommendations,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单的营养评分算法
|
||||
*/
|
||||
private calculateNutritionScore(summary: any): number {
|
||||
let score = 50; // 基础分数
|
||||
|
||||
// 基于热量是否合理调整分数
|
||||
const dailyCalories = summary.totalCalories / (summary.recordCount / 3); // 假设一天3餐
|
||||
if (dailyCalories >= 1500 && dailyCalories <= 2500) score += 20;
|
||||
else if (dailyCalories < 1200 || dailyCalories > 3000) score -= 20;
|
||||
|
||||
// 基于蛋白质摄入调整分数
|
||||
const dailyProtein = summary.totalProtein / (summary.recordCount / 3);
|
||||
if (dailyProtein >= 50 && dailyProtein <= 150) score += 15;
|
||||
else if (dailyProtein < 30) score -= 15;
|
||||
|
||||
// 基于膳食纤维调整分数
|
||||
const dailyFiber = summary.totalFiber / (summary.recordCount / 3);
|
||||
if (dailyFiber >= 25) score += 15;
|
||||
else if (dailyFiber < 10) score -= 10;
|
||||
|
||||
return Math.max(0, Math.min(100, score));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成基础营养建议
|
||||
*/
|
||||
private generateBasicRecommendations(summary: any): string[] {
|
||||
const recommendations: string[] = [];
|
||||
|
||||
const dailyCalories = summary.totalCalories / (summary.recordCount / 3);
|
||||
const dailyProtein = summary.totalProtein / (summary.recordCount / 3);
|
||||
const dailyFiber = summary.totalFiber / (summary.recordCount / 3);
|
||||
const dailySodium = summary.totalSodium / (summary.recordCount / 3);
|
||||
|
||||
if (dailyCalories < 1200) {
|
||||
recommendations.push('您的日均热量摄入偏低,建议适当增加营养密度高的食物。');
|
||||
} else if (dailyCalories > 2500) {
|
||||
recommendations.push('您的日均热量摄入偏高,建议控制portion size或选择低热量食物。');
|
||||
}
|
||||
|
||||
if (dailyProtein < 50) {
|
||||
recommendations.push('建议增加优质蛋白质摄入,如鸡胸肉、鱼类、豆制品等。');
|
||||
}
|
||||
|
||||
if (dailyFiber < 25) {
|
||||
recommendations.push('建议增加膳食纤维摄入,多吃蔬菜、水果和全谷物。');
|
||||
}
|
||||
|
||||
if (dailySodium > 2000) {
|
||||
recommendations.push('钠摄入偏高,建议减少加工食品和调味料的使用。');
|
||||
}
|
||||
|
||||
if (recommendations.length === 0) {
|
||||
recommendations.push('您的饮食结构相对均衡,继续保持良好的饮食习惯!');
|
||||
}
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user