feat(medications): 添加药物超时提醒功能

在MedicationRecord模型中添加overdueReminderSent字段追踪超时提醒状态,在提醒服务中新增定时任务检查超过服药时间1小时的未服用记录,并向用户发送鼓励提醒。该功能每10分钟检查一次,避免重复发送提醒,帮助用户及时补服错过的药物。
This commit is contained in:
richarjiang
2025-11-14 14:47:44 +08:00
parent 5a9be42a93
commit f04c2ccd5d
2 changed files with 165 additions and 2 deletions

View File

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

View File

@@ -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<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('主进程检测到,执行超时服药提醒检查...');
// 计算时间范围当前时间减去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<string, MedicationRecord[]>();
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<boolean> {
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<void> {
try {
await this.recordModel.update(
{ overdueReminderSent: true },
{
where: {
id: {
[Op.in]: recordIds,
},
},
},
);
this.logger.debug(`已标记 ${recordIds.length} 条记录为已超时提醒`);
} catch (error) {
this.logger.error('标记记录为已超时提醒失败', error.stack);
}
}
/**
* 手动为用户发送即时提醒(用于测试或特殊情况)
*/