From e25002e01823cbce54662fa1c0bf2509fa6adb1f Mon Sep 17 00:00:00 2001 From: richarjiang Date: Mon, 10 Nov 2025 11:04:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(medications):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E8=8D=AF=E7=89=A9=E6=BF=80=E6=B4=BB=E7=8A=B6=E6=80=81=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=8F=8A=E7=9B=B8=E5=85=B3=E8=AE=B0=E5=BD=95=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/medications/dto/create-medication.dto.ts | 11 ++ src/medications/medications.controller.ts | 11 +- src/medications/medications.service.ts | 165 +++++++++++++++++- .../services/record-generator.service.ts | 74 +++++--- 4 files changed, 235 insertions(+), 26 deletions(-) diff --git a/src/medications/dto/create-medication.dto.ts b/src/medications/dto/create-medication.dto.ts index 798b9b3..f7d870d 100644 --- a/src/medications/dto/create-medication.dto.ts +++ b/src/medications/dto/create-medication.dto.ts @@ -8,6 +8,7 @@ import { IsArray, IsDateString, IsOptional, + IsBoolean, Min, ArrayMinSize, Matches, @@ -101,4 +102,14 @@ export class CreateMedicationDto { @IsString() @IsOptional() note?: string; + + @ApiProperty({ + description: '是否激活', + example: true, + required: false, + default: true + }) + @IsOptional() + @IsBoolean() + isActive?: boolean; } \ No newline at end of file diff --git a/src/medications/medications.controller.ts b/src/medications/medications.controller.ts index 9df9acb..f161ea7 100644 --- a/src/medications/medications.controller.ts +++ b/src/medications/medications.controller.ts @@ -88,9 +88,14 @@ export class MedicationsController { updateDto, ); - // 如果更新了服药时间,重新设置提醒 - if (updateDto.medicationTimes) { - await this.reminderService.setupRemindersForMedication(medication); + // 如果更新了服药时间或isActive字段,重新设置提醒 + if (updateDto.medicationTimes || updateDto.isActive !== undefined) { + if (medication.isActive) { + await this.reminderService.setupRemindersForMedication(medication); + } else { + // 如果停用药物,取消提醒 + await this.reminderService.cancelRemindersForMedication(id); + } } return ApiResponseDto.success(medication, '更新成功'); diff --git a/src/medications/medications.service.ts b/src/medications/medications.service.ts index 5488ceb..44c5ebe 100644 --- a/src/medications/medications.service.ts +++ b/src/medications/medications.service.ts @@ -6,9 +6,13 @@ import { } from '@nestjs/common'; import { InjectModel } 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 } from 'sequelize'; +import * as dayjs from 'dayjs'; /** * 药物管理服务 @@ -20,6 +24,8 @@ export class MedicationsService { constructor( @InjectModel(Medication) private readonly medicationModel: typeof Medication, + @InjectModel(MedicationRecord) + private readonly recordModel: typeof MedicationRecord, ) {} /** @@ -45,7 +51,7 @@ export class MedicationsService { startDate: new Date(createDto.startDate), endDate: createDto.endDate ? new Date(createDto.endDate) : null, note: createDto.note, - isActive: true, + isActive: createDto.isActive !== undefined ? createDto.isActive : true, deleted: false, }); @@ -114,6 +120,9 @@ export class MedicationsService { ): Promise { const medication = await this.findOne(id, userId); + // 保存更新前的状态 + const wasActive = medication.isActive; + // 更新字段 if (updateDto.name !== undefined) { medication.name = updateDto.name; @@ -148,9 +157,22 @@ export class MedicationsService { if (updateDto.note !== undefined) { medication.note = updateDto.note; } + if (updateDto.isActive !== undefined) { + medication.isActive = updateDto.isActive; + } await medication.save(); + // 如果从激活状态变为停用状态,删除当天未服用的记录 + if (updateDto.isActive !== undefined && wasActive && !updateDto.isActive) { + await this.deleteTodayUntakenRecords(medication); + } + + // 如果更新了服药时间,更新当天相关的未服用记录 + if (updateDto.medicationTimes) { + await this.updateTodayUntakenRecords(medication, updateDto.medicationTimes); + } + this.logger.log(`成功更新药物 ${id}`); return medication; } @@ -173,9 +195,21 @@ export class MedicationsService { 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; } @@ -186,10 +220,139 @@ export class MedicationsService { 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 updateTodayUntakenRecords( + medication: Medication, + newMedicationTimes: string[], + ): 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} 的未服用记录`, + ); + + // 查询当天该药物的未服用记录(状态为 UPCOMING) + const recordsToUpdate = await this.recordModel.findAll({ + where: { + medicationId: medication.id, + userId: medication.userId, + status: MedicationStatusEnum.UPCOMING, + scheduledTime: { + [Op.between]: [todayStart, todayEnd], + }, + deleted: false, + }, + }); + + if (recordsToUpdate.length === 0) { + this.logger.log(`没有找到 ${medication.id} 需要更新的记录`); + return; + } + + // 排序记录,按计划时间升序 + recordsToUpdate.sort((a, b) => { + const timeA = dayjs(a.scheduledTime).format('HH:mm'); + const timeB = dayjs(b.scheduledTime).format('HH:mm'); + return timeA.localeCompare(timeB); + }); + + // 更新记录的计划时间 + for (let i = 0; i < recordsToUpdate.length && i < newMedicationTimes.length; i++) { + const record = recordsToUpdate[i]; + const newTime = newMedicationTimes[i]; + + // 解析新的时间字符串(HH:mm) + const [hours, minutes] = newTime.split(':').map(Number); + + // 重新计算计划服药时间(基于今天) + const newScheduledTime = dayjs(today) + .hour(hours) + .minute(minutes) + .second(0) + .millisecond(0) + .toDate(); + + record.scheduledTime = newScheduledTime; + await record.save(); + + this.logger.log( + `更新记录 ${record.id} 的时间从 ${dayjs(record.scheduledTime).format('HH:mm')} 到 ${newTime}`, + ); + } + + this.logger.log( + `成功更新了 ${recordsToUpdate.length} 条 ${medication.id} 的当天记录`, + ); + } + /** + * 删除当天未服用的药物记录 + * 当药物被停用时,删除当天生成的但还未服用的记录 + */ + private async deleteTodayUntakenRecords(medication: Medication): Promise { + // 使用当前时间作为基准,查询当前时间及未来的未服用记录 + const now = new Date(); + + this.logger.log( + `开始删除药物 ${medication.id} 从现在起(${dayjs(now).format('YYYY-MM-DD HH:mm')})的未服用记录`, + ); + + // 查询从现在开始的所有未服用记录(状态为 UPCOMING) + const recordsToDelete = await this.recordModel.findAll({ + where: { + medicationId: medication.id, + userId: medication.userId, + status: MedicationStatusEnum.UPCOMING, + scheduledTime: { + [Op.gte]: now, // 大于等于当前时间 + }, + deleted: false, + }, + }); + + this.logger.log( + `找到 ${recordsToDelete.length} 条需要删除的 ${medication.id} 记录`, + ); + + if (recordsToDelete.length === 0) { + this.logger.log(`没有找到 ${medication.id} 需要删除的记录`); + return; + } + + // 软删除记录 + for (const record of recordsToDelete) { + const scheduledTime = dayjs(record.scheduledTime).format('YYYY-MM-DD HH:mm'); + record.deleted = true; + await record.save(); + + this.logger.log( + `软删除记录 ${record.id},计划时间:${scheduledTime}`, + ); + } + + this.logger.log( + `成功删除了 ${recordsToDelete.length} 条 ${medication.id} 的未服用记录`, + ); + } } \ No newline at end of file diff --git a/src/medications/services/record-generator.service.ts b/src/medications/services/record-generator.service.ts index 1a53b2a..a3ac06f 100644 --- a/src/medications/services/record-generator.service.ts +++ b/src/medications/services/record-generator.service.ts @@ -128,8 +128,12 @@ export class RecordGeneratorService { // 判断初始状态 const now = new Date(); - const status = - scheduledTime <= now + const medicationStartDate = dayjs(medication.startDate).startOf('day'); + + // 如果药物开始日期是今天,无论当前时间如何,都设置为 UPCOMING + const status = targetDate.isSame(medicationStartDate, 'day') + ? MedicationStatusEnum.UPCOMING + : scheduledTime <= now ? MedicationStatusEnum.MISSED : MedicationStatusEnum.UPCOMING; @@ -187,29 +191,28 @@ export class RecordGeneratorService { */ async ensureRecordsExist(userId: string, date: string): Promise { const targetDate = dayjs(date).format('YYYY-MM-DD'); + const targetDateDayjs = dayjs(date).startOf('day'); - // 检查该日期是否已有记录 + // 1. 查询用户所有激活的药物 + const activeMedications = await this.medicationModel.findAll({ + where: { + userId, + isActive: true, + deleted: false, + }, + }); + + if (activeMedications.length === 0) { + this.logger.debug(`用户 ${userId} 没有激活的药物`); + return false; + } + + // 2. 检查该日期是否已有记录 const startOfDay = dayjs(date).startOf('day').toDate(); const endOfDay = dayjs(date).endOf('day').toDate(); - const existingRecords = await this.recordModel.count({ - where: { - userId, - deleted: false, - }, - }); - - // 简单判断:如果没有任何记录,则生成 - const recordsCount = await this.recordModel.count({ - where: { - userId, - deleted: false, - }, - }); - - // 这里使用更精确的查询 const Op = require('sequelize').Op; - const recordsOnDate = await this.recordModel.count({ + const existingRecords = await this.recordModel.findAll({ where: { userId, deleted: false, @@ -217,10 +220,37 @@ export class RecordGeneratorService { [Op.between]: [startOfDay, endOfDay], }, }, + attributes: ['medicationId'], }); - if (recordsOnDate === 0) { - await this.generateRecordsForDate(userId, targetDate); + // 3. 获取已有记录的药物ID集合 + const existingMedicationIds = new Set( + existingRecords.map((record) => record.medicationId), + ); + + // 4. 找出需要生成记录的药物(在有效期内且未生成记录的) + const medicationsNeedRecords = activeMedications.filter((medication) => { + // 检查是否在有效期内 + if (!this.isDateInMedicationRange(medication, targetDateDayjs)) { + return false; + } + // 检查是否已生成记录 + return !existingMedicationIds.has(medication.id); + }); + + // 5. 为需要的药物生成记录 + if (medicationsNeedRecords.length > 0) { + this.logger.log( + `为用户 ${userId} 在 ${targetDate} 生成 ${medicationsNeedRecords.length} 个药物的记录`, + ); + + for (const medication of medicationsNeedRecords) { + await this.generateRecordsForMedicationOnDate( + medication, + targetDateDayjs, + ); + } + return true; }