feat: 实现饮食记录确认流程

- 新增饮食记录确认流程,将自动记录模式升级为用户确认模式,提升用户交互体验。
- 实现两阶段饮食记录流程,支持AI识别食物并生成确认选项,用户选择后记录到数据库并提供营养分析。
- 扩展DTO层,新增相关数据结构以支持确认流程。
- 更新服务层,新增处理确认逻辑的方法,优化饮食记录的创建流程。
- 增强API文档,详细说明新流程及使用建议,确保开发者理解和使用新功能。
This commit is contained in:
richarjiang
2025-08-18 18:59:36 +08:00
parent 485ba1f67c
commit ede5730647
7 changed files with 903 additions and 30 deletions

View File

@@ -25,6 +25,33 @@ export interface DietAnalysisResult {
analysisText: string;
}
/**
* 食物确认选项接口
*/
export interface FoodConfirmationOption {
id: string;
label: string;
foodName: string;
portion: string;
calories: number;
mealType: MealType;
nutritionData: {
proteinGrams?: number;
carbohydrateGrams?: number;
fatGrams?: number;
fiberGrams?: number;
};
}
/**
* 食物识别确认结果接口
*/
export interface FoodRecognitionResult {
recognizedItems: FoodConfirmationOption[];
analysisText: string;
confidence: number;
}
/**
* 饮食分析服务
* 负责处理饮食相关的AI分析、营养评估和上下文构建
@@ -50,6 +77,47 @@ export class DietAnalysisService {
this.visionModel = this.configService.get<string>('DASHSCOPE_VISION_MODEL') || 'qwen-vl-max';
}
/**
* 食物识别用于用户确认 - 新的确认流程
* @param imageUrls 图片URL数组
* @returns 食物识别确认结果
*/
async recognizeFoodForConfirmation(imageUrls: string[]): Promise<FoodRecognitionResult> {
try {
const currentHour = new Date().getHours();
const suggestedMealType = this.getSuggestedMealType(currentHour);
const prompt = this.buildFoodRecognitionPrompt(suggestedMealType);
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: 0.3,
response_format: { type: 'json_object' } as any,
});
const rawResult = completion.choices?.[0]?.message?.content || '{}';
this.logger.log(`Food recognition result: ${rawResult}`);
return this.parseRecognitionResult(rawResult, suggestedMealType);
} catch (error) {
this.logger.error(`食物识别失败: ${error instanceof Error ? error.message : String(error)}`);
return {
recognizedItems: [],
analysisText: '食物识别失败,请稍后重试',
confidence: 0
};
}
}
/**
* 增强版饮食图片分析 - 返回结构化数据
* @param imageUrls 图片URL数组
@@ -91,6 +159,55 @@ export class DietAnalysisService {
}
}
/**
* 从用户确认的选项创建饮食记录
* @param userId 用户ID
* @param confirmedOption 用户确认的食物选项
* @param imageUrl 图片URL
* @returns 饮食记录响应
*/
async createDietRecordFromConfirmation(
userId: string,
confirmedOption: FoodConfirmationOption,
imageUrl: string
): Promise<CreateDietRecordDto | null> {
try {
const createDto: CreateDietRecordDto = {
mealType: confirmedOption.mealType,
foodName: confirmedOption.foodName,
portionDescription: confirmedOption.portion,
estimatedCalories: confirmedOption.calories,
proteinGrams: confirmedOption.nutritionData.proteinGrams,
carbohydrateGrams: confirmedOption.nutritionData.carbohydrateGrams,
fatGrams: confirmedOption.nutritionData.fatGrams,
fiberGrams: confirmedOption.nutritionData.fiberGrams,
source: DietRecordSource.Vision,
imageUrl: imageUrl,
aiAnalysisResult: {
shouldRecord: true,
confidence: 95, // 用户确认后置信度很高
extractedData: {
foodName: confirmedOption.foodName,
mealType: confirmedOption.mealType,
portionDescription: confirmedOption.portion,
estimatedCalories: confirmedOption.calories,
proteinGrams: confirmedOption.nutritionData.proteinGrams,
carbohydrateGrams: confirmedOption.nutritionData.carbohydrateGrams,
fatGrams: confirmedOption.nutritionData.fatGrams,
fiberGrams: confirmedOption.nutritionData.fiberGrams,
},
analysisText: `用户确认记录:${confirmedOption.label}`
}
};
await this.usersService.addDietRecord(userId, createDto);
return createDto;
} catch (error) {
this.logger.error(`用户确认添加饮食记录失败: ${error instanceof Error ? error.message : String(error)}`);
return null;
}
}
/**
* 处理饮食记录并添加到数据库
* @param userId 用户ID
@@ -203,6 +320,46 @@ export class DietAnalysisService {
}
}
/**
* 构建食物识别提示(用于确认流程)
* @param suggestedMealType 建议的餐次类型
* @returns 提示文本
*/
private buildFoodRecognitionPrompt(suggestedMealType: MealType): string {
return `作为专业营养分析师,请分析这张食物图片并生成用户确认选项。
当前时间建议餐次:${suggestedMealType}
请识别图片中的食物并为每种食物生成确认选项。返回以下格式的JSON
{
"confidence": number, // 整体识别置信度 0-100
"analysisText": string, // 简短的识别说明文字
"recognizedItems": [ // 识别的食物列表
{
"id": string, // 唯一标识符
"foodName": string, // 食物名称
"portion": string, // 份量描述(如"1碗"、"150g"等)
"calories": number, // 估算热量
"mealType": "${suggestedMealType}", // 餐次类型
"label": string, // 显示给用户的完整选项文本(如"一条鱼 200卡"
"nutritionData": {
"proteinGrams": number, // 蛋白质
"carbohydrateGrams": number, // 碳水化合物
"fatGrams": number, // 脂肪
"fiberGrams": number // 膳食纤维
}
}
]
}
要求:
1. 如果图片中有多种食物,为每种主要食物生成一个选项
2. label字段要简洁易懂格式如"一条鱼 200卡"、"一碗米饭 150卡"
3. 营养数据要合理估算
4. 如果图片模糊或无法识别返回空的recognizedItems数组
5. 最多生成5个选项优先选择主要食物`;
}
/**
* 构建饮食分析提示
* @param suggestedMealType 建议的餐次类型
@@ -242,6 +399,55 @@ export class DietAnalysisService {
4. analysisText要详细说明识别的食物和营养分析`;
}
/**
* 解析食物识别结果
* @param rawResult 原始结果字符串
* @param suggestedMealType 建议的餐次类型
* @returns 解析后的识别结果
*/
private parseRecognitionResult(rawResult: string, suggestedMealType: MealType): FoodRecognitionResult {
let parsedResult: any;
try {
parsedResult = JSON.parse(rawResult);
} catch (parseError) {
this.logger.error(`食物识别JSON解析失败: ${parseError}`);
return {
recognizedItems: [],
analysisText: '图片分析失败:无法解析识别结果',
confidence: 0
};
}
const recognizedItems: FoodConfirmationOption[] = [];
if (parsedResult.recognizedItems && Array.isArray(parsedResult.recognizedItems)) {
parsedResult.recognizedItems.forEach((item: any, index: number) => {
if (item.foodName && item.calories) {
recognizedItems.push({
id: item.id || `food_${index}`,
label: item.label || `${item.foodName} ${item.calories}`,
foodName: item.foodName,
portion: item.portion || '1份',
calories: this.validateNumber(item.calories, 1, 2000) || 0,
mealType: this.validateMealType(item.mealType) || suggestedMealType,
nutritionData: {
proteinGrams: this.validateNumber(item.nutritionData?.proteinGrams, 0, 200),
carbohydrateGrams: this.validateNumber(item.nutritionData?.carbohydrateGrams, 0, 500),
fatGrams: this.validateNumber(item.nutritionData?.fatGrams, 0, 200),
fiberGrams: this.validateNumber(item.nutritionData?.fiberGrams, 0, 50),
}
});
}
});
}
return {
recognizedItems,
analysisText: parsedResult.analysisText || '已识别图片中的食物',
confidence: Math.min(100, Math.max(0, parsedResult.confidence || 0))
};
}
/**
* 解析和验证分析结果
* @param rawResult 原始结果字符串