feat(medications): 添加药物超时提醒功能
在MedicationRecord模型中添加overdueReminderSent字段追踪超时提醒状态,在提醒服务中新增定时任务检查超过服药时间1小时的未服用记录,并向用户发送鼓励提醒。该功能每10分钟检查一次,避免重复发送提醒,帮助用户及时补服错过的药物。
This commit is contained in:
@@ -91,6 +91,14 @@ export class MedicationRecord extends Model {
|
|||||||
})
|
})
|
||||||
declare reminderSent: boolean;
|
declare reminderSent: boolean;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: DataType.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false,
|
||||||
|
comment: '是否已发送超时提醒',
|
||||||
|
})
|
||||||
|
declare overdueReminderSent: boolean;
|
||||||
|
|
||||||
// 关联关系
|
// 关联关系
|
||||||
@BelongsTo(() => Medication, 'medicationId')
|
@BelongsTo(() => Medication, 'medicationId')
|
||||||
declare medication: Medication;
|
declare medication: Medication;
|
||||||
|
|||||||
@@ -11,12 +11,13 @@ import * as dayjs from 'dayjs';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 药物提醒推送服务
|
* 药物提醒推送服务
|
||||||
* 在服药时间前15分钟发送推送提醒
|
* 在服药时间前15分钟发送推送提醒,并在超过服药时间1小时后发送鼓励提醒
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MedicationReminderService {
|
export class MedicationReminderService {
|
||||||
private readonly logger = new Logger(MedicationReminderService.name);
|
private readonly logger = new Logger(MedicationReminderService.name);
|
||||||
private readonly REMINDER_MINUTES_BEFORE = 15; // 提前15分钟提醒
|
private readonly REMINDER_MINUTES_BEFORE = 15; // 提前15分钟提醒
|
||||||
|
private readonly OVERDUE_HOURS_THRESHOLD = 1; // 超过1小时后发送超时提醒
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectModel(Medication)
|
@InjectModel(Medication)
|
||||||
@@ -28,7 +29,7 @@ export class MedicationReminderService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 每1分钟检查一次需要发送的提醒
|
* 每1分钟检查一次需要发送的提前提醒
|
||||||
* 只有主进程(NODE_APP_INSTANCE=0)执行,避免多进程重复发送
|
* 只有主进程(NODE_APP_INSTANCE=0)执行,避免多进程重复发送
|
||||||
*/
|
*/
|
||||||
@Cron('*/1 * * * *')
|
@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 是否发送成功
|
* @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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 手动为用户发送即时提醒(用于测试或特殊情况)
|
* 手动为用户发送即时提醒(用于测试或特殊情况)
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user