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