修复能力:统一DMS禁用态门禁能力声明

capabilities 中 manualRevokeAvailable 此前硬编码为 true,在 dms.enabled=false 时
仍向用户表示真实外部门禁撤权可执行。

- manualRevokeAvailable 与 dms.enabled 保持一致
- 新增 localRevokeGovernanceAvailable 与 revokeMode,明确区分外部派发与本地排队治理,
  本地授权状态治理与安全补偿路径继续可用,撤权入口不被移除
- 前端门禁能力默认按外部通道不可用初始化,接口返回前不向用户承诺不存在的能力
- 门禁服务未启用时注销确认文案如实说明仅完成本地待注销登记

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
wangzhibiaoandCursor committed 2026-09-22 17:04:50 +08:00
1 parent b7a9b89ec5
commit fb5834bb1e
3 files changed
+62 -5

No files matched your search

@@ -224,7 +224,13 @@ public class TrainAccessManagementController extends BaseController
Map<String, Object> data = new HashMap<>();
data.put("dmsEnabled", dmsEnabled);
data.put("manualIssueAvailable", dmsEnabled);
data.put("manualRevokeAvailable", true);
// FIX-07: 能力声明必须与真实可执行能力一致。DMS 禁用时无法向厂商发起真实撤权,
// 不得再硬编码 true 向用户表示外部门禁撤权可执行。
data.put("manualRevokeAvailable", dmsEnabled);
// 本地授权状态治理 (草稿注销 / 排队待注销) 不依赖外部通道,始终可用,
// 撤权入口因此保留,但语义由 revokeMode 明确区分,避免破坏安全补偿路径。
data.put("localRevokeGovernanceAvailable", true);
data.put("revokeMode", dmsEnabled ? "EXTERNAL_DISPATCH" : "LOCAL_QUEUE_ONLY");
return success(data);
}
@@ -137,5 +137,49 @@ public class AccommodationAndAuthApiControllerContractTest
Method revokeMethod = clazz.getMethod("revokeAuth", Long.class);
assertEquals("@ss.hasPermi('train:access:credential')", revokeMethod.getAnnotation(PreAuthorize.class).value());
}
/**
* FIX-07: 门禁能力声明必须与真实可执行能力一致。
* DMS 禁用时不得向用户表示真实外部门禁撤权可执行;
* 同时本地授权状态治理能力必须继续声明为可用,避免破坏安全补偿路径。
*/
@Test
public void testFix07_CapabilitiesMatchRealExecutableAbility() throws Exception
{
TrainAccessManagementController controller = new TrainAccessManagementController();
com.ruoyi.train.integration.dms.config.DmsProperties properties =
new com.ruoyi.train.integration.dms.config.DmsProperties();
java.lang.reflect.Field field = TrainAccessManagementController.class.getDeclaredField("dmsProperties");
field.setAccessible(true);
field.set(controller, properties);
// DMS 禁用态
properties.setEnabled(false);
@SuppressWarnings("unchecked")
java.util.Map<String, Object> disabled =
(java.util.Map<String, Object>) controller.getCapabilities().get("data");
assertEquals(Boolean.FALSE, disabled.get("dmsEnabled"));
assertEquals(Boolean.FALSE, disabled.get("manualIssueAvailable"));
assertEquals("DMS 禁用时严禁声明外部撤权可执行", Boolean.FALSE, disabled.get("manualRevokeAvailable"));
assertEquals("本地状态治理能力必须保留", Boolean.TRUE, disabled.get("localRevokeGovernanceAvailable"));
assertEquals("LOCAL_QUEUE_ONLY", disabled.get("revokeMode"));
// DMS 启用态
properties.setEnabled(true);
@SuppressWarnings("unchecked")
java.util.Map<String, Object> enabled =
(java.util.Map<String, Object>) controller.getCapabilities().get("data");
assertEquals(Boolean.TRUE, enabled.get("dmsEnabled"));
assertEquals(Boolean.TRUE, enabled.get("manualIssueAvailable"));
assertEquals(Boolean.TRUE, enabled.get("manualRevokeAvailable"));
assertEquals("EXTERNAL_DISPATCH", enabled.get("revokeMode"));
// 未注入配置时 fail closed
field.set(controller, null);
@SuppressWarnings("unchecked")
java.util.Map<String, Object> missing =
(java.util.Map<String, Object>) controller.getCapabilities().get("data");
assertEquals(Boolean.FALSE, missing.get("manualRevokeAvailable"));
}
}
+11 -4
View File
@@ -325,10 +325,13 @@ export default {
oneTimeDialogOpen: false,
oneTimeCredential: null,
maskedMode: true,
// 门禁能力默认按“外部通道不可用”初始化,避免接口返回前向用户承诺不存在的能力
dmsCapabilities: {
dmsEnabled: true,
manualIssueAvailable: true,
manualRevokeAvailable: true
dmsEnabled: false,
manualIssueAvailable: false,
manualRevokeAvailable: false,
localRevokeGovernanceAvailable: true,
revokeMode: 'LOCAL_QUEUE_ONLY'
}
}
},
@@ -449,7 +452,11 @@ export default {
handleRevoke(row) {
const studentDesc = row.studentName ? `${row.studentName} (${row.studentId || row.enrollmentId})` : (row.studentId || `报名ID:${row.enrollmentId}`)
const dormDesc = `${row.dormBuilding || ''} ${row.dormRoom || ''}`
this.$modal.confirm(`确定要注销学员【${studentDesc}】在宿舍【${dormDesc}】的门禁授权(授权ID: ${row.authId})吗?注销后该学员将无法通行该门禁。`).then(() => {
// 门禁服务未启用时只能完成本地授权状态治理,不得向用户承诺已完成真实门锁撤权
const revokeScopeTip = this.dmsCapabilities.manualRevokeAvailable
? '注销后该学员将无法通行该门禁。'
: '当前门禁服务未启用,本次操作仅会将授权置为待注销并登记撤权任务,真实门锁凭据需在门禁服务恢复后才能撤销。'
this.$modal.confirm(`确定要注销学员【${studentDesc}】在宿舍【${dormDesc}】的门禁授权(授权ID: ${row.authId})吗?${revokeScopeTip}`).then(() => {
return revokeAuth(row.authId)
}).then(res => {
const feedback = resolveRevokeFeedback(res.data)