feat(diet-records): 新增营养成分分析记录功能

- 添加营养成分分析记录数据模型和数据库集成
- 实现分析记录保存功能,支持成功和失败状态记录
- 新增获取用户营养成分分析记录的API接口
- 支持按日期范围、状态等条件筛选查询
- 提供分页查询功能,优化大数据量场景性能
This commit is contained in:
richarjiang
2025-10-16 11:25:31 +08:00
parent 91cac3134e
commit 4d1bc9259b
9 changed files with 561 additions and 7 deletions

View File

@@ -0,0 +1,167 @@
# Memory Bank
I am an expert software engineer with a unique characteristic: my memory resets completely between sessions. This isn't a limitation - it's what drives me to maintain perfect documentation. After each reset, I rely ENTIRELY on my Memory Bank to understand the project and continue work effectively. I MUST read ALL memory bank files at the start of EVERY task - this is not optional. The memory bank files are located in `.kilocode/rules/memory-bank` folder.
When I start a task, I will include `[Memory Bank: Active]` at the beginning of my response if I successfully read the memory bank files, or `[Memory Bank: Missing]` if the folder doesn't exist or is empty. If memory bank is missing, I will warn the user about potential issues and suggest initialization.
## Memory Bank Structure
The Memory Bank consists of core files and optional context files, all in Markdown format.
### Core Files (Required)
1. `brief.md`
This file is created and maintained manually by the developer. Don't edit this file directly but suggest to user to update it if it can be improved.
- Foundation document that shapes all other files
- Created at project start if it doesn't exist
- Defines core requirements and goals
- Source of truth for project scope
2. `product.md`
- Why this project exists
- Problems it solves
- How it should work
- User experience goals
3. `context.md`
This file should be short and factual, not creative or speculative.
- Current work focus
- Recent changes
- Next steps
4. `architecture.md`
- System architecture
- Source Code paths
- Key technical decisions
- Design patterns in use
- Component relationships
- Critical implementation paths
5. `tech.md`
- Technologies used
- Development setup
- Technical constraints
- Dependencies
- Tool usage patterns
### Additional Files
Create additional files/folders within memory-bank/ when they help organize:
- `tasks.md` - Documentation of repetitive tasks and their workflows
- Complex feature documentation
- Integration specifications
- API documentation
- Testing strategies
- Deployment procedures
## Core workflows
### Memory Bank Initialization
The initialization step is CRITICALLY IMPORTANT and must be done with extreme thoroughness as it defines all future effectiveness of the Memory Bank. This is the foundation upon which all future interactions will be built.
When user requests initialization of the memory bank (command `initialize memory bank`), I'll perform an exhaustive analysis of the project, including:
- All source code files and their relationships
- Configuration files and build system setup
- Project structure and organization patterns
- Documentation and comments
- Dependencies and external integrations
- Testing frameworks and patterns
I must be extremely thorough during initialization, spending extra time and effort to build a comprehensive understanding of the project. A high-quality initialization will dramatically improve all future interactions, while a rushed or incomplete initialization will permanently limit my effectiveness.
After initialization, I will ask the user to read through the memory bank files and verify product description, used technologies and other information. I should provide a summary of what I've understood about the project to help the user verify the accuracy of the memory bank files. I should encourage the user to correct any misunderstandings or add missing information, as this will significantly improve future interactions.
### Memory Bank Update
Memory Bank updates occur when:
1. Discovering new project patterns
2. After implementing significant changes
3. When user explicitly requests with the phrase **update memory bank** (MUST review ALL files)
4. When context needs clarification
If I notice significant changes that should be preserved but the user hasn't explicitly requested an update, I should suggest: "Would you like me to update the memory bank to reflect these changes?"
To execute Memory Bank update, I will:
1. Review ALL project files
2. Document current state
3. Document Insights & Patterns
4. If requested with additional context (e.g., "update memory bank using information from @/Makefile"), focus special attention on that source
Note: When triggered by **update memory bank**, I MUST review every memory bank file, even if some don't require updates. Focus particularly on context.md as it tracks current state.
### Add Task
When user completes a repetitive task (like adding support for a new model version) and wants to document it for future reference, they can request: **add task** or **store this as a task**.
This workflow is designed for repetitive tasks that follow similar patterns and require editing the same files. Examples include:
- Adding support for new AI model versions
- Implementing new API endpoints following established patterns
- Adding new features that follow existing architecture
Tasks are stored in the file `tasks.md` in the memory bank folder. The file is optional and can be empty. The file can store many tasks.
To execute Add Task workflow:
1. Create or update `tasks.md` in the memory bank folder
2. Document the task with:
- Task name and description
- Files that need to be modified
- Step-by-step workflow followed
- Important considerations or gotchas
- Example of the completed implementation
3. Include any context that was discovered during task execution but wasn't previously documented
Example task entry:
```markdown
## Add New Model Support
**Last performed:** [date]
**Files to modify:**
- `/providers/gemini.md` - Add model to documentation
- `/src/providers/gemini-config.ts` - Add model configuration
- `/src/constants/models.ts` - Add to model list
- `/tests/providers/gemini.test.ts` - Add test cases
**Steps:**
1. Add model configuration with proper token limits
2. Update documentation with model capabilities
3. Add to constants file for UI display
4. Write tests for new model configuration
**Important notes:**
- Check Google's documentation for exact token limits
- Ensure backward compatibility with existing configurations
- Test with actual API calls before committing
```
### Regular Task Execution
In the beginning of EVERY task I MUST read ALL memory bank files - this is not optional.
The memory bank files are located in `.kilocode/rules/memory-bank` folder. If the folder doesn't exist or is empty, I will warn user about potential issues with the memory bank. I will include `[Memory Bank: Active]` at the beginning of my response if I successfully read the memory bank files, or `[Memory Bank: Missing]` if the folder doesn't exist or is empty. If memory bank is missing, I will warn the user about potential issues and suggest initialization. I should briefly summarize my understanding of the project to confirm alignment with the user's expectations, like:
"[Memory Bank: Active] I understand we're building a React inventory system with barcode scanning. Currently implementing the scanner component that needs to work with the backend API."
When starting a task that matches a documented task in `tasks.md`, I should mention this and follow the documented workflow to ensure no steps are missed.
If the task was repetitive and might be needed again, I should suggest: "Would you like me to add this task to the memory bank for future reference?"
In the end of the task, when it seems to be completed, I will update `context.md` accordingly. If the change seems significant, I will suggest to the user: "Would you like me to update memory bank to reflect these changes?" I will not suggest updates for minor changes.
## Context Window Management
When the context window fills up during an extended session:
1. I should suggest updating the memory bank to preserve the current state
2. Recommend starting a fresh conversation/task
3. In the new conversation, I will automatically load the memory bank files to maintain continuity
## Technical Implementation
Memory Bank is built on Kilo Code's Custom Rules feature, with files stored as standard markdown documents that both the user and I can access.
## Important Notes
REMEMBER: After every memory reset, I begin completely fresh. The Memory Bank is my only link to previous work. It must be maintained with precision and clarity, as my effectiveness depends entirely on its accuracy.
If I detect inconsistencies between memory bank files, I should prioritize brief.md and note any discrepancies to the user.
IMPORTANT: I MUST read ALL memory bank files at the start of EVERY task - this is not optional. The memory bank files are located in `.kilocode/rules/memory-bank` folder.

View File

@@ -0,0 +1 @@
构建一个具有 AI 大模型集成的 nestjs 框架的后端服务,主要是关注用户身体健康、饮食习惯等应用场景

View File

@@ -8,4 +8,4 @@
- 代码提交 message 用中文
- 注意代码的可读性、架构实现要清晰
- 不要随意新增示例文件
- 接口规范: 接口的返回都需要遵循 base.dto.ts 文件中的规范
- 接口规范: 接口的返回都需要遵循 base.dto.ts 文件中的规范

View File

@@ -0,0 +1,20 @@
-- 创建营养成分分析记录表
CREATE TABLE IF NOT EXISTS `t_nutrition_analysis_records` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` VARCHAR(255) NOT NULL COMMENT '用户ID',
`image_url` VARCHAR(500) NOT NULL COMMENT '分析图片URL',
`analysis_result` JSON NOT NULL COMMENT '营养成分分析结果',
`status` VARCHAR(50) NULL COMMENT '分析状态',
`message` TEXT NULL COMMENT '分析消息',
`ai_provider` VARCHAR(50) NULL COMMENT 'AI模型提供商',
`ai_model` VARCHAR(100) NULL COMMENT '使用的AI模型',
`nutrition_count` INT NULL COMMENT '识别到的营养素数量',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否已删除',
PRIMARY KEY (`id`),
INDEX `idx_user_id` (`user_id`),
INDEX `idx_created_at` (`created_at`),
INDEX `idx_status` (`status`),
INDEX `idx_user_deleted` (`user_id`, `deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='营养成分分析记录表';

View File

@@ -19,6 +19,7 @@ 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 { NutritionAnalysisRecordsResponseDto, GetNutritionAnalysisRecordsQueryDto, NutritionAnalysisRecordDto } from './dto/nutrition-analysis-record.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';
@@ -189,7 +190,8 @@ export class DietRecordsController {
}
try {
const result = await this.nutritionAnalysisService.analyzeNutritionImage(requestDto.imageUrl);
// 传递用户ID以便保存分析记录
const result = await this.nutritionAnalysisService.analyzeNutritionImage(requestDto.imageUrl, user.sub);
this.logger.log(`营养成分表分析完成 - 用户ID: ${user.sub}, 成功: ${result.success}, 营养素数量: ${result.data.length}`);
@@ -204,4 +206,53 @@ export class DietRecordsController {
return NutritionAnalysisResponseDto.createError('营养成分表分析失败,请稍后重试');
}
}
/**
* 获取营养成分分析记录
*/
@UseGuards(JwtAuthGuard)
@Get('nutrition-analysis-records')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '获取营养成分分析记录' })
@ApiQuery({ name: 'startDate', required: false, description: '开始日期' })
@ApiQuery({ name: 'endDate', required: false, description: '结束日期' })
@ApiQuery({ name: 'status', required: false, description: '分析状态' })
@ApiQuery({ name: 'page', required: false, description: '页码' })
@ApiQuery({ name: 'limit', required: false, description: '每页数量' })
@ApiResponse({ status: 200, description: '成功获取营养成分分析记录', type: NutritionAnalysisRecordsResponseDto })
async getNutritionAnalysisRecords(
@Query() query: GetNutritionAnalysisRecordsQueryDto,
@CurrentUser() user: AccessTokenPayload,
): Promise<NutritionAnalysisRecordsResponseDto> {
this.logger.log(`获取营养成分分析记录 - 用户ID: ${user.sub}`);
try {
const result = await this.nutritionAnalysisService.getAnalysisRecords(user.sub, query);
// 转换为DTO格式
const recordDtos: NutritionAnalysisRecordDto[] = result.records.map(record => ({
id: record.id,
userId: record.userId,
imageUrl: record.imageUrl,
analysisResult: record.analysisResult,
status: record.status || '',
message: record.message || '',
aiProvider: record.aiProvider || '',
aiModel: record.aiModel || '',
nutritionCount: record.nutritionCount || 0,
createdAt: record.createdAt,
updatedAt: record.updatedAt,
}));
return NutritionAnalysisRecordsResponseDto.createSuccess(
recordDtos,
result.total,
result.page,
result.limit
);
} catch (error) {
this.logger.error(`获取营养成分分析记录失败 - 用户ID: ${user.sub}, 错误: ${error instanceof Error ? error.message : String(error)}`);
return NutritionAnalysisRecordsResponseDto.createError('获取营养成分分析记录失败,请稍后重试');
}
}
}

View File

@@ -5,12 +5,13 @@ 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 { NutritionAnalysisRecord } from './models/nutrition-analysis-record.model';
import { UsersModule } from '../users/users.module';
import { AiCoachModule } from '../ai-coach/ai-coach.module';
@Module({
imports: [
SequelizeModule.forFeature([UserDietHistory, ActivityLog]),
SequelizeModule.forFeature([UserDietHistory, ActivityLog, NutritionAnalysisRecord]),
UsersModule,
forwardRef(() => AiCoachModule),
],

View File

@@ -0,0 +1,114 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiResponseDto } from '../../base.dto';
/**
* 营养成分分析记录项DTO
*/
export class NutritionAnalysisRecordDto {
@ApiProperty({ description: '记录ID', example: 1 })
id: number;
@ApiProperty({ description: '用户ID', example: 'user123' })
userId: string;
@ApiProperty({ description: '分析图片URL', example: 'https://example.com/nutrition-label.jpg' })
imageUrl: string;
@ApiProperty({ description: '营养成分分析结果' })
analysisResult: any;
@ApiProperty({ description: '分析状态', example: 'success' })
status: string;
@ApiProperty({ description: '分析消息', example: '分析成功' })
message: string;
@ApiProperty({ description: 'AI模型提供商', example: 'dashscope' })
aiProvider: string;
@ApiProperty({ description: '使用的AI模型', example: 'qwen-vl-max' })
aiModel: string;
@ApiProperty({ description: '识别到的营养素数量', example: 15 })
nutritionCount: number;
@ApiProperty({ description: '创建时间' })
createdAt: Date;
@ApiProperty({ description: '更新时间' })
updatedAt: Date;
}
/**
* 营养成分分析记录列表响应DTO
*/
export class NutritionAnalysisRecordsResponseDto extends ApiResponseDto<{
records: NutritionAnalysisRecordDto[];
total: number;
page: number;
limit: number;
totalPages: number;
}> {
constructor(code: number, message: string, data: {
records: NutritionAnalysisRecordDto[];
total: number;
page: number;
limit: number;
totalPages: number;
}) {
super(code, message, data);
}
/**
* 创建成功响应
*/
static createSuccess(
records: NutritionAnalysisRecordDto[],
total: number,
page: number,
limit: number,
message: string = '获取营养分析记录成功'
): NutritionAnalysisRecordsResponseDto {
const totalPages = Math.ceil(total / limit);
return new NutritionAnalysisRecordsResponseDto(0, message, {
records,
total,
page,
limit,
totalPages,
});
}
/**
* 创建失败响应
*/
static createError(message: string = '获取营养分析记录失败'): NutritionAnalysisRecordsResponseDto {
return new NutritionAnalysisRecordsResponseDto(1, message, {
records: [],
total: 0,
page: 1,
limit: 20,
totalPages: 0,
});
}
}
/**
* 查询营养分析记录请求DTO
*/
export class GetNutritionAnalysisRecordsQueryDto {
@ApiProperty({ description: '页码', example: 1, required: false })
page?: number;
@ApiProperty({ description: '每页数量', example: 20, required: false })
limit?: number;
@ApiProperty({ description: '开始日期', example: '2023-01-01', required: false })
startDate?: string;
@ApiProperty({ description: '结束日期', example: '2023-12-31', required: false })
endDate?: string;
@ApiProperty({ description: '分析状态', example: 'success', required: false })
status?: string;
}

View File

@@ -0,0 +1,90 @@
import { Column, DataType, Model, PrimaryKey, Table } from 'sequelize-typescript';
@Table({
tableName: 't_nutrition_analysis_records',
underscored: true,
})
export class NutritionAnalysisRecord extends Model {
@PrimaryKey
@Column({
type: DataType.BIGINT,
autoIncrement: true,
})
declare id: number;
@Column({
type: DataType.STRING,
allowNull: false,
comment: '用户ID',
})
declare userId: string;
@Column({
type: DataType.STRING,
allowNull: false,
comment: '分析图片URL',
})
declare imageUrl: string;
@Column({
type: DataType.JSON,
allowNull: false,
comment: '营养成分分析结果',
})
declare analysisResult: Record<string, any>;
@Column({
type: DataType.STRING,
allowNull: true,
comment: '分析状态',
})
declare status: string | null;
@Column({
type: DataType.TEXT,
allowNull: true,
comment: '分析消息',
})
declare message: string | null;
@Column({
type: DataType.STRING,
allowNull: true,
comment: 'AI模型提供商',
})
declare aiProvider: string | null;
@Column({
type: DataType.STRING,
allowNull: true,
comment: '使用的AI模型',
})
declare aiModel: string | null;
@Column({
type: DataType.INTEGER,
allowNull: true,
comment: '识别到的营养素数量',
})
declare nutritionCount: number | null;
@Column({
type: DataType.DATE,
defaultValue: DataType.NOW,
})
declare createdAt: Date;
@Column({
type: DataType.DATE,
defaultValue: DataType.NOW,
})
declare updatedAt: Date;
@Column({
type: DataType.BOOLEAN,
allowNull: false,
defaultValue: false,
comment: '是否已删除',
})
declare deleted: boolean;
}

View File

@@ -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,
};
}
}