diff --git a/.kilocode/rules/rule.md b/.kilocode/rules/rule.md index 697eed4..c76e1e8 100644 --- a/.kilocode/rules/rule.md +++ b/.kilocode/rules/rule.md @@ -7,3 +7,5 @@ - 不要随意新增 markdown 文档 - 代码提交 message 用中文 - 注意代码的可读性、架构实现要清晰 +- 不要随意新增示例文件 +- 接口规范: 接口的返回都需要遵循 base.dto.ts 文件中的规范 diff --git a/src/ai-coach/dto/ai-chat.dto.ts b/src/ai-coach/dto/ai-chat.dto.ts index a75576e..2d9984e 100644 --- a/src/ai-coach/dto/ai-chat.dto.ts +++ b/src/ai-coach/dto/ai-chat.dto.ts @@ -152,7 +152,7 @@ export class MealDto { items: MealItemDto[]; } -export class NutritionAnalysisRequestDto { +export class NutritionAnalysisChatRequestDto { @ApiProperty({ description: '会话ID。未提供则创建新会话' }) @IsOptional() @IsString() diff --git a/src/base.dto.ts b/src/base.dto.ts index ce3b6cf..38aa0e4 100644 --- a/src/base.dto.ts +++ b/src/base.dto.ts @@ -1,3 +1,5 @@ +import { ApiProperty } from '@nestjs/swagger'; + export enum ResponseCode { SUCCESS = 0, ERROR = 1, @@ -8,3 +10,38 @@ export interface BaseResponseDto { message: string; data: T; } + +/** + * 通用API响应结构体 + * 包含code、message和data字段,各业务场景可以继承并只需定义data类型 + */ +export class ApiResponseDto { + @ApiProperty({ description: '响应状态码', example: ResponseCode.SUCCESS }) + code: ResponseCode; + + @ApiProperty({ description: '响应消息', example: '操作成功' }) + message: string; + + @ApiProperty({ description: '响应数据' }) + data: T; + + constructor(code: ResponseCode, message: string, data: T) { + this.code = code; + this.message = message; + this.data = data; + } + + /** + * 创建成功响应 + */ + static success(data: T, message: string = '操作成功'): ApiResponseDto { + return new ApiResponseDto(ResponseCode.SUCCESS, message, data); + } + + /** + * 创建失败响应 + */ + static error(message: string = '操作失败', data: T = null as any): ApiResponseDto { + return new ApiResponseDto(ResponseCode.ERROR, message, data); + } +} diff --git a/src/diet-records/diet-records.controller.ts b/src/diet-records/diet-records.controller.ts index b5a64d6..bee9ac6 100644 --- a/src/diet-records/diet-records.controller.ts +++ b/src/diet-records/diet-records.controller.ts @@ -185,22 +185,7 @@ export class DietRecordsController { 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格式不正确' - }; + return NutritionAnalysisResponseDto.createError('请提供图片URL'); } try { @@ -208,14 +193,15 @@ export class DietRecordsController { this.logger.log(`营养成分表分析完成 - 用户ID: ${user.sub}, 成功: ${result.success}, 营养素数量: ${result.data.length}`); - return result; + // 转换旧的响应格式到新的通用格式 + if (result.success) { + return NutritionAnalysisResponseDto.createSuccess(result.data, result.message || '分析成功'); + } else { + return NutritionAnalysisResponseDto.createError(result.message || '分析失败'); + } } catch (error) { this.logger.error(`营养成分表分析失败 - 用户ID: ${user.sub}, 错误: ${error instanceof Error ? error.message : String(error)}`); - return { - success: false, - data: [], - message: '营养成分表分析失败,请稍后重试' - }; + return NutritionAnalysisResponseDto.createError('营养成分表分析失败,请稍后重试'); } } } \ 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 index d2d8e90..b9b7b1b 100644 --- a/src/diet-records/dto/nutrition-analysis-request.dto.ts +++ b/src/diet-records/dto/nutrition-analysis-request.dto.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsNotEmpty, IsUrl } from 'class-validator'; /** * 营养成分分析请求DTO @@ -9,5 +10,8 @@ export class NutritionAnalysisRequestDto { example: 'https://example.com/nutrition-label.jpg', required: true }) + @IsString() + @IsNotEmpty() + @IsUrl() 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 index 137ea6c..46f192e 100644 --- a/src/diet-records/dto/nutrition-analysis.dto.ts +++ b/src/diet-records/dto/nutrition-analysis.dto.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; +import { ApiResponseDto } from '../../base.dto'; /** * 营养成分分析结果项 @@ -19,14 +20,24 @@ export class NutritionAnalysisItemDto { /** * 营养成分分析响应DTO + * 使用通用响应结构体 */ -export class NutritionAnalysisResponseDto { - @ApiProperty({ description: '操作是否成功', example: true }) - success: boolean; +export class NutritionAnalysisResponseDto extends ApiResponseDto { + constructor(code: number, message: string, data: NutritionAnalysisItemDto[]) { + super(code, message, data); + } - @ApiProperty({ description: '营养成分分析结果数组', type: [NutritionAnalysisItemDto] }) - data: NutritionAnalysisItemDto[]; + /** + * 创建成功响应 + */ + static createSuccess(data: NutritionAnalysisItemDto[], message: string = '营养成分分析成功'): NutritionAnalysisResponseDto { + return new NutritionAnalysisResponseDto(0, message, data); + } - @ApiProperty({ description: '响应消息', required: false }) - message?: string; + /** + * 创建失败响应 + */ + static createError(message: string = '营养成分分析失败'): NutritionAnalysisResponseDto { + return new NutritionAnalysisResponseDto(1, message, []); + } } \ 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 index 5299eb7..5f21090 100644 --- a/src/diet-records/services/nutrition-analysis.service.ts +++ b/src/diet-records/services/nutrition-analysis.service.ts @@ -1,6 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { OpenAI } from 'openai'; +import { ResponseCode } from '../../base.dto'; /** * 营养成分分析结果接口 @@ -14,6 +15,7 @@ export interface NutritionAnalysisResult { /** * 营养成分分析响应接口 + * 保持向后兼容,内部使用,外部使用 NutritionAnalysisResponseDto */ export interface NutritionAnalysisResponse { success: boolean; @@ -78,7 +80,7 @@ export class NutritionAnalysisService { const completion = await this.makeVisionApiCall(prompt, [imageUrl]); - const rawResult = completion.choices?.[0]?.message?.content || '[]'; + const rawResult = completion.choices?.[0]?.message?.content || '{"code": 1, "msg": "未获取到AI模型响应", "data": []}'; this.logger.log(`营养成分分析原始结果: ${rawResult}`); return this.parseNutritionAnalysisResult(rawResult); @@ -152,24 +154,36 @@ export class NutritionAnalysisService { **任务要求:** 1. 识别图片中的营养成分表,提取所有可见的营养素信息 2. 为每个营养素提供详细的健康建议和分析 -3. 返回严格的JSON数组格式,不包含任何额外的解释或对话文本 +3. 返回严格的JSON格式,包含code、msg和data字段 **输出格式要求:** -请严格按照以下JSON数组格式返回,每个对象包含四个字段: -[ - { - "key": "energy_kcal", - "name": "热量", - "value": "840千焦", - "analysis": "840千焦约等于201卡路里,占成人每日推荐摄入总热量的10%,属于中等热量水平。" - }, - { - "key": "protein", - "name": "蛋白质", - "value": "12.5g", - "analysis": "12.5克蛋白质占成人每日推荐摄入量的21%,是良好的蛋白质来源,有助于肌肉修复和生长。" - } -] +请严格按照以下JSON格式返回: +{ + "code": 0, + "msg": "分析成功", + "data": [ + { + "key": "energy_kcal", + "name": "热量", + "value": "840千焦", + "analysis": "840千焦约等于201卡路里,占成人每日推荐摄入总热量的10%,属于中等热量水平。" + }, + { + "key": "protein", + "name": "蛋白质", + "value": "12.5g", + "analysis": "12.5克蛋白质占成人每日推荐摄入量的21%,是良好的蛋白质来源,有助于肌肉修复和生长。" + } + ] +} + +**失败情况格式:** +如果无法识别营养成分表或分析失败,请返回: +{ + "code": 1, + "msg": "失败原因的具体描述", + "data": [] +} **营养素标识符对照表:** - 热量/能量: energy_kcal @@ -197,16 +211,18 @@ export class NutritionAnalysisService { - 其他营养素: other_nutrient **分析要求:** -1. 如果图片中没有营养成分表,返回空数组 [] -2. 为每个识别到的营养素提供具体的健康建议 -3. 建议应包含营养素的作用、摄入量参考和健康影响 -4. 数值分析要准确,建议要专业且实用 -5. 只返回JSON数组,不要包含任何其他文本 +1. 如果成功识别营养成分表,code设为0,msg为"分析成功" +2. 如果无法识别或分析失败,code设为1,msg详细说明失败原因 +3. 为每个识别到的营养素提供具体的健康建议 +4. 建议应包含营养素的作用、摄入量参考和健康影响 +5. 数值分析要准确,建议要专业且实用 +6. 只返回JSON对象,不要包含任何其他文本 **重要提醒:** -- 严格按照JSON数组格式返回 +- 严格按照JSON对象格式返回,包含code、msg和data字段 - 不要添加任何解释性文字或对话内容 -- 确保JSON格式正确,可以被直接解析`; +- 确保JSON格式正确,可以被直接解析 +- 必须返回完整的JSON结构,即使分析失败也要返回code和msg字段`; } /** @@ -230,9 +246,9 @@ export class NutritionAnalysisService { }; } - // 确保结果是数组 - if (!Array.isArray(parsedResult)) { - this.logger.error(`营养成分分析结果不是数组格式: ${typeof parsedResult}`); + // 检查响应格式 {code, msg, data} + if (!parsedResult || typeof parsedResult !== 'object' || !('code' in parsedResult)) { + this.logger.error(`营养成分分析结果格式不正确: ${JSON.stringify(parsedResult)}`); return { success: false, data: [], @@ -240,10 +256,32 @@ export class NutritionAnalysisService { }; } + const { code, msg, data } = parsedResult; + + // 如果大模型返回失败状态 + if (code === ResponseCode.ERROR) { + this.logger.warn(`大模型分析失败: ${msg || '未知错误'}`); + return { + success: false, + data: [], + message: msg || '营养成分表分析失败' + }; + } + + // 检查data是否为数组 + if (!Array.isArray(data)) { + this.logger.error(`营养成分分析data字段不是数组格式: ${typeof data}`); + return { + success: false, + data: [], + message: '营养成分表格式错误,data字段应为数组' + }; + } + // 验证和标准化每个营养素项 const nutritionData: NutritionAnalysisResult[] = []; - for (const item of parsedResult) { + for (const item of data) { if (item && typeof item === 'object' && item.key && item.name && item.value && item.analysis) { nutritionData.push({ key: String(item.key).trim(), @@ -260,7 +298,7 @@ export class NutritionAnalysisService { return { success: false, data: [], - message: '图片中未检测到有效的营养成分表信息' + message: msg || '图片中未检测到有效的营养成分表信息' }; } @@ -270,6 +308,7 @@ export class NutritionAnalysisService { success: true, data: nutritionData }; + } catch (error) { this.logger.error(`营养成分分析结果处理失败: ${error instanceof Error ? error.message : String(error)}`); return { diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index af4ae53..b9ed63c 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -222,13 +222,6 @@ export class UsersController { return { code: ResponseCode.ERROR, message: '请选择要上传的图片文件' }; } - - this.winstonLogger.info(`receive file, fileSize: ${file.size}`, { - context: 'UsersController', - userId: user?.sub, - file, - }) - const data = await this.cosService.uploadImage(user.sub, file); return data; } catch (error) {