添加删除营养成分分析记录的API端点,支持软删除机制 - 新增DELETE /nutrition-analysis-records/:id接口 - 添加DeleteNutritionAnalysisRecordResponseDto响应DTO - 在NutritionAnalysisService中实现deleteAnalysisRecord方法 - 包含完整的权限验证和错误处理逻辑
298 lines
12 KiB
TypeScript
298 lines
12 KiB
TypeScript
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('删除营养分析记录失败,请稍后重试');
|
||
}
|
||
}
|
||
} |