348 lines
10 KiB
TypeScript
348 lines
10 KiB
TypeScript
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 };
|
||
}
|
||
|
||
/**
|
||
* 获取用户所有正在进行的用药计划
|
||
*/
|
||
async findActiveMedications(userId: string): Promise<Medication[]> {
|
||
return this.medicationModel.findAll({
|
||
where: {
|
||
userId,
|
||
isActive: true,
|
||
deleted: false,
|
||
},
|
||
order: [['startDate', 'ASC']],
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 根据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} 的记录`,
|
||
);
|
||
}
|
||
}
|