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 { 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 { return this.medicationModel.findAll({ where: { userId, isActive: true, deleted: false, }, order: [['startDate', 'ASC']], }); } /** * 根据ID获取药物详情 */ async findOne(id: string, userId: string): Promise { 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 { 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 { 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 { 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 { 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 { // 获取当天的开始和结束时间 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 { 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} 的记录`, ); } }