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 { 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(); 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 { 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 { 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 { this.logger.log(`为药物 ${medication.id} 设置提醒(由定时任务自动触发)`); // 实际的提醒由定时任务 checkAndSendReminders 自动处理 // 这里只需要确保药物处于激活状态 } /** * 取消药物的所有提醒(停用或删除药物时调用) */ async cancelRemindersForMedication(medicationId: string): Promise { this.logger.log(`取消药物 ${medicationId} 的所有提醒`); // 由于提醒是基于记录的状态和药物的激活状态动态生成的 // 所以只需要确保药物被停用或删除,定时任务就不会再发送提醒 } /** * 获取用户今天的待提醒数量 */ async getTodayReminderCount(userId: string): Promise { 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; } }