修复门禁:补齐多授权撤销与事务补偿

This commit is contained in:
wangzhibiao committed 2026-09-21 12:51:13 +08:00
1 parent 0d5df72d8e
commit f8ca87cfab
3 files changed
+103 -20

No files matched your search

@@ -6,10 +6,13 @@ import com.ruoyi.train.integration.dms.client.DmsOperationStatusClient;
import com.ruoyi.train.integration.dms.config.DmsProperties;
import com.ruoyi.train.integration.dms.dto.DmsOperationStatusRequest;
import com.ruoyi.train.integration.dms.dto.DmsOperationStatusResponse;
import com.ruoyi.train.integration.dms.dto.DmsPasswordDeleteResponse;
import com.ruoyi.train.integration.dms.exception.DmsAuthenticationException;
import com.ruoyi.train.integration.dms.exception.DmsBusinessException;
import com.ruoyi.train.integration.dms.exception.DmsTransportException;
import com.ruoyi.train.integration.dms.gateway.DmsOperationStatusMapper;
import com.ruoyi.train.integration.dms.gateway.ExternalCredentialGateway;
import com.ruoyi.train.mapper.TrainDormAuthMapper;
import com.ruoyi.train.service.IExternalOperationService;
import com.ruoyi.train.service.ITrainDormAuthService;
import com.ruoyi.train.service.impl.ExternalOperationServiceImpl;
@@ -56,6 +59,10 @@ public class DmsOperationPoller {
private DmsOperationStatusClient operationStatusClient;
@org.springframework.beans.factory.annotation.Autowired(required = false)
private ITrainDormAuthService dormAuthService;
@org.springframework.beans.factory.annotation.Autowired(required = false)
private ExternalCredentialGateway credentialGateway;
@org.springframework.beans.factory.annotation.Autowired(required = false)
private TrainDormAuthMapper dormAuthMapper;
private final Set<Long> inFlightOperationIds = ConcurrentHashMap.newKeySet();
private Clock clock = Clock.systemUTC();
@@ -66,18 +73,53 @@ public class DmsOperationPoller {
public DmsOperationPoller(DmsProperties dmsProperties,
IExternalOperationService externalOperationService,
DmsOperationStatusClient operationStatusClient) {
this(dmsProperties, externalOperationService, operationStatusClient, null);
this(dmsProperties, externalOperationService, operationStatusClient, null, null, null);
}
public DmsOperationPoller(DmsProperties dmsProperties,
IExternalOperationService externalOperationService,
DmsOperationStatusClient operationStatusClient,
ITrainDormAuthService dormAuthService) {
this(dmsProperties, externalOperationService, operationStatusClient, dormAuthService, null, null);
}
@org.springframework.beans.factory.annotation.Autowired
public DmsOperationPoller(DmsProperties dmsProperties,
IExternalOperationService externalOperationService,
DmsOperationStatusClient operationStatusClient,
@org.springframework.beans.factory.annotation.Autowired(required = false) ITrainDormAuthService dormAuthService) {
@org.springframework.beans.factory.annotation.Autowired(required = false) ITrainDormAuthService dormAuthService,
@org.springframework.beans.factory.annotation.Autowired(required = false) ExternalCredentialGateway credentialGateway,
@org.springframework.beans.factory.annotation.Autowired(required = false) TrainDormAuthMapper dormAuthMapper) {
this.dmsProperties = dmsProperties;
this.externalOperationService = externalOperationService;
this.operationStatusClient = operationStatusClient;
this.dormAuthService = dormAuthService;
this.credentialGateway = credentialGateway;
this.dormAuthMapper = dormAuthMapper;
}
public void setCredentialGateway(ExternalCredentialGateway credentialGateway) {
this.credentialGateway = credentialGateway;
}
public void setDormAuthMapper(TrainDormAuthMapper dormAuthMapper) {
this.dormAuthMapper = dormAuthMapper;
}
public void setDmsProperties(DmsProperties dmsProperties) {
this.dmsProperties = dmsProperties;
}
public void setExternalOperationService(IExternalOperationService externalOperationService) {
this.externalOperationService = externalOperationService;
}
public void setOperationStatusClient(DmsOperationStatusClient operationStatusClient) {
this.operationStatusClient = operationStatusClient;
}
public void setDormAuthService(ITrainDormAuthService dormAuthService) {
this.dormAuthService = dormAuthService;
}
public void setClock(Clock clock) {
@@ -144,6 +186,11 @@ public class DmsOperationPoller {
}
private void processSingleOperation(TrainExternalOperation op, Instant now) {
if (TrainExternalOperation.STATUS_CREATED.equals(op.getOperationStatus())) {
processCreatedOperation(op, now);
return;
}
String extOpIdStr = op.getExternalOperationId();
if (extOpIdStr == null || extOpIdStr.trim().isEmpty()) {
// 未关联外部操作流水号,无法直接对账
@@ -317,11 +364,61 @@ public class DmsOperationPoller {
}
}
/**
* 崩溃补偿:处理处于 CREATED 状态且已到重试时间、尚未实际派发的撤销操作 (Req 18)
*/
private void processCreatedOperation(TrainExternalOperation op, Instant now) {
if (!TrainExternalOperation.TYPE_REVOKE.equals(op.getOperationType())) {
// Package A: 仅支持自动补偿撤销操作,Grant 由管理员交互手动下发
log.info("Operation {} is in CREATED state with type {}, skipping poller auto-dispatch", op.getOperationId(), op.getOperationType());
return;
}
if (dormAuthMapper == null || credentialGateway == null) {
log.warn("Poller dependencies missing for processing CREATED operation {}", op.getOperationId());
return;
}
TrainDormAuth auth = dormAuthMapper.selectTrainDormAuthByAuthId(op.getDoorAuthId());
if (auth == null) {
handlePollError(op, now, ERROR_CATEGORY_VENDOR_BIZ, "Associated auth not found: " + op.getDoorAuthId(), true);
return;
}
if (auth.getExternalAuthId() == null || auth.getExternalAuthId().trim().isEmpty()) {
handlePollError(op, now, ERROR_CATEGORY_VENDOR_BIZ, "UNRESOLVABLE_EXTERNAL_CREDENTIAL: Missing externalAuthId", true);
return;
}
Long externalKeyId;
try {
externalKeyId = Long.parseLong(auth.getExternalAuthId().trim());
} catch (NumberFormatException e) {
handlePollError(op, now, ERROR_CATEGORY_VENDOR_BIZ, "Invalid externalAuthId format: " + auth.getExternalAuthId(), true);
return;
}
try {
DmsPasswordDeleteResponse resp = credentialGateway.revokeOnlinePassword(externalKeyId);
String extOpIdStr = resp.getOperationId() != null ? String.valueOf(resp.getOperationId()) : null;
if (externalOperationService != null) {
externalOperationService.bindExternalReference(op.getOperationId(), extOpIdStr, null, null, "DmsPollerCrashCompensation");
externalOperationService.transitStatus(op.getOperationId(), TrainExternalOperation.STATUS_ACCEPTED, null, null, "DmsPollerCrashCompensation");
}
log.info("DmsPoller successfully compensated CREATED revoke operation {}: extOpId={}", op.getOperationId(), extOpIdStr);
} catch (Exception e) {
handlePollError(op, now, ERROR_CATEGORY_NETWORK, sanitize(e.getMessage()), false);
}
}
private String sanitize(String str) {
if (str == null) {
return null;
}
return str.replaceAll("(?i)(password|token|secret|appsecret)=[^&\\s]+", "$1=[PROTECTED]")
.replaceAll("(?i)(\"password\"\\s*:\\s*\")[^\"]+(\")", "$1[PROTECTED]$2");
String sanitized = str.replaceAll("(?i)(password|token|secret|appsecret)=[^&\\s]+", "$1=[PROTECTED]")
.replaceAll("(?i)(\"password\"\\s*:\\s*\")[^\"]+(\")", "$1[PROTECTED]$2");
sanitized = sanitized.replaceAll("(?<!\\d)(1[3-9]\\d)\\d{4}(\\d{4})(?!\\d)", "$1****$2");
return sanitized;
}
}
@@ -315,20 +315,6 @@ public class TrainAccommodationServiceImpl implements ITrainAccommodationService
newAcc.setRemark("由原住宿记录 " + oldAccommodationId + " (宿舍 " + oldAcc.getDormId() + ") 调房迁入");
insertTrainAccommodation(newAcc);
// 3. 调房遵循 GRANT_NEW_THEN_REVOKE_OLD:先为新房发起对账发放,旧房在新房生效前绝不注销
if (accessAuthorizationOrchestrator != null)
{
try
{
accessAuthorizationOrchestrator.reconcile(oldAcc.getEnrollmentId());
}
catch (Exception ex)
{
log.warn("Access reconcile triggered on transfer failed for enrollment {}: {}", oldAcc.getEnrollmentId(), ex.getMessage());
}
}
return newAcc;
}
@@ -64,8 +64,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<select id="selectPendingPollOperations" resultMap="TrainExternalOperationResult">
<include refid="selectTrainExternalOperationVo"/>
WHERE operation_status IN ('ACCEPTED', 'PENDING', 'RETRY_WAIT')
AND next_poll_time &lt;= #{maxPollTime}
WHERE operation_status IN ('CREATED', 'ACCEPTED', 'PENDING', 'RETRY_WAIT')
AND (next_poll_time IS NULL OR next_poll_time &lt;= #{maxPollTime})
<if test="vendorCode != null and vendorCode != ''">
AND vendor_code = #{vendorCode}
</if>