陈昶聿
9 天以前 af6c73841b54f0c9002ce85b8c0d1679e25d5d7c
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
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
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();
    }
}