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} 的类注释。
*
* 本类负责的部分(与原文逐行对应):
*
* - 批次上限 {@code ROLL_BATCH_LIMIT = 200}(原 L50 / L171);
* - 扫描日志 {@code [任务组-滚动] 扫描到 {} 个待评估的循环计划}(原 L175);
* - 逐条 {@code try/catch},单条失败不影响其它计划(原 L178-186);
* - 计数与收尾日志 {@code 本次推进 {} 个计划}(原 L188)。
*
*
* ⚠️ 与原实现的两处已知差异(都已登记)
*
* - 事务边界:原实现在 {@code rollOneSchedule} 上标 {@code @Transactional}
* (方法在同一个类里,通过 {@code SpringUtils.getAopProxy(this)} 自调以让注解生效,见原 L180)。
* 搬迁后 {@code @Transactional} 落在桥实现 {@code ScheduleRollPortBridge#rollOneSchedule} 上
* —— 跨 Bean 调用,事务照常生效,所以不再需要 AOP 自调那一步。
* 这是等价改写,不是行为变更。
* - 计划对象是重查的:原实现把"查询结果里的计划对象"直接传给 {@code rollOneSchedule};
* 端口只给 id,所以桥里按 id 重查一次。差异只在"两次读取之间计划是否被改过"这个毫秒级窗口,
* 且 roll 内部仍会做"本轮是否结束"的校验。已在桥的注释里说明。
*
*
* ⚠️ 过渡期禁止双跑
* 本类在第 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;
}
}
}