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