From 49dbde8836661b310be372bfa431cd81ae551ca4 Mon Sep 17 00:00:00 2001 From: wangzhibiao Date: Tue, 22 Sep 2026 00:04:51 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=80=83=E5=8B=A4=EF=BC=9A?= =?UTF-8?q?=E6=94=B6=E7=B4=A7=E9=A6=96=E6=AC=A1=E7=AD=BE=E5=88=B0=E5=8F=82?= =?UTF-8?q?=E8=AE=AD=E7=94=9F=E5=91=BD=E5=91=A8=E6=9C=9F=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../train/constant/AttendanceErrorCode.java | 16 ++++ .../impl/AttendanceAppServiceImpl.java | 75 +++++++++++++++++-- 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/constant/AttendanceErrorCode.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/constant/AttendanceErrorCode.java index b0cd90f98..0d5af1fb0 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/constant/AttendanceErrorCode.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/constant/AttendanceErrorCode.java @@ -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() { } diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/AttendanceAppServiceImpl.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/AttendanceAppServiceImpl.java index f47039c7a..0ad43c2d9 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/AttendanceAppServiceImpl.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/AttendanceAppServiceImpl.java @@ -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)