diff --git a/.vscode/settings.json b/.vscode/settings.json index 3f836a2..48c4e33 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "kiroAgent.configureMCP": "Enabled" + "kiroAgent.configureMCP": "Enabled", + "codingcopilot.enableCompletionLanguage": {} } \ No newline at end of file diff --git a/src/medications/models/medication.model.ts b/src/medications/models/medication.model.ts index b9223e5..a7e2ad7 100644 --- a/src/medications/models/medication.model.ts +++ b/src/medications/models/medication.model.ts @@ -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[]; diff --git a/src/medications/services/medication-reminder.service.ts b/src/medications/services/medication-reminder.service.ts index aaca3a9..c729277 100644 --- a/src/medications/services/medication-reminder.service.ts +++ b/src/medications/services/medication-reminder.service.ts @@ -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 { + this.logger.log('开始检查药品过期预警'); + + try { + // 检查是否为主进程(NODE_APP_INSTANCE 为 0) + const nodeAppInstance = this.configService.get('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 { + if (medications.length === 0) { + return; + } + + // 按用户分组 + const userMedsMap = new Map(); + 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 { + 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 { + 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 { + 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); + } + } } \ No newline at end of file