feat: 新增饮食记录和分析功能

- 创建饮食记录相关的数据库模型、DTO和API接口,支持用户手动添加和AI视觉识别记录饮食。
- 实现饮食分析服务,提供营养分析和健康建议,优化AI教练服务以集成饮食分析功能。
- 更新用户控制器,添加饮食记录的增删查改接口,增强用户饮食管理体验。
- 提供详细的API使用指南和数据库创建脚本,确保功能的完整性和可用性。
This commit is contained in:
richarjiang
2025-08-18 16:27:01 +08:00
parent 3d36ee90f0
commit 485ba1f67c
19 changed files with 2031 additions and 52 deletions

View File

@@ -7,6 +7,7 @@ import { AiConversation } from './models/ai-conversation.model';
import { PostureAssessment } from './models/posture-assessment.model';
import { UserProfile } from '../users/models/user-profile.model';
import { UsersService } from '../users/users.service';
import { DietAnalysisService, DietAnalysisResult } from './services/diet-analysis.service';
const SYSTEM_PROMPT = `作为一名资深的健康管家兼营养分析师Nutrition Analyst和健身教练我拥有丰富的专业知识包括但不限于
@@ -86,6 +87,8 @@ interface CommandResult {
cleanText: string;
}
@Injectable()
export class AiCoachService {
private readonly logger = new Logger(AiCoachService.name);
@@ -93,7 +96,11 @@ export class AiCoachService {
private readonly model: string;
private readonly visionModel: string;
constructor(private readonly configService: ConfigService, private readonly usersService: UsersService) {
constructor(
private readonly configService: ConfigService,
private readonly usersService: UsersService,
private readonly dietAnalysisService: DietAnalysisService,
) {
const dashScopeApiKey = this.configService.get<string>('DASHSCOPE_API_KEY') || 'sk-e3ff4494c2f1463a8910d5b3d05d3143';
const baseURL = this.configService.get<string>('DASHSCOPE_BASE_URL') || 'https://dashscope.aliyuncs.com/compatible-mode/v1';
@@ -195,6 +202,8 @@ export class AiCoachService {
}
}
async streamChat(params: {
userId: string;
conversationId: string;
@@ -226,15 +235,33 @@ export class AiCoachService {
messages.unshift({ role: 'system', content: weightContext });
}
} else if (commandResult.command === 'diet') {
// 使用视觉模型分析饮食图片
// 使用饮食分析服务处理图片
if (params.imageUrls) {
const dietAnalysis = await this.analyzeDietImage(params.imageUrls);
const dietAnalysisResult = await this.dietAnalysisService.analyzeDietImageEnhanced(params.imageUrls);
// 如果AI确定应该记录饮食则自动添加到数据库
const createDto = await this.dietAnalysisService.processDietRecord(
params.userId,
dietAnalysisResult,
params.imageUrls[0]
);
if (createDto) {
// 构建用户的最近饮食上下文用于营养分析
const nutritionContext = await this.dietAnalysisService.buildUserNutritionContext(params.userId);
if (nutritionContext) {
messages.unshift({ role: 'system', content: nutritionContext });
}
params.systemNotice = `系统提示:已成功为您记录了${createDto.foodName}的饮食信息(${createDto.portionDescription || ''},约${createDto.estimatedCalories || 0}卡路里)。`;
}
messages.push({
role: 'user',
content: `用户通过拍照记录饮食,图片分析结果如下\n${dietAnalysis}`
content: `用户通过拍照记录饮食,AI分析结果:\n${dietAnalysisResult.analysisText}`
});
messages.unshift({ role: 'system', content: this.dietAnalysisService.buildEnhancedDietAnalysisPrompt() });
}
messages.unshift({ role: 'system', content: this.buildDietAnalysisPrompt() });
}
// else if (this.isLikelyNutritionTopic(params.userContent, messages)) {
@@ -386,8 +413,10 @@ export class AiCoachService {
};
}
/**
* 构建饮食分析提示
* 构建饮食分析提示(保留原有方法用于兼容)
* @returns 饮食分析提示文本
*/
private buildDietAnalysisPrompt(): string {
@@ -413,53 +442,8 @@ export class AiCoachService {
请以结构化、清晰的方式输出结果,使用亲切专业的语言风格。`;
}
/**
* 分析饮食图片
* @param imageUrl 图片URL
* @returns 饮食分析结果
*/
private async analyzeDietImage(imageUrls: string[]): Promise<string> {
try {
const prompt = `请分析这张食物图片,识别其中的食物种类、分量,并提供以下信息:
1. 食物识别:
- 主要食材名称
- 烹饪方式
- 食物类型(主食、蛋白质、蔬菜、水果等)
2. 分量估算:
- 每种食物的大致重量或体积
- 使用常见单位描述100g、1碗、2片等
3. 营养分析:
- 估算总热量kcal
- 三大营养素含量(蛋白质、碳水化合物、脂肪)
- 主要维生素和矿物质
请以结构化、清晰的方式输出结果。`;
const completion = await this.client.chat.completions.create({
model: this.visionModel,
messages: [
{
role: 'user',
content: [
{ type: 'text', text: prompt },
...imageUrls.map((imageUrl) => ({ type: 'image_url', image_url: { url: imageUrl } as any })),
] as any,
},
],
temperature: 1,
});
this.logger.log(`diet image analysis result: ${completion.choices?.[0]?.message?.content}`);
return completion.choices?.[0]?.message?.content || '无法分析图片中的食物';
} catch (error) {
this.logger.error(`饮食图片分析失败: ${error instanceof Error ? error.message : String(error)}`);
return '饮食图片分析失败';
}
}
private deriveTitleIfEmpty(assistantReply: string): string | null {
if (!assistantReply) return null;