修复参训:串行化报到与状态迁移

This commit is contained in:
wangzhibiao committed 2026-09-21 21:43:52 +08:00
1 parent 579ce861b6
commit a7fe99f94d
5 files changed
+66 -6

No files matched your search

@@ -32,6 +32,23 @@ public interface TrainEnrollmentMapper
*/
TrainEnrollment selectTrainEnrollmentById(@Param("enrollmentId") Long enrollmentId);
/**
* 锁定指定参训学员行 (SELECT ... FOR UPDATE),用于报到与状态流转并发控制。
*
* @param enrollmentId 参训档案ID
* @return 参训记录
*/
TrainEnrollment selectTrainEnrollmentByIdForUpdate(@Param("enrollmentId") Long enrollmentId);
/**
* 按 enrollment_id 稳定升序锁定指定开班计划下的全部学员记录 (SELECT ... FOR UPDATE),
* 用于结训预检与结训归档流程防并发竞态。
*
* @param planId 开班计划ID
* @return 该计划学员记录列表
*/
List<TrainEnrollment> selectEnrollmentsForUpdateByPlanId(@Param("planId") String planId);
/**
* 根据计划ID与学员ID查询参训记录(用于唯一性校验)。
*
@@ -17,6 +17,10 @@ public interface ITrainEnrollmentService
{
TrainEnrollment selectTrainEnrollmentById(Long enrollmentId);
TrainEnrollment selectTrainEnrollmentByIdForUpdate(Long enrollmentId);
List<TrainEnrollment> selectEnrollmentsForUpdateByPlanId(String planId);
TrainEnrollment selectTrainEnrollmentByPlanIdAndStudentId(String planId, String studentId);
List<EnrollmentItemVO> selectEnrollmentItemList(EnrollmentQuery query);
@@ -377,7 +377,8 @@ public class EnrollmentAppServiceImpl implements EnrollmentAppService
throw new ServiceException("普通状态接口禁止直接设置为已完成状态,结训必须走开班计划结训流程");
}
TrainEnrollment enrollment = requireEnrollment(enrollmentId);
// 锁定参训学员记录行 (SELECT ... FOR UPDATE),保证状态流转并发串行化并消除 TOCTOU
TrainEnrollment enrollment = requireEnrollmentForUpdate(enrollmentId);
// 计划已结训或取消时,禁止状态变更
TrainClassPlan plan = requirePlan(enrollment.getPlanId());
checkPlanStatusNotLocked(plan);
@@ -568,6 +569,22 @@ public class EnrollmentAppServiceImpl implements EnrollmentAppService
return enrollment;
}
private TrainEnrollment requireEnrollmentForUpdate(Long enrollmentId)
{
if (enrollmentId == null)
{
throw new ServiceException(EnrollmentErrorCode.ENROLLMENT_NOT_FOUND_MESSAGE, EnrollmentErrorCode.ENROLLMENT_NOT_FOUND);
}
TrainEnrollment enrollment = trainEnrollmentService != null
? trainEnrollmentService.selectTrainEnrollmentByIdForUpdate(enrollmentId)
: (trainEnrollmentMapper != null ? trainEnrollmentMapper.selectTrainEnrollmentByIdForUpdate(enrollmentId) : null);
if (enrollment == null)
{
throw new ServiceException(EnrollmentErrorCode.ENROLLMENT_NOT_FOUND_MESSAGE, EnrollmentErrorCode.ENROLLMENT_NOT_FOUND);
}
return enrollment;
}
private void checkPlanStatusNotLocked(TrainClassPlan plan)
{
if (Integer.valueOf(2).equals(plan.getPlanStatus()) || Integer.valueOf(3).equals(plan.getPlanStatus()))
@@ -719,11 +736,8 @@ public class EnrollmentAppServiceImpl implements EnrollmentAppService
dto = new ReportConfirmDTO();
}
TrainEnrollment enrollment = trainEnrollmentMapper.selectTrainEnrollmentById(enrollmentId);
if (enrollment == null)
{
throw new ServiceException(EnrollmentErrorCode.ENROLLMENT_NOT_FOUND_MESSAGE, EnrollmentErrorCode.ENROLLMENT_NOT_FOUND);
}
// 锁定参训学员记录行 (SELECT ... FOR UPDATE),保证报到确认与状态流转串行化并消除 TOCTOU
TrainEnrollment enrollment = requireEnrollmentForUpdate(enrollmentId);
// 幂等与查重:若已报到(status==1)或已有报到记录
TrainClassReport existingReport = trainClassReportMapper.selectTrainClassReportByEnrollmentId(enrollmentId);
@@ -28,6 +28,18 @@ public class TrainEnrollmentServiceImpl implements ITrainEnrollmentService
return trainEnrollmentMapper.selectTrainEnrollmentById(enrollmentId);
}
@Override
public TrainEnrollment selectTrainEnrollmentByIdForUpdate(Long enrollmentId)
{
return trainEnrollmentMapper.selectTrainEnrollmentByIdForUpdate(enrollmentId);
}
@Override
public List<TrainEnrollment> selectEnrollmentsForUpdateByPlanId(String planId)
{
return trainEnrollmentMapper.selectEnrollmentsForUpdateByPlanId(planId);
}
@Override
public TrainEnrollment selectTrainEnrollmentByPlanIdAndStudentId(String planId, String studentId)
{
@@ -45,6 +45,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where enrollment_id = #{enrollmentId}
</select>
<select id="selectTrainEnrollmentByIdForUpdate" parameterType="Long" resultMap="TrainEnrollmentResult">
<include refid="selectTrainEnrollmentDomain" />
where enrollment_id = #{enrollmentId}
for update
</select>
<select id="selectEnrollmentsForUpdateByPlanId" resultMap="TrainEnrollmentResult">
<include refid="selectTrainEnrollmentDomain" />
where plan_id = #{planId}
order by enrollment_id asc
for update
</select>
<select id="selectTrainEnrollmentByPlanIdAndStudentId" resultMap="TrainEnrollmentResult">
<include refid="selectTrainEnrollmentDomain" />
where plan_id = #{planId} and student_id = #{studentId}