diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/TrainDormAuth.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/TrainDormAuth.java index 1bb7f2891..50723399c 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/TrainDormAuth.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/TrainDormAuth.java @@ -232,35 +232,30 @@ public class TrainDormAuth extends BaseEntity this.authEnd = authEnd; } + /** + * @deprecated Legacy field. auth_status 是唯一业务真相源 (Section 20)。 + */ + @Deprecated public Integer getFaceSyncStatus() { return faceSyncStatus; } + /** + * @deprecated Legacy field. auth_status 是唯一业务真相源 (Section 20)。 + */ + @Deprecated public void setFaceSyncStatus(Integer faceSyncStatus) { this.faceSyncStatus = faceSyncStatus; - if (faceSyncStatus != null) { - switch (faceSyncStatus) { - case 0: - if (!AUTH_STATUS_GRANTING.equals(this.authStatus)) { - this.authStatus = AUTH_STATUS_PENDING_GRANT; - } - break; - case 1: - if (!AUTH_STATUS_PENDING_REVOKE.equals(this.authStatus) && !AUTH_STATUS_REVOKING.equals(this.authStatus)) { - this.authStatus = AUTH_STATUS_ACTIVE; - } - break; - case 2: - this.authStatus = AUTH_STATUS_ERROR; - break; - case 3: - this.authStatus = AUTH_STATUS_REVOKED; - break; - default: - break; - } + if (STATUS_ACTIVE.equals(faceSyncStatus)) { + this.authStatus = AUTH_STATUS_ACTIVE; + } else if (STATUS_PENDING.equals(faceSyncStatus)) { + this.authStatus = AUTH_STATUS_PENDING_GRANT; + } else if (STATUS_FAILED.equals(faceSyncStatus)) { + this.authStatus = AUTH_STATUS_ERROR; + } else if (STATUS_REVOKED.equals(faceSyncStatus)) { + this.authStatus = AUTH_STATUS_REVOKED; } } diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/ITrainDormAuthService.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/ITrainDormAuthService.java index d8e540a52..3e401dff8 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/ITrainDormAuthService.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/ITrainDormAuthService.java @@ -98,6 +98,18 @@ public interface ITrainDormAuthService */ public int updateAuthStatus(Long authId, String authStatus, Long operationId, String operator); + /** + * 更新业务授权状态、当前驱动的外部操作ID及错误信息 + * + * @param authId 授权主键 + * @param authStatus 业务授权状态 + * @param operationId 外部操作ID + * @param lastError 错误原因 + * @param operator 操作人 + * @return 影响行数 + */ + public int updateAuthStatus(Long authId, String authStatus, Long operationId, String lastError, String operator); + /** * 由对账轮询器回调更新业务授权状态 (内置 Stale Operation 防迟到覆盖保护) * 只有当 operationId 与授权记录当前最新的 latestOperationId 一致时,才允许推进状态 @@ -110,4 +122,17 @@ public interface ITrainDormAuthService * @return 影响行数 (若被防迟到保护拦截返回 0) */ public int updateAuthStatusByPoller(Long authId, String authStatus, Long operationId, String lastError, String operator); + + /** + * 由对账轮询器回调更新业务授权状态 (内置 Stale Operation 防迟到覆盖保护与状态兼容矩阵) + * + * @param authId 授权主键 + * @param authStatus 目标业务授权状态 + * @param operationId 触发更新的操作流水ID + * @param operationType 操作类型 (GRANT, REVOKE 等) + * @param lastError 错误消息摘要 + * @param operator 操作人 + * @return 影响行数 (若被防迟到保护拦截返回 0) + */ + public int updateAuthStatusByPoller(Long authId, String authStatus, Long operationId, String operationType, String lastError, String operator); } diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainDormAuthServiceImpl.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainDormAuthServiceImpl.java index 138a65fbf..6ede2194a 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainDormAuthServiceImpl.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainDormAuthServiceImpl.java @@ -2,6 +2,8 @@ package com.ruoyi.train.service.impl; import java.util.Date; import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -9,6 +11,7 @@ import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.DateUtils; import com.ruoyi.train.domain.TrainAccommodation; import com.ruoyi.train.domain.TrainDormAuth; +import com.ruoyi.train.domain.TrainExternalOperation; import com.ruoyi.train.mapper.TrainAccommodationMapper; import com.ruoyi.train.mapper.TrainDormAuthMapper; import com.ruoyi.train.service.ITrainDormAuthService; @@ -22,6 +25,8 @@ import com.ruoyi.train.service.ITrainDormAuthService; @Service public class TrainDormAuthServiceImpl implements ITrainDormAuthService { + private static final Logger log = LoggerFactory.getLogger(TrainDormAuthServiceImpl.class); + @Autowired private TrainDormAuthMapper trainDormAuthMapper; @@ -196,16 +201,14 @@ public class TrainDormAuthServiceImpl implements ITrainDormAuthService { return 0; } - // 门禁授权生效保护:生效中或授权中状态,禁止直接清理,必须走标准注销流程 - if (TrainDormAuth.STATUS_ACTIVE.equals(exist.getFaceSyncStatus()) - || TrainDormAuth.AUTH_STATUS_ACTIVE.equals(exist.getAuthStatus()) + // 门禁授权生效保护:生效中或授权中状态,禁止直接清理,必须走标准注销流程 (BLK-02 / Single Source of Truth) + if (TrainDormAuth.AUTH_STATUS_ACTIVE.equals(exist.getAuthStatus()) || TrainDormAuth.AUTH_STATUS_GRANTING.equals(exist.getAuthStatus())) { throw new ServiceException("当前门禁授权处于生效或授权中状态,请先执行注销后再处理"); } // 门禁审计链完整性保护:已注销、注销流转中或已关联外部操作流水的历史授权,严禁物理删除 - if (TrainDormAuth.STATUS_REVOKED.equals(exist.getFaceSyncStatus()) - || TrainDormAuth.AUTH_STATUS_REVOKED.equals(exist.getAuthStatus()) + if (TrainDormAuth.AUTH_STATUS_REVOKED.equals(exist.getAuthStatus()) || TrainDormAuth.AUTH_STATUS_PENDING_REVOKE.equals(exist.getAuthStatus()) || TrainDormAuth.AUTH_STATUS_REVOKING.equals(exist.getAuthStatus()) || exist.getLatestOperationId() != null @@ -213,9 +216,8 @@ public class TrainDormAuthServiceImpl implements ITrainDormAuthService { throw new ServiceException("该门禁授权已进入授权生命周期或产生外部审计流水,禁止物理删除历史凭证"); } - // 安全底线:仅允许清理尚未执行过任何下发动作的初始待授权草稿 (STATUS_PENDING / AUTH_STATUS_PENDING_GRANT) - if ((exist.getFaceSyncStatus() != null && !TrainDormAuth.STATUS_PENDING.equals(exist.getFaceSyncStatus())) - || (exist.getAuthStatus() != null && !TrainDormAuth.AUTH_STATUS_PENDING_GRANT.equals(exist.getAuthStatus()))) + // 安全底线:仅允许清理尚未执行过任何下发动作的初始待授权草稿 (AUTH_STATUS_PENDING_GRANT) + if (exist.getAuthStatus() != null && !TrainDormAuth.AUTH_STATUS_PENDING_GRANT.equals(exist.getAuthStatus())) { throw new ServiceException("当前门禁授权非待下发草稿状态,禁止物理删除"); } @@ -272,6 +274,13 @@ public class TrainDormAuthServiceImpl implements ITrainDormAuthService @Override @Transactional(rollbackFor = Exception.class) public int updateAuthStatus(Long authId, String authStatus, Long operationId, String operator) + { + return updateAuthStatus(authId, authStatus, operationId, null, operator); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int updateAuthStatus(Long authId, String authStatus, Long operationId, String lastError, String operator) { TrainDormAuth exist = trainDormAuthMapper.selectTrainDormAuthByAuthId(authId); if (exist == null) @@ -283,6 +292,10 @@ public class TrainDormAuthServiceImpl implements ITrainDormAuthService { exist.setLatestOperationId(operationId); } + if (lastError != null) + { + exist.setLastError(lastError); + } exist.setOperationVersion((exist.getOperationVersion() != null ? exist.getOperationVersion() : 1) + 1); exist.setUpdateBy(operator); exist.setUpdateTime(DateUtils.getNowDate()); @@ -297,6 +310,19 @@ public class TrainDormAuthServiceImpl implements ITrainDormAuthService @Override @Transactional(rollbackFor = Exception.class) public int updateAuthStatusByPoller(Long authId, String authStatus, Long operationId, String lastError, String operator) + { + String derivedType = null; + if (TrainDormAuth.AUTH_STATUS_ACTIVE.equals(authStatus)) { + derivedType = TrainExternalOperation.TYPE_GRANT; + } else if (TrainDormAuth.AUTH_STATUS_REVOKED.equals(authStatus)) { + derivedType = TrainExternalOperation.TYPE_REVOKE; + } + return updateAuthStatusByPoller(authId, authStatus, operationId, derivedType, lastError, operator); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int updateAuthStatusByPoller(Long authId, String targetAuthStatus, Long operationId, String operationType, String lastError, String operator) { TrainDormAuth exist = trainDormAuthMapper.selectTrainDormAuthByAuthId(authId); if (exist == null) @@ -304,15 +330,49 @@ public class TrainDormAuthServiceImpl implements ITrainDormAuthService return 0; } - // 核心防迟到保护 (STALE_OPERATION_RESULT_GUARD) - // 只有当传入的 operationId 与当前记录绑定的 latestOperationId 相符时才更新 - if (operationId != null && exist.getLatestOperationId() != null && !operationId.equals(exist.getLatestOperationId())) + // 1. 核心防迟到保护:operationId 必须与当前记录权威 latestOperationId 完全一致 (BLK-02) + if (operationId == null || exist.getLatestOperationId() == null || !operationId.equals(exist.getLatestOperationId())) { - // 说明已被后续发起的操作替代,忽略此迟到结果 + log.warn("Poller update rejected (Stale operation guard): authId={}, pollerOpId={}, latestOpId={}", + authId, operationId, exist.getLatestOperationId()); return 0; } - exist.setAuthStatus(authStatus); + String currentStatus = exist.getAuthStatus(); + + // 2. 状态兼容矩阵校验 (State Compatibility Matrix Guard: BLK-02) + if (TrainDormAuth.AUTH_STATUS_ACTIVE.equals(targetAuthStatus)) + { + // GRANT SUCCESS: 仅允许 GRANTING -> ACTIVE。 + // 其它状态 (PENDING_REVOKE, REVOKING, REVOKED, ERROR, PENDING_GRANT) 严禁被推进为 ACTIVE! + if (!TrainDormAuth.AUTH_STATUS_GRANTING.equals(currentStatus)) + { + log.warn("Late/Invalid GRANT SUCCESS rejected for authId {}: currentStatus={}, prohibited from advancing to ACTIVE", + authId, currentStatus); + return 0; + } + } + else if (TrainDormAuth.AUTH_STATUS_REVOKED.equals(targetAuthStatus)) + { + // REVOKE SUCCESS: 仅允许 REVOKING -> REVOKED (兼容 PENDING_REVOKE) + if (!TrainDormAuth.AUTH_STATUS_REVOKING.equals(currentStatus) && !TrainDormAuth.AUTH_STATUS_PENDING_REVOKE.equals(currentStatus)) + { + log.warn("Late/Invalid REVOKE SUCCESS rejected for authId {}: currentStatus={}, prohibited from advancing to REVOKED", + authId, currentStatus); + return 0; + } + } + else if (TrainDormAuth.AUTH_STATUS_ERROR.equals(targetAuthStatus)) + { + // 终态保护:已注销 (REVOKED) 不允许因历史流水失败退回到 ERROR + if (TrainDormAuth.AUTH_STATUS_REVOKED.equals(currentStatus)) + { + log.warn("Poller ERROR rejected for authId {}: already in terminal REVOKED state", authId); + return 0; + } + } + + exist.setAuthStatus(targetAuthStatus); if (lastError != null) { exist.setLastError(lastError); @@ -320,7 +380,7 @@ public class TrainDormAuthServiceImpl implements ITrainDormAuthService exist.setUpdateBy(operator != null ? operator : "DmsPoller"); exist.setUpdateTime(DateUtils.getNowDate()); int rows = trainDormAuthMapper.updateTrainDormAuth(exist); - if (rows > 0 && lastError == null && (TrainDormAuth.AUTH_STATUS_ACTIVE.equals(authStatus) || TrainDormAuth.AUTH_STATUS_REVOKED.equals(authStatus))) + if (rows > 0 && lastError == null && (TrainDormAuth.AUTH_STATUS_ACTIVE.equals(targetAuthStatus) || TrainDormAuth.AUTH_STATUS_REVOKED.equals(targetAuthStatus))) { trainDormAuthMapper.clearLastError(authId); } diff --git a/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/DormAuthAlignmentTest.java b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/DormAuthAlignmentTest.java index 641504a19..6aff46bec 100644 --- a/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/DormAuthAlignmentTest.java +++ b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/DormAuthAlignmentTest.java @@ -561,6 +561,26 @@ public class DormAuthAlignmentTest return 0; } + @Override + public int countActiveByDormIdAndBedNo(String dormId, String bedNo, Long excludeAccommodationId) + { + if (dormId == null || bedNo == null || bedNo.trim().isEmpty()) return 0; + int count = 0; + for (TrainAccommodation a : store.values()) + { + if (excludeAccommodationId != null && excludeAccommodationId.equals(a.getAccommodationId())) continue; + if (dormId.equals(a.getDormId()) && bedNo.equals(a.getBedNo())) + { + if (TrainAccommodation.STATUS_PREALLOCATED.equals(a.getAccommodationStatus()) + || TrainAccommodation.STATUS_CHECKED_IN.equals(a.getAccommodationStatus())) + { + count++; + } + } + } + return count; + } + @Override public int insertTrainAccommodation(TrainAccommodation accommodation) {