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-280 | 85 |
* | {@code cloneForNextLoop} | L285-313 | 29 |
* | {@code copyPreachform} | L318-338 | 21 |
* | {@code advance} | L343-361 | 19 |
*
*
* ⚠️ 本类与 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();
}
}