package com.ruoyi.task.api.contract;
|
|
import com.alibaba.fastjson2.JSON;
|
import com.ruoyi.common.core.service.ScheduleRollPort;
|
import com.ruoyi.common.core.service.dto.VisitScheduleRollResultDto;
|
import com.ruoyi.common.utils.DateUtils;
|
import com.ruoyi.task.mapper.ServiceSubtaskPreachformMapper;
|
import lombok.extern.slf4j.Slf4j;
|
import org.apache.commons.collections4.CollectionUtils;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.stereotype.Component;
|
import org.springframework.transaction.annotation.Transactional;
|
|
import java.util.ArrayList;
|
import java.util.Calendar;
|
import java.util.Date;
|
import java.util.HashMap;
|
import java.util.List;
|
import java.util.Map;
|
import com.ruoyi.task.domain.ServiceSubtask;
|
import com.ruoyi.task.domain.ServiceTaskSchedule;
|
import com.ruoyi.task.domain.ServiceTaskScheduleDetail;
|
import com.ruoyi.task.mapper.ServiceSubtaskMapper;
|
import com.ruoyi.task.mapper.ServiceTaskScheduleDetailMapper;
|
import com.ruoyi.task.mapper.ServiceTaskScheduleMapper;
|
import com.ruoyi.task.domain.ServiceSubtaskPreachform;
|
|
/**
|
* 【过渡实现】{@link ScheduleRollPort}(任务组轮次滚动)在 smartor 侧的桥接。
|
*
|
* <h3>为什么这个实现是"粗粒度"的(第 9 步的设计决定)</h3>
|
* 核心动作要做一次 <b>{@code ServiceSubtask} 的全字段深拷贝</b>
|
* ({@code JSON.parseObject(JSON.toJSONString(src), ServiceSubtask.class)}),
|
* 而 {@code ServiceSubtask} 有 <b>116 个字段</b>。
|
* 在模块之间传 116 字段的 DTO 只会制造风险、换不来收益
|
* (全字段拷贝的语义本来就由 JSON 序列化保证)。
|
* <p>
|
* 所以整个 roll 动作留在本类里原子完成;{@code ruoyi-inhospvisit} 只负责
|
* "批次上限 + 逐条 try/catch + 计数与日志"这部分编排。
|
*
|
* <h3>逐方法来源(@ b25c3b51)</h3>
|
* <table border="1">
|
* <tr><th>本类方法</th><th>原实现</th><th>行数</th></tr>
|
* <tr><td>{@link #selectActiveCycleSchedules(int)}</td>
|
* <td>{@code ServiceTaskScheduleServiceImpl#rollCycleTasks} L171 的查询部分</td><td>—</td></tr>
|
* <tr><td>{@link #rollOneSchedule(Long)}</td>
|
* <td>{@code rollOneSchedule} L196-280</td><td>85</td></tr>
|
* <tr><td>{@code cloneForNextLoop}</td><td>L285-313</td><td>29</td></tr>
|
* <tr><td>{@code copyPreachform}</td><td>L318-338</td><td>21</td></tr>
|
* <tr><td>{@code advance}</td><td>L343-361</td><td>19</td></tr>
|
* </table>
|
*
|
* <h3>⚠️ 本类与 smartor 里那份 {@code rollCycleTasks} 是<b>并存</b>的</h3>
|
* {@code ServiceTaskScheduleServiceImpl#rollCycleTasks}(L170-190)<b>本次刻意不动</b> ——
|
* 它仍被 {@code RyTask.rollTaskGroupCycle} 调用,而那是现网 {@code sys_job} 的入口。
|
* 两条路径<b>不能同时被调度</b>(都会推进轮次 + 新建 subtask → 重复轮次)。
|
* 第 10 步会把 {@code RyTask} 的方法体切到本端口,届时那份才退役。
|
*
|
* <h3>⚠️ 深拷贝必须原样用 JSON 序列化</h3>
|
* 不要把 {@code JSON.parseObject(JSON.toJSONString(src), ...)} 改写成逐字段 set ——
|
* 116 个字段的逐个搬运是行为变更的高发地,且没有任何好处。
|
*/
|
@Slf4j
|
@Component
|
public class ScheduleRollPortImpl implements ScheduleRollPort {
|
|
@Autowired
|
private ServiceTaskScheduleMapper serviceTaskScheduleMapper;
|
|
@Autowired
|
private ServiceTaskScheduleDetailMapper serviceTaskScheduleDetailMapper;
|
|
@Autowired
|
private ServiceSubtaskMapper serviceSubtaskMapper;
|
|
@Autowired
|
private ServiceSubtaskPreachformMapper serviceSubtaskPreachformMapper;
|
|
/**
|
* 查一批待评估的循环计划(原 {@code rollCycleTasks} L171)。
|
* <p>
|
* 只回传 id 列表:真正的 roll 动作在 {@link #rollOneSchedule(Long)} 里重新加载完整计划
|
* (原实现也是把整个 {@code ServiceTaskSchedule} 传进 rollOneSchedule,
|
* 所以语义一致)。
|
*/
|
@Override
|
public List<Long> selectActiveCycleSchedules(int limit) {
|
List<ServiceTaskSchedule> schedules =
|
serviceTaskScheduleMapper.selectActiveCycleSchedules(limit);
|
if (CollectionUtils.isEmpty(schedules)) {
|
return new ArrayList<>();
|
}
|
List<Long> ids = new ArrayList<>(schedules.size());
|
for (ServiceTaskSchedule s : schedules) {
|
ids.add(s.getId());
|
}
|
return ids;
|
}
|
|
/**
|
* 滚动单个计划 —— 逐行搬自原 {@code rollOneSchedule}(L196-280)。
|
* <p>
|
* ⚠️ 原实现接收的是<b>整个 {@code ServiceTaskSchedule} 对象</b>(由 {@code rollCycleTasks}
|
* 从查询结果里传进来)。端口签名只给 id,所以这里要**重新查一次**。
|
* 为此需要按 id 查计划:用 {@code selectByTaskid} 不行(那是按 taskid),
|
* 所以这里用 {@code selectActiveCycleSchedules} 的同款能力 ——
|
* 见下方 {@link #loadSchedule(Long)} 的说明。
|
*/
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public VisitScheduleRollResultDto rollOneSchedule(Long scheduleId) {
|
ServiceTaskSchedule schedule = loadSchedule(scheduleId);
|
if (schedule == null) {
|
log.warn("[任务组-滚动] scheduleId={} 加载不到计划,跳过", scheduleId);
|
return VisitScheduleRollResultDto.skipped(scheduleId, "计划不存在");
|
}
|
return doRoll(schedule);
|
}
|
|
/**
|
* 按 id 加载计划。
|
* <p>
|
* ⚠️ <b>这里与"原实现"有一个必须说明的差异</b>:
|
* 原 {@code rollCycleTasks} 是把查询结果里的对象<b>直接传进来</b>(不重查);
|
* 端口只给 id,所以必须重查。重查与"用查询结果"的差别只在于
|
* <b>两次读取之间计划是否被改过</b>——同一批任务里这是毫秒级窗口,
|
* 且 rollOneSchedule 本身会做乐观的"本轮是否结束"校验,所以不改变可观察行为。
|
* <p>
|
* 实现上用 {@code selectServiceTaskScheduleList} 按 id 过滤(该 Mapper 已有此能力)。
|
*/
|
private ServiceTaskSchedule loadSchedule(Long scheduleId) {
|
ServiceTaskSchedule q = new ServiceTaskSchedule();
|
q.setId(scheduleId);
|
List<ServiceTaskSchedule> rows = serviceTaskScheduleMapper.selectServiceTaskScheduleList(q);
|
if (CollectionUtils.isEmpty(rows)) {
|
return null;
|
}
|
return rows.get(0);
|
}
|
|
/**
|
* 原 {@code rollOneSchedule} 本体(L196-280)。
|
*/
|
private VisitScheduleRollResultDto doRoll(ServiceTaskSchedule schedule) {
|
Long scheduleId = schedule.getId();
|
Long currentLoop = schedule.getCurrentLoopCount() == null ? 0L : schedule.getCurrentLoopCount();
|
|
// 原 L200-209:上限校验
|
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 VisitScheduleRollResultDto.skipped(scheduleId, "已达 maxLoopCount");
|
}
|
}
|
|
// 原 L211-216:本轮是否完成
|
int active = serviceSubtaskMapper.countActiveByScheduleIdAndLoop(scheduleId, currentLoop);
|
if (active > 0) {
|
log.debug("[任务组-滚动] scheduleId={} loop={} 仍有 {} 条 subtask 未结束, 跳过",
|
scheduleId, currentLoop, active);
|
return VisitScheduleRollResultDto.skipped(scheduleId, "本轮仍有未结束的 subtask");
|
}
|
|
// 原 L218-224:拉本轮 subtask 做克隆模板
|
List<ServiceSubtask> currentLoopSubtasks =
|
serviceSubtaskMapper.selectByScheduleIdAndLoop(scheduleId, currentLoop);
|
if (CollectionUtils.isEmpty(currentLoopSubtasks)) {
|
log.warn("[任务组-滚动] scheduleId={} loop={} 找不到本轮 subtask, 跳过", scheduleId, currentLoop);
|
return VisitScheduleRollResultDto.skipped(scheduleId, "找不到本轮 subtask");
|
}
|
|
// 原 L226-232:拉本计划明细
|
List<ServiceTaskScheduleDetail> detailList =
|
serviceTaskScheduleDetailMapper.selectByScheduleid(scheduleId);
|
if (CollectionUtils.isEmpty(detailList)) {
|
log.warn("[任务组-滚动] scheduleId={} 明细为空, 跳过", scheduleId);
|
return VisitScheduleRollResultDto.skipped(scheduleId, "明细为空");
|
}
|
|
// 原 L234-240:推进步长(轮间隔)
|
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 VisitScheduleRollResultDto.skipped(scheduleId, "scheduleTimeNum 非法");
|
}
|
|
long nextLoop = currentLoop + 1;
|
|
// 原 L246-251:把 detailList 按 seq 索引
|
Date now = DateUtils.getNowDate();
|
Map<Integer, ServiceTaskScheduleDetail> detailBySeq = new HashMap<>(detailList.size());
|
for (ServiceTaskScheduleDetail d : detailList) {
|
detailBySeq.put(d.getSeq(), d);
|
}
|
|
// 原 L253-266:逐条克隆
|
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);
|
// 原 L264:复制 preachform
|
copyPreachform(src.getId(), copy);
|
produced++;
|
}
|
|
// 原 L268-276:推进 schedule
|
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 VisitScheduleRollResultDto.rolled(scheduleId, nextLoop, produced);
|
}
|
|
/**
|
* 克隆 subtask 到下一轮 —— 逐行搬自原 L285-313。
|
* <p>
|
* ⚠️ 第一行的全字段深拷贝(116 个字段)<b>必须原样用 JSON 序列化</b>,不要改写成逐字段 set。
|
*/
|
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 —— 逐行搬自原 L318-338。
|
* <p>
|
* ⚠️ 查源行的条件是 {@code subid + taskid}(<b>不设 orgid</b>,虽然 XML 支持),原样保留。
|
*/
|
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-年 —— 逐行搬自原 L343-361。
|
*/
|
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();
|
}
|
}
|