修复门禁:阻断不确定下发重试并保护迟到结果

This commit is contained in:
wangzhibiao committed 2026-09-21 14:57:14 +08:00
1 parent 1d2ca54d26
commit 71bde5e674
4 files changed
+135 -35

No files matched your search

@@ -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;
}
}
@@ -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);
}
@@ -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);
}
@@ -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)
{