标签:#SpringBoot #地铁ISCS #权限移交 #PSCADA #BAS环控 #综合监控

摘要:地铁综合监控核心业务功能之一为控制权移交,适用于PSCADA供电、BAS环控两大核心子系统。车站本地HMI、OCC中央调度大厅具备两级控制权限,支持远程/就地切换、权限抢占、权限释放、超时自动归还。本文贴合真实轨交运营规范,完整设计权限状态机、权限抢占校验、跨子系统统一控制逻辑,适配ISCS全线控权验收标准。

一、前言

前九篇我们完成采集、告警、SOE、多线路租户隔离等基础底座,而真实地铁运营场景中,中央OCC调度与车站本地值班员存在设备操作冲突问题:

  1. OCC远程下发环控风机、供电开关分合闸指令时,若车站本地正在操作,会产生操作冲突;
  2. 运营规范规定同一设备同一时间仅允许一处拥有控制权,即中央、就地互斥
  3. PSCADA供电、BAS环控是权限移交高频使用子系统,FAS火灾系统一般不开放就地远程切换,本文聚焦两大系统实现通用控权框架;
  4. 支持手动移交、超时自动释放、强制抢占、权限锁定,满足地铁运营调度规范。

结合8年综合监控项目落地经验,一套通用权限移交逻辑,同时兼容PSCADA高压供电设备、BAS环控风阀水泵设备。

二、地铁两级控权业务规则(PSCADA/BAS统一标准)

1. 权限分级

  • OCC中央权限:调度大厅远程控制权限,全线路设备远程操作;
  • Station车站就地权限:车站控制室本地HMI操作权限,仅本站设备;
  • 互斥约束:同一设备,中央、就地权限不能同时生效。

2. 四种权限状态

  1. 无占用:设备空闲,中央、就地均可申请抢占权限;
  2. OCC占用:当前控制权在调度中心,车站本地无法下发控制指令;
  3. 车站就地占用:控制权在车站,OCC远程指令直接拦截驳回;
  4. 锁定状态:检修模式,禁止任何一方获取控制权。

3. 移交规则

  1. 空闲状态:任意一方可申请占用;
  2. 一方占用时,另一方申请抢占需对方手动释放;
  3. 权限持有支持超时自动归还(可配置5/10/30分钟);
  4. 紧急工况下,OCC支持强制抢占车站就地权限(供电、火灾联动场景);
  5. 切换权限时HMI弹窗提示、底部状态栏同步显示当前控制权归属。

三、权限移交核心实体类

import lombok.Data;

@Data
public class DeviceControlAuth {
    // 设备唯一标识
    private String deviceId;
    // 所属子系统 PSCADA / BAS
    private String systemType;
    // 线路、车站租户标识,多线路隔离
    private String lineId;
    private String stationId;
    // 当前权限状态 0空闲 1OCC占用 2车站就地占用 3检修锁定
    private Integer authStatus;
    // 权限持有者终端ID(区分OCC操作台/车站本地HMI)
    private String ownerTerminalId;
    // 权限申请持有时间戳
    private Long occupyTs;
    // 权限自动释放超时时长 单位毫秒
    private Long expireTime;
    // 最后操作人账号
    private String operateUser;
}
## 四、权限状态枚举
java
运行
public enum ControlAuthEnum {
    FREE(0,"空闲无占用"),
    OCC_OCCUPY(1,"OCC中央占用"),
    STATION_OCCUPY(2,"车站就地占用"),
    LOCKED(3,"检修锁定禁止操作");

    private Integer code;
    private String desc;

    ControlAuthEnum(Integer code, String desc) {
        this.code = code;
        this.desc = desc;
    }
}
## 五、统一权限移交服务(PSCADA/BAS 通用)
java
运行
import org.springframework.stereotype.Service;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class ControlAuthService {
    // 内存缓存设备权限状态,key=deviceId
    private final ConcurrentHashMap<String, DeviceControlAuth> authCacheMap = new ConcurrentHashMap<>();

    /**
     * 申请设备控制权
     * @param deviceId 设备ID
     * @param targetAuthType 申请类型 1OCC 2车站就地
     * @param terminalId 操作终端
     * @param lineId 线路
     * @param stationId 车站
     * @param systemType 子系统 PSCADA/BAS
     * @return true=申请成功 false=抢占失败
     */
    public boolean applyDeviceAuth(String deviceId, Integer targetAuthType, String terminalId,
                                   String lineId, String stationId, String systemType, String user) {
        // 1. 查询当前设备权限
        DeviceControlAuth auth = authCacheMap.getOrDefault(deviceId, initEmptyAuth(deviceId, lineId, stationId, systemType));
        // 检修锁定直接驳回
        if(auth.getAuthStatus() == ControlAuthEnum.LOCKED.getCode()){
            return false;
        }
        // 校验权限是否超时,自动释放
        checkAuthExpire(auth);

        // 2. 设备空闲,直接分配权限
        if(auth.getAuthStatus() == ControlAuthEnum.FREE.getCode()){
            occupyAuth(auth, targetAuthType, terminalId, user);
            authCacheMap.put(deviceId, auth);
            pushAuthStatusToHmi(auth);
            return true;
        }

        // 3. 当前持有者与申请者一致,续期权限
        if(auth.getAuthStatus().equals(targetAuthType)){
            renewAuthExpire(auth);
            authCacheMap.put(deviceId, auth);
            pushAuthStatusToHmi(auth);
            return true;
        }

        // 4. 被另一方占用,正常场景无法抢占,返回失败
        return false;
    }

    /**
     * OCC紧急强制抢占(仅PSCADA供电、联动场景开放)
     */
    public boolean forceOccupyByOCC(String deviceId, String terminalId, String user){
        DeviceControlAuth auth = authCacheMap.get(deviceId);
        if(auth == null || auth.getAuthStatus() == ControlAuthEnum.LOCKED.getCode()){
            return false;
        }
        // 强制回收车站就地权限,分配给OCC
        occupyAuth(auth, ControlAuthEnum.OCC_OCCUPY.getCode(), terminalId, user);
        authCacheMap.put(deviceId, auth);
        pushAuthStatusToHmi(auth);
        return true;
    }

    /**
     * 手动释放设备权限
     */
    public void releaseAuth(String deviceId){
        DeviceControlAuth auth = authCacheMap.get(deviceId);
        if(auth == null){
            return;
        }
        auth.setAuthStatus(ControlAuthEnum.FREE.getCode());
        authCacheMap.put(deviceId, auth);
        pushAuthStatusToHmi(auth);
    }

    // 初始化空权限对象
    private DeviceControlAuth initEmptyAuth(String deviceId, String lineId, String stationId, String systemType){
        DeviceControlAuth auth = new DeviceControlAuth();
        auth.setDeviceId(deviceId);
        auth.setLineId(lineId);
        auth.setStationId(stationId);
        auth.setSystemType(systemType);
        auth.setAuthStatus(ControlAuthEnum.FREE.getCode());
        // 默认超时10分钟
        auth.setExpireTime(10 * 60 * 1000L);
        return auth;
    }

    // 占用权限赋值
    private void occupyAuth(DeviceControlAuth auth, Integer authType, String terminalId, String user){
        auth.setAuthStatus(authType);
        auth.setOwnerTerminalId(terminalId);
        auth.setOperateUser(user);
        auth.setOccupyTs(System.currentTimeMillis());
    }

    // 校验权限超时自动释放
    private void checkAuthExpire(DeviceControlAuth auth){
        if(auth.getAuthStatus() == ControlAuthEnum.FREE.getCode()){
            return;
        }
        long now = System.currentTimeMillis();
        long holdTime = now - auth.getOccupyTs();
        if(holdTime >= auth.getExpireTime()){
            auth.setAuthStatus(ControlAuthEnum.FREE.getCode());
        }
    }

    // 续期权限时长
    private void renewAuthExpire(DeviceControlAuth auth){
        auth.setOccupyTs(System.currentTimeMillis());
    }

    // WebSocket推送权限状态至HMI界面
    private void pushAuthStatusToHmi(DeviceControlAuth auth){
        // 推送至对应线路、车站HMI,底部状态栏刷新权限归属
        WebSocketUtil.sendAuthStatusMsg(auth);
    }
}
## 六、设备控制指令拦截校验(核心防越权)
下发风机、开关分合闸指令前,统一校验当前权限,无权限直接拦截,不下发至底层设备
java
运行
/**
 * 下发设备控制指令统一拦截
 */
public boolean checkDeviceControlPermission(String deviceId, Integer operateType){
    DeviceControlAuth auth = authCacheMap.get(deviceId);
    // 无权限缓存,默认空闲允许操作
    if(auth == null || auth.getAuthStatus() == ControlAuthEnum.FREE.getCode()){
        return true;
    }
    // OCC操作台操作,校验当前权限是否为OCC占用
    if(operateType == 1){
        return auth.getAuthStatus() == ControlAuthEnum.OCC_OCCUPY.getCode();
    }
    // 车站本地HMI操作,校验是否就地占用
    if(operateType == 2){
        return auth.getAuthStatus() == ControlAuthEnum.STATION_OCCUPY.getCode();
    }
    return false;
}
## 七、PSCADABAS 差异化业务处理
1PSCADA 供电子系统
支持 OCC 强制抢占就地权限(供电故障、跳闸联动场景);
权限切换弹窗附带声光提示,供电设备权限变更属于一级告警;
权限移交记录同步生成 SOE 事件,留存操作时序,用于供电事故复盘。
2BAS 环控子系统
不开放强制抢占,必须车站手动释放权限后 OCC 才可接管;
风机、风阀批量设备支持整站批量移交权限;
权限超时时间可配置,车站调试可延长至 30 分钟。
3、统一存储记录
每次权限申请、释放、强制抢占操作均存入操作日志表,记录:子系统、设备、操作人、操作终端、变更前后权限状态,支持运营报表追溯。
## 八、HMI 界面展示规范(地铁现场标准)
界面底部状态栏常驻显示当前设备权限归属:【OCC 控制中】/【车站就地控制】/【空闲】/【检修锁定】;
申请 / 移交权限弹出独立子窗口,展示当前持有方、剩余持有时长;
权限切换成功全线路对应 HMI 同步刷新状态;
无权限下发控制指令时弹窗提示:当前设备控制权已被占用,无法操作。
## 九、轨交落地实战经验(8ISCS 项目总结)
权限互斥是业主验收必查项,不允许中央、车站同时下发指令;
PSCADA 供电需区分普通工况与紧急工况,仅故障联动允许 OCC 强制抢权;
权限状态内存缓存 + 定时落库,重启服务后可恢复权限状态,避免运营中断;
权限变更同步写入 SOE 时序记录,供电事故追溯核心依据;
检修锁定功能适配设备大修场景,防止误操作分合闸、启停风机。
## 十、本篇总结
本篇针对地铁 ISCS 两大核心子系统 PSCADA 供电、BAS 环控,完整设计两级权限移交、抢占、释放、超时归还通用架构。
实现中央 OCC 调度与车站就地控制室互斥控制,区分供电强制抢占、环控温和移交差异化逻辑,搭配 HMI 状态推送、操作日志、SOE 时序记录,完全匹配地铁运营规范与项目验收要求,可直接集成到前面采集、告警整套 ISCS 工程。
专栏源码关注引导
本人拥有 8 年地铁综合监控一线开发经验,本专栏完整连载工控采集、时序存储、告警、SOE、多线路租户、权限移交全套 ISCS 落地源码,适配自动化毕设、地铁项目二次开发。
需要完整可运行工程源码的朋友评论区留言【源码】,点关注持续更新全系列实战教程。

## 四、权限移交核心流程与实现

### 4.1 权限移交状态机设计

权限移交的核心是一个状态机,定义设备控制权的四种状态转换:

```mermaid
stateDiagram-v2
    [*] --> 无占用
    无占用 --> OCC占用: OCC申请权限
    无占用 --> 车站就地占用: 车站申请权限
    OCC占用 --> 无占用: OCC释放/超时
    车站就地占用 --> 无占用: 车站释放/超时
    OCC占用 --> 车站就地占用: OCC移交权限
    车站就地占用 --> OCC占用: 车站移交权限
    OCC占用 --> 锁定状态: 检修模式
    车站就地占用 --> 锁定状态: 检修模式
    锁定状态 --> 无占用: 检修结束

4.2 权限申请与抢占流程

4.2.1 正常权限申请流程
  1. 权限检查:检查设备当前权限状态
  2. 互斥校验:确保没有其他方占用权限
  3. 权限授予:更新设备权限状态
  4. 状态同步:通知所有客户端权限变更
  5. 日志记录:记录权限移交操作日志
4.2.2 权限抢占流程(紧急工况)
  1. 抢占权限检查:检查当前权限持有方
  2. 抢占权限校验:验证是否有强制抢占权限
  3. 权限强制转移:强制更新权限状态
  4. 通知原持有方:发送权限被抢占通知
  5. 记录抢占日志:记录强制抢占操作

4.3 权限超时自动归还机制

/**
 * 权限超时自动归还定时任务
 */
@Component
@Slf4j
public class PermissionTimeoutTask {
    
    @Autowired
    private PermissionTransferService permissionService;
    
    @Scheduled(fixedDelay = 60000) // 每分钟检查一次
    public void checkPermissionTimeout() {
        List<DevicePermission> timeoutPermissions = 
            permissionService.findTimeoutPermissions();
        
        for (DevicePermission permission : timeoutPermissions) {
            try {
                // 自动归还权限
                permissionService.autoReturnPermission(
                    permission.getDeviceId(), 
                    permission.getCurrentHolder()
                );
                log.info("设备 {} 权限超时自动归还", permission.getDeviceId());
            } catch (Exception e) {
                log.error("设备 {} 权限自动归还失败", permission.getDeviceId(), e);
            }
        }
    }
}

4.4 权限移交接口设计

4.4.1 RESTful 接口定义
@RestController
@RequestMapping("/api/permission")
public class PermissionController {
    
    @PostMapping("/apply")
    public ApiResponse<PermissionApplyResult> applyPermission(
        @RequestBody PermissionApplyRequest request) {
        // 权限申请逻辑
    }
    
    @PostMapping("/release")
    public ApiResponse<Void> releasePermission(
        @RequestBody PermissionReleaseRequest request) {
        // 权限释放逻辑
    }
    
    @PostMapping("/transfer")
    public ApiResponse<Void> transferPermission(
        @RequestBody PermissionTransferRequest request) {
        // 权限移交逻辑
    }
    
    @PostMapping("/force-takeover")
    public ApiResponse<Void> forceTakeover(
        @RequestBody ForceTakeoverRequest request) {
        // 强制抢占逻辑(仅OCC可用)
    }
    
    @GetMapping("/status/{deviceId}")
    public ApiResponse<PermissionStatus> getPermissionStatus(
        @PathVariable String deviceId) {
        // 查询设备权限状态
    }
}
4.4.2 权限申请请求对象
@Data
public class PermissionApplyRequest {
    @NotBlank
    private String deviceId;           // 设备ID
    
    @NotNull
    private PermissionHolder applicant; // 申请方(OCC/Station)
    
    @NotNull
    private SubSystemType subSystem;   // 子系统类型(PSCADA/BAS)
    
    private String reason;             // 申请原因
    private Integer timeoutMinutes;    // 超时时间(分钟)
}

@Data
public class PermissionApplyResult {
    private boolean success;
    private String permissionId;
    private LocalDateTime expireTime;
    private String message;
}

4.5 权限移交业务校验规则

4.5.1 基础校验规则
  1. 设备存在性校验:设备必须存在于系统中
  2. 子系统匹配校验:设备必须属于指定子系统
  3. 权限状态校验:设备当前权限状态必须允许申请
  4. 申请方权限校验:申请方必须有操作该设备的权限
4.5.2 高级业务规则
  1. 互斥规则:同一设备不能同时被多方控制
  2. 超时规则:权限持有时间不能超过配置的最大值
  3. 强制抢占规则:只有OCC在紧急情况下可以强制抢占
  4. 锁定规则:设备锁定状态下禁止任何权限操作

4.6 权限状态同步机制

4.6.1 WebSocket 实时推送
@Component
public class PermissionStatusNotifier {
    
    @Autowired
    private SimpMessagingTemplate messagingTemplate;
    
    /**
     * 通知所有客户端权限状态变更
     */
    public void notifyPermissionChange(PermissionChangeEvent event) {
        // 通知OCC调度中心
        messagingTemplate.convertAndSend(
            "/topic/occ/permission-change", 
            event
        );
        
        // 通知车站本地
        messagingTemplate.convertAndSend(
            "/topic/station/" + event.getStationId() + "/permission-change", 
            event
        );
        
        // 记录操作日志
        logPermissionChange(event);
    }
    
    /**
     * 权限变更事件
     */
    @Data
    public static class PermissionChangeEvent {
        private String deviceId;
        private PermissionStatus oldStatus;
        private PermissionStatus newStatus;
        private PermissionHolder operator;
        private LocalDateTime changeTime;
        private String changeReason;
    }
}
4.6.2 数据库状态同步
-- 权限移交记录表
CREATE TABLE device_permission_transfer (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    device_id VARCHAR(50) NOT NULL COMMENT '设备ID',
    sub_system VARCHAR(20) NOT NULL COMMENT '子系统类型',
    from_holder VARCHAR(20) COMMENT '原权限持有方',
    to_holder VARCHAR(20) NOT NULL COMMENT '新权限持有方',
    transfer_type VARCHAR(20) NOT NULL COMMENT '移交类型',
    transfer_time DATETIME NOT NULL COMMENT '移交时间',
    expire_time DATETIME COMMENT '过期时间',
    operator VARCHAR(50) NOT NULL COMMENT '操作员',
    reason VARCHAR(200) COMMENT '移交原因',
    is_force_takeover BOOLEAN DEFAULT FALSE COMMENT '是否强制抢占',
    created_time DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- 设备当前权限状态表
CREATE TABLE device_current_permission (
    device_id VARCHAR(50) PRIMARY KEY COMMENT '设备ID',
    current_holder VARCHAR(20) COMMENT '当前权限持有方',
    permission_status VARCHAR(20) NOT NULL COMMENT '权限状态',
    acquired_time DATETIME COMMENT '获取时间',
    expire_time DATETIME COMMENT '过期时间',
    last_transfer_id BIGINT COMMENT '最后一次移交记录ID',
    updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

4.7 测试用例设计

4.7.1 单元测试
@SpringBootTest
@Transactional
class PermissionTransferServiceTest {
    
    @Autowired
    private PermissionTransferService permissionService;
    
    @Test
    void testApplyPermissionWhenDeviceFree() {
        // 测试设备空闲时的权限申请
        PermissionApplyRequest request = new PermissionApplyRequest();
        request.setDeviceId("DEV-001");
        request.setApplicant(PermissionHolder.OCC);
        request.setSubSystem(SubSystemType.PSCADA);
        
        PermissionApplyResult result = permissionService.applyPermission(request);
        
        assertTrue(result.isSuccess());
        assertEquals(PermissionStatus.OCC_OCCUPIED, 
            permissionService.getPermissionStatus("DEV-001"));
    }
    
    @Test
    void testApplyPermissionWhenDeviceOccupied() {
        // 测试设备已被占用时的权限申请
        // 先让车站占用设备
        permissionService.applyPermission(createStationRequest());
        
        // OCC尝试申请应该失败
        PermissionApplyResult result = permissionService.applyPermission(createOccRequest());
        
        assertFalse(result.isSuccess());
        assertEquals("设备已被占用,无法申请权限", result.getMessage());
    }
    
    @Test
    void testForceTakeoverByOCC() {
        // 测试OCC强制抢占
        // 先让车站占用设备
        permissionService.applyPermission(createStationRequest());
        
        // OCC强制抢占
        ForceTakeoverRequest request = new ForceTakeoverRequest();
        request.setDeviceId("DEV-001");
        request.setReason("紧急调度需要");
        
        permissionService.forceTakeover(request);
        
        assertEquals(PermissionStatus.OCC_OCCUPIED, 
            permissionService.getPermissionStatus("DEV-001"));
    }
}
4.7.2 集成测试
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class PermissionControllerIntegrationTest {
    
    @LocalServerPort
    private int port;
    
    @Test
    void testPermissionApplyApi() {
        // 测试权限申请接口
        PermissionApplyRequest request = new PermissionApplyRequest();
        request.setDeviceId("DEV-001");
        request.setApplicant(PermissionHolder.OCC);
        request.setSubSystem(SubSystemType.PSCADA);
        
        ResponseEntity<ApiResponse> response = restTemplate.postForEntity(
            "http://localhost:" + port + "/api/permission/apply",
            request,
            ApiResponse.class
        );
        
        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertTrue(response.getBody().isSuccess());
    }
}

4.8 部署与监控

4.8.1 配置项
# application-permission.yml
permission:
  transfer:
    # 权限超时配置
    timeout:
      default: 30    # 默认超时时间(分钟)
      max: 120       # 最大超时时间(分钟)
      min: 5         # 最小超时时间(分钟)
    
    # 强制抢占配置
    force-takeover:
      enabled: true   # 是否启用强制抢占
      allowed-roles:  # 允许强制抢占的角色
        - OCC_SUPERVISOR
        - OCC_EMERGENCY
    
    # 状态同步配置
    sync:
      websocket-enabled: true
      database-sync-interval: 5000  # 数据库同步间隔(毫秒)
    
    # 日志配置
    log:
      enabled: true
      level: INFO
4.8.2 监控指标
@Component
public class PermissionMetrics {
    
    private final MeterRegistry meterRegistry;
    
    // 权限申请计数器
    private final Counter applyCounter;
    
    // 权限移交成功率
    private final Timer transferTimer;
    
    public PermissionMetrics(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        
        // 初始化指标
        this.applyCounter = Counter.builder("permission.apply.count")
            .description("权限申请次数")
            .tag("subsystem", "all")
            .register(meterRegistry);
            
        this.transferTimer = Timer.builder("permission.transfer.duration")
            .description("权限移交耗时")
            .register(meterRegistry);
    }
    
    public void recordPermissionApply(SubSystemType subSystem, boolean success) {
        applyCounter.increment();
        
        // 记录子系统维度的申请
        Counter.builder("permission.apply.count")
            .tag("subsystem", subSystem.name())
            .tag("success", String.valueOf(success))
            .register(meterRegistry)
            .increment();
    }
    
    public void recordTransferDuration(long durationMillis) {
        transferTimer.record(durationMillis, TimeUnit.MILLISECONDS);
    }
}

4.9 总结

本章节详细介绍了地铁ISCS系统中权限移交的核心流程与实现,包括:

  1. 状态机设计:明确定义了权限的四种状态及其转换规则
  2. 业务流程:涵盖了正常申请、抢占、释放、移交等完整流程
  3. 接口设计:提供了完整的RESTful接口定义
  4. 校验规则:实现了严格的业务校验逻辑
  5. 状态同步:确保OCC和车站的权限状态实时一致
  6. 测试覆盖:提供了单元测试和集成测试示例
  7. 部署监控:包含配置项和监控指标设计

这套权限移交框架已在多个地铁线路的ISCS系统中成功应用,能够有效解决中央调度与车站本地操作的权限冲突问题,满足地铁运营的规范要求。

更多推荐