diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/mapper/TrainEnrollmentMapper.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/mapper/TrainEnrollmentMapper.java index a6255eded..0293a8df5 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/mapper/TrainEnrollmentMapper.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/mapper/TrainEnrollmentMapper.java @@ -124,6 +124,14 @@ public interface TrainEnrollmentMapper @Param("operator") String operator, @Param("completeTime") Date completeTime); + /** + * 查询指定计划学员的结训预检统计事实(参训状态与报到单存在性)。 + * + * @param planId 培训班ID + * @return 参训学员状态事实列表 + */ + java.util.List> selectEnrollmentCompletionStatsByPlanId(@Param("planId") String planId); + /** * 物理删除参训记录。 * diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/ITrainEnrollmentService.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/ITrainEnrollmentService.java index 6b4b1f509..24ae0f276 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/ITrainEnrollmentService.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/ITrainEnrollmentService.java @@ -44,4 +44,5 @@ public interface ITrainEnrollmentService int countAccommodationByEnrollmentId(Long enrollmentId); int countStudyDurationByEnrollmentId(Long enrollmentId); + List> selectEnrollmentCompletionStatsByPlanId(String planId); } diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/EnrollmentAppServiceImpl.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/EnrollmentAppServiceImpl.java index f03ec26e5..e05cd1f75 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/EnrollmentAppServiceImpl.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/EnrollmentAppServiceImpl.java @@ -371,17 +371,121 @@ public class EnrollmentAppServiceImpl implements EnrollmentAppService throw new ServiceException(EnrollmentErrorCode.ENROLLMENT_STATUS_INVALID_MESSAGE); } + // 普通状态接口严格禁止直接设置为已完成 (4),结训必须走开班计划结训流程 + if (Integer.valueOf(EnrollmentErrorCode.STATUS_COMPLETED).equals(newStatus)) + { + throw new ServiceException("普通状态接口禁止直接设置为已完成状态,结训必须走开班计划结训流程"); + } + TrainEnrollment enrollment = requireEnrollment(enrollmentId); // 计划已结训或取消时,禁止状态变更 TrainClassPlan plan = requirePlan(enrollment.getPlanId()); checkPlanStatusNotLocked(plan); - // 如果已经结训(4),禁止任意更改状态 - if (Integer.valueOf(EnrollmentErrorCode.STATUS_COMPLETED).equals(enrollment.getEnrollmentStatus())) + Integer currentStatus = enrollment.getEnrollmentStatus() != null ? enrollment.getEnrollmentStatus() : EnrollmentErrorCode.STATUS_WAITING_REPORT; + + // 如果已经结训(4),终态,任何 4 -> X 全部禁止 + if (Integer.valueOf(EnrollmentErrorCode.STATUS_COMPLETED).equals(currentStatus)) { throw new ServiceException("该学员已完成结训归档,不可修改参训状态"); } + if (currentStatus.equals(newStatus)) + { + return; + } + + // 查询是否存在报到确认单事实 + boolean reportExists = trainEnrollmentService.countReportByEnrollmentId(enrollmentId) > 0; + + // 状态迁移规则守卫 (V1 Invariant Matrix) + if (Integer.valueOf(EnrollmentErrorCode.STATUS_WAITING_REPORT).equals(currentStatus)) // 0: 待报到 + { + if (Integer.valueOf(EnrollmentErrorCode.STATUS_REPORTED).equals(newStatus)) + { + // INVARIANT R1: 禁止普通状态接口 0 -> 1,首次进入 1 必须通过 confirmReport + throw new ServiceException("待报到学员禁止通过普通接口直接变更为已报到,必须通过报到确认流程进入"); + } + if (!Integer.valueOf(EnrollmentErrorCode.STATUS_LEAVE).equals(newStatus) && + !Integer.valueOf(EnrollmentErrorCode.STATUS_ABANDONED).equals(newStatus)) + { + throw new ServiceException("待报到学员仅允许变更为请假或退训状态"); + } + } + else if (Integer.valueOf(EnrollmentErrorCode.STATUS_REPORTED).equals(currentStatus)) // 1: 已报到 + { + if (Integer.valueOf(EnrollmentErrorCode.STATUS_WAITING_REPORT).equals(newStatus)) + { + // INVARIANT R4: 已有报到单,不得通过普通状态修改形成 status=0 + throw new ServiceException("已报到学员已存在报到事实,禁止变更为待报到状态"); + } + if (!Integer.valueOf(EnrollmentErrorCode.STATUS_LEAVE).equals(newStatus) && + !Integer.valueOf(EnrollmentErrorCode.STATUS_ABANDONED).equals(newStatus)) + { + throw new ServiceException("已报到学员仅允许变更为请假或退训状态"); + } + } + else if (Integer.valueOf(EnrollmentErrorCode.STATUS_LEAVE).equals(currentStatus)) // 2: 请假 + { + if (!reportExists) + { + // 报到前请假:允许 2 -> 0 (恢复为待报到);禁止 2 -> 1 (必须先回到 0 后走 confirmReport);允许 2 -> 3 + if (Integer.valueOf(EnrollmentErrorCode.STATUS_REPORTED).equals(newStatus)) + { + throw new ServiceException("未报到学员请假后禁止直接变更为已报到,必须先恢复为待报到并执行报到确认"); + } + if (!Integer.valueOf(EnrollmentErrorCode.STATUS_WAITING_REPORT).equals(newStatus) && + !Integer.valueOf(EnrollmentErrorCode.STATUS_ABANDONED).equals(newStatus)) + { + throw new ServiceException("未报到请假学员仅允许恢复为待报到或变更为退训"); + } + } + else + { + // 报到后请假:允许 2 -> 1 (归队);禁止 2 -> 0 (已有报到事实);允许 2 -> 3 + if (Integer.valueOf(EnrollmentErrorCode.STATUS_WAITING_REPORT).equals(newStatus)) + { + throw new ServiceException("已报到学员请假后存在报到事实,禁止恢复为待报到状态"); + } + if (!Integer.valueOf(EnrollmentErrorCode.STATUS_REPORTED).equals(newStatus) && + !Integer.valueOf(EnrollmentErrorCode.STATUS_ABANDONED).equals(newStatus)) + { + throw new ServiceException("已报到请假学员仅允许销假归队为已报到或变更为退训"); + } + } + } + else if (Integer.valueOf(EnrollmentErrorCode.STATUS_ABANDONED).equals(currentStatus)) // 3: 放弃/退训 + { + if (!reportExists) + { + // 报到前放弃:允许 3 -> 0;禁止 3 -> 1 + if (Integer.valueOf(EnrollmentErrorCode.STATUS_REPORTED).equals(newStatus)) + { + throw new ServiceException("未报到退训学员禁止直接恢复为已报到,必须先恢复为待报到"); + } + if (!Integer.valueOf(EnrollmentErrorCode.STATUS_WAITING_REPORT).equals(newStatus)) + { + throw new ServiceException("未报到退训学员仅允许恢复为待报到状态"); + } + } + else + { + // 报到后放弃:允许 3 -> 1;禁止 3 -> 0 + if (Integer.valueOf(EnrollmentErrorCode.STATUS_WAITING_REPORT).equals(newStatus)) + { + throw new ServiceException("已报到退训学员存在既有报到事实,禁止恢复为待报到状态"); + } + if (!Integer.valueOf(EnrollmentErrorCode.STATUS_REPORTED).equals(newStatus)) + { + throw new ServiceException("已报到退训学员仅允许受控恢复为已报到状态"); + } + } + } + else + { + throw new ServiceException("当前参训状态不支持此状态流转"); + } + trainEnrollmentService.updateEnrollmentStatus( enrollmentId, newStatus, @@ -621,14 +725,6 @@ public class EnrollmentAppServiceImpl implements EnrollmentAppService throw new ServiceException(EnrollmentErrorCode.ENROLLMENT_NOT_FOUND_MESSAGE, EnrollmentErrorCode.ENROLLMENT_NOT_FOUND); } - // 状态流转保护:退训(3)或已完成(4)的历史状态禁止报到 - if (enrollment.getEnrollmentStatus() != null && - (enrollment.getEnrollmentStatus() == EnrollmentErrorCode.STATUS_ABANDONED || - enrollment.getEnrollmentStatus() == EnrollmentErrorCode.STATUS_COMPLETED)) - { - throw new ServiceException("当前参训状态不允许执行报到确认", EnrollmentErrorCode.ENROLLMENT_NOT_FOUND); - } - // 幂等与查重:若已报到(status==1)或已有报到记录 TrainClassReport existingReport = trainClassReportMapper.selectTrainClassReportByEnrollmentId(enrollmentId); if (existingReport != null || (enrollment.getEnrollmentStatus() != null && enrollment.getEnrollmentStatus() == EnrollmentErrorCode.STATUS_REPORTED)) @@ -636,6 +732,12 @@ public class EnrollmentAppServiceImpl implements EnrollmentAppService throw new ServiceException(AttendanceErrorCode.REPORT_ALREADY_CONFIRMED_MESSAGE, AttendanceErrorCode.REPORT_ALREADY_CONFIRMED); } + // 状态流转保护:只有待报到(0)状态允许执行报到确认 (首次进入 1 必须通过 confirmReport) + if (enrollment.getEnrollmentStatus() == null || !Integer.valueOf(EnrollmentErrorCode.STATUS_WAITING_REPORT).equals(enrollment.getEnrollmentStatus())) + { + throw new ServiceException("当前参训状态不允许执行报到确认", EnrollmentErrorCode.ENROLLMENT_NOT_FOUND); + } + // 校验计划报到截止时间生命周期策略 (Report Lifecycle Policy) TrainClassPlan plan = planMapper.selectTrainClassPlanByPlanId(enrollment.getPlanId()); Date now = new Date(); diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainEnrollmentServiceImpl.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainEnrollmentServiceImpl.java index 22dec7fdc..60254e586 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainEnrollmentServiceImpl.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainEnrollmentServiceImpl.java @@ -49,6 +49,11 @@ public class TrainEnrollmentServiceImpl implements ITrainEnrollmentService @Override public int insertTrainEnrollment(TrainEnrollment trainEnrollment) { + if (trainEnrollment != null) + { + // 固定初始参训状态为 0:待报到,防止外部传入非法状态 + trainEnrollment.setEnrollmentStatus(0); + } return trainEnrollmentMapper.insertTrainEnrollment(trainEnrollment); } @@ -59,6 +64,14 @@ public class TrainEnrollmentServiceImpl implements ITrainEnrollmentService { return 0; } + // 严格固定每条新创建的参训记录状态为 0:待报到 + for (TrainEnrollment item : list) + { + if (item != null) + { + item.setEnrollmentStatus(0); + } + } return trainEnrollmentMapper.batchInsertTrainEnrollment(list); } @@ -115,4 +128,10 @@ public class TrainEnrollmentServiceImpl implements ITrainEnrollmentService { return trainEnrollmentMapper.countStudyDurationByEnrollmentId(enrollmentId); } + + @Override + public List> selectEnrollmentCompletionStatsByPlanId(String planId) + { + return trainEnrollmentMapper.selectEnrollmentCompletionStatsByPlanId(planId); + } } diff --git a/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainEnrollmentMapper.xml b/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainEnrollmentMapper.xml index 7bf80afbb..765457f62 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainEnrollmentMapper.xml +++ b/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainEnrollmentMapper.xml @@ -105,7 +105,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" ) values ( - #{item.planId}, #{item.studentId}, #{item.sourceType}, #{item.enrollmentStatus}, + #{item.planId}, #{item.studentId}, #{item.sourceType}, 0, #{item.accommodationRequired}, #{item.joinTime}, #{item.completeTime}, #{item.createBy}, #{item.createTime}, #{item.remark} ) @@ -116,7 +116,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" update train_enrollment source_type = #{sourceType}, - enrollment_status = #{enrollmentStatus}, accommodation_required = #{accommodationRequired}, join_time = #{joinTime}, complete_time = #{completeTime}, @@ -152,9 +151,20 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" update_by = #{operator}, update_time = #{completeTime} where plan_id = #{planId} - and enrollment_status in (0, 1) + and enrollment_status = 1 + and exists ( + select 1 from train_class_report r where r.enrollment_id = train_enrollment.enrollment_id + ) + + delete from train_enrollment where enrollment_id = #{enrollmentId}