From fb5834bb1e1da11341078a04a8ff3d4fc5ecd9bc Mon Sep 17 00:00:00 2001 From: wangzhibiao Date: Tue, 22 Sep 2026 17:04:50 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=83=BD=E5=8A=9B=EF=BC=9A?= =?UTF-8?q?=E7=BB=9F=E4=B8=80DMS=E7=A6=81=E7=94=A8=E6=80=81=E9=97=A8?= =?UTF-8?q?=E7=A6=81=E8=83=BD=E5=8A=9B=E5=A3=B0=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit capabilities 中 manualRevokeAvailable 此前硬编码为 true,在 dms.enabled=false 时 仍向用户表示真实外部门禁撤权可执行。 - manualRevokeAvailable 与 dms.enabled 保持一致 - 新增 localRevokeGovernanceAvailable 与 revokeMode,明确区分外部派发与本地排队治理, 本地授权状态治理与安全补偿路径继续可用,撤权入口不被移除 - 前端门禁能力默认按外部通道不可用初始化,接口返回前不向用户承诺不存在的能力 - 门禁服务未启用时注销确认文案如实说明仅完成本地待注销登记 Co-authored-by: Cursor --- .../TrainAccessManagementController.java | 8 +++- ...ationAndAuthApiControllerContractTest.java | 44 +++++++++++++++++++ build_ui/src/views/train/auth/index.vue | 15 +++++-- 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainAccessManagementController.java b/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainAccessManagementController.java index dfeb1ca46..f118b446b 100644 --- a/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainAccessManagementController.java +++ b/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainAccessManagementController.java @@ -224,7 +224,13 @@ public class TrainAccessManagementController extends BaseController Map 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); } diff --git a/RuoYi-Vue/ruoyi-admin/src/test/java/com/ruoyi/web/controller/train/AccommodationAndAuthApiControllerContractTest.java b/RuoYi-Vue/ruoyi-admin/src/test/java/com/ruoyi/web/controller/train/AccommodationAndAuthApiControllerContractTest.java index e6fb5df42..2305a5f98 100644 --- a/RuoYi-Vue/ruoyi-admin/src/test/java/com/ruoyi/web/controller/train/AccommodationAndAuthApiControllerContractTest.java +++ b/RuoYi-Vue/ruoyi-admin/src/test/java/com/ruoyi/web/controller/train/AccommodationAndAuthApiControllerContractTest.java @@ -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 disabled = + (java.util.Map) 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 enabled = + (java.util.Map) 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 missing = + (java.util.Map) controller.getCapabilities().get("data"); + assertEquals(Boolean.FALSE, missing.get("manualRevokeAvailable")); + } } diff --git a/build_ui/src/views/train/auth/index.vue b/build_ui/src/views/train/auth/index.vue index 98626291b..27968dc57 100644 --- a/build_ui/src/views/train/auth/index.vue +++ b/build_ui/src/views/train/auth/index.vue @@ -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)