feat(medications): 添加药物超时提醒功能
在MedicationRecord模型中添加overdueReminderSent字段追踪超时提醒状态,在提醒服务中新增定时任务检查超过服药时间1小时的未服用记录,并向用户发送鼓励提醒。该功能每10分钟检查一次,避免重复发送提醒,帮助用户及时补服错过的药物。
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动为用户发送即时提醒(用于测试或特殊情况)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user