Files
plates-server/src/medications/medications.service.ts
richarjiang f8fcc81438 feat(medications): 增强AI药品识别质量控制和多图片支持
- 新增图片可读性预检查机制,识别前先判断图片质量
- 设置置信度阈值为60%,低于阈值自动识别失败
- 支持多图片上传(正面、侧面、辅助图片)提高识别准确度
- 完善识别失败场景的错误分类和用户指导提示
- 新增药品有效期字段支持
- 优化AI提示词,强调安全优先原则
- 更新模型版本为 glm-4.5v 和 glm-4.5-air

数据库变更:
- Medication表新增 sideImageUrl, auxiliaryImageUrl, expiryDate 字段
- DTO层同步支持新增字段的传递和更新

质量控制策略:
- 图片模糊或不可读时直接返回失败
- 无法识别药品名称时主动失败
- 置信度<60%时拒绝识别,建议重新拍摄
- 宁可识别失败也不提供不准确的药品信息
2025-11-21 16:59:36 +08:00

333 lines
9.8 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 {
Injectable,
NotFoundException,
ForbiddenException,
Logger,
} from '@nestjs/common';
import { InjectModel, InjectConnection } from '@nestjs/sequelize';
import { Medication } from './models/medication.model';
import { MedicationRecord } from './models/medication-record.model';
import { CreateMedicationDto } from './dto/create-medication.dto';
import { UpdateMedicationDto } from './dto/update-medication.dto';
import { MedicationStatusEnum } from './enums/medication-status.enum';
import { v4 as uuidv4 } from 'uuid';
import { Op, Transaction } from 'sequelize';
import { Sequelize } from 'sequelize-typescript';
import * as dayjs from 'dayjs';
/**
* 药物管理服务
*/
@Injectable()
export class MedicationsService {
private readonly logger = new Logger(MedicationsService.name);
constructor(
@InjectModel(Medication)
private readonly medicationModel: typeof Medication,
@InjectModel(MedicationRecord)
private readonly recordModel: typeof MedicationRecord,
@InjectConnection()
private readonly sequelize: Sequelize,
) {}
/**
* 创建药物
*/
async create(
userId: string,
createDto: CreateMedicationDto,
): Promise<Medication> {
this.logger.log(`用户 ${userId} 创建药物:${createDto.name}`);
const medication = await this.medicationModel.create({
id: uuidv4(),
userId,
name: createDto.name,
photoUrl: createDto.photoUrl,
sideImageUrl: createDto.sideImageUrl,
auxiliaryImageUrl: createDto.auxiliaryImageUrl,
form: createDto.form,
dosageValue: createDto.dosageValue,
dosageUnit: createDto.dosageUnit,
timesPerDay: createDto.timesPerDay,
medicationTimes: createDto.medicationTimes,
repeatPattern: createDto.repeatPattern,
startDate: new Date(createDto.startDate),
endDate: createDto.endDate ? new Date(createDto.endDate) : null,
expiryDate: createDto.expiryDate ? new Date(createDto.expiryDate) : null,
note: createDto.note,
isActive: createDto.isActive !== undefined ? createDto.isActive : true,
deleted: false,
});
this.logger.log(`成功创建药物 ${medication.id}`);
return medication;
}
/**
* 获取用户的药物列表
*/
async findAll(
userId: string,
isActive?: boolean,
page: number = 1,
pageSize: number = 20,
): Promise<{ rows: Medication[]; total: number }> {
const where: any = {
userId,
deleted: false,
};
if (isActive !== undefined) {
where.isActive = isActive;
}
const { rows, count } = await this.medicationModel.findAndCountAll({
where,
limit: pageSize,
offset: (page - 1) * pageSize,
order: [['createdAt', 'DESC']],
});
return { rows, total: count };
}
/**
* 根据ID获取药物详情
*/
async findOne(id: string, userId: string): Promise<Medication> {
const medication = await this.medicationModel.findOne({
where: {
id,
deleted: false,
},
});
if (!medication) {
throw new NotFoundException('药物不存在');
}
// 验证所有权
if (medication.userId !== userId) {
throw new ForbiddenException('无权访问此药物');
}
return medication;
}
/**
* 更新药物信息
*/
async update(
id: string,
userId: string,
updateDto: UpdateMedicationDto,
): Promise<Medication> {
const medication = await this.findOne(id, userId);
// 保存更新前的状态
const wasActive = medication.isActive;
// 更新字段
if (updateDto.name !== undefined) {
medication.name = updateDto.name;
}
if (updateDto.photoUrl !== undefined) {
medication.photoUrl = updateDto.photoUrl;
}
if (updateDto.sideImageUrl !== undefined) {
medication.sideImageUrl = updateDto.sideImageUrl;
}
if (updateDto.auxiliaryImageUrl !== undefined) {
medication.auxiliaryImageUrl = updateDto.auxiliaryImageUrl;
}
if (updateDto.form !== undefined) {
medication.form = updateDto.form;
}
if (updateDto.dosageValue !== undefined) {
medication.dosageValue = updateDto.dosageValue;
}
if (updateDto.dosageUnit !== undefined) {
medication.dosageUnit = updateDto.dosageUnit;
}
if (updateDto.timesPerDay !== undefined) {
medication.timesPerDay = updateDto.timesPerDay;
}
if (updateDto.medicationTimes !== undefined) {
medication.medicationTimes = updateDto.medicationTimes;
}
if (updateDto.repeatPattern !== undefined) {
medication.repeatPattern = updateDto.repeatPattern;
}
if (updateDto.startDate !== undefined) {
medication.startDate = new Date(updateDto.startDate);
}
if (updateDto.endDate !== undefined) {
medication.endDate = updateDto.endDate ? new Date(updateDto.endDate) : null;
}
if (updateDto.expiryDate !== undefined) {
medication.expiryDate = updateDto.expiryDate ? new Date(updateDto.expiryDate) : null;
}
if (updateDto.note !== undefined) {
medication.note = updateDto.note;
}
if (updateDto.isActive !== undefined) {
medication.isActive = updateDto.isActive;
}
// 支持更新 AI 分析结果
if ((updateDto as any).aiAnalysis !== undefined) {
medication.aiAnalysis = (updateDto as any).aiAnalysis;
}
await medication.save();
// 如果从激活状态变为停用状态,删除当天未服用的记录
if (updateDto.isActive !== undefined && wasActive && !updateDto.isActive) {
await this.deleteTodayUntakenRecords(medication);
}
// 如果更新了服药时间,删除当天的记录,让系统重新生成新的记录
// 这样更简单可靠,与激活状态更新的处理逻辑保持一致
if (updateDto.medicationTimes) {
await this.deleteTodayUntakenRecords(medication);
this.logger.log(`已删除药物 ${id} 当天的记录,系统会根据新的服药时间重新生成`);
}
this.logger.log(`成功更新药物 ${id}`);
return medication;
}
/**
* 删除药物(软删除)
*/
async remove(id: string, userId: string): Promise<void> {
const transaction = await this.sequelize.transaction();
try {
const medication = await this.findOne(id, userId);
// 软删除药物
medication.deleted = true;
await medication.save({ transaction });
// 软删除所有相关的用药记录
await this.deleteAllMedicationRecords(medication, transaction);
await transaction.commit();
this.logger.log(`成功删除药物 ${id} 及其所有记录`);
} catch (error) {
await transaction.rollback();
this.logger.error(`删除药物失败: ${error instanceof Error ? error.message : '未知错误'}`);
throw error;
}
}
/**
* 停用药物
*/
async deactivate(id: string, userId: string): Promise<Medication> {
const medication = await this.findOne(id, userId);
// 如果已经是停用状态,不需要处理
if (!medication.isActive) {
this.logger.log(`药物 ${id} 已经是停用状态`);
return medication;
}
const wasActive = medication.isActive;
medication.isActive = false;
await medication.save();
// 删除当天未服用的记录
if (wasActive && !medication.isActive) {
await this.deleteTodayUntakenRecords(medication);
}
this.logger.log(`成功停用药物 ${id}`);
return medication;
}
/**
* 激活药物
*/
async activate(id: string, userId: string): Promise<Medication> {
const medication = await this.findOne(id, userId);
// 如果已经是激活状态,不需要处理
if (medication.isActive) {
this.logger.log(`药物 ${id} 已经是激活状态`);
return medication;
}
const wasActive = medication.isActive;
medication.isActive = true;
await medication.save();
// 当药物从停用变为激活时RecordGeneratorService 会负责生成新记录
// 但这里不需要手动处理,因为采用的是惰性生成策略
this.logger.log(`成功激活药物 ${id}`);
return medication;
}
/**
* 删除当天的药物记录
* 当药物被停用时,删除当天生成的所有记录
*/
private async deleteTodayUntakenRecords(medication: Medication, transaction?: Transaction): Promise<void> {
// 获取当天的开始和结束时间
const today = dayjs().format('YYYY-MM-DD');
const todayStart = dayjs(today).startOf('day').toDate();
const todayEnd = dayjs(today).endOf('day').toDate();
this.logger.log(
`开始删除药物 ${medication.id}${today} 的所有记录`,
);
// 使用批量更新而不是循环删除,提高性能
const [affectedCount] = await this.recordModel.update(
{ deleted: true },
{
where: {
medicationId: medication.id,
userId: medication.userId,
scheduledTime: {
[Op.between]: [todayStart, todayEnd], // 当天的所有记录
},
deleted: false,
},
transaction,
}
);
this.logger.log(
`成功批量软删除了 ${affectedCount}${medication.id} 的当天记录`,
);
}
/**
* 删除药物的所有相关记录
* 当药物被删除时,软删除所有相关的服药记录
*/
private async deleteAllMedicationRecords(medication: Medication, transaction?: Transaction): Promise<void> {
this.logger.log(`开始删除药物 ${medication.id} 的所有相关记录`);
// 使用批量更新而不是循环删除,提高性能
const [affectedCount] = await this.recordModel.update(
{ deleted: true },
{
where: {
medicationId: medication.id,
userId: medication.userId,
deleted: false,
},
transaction,
}
);
this.logger.log(
`成功批量软删除了 ${affectedCount}${medication.id} 的记录`,
);
}
}