feat(health): 新增服药任务自动生成定时任务
- 新增 MedicationTaskJob:每日生成服药任务、标记过期记录 - 新增 MedicationRemindJob:扫描并发送服药提醒 - 扩展 HealthMedicationRecordMapper:新增统计和查询方法 - 新增定时任务配置SQL和提醒日志表
This commit is contained in:
@@ -103,4 +103,30 @@ public interface HealthMedicationRecordMapper
|
|||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
public int removeHealthMedicationRecordByIds(Long[] ids);
|
public int removeHealthMedicationRecordByIds(Long[] ids);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计某计划某天的记录数(用于检查是否已生成)
|
||||||
|
*
|
||||||
|
* @param planId 计划ID
|
||||||
|
* @param date 日期
|
||||||
|
* @return 记录数
|
||||||
|
*/
|
||||||
|
public int countByPlanAndDate(@Param("planId") Long planId, @Param("date") String date);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记过期的服药记录(超过24小时未服用)
|
||||||
|
*
|
||||||
|
* @param expireThreshold 过期阈值时间
|
||||||
|
* @return 更新的记录数
|
||||||
|
*/
|
||||||
|
public int markExpiredRecords(@Param("expireThreshold") java.time.LocalDateTime expireThreshold);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询待提醒的服药记录
|
||||||
|
*
|
||||||
|
* @param startTime 开始时间
|
||||||
|
* @param endTime 结束时间
|
||||||
|
* @return 服药记录集合
|
||||||
|
*/
|
||||||
|
public List<HealthMedicationRecordVo> selectPendingRemindRecords(@Param("startTime") java.time.LocalDateTime startTime, @Param("endTime") java.time.LocalDateTime endTime);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
package com.intc.health.task;
|
||||||
|
|
||||||
|
import com.intc.health.domain.vo.HealthMedicationRecordVo;
|
||||||
|
import com.intc.health.mapper.HealthMedicationRecordMapper;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用药提醒定时器
|
||||||
|
* 用于扫描待提醒的服药任务并发送提醒
|
||||||
|
*
|
||||||
|
* @author bot5
|
||||||
|
* @date 2026-03-19
|
||||||
|
*/
|
||||||
|
@Component("medicationRemindJob")
|
||||||
|
public class MedicationRemindJob {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(MedicationRemindJob.class);
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private HealthMedicationRecordMapper recordMapper;
|
||||||
|
|
||||||
|
// TODO: 注入消息推送服务,后续实现
|
||||||
|
// @Resource
|
||||||
|
// private MessagePushService messagePushService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫描并发送服药提醒
|
||||||
|
* 定时任务配置:每分钟执行
|
||||||
|
* 调用方式:medicationRemindJob.scanAndSendReminders
|
||||||
|
*/
|
||||||
|
public void scanAndSendReminders() {
|
||||||
|
log.debug("========== 开始扫描服药提醒 ==========");
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
// 扫描未来1分钟内的待服药记录
|
||||||
|
LocalDateTime startTime = now.plusMinutes(-1);
|
||||||
|
LocalDateTime endTime = now.plusMinutes(1);
|
||||||
|
|
||||||
|
List<HealthMedicationRecordVo> pendingRecords = recordMapper.selectPendingRemindRecords(startTime, endTime);
|
||||||
|
|
||||||
|
if (pendingRecords == null || pendingRecords.isEmpty()) {
|
||||||
|
log.debug("没有需要提醒的服药记录");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("发现 {} 条待提醒的服药记录", pendingRecords.size());
|
||||||
|
|
||||||
|
for (HealthMedicationRecordVo record : pendingRecords) {
|
||||||
|
try {
|
||||||
|
sendReminder(record);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("发送服药提醒失败,记录ID: {}, 错误: {}", record.getId(), e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("========== 服药提醒扫描完成 ==========");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送服药提醒
|
||||||
|
*
|
||||||
|
* @param record 服药记录
|
||||||
|
*/
|
||||||
|
private void sendReminder(HealthMedicationRecordVo record) {
|
||||||
|
log.info("发送服药提醒: 人员={}, 药品={}, 计划时间={}",
|
||||||
|
record.getPersonName(),
|
||||||
|
record.getMedicineName(),
|
||||||
|
record.getScheduledTime());
|
||||||
|
|
||||||
|
// TODO: 实现消息推送逻辑
|
||||||
|
// 1. 查询用户的提醒设置
|
||||||
|
// 2. 根据设置选择推送渠道(APP推送/微信/短信)
|
||||||
|
// 3. 发送推送消息
|
||||||
|
// 4. 记录提醒日志
|
||||||
|
|
||||||
|
// 暂时只打印日志
|
||||||
|
String message = String.format("【服药提醒】%s,请按时服用 %s,剂量:%s%s",
|
||||||
|
record.getPersonName(),
|
||||||
|
record.getMedicineName(),
|
||||||
|
record.getDosage() != null ? record.getDosage() : "",
|
||||||
|
record.getDosageUnit() != null ? record.getDosageUnit() : "");
|
||||||
|
|
||||||
|
log.info("推送消息: {}", message);
|
||||||
|
|
||||||
|
// TODO: 调用消息推送服务
|
||||||
|
// messagePushService.push(record.getPersonId(), message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫描超时未服药记录并发送二次提醒
|
||||||
|
* 定时任务配置:每5分钟执行
|
||||||
|
* 调用方式:medicationRemindJob.scanOverdueRecords
|
||||||
|
*/
|
||||||
|
public void scanOverdueRecords() {
|
||||||
|
log.debug("========== 开始扫描超时未服药记录 ==========");
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
// 扫描超过计划时间30分钟仍未服药的记录
|
||||||
|
LocalDateTime overdueThreshold = now.minusMinutes(30);
|
||||||
|
|
||||||
|
// 查询超时的待服药记录
|
||||||
|
List<HealthMedicationRecordVo> overdueRecords = recordMapper.selectPendingRemindRecords(
|
||||||
|
overdueThreshold.minusMinutes(5), // 查询5分钟内超时的
|
||||||
|
overdueThreshold
|
||||||
|
);
|
||||||
|
|
||||||
|
if (overdueRecords == null || overdueRecords.isEmpty()) {
|
||||||
|
log.debug("没有超时未服药记录");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("发现 {} 条超时未服药记录", overdueRecords.size());
|
||||||
|
|
||||||
|
for (HealthMedicationRecordVo record : overdueRecords) {
|
||||||
|
try {
|
||||||
|
sendOverdueReminder(record);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("发送超时提醒失败,记录ID: {}, 错误: {}", record.getId(), e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("========== 超时未服药扫描完成 ==========");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送超时提醒(二次提醒/通知家属)
|
||||||
|
*
|
||||||
|
* @param record 服药记录
|
||||||
|
*/
|
||||||
|
private void sendOverdueReminder(HealthMedicationRecordVo record) {
|
||||||
|
log.warn("超时未服药: 人员={}, 药品={}, 计划时间={}",
|
||||||
|
record.getPersonName(),
|
||||||
|
record.getMedicineName(),
|
||||||
|
record.getScheduledTime());
|
||||||
|
|
||||||
|
// TODO: 实现超时提醒逻辑
|
||||||
|
// 1. 发送二次提醒给用户
|
||||||
|
// 2. 如果配置了家属通知,同时通知家属
|
||||||
|
// 3. 记录提醒日志
|
||||||
|
|
||||||
|
String message = String.format("【漏服提醒】%s 未按时服用 %s,请及时处理",
|
||||||
|
record.getPersonName(),
|
||||||
|
record.getMedicineName());
|
||||||
|
|
||||||
|
log.info("推送超时消息: {}", message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,333 @@
|
|||||||
|
package com.intc.health.task;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import com.alibaba.fastjson2.JSONArray;
|
||||||
|
import com.intc.common.core.utils.IdWorker;
|
||||||
|
import com.intc.health.domain.HealthMedicationRecord;
|
||||||
|
import com.intc.health.domain.vo.HealthMedicationPlanVo;
|
||||||
|
import com.intc.health.mapper.HealthMedicationPlanMapper;
|
||||||
|
import com.intc.health.mapper.HealthMedicationRecordMapper;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.time.DayOfWeek;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用药任务定时器
|
||||||
|
* 用于每日自动生成服药记录
|
||||||
|
*
|
||||||
|
* @author bot5
|
||||||
|
* @date 2026-03-19
|
||||||
|
*/
|
||||||
|
@Component("medicationTaskJob")
|
||||||
|
public class MedicationTaskJob {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(MedicationTaskJob.class);
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private HealthMedicationPlanMapper planMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private HealthMedicationRecordMapper recordMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每日生成服药任务
|
||||||
|
* 定时任务配置:每日 00:05 执行
|
||||||
|
* 调用方式:medicationTaskJob.generateDailyTasks
|
||||||
|
*/
|
||||||
|
public void generateDailyTasks() {
|
||||||
|
log.info("========== 开始生成每日服药任务 ==========");
|
||||||
|
LocalDate today = LocalDate.now();
|
||||||
|
int generatedCount = generateTasksForDate(today);
|
||||||
|
log.info("========== 每日服药任务生成完成,共生成 {} 条记录 ==========", generatedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成指定日期的服药任务
|
||||||
|
* 可用于补生成历史任务
|
||||||
|
*
|
||||||
|
* @param date 日期
|
||||||
|
* @return 生成的记录数
|
||||||
|
*/
|
||||||
|
public int generateTasksForDate(LocalDate date) {
|
||||||
|
int generatedCount = 0;
|
||||||
|
|
||||||
|
// 查询所有进行中的计划
|
||||||
|
List<HealthMedicationPlanVo> activePlans = planMapper.selectActivePlanList();
|
||||||
|
if (activePlans == null || activePlans.isEmpty()) {
|
||||||
|
log.info("没有进行中的用药计划");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
String dateStr = date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
|
||||||
|
|
||||||
|
for (HealthMedicationPlanVo plan : activePlans) {
|
||||||
|
try {
|
||||||
|
// 检查该计划当天是否需要服药
|
||||||
|
if (!shouldTakeMedicineOnDate(plan, date)) {
|
||||||
|
log.debug("计划 {} 在 {} 不需要服药", plan.getPlanName(), dateStr);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查该计划当天是否已生成过记录
|
||||||
|
int existCount = recordMapper.countByPlanAndDate(plan.getId(), dateStr);
|
||||||
|
if (existCount > 0) {
|
||||||
|
log.debug("计划 {} 在 {} 已有 {} 条记录,跳过", plan.getPlanName(), dateStr, existCount);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成当天的服药记录
|
||||||
|
List<HealthMedicationRecord> records = generateRecordsForPlan(plan, date);
|
||||||
|
if (!records.isEmpty()) {
|
||||||
|
recordMapper.batchInsertHealthMedicationRecord(records);
|
||||||
|
generatedCount += records.size();
|
||||||
|
log.info("计划 {} 在 {} 生成了 {} 条服药记录", plan.getPlanName(), dateStr, records.size());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("生成计划 {} 的服药记录失败: {}", plan.getId(), e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return generatedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断某计划在指定日期是否需要服药
|
||||||
|
*
|
||||||
|
* @param plan 用药计划
|
||||||
|
* @param date 日期
|
||||||
|
* @return true-需要服药,false-不需要
|
||||||
|
*/
|
||||||
|
private boolean shouldTakeMedicineOnDate(HealthMedicationPlanVo plan, LocalDate date) {
|
||||||
|
// 检查是否在计划日期范围内
|
||||||
|
LocalDate startDate = plan.getStartDate() != null ?
|
||||||
|
plan.getStartDate().toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate() : null;
|
||||||
|
LocalDate endDate = plan.getEndDate() != null ?
|
||||||
|
plan.getEndDate().toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate() : null;
|
||||||
|
|
||||||
|
if (startDate != null && date.isBefore(startDate)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (endDate != null && date.isAfter(endDate)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据频率类型判断
|
||||||
|
String frequencyType = plan.getFrequencyType();
|
||||||
|
if (frequencyType == null) {
|
||||||
|
frequencyType = "1"; // 默认每日
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (frequencyType) {
|
||||||
|
case "1": // 每日
|
||||||
|
return true;
|
||||||
|
case "2": // 隔日
|
||||||
|
return shouldTakeOnAlternateDay(plan, date);
|
||||||
|
case "3": // 每周
|
||||||
|
return shouldTakeOnWeekly(plan, date);
|
||||||
|
case "4": // 自定义
|
||||||
|
return shouldTakeOnCustom(plan, date);
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断隔日服药
|
||||||
|
*/
|
||||||
|
private boolean shouldTakeOnAlternateDay(HealthMedicationPlanVo plan, LocalDate date) {
|
||||||
|
LocalDate startDate = plan.getStartDate() != null ?
|
||||||
|
plan.getStartDate().toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate() : date;
|
||||||
|
long daysBetween = ChronoUnit.DAYS.between(startDate, date);
|
||||||
|
return daysBetween % 2 == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断每周服药
|
||||||
|
*/
|
||||||
|
private boolean shouldTakeOnWeekly(HealthMedicationPlanVo plan, LocalDate date) {
|
||||||
|
String weekDays = plan.getWeekDays();
|
||||||
|
if (weekDays == null || weekDays.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// weekDays 格式为 JSON 数组,如 [1,3,5] 表示周一三五
|
||||||
|
JSONArray daysArray = JSON.parseArray(weekDays);
|
||||||
|
DayOfWeek dayOfWeek = date.getDayOfWeek();
|
||||||
|
int dayValue = dayOfWeek.getValue(); // 1=周一, 7=周日
|
||||||
|
|
||||||
|
for (int i = 0; i < daysArray.size(); i++) {
|
||||||
|
if (daysArray.getInteger(i) == dayValue) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("解析周几配置失败: {}", weekDays, e);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断自定义频率服药
|
||||||
|
* 暂时默认返回 true,后续可根据具体需求扩展
|
||||||
|
*/
|
||||||
|
private boolean shouldTakeOnCustom(HealthMedicationPlanVo plan, LocalDate date) {
|
||||||
|
// 自定义频率可以根据 remark 或其他字段配置
|
||||||
|
// 暂时默认每日都服药
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为某个计划生成指定日期的服药记录
|
||||||
|
*
|
||||||
|
* @param plan 用药计划
|
||||||
|
* @param date 日期
|
||||||
|
* @return 服药记录列表
|
||||||
|
*/
|
||||||
|
private List<HealthMedicationRecord> generateRecordsForPlan(HealthMedicationPlanVo plan, LocalDate date) {
|
||||||
|
List<HealthMedicationRecord> records = new ArrayList<>();
|
||||||
|
|
||||||
|
// 获取服药时间点
|
||||||
|
String timePoints = plan.getTimePoints();
|
||||||
|
if (timePoints == null || timePoints.isEmpty()) {
|
||||||
|
// 如果没有配置时间点,根据频率生成默认时间点
|
||||||
|
timePoints = generateDefaultTimePoints(plan.getFrequency());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
JSONArray timePointsArray = JSON.parseArray(timePoints);
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
|
||||||
|
for (int i = 0; i < timePointsArray.size(); i++) {
|
||||||
|
String timeStr = timePointsArray.getString(i);
|
||||||
|
LocalTime time = parseTime(timeStr);
|
||||||
|
|
||||||
|
HealthMedicationRecord record = new HealthMedicationRecord();
|
||||||
|
record.setId(IdWorker.getId());
|
||||||
|
record.setPlanId(plan.getId());
|
||||||
|
record.setPersonId(plan.getPersonId());
|
||||||
|
record.setMedicineId(plan.getMedicineId());
|
||||||
|
record.setScheduledTime(LocalDateTime.of(date, time));
|
||||||
|
record.setDosage(plan.getDosage());
|
||||||
|
record.setDosageUnit(plan.getDosageUnit());
|
||||||
|
record.setStatus("1"); // 待服用
|
||||||
|
record.setCreateTime(now);
|
||||||
|
record.setDelFlag("0");
|
||||||
|
|
||||||
|
records.add(record);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("解析时间点配置失败: {}", timePoints, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据每日次数生成默认时间点
|
||||||
|
*
|
||||||
|
* @param frequency 每日次数
|
||||||
|
* @return JSON 格式的时间点数组
|
||||||
|
*/
|
||||||
|
private String generateDefaultTimePoints(Integer frequency) {
|
||||||
|
if (frequency == null || frequency <= 0) {
|
||||||
|
frequency = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONArray timePoints = new JSONArray();
|
||||||
|
switch (frequency) {
|
||||||
|
case 1:
|
||||||
|
timePoints.add("08:00");
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
timePoints.add("08:00");
|
||||||
|
timePoints.add("20:00");
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
timePoints.add("08:00");
|
||||||
|
timePoints.add("12:00");
|
||||||
|
timePoints.add("20:00");
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
timePoints.add("08:00");
|
||||||
|
timePoints.add("12:00");
|
||||||
|
timePoints.add("16:00");
|
||||||
|
timePoints.add("20:00");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// 超过4次,均匀分布在 8:00-22:00 之间
|
||||||
|
int startHour = 8;
|
||||||
|
int endHour = 22;
|
||||||
|
int interval = (endHour - startHour) / frequency;
|
||||||
|
for (int i = 0; i < frequency; i++) {
|
||||||
|
int hour = startHour + i * interval;
|
||||||
|
timePoints.add(String.format("%02d:00", hour));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return timePoints.toJSONString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析时间字符串
|
||||||
|
*
|
||||||
|
* @param timeStr 时间字符串,格式如 "08:00" 或 "8:00"
|
||||||
|
* @return LocalTime
|
||||||
|
*/
|
||||||
|
private LocalTime parseTime(String timeStr) {
|
||||||
|
try {
|
||||||
|
// 支持多种格式
|
||||||
|
String normalized = timeStr.trim();
|
||||||
|
if (normalized.length() == 4 && !normalized.contains(":")) {
|
||||||
|
// "0800" -> "08:00"
|
||||||
|
normalized = normalized.substring(0, 2) + ":" + normalized.substring(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] parts = normalized.split(":");
|
||||||
|
int hour = Integer.parseInt(parts[0]);
|
||||||
|
int minute = parts.length > 1 ? Integer.parseInt(parts[1]) : 0;
|
||||||
|
return LocalTime.of(hour, minute);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("解析时间失败: {},使用默认时间 08:00", timeStr);
|
||||||
|
return LocalTime.of(8, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查并标记过期的服药记录
|
||||||
|
* 定时任务配置:每5分钟执行
|
||||||
|
* 调用方式:medicationTaskJob.markExpiredRecords
|
||||||
|
*/
|
||||||
|
public void markExpiredRecords() {
|
||||||
|
log.info("========== 开始检查过期服药记录 ==========");
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
// 超过计划时间24小时的记录标记为过期
|
||||||
|
LocalDateTime expireThreshold = now.minusHours(24);
|
||||||
|
|
||||||
|
int count = recordMapper.markExpiredRecords(expireThreshold);
|
||||||
|
log.info("========== 标记过期记录完成,共 {} 条 ==========", count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动触发生成指定日期的任务(用于测试或补录)
|
||||||
|
*
|
||||||
|
* @param dateStr 日期字符串,格式 yyyy-MM-dd
|
||||||
|
* @return 生成的记录数
|
||||||
|
*/
|
||||||
|
public int generateTasksManually(String dateStr) {
|
||||||
|
LocalDate date = LocalDate.parse(dateStr, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
|
||||||
|
return generateTasksForDate(date);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -169,4 +169,25 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
#{id}
|
#{id}
|
||||||
</foreach>
|
</foreach>
|
||||||
</update>
|
</update>
|
||||||
|
|
||||||
|
<select id="countByPlanAndDate" resultType="int">
|
||||||
|
select count(1) from health_medication_record
|
||||||
|
where del_flag='0' and plan_id = #{planId}
|
||||||
|
and date(scheduled_time) = date(#{date})
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<update id="markExpiredRecords">
|
||||||
|
update health_medication_record
|
||||||
|
set status = '4', update_time = current_timestamp
|
||||||
|
where del_flag='0' and status = '1'
|
||||||
|
and scheduled_time < #{expireThreshold}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<select id="selectPendingRemindRecords" resultMap="HealthMedicationRecordResult">
|
||||||
|
<include refid="selectHealthMedicationRecordVo"/>
|
||||||
|
where a.del_flag='0' and a.status = '1'
|
||||||
|
and a.scheduled_time >= #{startTime}
|
||||||
|
and a.scheduled_time <= #{endTime}
|
||||||
|
order by a.scheduled_time asc
|
||||||
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
90
sql/20250319-medication-task-job.sql
Normal file
90
sql/20250319-medication-task-job.sql
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
-- ========================================
|
||||||
|
-- 用药任务定时任务配置
|
||||||
|
-- 在 sys_job 表中插入定时任务
|
||||||
|
-- ========================================
|
||||||
|
|
||||||
|
-- 1. 每日生成服药任务(每日 00:05 执行)
|
||||||
|
INSERT INTO sys_job (job_id, job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark)
|
||||||
|
VALUES (
|
||||||
|
nextval('seq_sys_job'),
|
||||||
|
'每日生成服药任务',
|
||||||
|
'DEFAULT',
|
||||||
|
'medicationTaskJob.generateDailyTasks',
|
||||||
|
'0 5 0 * * ?',
|
||||||
|
'2',
|
||||||
|
'1',
|
||||||
|
'0',
|
||||||
|
'admin',
|
||||||
|
NOW(),
|
||||||
|
'每日凌晨00:05自动生成当天的服药任务'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 2. 扫描并发送服药提醒(每分钟执行)
|
||||||
|
INSERT INTO sys_job (job_id, job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark)
|
||||||
|
VALUES (
|
||||||
|
nextval('seq_sys_job'),
|
||||||
|
'扫描服药提醒',
|
||||||
|
'DEFAULT',
|
||||||
|
'medicationRemindJob.scanAndSendReminders',
|
||||||
|
'0 * * * * ?',
|
||||||
|
'2',
|
||||||
|
'1',
|
||||||
|
'0',
|
||||||
|
'admin',
|
||||||
|
NOW(),
|
||||||
|
'每分钟扫描待提醒的服药任务并发送提醒'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 3. 扫描超时未服药记录(每5分钟执行)
|
||||||
|
INSERT INTO sys_job (job_id, job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark)
|
||||||
|
VALUES (
|
||||||
|
nextval('seq_sys_job'),
|
||||||
|
'扫描超时未服药',
|
||||||
|
'DEFAULT',
|
||||||
|
'medicationRemindJob.scanOverdueRecords',
|
||||||
|
'0 */5 * * * ?',
|
||||||
|
'2',
|
||||||
|
'1',
|
||||||
|
'0',
|
||||||
|
'admin',
|
||||||
|
NOW(),
|
||||||
|
'每5分钟扫描超时未服药的记录并发送提醒'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 4. 标记过期服药记录(每小时执行)
|
||||||
|
INSERT INTO sys_job (job_id, job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark)
|
||||||
|
VALUES (
|
||||||
|
nextval('seq_sys_job'),
|
||||||
|
'标记过期服药记录',
|
||||||
|
'DEFAULT',
|
||||||
|
'medicationTaskJob.markExpiredRecords',
|
||||||
|
'0 0 * * * ?',
|
||||||
|
'2',
|
||||||
|
'1',
|
||||||
|
'0',
|
||||||
|
'admin',
|
||||||
|
NOW(),
|
||||||
|
'每小时检查并标记过期的服药记录'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ========================================
|
||||||
|
-- 提醒日志表(可选,用于记录提醒历史)
|
||||||
|
-- ========================================
|
||||||
|
DROP TABLE IF EXISTS health_medication_remind_log;
|
||||||
|
CREATE TABLE health_medication_remind_log (
|
||||||
|
id BIGINT NOT NULL,
|
||||||
|
record_id BIGINT NOT NULL,
|
||||||
|
plan_id BIGINT NOT NULL,
|
||||||
|
person_id BIGINT NOT NULL,
|
||||||
|
remind_type CHAR(1) DEFAULT '1' COMMENT '提醒类型:1-按时提醒 2-超时提醒 3-漏服通知家属',
|
||||||
|
remind_time DATETIME NOT NULL,
|
||||||
|
remind_channel VARCHAR(20) DEFAULT 'app' COMMENT '提醒渠道:app/sms/wechat',
|
||||||
|
remind_result CHAR(1) DEFAULT '1' COMMENT '结果:1-成功 2-失败',
|
||||||
|
error_msg VARCHAR(500) NULL,
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
) ENGINE=InnoDB COMMENT='用药提醒日志表';
|
||||||
|
|
||||||
|
CREATE INDEX idx_rmrl_record_id ON health_medication_remind_log(record_id);
|
||||||
|
CREATE INDEX idx_rmrl_person_id ON health_medication_remind_log(person_id);
|
||||||
|
CREATE INDEX idx_rmrl_remind_time ON health_medication_remind_log(remind_time);
|
||||||
Reference in New Issue
Block a user