diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/EnrollmentPlanCompletionAdapter.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/EnrollmentPlanCompletionAdapter.java index 26f3680a5..7ffbf910e 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/EnrollmentPlanCompletionAdapter.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/EnrollmentPlanCompletionAdapter.java @@ -38,10 +38,82 @@ public class EnrollmentPlanCompletionAdapter implements PlanEnrollmentCompletion @Autowired(required = false) private AccessAuthorizationOrchestrator accessAuthorizationOrchestrator; + @Override + public void precheckCompletion(String planId) + { + List> stats = trainEnrollmentService.selectEnrollmentCompletionStatsByPlanId(planId); + int status0Waiting = 0; + int status1Reported = 0; + int status2Leave = 0; + int status3Abandoned = 0; + int status4Completed = 0; + int integrityErrorCount = 0; + + if (stats != null) + { + for (java.util.Map map : stats) + { + Object stObj = map.get("enrollmentStatus") != null ? map.get("enrollmentStatus") : map.get("enrollment_status"); + int st = stObj instanceof Number ? ((Number) stObj).intValue() : 0; + Object repObj = map.get("hasReport") != null ? map.get("hasReport") : map.get("has_report"); + int hasRep = repObj instanceof Number ? ((Number) repObj).intValue() : 0; + + if (st == 0) + { + status0Waiting++; + } + else if (st == 1) + { + status1Reported++; + if (hasRep == 0) + { + integrityErrorCount++; + } + } + else if (st == 2) + { + status2Leave++; + } + else if (st == 3) + { + status3Abandoned++; + } + else if (st == 4) + { + status4Completed++; + } + } + } + + if (status0Waiting > 0 || status2Leave > 0 || integrityErrorCount > 0) + { + StringBuilder msg = new StringBuilder("计划暂不能结训:"); + List reasons = new java.util.ArrayList<>(); + if (status0Waiting > 0) + { + reasons.add("待报到 " + status0Waiting + " 人 (WAITING_REPORT_ENROLLMENTS_EXIST)"); + } + if (status2Leave > 0) + { + reasons.add("请假 " + status2Leave + " 人 (LEAVE_ENROLLMENTS_REQUIRE_RESOLUTION)"); + } + if (integrityErrorCount > 0) + { + reasons.add("已报到缺失报到单异常 " + integrityErrorCount + " 人 (REPORTED_WITHOUT_REPORT_INTEGRITY_ERROR)"); + } + msg.append(String.join(",", reasons)).append("。请先处理后再结训。"); + log.warn("计划结训预检拦截: planId={}, 原因={}", planId, msg.toString()); + throw new com.ruoyi.common.exception.ServiceException(msg.toString()); + } + } + @Override @Transactional(propagation = Propagation.MANDATORY, rollbackFor = Exception.class) public void completeEnrollments(String planId, String operator, Date completeTime) { + // 结训预检 (All-or-Nothing 守卫) + precheckCompletion(planId); + log.info("执行计划结训学员状态联动归档: planId={}, operator={}, completeTime={}", planId, operator, completeTime); int updatedRows = trainEnrollmentService.completeActiveEnrollmentsByPlanId(planId, operator, completeTime); log.info("计划结训学员归档完成: planId={}, 更新记录数={}", planId, updatedRows); diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/PlanAppServiceImpl.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/PlanAppServiceImpl.java index 6ef4af0e0..d60236095 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/PlanAppServiceImpl.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/PlanAppServiceImpl.java @@ -158,6 +158,12 @@ public class PlanAppServiceImpl implements PlanAppService throw error(PlanErrorCode.PLAN_STATUS_LOCKED_MESSAGE, PlanErrorCode.PLAN_STATUS_LOCKED); } + // 结训预检 (All-or-Nothing 守卫:存在待报到、请假或报到数据异常学员时阻止结训) + for (PlanEnrollmentCompletionPort port : completionPorts) + { + port.precheckCompletion(planId); + } + Date completeTime = DateUtils.getNowDate(); TrainClassPlan statusUpdate = new TrainClassPlan(); statusUpdate.setPlanId(planId); diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/port/PlanEnrollmentCompletionPort.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/port/PlanEnrollmentCompletionPort.java index 5c81c4689..43788e48e 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/port/PlanEnrollmentCompletionPort.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/port/PlanEnrollmentCompletionPort.java @@ -8,5 +8,13 @@ import java.util.Date; */ public interface PlanEnrollmentCompletionPort { + /** + * 结训前预检参训人员状态完整性 (All-or-Nothing Precheck)。 + * 若存在 0:待报到、2:请假、或 1:已报到但无报到记录异常,抛出异常阻止结训。 + * + * @param planId 培训班ID + */ + default void precheckCompletion(String planId) {} + void completeEnrollments(String planId, String operator, Date completeTime); }