import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { OpenAI } from 'openai'; import { DietRecordsService } from '../../diet-records/diet-records.service'; import { CreateDietRecordDto } from '../../users/dto/diet-record.dto'; import { MealType, DietRecordSource } from '../../users/models/user-diet-history.model'; /** * 饮食分析结果接口 */ export interface DietAnalysisResult { shouldRecord: boolean; confidence: number; extractedData?: { foodName: string; mealType: MealType; portionDescription?: string; estimatedCalories?: number; proteinGrams?: number; carbohydrateGrams?: number; fatGrams?: number; fiberGrams?: number; nutritionDetails?: any; }; 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 { items: FoodConfirmationOption[]; // 修改:统一使用items作为数组字段名 analysisText: string; confidence: number; isFoodDetected: boolean; // 是否识别到食物 nonFoodMessage?: string; // 非食物提示信息 } /** * 饮食分析服务 * 负责处理饮食相关的AI分析、营养评估和上下文构建 * * 支持多种AI模型: * - GLM-4.5V (智谱AI) - 设置 AI_VISION_PROVIDER=glm * - Qwen VL (阿里云DashScope) - 设置 AI_VISION_PROVIDER=dashscope (默认) */ @Injectable() export class DietAnalysisService { private readonly logger = new Logger(DietAnalysisService.name); private readonly client: OpenAI; private readonly visionModel: string; private readonly model: string; private readonly apiProvider: string; constructor( private readonly configService: ConfigService, private readonly dietRecordsService: DietRecordsService, ) { // Support both GLM-4.5V and DashScope (Qwen) models this.apiProvider = this.configService.get('AI_VISION_PROVIDER') || 'dashscope'; if (this.apiProvider === 'glm') { // GLM-4.5V Configuration const glmApiKey = this.configService.get('GLM_API_KEY'); const glmBaseURL = this.configService.get('GLM_BASE_URL') || 'https://open.bigmodel.cn/api/paas/v4'; this.client = new OpenAI({ apiKey: glmApiKey, baseURL: glmBaseURL, }); this.model = this.configService.get('GLM_MODEL') || 'glm-4-flash'; this.visionModel = this.configService.get('GLM_VISION_MODEL') || 'glm-4v-plus'; } else { // DashScope Configuration (default) const dashScopeApiKey = this.configService.get('DASHSCOPE_API_KEY') || 'sk-e3ff4494c2f1463a8910d5b3d05d3143'; const baseURL = this.configService.get('DASHSCOPE_BASE_URL') || 'https://dashscope.aliyuncs.com/compatible-mode/v1'; this.client = new OpenAI({ apiKey: dashScopeApiKey, baseURL, }); this.model = this.configService.get('DASHSCOPE_MODEL') || 'qwen-flash'; this.visionModel = this.configService.get('DASHSCOPE_VISION_MODEL') || 'qwen-vl-max'; } } /** * 制作视觉模型API调用 - 兼容GLM-4.5V和DashScope * @param prompt 提示文本 * @param imageUrls 图片URL数组 * @returns API响应 */ private async makeVisionApiCall(prompt: string, imageUrls: string[]) { const baseParams = { model: this.visionModel, temperature: 0.3, response_format: { type: 'json_object' } as any, }; if (this.apiProvider === 'glm') { // GLM-4.5V format return await this.client.chat.completions.create({ ...baseParams, messages: [ { role: 'user', content: [ { type: 'text', text: prompt }, ...imageUrls.map((imageUrl) => ({ type: 'image_url', image_url: { url: imageUrl } } as any)), ] as any, }, ], } as any); } else { // DashScope format (default) return await this.client.chat.completions.create({ ...baseParams, messages: [ { role: 'user', content: [ { type: 'text', text: prompt }, ...imageUrls.map((imageUrl) => ({ type: 'image_url', image_url: { url: imageUrl } as any })), ] as any, }, ], }); } } /** * 制作文本模型API调用 - 兼容GLM-4.5和DashScope * @param prompt 提示文本 * @param userText 用户文本 * @returns API响应 */ private async makeTextApiCall(prompt: string, userText: string) { const baseParams = { model: this.model, temperature: 0.3, response_format: { type: 'json_object' } as any, }; return await this.client.chat.completions.create({ ...baseParams, messages: [ { role: 'user', content: `${prompt}\n\n用户描述:${userText}` } ], }); } /** * 食物识别用于用户确认 - 新的确认流程 * @param imageUrls 图片URL数组 * @returns 食物识别确认结果 */ async recognizeFoodForConfirmation(imageUrls: string[]): Promise { try { const currentHour = new Date().getHours(); const suggestedMealType = this.getSuggestedMealType(currentHour); const prompt = this.buildFoodRecognitionPrompt(suggestedMealType); const completion = await this.makeVisionApiCall(prompt, imageUrls); 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 { items: [], analysisText: '食物识别失败,请稍后重试', confidence: 0, isFoodDetected: false, nonFoodMessage: '服务暂时不可用,请稍后重试' }; } } /** * 增强版饮食图片分析 - 返回结构化数据 * @param imageUrls 图片URL数组 * @returns 结构化的饮食分析结果 */ async analyzeDietImageEnhanced(imageUrls: string[]): Promise { try { const currentHour = new Date().getHours(); const suggestedMealType = this.getSuggestedMealType(currentHour); const prompt = this.buildDietAnalysisPrompt(suggestedMealType); const completion = await this.makeVisionApiCall(prompt, imageUrls); const rawResult = completion.choices?.[0]?.message?.content || '{}'; this.logger.log(`Enhanced diet analysis result: ${rawResult}`); return this.parseAndValidateResult(rawResult, suggestedMealType); } catch (error) { this.logger.error(`增强版饮食图片分析失败: ${error instanceof Error ? error.message : String(error)}`); return { shouldRecord: false, confidence: 0, analysisText: '饮食图片分析失败,请稍后重试' }; } } /** * 分析用户文本中的饮食信息 * @param userText 用户输入的文本 * @returns 饮食分析结果 */ async analyzeDietFromText(userText: string): Promise { try { const currentHour = new Date().getHours(); const suggestedMealType = this.getSuggestedMealType(currentHour); const prompt = this.buildTextDietAnalysisPrompt(suggestedMealType); const completion = await this.makeTextApiCall(prompt, userText); const rawResult = completion.choices?.[0]?.message?.content || '{}'; this.logger.log(`Text diet analysis result: ${rawResult}`); return this.parseAndValidateResult(rawResult, suggestedMealType); } catch (error) { this.logger.error(`文本饮食分析失败: ${error instanceof Error ? error.message : String(error)}`); return { shouldRecord: false, confidence: 0, analysisText: '文本饮食分析失败,请稍后重试' }; } } /** * 从用户确认的选项创建饮食记录 * @param userId 用户ID * @param confirmedOption 用户确认的食物选项 * @param imageUrl 图片URL * @returns 饮食记录响应 */ async createDietRecordFromConfirmation( userId: string, confirmedOption: FoodConfirmationOption, imageUrl: string ): Promise { 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.dietRecordsService.addDietRecord(userId, createDto); return createDto; } catch (error) { this.logger.error(`用户确认添加饮食记录失败: ${error instanceof Error ? error.message : String(error)}`); return null; } } /** * 处理饮食记录并添加到数据库 * @param userId 用户ID * @param analysisResult 分析结果 * @param imageUrl 图片URL(可选,文本记录时为空) * @returns 饮食记录响应 */ async processDietRecord(userId: string, analysisResult: DietAnalysisResult, imageUrl?: string): Promise { if (!analysisResult.shouldRecord || !analysisResult.extractedData) { return null; } try { // 根据是否有图片URL来确定数据源 const source = imageUrl ? DietRecordSource.Vision : DietRecordSource.Manual; const createDto: CreateDietRecordDto = { mealType: analysisResult.extractedData.mealType, foodName: analysisResult.extractedData.foodName, portionDescription: analysisResult.extractedData.portionDescription, estimatedCalories: analysisResult.extractedData.estimatedCalories, proteinGrams: analysisResult.extractedData.proteinGrams, carbohydrateGrams: analysisResult.extractedData.carbohydrateGrams, fatGrams: analysisResult.extractedData.fatGrams, fiberGrams: analysisResult.extractedData.fiberGrams, source: source, imageUrl: imageUrl || undefined, aiAnalysisResult: analysisResult, }; await this.dietRecordsService.addDietRecord(userId, createDto); return createDto; } catch (error) { this.logger.error(`自动添加饮食记录失败: ${error instanceof Error ? error.message : String(error)}`); return null; } } /** * 构建包含用户营养信息的系统提示 * @param userId 用户ID * @returns 营养上下文字符串 */ async buildUserNutritionContext(userId: string): Promise { try { // 获取最近10顿饮食记录 const recentDietHistory = await this.dietRecordsService.getDietHistory(userId, { limit: 10 }); if (recentDietHistory.total === 0) { return '\n\n=== 用户营养信息 ===\n这是用户的第一次饮食记录,请给予鼓励并介绍饮食记录的价值。\n'; } let context = '\n\n=== 用户最近饮食记录分析 ===\n'; // 获取营养汇总 const nutritionSummary = await this.dietRecordsService.getRecentNutritionSummary(userId, 10); context += this.buildNutritionSummaryText(nutritionSummary); context += this.buildMealDistributionText(recentDietHistory.records); context += this.buildRecentMealsText(recentDietHistory.records); context += this.buildNutritionTrendText(nutritionSummary); context += `\n请基于用户的饮食记录历史,提供个性化的营养分析和健康建议。`; return context; } catch (error) { this.logger.error(`构建用户营养上下文失败: ${error instanceof Error ? error.message : String(error)}`); return ''; } } /** * 构建增强版饮食分析提示 * @returns 增强版饮食分析提示文本 */ buildEnhancedDietAnalysisPrompt(): string { return `增强版饮食分析专家模式: 你是一位资深的营养分析师,专门负责处理用户的饮食记录和营养分析。用户已通过AI视觉识别记录了饮食信息,你需要: 1. 综合分析: - 结合用户最近的饮食记录趋势 - 评估当前这餐在整体饮食结构中的作用 - 分析营养素搭配的合理性 2. 个性化建议: - 基于用户的历史饮食记录给出针对性建议 - 考虑营养平衡、热量控制、健康目标等因素 - 提供具体可执行的改善方案 3. 健康指导: - 如果发现营养不均衡,给出调整建议 - 推荐搭配食物或下一餐的建议 - 强调长期健康饮食习惯的重要性 请以温暖、专业、实用的语言风格回复,让用户感受到个性化的关怀和专业的指导。`; } /** * 根据时间推断餐次类型 * @param currentHour 当前小时 * @returns 建议的餐次类型 */ private getSuggestedMealType(currentHour: number): MealType { if (currentHour >= 6 && currentHour < 10) { return MealType.Breakfast; } else if (currentHour >= 11 && currentHour < 15) { return MealType.Lunch; } else if (currentHour >= 17 && currentHour < 21) { return MealType.Dinner; } else { return MealType.Snack; } } /** * 构建食物识别提示(用于确认流程) * @param suggestedMealType 建议的餐次类型 * @returns 提示文本 */ private buildFoodRecognitionPrompt(suggestedMealType: MealType): string { return `作为专业营养分析师,请分析这张图片并判断是否包含食物。 当前时间建议餐次:${suggestedMealType} **首先判断图片内容:** - 如果图片中包含食物,请识别并生成确认选项 - 如果图片中不包含食物(如风景、人物、物品、文字等),请明确标识 返回以下格式的JSON: { "confidence": number, // 整体识别置信度 0-100 "analysisText": string, // 简短的识别说明文字 "isFoodDetected": boolean, // 是否检测到食物 "nonFoodMessage": string, // 当isFoodDetected为false时的提示信息 "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. **非食物情况:** - 如果图片中没有食物,设置 isFoodDetected: false - recognizedItems 返回空数组 - nonFoodMessage 提供友好的提示,如"图片中未检测到食物,请上传包含食物的图片" - confidence 设置为对判断的置信度 2. **食物识别情况:** - 设置 isFoodDetected: true - nonFoodMessage 可以为空或不设置 - 如果图片中有多种食物,为每种主要食物生成一个选项 - 如果图片中只有一种食物,也要生成一个包含该食物的数组选项 - label字段要简洁易懂,格式如"一条鱼 200卡"、"一碗米饭 150卡" - 营养数据要合理估算 - 最多生成8个选项,优先选择主要食物 - 对于复合菜品(如盖浇饭、汤面等),既可以作为整体记录,也可以分解为主要组成部分 3. **模糊情况:** - 如果图片模糊但能看出是食物相关,设置 isFoodDetected: true,但返回空的recognizedItems数组 - analysisText 说明"图片模糊,无法准确识别食物"`; } /** * 构建饮食分析提示 * @param suggestedMealType 建议的餐次类型 * @returns 提示文本 */ private buildDietAnalysisPrompt(suggestedMealType: MealType): string { return `作为专业营养分析师,请分析这张食物图片并以严格JSON格式返回结果。 当前时间建议餐次:${suggestedMealType} 请返回以下格式的JSON(不要包含其他文本): { "shouldRecord": boolean, // 是否应该记录(如果图片清晰且包含食物则为true) "confidence": number, // 识别置信度 0-100 "extractedData": { "foodName": string, // 主要食物名称(简洁) "mealType": "${suggestedMealType}", // 餐次类型,优先使用建议值 "portionDescription": string, // 份量描述(如"1碗"、"200g"等) "estimatedCalories": number, // 估算总热量 "proteinGrams": number, // 蛋白质含量(克) "carbohydrateGrams": number, // 碳水化合物含量(克) "fatGrams": number, // 脂肪含量(克) "fiberGrams": number, // 膳食纤维含量(克) "nutritionDetails": { // 其他营养信息 "mainIngredients": string[], // 主要食材列表 "cookingMethod": string, // 烹饪方式 "foodCategories": string[] // 食物分类(如"主食"、"蛋白质"等) } }, "analysisText": string // 详细的文字分析说明 } 重要提示: 1. 如果图片模糊、无食物或无法识别,设置shouldRecord为false 2. 营养数据要基于识别的食物种类和分量合理估算 3. foodName要简洁明了,便于记录和查找 4. analysisText要详细说明识别的食物和营养分析`; } /** * 构建文本饮食分析提示 * @param suggestedMealType 建议的餐次类型 * @returns 提示文本 */ private buildTextDietAnalysisPrompt(suggestedMealType: MealType): string { return `作为专业营养分析师,请分析用户描述的饮食内容并以严格JSON格式返回结果。 当前时间建议餐次:${suggestedMealType} 请返回以下格式的JSON(不要包含其他文本): { "shouldRecord": boolean, // 是否应该记录(如果描述包含具体食物则为true) "confidence": number, // 识别置信度 0-100 "extractedData": { "foodName": string, // 主要食物名称(简洁,如"鸡胸肉沙拉"、"牛肉面"等) "mealType": "${suggestedMealType}", // 餐次类型,优先使用建议值 "portionDescription": string, // 份量描述(如"1碗"、"200g"、"一份"等) "estimatedCalories": number, // 估算总热量 "proteinGrams": number, // 蛋白质含量(克) "carbohydrateGrams": number, // 碳水化合物含量(克) "fatGrams": number, // 脂肪含量(克) "fiberGrams": number, // 膳食纤维含量(克) "nutritionDetails": { // 其他营养信息 "mainIngredients": string[], // 主要食材列表 "cookingMethod": string, // 烹饪方式(如"清蒸"、"炒制"、"生食"等) "foodCategories": string[] // 食物分类(如"主食"、"蛋白质"、"蔬菜"等) } }, "analysisText": string // 详细的文字分析说明 } 分析要求: 1. 仔细识别用户描述中的食物名称、数量、烹饪方式等信息 2. 如果描述模糊或不包含具体食物,设置shouldRecord为false 3. 营养数据要基于识别的食物种类和分量合理估算 4. foodName要简洁明了,便于记录和查找 5. 支持中文食物描述,如"一碗米饭"、"两个鸡蛋"、"一份青菜"等 6. analysisText要详细说明识别的食物和营养分析 7. 如果用户提到多种食物,选择主要的食物作为记录对象,或合并为一餐记录 示例用户输入: - "今天早餐吃了一碗燕麦粥加香蕉" - "午餐点了一份鸡胸肉沙拉" - "晚上吃了牛肉面,还有小菜" - "刚吃了两个苹果当零食"`; } /** * 解析食物识别结果 * @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 { 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 || `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 || '已识别图片中的食物'; } return { items: recognizedItems, analysisText, confidence: Math.min(100, Math.max(0, parsedResult.confidence || 0)), isFoodDetected, nonFoodMessage: !isFoodDetected ? (nonFoodMessage || '图片中未检测到食物,请上传包含食物的图片') : undefined }; } /** * 解析和验证分析结果 * @param rawResult 原始结果字符串 * @param suggestedMealType 建议的餐次类型 * @returns 验证后的分析结果 */ private parseAndValidateResult(rawResult: string, suggestedMealType: MealType): DietAnalysisResult { let parsedResult: any; try { parsedResult = JSON.parse(rawResult); } catch (parseError) { this.logger.error(`JSON解析失败: ${parseError}`); return { shouldRecord: false, confidence: 0, analysisText: '图片分析失败:无法解析分析结果' }; } // 验证和标准化结果 const result: DietAnalysisResult = { shouldRecord: parsedResult.shouldRecord || false, confidence: Math.min(100, Math.max(0, parsedResult.confidence || 0)), analysisText: parsedResult.analysisText || '未提供分析说明' }; if (result.shouldRecord && parsedResult.extractedData) { const data = parsedResult.extractedData; result.extractedData = { foodName: data.foodName || '未知食物', mealType: this.validateMealType(data.mealType) || suggestedMealType, portionDescription: data.portionDescription, estimatedCalories: this.validateNumber(data.estimatedCalories, 0, 2000), proteinGrams: this.validateNumber(data.proteinGrams, 0, 200), carbohydrateGrams: this.validateNumber(data.carbohydrateGrams, 0, 500), fatGrams: this.validateNumber(data.fatGrams, 0, 200), fiberGrams: this.validateNumber(data.fiberGrams, 0, 50), nutritionDetails: data.nutritionDetails }; } return result; } /** * 构建营养汇总文本 * @param nutritionSummary 营养汇总数据 * @returns 汇总文本 */ private buildNutritionSummaryText(nutritionSummary: any): string { let text = `最近${nutritionSummary.recordCount}顿饮食汇总:\n`; text += `- 总热量:${nutritionSummary.totalCalories.toFixed(0)}卡路里\n`; text += `- 蛋白质:${nutritionSummary.totalProtein.toFixed(1)}g\n`; text += `- 碳水化合物:${nutritionSummary.totalCarbohydrates.toFixed(1)}g\n`; text += `- 脂肪:${nutritionSummary.totalFat.toFixed(1)}g\n`; if (nutritionSummary.totalFiber > 0) { text += `- 膳食纤维:${nutritionSummary.totalFiber.toFixed(1)}g\n`; } return text; } /** * 构建餐次分布文本 * @param records 饮食记录 * @returns 分布文本 */ private buildMealDistributionText(records: any[]): string { const mealTypeCount: Record = {}; records.forEach(record => { mealTypeCount[record.mealType] = (mealTypeCount[record.mealType] || 0) + 1; }); if (Object.keys(mealTypeCount).length === 0) { return ''; } let text = `\n餐次分布:`; Object.entries(mealTypeCount).forEach(([mealType, count]) => { const mealTypeName = this.getMealTypeName(mealType); text += ` ${mealTypeName}${count}次`; }); text += `\n`; return text; } /** * 构建最近饮食详情文本 * @param records 饮食记录 * @returns 详情文本 */ private buildRecentMealsText(records: any[]): string { if (records.length === 0) { return ''; } let text = `\n最近饮食记录:\n`; const recentMeals = records.slice(0, 3); recentMeals.forEach((record, index) => { const date = new Date(record.createdAt).toLocaleDateString('zh-CN'); const time = new Date(record.createdAt).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }); const mealTypeName = this.getMealTypeName(record.mealType); text += `${index + 1}. ${date} ${time} ${mealTypeName}:${record.foodName}`; if (record.estimatedCalories) { text += ` (约${record.estimatedCalories}卡路里)`; } text += `\n`; }); return text; } /** * 构建营养趋势分析文本 * @param nutritionSummary 营养汇总数据 * @returns 趋势分析文本 */ private buildNutritionTrendText(nutritionSummary: any): string { const avgCaloriesPerMeal = nutritionSummary.totalCalories / nutritionSummary.recordCount; const avgProteinPerMeal = nutritionSummary.totalProtein / nutritionSummary.recordCount; let text = `\n营养趋势分析:\n`; if (avgCaloriesPerMeal < 300) { text += `- 平均每餐热量偏低(${avgCaloriesPerMeal.toFixed(0)}卡路里),建议增加营养密度\n`; } else if (avgCaloriesPerMeal > 800) { text += `- 平均每餐热量较高(${avgCaloriesPerMeal.toFixed(0)}卡路里),建议注意控制分量\n`; } else { text += `- 平均每餐热量适中(${avgCaloriesPerMeal.toFixed(0)}卡路里)\n`; } if (avgProteinPerMeal < 15) { text += `- 蛋白质摄入偏低,建议增加优质蛋白质食物\n`; } else { text += `- 蛋白质摄入良好\n`; } return text; } /** * 获取餐次类型的中文名称 * @param mealType 餐次类型 * @returns 中文名称 */ private getMealTypeName(mealType: string): string { const mealTypeNames: Record = { [MealType.Breakfast]: '早餐', [MealType.Lunch]: '午餐', [MealType.Dinner]: '晚餐', [MealType.Snack]: '加餐', [MealType.Other]: '其他' }; return mealTypeNames[mealType] || mealType; } /** * 验证餐次类型 * @param mealType 餐次类型字符串 * @returns 验证后的餐次类型或null */ private validateMealType(mealType: string): MealType | null { const validTypes = Object.values(MealType); return validTypes.includes(mealType as MealType) ? (mealType as MealType) : null; } /** * 验证数字范围 * @param value 值 * @param min 最小值 * @param max 最大值 * @returns 验证后的数字或undefined */ private validateNumber(value: any, min: number, max: number): number | undefined { const num = parseFloat(value); if (isNaN(num)) return undefined; return Math.max(min, Math.min(max, num)); } }