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
new file mode 100644
index 000000000..b4533247c
--- /dev/null
+++ b/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainAccessManagementController.java
@@ -0,0 +1,245 @@
+package com.ruoyi.web.controller.train;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.train.domain.TrainDormAuth;
+import com.ruoyi.train.domain.TrainExternalOperation;
+import com.ruoyi.train.domain.TrainExternalResourceMapping;
+import com.ruoyi.train.integration.dms.orchestration.AccessAuthorizationOrchestrator;
+import com.ruoyi.train.integration.dms.orchestration.ReconciliationResult;
+import com.ruoyi.train.service.IExternalOperationService;
+import com.ruoyi.train.service.IExternalResourceMappingService;
+import com.ruoyi.train.service.ITrainDormAuthService;
+
+/**
+ * 宿舍门禁与外部资源映射管理端Controller (Wave 3B)
+ *
+ * 安全红线:
+ * 1. 严格限制管理员权限访问 (train:access:manage / train:access:query);
+ * 2. 严禁提供远程开锁 (Remote Unlock) 入口;
+ * 3. 严禁向前端返回任何明文密码 (password)、Token 或 Secret;
+ * 4. 手动对账严格遵循领域业务前置条件校验,绝不提供绕过前置条件的强开接口。
+ *
+ * @author ruoyi
+ * @date 2026-09-16
+ */
+@RestController
+@RequestMapping("/train/access")
+public class TrainAccessManagementController extends BaseController
+{
+ private static final String DEFAULT_VENDOR = "DMS";
+
+ @Autowired
+ private IExternalResourceMappingService externalResourceMappingService;
+
+ @Autowired
+ private ITrainDormAuthService trainDormAuthService;
+
+ @Autowired
+ private IExternalOperationService externalOperationService;
+
+ @Autowired
+ private AccessAuthorizationOrchestrator accessAuthorizationOrchestrator;
+
+ /**
+ * 查询指定宿舍的外部资源映射状态(包含房间映射、门锁映射及主锁配置健康度)
+ */
+ @PreAuthorize("@ss.hasPermi('train:access:query')")
+ @GetMapping("/mapping/dorm/{dormId}")
+ public AjaxResult getDormMapping(@PathVariable("dormId") String dormId)
+ {
+ TrainExternalResourceMapping roomMapping = externalResourceMappingService.getActiveRoomMapping(DEFAULT_VENDOR, dormId);
+ List lockMappings = externalResourceMappingService.getActiveLockMappings(DEFAULT_VENDOR, dormId);
+ if (lockMappings == null)
+ {
+ lockMappings = new ArrayList<>();
+ }
+
+ long primaryCount = lockMappings.stream()
+ .filter(l -> l.getIsPrimary() != null && l.getIsPrimary() == 1)
+ .count();
+
+ Map data = new HashMap<>();
+ data.put("dormId", dormId);
+ data.put("vendorCode", DEFAULT_VENDOR);
+ data.put("roomMapping", roomMapping);
+ data.put("lockMappings", lockMappings);
+ data.put("lockCount", lockMappings.size());
+ data.put("primaryLockCount", primaryCount);
+ data.put("hasPrimaryLock", primaryCount == 1);
+ data.put("isConfigValid", primaryCount == 1 && roomMapping != null);
+
+ if (primaryCount == 0)
+ {
+ data.put("configError", "未配置主门锁 (0 primary lock),门禁凭据无法自动下发");
+ }
+ else if (primaryCount > 1)
+ {
+ data.put("configError", "配置异常:存在多把主门锁 (" + primaryCount + " primary locks),违反单一主锁最少特权原则");
+ }
+ else if (roomMapping == null)
+ {
+ data.put("configError", "未绑定外部房间映射 (No active room mapping)");
+ }
+ else
+ {
+ data.put("configError", null);
+ }
+
+ return success(data);
+ }
+
+ /**
+ * 绑定或更新宿舍外部房间映射
+ */
+ @PreAuthorize("@ss.hasPermi('train:access:manage')")
+ @PostMapping("/mapping/bind-room")
+ public AjaxResult bindRoomMapping(@RequestBody TrainExternalResourceMapping mapping)
+ {
+ if (mapping.getVendorCode() == null || mapping.getVendorCode().trim().isEmpty())
+ {
+ mapping.setVendorCode(DEFAULT_VENDOR);
+ }
+ mapping.setCreateBy(getUsernameSafe());
+ TrainExternalResourceMapping result = externalResourceMappingService.bindRoomMapping(mapping);
+ return success(result);
+ }
+
+ /**
+ * 绑定或新增宿舍外部门锁映射 (支持一房多锁)
+ */
+ @PreAuthorize("@ss.hasPermi('train:access:manage')")
+ @PostMapping("/mapping/bind-lock")
+ public AjaxResult bindLockMapping(@RequestBody TrainExternalResourceMapping mapping)
+ {
+ if (mapping.getVendorCode() == null || mapping.getVendorCode().trim().isEmpty())
+ {
+ mapping.setVendorCode(DEFAULT_VENDOR);
+ }
+ mapping.setCreateBy(getUsernameSafe());
+ TrainExternalResourceMapping result = externalResourceMappingService.bindLockMapping(mapping);
+ return success(result);
+ }
+
+ /**
+ * 解除/注销指定的外部资源映射
+ */
+ @PreAuthorize("@ss.hasPermi('train:access:manage')")
+ @DeleteMapping("/mapping/{mappingId}")
+ public AjaxResult unbindMapping(@PathVariable("mappingId") Long mappingId)
+ {
+ boolean ok = externalResourceMappingService.deactivateMapping(mappingId, getUsernameSafe());
+ return toAjax(ok ? 1 : 0);
+ }
+
+ /**
+ * 查询门禁授权列表 (支持按学员、宿舍、状态筛选)
+ */
+ @PreAuthorize("@ss.hasPermi('train:access:query')")
+ @GetMapping("/auth/list")
+ public TableDataInfo listAuth(TrainDormAuth trainDormAuth)
+ {
+ startPage();
+ List list = trainDormAuthService.selectTrainDormAuthList(trainDormAuth);
+ return getDataTable(list);
+ }
+
+ /**
+ * 获取指定门禁授权详细信息
+ */
+ @PreAuthorize("@ss.hasPermi('train:access:query')")
+ @GetMapping("/auth/{authId}")
+ public AjaxResult getAuthDetail(@PathVariable("authId") Long authId)
+ {
+ TrainDormAuth auth = trainDormAuthService.selectTrainDormAuthByAuthId(authId);
+ return success(auth);
+ }
+
+ /**
+ * 查询指定门禁授权下的外部操作流水历史 (全要素脱敏返回,绝不泄漏密码或凭据)
+ */
+ @PreAuthorize("@ss.hasPermi('train:access:query')")
+ @GetMapping("/operations/{doorAuthId}")
+ public AjaxResult getOperationHistory(@PathVariable("doorAuthId") Long doorAuthId)
+ {
+ List operations = externalOperationService.listOperationsByDoorAuthId(doorAuthId);
+ List