实现了包含药物信息管理、服药记录追踪、统计分析、自动状态更新和推送提醒的完整药物管理系统。 核心功能: - 药物 CRUD 操作,支持多种剂型和自定义服药时间 - 惰性生成服药记录策略,查询时才生成当天记录 - 定时任务自动更新过期记录状态(每30分钟) - 服药前15分钟自动推送提醒(每5分钟检查) - 每日/范围/总体统计分析功能 - 完整的 API 文档和数据库建表脚本 技术实现: - 使用 Sequelize ORM 管理 MySQL 数据表 - 集成 @nestjs/schedule 实现定时任务 - 复用现有推送通知系统发送提醒 - 采用软删除和权限验证保障数据安全
211 lines
6.2 KiB
TypeScript
211 lines
6.2 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
|
import { Cron } from '@nestjs/schedule';
|
|
import { InjectModel } from '@nestjs/sequelize';
|
|
import { Medication } from '../models/medication.model';
|
|
import { MedicationRecord } from '../models/medication-record.model';
|
|
import { MedicationStatusEnum } from '../enums/medication-status.enum';
|
|
import { PushNotificationsService } from '../../push-notifications/push-notifications.service';
|
|
import { Op } from 'sequelize';
|
|
import * as dayjs from 'dayjs';
|
|
|
|
/**
|
|
* 药物提醒推送服务
|
|
* 在服药时间前15分钟发送推送提醒
|
|
*/
|
|
@Injectable()
|
|
export class MedicationReminderService {
|
|
private readonly logger = new Logger(MedicationReminderService.name);
|
|
private readonly REMINDER_MINUTES_BEFORE = 15; // 提前15分钟提醒
|
|
|
|
constructor(
|
|
@InjectModel(Medication)
|
|
private readonly medicationModel: typeof Medication,
|
|
@InjectModel(MedicationRecord)
|
|
private readonly recordModel: typeof MedicationRecord,
|
|
private readonly pushService: PushNotificationsService,
|
|
) {}
|
|
|
|
/**
|
|
* 每5分钟检查一次需要发送的提醒
|
|
*/
|
|
@Cron('*/5 * * * *')
|
|
async checkAndSendReminders(): Promise<void> {
|
|
this.logger.log('开始检查服药提醒');
|
|
|
|
try {
|
|
// 计算时间范围:当前时间 + 15分钟
|
|
const now = new Date();
|
|
const reminderTime = dayjs(now)
|
|
.add(this.REMINDER_MINUTES_BEFORE, 'minute')
|
|
.toDate();
|
|
|
|
// 查找在接下来15分钟内需要提醒的记录
|
|
const startRange = now;
|
|
const endRange = dayjs(now).add(5, 'minute').toDate(); // 5分钟窗口期
|
|
|
|
const upcomingRecords = await this.recordModel.findAll({
|
|
where: {
|
|
status: MedicationStatusEnum.UPCOMING,
|
|
deleted: false,
|
|
scheduledTime: {
|
|
[Op.between]: [
|
|
dayjs(startRange).add(this.REMINDER_MINUTES_BEFORE, 'minute').toDate(),
|
|
dayjs(endRange).add(this.REMINDER_MINUTES_BEFORE, 'minute').toDate(),
|
|
],
|
|
},
|
|
},
|
|
include: [
|
|
{
|
|
model: Medication,
|
|
as: 'medication',
|
|
where: {
|
|
isActive: true,
|
|
deleted: false,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
if (upcomingRecords.length === 0) {
|
|
this.logger.debug('没有需要发送的服药提醒');
|
|
return;
|
|
}
|
|
|
|
this.logger.log(`找到 ${upcomingRecords.length} 条需要发送提醒的记录`);
|
|
|
|
// 按用户分组发送提醒
|
|
const userRecordsMap = new Map<string, MedicationRecord[]>();
|
|
for (const record of upcomingRecords) {
|
|
const userId = record.userId;
|
|
if (!userRecordsMap.has(userId)) {
|
|
userRecordsMap.set(userId, []);
|
|
}
|
|
userRecordsMap.get(userId)!.push(record);
|
|
}
|
|
|
|
// 为每个用户发送提醒
|
|
for (const [userId, records] of userRecordsMap.entries()) {
|
|
await this.sendReminderToUser(userId, records);
|
|
}
|
|
|
|
this.logger.log(`成功发送 ${upcomingRecords.length} 条服药提醒`);
|
|
} catch (error) {
|
|
this.logger.error('检查服药提醒失败', error.stack);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 为单个用户发送提醒
|
|
*/
|
|
private async sendReminderToUser(
|
|
userId: string,
|
|
records: MedicationRecord[],
|
|
): Promise<void> {
|
|
try {
|
|
const medicationNames = records
|
|
.map((r) => r.medication?.name)
|
|
.filter(Boolean)
|
|
.join('、');
|
|
|
|
const title = '服药提醒';
|
|
const body =
|
|
records.length === 1
|
|
? `该服用 ${medicationNames} 了`
|
|
: `该服用 ${records.length} 种药物了:${medicationNames}`;
|
|
|
|
await this.pushService.sendNotification({
|
|
userIds: [userId],
|
|
title,
|
|
body,
|
|
payload: {
|
|
type: 'medication_reminder',
|
|
recordIds: records.map((r) => r.id),
|
|
medicationIds: records.map((r) => r.medicationId),
|
|
},
|
|
sound: 'default',
|
|
badge: 1,
|
|
});
|
|
|
|
this.logger.log(`成功向用户 ${userId} 发送服药提醒`);
|
|
} catch (error) {
|
|
this.logger.error(
|
|
`向用户 ${userId} 发送服药提醒失败`,
|
|
error.stack,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 手动为用户发送即时提醒(用于测试或特殊情况)
|
|
*/
|
|
async sendImmediateReminder(userId: string, recordId: string): Promise<void> {
|
|
const record = await this.recordModel.findOne({
|
|
where: {
|
|
id: recordId,
|
|
userId,
|
|
deleted: false,
|
|
},
|
|
include: [
|
|
{
|
|
model: Medication,
|
|
as: 'medication',
|
|
},
|
|
],
|
|
});
|
|
|
|
if (!record || !record.medication) {
|
|
throw new Error('服药记录不存在');
|
|
}
|
|
|
|
await this.sendReminderToUser(userId, [record]);
|
|
}
|
|
|
|
/**
|
|
* 为新创建的药物设置提醒(预留方法,实际提醒由定时任务触发)
|
|
*/
|
|
async setupRemindersForMedication(medication: Medication): Promise<void> {
|
|
this.logger.log(`为药物 ${medication.id} 设置提醒(由定时任务自动触发)`);
|
|
// 实际的提醒由定时任务 checkAndSendReminders 自动处理
|
|
// 这里只需要确保药物处于激活状态
|
|
}
|
|
|
|
/**
|
|
* 取消药物的所有提醒(停用或删除药物时调用)
|
|
*/
|
|
async cancelRemindersForMedication(medicationId: string): Promise<void> {
|
|
this.logger.log(`取消药物 ${medicationId} 的所有提醒`);
|
|
// 由于提醒是基于记录的状态和药物的激活状态动态生成的
|
|
// 所以只需要确保药物被停用或删除,定时任务就不会再发送提醒
|
|
}
|
|
|
|
/**
|
|
* 获取用户今天的待提醒数量
|
|
*/
|
|
async getTodayReminderCount(userId: string): Promise<number> {
|
|
const startOfDay = dayjs().startOf('day').toDate();
|
|
const endOfDay = dayjs().endOf('day').toDate();
|
|
|
|
const count = await this.recordModel.count({
|
|
where: {
|
|
userId,
|
|
status: MedicationStatusEnum.UPCOMING,
|
|
deleted: false,
|
|
scheduledTime: {
|
|
[Op.between]: [startOfDay, endOfDay],
|
|
},
|
|
},
|
|
include: [
|
|
{
|
|
model: Medication,
|
|
as: 'medication',
|
|
where: {
|
|
isActive: true,
|
|
deleted: false,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
return count;
|
|
}
|
|
} |