package com.ruoyi.inhospvisit.service; import com.ruoyi.common.core.service.ScheduleRollPort; import com.ruoyi.common.core.service.dto.VisitScheduleRollResultDto; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; /** * 任务组「轮次滚动」编排器 —— 逐行搬自 * {@code ServiceTaskScheduleServiceImpl#rollCycleTasks}(L170-190,21 行)。 * *

为什么这里只有"编排"(第 9 步的设计)

* 真正滚动单个计划要做一次 {@code ServiceSubtask} 的 116 字段全字段深拷贝, * 那部分刻意留在实现侧(smartor 的 {@code ScheduleRollPortBridge}), * 不跨越模块边界传 116 字段的 DTO —— 理由见 {@link ScheduleRollPort} 的类注释。 *

* 本类负责的部分(与原文逐行对应): *

    *
  1. 批次上限 {@code ROLL_BATCH_LIMIT = 200}(原 L50 / L171);
  2. *
  3. 扫描日志 {@code [任务组-滚动] 扫描到 {} 个待评估的循环计划}(原 L175);
  4. *
  5. 逐条 {@code try/catch},单条失败不影响其它计划(原 L178-186);
  6. *
  7. 计数与收尾日志 {@code 本次推进 {} 个计划}(原 L188)。
  8. *
* *

⚠️ 与原实现的两处已知差异(都已登记)

*
    *
  1. 事务边界:原实现在 {@code rollOneSchedule} 上标 {@code @Transactional} * (方法在同一个类里,通过 {@code SpringUtils.getAopProxy(this)} 自调以让注解生效,见原 L180)。 * 搬迁后 {@code @Transactional} 落在桥实现 {@code ScheduleRollPortBridge#rollOneSchedule} 上 * —— 跨 Bean 调用,事务照常生效,所以不再需要 AOP 自调那一步。 * 这是等价改写,不是行为变更。
  2. *
  3. 计划对象是重查的:原实现把"查询结果里的计划对象"直接传给 {@code rollOneSchedule}; * 端口只给 id,所以桥里按 id 重查一次。差异只在"两次读取之间计划是否被改过"这个毫秒级窗口, * 且 roll 内部仍会做"本轮是否结束"的校验。已在桥的注释里说明。
  4. *
* *

⚠️ 过渡期禁止双跑

* 本类在第 10 步(把 {@code RyTask.rollTaskGroupCycle} 的方法体切过来)之前, * 不能被挂到任何调度上:老的 {@code rollCycleTasks} 还在跑, * 两边都会"推进轮次 + 新建 subtask",双跑会造成重复轮次。 * 目前本类只被自检程序调用。 */ @Slf4j @Service public class TaskGroupCycleRoller { /** 单次扫描的最大计划数,避免长事务(原 {@code ServiceTaskScheduleServiceImpl} L50)。 */ private static final int ROLL_BATCH_LIMIT = 200; @Autowired private ObjectProvider rollPortProvider; /** * 扫描循环任务组计划并推进已完成的轮次。 *

* 逐行对应原 {@code rollCycleTasks()}(L170-190)。 * * @return 本次推进的计划数 */ public int rollCycleTasks() { ScheduleRollPort port = rollPort(); if (port == null) { log.warn("【出入院随访】ScheduleRollPort 不可用(任务组端口未装配),本次不推进任何轮次。"); return 0; } // 原 L171-174 List scheduleIds = port.selectActiveCycleSchedules(ROLL_BATCH_LIMIT); if (CollectionUtils.isEmpty(scheduleIds)) { return 0; } // 原 L175 log.info("[任务组-滚动] 扫描到 {} 个待评估的循环计划", scheduleIds.size()); int rolled = 0; for (Long scheduleId : scheduleIds) { try { // 原 L180:if (SpringUtils.getAopProxy(this).rollOneSchedule(schedule)) rolled++; // 搬迁后 @Transactional 在桥实现上,跨 Bean 调用即生效,无需 AOP 自调。 VisitScheduleRollResultDto result = port.rollOneSchedule(scheduleId); if (result != null && result.isRolled()) { rolled++; } } catch (Exception e) { // 原 L183-186:单条失败不影响其它计划,记日志继续 log.error("[任务组-滚动] scheduleId={} 滚动失败:{}", scheduleId, e.getMessage(), e); } } // 原 L188-189 log.info("[任务组-滚动] 本次推进 {} 个计划", rolled); return rolled; } private ScheduleRollPort rollPort() { if (rollPortProvider == null) { return null; } try { return rollPortProvider.stream().findFirst().orElse(null); } catch (Exception e) { log.warn("【出入院随访】获取 ScheduleRollPort 失败:{}", e.toString()); return null; } } }