实现了包含药物信息管理、服药记录追踪、统计分析、自动状态更新和推送提醒的完整药物管理系统。 核心功能: - 药物 CRUD 操作,支持多种剂型和自定义服药时间 - 惰性生成服药记录策略,查询时才生成当天记录 - 定时任务自动更新过期记录状态(每30分钟) - 服药前15分钟自动推送提醒(每5分钟检查) - 每日/范围/总体统计分析功能 - 完整的 API 文档和数据库建表脚本 技术实现: - 使用 Sequelize ORM 管理 MySQL 数据表 - 集成 @nestjs/schedule 实现定时任务 - 复用现有推送通知系统发送提醒 - 采用软删除和权限验证保障数据安全
229 lines
6.3 KiB
TypeScript
229 lines
6.3 KiB
TypeScript
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;
|
||
}
|
||
} |