feat(medications): 添加药品过期预警功能,支持多时间段提醒和标记重置
This commit is contained in:
@@ -162,6 +162,30 @@ export class Medication extends Model {
|
||||
})
|
||||
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')
|
||||
declare records: MedicationRecord[];
|
||||
|
||||
@@ -12,12 +12,16 @@ import * as dayjs from 'dayjs';
|
||||
/**
|
||||
* 药物提醒推送服务
|
||||
* 在服药时间前15分钟发送推送提醒,并在超过服药时间1小时后发送鼓励提醒
|
||||
* 同时检查药品过期时间,分别在提前一个月、一周、一天进行预警
|
||||
*/
|
||||
@Injectable()
|
||||
export class MedicationReminderService {
|
||||
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 EXPIRY_ONE_MONTH_DAYS = 30; // 提前一个月预警
|
||||
private readonly EXPIRY_ONE_WEEK_DAYS = 7; // 提前一周预警
|
||||
private readonly EXPIRY_ONE_DAY_DAYS = 1; // 提前一天预警
|
||||
|
||||
constructor(
|
||||
@InjectModel(Medication)
|
||||
@@ -408,4 +412,274 @@ export class MedicationReminderService {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user