feat(medications): 添加药品过期预警功能,支持多时间段提醒和标记重置
This commit is contained in:
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"kiroAgent.configureMCP": "Enabled"
|
"kiroAgent.configureMCP": "Enabled",
|
||||||
|
"codingcopilot.enableCompletionLanguage": {}
|
||||||
}
|
}
|
||||||
@@ -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[];
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user