修复考勤:收紧首次签到参训生命周期校验

This commit is contained in:
wangzhibiao committed 2026-09-22 00:04:51 +08:00
1 parent 38d4851a18
commit 49dbde8836
2 files changed
+83 -8

No files matched your search

@@ -37,6 +37,22 @@ public final class AttendanceErrorCode
public static final int ATTENDANCE_ALREADY_SIGNED_OUT = 40084;
public static final String ATTENDANCE_ALREADY_SIGNED_OUT_MESSAGE = "该考勤记录已完成签退,不可重复签退";
/** 签到需已报到学员:学员尚未完成开班报到,禁止录入课次考勤 */
public static final int ATTENDANCE_SIGN_IN_REQUIRES_REPORTED_ENROLLMENT = 40085;
public static final String ATTENDANCE_SIGN_IN_REQUIRES_REPORTED_ENROLLMENT_MESSAGE = "学员尚未完成开班报到,禁止录入课次考勤";
/** 学员参训状态已放弃,禁止录入课次考勤 */
public static final int ATTENDANCE_SIGN_IN_ENROLLMENT_ABANDONED = 40086;
public static final String ATTENDANCE_SIGN_IN_ENROLLMENT_ABANDONED_MESSAGE = "学员参训状态已放弃,禁止录入课次考勤";
/** 学员参训档案已结训完成,禁止录入课次考勤 */
public static final int ATTENDANCE_SIGN_IN_ENROLLMENT_COMPLETED = 40087;
public static final String ATTENDANCE_SIGN_IN_ENROLLMENT_COMPLETED_MESSAGE = "学员参训档案已结训完成,禁止录入课次考勤";
/** 学员参训生命周期状态不合法,禁止录入课次考勤 */
public static final int ATTENDANCE_SIGN_IN_ENROLLMENT_STATUS_INVALID = 40088;
public static final String ATTENDANCE_SIGN_IN_ENROLLMENT_STATUS_INVALID_MESSAGE = "学员参训生命周期状态不合法,禁止录入课次考勤";
private AttendanceErrorCode()
{
}
@@ -81,13 +81,17 @@ public class AttendanceAppServiceImpl implements AttendanceAppService
// 幂等性与查重:基于 (enrollment_id, lesson_id) 查重,存在已有签到记录时不重复覆盖
TrainStudentCheckin existing = trainStudentCheckinMapper.selectTrainStudentCheckinByEnrollmentAndLesson(dto.getEnrollmentId(), dto.getLessonId());
if (existing != null && existing.getSignInTime() != null)
{
// 已有签到记录,幂等返回已有记录,不覆盖也不受后续生命周期流转拦截
return existing;
}
// 核心生命周期守卫:即将产生新的 signIn 事实,严格校验 enrollment lifecycle eligibility
assertEnrollmentEligibleForSignIn(enrollment);
if (existing != null)
{
if (existing.getSignInTime() != null)
{
// 已有签到记录,幂等返回已有记录,不覆盖
return existing;
}
Date now = dto.getSignInTime() != null ? dto.getSignInTime() : new Date();
existing.setSignInTime(now);
existing.setCheckinWay(dto.getCheckinWay() != null ? dto.getCheckinWay() : 1);
@@ -297,6 +301,8 @@ public class AttendanceAppServiceImpl implements AttendanceAppService
vo.setPhoneMasked(maskPhone(item.getPhone()));
vo.setEnrollmentStatus(item.getEnrollmentStatus());
boolean eligibleForSignIn = isEnrollmentEligibleForSignIn(item.getEnrollmentStatus());
TrainStudentCheckin c = checkinMap.get(item.getEnrollmentId());
if (c != null)
{
@@ -305,8 +311,8 @@ public class AttendanceAppServiceImpl implements AttendanceAppService
vo.setAttendanceStatus(c.getAttendanceStatus());
vo.setSignInTime(c.getSignInTime());
vo.setSignOutTime(c.getSignOutTime());
// 仅当已有记录但未打签到时间时允许补签;已有签到时间则禁止重复签到
vo.setCanSignIn(c.getSignInTime() == null);
// 仅当已有记录但未打签到时间,且当前参训状态允许签到时才允许补签;已有签到事实禁止重复签到
vo.setCanSignIn(c.getSignInTime() == null && eligibleForSignIn);
vo.setCanSignOut(c.getSignInTime() != null && c.getSignOutTime() == null);
vo.setCanAdjust(true);
}
@@ -317,7 +323,8 @@ public class AttendanceAppServiceImpl implements AttendanceAppService
vo.setAttendanceStatus(null);
vo.setSignInTime(null);
vo.setSignOutTime(null);
vo.setCanSignIn(true);
// 无打卡记录时,仅当学员处于允许签到的参训生命周期状态 (1:已报到 或 2:请假) 时才允许签到
vo.setCanSignIn(eligibleForSignIn);
vo.setCanSignOut(false);
vo.setCanAdjust(false);
}
@@ -327,6 +334,58 @@ public class AttendanceAppServiceImpl implements AttendanceAppService
return roster;
}
/**
* 校验参训学员生命周期状态是否允许生成新的签到事实。
*
* 冻结业务规则:
* 0 待报到 -> 拒绝 (必须先经过 confirmReport 完成 0 -> 1,不能用考勤签到绕过报到生命周期)
* 1 已报到 -> 允许
* 2 请假 -> 允许 (当前 Preview Default 允许签到;LEAVE_SIGNIN_POLICY = PREVIEW_DEFAULT_ALLOWED / PENDING_CUSTOMER_CONFIRMATION)
* 3 放弃 -> 拒绝
* 4 已完成 -> 拒绝
* 未知/null -> fail closed 拒绝
*/
private void assertEnrollmentEligibleForSignIn(TrainEnrollment enrollment)
{
if (enrollment == null || enrollment.getEnrollmentStatus() == null)
{
throw new ServiceException(AttendanceErrorCode.ATTENDANCE_SIGN_IN_ENROLLMENT_STATUS_INVALID_MESSAGE,
AttendanceErrorCode.ATTENDANCE_SIGN_IN_ENROLLMENT_STATUS_INVALID);
}
int status = enrollment.getEnrollmentStatus();
switch (status)
{
case 1: // 1:已报到
case 2: // 2:请假 (LEAVE_SIGNIN_POLICY: PREVIEW_DEFAULT_ALLOWED; PENDING_CUSTOMER_CONFIRMATION)
return;
case 0: // 0:待报到
throw new ServiceException(AttendanceErrorCode.ATTENDANCE_SIGN_IN_REQUIRES_REPORTED_ENROLLMENT_MESSAGE,
AttendanceErrorCode.ATTENDANCE_SIGN_IN_REQUIRES_REPORTED_ENROLLMENT);
case 3: // 3:放弃
throw new ServiceException(AttendanceErrorCode.ATTENDANCE_SIGN_IN_ENROLLMENT_ABANDONED_MESSAGE,
AttendanceErrorCode.ATTENDANCE_SIGN_IN_ENROLLMENT_ABANDONED);
case 4: // 4:已完成
throw new ServiceException(AttendanceErrorCode.ATTENDANCE_SIGN_IN_ENROLLMENT_COMPLETED_MESSAGE,
AttendanceErrorCode.ATTENDANCE_SIGN_IN_ENROLLMENT_COMPLETED);
default:
throw new ServiceException(AttendanceErrorCode.ATTENDANCE_SIGN_IN_ENROLLMENT_STATUS_INVALID_MESSAGE,
AttendanceErrorCode.ATTENDANCE_SIGN_IN_ENROLLMENT_STATUS_INVALID);
}
}
/**
* 判定指定参训生命周期状态是否允许生成新签到事实 (供名册 canSignIn 统一复用)。
*/
private boolean isEnrollmentEligibleForSignIn(Integer status)
{
if (status == null)
{
return false;
}
return status == 1 || status == 2;
}
private String maskPhone(String phone)
{
if (phone == null || phone.length() < 7)