perf:修复健康数据

This commit is contained in:
2025-09-12 23:00:49 +08:00
parent cf02fda4ec
commit dc06dfbebd
4 changed files with 671 additions and 406 deletions

View File

@@ -7,7 +7,7 @@ import { AccessTokenPayload } from '../users/services/apple-auth.service';
import { AiCoachService } from './ai-coach.service';
import { AiChatRequestDto, AiChatResponseDto, AiResponseDataDto } from './dto/ai-chat.dto';
import { PostureAssessmentRequestDto, PostureAssessmentResponseDto } from './dto/posture-assessment.dto';
import { FoodRecognitionRequestDto, FoodRecognitionResponseDto } from './dto/food-recognition.dto';
import { FoodRecognitionRequestDto, FoodRecognitionResponseDto, TextFoodAnalysisRequestDto } from './dto/food-recognition.dto';
import { DietAnalysisService } from './services/diet-analysis.service';
import { UsersService } from '../users/users.service';
@@ -207,6 +207,51 @@ export class AiCoachController {
return response;
}
@Post('text-food-analysis')
@ApiOperation({
summary: '文本食物分析服务',
description: '分析用户口述的饮食文本内容,识别食物并返回数组格式的选项列表。支持中文食物描述,如"吃了一碗米饭"、"喝了一杯牛奶"等。返回数据结构与图片识别接口保持一致。'
})
@ApiBody({ type: TextFoodAnalysisRequestDto })
async analyzeTextFood(
@Body() body: TextFoodAnalysisRequestDto,
@CurrentUser() user: AccessTokenPayload,
): Promise<FoodRecognitionResponseDto> {
this.logger.log(`Text food analysis request from user: ${user.sub}, text: "${body.text}"`);
const result = await this.dietAnalysisService.analyzeTextFoodForConfirmation(body.text);
// 转换为DTO格式
const response: FoodRecognitionResponseDto = {
items: result.items.map(item => ({
id: item.id,
label: item.label,
foodName: item.foodName,
portion: item.portion,
calories: item.calories,
mealType: item.mealType,
nutritionData: {
proteinGrams: item.nutritionData.proteinGrams,
carbohydrateGrams: item.nutritionData.carbohydrateGrams,
fatGrams: item.nutritionData.fatGrams,
fiberGrams: item.nutritionData.fiberGrams,
}
})),
analysisText: result.analysisText,
confidence: result.confidence,
isFoodDetected: result.isFoodDetected,
nonFoodMessage: result.nonFoodMessage
};
if (!result.isFoodDetected) {
this.logger.log(`Non-food detected in text for user: ${user.sub}, message: ${result.nonFoodMessage}`);
} else {
this.logger.log(`Text food analysis completed: ${result.items.length} items recognized`);
}
return response;
}
}

View File

@@ -132,4 +132,17 @@ export class FoodConfirmationRequestDto {
@IsOptional()
@IsString()
imageUrl?: string;
}
/**
* 文本食物分析请求DTO
*/
export class TextFoodAnalysisRequestDto {
@ApiProperty({
description: '用户描述的饮食文本内容',
example: '今天早餐吃了一碗燕麦粥加香蕉'
})
@IsString()
@IsNotEmpty()
text: string;
}

View File

@@ -261,6 +261,39 @@ export class DietAnalysisService {
}
}
/**
* 分析文本中的食物用于用户确认 - 与图片识别接口保持数据结构一致
* @param userText 用户输入的文本描述
* @returns 食物识别确认结果
*/
async analyzeTextFoodForConfirmation(userText: string): Promise<FoodRecognitionResult> {
try {
this.logger.log(`Text food analysis request: ${userText}`);
const currentHour = new Date().getHours();
const suggestedMealType = this.getSuggestedMealType(currentHour);
// 使用专门的多食物文本分析 prompt
const prompt = this.buildMultiFoodTextAnalysisPrompt(suggestedMealType);
const completion = await this.makeTextApiCall(prompt, userText);
const rawResult = completion.choices?.[0]?.message?.content || '{}';
this.logger.log(`Multi-food text analysis result: ${rawResult}`);
// 直接解析为多食物结构
return this.parseMultiFoodTextResult(rawResult, suggestedMealType, userText);
} catch (error) {
this.logger.error(`文本食物分析失败: ${error instanceof Error ? error.message : String(error)}`);
return {
items: [],
analysisText: '文本食物分析失败,请稍后重试',
confidence: 0,
isFoodDetected: false,
nonFoodMessage: '服务暂时不可用,请稍后重试'
};
}
}
/**
* 从用户确认的选项创建饮食记录
* @param userId 用户ID
@@ -524,6 +557,112 @@ export class DietAnalysisService {
4. analysisText要详细说明识别的食物和营养分析`;
}
/**
* 将 DietAnalysisResult 转换为 FoodRecognitionResult
* @param analysisResult 饮食分析结果
* @param originalText 原始用户文本
* @returns 食物识别结果
*/
private convertDietAnalysisToFoodRecognition(
analysisResult: DietAnalysisResult,
originalText: string
): FoodRecognitionResult {
// 如果分析失败或不应该记录,返回未检测到食物的结果
if (!analysisResult.shouldRecord || !analysisResult.extractedData) {
return {
items: [],
analysisText: analysisResult.analysisText || '未能从文本中识别到具体食物信息',
confidence: analysisResult.confidence,
isFoodDetected: false,
nonFoodMessage: '请描述更具体的食物信息,如"吃了一碗米饭"、"喝了一杯牛奶"等'
};
}
// 转换为食物确认选项格式
const foodOption: FoodConfirmationOption = {
id: `text_food_${Date.now()}`,
label: `${analysisResult.extractedData.foodName} ${analysisResult.extractedData.estimatedCalories || 0}`,
foodName: analysisResult.extractedData.foodName,
portion: analysisResult.extractedData.portionDescription || '1份',
calories: analysisResult.extractedData.estimatedCalories || 0,
mealType: analysisResult.extractedData.mealType,
nutritionData: {
proteinGrams: analysisResult.extractedData.proteinGrams,
carbohydrateGrams: analysisResult.extractedData.carbohydrateGrams,
fatGrams: analysisResult.extractedData.fatGrams,
fiberGrams: analysisResult.extractedData.fiberGrams,
}
};
return {
items: [foodOption],
analysisText: analysisResult.analysisText || `基于您的描述"${originalText}",识别出以下食物`,
confidence: analysisResult.confidence,
isFoodDetected: true
};
}
/**
* 构建多食物文本分析提示 - 支持识别多种食物
* @param suggestedMealType 建议的餐次类型
* @returns 提示文本
*/
private buildMultiFoodTextAnalysisPrompt(suggestedMealType: MealType): string {
return `作为专业营养分析师,请分析用户描述的饮食内容,支持识别多种食物。
当前时间建议餐次:${suggestedMealType}
请返回以下格式的JSON不要包含其他文本
{
"confidence": number, // 整体识别置信度 0-100
"analysisText": string, // 简短的识别说明文字
"isFoodDetected": boolean, // 是否检测到食物
"nonFoodMessage": string, // 当isFoodDetected为false时的提示信息
"recognizedItems": [ // 识别的食物列表
{
"id": string, // 唯一标识符(使用 food_1, food_2 等)
"foodName": string, // 食物名称(简洁)
"portion": string, // 份量描述(如"1碗"、"1份"等)
"calories": number, // 估算热量
"mealType": "${suggestedMealType}", // 餐次类型
"label": string, // 显示给用户的完整选项文本(如"一碗米饭 280卡"
"nutritionData": {
"proteinGrams": number, // 蛋白质
"carbohydrateGrams": number, // 碳水化合物
"fatGrams": number, // 脂肪
"fiberGrams": number // 膳食纤维
}
}
]
}
**分析要求:**
1. **多食物识别:**
- 如果用户描述包含多种食物(如"一碗米饭,一份麻辣香锅"),为每种主要食物创建独立的选项
- 每个食物项目都要有准确的营养数据估算
- 最多识别8种不同的食物
2. **营养数据估算:**
- 根据食物种类、份量、烹饪方式合理估算营养成分
- 热量要基于常见份量进行估算
- 营养数据要符合该食物的实际营养特征
3. **处理复合菜品:**
- 对于复合菜品(如"麻辣香锅"、"宫保鸡丁"),可以作为整体记录
- 也可以分解为主要组成部分(如肉类、蔬菜、主食等)
- 优先选择用户更容易理解的记录方式
4. **非食物处理:**
- 如果描述不包含具体食物,设置 isFoodDetected: false
- 提供友好的提示信息
**示例输入处理:**
- "今天中午吃了一碗米饭,一份麻辣香锅" → 识别为2个选项
- "早餐吃了燕麦粥加香蕉和牛奶" → 可识别为1个复合选项或3个独立选项
- "晚上吃了牛肉面" → 识别为1个选项面条+牛肉的复合菜品)
- "喝了水" → isFoodDetected: false水不是营养食物`;
}
/**
* 构建文本饮食分析提示
* @param suggestedMealType 建议的餐次类型
@@ -572,6 +711,75 @@ export class DietAnalysisService {
- "刚吃了两个苹果当零食"`;
}
/**
* 解析多食物文本分析结果
* @param rawResult 原始结果字符串
* @param suggestedMealType 建议的餐次类型
* @param originalText 原始用户文本
* @returns 解析后的识别结果
*/
private parseMultiFoodTextResult(rawResult: string, suggestedMealType: MealType, originalText: string): FoodRecognitionResult {
let parsedResult: any;
try {
parsedResult = JSON.parse(rawResult);
} catch (parseError) {
this.logger.error(`多食物文本分析JSON解析失败: ${parseError}`);
return {
items: [],
analysisText: '文本分析失败:无法解析识别结果',
confidence: 0,
isFoodDetected: false,
nonFoodMessage: '文本分析失败,请重新描述您吃的食物'
};
}
// 检查是否识别到食物
const isFoodDetected = parsedResult.isFoodDetected !== false; // 默认为true除非明确设置为false
const nonFoodMessage = parsedResult.nonFoodMessage || '';
const recognizedItems: FoodConfirmationOption[] = [];
// 只有在识别到食物时才处理食物项目
if (isFoodDetected && parsedResult.recognizedItems && Array.isArray(parsedResult.recognizedItems)) {
parsedResult.recognizedItems.forEach((item: any, index: number) => {
if (item.foodName && item.calories) {
recognizedItems.push({
id: item.id || `text_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),
}
});
}
});
}
// 根据是否识别到食物设置不同的分析文本
let analysisText = parsedResult.analysisText || '';
if (!isFoodDetected) {
analysisText = analysisText || '文本中未检测到具体食物信息';
} else if (recognizedItems.length === 0) {
analysisText = analysisText || '无法准确解析食物信息';
} else {
analysisText = analysisText || `基于您的描述"${originalText}",识别出 ${recognizedItems.length} 种食物`;
}
return {
items: recognizedItems,
analysisText,
confidence: Math.min(100, Math.max(0, parsedResult.confidence || 0)),
isFoodDetected,
nonFoodMessage: !isFoodDetected ? (nonFoodMessage || '请描述更具体的食物信息,如"吃了一碗米饭"、"喝了一杯牛奶"等') : undefined
};
}
/**
* 解析食物识别结果
* @param rawResult 原始结果字符串