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<ServiceTaskScheduleDetail> 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<ServiceTaskScheduleDetail> 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<ServiceTaskSchedule> 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<ServiceSubtask> currentLoopSubtasks =
|
serviceSubtaskMapper.selectByScheduleIdAndLoop(scheduleId, currentLoop);
|
if (CollectionUtils.isEmpty(currentLoopSubtasks)) {
|
log.warn("[任务组-滚动] scheduleId={} loop={} 找不到本轮 subtask, 跳过", scheduleId, currentLoop);
|
return false;
|
}
|
|
// 拉本计划明细:seq -> detail,用来定位下一轮每条 subtask 用哪个模板
|
List<ServiceTaskScheduleDetail> 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<Integer, ServiceTaskScheduleDetail> 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<ServiceSubtaskPreachform> 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();
|
}
|
}
|