Files
plates-server/src/diet-records/diet-records.controller.ts
richarjiang 66a9e65d9b feat(diet-records): 新增营养成分分析记录删除功能
添加删除营养成分分析记录的API端点,支持软删除机制
- 新增DELETE /nutrition-analysis-records/:id接口
- 添加DeleteNutritionAnalysisRecordResponseDto响应DTO
- 在NutritionAnalysisService中实现deleteAnalysisRecord方法
- 包含完整的权限验证和错误处理逻辑
2025-10-16 16:43:42 +08:00

298 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
Controller,
Get,
Post,
Body,
Param,
HttpCode,
HttpStatus,
Put,
Delete,
Query,
Logger,
UseGuards,
NotFoundException,
} 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 { NutritionAnalysisRecordsResponseDto, GetNutritionAnalysisRecordsQueryDto, NutritionAnalysisRecordDto, DeleteNutritionAnalysisRecordResponseDto } 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';
@ApiTags('diet-records')
@Controller('diet-records')
export class DietRecordsController {
private readonly logger = new Logger(DietRecordsController.name);
constructor(
private readonly dietRecordsService: DietRecordsService,
private readonly nutritionAnalysisService: NutritionAnalysisService,
) { }
/**
* 添加饮食记录
*/
@UseGuards(JwtAuthGuard)
@Post()
@HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: '添加饮食记录' })
@ApiBody({ type: CreateDietRecordDto })
@ApiResponse({ status: 201, description: '成功添加饮食记录', type: DietRecordResponseDto })
async addDietRecord(
@Body() createDto: CreateDietRecordDto,
@CurrentUser() user: AccessTokenPayload,
): Promise<DietRecordResponseDto> {
this.logger.log(`添加饮食记录 - 用户ID: ${user.sub}, 食物: ${createDto.foodName}`);
return this.dietRecordsService.addDietRecord(user.sub, createDto);
}
/**
* 获取饮食记录历史
*/
@UseGuards(JwtAuthGuard)
@Get()
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '获取饮食记录历史' })
@ApiQuery({ name: 'startDate', required: false, description: '开始日期' })
@ApiQuery({ name: 'endDate', required: false, description: '结束日期' })
@ApiQuery({ name: 'mealType', required: false, description: '餐次类型' })
@ApiQuery({ name: 'page', required: false, description: '页码' })
@ApiQuery({ name: 'limit', required: false, description: '每页数量' })
@ApiResponse({ status: 200, description: '成功获取饮食记录', type: DietHistoryResponseDto })
async getDietHistory(
@Query() query: GetDietHistoryQueryDto,
@CurrentUser() user: AccessTokenPayload,
): Promise<DietHistoryResponseDto> {
this.logger.log(`获取饮食记录 - 用户ID: ${user.sub}`);
return this.dietRecordsService.getDietHistory(user.sub, query);
}
/**
* 更新饮食记录
*/
@UseGuards(JwtAuthGuard)
@Put(':id')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '更新饮食记录' })
@ApiBody({ type: UpdateDietRecordDto })
@ApiResponse({ status: 200, description: '成功更新饮食记录', type: DietRecordResponseDto })
async updateDietRecord(
@Param('id') recordId: string,
@Body() updateDto: UpdateDietRecordDto,
@CurrentUser() user: AccessTokenPayload,
): Promise<DietRecordResponseDto> {
this.logger.log(`更新饮食记录 - 用户ID: ${user.sub}, 记录ID: ${recordId}`);
return this.dietRecordsService.updateDietRecord(user.sub, parseInt(recordId), updateDto);
}
/**
* 删除饮食记录
*/
@UseGuards(JwtAuthGuard)
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: '删除饮食记录' })
@ApiResponse({ status: 204, description: '成功删除饮食记录' })
async deleteDietRecord(
@Param('id') recordId: string,
@CurrentUser() user: AccessTokenPayload,
): Promise<void> {
this.logger.log(`删除饮食记录 - 用户ID: ${user.sub}, 记录ID: ${recordId}`);
const success = await this.dietRecordsService.deleteDietRecord(user.sub, parseInt(recordId));
if (!success) {
throw new NotFoundException('饮食记录不存在');
}
}
/**
* 获取营养汇总分析
*/
@UseGuards(JwtAuthGuard)
@Get('nutrition-summary')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '获取营养汇总分析' })
@ApiQuery({ name: 'mealCount', required: false, description: '分析的餐次数量默认10' })
@ApiResponse({ status: 200, description: '成功获取营养汇总', type: NutritionSummaryDto })
async getNutritionSummary(
@Query('mealCount') mealCount: string,
@CurrentUser() user: AccessTokenPayload,
): Promise<NutritionSummaryDto> {
this.logger.log(`获取营养汇总 - 用户ID: ${user.sub}`);
const count = mealCount ? parseInt(mealCount) : 10;
return this.dietRecordsService.getRecentNutritionSummary(user.sub, count);
}
/**
* 根据图片URL识别食物并转换为饮食记录格式
*/
@UseGuards(JwtAuthGuard)
@Post('recognize-food-to-records')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '根据图片URL识别食物并转换为饮食记录格式' })
@ApiBody({ type: FoodRecognitionRequestDto })
@ApiResponse({ status: 200, description: '成功识别食物并转换为饮食记录格式', type: FoodRecognitionToDietRecordsResponseDto })
async recognizeFoodToDietRecords(
@Body() requestDto: FoodRecognitionRequestDto,
@CurrentUser() user: AccessTokenPayload,
): Promise<FoodRecognitionToDietRecordsResponseDto> {
this.logger.log(`识别食物转饮食记录 - 用户ID: ${user.sub}, 图片URL: ${requestDto.imageUrl}`);
return this.dietRecordsService.recognizeFoodToDietRecords(
requestDto.imageUrl,
requestDto.mealType
);
}
/**
* 根据图片URL识别食物原始格式
*/
@UseGuards(JwtAuthGuard)
@Post('recognize-food')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '根据图片URL识别食物' })
@ApiBody({ type: FoodRecognitionRequestDto })
@ApiResponse({ status: 200, description: '成功识别食物', type: FoodRecognitionResponseDto })
async recognizeFood(
@Body() requestDto: FoodRecognitionRequestDto,
@CurrentUser() user: AccessTokenPayload,
): Promise<FoodRecognitionResponseDto> {
this.logger.log(`识别食物 - 用户ID: ${user.sub}, 图片URL: ${requestDto.imageUrl}`);
return this.dietRecordsService.recognizeFood(
requestDto.imageUrl,
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<NutritionAnalysisResponseDto> {
this.logger.log(`分析营养成分表 - 用户ID: ${user.sub}, 图片URL: ${requestDto.imageUrl}`);
if (!requestDto.imageUrl) {
return NutritionAnalysisResponseDto.createError('请提供图片URL');
}
try {
// 传递用户ID以便保存分析记录
const result = await this.nutritionAnalysisService.analyzeNutritionImage(requestDto.imageUrl, user.sub);
this.logger.log(`营养成分表分析完成 - 用户ID: ${user.sub}, 成功: ${result.success}, 营养素数量: ${result.data.length}`);
// 转换旧的响应格式到新的通用格式
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 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 convertedQuery = {
page: query.page ? parseInt(query.page, 10) : undefined,
limit: query.limit ? parseInt(query.limit, 10) : undefined,
startDate: query.startDate,
endDate: query.endDate,
status: query.status,
};
const result = await this.nutritionAnalysisService.getAnalysisRecords(user.sub, convertedQuery);
// 转换为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('获取营养成分分析记录失败,请稍后重试');
}
}
/**
* 删除营养成分分析记录
*/
@UseGuards(JwtAuthGuard)
@Delete('nutrition-analysis-records/:id')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '删除营养成分分析记录' })
@ApiResponse({ status: 200, description: '成功删除营养成分分析记录', type: DeleteNutritionAnalysisRecordResponseDto })
@ApiResponse({ status: 404, description: '营养分析记录不存在' })
async deleteNutritionAnalysisRecord(
@Param('id') recordId: string,
@CurrentUser() user: AccessTokenPayload,
): Promise<DeleteNutritionAnalysisRecordResponseDto> {
this.logger.log(`删除营养成分分析记录 - 用户ID: ${user.sub}, 记录ID: ${recordId}`);
try {
const success = await this.nutritionAnalysisService.deleteAnalysisRecord(user.sub, parseInt(recordId));
if (!success) {
this.logger.warn(`删除营养成分分析记录失败 - 用户ID: ${user.sub}, 记录ID: ${recordId}, 记录不存在或无权限`);
return DeleteNutritionAnalysisRecordResponseDto.createError('营养分析记录不存在或无权限删除');
}
this.logger.log(`营养成分分析记录删除成功 - 用户ID: ${user.sub}, 记录ID: ${recordId}`);
return DeleteNutritionAnalysisRecordResponseDto.createSuccess();
} catch (error) {
this.logger.error(`删除营养成分分析记录失败 - 用户ID: ${user.sub}, 记录ID: ${recordId}, 错误: ${error instanceof Error ? error.message : String(error)}`);
return DeleteNutritionAnalysisRecordResponseDto.createError('删除营养分析记录失败,请稍后重试');
}
}
}