修复参训:收紧报到状态与报到记录一致性

This commit is contained in:
wangzhibiao committed 2026-09-21 21:43:52 +08:00
1 parent 481c998e82
commit 1c395b7827
5 files changed
+153 -13

No files matched your search

@@ -124,6 +124,14 @@ public interface TrainEnrollmentMapper
@Param("operator") String operator,
@Param("completeTime") Date completeTime);
/**
* 查询指定计划学员的结训预检统计事实(参训状态与报到单存在性)。
*
* @param planId 培训班ID
* @return 参训学员状态事实列表
*/
java.util.List<java.util.Map<String, Object>> selectEnrollmentCompletionStatsByPlanId(@Param("planId") String planId);
/**
* 物理删除参训记录。
*
@@ -44,4 +44,5 @@ public interface ITrainEnrollmentService
int countAccommodationByEnrollmentId(Long enrollmentId);
int countStudyDurationByEnrollmentId(Long enrollmentId);
List<java.util.Map<String, Object>> selectEnrollmentCompletionStatsByPlanId(String planId);
}
@@ -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();
@@ -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<java.util.Map<String, Object>> selectEnrollmentCompletionStatsByPlanId(String planId)
{
return trainEnrollmentMapper.selectEnrollmentCompletionStatsByPlanId(planId);
}
}
@@ -105,7 +105,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
) values
<foreach collection="list" item="item" separator=",">
(
#{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
<set>
<if test="sourceType != null">source_type = #{sourceType},</if>
<if test="enrollmentStatus != null">enrollment_status = #{enrollmentStatus},</if>
<if test="accommodationRequired != null">accommodation_required = #{accommodationRequired},</if>
<if test="joinTime != null">join_time = #{joinTime},</if>
<if test="completeTime != null">complete_time = #{completeTime},</if>
@@ -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
)
</update>
<select id="selectEnrollmentCompletionStatsByPlanId" parameterType="String" resultType="java.util.Map">
select e.enrollment_id as "enrollmentId",
e.enrollment_status as "enrollmentStatus",
(case when exists (select 1 from train_class_report r where r.enrollment_id = e.enrollment_id) then 1 else 0 end) as "hasReport"
from train_enrollment e
where e.plan_id = #{planId}
</select>
<delete id="deleteTrainEnrollmentById" parameterType="Long">
delete from train_enrollment where enrollment_id = #{enrollmentId}
</delete>