feat(medications): 添加提醒发送状态追踪,防止重复推送
- 新增 reminder_sent 字段到服药记录表,用于标记提醒发送状态 - 添加数据库索引优化未发送提醒记录的查询性能 - 提醒检查频率从 5 分钟优化至 1 分钟,提升及时性 - 添加主进程检测机制,避免多进程环境下重复发送提醒 - 增强错误处理和发送结果统计功能
This commit is contained in:
@@ -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`);
|
||||
@@ -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;
|
||||
|
||||
@@ -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<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('主进程检测到,执行服药提醒检查...');
|
||||
|
||||
// 计算时间范围:当前时间 + 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<void> {
|
||||
): Promise<boolean> {
|
||||
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<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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user