diff --git a/sql-scripts/2025-01-11-add-reminder-sent-to-medication-records.sql b/sql-scripts/2025-01-11-add-reminder-sent-to-medication-records.sql new file mode 100644 index 0000000..f16e249 --- /dev/null +++ b/sql-scripts/2025-01-11-add-reminder-sent-to-medication-records.sql @@ -0,0 +1,11 @@ +-- 为 t_medication_records 表添加 reminder_sent 字段 +-- 用于标记该条服药记录是否已经发送了提醒通知 + +ALTER TABLE `t_medication_records` +ADD COLUMN `reminder_sent` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否已发送提醒' AFTER `deleted`; + +-- 为已存在的记录设置默认值 +UPDATE `t_medication_records` SET `reminder_sent` = 0 WHERE `reminder_sent` IS NULL; + +-- 添加索引以优化查询性能(查询未发送提醒的记录) +CREATE INDEX `idx_reminder_sent_status_scheduled` ON `t_medication_records` (`reminder_sent`, `status`, `scheduled_time`); \ No newline at end of file diff --git a/src/medications/models/medication-record.model.ts b/src/medications/models/medication-record.model.ts index 4ac7ca5..5f4d5fa 100644 --- a/src/medications/models/medication-record.model.ts +++ b/src/medications/models/medication-record.model.ts @@ -83,6 +83,14 @@ export class MedicationRecord extends Model { }) declare deleted: boolean; + @Column({ + type: DataType.BOOLEAN, + allowNull: false, + defaultValue: false, + comment: '是否已发送提醒', + }) + declare reminderSent: 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 c3fd1b5..16cebb7 100644 --- a/src/medications/services/medication-reminder.service.ts +++ b/src/medications/services/medication-reminder.service.ts @@ -1,4 +1,5 @@ import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { Cron } from '@nestjs/schedule'; import { InjectModel } from '@nestjs/sequelize'; import { Medication } from '../models/medication.model'; @@ -23,30 +24,42 @@ export class MedicationReminderService { @InjectModel(MedicationRecord) private readonly recordModel: typeof MedicationRecord, private readonly pushService: PushNotificationsService, + private readonly configService: ConfigService, ) {} /** - * 每5分钟检查一次需要发送的提醒 + * 每1分钟检查一次需要发送的提醒 + * 只有主进程(NODE_APP_INSTANCE=0)执行,避免多进程重复发送 */ - @Cron('*/5 * * * *') + @Cron('*/1 * * * *') async checkAndSendReminders(): 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('主进程检测到,执行服药提醒检查...'); + // 计算时间范围:当前时间 + 15分钟 const now = new Date(); const reminderTime = dayjs(now) .add(this.REMINDER_MINUTES_BEFORE, 'minute') .toDate(); - // 查找在接下来15分钟内需要提醒的记录 + // 查找在接下来1分钟内需要提醒的记录 const startRange = now; - const endRange = dayjs(now).add(5, 'minute').toDate(); // 5分钟窗口期 + const endRange = dayjs(now).add(1, 'minute').toDate(); // 1分钟窗口期 const upcomingRecords = await this.recordModel.findAll({ where: { status: MedicationStatusEnum.UPCOMING, deleted: false, + reminderSent: false, // 只查询未发送提醒的记录 scheduledTime: { [Op.between]: [ dayjs(startRange).add(this.REMINDER_MINUTES_BEFORE, 'minute').toDate(), @@ -84,11 +97,21 @@ export class MedicationReminderService { } // 为每个用户发送提醒 + let successCount = 0; + let failedCount = 0; + for (const [userId, records] of userRecordsMap.entries()) { - await this.sendReminderToUser(userId, records); + const success = await this.sendReminderToUser(userId, records); + if (success) { + successCount += records.length; + // 标记这些记录已发送提醒 + await this.markRecordsAsReminded(records.map(r => r.id)); + } else { + failedCount += records.length; + } } - this.logger.log(`成功发送 ${upcomingRecords.length} 条服药提醒`); + this.logger.log(`服药提醒发送完成 - 成功: ${successCount}, 失败: ${failedCount}`); } catch (error) { this.logger.error('检查服药提醒失败', error.stack); } @@ -96,11 +119,12 @@ export class MedicationReminderService { /** * 为单个用户发送提醒 + * @returns 是否发送成功 */ private async sendReminderToUser( userId: string, records: MedicationRecord[], - ): Promise { + ): Promise { try { const medicationNames = records .map((r) => r.medication?.name) @@ -127,11 +151,34 @@ export class MedicationReminderService { }); this.logger.log(`成功向用户 ${userId} 发送服药提醒`); + return true; } catch (error) { this.logger.error( `向用户 ${userId} 发送服药提醒失败`, error.stack, ); + return false; + } + } + + /** + * 标记记录为已发送提醒 + */ + private async markRecordsAsReminded(recordIds: string[]): Promise { + try { + await this.recordModel.update( + { reminderSent: true }, + { + where: { + id: { + [Op.in]: recordIds, + }, + }, + }, + ); + this.logger.debug(`已标记 ${recordIds.length} 条记录为已提醒`); + } catch (error) { + this.logger.error('标记记录为已提醒失败', error.stack); } }