修复门禁:退宿时正确进入授权撤销流程
退宿后 enrollment 仍为已报到但已无有效住宿,reconcile(enrollmentId) 只会返回 WAITING_FOR_PREREQUISITE,被退宿住宿对应的 train_dorm_auth 因此永远无法进入撤权状态。 - 编排器新增住宿关系终止撤权路径 revokeAuthorizationsForAccommodation, 只针对被终止的那一条住宿记录,不改动 reconcile 整体对账语义 - 撤权全程复用既有安全机制:DMS 禁用态语义、CAS 原子 Claim、AMBIGUOUS 判定、 禁止盲重发,绝不直接 UPDATE auth_status='REVOKED' 绕过状态机 - 已处于 REVOKED / REVOKING / PENDING_REVOKE 的授权直接跳过,避免重复排队 - 退宿改走该路径,撤权推进失败不回滚退宿事实 - 调房不受影响,仍保持新房授权生效后再撤旧房 (GRANT_NEW_THEN_REVOKE_OLD) - 补充住宿终止撤权与住宿状态守卫单元测试 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
aeb76ce35a
commit
dc3990b29a
4 files changed
+415
-2
No files matched your search
+52
@@ -261,6 +261,58 @@ public class AccessAuthorizationOrchestrator {
|
||||
return ReconciliationResult.awaitingManualIssue(authId, "Prerequisites met; waiting for administrator manual issue");
|
||||
}
|
||||
|
||||
/**
|
||||
* 住宿关系终止撤权路径 (FIX-01)
|
||||
* <p>
|
||||
* 冻结业务语义:退宿 = 该住宿关系终止,该住宿产生的门禁授权必须进入撤销流程。
|
||||
* <p>
|
||||
* 设计约束:
|
||||
* 1. 本方法只针对"被终止的那一条住宿记录",不改动 reconcile() 的整体对账语义,
|
||||
* 因此调房 (GRANT_NEW_THEN_REVOKE_OLD) 的"先新房 ACTIVE 再撤旧房"顺序不受影响。
|
||||
* 2. 全程复用 executeBatchRevokeForAuths → executeRevokeFlow,继承既有安全机制:
|
||||
* DMS 禁用态语义、CAS 原子 Claim、AMBIGUOUS 判定、禁止盲重发。
|
||||
* 3. 绝不直接 UPDATE auth_status='REVOKED' 绕过状态机。
|
||||
* 4. 已处于撤销流程中或已撤销的授权直接跳过,避免重复排队与重复调用厂商 mutation。
|
||||
*
|
||||
* @param accommodationId 被终止的住宿安排ID
|
||||
* @param reason 终止原因标识
|
||||
* @param operator 操作人
|
||||
* @return 批量撤权结果
|
||||
*/
|
||||
public BatchReconciliationResult revokeAuthorizationsForAccommodation(Long accommodationId, String reason, String operator) {
|
||||
TrainAccommodation acc = accommodationId != null && accommodationMapper != null
|
||||
? accommodationMapper.selectTrainAccommodationByAccommodationId(accommodationId) : null;
|
||||
Long enrollmentId = acc != null ? acc.getEnrollmentId() : null;
|
||||
|
||||
if (accommodationId == null) {
|
||||
BatchReconciliationResult empty = new BatchReconciliationResult(null);
|
||||
empty.setOutcome(BatchReconciliationResult.OUTCOME_ALL_SETTLED);
|
||||
return empty;
|
||||
}
|
||||
|
||||
TrainDormAuth auth = dormAuthMapper != null ? dormAuthMapper.selectTrainDormAuthByAccommodationId(accommodationId) : null;
|
||||
if (auth == null || auth.getAuthId() == null) {
|
||||
BatchReconciliationResult empty = new BatchReconciliationResult(enrollmentId);
|
||||
empty.setOutcome(BatchReconciliationResult.OUTCOME_ALL_SETTLED);
|
||||
return empty;
|
||||
}
|
||||
|
||||
// 已在撤销流程中或已撤销:不重复推进,避免重复排队与盲重发
|
||||
String status = auth.getAuthStatus();
|
||||
if (TrainDormAuth.AUTH_STATUS_REVOKED.equals(status)
|
||||
|| TrainDormAuth.AUTH_STATUS_REVOKING.equals(status)
|
||||
|| TrainDormAuth.AUTH_STATUS_PENDING_REVOKE.equals(status)) {
|
||||
BatchReconciliationResult settled = new BatchReconciliationResult(enrollmentId);
|
||||
settled.addItem(new ReconciliationItemVO(auth.getAuthId(), accommodationId, "REVOKE", "NO_ACTION",
|
||||
"Authorization already in revoke lifecycle: " + status));
|
||||
settled.setNoAction(settled.getNoAction() + 1);
|
||||
settled.computeOutcome();
|
||||
return settled;
|
||||
}
|
||||
|
||||
return executeBatchRevokeForAuths(enrollmentId, Collections.singletonList(auth), reason, operator);
|
||||
}
|
||||
|
||||
/**
|
||||
* 抽取统一批量撤权处理方法 (MAJ-04: 退训与调房公用,任何单项进入批次统计,全失败绝不报成功)
|
||||
*/
|
||||
|
||||
+8
-2
@@ -298,13 +298,19 @@ public class TrainAccommodationServiceImpl implements ITrainAccommodationService
|
||||
TrainAccommodation.STATUS_CHECKED_OUT, operator, DateUtils.getNowDate());
|
||||
if (rows > 0 && accessAuthorizationOrchestrator != null)
|
||||
{
|
||||
// FIX-01: 退宿即住宿关系终止,必须让该住宿产生的门禁授权进入撤销流程。
|
||||
// 此处不能再走 reconcile(enrollmentId):退宿后已无有效住宿,reconcile 会直接返回
|
||||
// WAITING_FOR_PREREQUISITE,旧授权将永远停留在原状态而不被撤销。
|
||||
// 撤权全程复用编排器既有安全机制 (DMS 禁用态语义 / CAS Claim / AMBIGUOUS / 禁止盲重发)。
|
||||
try
|
||||
{
|
||||
accessAuthorizationOrchestrator.reconcile(exist.getEnrollmentId());
|
||||
accessAuthorizationOrchestrator.revokeAuthorizationsForAccommodation(
|
||||
accommodationId, "AccommodationCheckedOut", operator);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.warn("Access reconcile triggered on checkout failed for enrollment {}: {}", exist.getEnrollmentId(), ex.getMessage());
|
||||
// 撤权推进失败不回滚退宿事实,授权侧会留下 ERROR/待处理状态供人工核查
|
||||
log.error("Access revoke triggered on checkout failed for accommodation {}: {}", accommodationId, ex.getMessage());
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
|
||||
+138
@@ -613,6 +613,144 @@ public class AccessAuthorizationOrchestrationTest {
|
||||
TrainDormAuth.AUTH_STATUS_ACTIVE, auth.getAuthStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* FIX-01: 退宿撤权路径 — 授权已 ACTIVE 时,住宿终止必须推进到 REVOKING 并真实发起外部撤权。
|
||||
* 同时锁定反向事实:退宿后 reconcile(enrollmentId) 只会返回 WAITING_FOR_PREREQUISITE,
|
||||
* 因此撤权绝不能依赖 reconcile。
|
||||
*/
|
||||
@Test
|
||||
public void testFix01_AccommodationTerminationRevoke_ActiveAuthEntersRevokeFlow() {
|
||||
Long enrollmentId = 20L;
|
||||
TrainEnrollment enrollment = new TrainEnrollment();
|
||||
enrollment.setEnrollmentId(enrollmentId);
|
||||
enrollment.setStudentId("STU-020");
|
||||
enrollment.setPlanId("PLAN-101");
|
||||
enrollment.setEnrollmentStatus(1);
|
||||
enrollmentDb.put(enrollmentId, enrollment);
|
||||
|
||||
TrainAccommodation acc = new TrainAccommodation();
|
||||
acc.setAccommodationId(200L);
|
||||
acc.setEnrollmentId(enrollmentId);
|
||||
acc.setDormId("DORM-201");
|
||||
acc.setAccommodationStatus(1);
|
||||
accommodationDb.put(200L, acc);
|
||||
|
||||
ReconciliationResult grant = orchestrator.reconcile(enrollmentId);
|
||||
orchestrator.issueAuthorization(grant.getDoorAuthId(), "admin");
|
||||
mockServer.setOperationStatus(99001L, "success", "Success");
|
||||
poller.pollPendingOperations();
|
||||
|
||||
TrainDormAuth auth = dormAuthDb.get(200L);
|
||||
assertEquals(TrainDormAuth.AUTH_STATUS_ACTIVE, auth.getAuthStatus());
|
||||
|
||||
// 办理退宿:住宿状态 1 -> 2
|
||||
acc.setAccommodationStatus(2);
|
||||
|
||||
// 反向事实:此时 reconcile 无法推进撤权,只会等待前置条件
|
||||
ReconciliationResult stale = orchestrator.reconcile(enrollmentId);
|
||||
assertEquals(ReconciliationDecision.WAITING_FOR_PREREQUISITE, stale.getDecision());
|
||||
assertEquals("reconcile must not revoke on checkout", TrainDormAuth.AUTH_STATUS_ACTIVE, auth.getAuthStatus());
|
||||
|
||||
// 退宿撤权路径:授权必须进入撤销流程
|
||||
BatchReconciliationResult batch = orchestrator.revokeAuthorizationsForAccommodation(200L, "AccommodationCheckedOut", "admin");
|
||||
assertEquals(1, batch.getDispatched());
|
||||
assertEquals(0, batch.getFailed());
|
||||
assertEquals("Auth must enter REVOKING, never be force-set to REVOKED",
|
||||
TrainDormAuth.AUTH_STATUS_REVOKING, auth.getAuthStatus());
|
||||
assertEquals(1, mockServer.getPasswordDeletePostCounter());
|
||||
|
||||
// Poller 确认后才进入终态
|
||||
mockServer.setOperationStatus(99002L, "success", "Delete confirmed");
|
||||
poller.pollPendingOperations();
|
||||
assertEquals(TrainDormAuth.AUTH_STATUS_REVOKED, auth.getAuthStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* FIX-01 + DMS disabled: 存在外部凭据时必须沉淀为 PENDING_REVOKE 并排队,
|
||||
* 绝不虚假标记 REVOKED,且零真实 DMS mutation。
|
||||
*/
|
||||
@Test
|
||||
public void testFix01_AccommodationTerminationRevoke_DmsDisabledWithExternalCredential() {
|
||||
properties.setEnabled(false);
|
||||
|
||||
TrainAccommodation acc = new TrainAccommodation();
|
||||
acc.setAccommodationId(210L);
|
||||
acc.setEnrollmentId(21L);
|
||||
acc.setDormId("DORM-201");
|
||||
acc.setAccommodationStatus(2);
|
||||
accommodationDb.put(210L, acc);
|
||||
|
||||
TrainDormAuth auth = new TrainDormAuth();
|
||||
auth.setAuthId(2100L);
|
||||
auth.setAccommodationId(210L);
|
||||
auth.setAuthStatus(TrainDormAuth.AUTH_STATUS_ACTIVE);
|
||||
auth.setExternalAuthId("778899");
|
||||
dormAuthDb.put(210L, auth);
|
||||
|
||||
int opCountBefore = operationDb.size();
|
||||
BatchReconciliationResult batch = orchestrator.revokeAuthorizationsForAccommodation(210L, "AccommodationCheckedOut", "admin");
|
||||
|
||||
assertEquals("DMS disabled must queue instead of dispatch", 1, batch.getQueued());
|
||||
assertEquals("Must not fake REVOKED while external credential still exists",
|
||||
TrainDormAuth.AUTH_STATUS_PENDING_REVOKE, auth.getAuthStatus());
|
||||
assertEquals("A REVOKE operation must be queued", opCountBefore + 1, operationDb.size());
|
||||
assertEquals("REAL_DMS_MUTATION_COUNT must stay 0", 0, mockServer.getPasswordDeletePostCounter());
|
||||
|
||||
// 幂等:重复推进不得产生第二条排队操作,也不得盲重发
|
||||
int opCountAfterFirst = operationDb.size();
|
||||
BatchReconciliationResult second = orchestrator.revokeAuthorizationsForAccommodation(210L, "AccommodationCheckedOut", "admin");
|
||||
assertEquals(1, second.getNoAction());
|
||||
assertEquals(opCountAfterFirst, operationDb.size());
|
||||
assertEquals(0, mockServer.getPasswordDeletePostCounter());
|
||||
}
|
||||
|
||||
/**
|
||||
* FIX-01 + DMS disabled: 从未下发过的草稿授权 (PENDING_GRANT 且无外部凭据) 允许本地安全终止。
|
||||
*/
|
||||
@Test
|
||||
public void testFix01_AccommodationTerminationRevoke_DmsDisabledDraftAuthLocallyRevoked() {
|
||||
properties.setEnabled(false);
|
||||
|
||||
TrainAccommodation acc = new TrainAccommodation();
|
||||
acc.setAccommodationId(220L);
|
||||
acc.setEnrollmentId(22L);
|
||||
acc.setDormId("DORM-201");
|
||||
acc.setAccommodationStatus(2);
|
||||
accommodationDb.put(220L, acc);
|
||||
|
||||
TrainDormAuth auth = new TrainDormAuth();
|
||||
auth.setAuthId(2200L);
|
||||
auth.setAccommodationId(220L);
|
||||
auth.setAuthStatus(TrainDormAuth.AUTH_STATUS_PENDING_GRANT);
|
||||
dormAuthDb.put(220L, auth);
|
||||
|
||||
BatchReconciliationResult batch = orchestrator.revokeAuthorizationsForAccommodation(220L, "AccommodationCheckedOut", "admin");
|
||||
|
||||
assertEquals(1, batch.getNoAction());
|
||||
assertEquals(TrainDormAuth.AUTH_STATUS_REVOKED, auth.getAuthStatus());
|
||||
assertEquals("REAL_DMS_MUTATION_COUNT must stay 0", 0, mockServer.getPasswordDeletePostCounter());
|
||||
}
|
||||
|
||||
/**
|
||||
* FIX-01: 无授权记录的住宿终止安全返回,不制造噪音操作。
|
||||
*/
|
||||
@Test
|
||||
public void testFix01_AccommodationTerminationRevoke_NoAuthIsSettled() {
|
||||
TrainAccommodation acc = new TrainAccommodation();
|
||||
acc.setAccommodationId(230L);
|
||||
acc.setEnrollmentId(23L);
|
||||
acc.setDormId("DORM-201");
|
||||
acc.setAccommodationStatus(2);
|
||||
accommodationDb.put(230L, acc);
|
||||
|
||||
int opCountBefore = operationDb.size();
|
||||
BatchReconciliationResult batch = orchestrator.revokeAuthorizationsForAccommodation(230L, "AccommodationCheckedOut", "admin");
|
||||
|
||||
assertEquals(BatchReconciliationResult.OUTCOME_ALL_SETTLED, batch.getOutcome());
|
||||
assertEquals(opCountBefore, operationDb.size());
|
||||
assertEquals(0, mockServer.getPasswordDeletePostCounter());
|
||||
}
|
||||
|
||||
/* ==================== 辅助 Mock 工厂方法 ==================== */
|
||||
|
||||
private TrainDormAuth getAuthById(Long authId) {
|
||||
|
||||
+217
@@ -541,8 +541,225 @@ public class AccommodationServiceTest
|
||||
assertNotNull(accommodationService.selectTrainAccommodationByAccommodationId(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* FIX-01: 退宿必须进入住宿关系终止撤权路径,而不是走 reconcile(enrollmentId)。
|
||||
* 退宿后已无有效住宿,reconcile 只会返回 WAITING_FOR_PREREQUISITE,旧授权将永远不被撤销。
|
||||
*/
|
||||
@Test
|
||||
public void testFix01_CheckOutTriggersAccommodationScopedRevoke() throws Exception
|
||||
{
|
||||
RecordingOrchestrator recorder = new RecordingOrchestrator();
|
||||
setField(accommodationService, "accessAuthorizationOrchestrator", recorder);
|
||||
|
||||
TrainAccommodation acc = new TrainAccommodation();
|
||||
acc.setEnrollmentId(1001L);
|
||||
acc.setDormId("DORM_101");
|
||||
acc.setBedNo("1号床");
|
||||
accommodationService.insertTrainAccommodation(acc);
|
||||
Long accId = acc.getAccommodationId();
|
||||
accommodationService.checkIn(accId, "operator1");
|
||||
|
||||
recorder.reset();
|
||||
accommodationService.checkOut(accId, "operator2");
|
||||
|
||||
assertEquals("退宿必须触发住宿关系终止撤权", 1, recorder.revokeCallCount);
|
||||
assertEquals(accId, recorder.lastRevokedAccommodationId);
|
||||
assertEquals("退宿不得再依赖 reconcile 推进撤权", 0, recorder.reconcileCallCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* FIX-01: 撤权推进失败不得回滚退宿事实,退宿本身必须成功。
|
||||
*/
|
||||
@Test
|
||||
public void testFix01_CheckOutSucceedsEvenWhenRevokeFails() throws Exception
|
||||
{
|
||||
RecordingOrchestrator recorder = new RecordingOrchestrator();
|
||||
recorder.throwOnRevoke = true;
|
||||
setField(accommodationService, "accessAuthorizationOrchestrator", recorder);
|
||||
|
||||
TrainAccommodation acc = new TrainAccommodation();
|
||||
acc.setEnrollmentId(1001L);
|
||||
acc.setDormId("DORM_101");
|
||||
acc.setBedNo("1号床");
|
||||
accommodationService.insertTrainAccommodation(acc);
|
||||
Long accId = acc.getAccommodationId();
|
||||
|
||||
accommodationService.checkOut(accId, "operator2");
|
||||
assertEquals(TrainAccommodation.STATUS_CHECKED_OUT,
|
||||
accommodationService.selectTrainAccommodationByAccommodationId(accId).getAccommodationStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* FIX-01: 调房不得被退宿撤权路径影响。transfer 内部不走 checkOut,
|
||||
* 旧住宿授权仍由新房授权生效后的对账负责撤销 (GRANT_NEW_THEN_REVOKE_OLD)。
|
||||
*/
|
||||
@Test
|
||||
public void testFix01_TransferDoesNotUseCheckoutRevokePath() throws Exception
|
||||
{
|
||||
RecordingOrchestrator recorder = new RecordingOrchestrator();
|
||||
setField(accommodationService, "accessAuthorizationOrchestrator", recorder);
|
||||
|
||||
TrainAccommodation acc = new TrainAccommodation();
|
||||
acc.setEnrollmentId(1001L);
|
||||
acc.setDormId("DORM_101");
|
||||
acc.setBedNo("1号床");
|
||||
accommodationService.insertTrainAccommodation(acc);
|
||||
accommodationService.checkIn(acc.getAccommodationId(), "operator1");
|
||||
|
||||
recorder.reset();
|
||||
TrainAccommodation newAcc = accommodationService.transfer(acc.getAccommodationId(), "DORM_101", "2号床", "admin");
|
||||
|
||||
assertNotNull(newAcc.getAccommodationId());
|
||||
assertEquals("调房绝不得触发退宿撤权路径", 0, recorder.revokeCallCount);
|
||||
assertEquals("调房仍由新住宿对账驱动", 1, recorder.reconcileCallCount);
|
||||
assertEquals(TrainAccommodation.STATUS_CHECKED_OUT,
|
||||
accommodationService.selectTrainAccommodationByAccommodationId(acc.getAccommodationId()).getAccommodationStatus());
|
||||
assertEquals(TrainAccommodation.STATUS_CHECKED_IN, newAcc.getAccommodationStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* FIX-04: 通用 update 不得修改住宿生命周期状态。
|
||||
*/
|
||||
@Test
|
||||
public void testFix04_GenericUpdateCannotChangeAccommodationStatus()
|
||||
{
|
||||
TrainAccommodation acc = new TrainAccommodation();
|
||||
acc.setEnrollmentId(1001L);
|
||||
acc.setDormId("DORM_101");
|
||||
acc.setBedNo("1号床");
|
||||
accommodationService.insertTrainAccommodation(acc);
|
||||
Long accId = acc.getAccommodationId();
|
||||
|
||||
TrainAccommodation update = new TrainAccommodation();
|
||||
update.setAccommodationId(accId);
|
||||
update.setAccommodationStatus(TrainAccommodation.STATUS_CHECKED_OUT);
|
||||
try
|
||||
{
|
||||
accommodationService.updateTrainAccommodation(update);
|
||||
fail("通用 update 修改住宿状态未被拦截");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertTrue(e.getMessage().contains("专属业务操作"));
|
||||
}
|
||||
assertEquals(TrainAccommodation.STATUS_PREALLOCATED,
|
||||
accommodationService.selectTrainAccommodationByAccommodationId(accId).getAccommodationStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* FIX-04: 通用 update 传入与现状相同的状态时放行,但绝不下发状态列。
|
||||
*/
|
||||
@Test
|
||||
public void testFix04_GenericUpdateWithUnchangedStatusDoesNotWriteStatusColumn()
|
||||
{
|
||||
TrainAccommodation acc = new TrainAccommodation();
|
||||
acc.setEnrollmentId(1001L);
|
||||
acc.setDormId("DORM_101");
|
||||
acc.setBedNo("1号床");
|
||||
accommodationService.insertTrainAccommodation(acc);
|
||||
Long accId = acc.getAccommodationId();
|
||||
|
||||
TrainAccommodation update = new TrainAccommodation();
|
||||
update.setAccommodationId(accId);
|
||||
update.setAccommodationStatus(TrainAccommodation.STATUS_PREALLOCATED);
|
||||
update.setRemark("仅修改备注");
|
||||
accommodationService.updateTrainAccommodation(update);
|
||||
|
||||
// 状态字段在下发 Mapper 前被清空,SQL 的 <if test="accommodationStatus != null"> 因此永不命中,
|
||||
// 通用更新路径在任何情况下都不会写 accommodation_status 列
|
||||
assertNull("通用更新路径不得携带状态列下发", update.getAccommodationStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* FIX-04: 住宿状态白名单守卫,"3 已调房"等不存在的状态与任意非法整数一律拒绝入库。
|
||||
*/
|
||||
@Test
|
||||
public void testFix04_IllegalAccommodationStatusIsRejectedOnInsert()
|
||||
{
|
||||
for (Integer illegal : new Integer[]{3, 4, 99, -1})
|
||||
{
|
||||
TrainAccommodation acc = new TrainAccommodation();
|
||||
acc.setEnrollmentId(1001L);
|
||||
acc.setDormId("DORM_101");
|
||||
acc.setBedNo("1号床");
|
||||
acc.setAccommodationStatus(illegal);
|
||||
try
|
||||
{
|
||||
accommodationService.insertTrainAccommodation(acc);
|
||||
fail("非法住宿状态 " + illegal + " 未被拦截");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertTrue(e.getMessage().contains("住宿状态不合法"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FIX-04: 新增住宿不得直接置为已退宿。
|
||||
*/
|
||||
@Test
|
||||
public void testFix04_InsertCannotDirectlyCreateCheckedOutRecord()
|
||||
{
|
||||
TrainAccommodation acc = new TrainAccommodation();
|
||||
acc.setEnrollmentId(1001L);
|
||||
acc.setDormId("DORM_101");
|
||||
acc.setBedNo("1号床");
|
||||
acc.setAccommodationStatus(TrainAccommodation.STATUS_CHECKED_OUT);
|
||||
try
|
||||
{
|
||||
accommodationService.insertTrainAccommodation(acc);
|
||||
fail("新增即退宿未被拦截");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertTrue(e.getMessage().contains("不能直接置为已退宿"));
|
||||
}
|
||||
}
|
||||
|
||||
// ================================= 内部 Mock 类 =================================
|
||||
|
||||
/**
|
||||
* 记录型编排器:只统计调用,不产生任何外部副作用 (REAL_DMS_MUTATION_COUNT = 0)。
|
||||
*/
|
||||
private static class RecordingOrchestrator extends com.ruoyi.train.integration.dms.orchestration.AccessAuthorizationOrchestrator
|
||||
{
|
||||
private int revokeCallCount = 0;
|
||||
private int reconcileCallCount = 0;
|
||||
private Long lastRevokedAccommodationId = null;
|
||||
private boolean throwOnRevoke = false;
|
||||
|
||||
private void reset()
|
||||
{
|
||||
revokeCallCount = 0;
|
||||
reconcileCallCount = 0;
|
||||
lastRevokedAccommodationId = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.ruoyi.train.integration.dms.orchestration.BatchReconciliationResult revokeAuthorizationsForAccommodation(
|
||||
Long accommodationId, String reason, String operator)
|
||||
{
|
||||
revokeCallCount++;
|
||||
lastRevokedAccommodationId = accommodationId;
|
||||
if (throwOnRevoke)
|
||||
{
|
||||
throw new IllegalStateException("模拟撤权推进失败");
|
||||
}
|
||||
com.ruoyi.train.integration.dms.orchestration.BatchReconciliationResult batch =
|
||||
new com.ruoyi.train.integration.dms.orchestration.BatchReconciliationResult(null);
|
||||
batch.setOutcome(com.ruoyi.train.integration.dms.orchestration.BatchReconciliationResult.OUTCOME_ALL_SETTLED);
|
||||
return batch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.ruoyi.train.integration.dms.orchestration.ReconciliationResult reconcile(Long enrollmentId)
|
||||
{
|
||||
reconcileCallCount++;
|
||||
return com.ruoyi.train.integration.dms.orchestration.ReconciliationResult.noAction("test stub");
|
||||
}
|
||||
}
|
||||
|
||||
private static class MockAccommodationMapper implements TrainAccommodationMapper
|
||||
{
|
||||
private final Map<Long, TrainAccommodation> store = new HashMap<>();
|
||||
|
||||
Reference in new issue
Block a user