feat(medications): 添加提醒发送状态追踪,防止重复推送

- 新增 reminder_sent 字段到服药记录表,用于标记提醒发送状态
- 添加数据库索引优化未发送提醒记录的查询性能
- 提醒检查频率从 5 分钟优化至 1 分钟,提升及时性
- 添加主进程检测机制,避免多进程环境下重复发送提醒
- 增强错误处理和发送结果统计功能
This commit is contained in:
richarjiang
2025-11-11 11:13:44 +08:00
parent 2850eba7cf
commit d9c144ff87
3 changed files with 73 additions and 7 deletions

View File

@@ -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`);

View File

@@ -83,6 +83,14 @@ export class MedicationRecord extends Model {
}) })
declare deleted: boolean; declare deleted: boolean;
@Column({
type: DataType.BOOLEAN,
allowNull: false,
defaultValue: false,
comment: '是否已发送提醒',
})
declare reminderSent: boolean;
// 关联关系 // 关联关系
@BelongsTo(() => Medication, 'medicationId') @BelongsTo(() => Medication, 'medicationId')
declare medication: Medication; declare medication: Medication;

View File

@@ -1,4 +1,5 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Cron } from '@nestjs/schedule'; import { Cron } from '@nestjs/schedule';
import { InjectModel } from '@nestjs/sequelize'; import { InjectModel } from '@nestjs/sequelize';
import { Medication } from '../models/medication.model'; import { Medication } from '../models/medication.model';
@@ -23,30 +24,42 @@ export class MedicationReminderService {
@InjectModel(MedicationRecord) @InjectModel(MedicationRecord)
private readonly recordModel: typeof MedicationRecord, private readonly recordModel: typeof MedicationRecord,
private readonly pushService: PushNotificationsService, private readonly pushService: PushNotificationsService,
private readonly configService: ConfigService,
) {} ) {}
/** /**
* 每5分钟检查一次需要发送的提醒 * 每1分钟检查一次需要发送的提醒
* 只有主进程NODE_APP_INSTANCE=0执行避免多进程重复发送
*/ */
@Cron('*/5 * * * *') @Cron('*/1 * * * *')
async checkAndSendReminders(): Promise<void> { async checkAndSendReminders(): Promise<void> {
this.logger.log('开始检查服药提醒'); this.logger.log('开始检查服药提醒');
try { 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('主进程检测到,执行服药提醒检查...');
// 计算时间范围:当前时间 + 15分钟 // 计算时间范围:当前时间 + 15分钟
const now = new Date(); const now = new Date();
const reminderTime = dayjs(now) const reminderTime = dayjs(now)
.add(this.REMINDER_MINUTES_BEFORE, 'minute') .add(this.REMINDER_MINUTES_BEFORE, 'minute')
.toDate(); .toDate();
// 查找在接下来15分钟内需要提醒的记录 // 查找在接下来1分钟内需要提醒的记录
const startRange = now; 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({ const upcomingRecords = await this.recordModel.findAll({
where: { where: {
status: MedicationStatusEnum.UPCOMING, status: MedicationStatusEnum.UPCOMING,
deleted: false, deleted: false,
reminderSent: false, // 只查询未发送提醒的记录
scheduledTime: { scheduledTime: {
[Op.between]: [ [Op.between]: [
dayjs(startRange).add(this.REMINDER_MINUTES_BEFORE, 'minute').toDate(), 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()) { 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) { } catch (error) {
this.logger.error('检查服药提醒失败', error.stack); this.logger.error('检查服药提醒失败', error.stack);
} }
@@ -96,11 +119,12 @@ export class MedicationReminderService {
/** /**
* 为单个用户发送提醒 * 为单个用户发送提醒
* @returns 是否发送成功
*/ */
private async sendReminderToUser( private async sendReminderToUser(
userId: string, userId: string,
records: MedicationRecord[], records: MedicationRecord[],
): Promise<void> { ): Promise<boolean> {
try { try {
const medicationNames = records const medicationNames = records
.map((r) => r.medication?.name) .map((r) => r.medication?.name)
@@ -127,11 +151,34 @@ export class MedicationReminderService {
}); });
this.logger.log(`成功向用户 ${userId} 发送服药提醒`); this.logger.log(`成功向用户 ${userId} 发送服药提醒`);
return true;
} catch (error) { } catch (error) {
this.logger.error( this.logger.error(
`向用户 ${userId} 发送服药提醒失败`, `向用户 ${userId} 发送服药提醒失败`,
error.stack, error.stack,
); );
return false;
}
}
/**
* 标记记录为已发送提醒
*/
private async markRecordsAsReminded(recordIds: string[]): Promise<void> {
try {
await this.recordModel.update(
{ reminderSent: true },
{
where: {
id: {
[Op.in]: recordIds,
},
},
},
);
this.logger.debug(`已标记 ${recordIds.length} 条记录为已提醒`);
} catch (error) {
this.logger.error('标记记录为已提醒失败', error.stack);
} }
} }