Files
plates-server/src/medications/medications.service.ts

348 lines
10 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 };
}
/**
* 获取用户所有正在进行的用药计划
*/
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} 的记录`,
);
}
}