feat(medications): 添加药品过期预警功能,支持多时间段提醒和标记重置

This commit is contained in:
richarjiang
2025-12-03 19:22:46 +08:00
parent b12956d80a
commit 03bd0b041e
3 changed files with 301 additions and 2 deletions

View File

@@ -1,3 +1,4 @@
{ {
"kiroAgent.configureMCP": "Enabled" "kiroAgent.configureMCP": "Enabled",
"codingcopilot.enableCompletionLanguage": {}
} }

View File

@@ -162,6 +162,30 @@ export class Medication extends Model {
}) })
declare deleted: boolean; declare deleted: boolean;
@Column({
type: DataType.BOOLEAN,
allowNull: false,
defaultValue: false,
comment: '是否已发送一个月过期预警',
})
declare expiryOneMonthWarned: boolean;
@Column({
type: DataType.BOOLEAN,
allowNull: false,
defaultValue: false,
comment: '是否已发送一周过期预警',
})
declare expiryOneWeekWarned: boolean;
@Column({
type: DataType.BOOLEAN,
allowNull: false,
defaultValue: false,
comment: '是否已发送一天过期预警',
})
declare expiryOneDayWarned: boolean;
// 关联关系 // 关联关系
@HasMany(() => MedicationRecord, 'medicationId') @HasMany(() => MedicationRecord, 'medicationId')
declare records: MedicationRecord[]; declare records: MedicationRecord[];

View File

@@ -12,12 +12,16 @@ import * as dayjs from 'dayjs';
/** /**
* 药物提醒推送服务 * 药物提醒推送服务
* 在服药时间前15分钟发送推送提醒并在超过服药时间1小时后发送鼓励提醒 * 在服药时间前15分钟发送推送提醒并在超过服药时间1小时后发送鼓励提醒
* 同时检查药品过期时间,分别在提前一个月、一周、一天进行预警
*/ */
@Injectable() @Injectable()
export class MedicationReminderService { export class MedicationReminderService {
private readonly logger = new Logger(MedicationReminderService.name); private readonly logger = new Logger(MedicationReminderService.name);
private readonly REMINDER_MINUTES_BEFORE = 5; // 提前5分钟提醒 private readonly REMINDER_MINUTES_BEFORE = 2; // 提前5分钟提醒
private readonly OVERDUE_HOURS_THRESHOLD = 1; // 超过1小时后发送超时提醒 private readonly OVERDUE_HOURS_THRESHOLD = 1; // 超过1小时后发送超时提醒
private readonly EXPIRY_ONE_MONTH_DAYS = 30; // 提前一个月预警
private readonly EXPIRY_ONE_WEEK_DAYS = 7; // 提前一周预警
private readonly EXPIRY_ONE_DAY_DAYS = 1; // 提前一天预警
constructor( constructor(
@InjectModel(Medication) @InjectModel(Medication)
@@ -408,4 +412,274 @@ export class MedicationReminderService {
return count; return count;
} }
/**
* 每天早上9点检查药品过期预警
* 分别在提前一个月、一周、一天进行预警
* 只有主进程NODE_APP_INSTANCE=0执行避免多进程重复发送
*/
@Cron('0 9 * * *')
async checkAndSendExpiryWarnings(): Promise<void> {
this.logger.log('开始检查药品过期预警');
try {
// 检查是否为主进程NODE_APP_INSTANCE 为 0
const nodeAppInstance = this.configService.get<number>('NODE_APP_INSTANCE', 0);
if (Number(nodeAppInstance) !== 0) {
this.logger.debug(`不是主进程 (instance: ${nodeAppInstance}),跳过药品过期预警检查`);
return;
}
this.logger.log('主进程检测到,执行药品过期预警检查...');
const now = dayjs();
const today = now.startOf('day');
// 计算各个预警时间点
const oneMonthLater = today.add(this.EXPIRY_ONE_MONTH_DAYS, 'day').toDate();
const oneWeekLater = today.add(this.EXPIRY_ONE_WEEK_DAYS, 'day').toDate();
const oneDayLater = today.add(this.EXPIRY_ONE_DAY_DAYS, 'day').toDate();
// 查找需要发送一个月预警的药品过期时间在30天内且未发送过一个月预警
const oneMonthWarningMeds = await this.medicationModel.findAll({
where: {
isActive: true,
deleted: false,
expiryDate: {
[Op.not]: null,
[Op.lte]: oneMonthLater,
[Op.gt]: oneWeekLater, // 排除已经进入一周预警范围的
},
expiryOneMonthWarned: false,
},
});
// 查找需要发送一周预警的药品过期时间在7天内且未发送过一周预警
const oneWeekWarningMeds = await this.medicationModel.findAll({
where: {
isActive: true,
deleted: false,
expiryDate: {
[Op.not]: null,
[Op.lte]: oneWeekLater,
[Op.gt]: oneDayLater, // 排除已经进入一天预警范围的
},
expiryOneWeekWarned: false,
},
});
// 查找需要发送一天预警的药品过期时间在1天内且未发送过一天预警
const oneDayWarningMeds = await this.medicationModel.findAll({
where: {
isActive: true,
deleted: false,
expiryDate: {
[Op.not]: null,
[Op.lte]: oneDayLater,
[Op.gte]: today.toDate(), // 还未过期
},
expiryOneDayWarned: false,
},
});
// 查找已过期的药品(用于发送已过期提醒)
const expiredMeds = await this.medicationModel.findAll({
where: {
isActive: true,
deleted: false,
expiryDate: {
[Op.not]: null,
[Op.lt]: today.toDate(),
},
expiryOneDayWarned: false, // 复用一天预警标记
},
});
this.logger.log(
`找到需要预警的药品 - 一个月: ${oneMonthWarningMeds.length}, 一周: ${oneWeekWarningMeds.length}, 一天: ${oneDayWarningMeds.length}, 已过期: ${expiredMeds.length}`,
);
// 发送一个月预警
await this.sendExpiryWarnings(oneMonthWarningMeds, 'one_month');
// 发送一周预警
await this.sendExpiryWarnings(oneWeekWarningMeds, 'one_week');
// 发送一天预警
await this.sendExpiryWarnings(oneDayWarningMeds, 'one_day');
// 发送已过期提醒
await this.sendExpiryWarnings(expiredMeds, 'expired');
this.logger.log('药品过期预警检查完成');
} catch (error) {
this.logger.error('检查药品过期预警失败', error.stack);
}
}
/**
* 发送药品过期预警
*/
private async sendExpiryWarnings(
medications: Medication[],
warningType: 'one_month' | 'one_week' | 'one_day' | 'expired',
): Promise<void> {
if (medications.length === 0) {
return;
}
// 按用户分组
const userMedsMap = new Map<string, Medication[]>();
for (const med of medications) {
const userId = med.userId;
if (!userMedsMap.has(userId)) {
userMedsMap.set(userId, []);
}
userMedsMap.get(userId)!.push(med);
}
// 为每个用户发送预警
for (const [userId, meds] of userMedsMap.entries()) {
const success = await this.sendExpiryWarningToUser(userId, meds, warningType);
if (success) {
// 标记已发送预警
await this.markMedicationsAsExpiryWarned(
meds.map((m) => m.id),
warningType,
);
}
}
}
/**
* 为单个用户发送药品过期预警
*/
private async sendExpiryWarningToUser(
userId: string,
medications: Medication[],
warningType: 'one_month' | 'one_week' | 'one_day' | 'expired',
): Promise<boolean> {
try {
const medicationNames = medications.map((m) => m.name).join('、');
let title: string;
let body: string;
switch (warningType) {
case 'one_month':
title = '药品即将过期提醒';
body =
medications.length === 1
? `您的药品「${medicationNames}」将在一个月内过期,请注意及时更换。`
: `您有 ${medications.length} 种药品将在一个月内过期:${medicationNames},请注意及时更换。`;
break;
case 'one_week':
title = '药品过期预警';
body =
medications.length === 1
? `您的药品「${medicationNames}」将在一周内过期,请尽快更换!`
: `您有 ${medications.length} 种药品将在一周内过期:${medicationNames},请尽快更换!`;
break;
case 'one_day':
title = '药品过期紧急提醒';
body =
medications.length === 1
? `您的药品「${medicationNames}」明天即将过期,请立即检查并更换!`
: `您有 ${medications.length} 种药品明天即将过期:${medicationNames},请立即检查并更换!`;
break;
case 'expired':
title = '药品已过期警告';
body =
medications.length === 1
? `您的药品「${medicationNames}」已过期,请勿继续服用,并及时更换新药!`
: `您有 ${medications.length} 种药品已过期:${medicationNames},请勿继续服用,并及时更换新药!`;
break;
}
await this.pushService.sendNotification({
userIds: [userId],
title,
body,
payload: {
type: 'medication_expiry_warning',
warningType,
medicationIds: medications.map((m) => m.id),
},
sound: 'default',
badge: 1,
});
this.logger.log(`成功向用户 ${userId} 发送药品过期预警 (${warningType})`);
return true;
} catch (error) {
this.logger.error(
`向用户 ${userId} 发送药品过期预警失败`,
error.stack,
);
return false;
}
}
/**
* 标记药品已发送过期预警
*/
private async markMedicationsAsExpiryWarned(
medicationIds: string[],
warningType: 'one_month' | 'one_week' | 'one_day' | 'expired',
): Promise<void> {
try {
let updateField: string;
switch (warningType) {
case 'one_month':
updateField = 'expiryOneMonthWarned';
break;
case 'one_week':
updateField = 'expiryOneWeekWarned';
break;
case 'one_day':
case 'expired':
updateField = 'expiryOneDayWarned';
break;
}
await this.medicationModel.update(
{ [updateField]: true },
{
where: {
id: {
[Op.in]: medicationIds,
},
},
},
);
this.logger.debug(
`已标记 ${medicationIds.length} 种药品为已发送 ${warningType} 过期预警`,
);
} catch (error) {
this.logger.error('标记药品过期预警失败', error.stack);
}
}
/**
* 重置药品的过期预警标记(当用户更新药品有效期时调用)
*/
async resetExpiryWarnings(medicationId: string): Promise<void> {
try {
await this.medicationModel.update(
{
expiryOneMonthWarned: false,
expiryOneWeekWarned: false,
expiryOneDayWarned: false,
},
{
where: {
id: medicationId,
},
},
);
this.logger.log(`已重置药品 ${medicationId} 的过期预警标记`);
} catch (error) {
this.logger.error('重置药品过期预警标记失败', error.stack);
}
}
} }