From 5c2c9dfae84e49ce52e2ada7bf25c8cba3f12f87 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Thu, 16 Oct 2025 10:03:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(diet-records):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E8=90=A5=E5=85=BB=E6=88=90=E5=88=86=E8=A1=A8=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E5=88=86=E6=9E=90=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加营养成分表图片识别API接口,支持通过AI模型分析食物营养成分 - 新增NutritionAnalysisService服务,集成GLM-4.5V和Qwen VL视觉模型 - 实现营养成分提取和健康建议生成功能 - 添加完整的API文档和TypeScript类型定义 - 支持多种营养素类型识别,包括热量、蛋白质、脂肪等20+种营养素 --- .kilocode/rules/rule.md | 3 +- docs/nutrition-analysis-api.md | 295 ++++++++++++++++++ src/diet-records/diet-records.controller.ts | 57 ++++ src/diet-records/diet-records.module.ts | 5 +- .../dto/nutrition-analysis-request.dto.ts | 13 + .../dto/nutrition-analysis.dto.ts | 32 ++ .../services/nutrition-analysis.service.ts | 282 +++++++++++++++++ 7 files changed, 684 insertions(+), 3 deletions(-) create mode 100644 docs/nutrition-analysis-api.md create mode 100644 src/diet-records/dto/nutrition-analysis-request.dto.ts create mode 100644 src/diet-records/dto/nutrition-analysis.dto.ts create mode 100644 src/diet-records/services/nutrition-analysis.service.ts diff --git a/.kilocode/rules/rule.md b/.kilocode/rules/rule.md index e678a2f..697eed4 100644 --- a/.kilocode/rules/rule.md +++ b/.kilocode/rules/rule.md @@ -1,8 +1,9 @@ # rule.md -这是一个 nodejs 基于 nestjs 框架的项目 +你是一名拥有 20 年服务端开发经验的 javascript 工程师,这是一个 nodejs 基于 nestjs 框架的项目,与健康、健身、减肥相关 ## 指导原则 - 不要随意新增 markdown 文档 - 代码提交 message 用中文 +- 注意代码的可读性、架构实现要清晰 diff --git a/docs/nutrition-analysis-api.md b/docs/nutrition-analysis-api.md new file mode 100644 index 0000000..69f9020 --- /dev/null +++ b/docs/nutrition-analysis-api.md @@ -0,0 +1,295 @@ +# 营养成分表分析 API 文档 + +## 接口概述 + +本接口用于分析食物营养成分表图片,通过AI大模型智能识别图片中的营养成分信息,并为每个营养素提供详细的健康建议。 + +## 接口信息 + +- **接口地址**: `POST /diet-records/analyze-nutrition-image` +- **请求方式**: POST +- **内容类型**: `application/json` +- **认证方式**: Bearer Token (JWT) + +## 请求参数 + +### Headers + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| Authorization | string | 是 | JWT认证令牌,格式:`Bearer {token}` | +| Content-Type | string | 是 | 固定值:`application/json` | + +### Body 参数 + +| 参数名 | 类型 | 必填 | 说明 | 示例 | +|--------|------|------|------|------| +| imageUrl | string | 是 | 营养成分表图片的URL地址 | `https://example.com/nutrition-label.jpg` | + +#### 请求示例 + +```json +{ + "imageUrl": "https://example.com/nutrition-label.jpg" +} +``` + +## 响应格式 + +### 成功响应 + +```json +{ + "success": true, + "data": [ + { + "key": "energy_kcal", + "name": "热量", + "value": "840千焦", + "analysis": "840千焦约等于201卡路里,占成人每日推荐摄入总热量的10%,属于中等热量水平。" + }, + { + "key": "protein", + "name": "蛋白质", + "value": "12.5g", + "analysis": "12.5克蛋白质占成人每日推荐摄入量的21%,是良好的蛋白质来源,有助于肌肉修复和生长。" + }, + { + "key": "fat", + "name": "脂肪", + "value": "6.8g", + "analysis": "6.8克脂肪含量适中,主要包含不饱和脂肪酸,有助于维持正常的生理功能。" + }, + { + "key": "carbohydrate", + "name": "碳水化合物", + "value": "28.5g", + "analysis": "28.5克碳水化合物提供主要能量来源,建议搭配运动以充分利用能量。" + }, + { + "key": "sodium", + "name": "钠", + "value": "480mg", + "analysis": "480毫克钠含量适中,约占成人每日推荐摄入量的20%,高血压患者需注意控制总钠摄入。" + } + ] +} +``` + +### 错误响应 + +```json +{ + "success": false, + "data": [], + "message": "错误描述信息" +} +``` + +## 响应字段说明 + +### 通用字段 + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| success | boolean | 操作是否成功 | +| data | array | 营养成分分析结果数组 | +| message | string | 错误信息(仅在失败时返回) | + +### 营养成分项字段 (data数组中的对象) + +| 字段名 | 类型 | 说明 | 示例 | +|--------|------|------|------| +| key | string | 营养素的唯一标识符 | `energy_kcal` | +| name | string | 营养素的中文名称 | `热量` | +| value | string | 从图片中识别的原始值和单位 | `840千焦` | +| analysis | string | 针对该营养素的详细健康建议 | `840千焦约等于201卡路里...` | + +## 支持的营养素类型 + +| 营养素 | key值 | 中文名称 | +|--------|-------|----------| +| 热量/能量 | energy_kcal | 热量 | +| 蛋白质 | protein | 蛋白质 | +| 脂肪 | fat | 脂肪 | +| 碳水化合物 | carbohydrate | 碳水化合物 | +| 膳食纤维 | fiber | 膳食纤维 | +| 钠 | sodium | 钠 | +| 钙 | calcium | 钙 | +| 铁 | iron | 铁 | +| 锌 | zinc | 锌 | +| 维生素C | vitamin_c | 维生素C | +| 维生素A | vitamin_a | 维生素A | +| 维生素D | vitamin_d | 维生素D | +| 维生素E | vitamin_e | 维生素E | +| 维生素B1 | vitamin_b1 | 维生素B1 | +| 维生素B2 | vitamin_b2 | 维生素B2 | +| 维生素B6 | vitamin_b6 | 维生素B6 | +| 维生素B12 | vitamin_b12 | 维生素B12 | +| 叶酸 | folic_acid | 叶酸 | +| 胆固醇 | cholesterol | 胆固醇 | +| 饱和脂肪 | saturated_fat | 饱和脂肪 | +| 反式脂肪 | trans_fat | 反式脂肪 | +| 糖 | sugar | 糖 | + +## 错误码说明 + +| HTTP状态码 | 错误信息 | 说明 | +|------------|----------|------| +| 400 | 请提供图片URL | 请求体中缺少imageUrl参数 | +| 400 | 图片URL格式不正确 | 提供的URL格式无效 | +| 401 | 未授权访问 | 缺少或无效的JWT令牌 | +| 500 | 营养成分表分析失败,请稍后重试 | AI模型调用失败或服务器内部错误 | +| 500 | 图片中未检测到有效的营养成分表信息 | 图片中未识别到营养成分表 | + +## 使用注意事项 + +### 图片要求 + +1. **图片格式**: 支持 JPG、PNG、WebP 格式 +2. **图片内容**: 必须包含清晰的营养成分表 +3. **图片质量**: 建议使用高清、无模糊、光线充足的图片 +4. **URL要求**: 图片URL必须是公网可访问的地址 + +### 最佳实践 + +1. **URL有效性**: 确保提供的图片URL在分析期间保持可访问 +2. **图片预处理**: 建议在客户端对图片进行适当的裁剪,突出营养成分表部分 +3. **错误处理**: 客户端应妥善处理各种错误情况,提供友好的用户提示 +4. **重试机制**: 对于网络或服务器错误,建议实现适当的重试机制 + +### 限制说明 + +1. **调用频率**: 建议客户端控制调用频率,避免过于频繁的请求 +2. **图片大小**: 虽然不直接限制图片大小,但过大的图片可能影响处理速度 +3. **并发限制**: 服务端可能有并发请求限制,建议客户端实现队列机制 + +## 客户端集成示例 + +### JavaScript/TypeScript 示例 + +```typescript +interface NutritionAnalysisRequest { + imageUrl: string; +} + +interface NutritionAnalysisItem { + key: string; + name: string; + value: string; + analysis: string; +} + +interface NutritionAnalysisResponse { + success: boolean; + data: NutritionAnalysisItem[]; + message?: string; +} + +async function analyzeNutritionImage( + imageUrl: string, + token: string +): Promise { + try { + const response = await fetch('/diet-records/analyze-nutrition-image', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ imageUrl }) + }); + + const result = await response.json(); + + if (!response.ok) { + throw new Error(result.message || '请求失败'); + } + + return result; + } catch (error) { + console.error('营养成分分析失败:', error); + throw error; + } +} + +// 使用示例 +const token = 'your-jwt-token'; +const imageUrl = 'https://example.com/nutrition-label.jpg'; + +analyzeNutritionImage(imageUrl, token) + .then(result => { + if (result.success) { + console.log('识别到营养素数量:', result.data.length); + result.data.forEach(item => { + console.log(`${item.name}: ${item.value}`); + console.log(`建议: ${item.analysis}`); + }); + } else { + console.error('分析失败:', result.message); + } + }) + .catch(error => { + console.error('请求异常:', error); + }); +``` + +### Swift 示例 + +```swift +struct NutritionAnalysisRequest: Codable { + let imageUrl: String +} + +struct NutritionAnalysisItem: Codable { + let key: String + let name: String + let value: String + let analysis: String +} + +struct NutritionAnalysisResponse: Codable { + let success: Bool + let data: [NutritionAnalysisItem] + let message: String? +} + +class NutritionAnalysisService { + func analyzeNutritionImage(imageUrl: String, token: String) async throws -> NutritionAnalysisResponse { + guard let url = URL(string: "/diet-records/analyze-nutrition-image") else { + throw URLError(.badURL) + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + + let requestBody = NutritionAnalysisRequest(imageUrl: imageUrl) + request.httpBody = try JSONEncoder().encode(requestBody) + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse else { + throw URLError(.badServerResponse) + } + + guard 200...299 ~= httpResponse.statusCode else { + throw NSError(domain: "APIError", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: "HTTP Error"]) + } + + let result = try JSONDecoder().decode(NutritionAnalysisResponse.self, from: data) + return result + } +} +``` + +## 更新日志 + +| 版本 | 日期 | 更新内容 | +|------|------|----------| +| 1.0.0 | 2024-10-16 | 初始版本,支持营养成分表图片分析功能 | + +## 技术支持 + +如有技术问题或集成困难,请联系开发团队获取支持。 \ No newline at end of file diff --git a/src/diet-records/diet-records.controller.ts b/src/diet-records/diet-records.controller.ts index c30e9f8..b5a64d6 100644 --- a/src/diet-records/diet-records.controller.ts +++ b/src/diet-records/diet-records.controller.ts @@ -15,7 +15,10 @@ import { } from '@nestjs/common'; import { ApiOperation, ApiBody, ApiResponse, ApiTags, ApiQuery } from '@nestjs/swagger'; import { DietRecordsService } from './diet-records.service'; +import { NutritionAnalysisService } from './services/nutrition-analysis.service'; import { CreateDietRecordDto, UpdateDietRecordDto, GetDietHistoryQueryDto, DietRecordResponseDto, DietHistoryResponseDto, NutritionSummaryDto, FoodRecognitionRequestDto, FoodRecognitionResponseDto, FoodRecognitionToDietRecordsResponseDto } from '../users/dto/diet-record.dto'; +import { NutritionAnalysisResponseDto } from './dto/nutrition-analysis.dto'; +import { NutritionAnalysisRequestDto } from './dto/nutrition-analysis-request.dto'; import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; import { CurrentUser } from '../common/decorators/current-user.decorator'; import { AccessTokenPayload } from '../users/services/apple-auth.service'; @@ -27,6 +30,7 @@ export class DietRecordsController { constructor( private readonly dietRecordsService: DietRecordsService, + private readonly nutritionAnalysisService: NutritionAnalysisService, ) { } /** @@ -161,4 +165,57 @@ export class DietRecordsController { requestDto.mealType ); } + + /** + * 分析食物营养成分表图片 + */ + @UseGuards(JwtAuthGuard) + @Post('analyze-nutrition-image') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '分析食物营养成分表图片' }) + @ApiBody({ type: NutritionAnalysisRequestDto }) + @ApiResponse({ status: 200, description: '成功分析营养成分表', type: NutritionAnalysisResponseDto }) + @ApiResponse({ status: 400, description: '请求参数错误' }) + @ApiResponse({ status: 401, description: '未授权访问' }) + @ApiResponse({ status: 500, description: '服务器内部错误' }) + async analyzeNutritionImage( + @Body() requestDto: NutritionAnalysisRequestDto, + @CurrentUser() user: AccessTokenPayload, + ): Promise { + this.logger.log(`分析营养成分表 - 用户ID: ${user.sub}, 图片URL: ${requestDto.imageUrl}`); + + if (!requestDto.imageUrl) { + return { + success: false, + data: [], + message: '请提供图片URL' + }; + } + + // 验证URL格式 + try { + new URL(requestDto.imageUrl); + } catch (error) { + return { + success: false, + data: [], + message: '图片URL格式不正确' + }; + } + + try { + const result = await this.nutritionAnalysisService.analyzeNutritionImage(requestDto.imageUrl); + + this.logger.log(`营养成分表分析完成 - 用户ID: ${user.sub}, 成功: ${result.success}, 营养素数量: ${result.data.length}`); + + return result; + } catch (error) { + this.logger.error(`营养成分表分析失败 - 用户ID: ${user.sub}, 错误: ${error instanceof Error ? error.message : String(error)}`); + return { + success: false, + data: [], + message: '营养成分表分析失败,请稍后重试' + }; + } + } } \ No newline at end of file diff --git a/src/diet-records/diet-records.module.ts b/src/diet-records/diet-records.module.ts index 5f526b1..729e83a 100644 --- a/src/diet-records/diet-records.module.ts +++ b/src/diet-records/diet-records.module.ts @@ -2,6 +2,7 @@ import { Module, forwardRef } from '@nestjs/common'; import { SequelizeModule } from '@nestjs/sequelize'; import { DietRecordsController } from './diet-records.controller'; import { DietRecordsService } from './diet-records.service'; +import { NutritionAnalysisService } from './services/nutrition-analysis.service'; import { UserDietHistory } from '../users/models/user-diet-history.model'; import { ActivityLog } from '../activity-logs/models/activity-log.model'; import { UsersModule } from '../users/users.module'; @@ -14,7 +15,7 @@ import { AiCoachModule } from '../ai-coach/ai-coach.module'; forwardRef(() => AiCoachModule), ], controllers: [DietRecordsController], - providers: [DietRecordsService], - exports: [DietRecordsService], + providers: [DietRecordsService, NutritionAnalysisService], + exports: [DietRecordsService, NutritionAnalysisService], }) export class DietRecordsModule { } \ No newline at end of file diff --git a/src/diet-records/dto/nutrition-analysis-request.dto.ts b/src/diet-records/dto/nutrition-analysis-request.dto.ts new file mode 100644 index 0000000..d2d8e90 --- /dev/null +++ b/src/diet-records/dto/nutrition-analysis-request.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; + +/** + * 营养成分分析请求DTO + */ +export class NutritionAnalysisRequestDto { + @ApiProperty({ + description: '营养成分表图片URL', + example: 'https://example.com/nutrition-label.jpg', + required: true + }) + imageUrl: string; +} \ No newline at end of file diff --git a/src/diet-records/dto/nutrition-analysis.dto.ts b/src/diet-records/dto/nutrition-analysis.dto.ts new file mode 100644 index 0000000..137ea6c --- /dev/null +++ b/src/diet-records/dto/nutrition-analysis.dto.ts @@ -0,0 +1,32 @@ +import { ApiProperty } from '@nestjs/swagger'; + +/** + * 营养成分分析结果项 + */ +export class NutritionAnalysisItemDto { + @ApiProperty({ description: '营养素的唯一标识', example: 'energy_kcal' }) + key: string; + + @ApiProperty({ description: '营养素的中文名称', example: '热量' }) + name: string; + + @ApiProperty({ description: '从图片中识别的原始值和单位', example: '840千焦' }) + value: string; + + @ApiProperty({ description: '针对该营养素的详细健康建议', example: '840千焦约等于201卡路里,占成人每日推荐摄入总热量的10%,属于中等热量水平。' }) + analysis: string; +} + +/** + * 营养成分分析响应DTO + */ +export class NutritionAnalysisResponseDto { + @ApiProperty({ description: '操作是否成功', example: true }) + success: boolean; + + @ApiProperty({ description: '营养成分分析结果数组', type: [NutritionAnalysisItemDto] }) + data: NutritionAnalysisItemDto[]; + + @ApiProperty({ description: '响应消息', required: false }) + message?: string; +} \ No newline at end of file diff --git a/src/diet-records/services/nutrition-analysis.service.ts b/src/diet-records/services/nutrition-analysis.service.ts new file mode 100644 index 0000000..5299eb7 --- /dev/null +++ b/src/diet-records/services/nutrition-analysis.service.ts @@ -0,0 +1,282 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { OpenAI } from 'openai'; + +/** + * 营养成分分析结果接口 + */ +export interface NutritionAnalysisResult { + key: string; // 营养素的唯一标识,如 energy_kcal + name: string; // 营养素的中文名称,如"热量" + value: string; // 从图片中识别的原始值和单位,如"840千焦" + analysis: string; // 针对该营养素的详细健康建议 +} + +/** + * 营养成分分析响应接口 + */ +export interface NutritionAnalysisResponse { + success: boolean; + data: NutritionAnalysisResult[]; + message?: string; +} + +/** + * 营养成分分析服务 + * 负责处理食物营养成分表的AI分析 + * + * 支持多种AI模型: + * - GLM-4.5V (智谱AI) - 设置 AI_VISION_PROVIDER=glm + * - Qwen VL (阿里云DashScope) - 设置 AI_VISION_PROVIDER=dashscope (默认) + */ +@Injectable() +export class NutritionAnalysisService { + private readonly logger = new Logger(NutritionAnalysisService.name); + private readonly client: OpenAI; + private readonly visionModel: string; + private readonly apiProvider: string; + + constructor(private readonly configService: ConfigService) { + // 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.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.visionModel = this.configService.get('DASHSCOPE_VISION_MODEL') || 'qwen-vl-max'; + } + } + + /** + * 分析食物营养成分表图片 + * @param imageUrl 图片URL + * @returns 营养成分分析结果 + */ + async analyzeNutritionImage(imageUrl: string): Promise { + try { + this.logger.log(`开始分析营养成分表图片: ${imageUrl}`); + + const prompt = this.buildNutritionAnalysisPrompt(); + + const completion = await this.makeVisionApiCall(prompt, [imageUrl]); + + const rawResult = completion.choices?.[0]?.message?.content || '[]'; + this.logger.log(`营养成分分析原始结果: ${rawResult}`); + + return this.parseNutritionAnalysisResult(rawResult); + } catch (error) { + this.logger.error(`营养成分表分析失败: ${error instanceof Error ? error.message : String(error)}`); + return { + success: false, + data: [], + message: '营养成分表分析失败,请稍后重试' + }; + } + } + + /** + * 制作视觉模型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, + }; + + // 处理图片URL + const processedImages = imageUrls.map((imageUrl) => ({ + type: 'image_url', + image_url: { url: imageUrl } 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 }, + ...processedImages, + ] as any, + }, + ], + } as any); + } else { + // DashScope format (default) + return await this.client.chat.completions.create({ + ...baseParams, + messages: [ + { + role: 'user', + content: [ + { type: 'text', text: prompt }, + ...processedImages, + ] as any, + }, + ], + }); + } + } + + /** + * 构建营养成分分析提示 + * @returns 提示文本 + */ + private buildNutritionAnalysisPrompt(): string { + return `作为专业的营养分析师,请仔细分析这张图片中的营养成分表。 + +**任务要求:** +1. 识别图片中的营养成分表,提取所有可见的营养素信息 +2. 为每个营养素提供详细的健康建议和分析 +3. 返回严格的JSON数组格式,不包含任何额外的解释或对话文本 + +**输出格式要求:** +请严格按照以下JSON数组格式返回,每个对象包含四个字段: +[ + { + "key": "energy_kcal", + "name": "热量", + "value": "840千焦", + "analysis": "840千焦约等于201卡路里,占成人每日推荐摄入总热量的10%,属于中等热量水平。" + }, + { + "key": "protein", + "name": "蛋白质", + "value": "12.5g", + "analysis": "12.5克蛋白质占成人每日推荐摄入量的21%,是良好的蛋白质来源,有助于肌肉修复和生长。" + } +] + +**营养素标识符对照表:** +- 热量/能量: energy_kcal +- 蛋白质: protein +- 脂肪: fat +- 碳水化合物: carbohydrate +- 膳食纤维: fiber +- 钠: sodium +- 钙: calcium +- 铁: iron +- 锌: zinc +- 维生素C: vitamin_c +- 维生素A: vitamin_a +- 维生素D: vitamin_d +- 维生素E: vitamin_e +- 维生素B1: vitamin_b1 +- 维生素B2: vitamin_b2 +- 维生素B6: vitamin_b6 +- 维生素B12: vitamin_b12 +- 叶酸: folic_acid +- 胆固醇: cholesterol +- 饱和脂肪: saturated_fat +- 反式脂肪: trans_fat +- 糖: sugar +- 其他营养素: other_nutrient + +**分析要求:** +1. 如果图片中没有营养成分表,返回空数组 [] +2. 为每个识别到的营养素提供具体的健康建议 +3. 建议应包含营养素的作用、摄入量参考和健康影响 +4. 数值分析要准确,建议要专业且实用 +5. 只返回JSON数组,不要包含任何其他文本 + +**重要提醒:** +- 严格按照JSON数组格式返回 +- 不要添加任何解释性文字或对话内容 +- 确保JSON格式正确,可以被直接解析`; + } + + /** + * 解析营养成分分析结果 + * @param rawResult 原始结果字符串 + * @returns 解析后的分析结果 + */ + private parseNutritionAnalysisResult(rawResult: string): NutritionAnalysisResponse { + try { + // 尝试解析JSON + let parsedResult: any; + try { + parsedResult = JSON.parse(rawResult); + } catch (parseError) { + this.logger.error(`营养成分分析JSON解析失败: ${parseError}`); + this.logger.error(`原始结果: ${rawResult}`); + return { + success: false, + data: [], + message: '营养成分表解析失败,无法识别有效的营养信息' + }; + } + + // 确保结果是数组 + if (!Array.isArray(parsedResult)) { + this.logger.error(`营养成分分析结果不是数组格式: ${typeof parsedResult}`); + return { + success: false, + data: [], + message: '营养成分表格式错误,无法识别有效的营养信息' + }; + } + + // 验证和标准化每个营养素项 + const nutritionData: NutritionAnalysisResult[] = []; + + for (const item of parsedResult) { + if (item && typeof item === 'object' && item.key && item.name && item.value && item.analysis) { + nutritionData.push({ + key: String(item.key).trim(), + name: String(item.name).trim(), + value: String(item.value).trim(), + analysis: String(item.analysis).trim() + }); + } else { + this.logger.warn(`跳过无效的营养素项: ${JSON.stringify(item)}`); + } + } + + if (nutritionData.length === 0) { + return { + success: false, + data: [], + message: '图片中未检测到有效的营养成分表信息' + }; + } + + this.logger.log(`成功解析 ${nutritionData.length} 项营养素信息`); + + return { + success: true, + data: nutritionData + }; + } catch (error) { + this.logger.error(`营养成分分析结果处理失败: ${error instanceof Error ? error.message : String(error)}`); + return { + success: false, + data: [], + message: '营养成分表处理失败,请稍后重试' + }; + } + } +} \ No newline at end of file