package com.smartor.service.impl; import com.alibaba.fastjson2.JSON; import com.ruoyi.common.exception.base.BaseException; import com.ruoyi.common.utils.DateUtils; import com.ruoyi.common.utils.spring.SpringUtils; import com.smartor.domain.ServiceSubtask; import com.smartor.domain.ServiceSubtaskPreachform; import com.smartor.domain.ServiceTaskSchedule; import com.smartor.domain.ServiceTaskScheduleDetail; import com.smartor.domain.ServiceTaskScheduleVO; import com.smartor.mapper.ServiceSubtaskMapper; import com.smartor.mapper.ServiceSubtaskPreachformMapper; import com.smartor.mapper.ServiceTaskScheduleDetailMapper; import com.smartor.mapper.ServiceTaskScheduleMapper; import com.smartor.service.IServiceTaskScheduleService; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Calendar; import java.util.Date; import java.util.List; /** * 随访任务计划 Service 实现 * * @author smartor * @date 2026-06-30 */ @Slf4j @Service public class ServiceTaskScheduleServiceImpl implements IServiceTaskScheduleService { @Autowired private ServiceTaskScheduleMapper serviceTaskScheduleMapper; @Autowired private ServiceTaskScheduleDetailMapper serviceTaskScheduleDetailMapper; @Autowired private ServiceSubtaskMapper serviceSubtaskMapper; @Autowired private ServiceSubtaskPreachformMapper serviceSubtaskPreachformMapper; /** 单次扫描的最大计划数,避免长事务。 */ private static final int ROLL_BATCH_LIMIT = 200; @Override @Transactional(rollbackFor = Exception.class) public ServiceTaskSchedule saveScheduleWithDetail(ServiceTaskScheduleVO vo) { if (vo == null || vo.getTaskid() == null) { throw new BaseException("任务计划入参非法:taskid 不能为空"); } if (CollectionUtils.isEmpty(vo.getDetailList())) { throw new BaseException("任务计划明细不能为空(至少一条 detail)"); } // 1. 旧计划软删 serviceTaskScheduleMapper.deleteByTaskid(vo.getTaskid()); serviceTaskScheduleDetailMapper.deleteByTaskid(vo.getTaskid()); // 2. 主计划落库 Date now = DateUtils.getNowDate(); ServiceTaskSchedule schedule = new ServiceTaskSchedule(); // 拷贝父类(ServiceTaskSchedule)字段 schedule.setTaskid(vo.getTaskid()); schedule.setMaxLoopCount(vo.getMaxLoopCount()); schedule.setCurrentLoopCount(vo.getCurrentLoopCount() == null ? 0L : vo.getCurrentLoopCount()); schedule.setCurrentSeq(vo.getCurrentSeq() == null ? 0 : vo.getCurrentSeq()); schedule.setNextSeq(vo.getNextSeq() == null ? 1 : vo.getNextSeq()); schedule.setScheduleStyle(vo.getScheduleStyle() == null ? 0 : vo.getScheduleStyle()); schedule.setScheduleTimeNum(vo.getScheduleTimeNum() == null ? 0 : vo.getScheduleTimeNum()); schedule.setScheduleTimeUnit(vo.getScheduleTimeUnit() == null ? 0 : vo.getScheduleTimeUnit()); schedule.setNextExecuteTime(vo.getNextExecuteTime()); schedule.setScheduleBenchmark(vo.getScheduleBenchmark() == null ? 0 : vo.getScheduleBenchmark()); schedule.setScheduleBenchmarkCustomTime(vo.getScheduleBenchmarkCustomTime()); schedule.setRemark(vo.getRemark()); schedule.setDelFlag("0"); schedule.setCreateBy(vo.getCreateBy()); schedule.setUpdateBy(vo.getCreateBy()); schedule.setCreateTime(now); schedule.setUpdateTime(now); schedule.setOrgid(vo.getOrgid()); schedule.setCampusid(vo.getCampusid()); serviceTaskScheduleMapper.insertServiceTaskSchedule(schedule); // 3. 明细落库 List detailList = vo.getDetailList(); int seq = 1; for (ServiceTaskScheduleDetail detail : detailList) { detail.setTaskid(vo.getTaskid()); detail.setScheduleid(schedule.getId()); if (detail.getSeq() == null) { detail.setSeq(seq); } if (detail.getDayOffset() == null) { throw new BaseException("计划明细 seq=" + detail.getSeq() + " 的 dayOffset 不能为空"); } detail.setDelFlag("0"); detail.setCreateBy(vo.getCreateBy()); detail.setUpdateBy(vo.getCreateBy()); detail.setCreateTime(now); detail.setUpdateTime(now); detail.setOrgid(vo.getOrgid()); detail.setCampusid(vo.getCampusid()); seq++; } serviceTaskScheduleDetailMapper.batchInsertServiceTaskScheduleDetail(detailList); log.info("[任务组] taskid={} 计划保存完成, scheduleId={}, detail 条数={}", vo.getTaskid(), schedule.getId(), detailList.size()); return schedule; } @Override public ServiceTaskScheduleVO getScheduleByTaskid(Long taskid) { ServiceTaskSchedule schedule = serviceTaskScheduleMapper.selectByTaskid(taskid); if (schedule == null) { return null; } ServiceTaskScheduleVO vo = new ServiceTaskScheduleVO(); vo.setId(schedule.getId()); vo.setTaskid(schedule.getTaskid()); vo.setMaxLoopCount(schedule.getMaxLoopCount()); vo.setCurrentLoopCount(schedule.getCurrentLoopCount()); vo.setCurrentSeq(schedule.getCurrentSeq()); vo.setNextSeq(schedule.getNextSeq()); vo.setScheduleStyle(schedule.getScheduleStyle()); vo.setScheduleTimeNum(schedule.getScheduleTimeNum()); vo.setScheduleTimeUnit(schedule.getScheduleTimeUnit()); vo.setNextExecuteTime(schedule.getNextExecuteTime()); vo.setScheduleBenchmark(schedule.getScheduleBenchmark()); vo.setScheduleBenchmarkCustomTime(schedule.getScheduleBenchmarkCustomTime()); vo.setRemark(schedule.getRemark()); vo.setOrgid(schedule.getOrgid()); vo.setCampusid(schedule.getCampusid()); vo.setDetailList(serviceTaskScheduleDetailMapper.selectByScheduleid(schedule.getId())); return vo; } @Override public ServiceTaskSchedule selectByTaskid(Long taskid) { return serviceTaskScheduleMapper.selectByTaskid(taskid); } @Override public List selectDetailByScheduleid(Long scheduleid) { return serviceTaskScheduleDetailMapper.selectByScheduleid(scheduleid); } @Override @Transactional(rollbackFor = Exception.class) public int removeByTaskid(Long taskid) { int n = serviceTaskScheduleMapper.deleteByTaskid(taskid); serviceTaskScheduleDetailMapper.deleteByTaskid(taskid); return n; } @Override public int rollCycleTasks() { List schedules = serviceTaskScheduleMapper.selectActiveCycleSchedules(ROLL_BATCH_LIMIT); if (CollectionUtils.isEmpty(schedules)) { return 0; } log.info("[任务组-滚动] 扫描到 {} 个待评估的循环计划", schedules.size()); int rolled = 0; for (ServiceTaskSchedule schedule : schedules) { try { // 通过 AOP 代理调用,确保 @Transactional 生效 if (SpringUtils.getAopProxy(this).rollOneSchedule(schedule)) { rolled++; } } catch (Exception e) { // 单条失败不影响其它计划,记日志继续 log.error("[任务组-滚动] scheduleId={} 滚动失败:{}", schedule.getId(), e.getMessage(), e); } } log.info("[任务组-滚动] 本次推进 {} 个计划", rolled); return rolled; } /** * 单个计划的滚动:本轮全部 finalize 才推进,每次仅推进 1 轮。 */ @Transactional(rollbackFor = Exception.class) public boolean rollOneSchedule(ServiceTaskSchedule schedule) { Long scheduleId = schedule.getId(); Long currentLoop = schedule.getCurrentLoopCount() == null ? 0L : schedule.getCurrentLoopCount(); // 上限校验 Long max = schedule.getMaxLoopCount(); if (max != null && max > 0 && currentLoop + 1 >= max) { // 注意:先判断"再开一轮是否会超限"——currentLoop 是已完成轮次,下一轮号 = currentLoop+1 // 当 currentLoop+1 == max 时正好处于最后一轮,不再产生新一轮 if (currentLoop >= max) { log.info("[任务组-滚动] scheduleId={} 已达 maxLoopCount={}, 跳过", scheduleId, max); return false; } } // 本轮是否完成 int active = serviceSubtaskMapper.countActiveByScheduleIdAndLoop(scheduleId, currentLoop); if (active > 0) { log.debug("[任务组-滚动] scheduleId={} loop={} 仍有 {} 条 subtask 未结束, 跳过", scheduleId, currentLoop, active); return false; } // 拉本轮 subtask 做克隆模板(取去重后的患者集合) List currentLoopSubtasks = serviceSubtaskMapper.selectByScheduleIdAndLoop(scheduleId, currentLoop); if (CollectionUtils.isEmpty(currentLoopSubtasks)) { log.warn("[任务组-滚动] scheduleId={} loop={} 找不到本轮 subtask, 跳过", scheduleId, currentLoop); return false; } // 拉本计划明细:seq -> detail,用来定位下一轮每条 subtask 用哪个模板 List detailList = serviceTaskScheduleDetailMapper.selectByScheduleid(scheduleId); if (CollectionUtils.isEmpty(detailList)) { log.warn("[任务组-滚动] scheduleId={} 明细为空, 跳过", scheduleId); return false; } // 推进步长(轮间隔) int stepNum = schedule.getScheduleTimeNum() == null ? 0 : schedule.getScheduleTimeNum(); int stepUnit = schedule.getScheduleTimeUnit() == null ? 1 : schedule.getScheduleTimeUnit(); if (stepNum <= 0) { log.warn("[任务组-滚动] scheduleId={} scheduleTimeNum={} 非法, 跳过", scheduleId, stepNum); return false; } long nextLoop = currentLoop + 1; // 按 patid+inhospid 去重(同患者本轮可能有 N 条 detail,模板任选其一即可——克隆时按 seq 复用明细) // 为简化:直接按 schedule_seq 复制 N 条,每条用对应 detail。 Date now = DateUtils.getNowDate(); // 把 detailList 按 seq 索引 java.util.Map detailBySeq = new java.util.HashMap<>(detailList.size()); for (ServiceTaskScheduleDetail d : detailList) { detailBySeq.put(d.getSeq(), d); } int produced = 0; for (ServiceSubtask src : currentLoopSubtasks) { ServiceTaskScheduleDetail detail = detailBySeq.get(src.getScheduleSeq()); if (detail == null) { log.warn("[任务组-滚动] scheduleId={} 找不到 seq={} 对应的明细, 跳过 subtask id={}", scheduleId, src.getScheduleSeq(), src.getId()); continue; } ServiceSubtask copy = cloneForNextLoop(src, detail, nextLoop, stepNum, stepUnit, now); serviceSubtaskMapper.insertServiceSubtask(copy); // 复制 preachform copyPreachform(src.getId(), copy); produced++; } // 推进 schedule 的 current_loop_count / next_seq / next_execute_time ServiceTaskSchedule upd = new ServiceTaskSchedule(); upd.setId(scheduleId); upd.setCurrentLoopCount(nextLoop); upd.setCurrentSeq(1); upd.setNextSeq(detailList.size() > 1 ? 2 : 1); upd.setNextExecuteTime(advance(now, stepNum, stepUnit)); upd.setUpdateTime(now); serviceTaskScheduleMapper.updateServiceTaskSchedule(upd); log.info("[任务组-滚动] scheduleId={} 推进到第 {} 轮, 新建 {} 条 subtask", scheduleId, nextLoop, produced); return true; } /** * 克隆 subtask 到下一轮:清主键、清回签字段、推进 visit_time、设置 loop_count。 */ private ServiceSubtask cloneForNextLoop(ServiceSubtask src, ServiceTaskScheduleDetail detail, long nextLoop, int stepNum, int stepUnit, Date now) { ServiceSubtask copy = JSON.parseObject(JSON.toJSONString(src), ServiceSubtask.class); copy.setId(null); copy.setSenduuid(null); copy.setResult(null); copy.setFinishtime(null); copy.setExrecallcount(null); copy.setCurrentPreachform(null); copy.setSendstate(2L); copy.setLoopCount(nextLoop); copy.setScheduleDetailId(detail.getId()); // 模板:detail 优先 if (detail.getTemplateid() != null) { copy.setTemplateid(detail.getTemplateid()); copy.setTemplatename(detail.getTemplatename()); } if (detail.getLibtemplateid() != null) { copy.setLibtemplateid(detail.getLibtemplateid()); } Date base = src.getVisitTime() != null ? src.getVisitTime() : now; Date nextVisit = advance(base, stepNum, stepUnit); copy.setVisitTime(nextVisit); copy.setSenddate(nextVisit); copy.setLongSendTime(nextVisit); copy.setCreateTime(now); copy.setUpdateTime(now); return copy; } /** * 复制源 subtask 的发送方式行到新 subtask。 */ private void copyPreachform(Long srcSubId, ServiceSubtask newSubtask) { ServiceSubtaskPreachform q = new ServiceSubtaskPreachform(); q.setSubid(srcSubId); q.setTaskid(newSubtask.getTaskid()); List list = serviceSubtaskPreachformMapper.selectServiceSubtaskPreachformList(q); if (CollectionUtils.isEmpty(list)) { return; } for (ServiceSubtaskPreachform src : list) { ServiceSubtaskPreachform pf = new ServiceSubtaskPreachform(); pf.setSort(src.getSort()); pf.setPreachform(src.getPreachform()); pf.setCompensateTime(src.getCompensateTime()); pf.setTaskid(newSubtask.getTaskid()); pf.setSubid(newSubtask.getId()); pf.setSendstate("1"); pf.setOrgid(newSubtask.getOrgid()); pf.setCreateTime(new Date()); serviceSubtaskPreachformMapper.insertServiceSubtaskPreachform(pf); } } /** * 按 schedule_time_unit 推进时间:1-天 2-周 3-月 4-年 */ private Date advance(Date base, int num, int unit) { Calendar c = Calendar.getInstance(); c.setTime(base); switch (unit) { case 2: c.add(Calendar.WEEK_OF_YEAR, num); break; case 3: c.add(Calendar.MONTH, num); break; case 4: c.add(Calendar.YEAR, num); break; case 1: default: c.add(Calendar.DAY_OF_MONTH, num); } return c.getTime(); } }