feat(diet-records): 新增营养成分分析记录功能
- 添加营养成分分析记录数据模型和数据库集成 - 实现分析记录保存功能,支持成功和失败状态记录 - 新增获取用户营养成分分析记录的API接口 - 支持按日期范围、状态等条件筛选查询 - 提供分页查询功能,优化大数据量场景性能
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { OpenAI } from 'openai';
|
||||
import { InjectModel } from '@nestjs/sequelize';
|
||||
import { Op } from 'sequelize';
|
||||
import { ResponseCode } from '../../base.dto';
|
||||
import { NutritionAnalysisRecord } from '../models/nutrition-analysis-record.model';
|
||||
|
||||
/**
|
||||
* 营养成分分析结果接口
|
||||
@@ -38,7 +41,11 @@ export class NutritionAnalysisService {
|
||||
private readonly visionModel: string;
|
||||
private readonly apiProvider: string;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
@InjectModel(NutritionAnalysisRecord)
|
||||
private readonly nutritionAnalysisRecordModel: typeof NutritionAnalysisRecord,
|
||||
) {
|
||||
// Support both GLM-4.5V and DashScope (Qwen) models
|
||||
this.apiProvider = this.configService.get<string>('AI_VISION_PROVIDER') || 'dashscope';
|
||||
|
||||
@@ -72,9 +79,9 @@ export class NutritionAnalysisService {
|
||||
* @param imageUrl 图片URL
|
||||
* @returns 营养成分分析结果
|
||||
*/
|
||||
async analyzeNutritionImage(imageUrl: string): Promise<NutritionAnalysisResponse> {
|
||||
async analyzeNutritionImage(imageUrl: string, userId?: string): Promise<NutritionAnalysisResponse> {
|
||||
try {
|
||||
this.logger.log(`开始分析营养成分表图片: ${imageUrl}`);
|
||||
this.logger.log(`开始分析营养成分表图片: ${imageUrl}, 用户ID: ${userId}`);
|
||||
|
||||
const prompt = this.buildNutritionAnalysisPrompt();
|
||||
|
||||
@@ -83,9 +90,26 @@ export class NutritionAnalysisService {
|
||||
const rawResult = completion.choices?.[0]?.message?.content || '{"code": 1, "msg": "未获取到AI模型响应", "data": []}';
|
||||
this.logger.log(`营养成分分析原始结果: ${rawResult}`);
|
||||
|
||||
return this.parseNutritionAnalysisResult(rawResult);
|
||||
const result = this.parseNutritionAnalysisResult(rawResult);
|
||||
|
||||
// 如果提供了用户ID,保存分析记录
|
||||
if (userId) {
|
||||
await this.saveAnalysisRecord(userId, imageUrl, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error(`营养成分表分析失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||
|
||||
// 如果提供了用户ID,保存失败记录
|
||||
if (userId) {
|
||||
await this.saveAnalysisRecord(userId, imageUrl, {
|
||||
success: false,
|
||||
data: [],
|
||||
message: '营养成分表分析失败,请稍后重试'
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
@@ -318,4 +342,90 @@ export class NutritionAnalysisService {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存营养成分分析记录
|
||||
* @param userId 用户ID
|
||||
* @param imageUrl 图片URL
|
||||
* @param result 分析结果
|
||||
*/
|
||||
private async saveAnalysisRecord(
|
||||
userId: string,
|
||||
imageUrl: string,
|
||||
result: NutritionAnalysisResponse
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.nutritionAnalysisRecordModel.create({
|
||||
userId,
|
||||
imageUrl,
|
||||
analysisResult: result,
|
||||
status: result.success ? 'success' : 'failed',
|
||||
message: result.message || '',
|
||||
aiProvider: this.apiProvider,
|
||||
aiModel: this.visionModel,
|
||||
nutritionCount: result.data.length,
|
||||
});
|
||||
this.logger.log(`营养成分分析记录已保存 - 用户ID: ${userId}, 成功: ${result.success}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`保存营养成分分析记录失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的营养成分分析记录
|
||||
* @param userId 用户ID
|
||||
* @param query 查询参数
|
||||
* @returns 分析记录列表
|
||||
*/
|
||||
async getAnalysisRecords(
|
||||
userId: string,
|
||||
query: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
status?: string;
|
||||
}
|
||||
): Promise<{
|
||||
records: NutritionAnalysisRecord[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
}> {
|
||||
const where: any = { userId, deleted: false };
|
||||
|
||||
// 日期过滤
|
||||
if (query.startDate || query.endDate) {
|
||||
where.createdAt = {} as any;
|
||||
if (query.startDate) where.createdAt[Op.gte] = new Date(query.startDate);
|
||||
if (query.endDate) where.createdAt[Op.lte] = new Date(query.endDate);
|
||||
}
|
||||
|
||||
// 状态过滤
|
||||
if (query.status) {
|
||||
where.status = query.status;
|
||||
}
|
||||
|
||||
const limit = Math.min(100, Math.max(1, query.limit || 20));
|
||||
const page = Math.max(1, query.page || 1);
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const { rows, count } = await this.nutritionAnalysisRecordModel.findAndCountAll({
|
||||
where,
|
||||
order: [['created_at', 'DESC']],
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
|
||||
const totalPages = Math.ceil(count / limit);
|
||||
|
||||
return {
|
||||
records: rows,
|
||||
total: count,
|
||||
page,
|
||||
limit,
|
||||
totalPages,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user