diff --git a/src/medications/models/medication-record.model.ts b/src/medications/models/medication-record.model.ts index 5f4d5fa..94cee59 100644 --- a/src/medications/models/medication-record.model.ts +++ b/src/medications/models/medication-record.model.ts @@ -91,6 +91,14 @@ export class MedicationRecord extends Model { }) declare reminderSent: boolean; + @Column({ + type: DataType.BOOLEAN, + allowNull: false, + defaultValue: false, + comment: '是否已发送超时提醒', + }) + declare overdueReminderSent: boolean; + // 关联关系 @BelongsTo(() => Medication, 'medicationId') declare medication: Medication; diff --git a/src/medications/services/medication-reminder.service.ts b/src/medications/services/medication-reminder.service.ts index 16cebb7..38b4eeb 100644 --- a/src/medications/services/medication-reminder.service.ts +++ b/src/medications/services/medication-reminder.service.ts @@ -11,12 +11,13 @@ import * as dayjs from 'dayjs'; /** * 药物提醒推送服务 - * 在服药时间前15分钟发送推送提醒 + * 在服药时间前15分钟发送推送提醒,并在超过服药时间1小时后发送鼓励提醒 */ @Injectable() export class MedicationReminderService { private readonly logger = new Logger(MedicationReminderService.name); private readonly REMINDER_MINUTES_BEFORE = 15; // 提前15分钟提醒 + private readonly OVERDUE_HOURS_THRESHOLD = 1; // 超过1小时后发送超时提醒 constructor( @InjectModel(Medication) @@ -28,7 +29,7 @@ export class MedicationReminderService { ) {} /** - * 每1分钟检查一次需要发送的提醒 + * 每1分钟检查一次需要发送的提前提醒 * 只有主进程(NODE_APP_INSTANCE=0)执行,避免多进程重复发送 */ @Cron('*/1 * * * *') @@ -117,6 +118,90 @@ export class MedicationReminderService { } } + /** + * 每10分钟检查一次是否有超过服药时间1小时的未服用记录 + * 只有主进程(NODE_APP_INSTANCE=0)执行,避免多进程重复发送 + */ + @Cron('*/10 * * * *') + async checkAndSendOverdueReminders(): 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('主进程检测到,执行超时服药提醒检查...'); + + // 计算时间范围:当前时间减去1小时 + const now = new Date(); + const overdueThreshold = dayjs(now) + .subtract(this.OVERDUE_HOURS_THRESHOLD, 'hour') + .toDate(); + + // 查找超过计划服用时间1小时但状态仍为UPCOMING的记录 + const overdueRecords = await this.recordModel.findAll({ + where: { + status: MedicationStatusEnum.UPCOMING, + deleted: false, + overdueReminderSent: false, // 只查询未发送超时提醒的记录 + scheduledTime: { + [Op.lt]: overdueThreshold, // 计划时间早于1小时前 + }, + }, + include: [ + { + model: Medication, + as: 'medication', + where: { + isActive: true, + deleted: false, + }, + }, + ], + }); + + if (overdueRecords.length === 0) { + this.logger.debug('没有需要发送的超时服药提醒'); + return; + } + + this.logger.log(`找到 ${overdueRecords.length} 条需要发送超时提醒的记录`); + + // 按用户分组发送提醒 + const userRecordsMap = new Map(); + for (const record of overdueRecords) { + const userId = record.userId; + if (!userRecordsMap.has(userId)) { + userRecordsMap.set(userId, []); + } + userRecordsMap.get(userId)!.push(record); + } + + // 为每个用户发送超时提醒 + let successCount = 0; + let failedCount = 0; + + for (const [userId, records] of userRecordsMap.entries()) { + const success = await this.sendOverdueReminderToUser(userId, records); + if (success) { + successCount += records.length; + // 标记这些记录已发送超时提醒 + await this.markRecordsAsOverdueReminded(records.map(r => r.id)); + } else { + failedCount += records.length; + } + } + + this.logger.log(`超时服药提醒发送完成 - 成功: ${successCount}, 失败: ${failedCount}`); + } catch (error) { + this.logger.error('检查超时服药提醒失败', error.stack); + } + } + /** * 为单个用户发送提醒 * @returns 是否发送成功 @@ -161,6 +246,55 @@ export class MedicationReminderService { } } + /** + * 为单个用户发送超时鼓励提醒 + * @returns 是否发送成功 + */ + private async sendOverdueReminderToUser( + userId: string, + records: MedicationRecord[], + ): Promise { + try { + const medicationNames = records + .map((r) => r.medication?.name) + .filter(Boolean) + .join('、'); + + // 计算超时时间 + const overdueHours = Math.max(...records.map(r => + Math.floor(dayjs().diff(dayjs(r.scheduledTime), 'hour')) + )); + + const title = '服药超时提醒'; + const body = + records.length === 1 + ? `您已经错过了 ${medicationNames} 的服用时间超过 ${overdueHours} 小时,请尽快服用!坚持按时服药有助于您的健康恢复。` + : `您已经错过了 ${records.length} 种药物的服用时间,请尽快服用!坚持按时服药有助于您的健康恢复。`; + + await this.pushService.sendNotification({ + userIds: [userId], + title, + body, + payload: { + type: 'medication_overdue_reminder', + recordIds: records.map((r) => r.id), + medicationIds: records.map((r) => r.medicationId), + }, + sound: 'default', + badge: 1, + }); + + this.logger.log(`成功向用户 ${userId} 发送超时服药提醒`); + return true; + } catch (error) { + this.logger.error( + `向用户 ${userId} 发送超时服药提醒失败`, + error.stack, + ); + return false; + } + } + /** * 标记记录为已发送提醒 */ @@ -182,6 +316,27 @@ export class MedicationReminderService { } } + /** + * 标记记录为已发送超时提醒 + */ + private async markRecordsAsOverdueReminded(recordIds: string[]): Promise { + try { + await this.recordModel.update( + { overdueReminderSent: true }, + { + where: { + id: { + [Op.in]: recordIds, + }, + }, + }, + ); + this.logger.debug(`已标记 ${recordIds.length} 条记录为已超时提醒`); + } catch (error) { + this.logger.error('标记记录为已超时提醒失败', error.stack); + } + } + /** * 手动为用户发送即时提醒(用于测试或特殊情况) */