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 侧的桥接。 * *

为什么这个实现是"粗粒度"的(第 9 步的设计决定)

* 核心动作要做一次 {@code ServiceSubtask} 的全字段深拷贝 * ({@code JSON.parseObject(JSON.toJSONString(src), ServiceSubtask.class)}), * 而 {@code ServiceSubtask} 有 116 个字段。 * 在模块之间传 116 字段的 DTO 只会制造风险、换不来收益 * (全字段拷贝的语义本来就由 JSON 序列化保证)。 *

* 所以整个 roll 动作留在本类里原子完成;{@code ruoyi-inhospvisit} 只负责 * "批次上限 + 逐条 try/catch + 计数与日志"这部分编排。 * *

逐方法来源(@ b25c3b51)

* * * * * * * * * *
本类方法原实现行数
{@link #selectActiveCycleSchedules(int)}{@code ServiceTaskScheduleServiceImpl#rollCycleTasks} L171 的查询部分—
{@link #rollOneSchedule(Long)}{@code rollOneSchedule} L196-28085
{@code cloneForNextLoop}L285-31329
{@code copyPreachform}L318-33821
{@code advance}L343-36119
* *

⚠️ 本类与 smartor 里那份 {@code rollCycleTasks} 是并存的

* {@code ServiceTaskScheduleServiceImpl#rollCycleTasks}(L170-190)本次刻意不动 —— * 它仍被 {@code RyTask.rollTaskGroupCycle} 调用,而那是现网 {@code sys_job} 的入口。 * 两条路径不能同时被调度(都会推进轮次 + 新建 subtask → 重复轮次)。 * 第 10 步会把 {@code RyTask} 的方法体切到本端口,届时那份才退役。 * *

⚠️ 深拷贝必须原样用 JSON 序列化

* 不要把 {@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)。 *

* 只回传 id 列表:真正的 roll 动作在 {@link #rollOneSchedule(Long)} 里重新加载完整计划 * (原实现也是把整个 {@code ServiceTaskSchedule} 传进 rollOneSchedule, * 所以语义一致)。 */ @Override public List selectActiveCycleSchedules(int limit) { List schedules = serviceTaskScheduleMapper.selectActiveCycleSchedules(limit); if (CollectionUtils.isEmpty(schedules)) { return new ArrayList<>(); } List ids = new ArrayList<>(schedules.size()); for (ServiceTaskSchedule s : schedules) { ids.add(s.getId()); } return ids; } /** * 滚动单个计划 —— 逐行搬自原 {@code rollOneSchedule}(L196-280)。 *

* ⚠️ 原实现接收的是整个 {@code ServiceTaskSchedule} 对象(由 {@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 加载计划。 *

* ⚠️ 这里与"原实现"有一个必须说明的差异: * 原 {@code rollCycleTasks} 是把查询结果里的对象直接传进来(不重查); * 端口只给 id,所以必须重查。重查与"用查询结果"的差别只在于 * 两次读取之间计划是否被改过——同一批任务里这是毫秒级窗口, * 且 rollOneSchedule 本身会做乐观的"本轮是否结束"校验,所以不改变可观察行为。 *

* 实现上用 {@code selectServiceTaskScheduleList} 按 id 过滤(该 Mapper 已有此能力)。 */ private ServiceTaskSchedule loadSchedule(Long scheduleId) { ServiceTaskSchedule q = new ServiceTaskSchedule(); q.setId(scheduleId); List 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 currentLoopSubtasks = serviceSubtaskMapper.selectByScheduleIdAndLoop(scheduleId, currentLoop); if (CollectionUtils.isEmpty(currentLoopSubtasks)) { log.warn("[任务组-滚动] scheduleId={} loop={} 找不到本轮 subtask, 跳过", scheduleId, currentLoop); return VisitScheduleRollResultDto.skipped(scheduleId, "找不到本轮 subtask"); } // 原 L226-232:拉本计划明细 List 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 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。 *

* ⚠️ 第一行的全字段深拷贝(116 个字段)必须原样用 JSON 序列化,不要改写成逐字段 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。 *

* ⚠️ 查源行的条件是 {@code subid + taskid}(不设 orgid,虽然 XML 支持),原样保留。 */ 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-年 —— 逐行搬自原 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(); } }