feat(medications): 新增药物激活状态管理及相关记录更新功能

This commit is contained in:
richarjiang
2025-11-10 11:04:36 +08:00
parent 188b4addca
commit e25002e018
4 changed files with 235 additions and 26 deletions

View File

@@ -8,6 +8,7 @@ import {
IsArray, IsArray,
IsDateString, IsDateString,
IsOptional, IsOptional,
IsBoolean,
Min, Min,
ArrayMinSize, ArrayMinSize,
Matches, Matches,
@@ -101,4 +102,14 @@ export class CreateMedicationDto {
@IsString() @IsString()
@IsOptional() @IsOptional()
note?: string; note?: string;
@ApiProperty({
description: '是否激活',
example: true,
required: false,
default: true
})
@IsOptional()
@IsBoolean()
isActive?: boolean;
} }

View File

@@ -88,9 +88,14 @@ export class MedicationsController {
updateDto, updateDto,
); );
// 如果更新了服药时间,重新设置提醒 // 如果更新了服药时间或isActive字段,重新设置提醒
if (updateDto.medicationTimes) { if (updateDto.medicationTimes || updateDto.isActive !== undefined) {
await this.reminderService.setupRemindersForMedication(medication); if (medication.isActive) {
await this.reminderService.setupRemindersForMedication(medication);
} else {
// 如果停用药物,取消提醒
await this.reminderService.cancelRemindersForMedication(id);
}
} }
return ApiResponseDto.success(medication, '更新成功'); return ApiResponseDto.success(medication, '更新成功');

View File

@@ -6,9 +6,13 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize'; import { InjectModel } from '@nestjs/sequelize';
import { Medication } from './models/medication.model'; import { Medication } from './models/medication.model';
import { MedicationRecord } from './models/medication-record.model';
import { CreateMedicationDto } from './dto/create-medication.dto'; import { CreateMedicationDto } from './dto/create-medication.dto';
import { UpdateMedicationDto } from './dto/update-medication.dto'; import { UpdateMedicationDto } from './dto/update-medication.dto';
import { MedicationStatusEnum } from './enums/medication-status.enum';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { Op } from 'sequelize';
import * as dayjs from 'dayjs';
/** /**
* 药物管理服务 * 药物管理服务
@@ -20,6 +24,8 @@ export class MedicationsService {
constructor( constructor(
@InjectModel(Medication) @InjectModel(Medication)
private readonly medicationModel: typeof 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), startDate: new Date(createDto.startDate),
endDate: createDto.endDate ? new Date(createDto.endDate) : null, endDate: createDto.endDate ? new Date(createDto.endDate) : null,
note: createDto.note, note: createDto.note,
isActive: true, isActive: createDto.isActive !== undefined ? createDto.isActive : true,
deleted: false, deleted: false,
}); });
@@ -114,6 +120,9 @@ export class MedicationsService {
): Promise<Medication> { ): Promise<Medication> {
const medication = await this.findOne(id, userId); const medication = await this.findOne(id, userId);
// 保存更新前的状态
const wasActive = medication.isActive;
// 更新字段 // 更新字段
if (updateDto.name !== undefined) { if (updateDto.name !== undefined) {
medication.name = updateDto.name; medication.name = updateDto.name;
@@ -148,9 +157,22 @@ export class MedicationsService {
if (updateDto.note !== undefined) { if (updateDto.note !== undefined) {
medication.note = updateDto.note; medication.note = updateDto.note;
} }
if (updateDto.isActive !== undefined) {
medication.isActive = updateDto.isActive;
}
await medication.save(); 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}`); this.logger.log(`成功更新药物 ${id}`);
return medication; return medication;
} }
@@ -173,9 +195,21 @@ export class MedicationsService {
async deactivate(id: string, userId: string): Promise<Medication> { async deactivate(id: string, userId: string): Promise<Medication> {
const medication = await this.findOne(id, userId); const medication = await this.findOne(id, userId);
// 如果已经是停用状态,不需要处理
if (!medication.isActive) {
this.logger.log(`药物 ${id} 已经是停用状态`);
return medication;
}
const wasActive = medication.isActive;
medication.isActive = false; medication.isActive = false;
await medication.save(); await medication.save();
// 删除当天未服用的记录
if (wasActive && !medication.isActive) {
await this.deleteTodayUntakenRecords(medication);
}
this.logger.log(`成功停用药物 ${id}`); this.logger.log(`成功停用药物 ${id}`);
return medication; return medication;
} }
@@ -186,10 +220,139 @@ export class MedicationsService {
async activate(id: string, userId: string): Promise<Medication> { async activate(id: string, userId: string): Promise<Medication> {
const medication = await this.findOne(id, userId); const medication = await this.findOne(id, userId);
// 如果已经是激活状态,不需要处理
if (medication.isActive) {
this.logger.log(`药物 ${id} 已经是激活状态`);
return medication;
}
const wasActive = medication.isActive;
medication.isActive = true; medication.isActive = true;
await medication.save(); await medication.save();
// 当药物从停用变为激活时RecordGeneratorService 会负责生成新记录
// 但这里不需要手动处理,因为采用的是惰性生成策略
this.logger.log(`成功激活药物 ${id}`); this.logger.log(`成功激活药物 ${id}`);
return medication; return medication;
} }
/**
* 更新当天相关的未服用记录
* 当药物的服药时间被更新时,将当天未服用的记录更新为新的服药时间
*/
private async updateTodayUntakenRecords(
medication: Medication,
newMedicationTimes: string[],
): 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} 的未服用记录`,
);
// 查询当天该药物的未服用记录(状态为 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<void> {
// 使用当前时间作为基准,查询当前时间及未来的未服用记录
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} 的未服用记录`,
);
}
} }

View File

@@ -128,8 +128,12 @@ export class RecordGeneratorService {
// 判断初始状态 // 判断初始状态
const now = new Date(); const now = new Date();
const status = const medicationStartDate = dayjs(medication.startDate).startOf('day');
scheduledTime <= now
// 如果药物开始日期是今天,无论当前时间如何,都设置为 UPCOMING
const status = targetDate.isSame(medicationStartDate, 'day')
? MedicationStatusEnum.UPCOMING
: scheduledTime <= now
? MedicationStatusEnum.MISSED ? MedicationStatusEnum.MISSED
: MedicationStatusEnum.UPCOMING; : MedicationStatusEnum.UPCOMING;
@@ -187,29 +191,28 @@ export class RecordGeneratorService {
*/ */
async ensureRecordsExist(userId: string, date: string): Promise<boolean> { async ensureRecordsExist(userId: string, date: string): Promise<boolean> {
const targetDate = dayjs(date).format('YYYY-MM-DD'); 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 startOfDay = dayjs(date).startOf('day').toDate();
const endOfDay = dayjs(date).endOf('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 Op = require('sequelize').Op;
const recordsOnDate = await this.recordModel.count({ const existingRecords = await this.recordModel.findAll({
where: { where: {
userId, userId,
deleted: false, deleted: false,
@@ -217,10 +220,37 @@ export class RecordGeneratorService {
[Op.between]: [startOfDay, endOfDay], [Op.between]: [startOfDay, endOfDay],
}, },
}, },
attributes: ['medicationId'],
}); });
if (recordsOnDate === 0) { // 3. 获取已有记录的药物ID集合
await this.generateRecordsForDate(userId, targetDate); 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; return true;
} }