Files
plates-server/src/medications/services/record-generator.service.ts
richarjiang 188b4addca feat(medications): 新增完整的药物管理和服药提醒功能
实现了包含药物信息管理、服药记录追踪、统计分析、自动状态更新和推送提醒的完整药物管理系统。

核心功能:
- 药物 CRUD 操作,支持多种剂型和自定义服药时间
- 惰性生成服药记录策略,查询时才生成当天记录
- 定时任务自动更新过期记录状态(每30分钟)
- 服药前15分钟自动推送提醒(每5分钟检查)
- 每日/范围/总体统计分析功能
- 完整的 API 文档和数据库建表脚本

技术实现:
- 使用 Sequelize ORM 管理 MySQL 数据表
- 集成 @nestjs/schedule 实现定时任务
- 复用现有推送通知系统发送提醒
- 采用软删除和权限验证保障数据安全
2025-11-07 17:29:11 +08:00

229 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Injectable, Logger } from '@nestjs/common';
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 { RepeatPatternEnum } from '../enums/repeat-pattern.enum';
import { v4 as uuidv4 } from 'uuid';
import * as dayjs from 'dayjs';
import * as utc from 'dayjs/plugin/utc';
import * as timezone from 'dayjs/plugin/timezone';
dayjs.extend(utc);
dayjs.extend(timezone);
/**
* 服药记录生成服务
* 实现惰性生成策略:当查询时检查并生成当天记录
*/
@Injectable()
export class RecordGeneratorService {
private readonly logger = new Logger(RecordGeneratorService.name);
constructor(
@InjectModel(Medication)
private readonly medicationModel: typeof Medication,
@InjectModel(MedicationRecord)
private readonly recordModel: typeof MedicationRecord,
) {}
/**
* 为指定日期生成服药记录
* @param userId 用户ID
* @param date 日期字符串YYYY-MM-DD
*/
async generateRecordsForDate(userId: string, date: string): Promise<void> {
this.logger.log(`开始为用户 ${userId} 生成 ${date} 的服药记录`);
// 解析目标日期
const targetDate = dayjs(date).startOf('day');
// 查询用户所有激活的药物
const medications = await this.medicationModel.findAll({
where: {
userId,
isActive: true,
deleted: false,
},
});
if (medications.length === 0) {
this.logger.log(`用户 ${userId} 没有激活的药物`);
return;
}
// 为每个药物生成当天的服药记录
for (const medication of medications) {
await this.generateRecordsForMedicationOnDate(medication, targetDate);
}
this.logger.log(`成功为用户 ${userId} 生成 ${date} 的服药记录`);
}
/**
* 为单个药物在指定日期生成服药记录
*/
private async generateRecordsForMedicationOnDate(
medication: Medication,
targetDate: dayjs.Dayjs,
): Promise<void> {
// 检查该日期是否在药物的有效期内
if (!this.isDateInMedicationRange(medication, targetDate)) {
this.logger.debug(
`药物 ${medication.id}${targetDate.format('YYYY-MM-DD')} 不在有效期内`,
);
return;
}
// 检查是否已经生成过该日期的记录
const existingRecords = await this.recordModel.findAll({
where: {
medicationId: medication.id,
userId: medication.userId,
deleted: false,
},
});
// 过滤出当天的记录
const recordsOnDate = existingRecords.filter((record) => {
const recordDate = dayjs(record.scheduledTime).startOf('day');
return recordDate.isSame(targetDate, 'day');
});
if (recordsOnDate.length > 0) {
this.logger.debug(
`药物 ${medication.id}${targetDate.format('YYYY-MM-DD')} 的记录已存在`,
);
return;
}
// 根据重复模式生成记录
if (medication.repeatPattern === RepeatPatternEnum.DAILY) {
await this.generateDailyRecords(medication, targetDate);
}
// 未来可以扩展 WEEKLY 和 CUSTOM 模式
}
/**
* 生成每日重复模式的记录
*/
private async generateDailyRecords(
medication: Medication,
targetDate: dayjs.Dayjs,
): Promise<void> {
const records: any[] = [];
// 为每个服药时间生成一条记录
for (const timeStr of medication.medicationTimes) {
// 解析时间字符串HH:mm
const [hours, minutes] = timeStr.split(':').map(Number);
// 创建计划服药时间UTC
const scheduledTime = targetDate
.hour(hours)
.minute(minutes)
.second(0)
.millisecond(0)
.toDate();
// 判断初始状态
const now = new Date();
const status =
scheduledTime <= now
? MedicationStatusEnum.MISSED
: MedicationStatusEnum.UPCOMING;
records.push({
id: uuidv4(),
medicationId: medication.id,
userId: medication.userId,
scheduledTime,
actualTime: null,
status,
note: null,
deleted: false,
});
}
// 批量创建记录
if (records.length > 0) {
await this.recordModel.bulkCreate(records);
this.logger.log(
`为药物 ${medication.id}${targetDate.format('YYYY-MM-DD')} 生成了 ${records.length} 条记录`,
);
}
}
/**
* 检查日期是否在药物有效期内
*/
private isDateInMedicationRange(
medication: Medication,
targetDate: dayjs.Dayjs,
): boolean {
const startDate = dayjs(medication.startDate).startOf('day');
const endDate = medication.endDate
? dayjs(medication.endDate).startOf('day')
: null;
// 检查是否在开始日期之后
if (targetDate.isBefore(startDate, 'day')) {
return false;
}
// 检查是否在结束日期之前(如果有结束日期)
if (endDate && targetDate.isAfter(endDate, 'day')) {
return false;
}
return true;
}
/**
* 检查并生成指定日期的记录(如果不存在)
* @param userId 用户ID
* @param date 日期字符串YYYY-MM-DD
* @returns 是否生成了新记录
*/
async ensureRecordsExist(userId: string, date: string): Promise<boolean> {
const targetDate = dayjs(date).format('YYYY-MM-DD');
// 检查该日期是否已有记录
const startOfDay = dayjs(date).startOf('day').toDate();
const endOfDay = dayjs(date).endOf('day').toDate();
const existingRecords = await this.recordModel.count({
where: {
userId,
deleted: false,
},
});
// 简单判断:如果没有任何记录,则生成
const recordsCount = await this.recordModel.count({
where: {
userId,
deleted: false,
},
});
// 这里使用更精确的查询
const Op = require('sequelize').Op;
const recordsOnDate = await this.recordModel.count({
where: {
userId,
deleted: false,
scheduledTime: {
[Op.between]: [startOfDay, endOfDay],
},
},
});
if (recordsOnDate === 0) {
await this.generateRecordsForDate(userId, targetDate);
return true;
}
return false;
}
}