陈昶聿
6 天以前 839e23b23b2bfb13282e9e5f621d32a1c55186ea
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
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();
    }
}