Преглед на файлове

feat(cockpit): add current warnings (#39)

Kazerin преди 1 месец
родител
ревизия
739116c474

+ 236 - 0
docs/cockpit/current-warnings-demo.html

@@ -0,0 +1,236 @@
+<!doctype html>
+<html lang="zh-CN">
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <title>预警处置 / 当前预警</title>
+  <style>
+    :root { --ink:#16324f; --muted:#64748b; --line:#d9e1ea; --panel:#fff; --page:#edf2f7; --blue:#1470ef; }
+    * { box-sizing:border-box; }
+    body { margin:0; min-height:100vh; background:var(--page); color:var(--ink); font-family:"Microsoft YaHei",Arial,sans-serif; font-size:13px; }
+    header { display:flex; flex-wrap:wrap; gap:12px; justify-content:space-between; align-items:center; padding:14px 20px; background:var(--panel); border-bottom:1px solid var(--line); }
+    h1 { margin:0; font-size:18px; }
+    .tabs { display:inline-flex; border:1px solid var(--line); border-radius:5px; overflow:hidden; }
+    .tabs button { border:0; border-right:1px solid var(--line); padding:8px 16px; background:#f4f7fa; }
+    .tabs button.on { background:var(--blue); color:#fff; }
+    .meta { display:flex; gap:14px; color:var(--muted); flex-wrap:wrap; }
+    .meta b { color:var(--ink); font-weight:600; }
+    button { cursor:pointer; border:1px solid #b9c8d5; background:var(--panel); padding:7px 12px; border-radius:4px; }
+    button:hover { border-color:var(--blue); color:var(--blue); }
+    select { border:1px solid #b9c8d5; border-radius:4px; padding:6px 8px; background:#fff; }
+    main { display:grid; grid-template-columns:minmax(360px,440px) minmax(0,1fr); min-height:calc(100vh - 57px); }
+    aside { padding:16px 18px; background:var(--panel); border-right:1px solid var(--line); overflow:auto; }
+    section { display:grid; grid-template-rows:auto minmax(0,1fr) auto; min-width:0; }
+    h2 { margin:0 0 10px; font-size:14px; }
+    .filters { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-bottom:12px; }
+    .filters label { display:flex; flex-direction:column; gap:4px; color:var(--muted); font-size:12px; }
+    table { width:100%; border-collapse:collapse; font-size:12px; }
+    th,td { text-align:left; padding:6px 7px; border-bottom:1px solid #edf1f5; }
+    th { color:var(--muted); font-weight:600; }
+    .badge { display:inline-block; padding:1px 8px; border-radius:9px; border:1px solid currentColor; font-size:11px; }
+    .b-RED { color:#d0342c; background:#fdecea; } .b-YELLOW { color:#9a6b00; background:#fdf3d7; }
+    .b-BLUE { color:#1470ef; background:#e8f0fe; } .b-GREEN { color:#0f8a4d; background:#e5f5ec; }
+    .b-UNKNOWN { color:#64748b; background:#eef1f4; }
+    .toolbar { display:flex; flex-wrap:wrap; gap:10px; justify-content:space-between; align-items:center; padding:10px 18px; background:var(--panel); border-bottom:1px solid var(--line); }
+    .map { position:relative; background:#e2e9f0; background-image:linear-gradient(#d3dce5 1px,transparent 1px),linear-gradient(90deg,#d3dce5 1px,transparent 1px); background-size:56px 56px; }
+    .marker { position:absolute; width:15px; height:15px; margin:-7px 0 0 -7px; border-radius:50%; background:#9aa9bb; border:2px solid #fff; box-shadow:0 1px 3px rgba(16,24,40,.3); cursor:pointer; }
+    .marker.c-RED { background:#d0342c; } .marker.c-YELLOW { background:#f0a500; }
+    .marker.c-BLUE { background:#1470ef; } .marker.c-GREEN { background:#0f8a4d; }
+    .marker.c-UNKNOWN { background:#8a94a0; }
+    .marker.hot { outline:3px solid var(--ink); outline-offset:1px; }
+    .label { position:absolute; font-size:11px; transform:translate(9px,-7px); background:rgba(255,255,255,.92); padding:1px 5px; border-radius:3px; border:1px solid var(--line); pointer-events:none; }
+    .caption { padding:10px 18px; background:var(--panel); border-top:1px solid var(--line); color:var(--muted); }
+    .panel { margin-top:14px; border:1px solid var(--line); border-radius:5px; padding:10px 12px; min-height:120px; }
+    .panel h3 { margin:0 0 8px; font-size:13px; }
+    .panel ul { margin:0; padding-left:18px; }
+    .panel.failed { border-color:#d0342c; background:#fdf1f0; }
+    .legend { display:flex; flex-wrap:wrap; gap:12px; color:var(--muted); font-size:12px; }
+    .legend i { display:inline-block; width:10px; height:10px; border-radius:50%; margin-right:4px; }
+    .empty { color:var(--muted); padding:12px; border:1px dashed var(--line); border-radius:5px; }
+    @media (max-width:940px){ main{grid-template-columns:1fr;} .map{min-height:340px;} }
+  </style>
+</head>
+<body>
+<header>
+  <h1>预警处置 / 当前预警</h1>
+  <div class="tabs">
+    <button id="tabWARNING" class="on" type="button">当前预警</button>
+    <button id="tabALARM" type="button">当前报警</button>
+  </div>
+  <div class="meta"><span>事件 <b id="count">0</b></span><span>模式 <b id="modeName">当前预警</b></span><span>工单分区 <b id="woState">--</b></span><span>更新 <b id="updated">--</b></span></div>
+</header>
+<main>
+  <aside>
+    <h2>筛选</h2>
+    <div class="filters">
+      <label>等级
+        <select id="fLevel"><option value="">全部</option><option value="1">1 级(红)</option><option value="2">2 级(黄)</option><option value="3">3 级(蓝)</option><option value="4">4 级(绿)</option></select>
+      </label>
+      <label>行业
+        <select id="fIndustry"><option value="">全部行业</option><option value="GAS">燃气</option><option value="WATER_SUPPLY">供水</option><option value="DRAINAGE">排水</option></select>
+      </label>
+      <label>状态
+        <select id="fStatus"><option value="">全部状态</option><option value="ACTIVE">进行中</option><option value="PENDING">待处置</option><option value="PROCESSING">处理中</option><option value="RELEASED">已解除</option><option value="HANDLED">已处置</option></select>
+      </label>
+    </div>
+    <div class="toolbar" style="padding:0 0 12px; border-bottom:none">
+      <button id="apply" type="button">应用筛选</button>
+      <button id="reset" type="button">重置</button>
+      <button id="refresh" type="button">手动刷新</button>
+      <button id="toggleFail" type="button">模拟工单分区失败</button>
+    </div>
+
+    <h2>当前事件列表</h2>
+    <table><thead><tr><th>事件</th><th>等级</th><th>数值</th><th>设备</th><th>工单</th></tr></thead>
+      <tbody id="eventRows"></tbody></table>
+    <div class="empty" id="empty" style="display:none">当前筛选下没有事件(空列表状态)。</div>
+    <div class="panel" id="detail"><h3>详情</h3><p style="color:var(--muted)">点击事件行或地图标记查看设备与工单状态。</p></div>
+  </aside>
+  <section>
+    <div class="toolbar">
+      <strong>中央一张图</strong>
+      <div class="legend">
+        <span><i style="background:#d0342c"></i>1级红</span>
+        <span><i style="background:#f0a500"></i>2级黄</span>
+        <span><i style="background:#1470ef"></i>3级蓝</span>
+        <span><i style="background:#0f8a4d"></i>4级绿</span>
+        <span><i style="background:#8a94a0"></i>未知</span>
+      </div>
+    </div>
+    <div class="map" id="map"></div>
+    <div class="caption" id="caption">点击事件定位设备;不同等级用红黄蓝绿区分</div>
+  </section>
+</main>
+<script>
+  const COLOR = { 1: "RED", 2: "YELLOW", 3: "BLUE", 4: "GREEN" };
+  const TYPE = { GAS_SENSOR: "燃气浓度计", WATER_SUPPLY_SENSOR: "供水压力计", SEWAGE_ENV: "污水环境监测" };
+  const INDUSTRY = { GAS: "燃气", WATER_SUPPLY: "供水", DRAINAGE: "排水" };
+  const STATUS_NAME = { ACTIVE: "进行中", PENDING: "待处置", PROCESSING: "处理中", RELEASED: "已解除", HANDLED: "已处置" };
+  const WO_STATES = { 1: "待派单", 2: "待接单", 3: "已接单", 4: "处理中", 5: "待验收", 6: "已结案", 7: "已取消" };
+  const levelColor = level => (level >= 1 && level <= 4 ? COLOR[level] : "UNKNOWN");
+
+  const warnings = [
+    { no: "W-001", name: "燃气浓度预警", level: 1, status: "ACTIVE", value: "12.5", unit: "mg/m³", time: "09:50",
+      device: "D-GAS-1", deviceName: "燃气浓度计-001", location: "城区A-燃气1号点", type: "GAS_SENSOR", lon: 110.130, lat: 28.440,
+      wo: { code: "WO-1", state: 4, stateName: "处理中", desc: "燃气浓度处置" } },
+    { no: "W-002", name: "燃气压力预警", level: 2, status: "ACTIVE", value: "0.32", unit: "MPa", time: "09:45",
+      device: "D-GAS-2", deviceName: "燃气浓度计-002", location: "城区A-燃气2号点", type: "GAS_SENSOR", lon: 110.131, lat: 28.441, wo: null },
+    { no: "W-003", name: "供水压力预警", level: 3, status: "PENDING", value: "0.18", unit: "MPa", time: "09:40",
+      device: "D-WS-1", deviceName: "供水压力计-001", location: "城区B-供水1号点", type: "WATER_SUPPLY_SENSOR", lon: 110.150, lat: 28.460,
+      wo: { code: "WO-2", state: 1, stateName: "待派单", desc: "供水压力处置" } },
+    { no: "W-004", name: "污水浓度预警", level: 4, status: "RELEASED", value: "18.2", unit: "mg/L", time: "09:35",
+      device: "D-DR-1", deviceName: "污水环境监测仪-001", location: "城区C-污水1号点", type: "SEWAGE_ENV", lon: 110.170, lat: 28.480, wo: null },
+    { no: "W-005", name: "供水流量预警", level: 9, status: "ACTIVE", value: "66.6", unit: "L/s", time: "09:30",
+      device: "D-WS-2", deviceName: "供水压力计-002", location: "城区B-供水2号点", type: "WATER_SUPPLY_SENSOR", lon: 110.151, lat: 28.461,
+      wo: { code: "WO-3", state: 6, stateName: "已结案", desc: "供水流量处置" } }
+  ];
+  const alarms = [
+    { no: "A-001", name: "燃气浓度报警", level: 1, status: "HANDLED", value: "22.6", unit: "mg/m³", time: "09:55",
+      device: "D-GAS-3", deviceName: "燃气浓度计-003", location: "城区A-燃气3号点", type: "GAS_SENSOR", lon: 110.132, lat: 28.442,
+      wo: { code: "WO-4", state: 7, stateName: "已取消", desc: "燃气浓度处置" } },
+    { no: "A-002", name: "污水浓度报警", level: 2, status: "PROCESSING", value: "42.1", unit: "mg/L", time: "09:32",
+      device: "D-DR-1", deviceName: "污水环境监测仪-001", location: "城区C-污水1号点", type: "SEWAGE_ENV", lon: 110.170, lat: 28.480, wo: null }
+  ];
+
+  let mode = "WARNING";
+  let woFailed = false;
+  const filters = { level: "", industry: "", status: "" };
+
+  function events() { return mode === "WARNING" ? warnings : alarms; }
+  const industryOf = e => e.type === "GAS_SENSOR" ? "GAS" : e.type === "WATER_SUPPLY_SENSOR" ? "WATER_SUPPLY" : "DRAINAGE";
+
+  function render() {
+    const filtered = events().filter(e =>
+      (!filters.level || String(e.level) === filters.level) &&
+      (!filters.industry || industryOf(e) === filters.industry) &&
+      (!filters.status || e.status === filters.status));
+
+    document.getElementById("count").textContent = filtered.length;
+    document.getElementById("modeName").textContent = mode === "WARNING" ? "当前预警" : "当前报警";
+    document.getElementById("woState").textContent = woFailed ? "FAILED" : (filtered.some(e => e.wo) ? "SUCCESS" : "EMPTY");
+    document.getElementById("updated").textContent = new Date().toLocaleTimeString("zh-CN");
+
+    document.getElementById("empty").style.display = filtered.length ? "none" : "block";
+    document.getElementById("eventRows").innerHTML = filtered.map((e, i) => `
+      <tr>
+        <td><a href="#" data-i="${i}">${e.no}</a></td>
+        <td><span class="badge b-${levelColor(e.level)}">${levelColor(e.level)}</span></td>
+        <td>${e.value} ${e.unit}</td>
+        <td>${e.device}</td>
+        <td>${woFailed ? "--" : e.wo ? `<span class="badge">${WO_STATES[e.wo.state]}</span>` : "无工单"}</td>
+      </tr>`).join("");
+
+    const map = document.getElementById("map");
+    map.innerHTML = "";
+    if (filtered.length) {
+      const lons = filtered.map(e => e.lon), lats = filtered.map(e => e.lat);
+      const minLon = Math.min(...lons), maxLon = Math.max(...lons), minLat = Math.min(...lats), maxLat = Math.max(...lats);
+      const xs = e => 12 + (e.lon - minLon) / Math.max(0.001, maxLon - minLon) * 76;
+      const ys = e => 12 + (maxLat - e.lat) / Math.max(0.001, maxLat - minLat) * 76;
+      filtered.forEach(e => {
+        const el = document.createElement("div");
+        el.className = "marker c-" + levelColor(e.level);
+        el.style.left = xs(e) + "%"; el.style.top = ys(e) + "%";
+        el.title = e.no;
+        el.addEventListener("click", () => showDetail(e));
+        map.appendChild(el);
+        const l = document.createElement("div");
+        l.className = "label"; l.style.left = xs(e) + "%"; l.style.top = ys(e) + "%";
+        l.textContent = e.no;
+        map.appendChild(l);
+      });
+    }
+    document.getElementById("caption").textContent =
+      `${mode === "WARNING" ? "预警" : "报警"} ${filtered.length} 条 · ` + filtered.map(e => e.no).join("、") || "无";
+
+    document.querySelectorAll("#eventRows a").forEach(a =>
+      a.addEventListener("click", ev => { ev.preventDefault(); showDetail(filtered[+a.dataset.i]); }));
+  }
+
+  function showDetail(e) {
+    const box = document.getElementById("detail");
+    box.classList.toggle("failed", woFailed);
+    box.innerHTML = `<h3>${e.no} ${e.name}</h3><ul>
+      <li>等级:${levelColor(e.level)}(${e.level} 级) 状态:${STATUS_NAME[e.status] || e.status} 时间:${e.time}</li>
+      <li>数值:${e.value} ${e.unit} 设备:${e.device}(${e.deviceName})</li>
+      <li>位置:${e.location} 类型:${TYPE[e.type]}</li>
+      ${woFailed
+        ? `<li class="empty" style="margin-top:6px">工单状态分区不可用(分区失败独立呈现,事件列表不受影响)。</li>`
+        : e.wo
+          ? `<li>工单:${e.wo.code} ${e.wo.desc} 状态:<span class="badge">${WO_STATES[e.wo.state]}</span></li>`
+          : `<li>关联工单:无</li>`}
+    </ul>`;
+    document.querySelectorAll(".marker").forEach(m => m.classList.toggle("hot", m.title === e.no));
+  }
+
+  document.getElementById("tabWARNING").addEventListener("click", () => setMode("WARNING"));
+  document.getElementById("tabALARM").addEventListener("click", () => setMode("ALARM"));
+  function setMode(next) {
+    mode = next;
+    document.getElementById("tabWARNING").classList.toggle("on", mode === "WARNING");
+    document.getElementById("tabALARM").classList.toggle("on", mode === "ALARM");
+    render();
+  }
+  document.getElementById("apply").addEventListener("click", () => {
+    filters.level = document.getElementById("fLevel").value;
+    filters.industry = document.getElementById("fIndustry").value;
+    filters.status = document.getElementById("fStatus").value;
+    render();
+  });
+  document.getElementById("reset").addEventListener("click", () => {
+    Object.keys(filters).forEach(k => filters[k] = "");
+    document.getElementById("fLevel").value = "";
+    document.getElementById("fIndustry").value = "";
+    document.getElementById("fStatus").value = "";
+    render();
+  });
+  document.getElementById("refresh").addEventListener("click", render);
+  document.getElementById("toggleFail").addEventListener("click", () => {
+    woFailed = !woFailed;
+    document.getElementById("toggleFail").textContent = woFailed ? "恢复工单分区" : "模拟工单分区失败";
+    render();
+  });
+  render();
+</script>
+</body>
+</html>

+ 75 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/cockpit/CockpitCurrentEventsController.java

@@ -0,0 +1,75 @@
+package com.zksy.web.controller.cockpit;
+
+import com.zksy.cockpit.event.CockpitCurrentEventsService;
+import com.zksy.cockpit.foundation.CockpitErrorPayload;
+import com.zksy.cockpit.foundation.CockpitInvalidRequestException;
+import com.zksy.cockpit.foundation.CockpitReadException;
+import com.zksy.common.core.domain.AjaxResult;
+import com.zksy.common.utils.StringUtils;
+import com.zksy.common.utils.uuid.IdUtils;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+
+@RestController
+public class CockpitCurrentEventsController {
+    private static final String REQUEST_ID_HEADER = "X-Request-Id";
+
+    private final CockpitCurrentEventsService currentEventsService;
+
+    public CockpitCurrentEventsController(CockpitCurrentEventsService currentEventsService) {
+        this.currentEventsService = currentEventsService;
+    }
+
+    @GetMapping("/api/cockpit/v1/events/current")
+    @PreAuthorize("isAuthenticated()")
+    public ResponseEntity<AjaxResult> currentEvents(
+            @RequestParam(value = "mode", required = false) String mode,
+            @RequestParam(value = "level", required = false) String level,
+            @RequestParam(value = "industry", required = false) String industry,
+            @RequestParam(value = "status", required = false) String status,
+            @RequestParam(value = "page", defaultValue = "1") Integer page,
+            @RequestParam(value = "pageSize", defaultValue = "100") Integer pageSize,
+            @RequestHeader(value = REQUEST_ID_HEADER, required = false) String requestId) {
+        return ResponseEntity.ok(AjaxResult.success(
+                currentEventsService.load(
+                        resolveRequestId(requestId),
+                        mode,
+                        level,
+                        industry,
+                        status,
+                        page,
+                        pageSize)));
+    }
+
+    @ExceptionHandler(CockpitInvalidRequestException.class)
+    public ResponseEntity<AjaxResult> handleInvalidRequest(
+            HttpServletRequest request,
+            CockpitInvalidRequestException exception) {
+        return error(HttpStatus.BAD_REQUEST, currentEventsService.invalidRequest(
+                resolveRequestId(request.getHeader(REQUEST_ID_HEADER)),
+                exception.getMessage()));
+    }
+
+    @ExceptionHandler(CockpitReadException.class)
+    public ResponseEntity<AjaxResult> handleReadFailure(HttpServletRequest request) {
+        return error(HttpStatus.INTERNAL_SERVER_ERROR, currentEventsService.serverError(
+                resolveRequestId(request.getHeader(REQUEST_ID_HEADER))));
+    }
+
+    private ResponseEntity<AjaxResult> error(HttpStatus status, CockpitErrorPayload payload) {
+        return ResponseEntity.status(status)
+                .body(new AjaxResult(status.value(), payload.getMessage(), payload));
+    }
+
+    private String resolveRequestId(String requestId) {
+        return StringUtils.isNotEmpty(requestId) ? requestId : IdUtils.fastSimpleUUID();
+    }
+}

+ 445 - 0
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitCurrentEventsControllerTest.java

@@ -0,0 +1,445 @@
+package com.zksy.web.controller.cockpit;
+
+import com.zksy.cockpit.event.CockpitCurrentEventsAdapter;
+import com.zksy.cockpit.event.CockpitCurrentEventsService;
+import com.zksy.cockpit.event.CockpitCurrentEventsSnapshot;
+import com.zksy.cockpit.foundation.CockpitReadException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PatchMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+
+import java.lang.reflect.Method;
+import java.math.BigDecimal;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+class CockpitCurrentEventsControllerTest {
+
+    private static final String ENDPOINT = "/api/cockpit/v1/events/current";
+
+    private CockpitCurrentEventsAdapter adapter;
+    private MockMvc mockMvc;
+
+    @BeforeEach
+    void setUp() {
+        adapter = mock(CockpitCurrentEventsAdapter.class);
+        CockpitCurrentEventsService service = new CockpitCurrentEventsService(
+                adapter,
+                Clock.fixed(Instant.parse("2026-09-02T02:00:00Z"), ZoneOffset.UTC));
+        mockMvc = MockMvcBuilders
+                .standaloneSetup(new CockpitCurrentEventsController(service))
+                .build();
+        when(adapter.load()).thenReturn(fixture());
+    }
+
+    @Test
+    void warningModeExposesEnvelopePagedEventsColorsAndWorkOrderSummaries() throws Exception {
+        mockMvc.perform(get(ENDPOINT)
+                        .param("mode", "WARNING")
+                        .header("X-Request-Id", "current-warnings-001"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data.requestId").value("current-warnings-001"))
+                .andExpect(jsonPath("$.data.apiVersion").value("v1"))
+                .andExpect(jsonPath("$.data.readOnly").value(true))
+                .andExpect(jsonPath("$.data.displayName").value("当前预警"))
+                .andExpect(jsonPath("$.data.timezone").value("Asia/Shanghai"))
+                .andExpect(jsonPath("$.data.mode").value("WARNING"))
+                .andExpect(jsonPath("$.data.filters.mode").value("WARNING"))
+                .andExpect(jsonPath("$.data.filters.level").value(nullValue()))
+                .andExpect(jsonPath("$.data.filters.industry").value(nullValue()))
+                .andExpect(jsonPath("$.data.filters.status").value(nullValue()))
+                .andExpect(jsonPath("$.data.pagination.page").value(1))
+                .andExpect(jsonPath("$.data.pagination.pageSize").value(100))
+                .andExpect(jsonPath("$.data.pagination.total").value(5))
+                .andExpect(jsonPath("$.data.pagination.totalPages").value(1))
+                .andExpect(jsonPath("$.data.pagination.hasMore").value(false))
+                .andExpect(jsonPath("$.data.sections.events.state").value("SUCCESS"))
+                .andExpect(jsonPath("$.data.sections.events.data", hasSize(5)))
+                .andExpect(jsonPath("$.data.sections.events.data[0].eventNo").value("W-001"))
+                .andExpect(jsonPath("$.data.sections.events.data[0].level").value(1))
+                .andExpect(jsonPath("$.data.sections.events.data[0].levelColor").value("RED"))
+                .andExpect(jsonPath("$.data.sections.events.data[0].deviceCode").value("D-GAS-1"))
+                .andExpect(jsonPath("$.data.sections.events.data[0].industry").value("GAS"))
+                .andExpect(jsonPath("$.data.sections.events.data[0].workOrderSummary.workOrderCode").value("WO-1"))
+                .andExpect(jsonPath("$.data.sections.events.data[0].workOrderSummary.stateCode").value(4))
+                .andExpect(jsonPath("$.data.sections.events.data[0].workOrderSummary.stateName").value("处理中"))
+                .andExpect(jsonPath("$.data.sections.events.data[1].eventNo").value("W-002"))
+                .andExpect(jsonPath("$.data.sections.events.data[1].levelColor").value("YELLOW"))
+                .andExpect(jsonPath("$.data.sections.events.data[1].workOrderSummary").value(nullValue()))
+                .andExpect(jsonPath("$.data.sections.events.data[2].eventNo").value("W-003"))
+                .andExpect(jsonPath("$.data.sections.events.data[2].levelColor").value("BLUE"))
+                .andExpect(jsonPath("$.data.sections.events.data[3].eventNo").value("W-004"))
+                .andExpect(jsonPath("$.data.sections.events.data[3].levelColor").value("GREEN"))
+                .andExpect(jsonPath("$.data.sections.events.data[4].eventNo").value("W-005"))
+                .andExpect(jsonPath("$.data.sections.events.data[4].levelColor").value("UNKNOWN"))
+                .andExpect(jsonPath("$.data.sections.events.data[4].workOrderSummary.stateName").value("已结案"))
+                .andExpect(jsonPath("$.data.sections.workOrders.state").value("SUCCESS"))
+                .andExpect(jsonPath("$.data.sections.workOrders.reason").value("SUCCESS"))
+                .andExpect(jsonPath("$.data.mapFilter.mode").value("WARNING"))
+                .andExpect(jsonPath("$.data.mapFilter.deviceCodes", hasSize(5)));
+    }
+
+    @Test
+    void alarmModeReturnsAlarmsOnlyAndKeepsFamilySeparate() throws Exception {
+        mockMvc.perform(get(ENDPOINT).param("mode", "ALARM"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.displayName").value("当前报警"))
+                .andExpect(jsonPath("$.data.mode").value("ALARM"))
+                .andExpect(jsonPath("$.data.pagination.total").value(2))
+                .andExpect(jsonPath("$.data.sections.events.data", hasSize(2)))
+                .andExpect(jsonPath("$.data.sections.events.data[0].eventNo").value("A-001"))
+                .andExpect(jsonPath("$.data.sections.events.data[0].levelColor").value("RED"))
+                .andExpect(jsonPath("$.data.sections.events.data[0].workOrderSummary.stateName").value("已取消"))
+                .andExpect(jsonPath("$.data.sections.events.data[1].eventNo").value("A-002"))
+                .andExpect(jsonPath("$.data.sections.events.data[1].workOrderSummary").value(nullValue()))
+                .andExpect(jsonPath("$.data.sections.workOrders.state").value("SUCCESS"))
+                .andExpect(jsonPath("$.data.mapFilter.deviceCodes", hasSize(2)));
+    }
+
+    @Test
+    void levelColorsFollowOneRedTwoYellowThreeBlueFourGreen() throws Exception {
+        mockMvc.perform(get(ENDPOINT).param("mode", "WARNING"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.sections.events.data[0].levelColor").value("RED"))
+                .andExpect(jsonPath("$.data.sections.events.data[1].levelColor").value("YELLOW"))
+                .andExpect(jsonPath("$.data.sections.events.data[2].levelColor").value("BLUE"))
+                .andExpect(jsonPath("$.data.sections.events.data[3].levelColor").value("GREEN"))
+                .andExpect(jsonPath("$.data.sections.events.data[4].levelColor").value("UNKNOWN"));
+    }
+
+    @Test
+    void unknownOrNonFormalEventTypesAreDroppedFromTheList() throws Exception {
+        mockMvc.perform(get(ENDPOINT).param("mode", "WARNING"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.pagination.total").value(5))
+                .andExpect(jsonPath("$.data.sections.events.data[0].eventNo").value("W-001"))
+                .andExpect(jsonPath("$.data.mapFilter.deviceCodes", hasSize(5)));
+    }
+
+    @Test
+    void emptyWorkOrderLinksProduceEmptySectionAndNullSummaries() throws Exception {
+        when(adapter.load()).thenReturn(fixtureWithNoWorkOrders());
+
+        mockMvc.perform(get(ENDPOINT).param("mode", "WARNING"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.sections.events.state").value("SUCCESS"))
+                .andExpect(jsonPath("$.data.sections.events.data[0].workOrderSummary").value(nullValue()))
+                .andExpect(jsonPath("$.data.sections.workOrders.state").value("EMPTY"))
+                .andExpect(jsonPath("$.data.sections.workOrders.reason").value("NO_WORK_ORDERS"));
+    }
+
+    @Test
+    void workOrderSectionFailureRendersIndependentlyWhileEventsStayVisible() throws Exception {
+        when(adapter.load()).thenReturn(fixtureWithFailedWorkOrders());
+
+        mockMvc.perform(get(ENDPOINT).param("mode", "WARNING"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.sections.events.state").value("SUCCESS"))
+                .andExpect(jsonPath("$.data.sections.events.data", hasSize(5)))
+                .andExpect(jsonPath("$.data.sections.events.data[0].workOrderSummary").value(nullValue()))
+                .andExpect(jsonPath("$.data.sections.workOrders.state").value("FAILED"))
+                .andExpect(jsonPath("$.data.sections.workOrders.reason").value("WORK_ORDER_SECTION_UNAVAILABLE"))
+                .andExpect(jsonPath("$.data.sections.workOrders.message").value("工单状态分区暂不可用"));
+    }
+
+    @Test
+    void emptyListReturnsSuccessfulEmptyState() throws Exception {
+        when(adapter.load()).thenReturn(CockpitCurrentEventsSnapshot.empty("2026-09-02T09:59:50+08:00"));
+
+        mockMvc.perform(get(ENDPOINT).param("mode", "WARNING"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.pagination.total").value(0))
+                .andExpect(jsonPath("$.data.sections.events.state").value("EMPTY"))
+                .andExpect(jsonPath("$.data.sections.events.reason").value("NO_EVENTS"))
+                .andExpect(jsonPath("$.data.sections.events.data").value(nullValue()))
+                .andExpect(jsonPath("$.data.sections.workOrders.state").value("EMPTY"))
+                .andExpect(jsonPath("$.data.mapFilter.deviceCodes", hasSize(0)));
+    }
+
+    @Test
+    void paginationSlicesEventsAndTotalsMatch() throws Exception {
+        mockMvc.perform(get(ENDPOINT)
+                        .param("mode", "WARNING")
+                        .param("page", "1")
+                        .param("pageSize", "2"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.pagination.total").value(5))
+                .andExpect(jsonPath("$.data.pagination.totalPages").value(3))
+                .andExpect(jsonPath("$.data.pagination.hasMore").value(true))
+                .andExpect(jsonPath("$.data.sections.events.data", hasSize(2)))
+                .andExpect(jsonPath("$.data.sections.events.data[0].eventNo").value("W-001"))
+                .andExpect(jsonPath("$.data.sections.events.data[1].eventNo").value("W-002"));
+
+        mockMvc.perform(get(ENDPOINT)
+                        .param("mode", "WARNING")
+                        .param("page", "3")
+                        .param("pageSize", "2"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.sections.events.data", hasSize(1)))
+                .andExpect(jsonPath("$.data.sections.events.data[0].eventNo").value("W-005"))
+                .andExpect(jsonPath("$.data.pagination.hasMore").value(false));
+    }
+
+    @Test
+    void levelIndustryAndStatusFiltersNarrowListAndMapFilter() throws Exception {
+        mockMvc.perform(get(ENDPOINT)
+                        .param("mode", "WARNING")
+                        .param("level", "2"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.pagination.total").value(1))
+                .andExpect(jsonPath("$.data.sections.events.data[0].eventNo").value("W-002"))
+                .andExpect(jsonPath("$.data.mapFilter.level").value(2));
+
+        mockMvc.perform(get(ENDPOINT)
+                        .param("mode", "WARNING")
+                        .param("industry", "WATER_SUPPLY"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.pagination.total").value(2))
+                .andExpect(jsonPath("$.data.mapFilter.industry").value("WATER_SUPPLY"));
+
+        mockMvc.perform(get(ENDPOINT)
+                        .param("mode", "WARNING")
+                        .param("status", "ACTIVE"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.pagination.total").value(3))
+                .andExpect(jsonPath("$.data.sections.events.data[0].eventNo").value("W-001"))
+                .andExpect(jsonPath("$.data.sections.events.data[2].eventNo").value("W-005"))
+                .andExpect(jsonPath("$.data.mapFilter.status").value("ACTIVE"));
+    }
+
+    @Test
+    void missingOrInvalidModeReturnsTypedBadRequestBeforeReadAdapterIsCalled() throws Exception {
+        mockMvc.perform(get(ENDPOINT))
+                .andExpect(status().isBadRequest())
+                .andExpect(jsonPath("$.code").value(400))
+                .andExpect(jsonPath("$.data.category").value("INVALID_REQUEST"))
+                .andExpect(jsonPath("$.data.message").value("mode must be WARNING or ALARM"));
+
+        mockMvc.perform(get(ENDPOINT).param("mode", "WHATEVER"))
+                .andExpect(status().isBadRequest())
+                .andExpect(jsonPath("$.data.message").value("mode must be WARNING or ALARM"));
+
+        verifyNoInteractions(adapter);
+    }
+
+    @Test
+    void invalidLevelIndustryOrPaginationReturnsTypedBadRequestBeforeReadAdapterIsCalled() throws Exception {
+        mockMvc.perform(get(ENDPOINT).param("mode", "WARNING").param("level", "abc"))
+                .andExpect(status().isBadRequest())
+                .andExpect(jsonPath("$.data.message").value("level must be an integer between 1 and 4"));
+
+        mockMvc.perform(get(ENDPOINT).param("mode", "WARNING").param("level", "5"))
+                .andExpect(status().isBadRequest())
+                .andExpect(jsonPath("$.data.message").value("level must be an integer between 1 and 4"));
+
+        mockMvc.perform(get(ENDPOINT).param("mode", "WARNING").param("industry", "MANHOLE"))
+                .andExpect(status().isBadRequest())
+                .andExpect(jsonPath("$.data.message").value(
+                        "industry must be GAS, WATER_SUPPLY or DRAINAGE"));
+
+        mockMvc.perform(get(ENDPOINT)
+                        .param("mode", "WARNING")
+                        .param("page", "0")
+                        .param("pageSize", "1001"))
+                .andExpect(status().isBadRequest())
+                .andExpect(jsonPath("$.data.message").value("当前预警分页参数不合法"));
+
+        verifyNoInteractions(adapter);
+    }
+
+    @Test
+    void serverFailureReturnsTypedServerError() throws Exception {
+        when(adapter.load()).thenThrow(new CockpitReadException("upstream unavailable"));
+
+        mockMvc.perform(get(ENDPOINT)
+                        .param("mode", "WARNING")
+                        .header("X-Request-Id", "current-warnings-500"))
+                .andExpect(status().isInternalServerError())
+                .andExpect(jsonPath("$.code").value(500))
+                .andExpect(jsonPath("$.data.category").value("SERVER_ERROR"))
+                .andExpect(jsonPath("$.data.message").value("驾驶舱当前预警数据暂不可用"))
+                .andExpect(jsonPath("$.data.requestId").value("current-warnings-500"));
+    }
+
+    @Test
+    void mutatingRequestIsRejectedBeforeReadAdapterIsCalled() throws Exception {
+        mockMvc.perform(post(ENDPOINT))
+                .andExpect(status().isMethodNotAllowed());
+
+        verifyNoInteractions(adapter);
+    }
+
+    @Test
+    void controllerOnlyExposesAuthenticatedReadOnlyOperations() {
+        assertTrue(Arrays.stream(CockpitCurrentEventsController.class.getDeclaredMethods())
+                .filter(this::hasRequestMappingAnnotation)
+                .allMatch(CockpitCurrentEventsControllerTest::isReadOnlyMappedMethod));
+
+        assertTrue(Arrays.stream(CockpitCurrentEventsController.class.getDeclaredMethods())
+                .anyMatch(method -> method.isAnnotationPresent(PreAuthorize.class)
+                        && "isAuthenticated()".equals(
+                                method.getAnnotation(PreAuthorize.class).value())));
+    }
+
+    private boolean hasRequestMappingAnnotation(Method method) {
+        return method.isAnnotationPresent(org.springframework.web.bind.annotation.RequestMapping.class)
+                || method.isAnnotationPresent(GetMapping.class)
+                || method.isAnnotationPresent(PostMapping.class)
+                || method.isAnnotationPresent(PutMapping.class)
+                || method.isAnnotationPresent(PatchMapping.class)
+                || method.isAnnotationPresent(DeleteMapping.class);
+    }
+
+    private static boolean isReadOnlyMappedMethod(Method method) {
+        return method.isAnnotationPresent(GetMapping.class)
+                && !method.isAnnotationPresent(PostMapping.class)
+                && !method.isAnnotationPresent(PutMapping.class)
+                && !method.isAnnotationPresent(PatchMapping.class)
+                && !method.isAnnotationPresent(DeleteMapping.class);
+    }
+
+    private static CockpitCurrentEventsSnapshot fixture() {
+        return new CockpitCurrentEventsSnapshot(
+                "2026-09-02T09:59:50+08:00",
+                types(),
+                Arrays.asList(
+                        warning("W-001", "燃气浓度预警", "GAS_CONCENTRATION", 1, "ACTIVE",
+                                "2026-09-02T09:50:00+08:00", "2026-09-02T09:52:00+08:00",
+                                "12.5", "mg/m³", "D-GAS-1", "燃气浓度计-001", "城区A-燃气1号点",
+                                "GAS_SENSOR", "110.130", "28.440"),
+                        warning("W-002", "燃气压力预警", "GAS_PRESSURE", 2, "ACTIVE",
+                                "2026-09-02T09:45:00+08:00", "2026-09-02T09:46:00+08:00",
+                                "0.32", "MPa", "D-GAS-2", "燃气浓度计-002", "城区A-燃气2号点",
+                                "GAS_SENSOR", "110.131", "28.441"),
+                        warning("W-003", "供水压力预警", "WATER_PRESSURE", 3, "PENDING",
+                                "2026-09-02T09:40:00+08:00", "2026-09-02T09:41:00+08:00",
+                                "0.18", "MPa", "D-WS-1", "供水压力计-001", "城区B-供水1号点",
+                                "WATER_SUPPLY_SENSOR", "110.150", "28.460"),
+                        warning("W-004", "污水浓度预警", "SEWAGE_CONCENTRATION", 4, "RELEASED",
+                                "2026-09-02T09:35:00+08:00", "2026-09-02T09:36:00+08:00",
+                                "18.2", "mg/L", "D-DR-1", "污水环境监测仪-001", "城区C-污水1号点",
+                                "SEWAGE_ENV", "110.170", "28.480"),
+                        warning("W-005", "供水流量预警", "WATER_FLOW", 9, "ACTIVE",
+                                "2026-09-02T09:30:00+08:00", "2026-09-02T09:31:00+08:00",
+                                "66.6", "L/s", "D-WS-2", "供水压力计-002", "城区B-供水2号点",
+                                "WATER_SUPPLY_SENSOR", "110.151", "28.461"),
+                        warning("W-006", "未知类型预警", "LEGACY_EVENT", 1, "ACTIVE",
+                                "2026-09-02T09:55:00+08:00", null,
+                                "1", "x", "D-LEGACY", "旧设备", "旧位置",
+                                "LEGACY", "110.100", "28.400"),
+                        warning("W-007", "智能井盖预警", "MANHOLE_EVENT", 1, "ACTIVE",
+                                "2026-09-02T09:55:00+08:00", null,
+                                "1", "x", "D-MH", "智能井盖", "井盖位置",
+                                "SMART_MANHOLE", "110.101", "28.401")),
+                Arrays.asList(
+                        alarm("A-001", "燃气浓度报警", "GAS_CONCENTRATION", 1, "HANDLED",
+                                "2026-09-02T09:55:00+08:00", "2026-09-02T09:56:00+08:00",
+                                "22.6", "mg/m³", "D-GAS-3", "燃气浓度计-003", "城区A-燃气3号点",
+                                "GAS_SENSOR", "110.132", "28.442"),
+                        alarm("A-002", "污水浓度报警", "SEWAGE_CONCENTRATION", 2, "PROCESSING",
+                                "2026-09-02T09:32:00+08:00", "2026-09-02T09:33:00+08:00",
+                                "42.1", "mg/L", "D-DR-1", "污水环境监测仪-001", "城区C-污水1号点",
+                                "SEWAGE_ENV", "110.170", "28.480")),
+                Arrays.asList(
+                        workOrder("W-001", "WO-1", 2, "燃气浓度处置", 4),
+                        workOrder("W-003", "WO-2", 3, "供水压力处置", 1),
+                        workOrder("W-005", "WO-3", 2, "供水流量处置", 6),
+                        workOrder("A-001", "WO-4", 1, "燃气浓度处置", 7)));
+    }
+
+    private static List<CockpitCurrentEventsSnapshot.SourceType> types() {
+        return Arrays.asList(
+                new CockpitCurrentEventsSnapshot.SourceType(
+                        "GAS_SENSOR", "燃气浓度计", "GAS", "SMART_DEVICE"),
+                new CockpitCurrentEventsSnapshot.SourceType(
+                        "WATER_SUPPLY_SENSOR", "供水压力计", "WATER_SUPPLY", "SMART_DEVICE"),
+                new CockpitCurrentEventsSnapshot.SourceType(
+                        "SEWAGE_ENV", "污水环境监测", "DRAINAGE", "SMART_DEVICE"),
+                new CockpitCurrentEventsSnapshot.SourceType(
+                        "WATER_PLANT", "水厂", "WATER_SUPPLY", "FACILITY"),
+                new CockpitCurrentEventsSnapshot.SourceType(
+                        "SMART_MANHOLE", "智能井盖", null, "SMART_DEVICE"));
+    }
+
+    private static CockpitCurrentEventsSnapshot fixtureWithNoWorkOrders() {
+        return new CockpitCurrentEventsSnapshot(
+                "2026-09-02T09:59:50+08:00",
+                types(),
+                Collections.singletonList(
+                        warning("W-001", "燃气浓度预警", "GAS_CONCENTRATION", 1, "ACTIVE",
+                                "2026-09-02T09:50:00+08:00", "2026-09-02T09:52:00+08:00",
+                                "12.5", "mg/m³", "D-GAS-1", "燃气浓度计-001", "城区A-燃气1号点",
+                                "GAS_SENSOR", "110.130", "28.440")),
+                Collections.singletonList(
+                        alarm("A-001", "燃气浓度报警", "GAS_CONCENTRATION", 1, "HANDLED",
+                                "2026-09-02T09:55:00+08:00", "2026-09-02T09:56:00+08:00",
+                                "22.6", "mg/m³", "D-GAS-3", "燃气浓度计-003", "城区A-燃气3号点",
+                                "GAS_SENSOR", "110.132", "28.442")),
+                Collections.emptyList());
+    }
+
+    private static CockpitCurrentEventsSnapshot fixtureWithFailedWorkOrders() {
+        CockpitCurrentEventsSnapshot base = fixture();
+        return new CockpitCurrentEventsSnapshot(
+                base.getUpdatedAt(),
+                base.getTypes(),
+                base.getWarnings(),
+                base.getAlarms(),
+                null);
+    }
+
+    private static CockpitCurrentEventsSnapshot.SourceWarning warning(
+            String no, String name, String type, Integer level, String status,
+            String eventTime, String updatedTime, String value, String unit,
+            String deviceCode, String deviceName, String location, String typeCode,
+            String longitude, String latitude) {
+        return new CockpitCurrentEventsSnapshot.SourceWarning(
+                no, name, type, level, status, eventTime, updatedTime,
+                new BigDecimal(value), unit, deviceCode, deviceName, location, typeCode,
+                new BigDecimal(longitude), new BigDecimal(latitude));
+    }
+
+    private static CockpitCurrentEventsSnapshot.SourceAlarm alarm(
+            String id, String name, String type, Integer level, String status,
+            String eventTime, String updatedTime, String value, String unit,
+            String deviceCode, String deviceName, String location, String typeCode,
+            String longitude, String latitude) {
+        return new CockpitCurrentEventsSnapshot.SourceAlarm(
+                id, name, type, level, status, eventTime, updatedTime,
+                new BigDecimal(value), unit, deviceCode, deviceName, location, typeCode,
+                new BigDecimal(longitude), new BigDecimal(latitude));
+    }
+
+    private static CockpitCurrentEventsSnapshot.SourceWorkOrderLink workOrder(
+            String eventNo, String workOrderCode, Integer orderLevel, String orderDesc, Integer stateCode) {
+        return new CockpitCurrentEventsSnapshot.SourceWorkOrderLink(
+                eventNo, workOrderCode, orderLevel, orderDesc, stateCode,
+                "2026-09-02T09:00:00+08:00",
+                "2026-09-02T09:30:00+08:00",
+                "2026-09-02T18:00:00+08:00",
+                null);
+    }
+}

+ 5 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/CockpitCurrentEventsAdapter.java

@@ -0,0 +1,5 @@
+package com.zksy.cockpit.event;
+
+public interface CockpitCurrentEventsAdapter {
+    CockpitCurrentEventsSnapshot load();
+}

+ 258 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/CockpitCurrentEventsResponse.java

@@ -0,0 +1,258 @@
+package com.zksy.cockpit.event;
+
+import com.zksy.cockpit.detail.CockpitSectionResult;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+public class CockpitCurrentEventsResponse {
+    private final String requestId;
+    private final String apiVersion;
+    private final boolean readOnly;
+    private final String displayName;
+    private final String timezone;
+    private final String mode;
+    private final String updatedAt;
+    private final Filters filters;
+    private final Pagination pagination;
+    private final MapFilter mapFilter;
+    private final Sections sections;
+
+    public CockpitCurrentEventsResponse(
+            String requestId,
+            String apiVersion,
+            boolean readOnly,
+            String displayName,
+            String timezone,
+            String mode,
+            String updatedAt,
+            Filters filters,
+            Pagination pagination,
+            MapFilter mapFilter,
+            Sections sections) {
+        this.requestId = requestId;
+        this.apiVersion = apiVersion;
+        this.readOnly = readOnly;
+        this.displayName = displayName;
+        this.timezone = timezone;
+        this.mode = mode;
+        this.updatedAt = updatedAt;
+        this.filters = filters;
+        this.pagination = pagination;
+        this.mapFilter = mapFilter;
+        this.sections = sections;
+    }
+
+    public String getRequestId() { return requestId; }
+    public String getApiVersion() { return apiVersion; }
+    public boolean isReadOnly() { return readOnly; }
+    public String getDisplayName() { return displayName; }
+    public String getTimezone() { return timezone; }
+    public String getMode() { return mode; }
+    public String getUpdatedAt() { return updatedAt; }
+    public Filters getFilters() { return filters; }
+    public Pagination getPagination() { return pagination; }
+    public MapFilter getMapFilter() { return mapFilter; }
+    public Sections getSections() { return sections; }
+
+    public static class Filters {
+        private final String mode;
+        private final Integer level;
+        private final String industry;
+        private final String status;
+
+        public Filters(String mode, Integer level, String industry, String status) {
+            this.mode = mode;
+            this.level = level;
+            this.industry = industry;
+            this.status = status;
+        }
+
+        public String getMode() { return mode; }
+        public Integer getLevel() { return level; }
+        public String getIndustry() { return industry; }
+        public String getStatus() { return status; }
+    }
+
+    public static class Pagination {
+        private final int page;
+        private final int pageSize;
+        private final int total;
+        private final int totalPages;
+        private final boolean hasMore;
+
+        public Pagination(int page, int pageSize, int total, int totalPages, boolean hasMore) {
+            this.page = page;
+            this.pageSize = pageSize;
+            this.total = total;
+            this.totalPages = totalPages;
+            this.hasMore = hasMore;
+        }
+
+        public int getPage() { return page; }
+        public int getPageSize() { return pageSize; }
+        public int getTotal() { return total; }
+        public int getTotalPages() { return totalPages; }
+        public boolean isHasMore() { return hasMore; }
+    }
+
+    public static class MapFilter {
+        private final String mode;
+        private final Integer level;
+        private final String industry;
+        private final String status;
+        private final List<String> deviceCodes;
+
+        public MapFilter(String mode, Integer level, String industry, String status, List<String> deviceCodes) {
+            this.mode = mode;
+            this.level = level;
+            this.industry = industry;
+            this.status = status;
+            this.deviceCodes = deviceCodes;
+        }
+
+        public String getMode() { return mode; }
+        public Integer getLevel() { return level; }
+        public String getIndustry() { return industry; }
+        public String getStatus() { return status; }
+        public List<String> getDeviceCodes() { return deviceCodes; }
+    }
+
+    public static class Sections {
+        private final CockpitSectionResult<List<EventItem>> events;
+        private final CockpitSectionResult<Object> workOrders;
+
+        public Sections(
+                CockpitSectionResult<List<EventItem>> events,
+                CockpitSectionResult<Object> workOrders) {
+            this.events = events;
+            this.workOrders = workOrders;
+        }
+
+        public CockpitSectionResult<List<EventItem>> getEvents() {
+            return events;
+        }
+
+        public CockpitSectionResult<Object> getWorkOrders() {
+            return workOrders;
+        }
+    }
+
+    public static class EventItem {
+        private final String eventNo;
+        private final String eventName;
+        private final String eventType;
+        private final Integer level;
+        private final String levelColor;
+        private final String status;
+        private final String eventTime;
+        private final String updatedTime;
+        private final BigDecimal value;
+        private final String unit;
+        private final String deviceCode;
+        private final String deviceName;
+        private final String location;
+        private final String industry;
+        private final BigDecimal longitude;
+        private final BigDecimal latitude;
+        private final WorkOrderSummary workOrderSummary;
+
+        public EventItem(
+                String eventNo,
+                String eventName,
+                String eventType,
+                Integer level,
+                String levelColor,
+                String status,
+                String eventTime,
+                String updatedTime,
+                BigDecimal value,
+                String unit,
+                String deviceCode,
+                String deviceName,
+                String location,
+                String industry,
+                BigDecimal longitude,
+                BigDecimal latitude,
+                WorkOrderSummary workOrderSummary) {
+            this.eventNo = eventNo;
+            this.eventName = eventName;
+            this.eventType = eventType;
+            this.level = level;
+            this.levelColor = levelColor;
+            this.status = status;
+            this.eventTime = eventTime;
+            this.updatedTime = updatedTime;
+            this.value = value;
+            this.unit = unit;
+            this.deviceCode = deviceCode;
+            this.deviceName = deviceName;
+            this.location = location;
+            this.industry = industry;
+            this.longitude = longitude;
+            this.latitude = latitude;
+            this.workOrderSummary = workOrderSummary;
+        }
+
+        public String getEventNo() { return eventNo; }
+        public String getEventName() { return eventName; }
+        public String getEventType() { return eventType; }
+        public Integer getLevel() { return level; }
+        public String getLevelColor() { return levelColor; }
+        public String getStatus() { return status; }
+        public String getEventTime() { return eventTime; }
+        public String getUpdatedTime() { return updatedTime; }
+        public BigDecimal getValue() { return value; }
+        public String getUnit() { return unit; }
+        public String getDeviceCode() { return deviceCode; }
+        public String getDeviceName() { return deviceName; }
+        public String getLocation() { return location; }
+        public String getIndustry() { return industry; }
+        public BigDecimal getLongitude() { return longitude; }
+        public BigDecimal getLatitude() { return latitude; }
+        public WorkOrderSummary getWorkOrderSummary() { return workOrderSummary; }
+    }
+
+    public static class WorkOrderSummary {
+        private final String workOrderCode;
+        private final Integer orderLevel;
+        private final String orderDesc;
+        private final Integer stateCode;
+        private final String stateName;
+        private final String createTime;
+        private final String updateTime;
+        private final String planFinishTime;
+        private final String realFinishTime;
+
+        public WorkOrderSummary(
+                String workOrderCode,
+                Integer orderLevel,
+                String orderDesc,
+                Integer stateCode,
+                String stateName,
+                String createTime,
+                String updateTime,
+                String planFinishTime,
+                String realFinishTime) {
+            this.workOrderCode = workOrderCode;
+            this.orderLevel = orderLevel;
+            this.orderDesc = orderDesc;
+            this.stateCode = stateCode;
+            this.stateName = stateName;
+            this.createTime = createTime;
+            this.updateTime = updateTime;
+            this.planFinishTime = planFinishTime;
+            this.realFinishTime = realFinishTime;
+        }
+
+        public String getWorkOrderCode() { return workOrderCode; }
+        public Integer getOrderLevel() { return orderLevel; }
+        public String getOrderDesc() { return orderDesc; }
+        public Integer getStateCode() { return stateCode; }
+        public String getStateName() { return stateName; }
+        public String getCreateTime() { return createTime; }
+        public String getUpdateTime() { return updateTime; }
+        public String getPlanFinishTime() { return planFinishTime; }
+        public String getRealFinishTime() { return realFinishTime; }
+    }
+}

+ 494 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/CockpitCurrentEventsService.java

@@ -0,0 +1,494 @@
+package com.zksy.cockpit.event;
+
+import com.zksy.cockpit.detail.CockpitSectionResult;
+import com.zksy.cockpit.foundation.CockpitErrorCategory;
+import com.zksy.cockpit.foundation.CockpitErrorPayload;
+import com.zksy.cockpit.foundation.CockpitInvalidRequestException;
+import com.zksy.cockpit.workorder.CockpitWorkOrderStateDictionary;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.Clock;
+import java.time.OffsetDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Read-only aggregation for 预警处置 / 当前预警.
+ *
+ * <p>Resolves modes (WARNING / ALARM), filters, level colors (1 red / 2 yellow /
+ * 3 blue / 4 green), pagination and work-order summaries. Warning and alarm event
+ * families are never mixed. The work-order summary section fails independently of
+ * the event list so a partial detail failure cannot hide the events.
+ */
+@Service
+public class CockpitCurrentEventsService {
+    private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai");
+    private static final List<String> INDUSTRIES = Arrays.asList("GAS", "WATER_SUPPLY", "DRAINAGE");
+    private static final String SMART_DEVICE = "SMART_DEVICE";
+    private static final int DEFAULT_PAGE_SIZE = 100;
+    private static final int MAX_PAGE_SIZE = 1000;
+    private static final String WARNING = "WARNING";
+    private static final String ALARM = "ALARM";
+
+    private final CockpitCurrentEventsAdapter adapter;
+    private final Clock clock;
+    private RuntimeState state;
+
+    public CockpitCurrentEventsService(
+            CockpitCurrentEventsAdapter adapter,
+            Clock clock) {
+        this.adapter = adapter;
+        this.clock = clock;
+    }
+
+    public synchronized CockpitCurrentEventsResponse load(
+            String requestId,
+            String mode,
+            String level,
+            String industry,
+            String status,
+            Integer page,
+            Integer pageSize) {
+        String resolvedMode = resolveMode(mode);
+        Integer resolvedLevel = resolveLevel(level);
+        String selectedIndustry = resolveIndustry(industry);
+        String selectedStatus = isBlank(status) ? null : status.trim();
+        int safePage = page == null ? 1 : page;
+        int safePageSize = pageSize == null ? DEFAULT_PAGE_SIZE : pageSize;
+        validatePagination(safePage, safePageSize);
+        refreshIfNeeded();
+        return build(
+                requestId,
+                resolvedMode,
+                resolvedLevel,
+                selectedIndustry,
+                selectedStatus,
+                safePage,
+                safePageSize);
+    }
+
+    public CockpitErrorPayload invalidRequest(String requestId, String message) {
+        return new CockpitErrorPayload(
+                CockpitErrorCategory.INVALID_REQUEST,
+                message,
+                requestId,
+                businessTime());
+    }
+
+    public CockpitErrorPayload serverError(String requestId) {
+        return new CockpitErrorPayload(
+                CockpitErrorCategory.SERVER_ERROR,
+                "驾驶舱当前预警数据暂不可用",
+                requestId,
+                businessTime());
+    }
+
+    private synchronized void refreshIfNeeded() {
+        if (state != null) {
+            return;
+        }
+        CockpitCurrentEventsSnapshot loaded = adapter.load();
+        state = new RuntimeState(loaded == null
+                ? CockpitCurrentEventsSnapshot.empty(businessTime())
+                : loaded);
+    }
+
+    private CockpitCurrentEventsResponse build(
+            String requestId,
+            String mode,
+            Integer level,
+            String industry,
+            String status,
+            int page,
+            int pageSize) {
+        CockpitCurrentEventsSnapshot snapshot = state.snapshot;
+        Map<String, CockpitCurrentEventsSnapshot.SourceType> types = types(snapshot);
+        List<EventRow> rows = WARNING.equals(mode)
+                ? warningRows(snapshot, types, level, industry, status)
+                : alarmRows(snapshot, types, level, industry, status);
+        rows.sort(eventOrdering());
+
+        int total = rows.size();
+        int totalPages = total == 0 ? 0 : (total + pageSize - 1) / pageSize;
+        int fromIndex = Math.min((page - 1) * pageSize, total);
+        int toIndex = Math.min(fromIndex + pageSize, total);
+
+        Map<String, CockpitCurrentEventsResponse.WorkOrderSummary> summaries =
+                workOrderSummaries(snapshot.getWorkOrders());
+        List<CockpitCurrentEventsResponse.EventItem> items = new ArrayList<>();
+        for (EventRow row : rows.subList(fromIndex, toIndex)) {
+            items.add(row.toItem(summaries));
+        }
+
+        Set<String> deviceCodes = new LinkedHashSet<>();
+        for (EventRow row : rows) {
+            if (!isBlank(row.deviceCode)) {
+                deviceCodes.add(row.deviceCode.trim());
+            }
+        }
+
+        CockpitSectionResult<List<CockpitCurrentEventsResponse.EventItem>> eventsSection =
+                items.isEmpty()
+                        ? CockpitSectionResult.empty("NO_EVENTS")
+                        : CockpitSectionResult.success(items);
+        CockpitSectionResult<Object> workOrdersSection = workOrdersSection(snapshot.getWorkOrders());
+
+        String updatedAt = isBlank(snapshot.getUpdatedAt())
+                ? businessTime()
+                : snapshot.getUpdatedAt();
+
+        return new CockpitCurrentEventsResponse(
+                requestId,
+                "v1",
+                true,
+                WARNING.equals(mode) ? "当前预警" : "当前报警",
+                "Asia/Shanghai",
+                mode,
+                updatedAt,
+                new CockpitCurrentEventsResponse.Filters(mode, level, industry, status),
+                new CockpitCurrentEventsResponse.Pagination(
+                        page, pageSize, total, totalPages, toIndex < total),
+                new CockpitCurrentEventsResponse.MapFilter(
+                        mode, level, industry, status, sorted(deviceCodes)),
+                new CockpitCurrentEventsResponse.Sections(eventsSection, workOrdersSection));
+    }
+
+    private List<EventRow> warningRows(
+            CockpitCurrentEventsSnapshot snapshot,
+            Map<String, CockpitCurrentEventsSnapshot.SourceType> types,
+            Integer level,
+            String industry,
+            String status) {
+        List<EventRow> rows = new ArrayList<>();
+        if (snapshot.getWarnings() == null) {
+            return rows;
+        }
+        for (CockpitCurrentEventsSnapshot.SourceWarning warning : snapshot.getWarnings()) {
+            if (warning == null) {
+                continue;
+            }
+            CockpitCurrentEventsSnapshot.SourceType type = resolveType(types, warning.getTypeCode());
+            if (!inScope(type, industry)) {
+                continue;
+            }
+            if (level != null && !level.equals(warning.getLevel())) {
+                continue;
+            }
+            if (status != null && !status.equals(warning.getStatus())) {
+                continue;
+            }
+            rows.add(new EventRow(
+                    warning.getWarningNo(),
+                    warning.getWarningName(),
+                    warning.getWarningType(),
+                    warning.getLevel(),
+                    warning.getStatus(),
+                    warning.getEventTime(),
+                    warning.getUpdatedTime(),
+                    warning.getValue(),
+                    warning.getUnit(),
+                    warning.getDeviceCode(),
+                    warning.getDeviceName(),
+                    warning.getLocation(),
+                    type.getIndustry(),
+                    warning.getLongitude(),
+                    warning.getLatitude()));
+        }
+        return rows;
+    }
+
+    private List<EventRow> alarmRows(
+            CockpitCurrentEventsSnapshot snapshot,
+            Map<String, CockpitCurrentEventsSnapshot.SourceType> types,
+            Integer level,
+            String industry,
+            String status) {
+        List<EventRow> rows = new ArrayList<>();
+        if (snapshot.getAlarms() == null) {
+            return rows;
+        }
+        for (CockpitCurrentEventsSnapshot.SourceAlarm alarm : snapshot.getAlarms()) {
+            if (alarm == null) {
+                continue;
+            }
+            CockpitCurrentEventsSnapshot.SourceType type = resolveType(types, alarm.getTypeCode());
+            if (!inScope(type, industry)) {
+                continue;
+            }
+            if (level != null && !level.equals(alarm.getLevel())) {
+                continue;
+            }
+            if (status != null && !status.equals(alarm.getStatus())) {
+                continue;
+            }
+            rows.add(new EventRow(
+                    alarm.getAlarmId(),
+                    alarm.getWarningName(),
+                    alarm.getWarningType(),
+                    alarm.getLevel(),
+                    alarm.getStatus(),
+                    alarm.getEventTime(),
+                    alarm.getUpdatedTime(),
+                    alarm.getValue(),
+                    alarm.getUnit(),
+                    alarm.getDeviceCode(),
+                    alarm.getDeviceName(),
+                    alarm.getLocation(),
+                    type.getIndustry(),
+                    alarm.getLongitude(),
+                    alarm.getLatitude()));
+        }
+        return rows;
+    }
+
+    private static boolean inScope(
+            CockpitCurrentEventsSnapshot.SourceType type,
+            String industry) {
+        if (type == null
+                || !SMART_DEVICE.equals(type.getObjectKind())
+                || type.getIndustry() == null
+                || !INDUSTRIES.contains(type.getIndustry())) {
+            return false;
+        }
+        if (industry != null && !industry.equals(type.getIndustry())) {
+            return false;
+        }
+        return true;
+    }
+
+    private static Map<String, CockpitCurrentEventsResponse.WorkOrderSummary> workOrderSummaries(
+            List<CockpitCurrentEventsSnapshot.SourceWorkOrderLink> links) {
+        Map<String, CockpitCurrentEventsResponse.WorkOrderSummary> result = new LinkedHashMap<>();
+        if (links == null) {
+            return result;
+        }
+        for (CockpitCurrentEventsSnapshot.SourceWorkOrderLink link : links) {
+            if (link == null || isBlank(link.getEventNo())) {
+                continue;
+            }
+            result.put(link.getEventNo().trim(), new CockpitCurrentEventsResponse.WorkOrderSummary(
+                    link.getWorkOrderCode(),
+                    link.getOrderLevel(),
+                    link.getOrderDesc(),
+                    link.getStateCode(),
+                    stateName(link.getStateCode()),
+                    link.getCreateTime(),
+                    link.getUpdateTime(),
+                    link.getPlanFinishTime(),
+                    link.getRealFinishTime()));
+        }
+        return result;
+    }
+
+    private static String stateName(Integer stateCode) {
+        return CockpitWorkOrderStateDictionary.byCode(stateCode) == null
+                ? null
+                : CockpitWorkOrderStateDictionary.byCode(stateCode).getName();
+    }
+
+    private static CockpitSectionResult<Object> workOrdersSection(
+            List<CockpitCurrentEventsSnapshot.SourceWorkOrderLink> links) {
+        if (links == null) {
+            return CockpitSectionResult.failure(
+                    "WORK_ORDER_SECTION_UNAVAILABLE", "工单状态分区暂不可用");
+        }
+        if (links.isEmpty()) {
+            return CockpitSectionResult.empty("NO_WORK_ORDERS");
+        }
+        return CockpitSectionResult.success(null);
+    }
+
+    private static Comparator<EventRow> eventOrdering() {
+        Comparator<EventRow> byTime = (left, right) -> {
+            if (left.eventTime == null && right.eventTime == null) {
+                return 0;
+            }
+            if (left.eventTime == null) {
+                return 1;
+            }
+            if (right.eventTime == null) {
+                return -1;
+            }
+            return right.eventTime.compareTo(left.eventTime);
+        };
+        return byTime.thenComparing(
+                EventRow::getEventNo, Comparator.nullsLast(String::compareTo));
+    }
+
+    private static String resolveMode(String mode) {
+        if (isBlank(mode)) {
+            throw new CockpitInvalidRequestException("mode must be WARNING or ALARM");
+        }
+        String resolved = mode.trim();
+        if (!WARNING.equals(resolved) && !ALARM.equals(resolved)) {
+            throw new CockpitInvalidRequestException("mode must be WARNING or ALARM");
+        }
+        return resolved;
+    }
+
+    private static Integer resolveLevel(String level) {
+        if (isBlank(level)) {
+            return null;
+        }
+        try {
+            int resolved = Integer.parseInt(level.trim());
+            if (resolved < 1 || resolved > 4) {
+                throw new CockpitInvalidRequestException(
+                        "level must be an integer between 1 and 4");
+            }
+            return resolved;
+        } catch (NumberFormatException exception) {
+            throw new CockpitInvalidRequestException(
+                    "level must be an integer between 1 and 4");
+        }
+    }
+
+    private static String resolveIndustry(String industry) {
+        if (isBlank(industry)) {
+            return null;
+        }
+        String resolved = industry.trim();
+        if (!INDUSTRIES.contains(resolved)) {
+            throw new CockpitInvalidRequestException("industry must be GAS, WATER_SUPPLY or DRAINAGE");
+        }
+        return resolved;
+    }
+
+    private static void validatePagination(int page, int pageSize) {
+        if (page < 1 || pageSize < 1 || pageSize > MAX_PAGE_SIZE) {
+            throw new CockpitInvalidRequestException("当前预警分页参数不合法");
+        }
+    }
+
+    private static Map<String, CockpitCurrentEventsSnapshot.SourceType> types(
+            CockpitCurrentEventsSnapshot snapshot) {
+        Map<String, CockpitCurrentEventsSnapshot.SourceType> result = new LinkedHashMap<>();
+        if (snapshot.getTypes() != null) {
+            for (CockpitCurrentEventsSnapshot.SourceType type : snapshot.getTypes()) {
+                if (type != null && !isBlank(type.getCode())) {
+                    result.putIfAbsent(type.getCode().trim(), type);
+                }
+            }
+        }
+        return result;
+    }
+
+    private static CockpitCurrentEventsSnapshot.SourceType resolveType(
+            Map<String, CockpitCurrentEventsSnapshot.SourceType> types,
+            String typeCode) {
+        return isBlank(typeCode) ? null : types.get(typeCode.trim());
+    }
+
+    private static List<String> sorted(Set<String> values) {
+        List<String> result = new ArrayList<>(values);
+        Collections.sort(result, Comparator.nullsLast(String::compareTo));
+        return result;
+    }
+
+    private String businessTime() {
+        return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(
+                OffsetDateTime.now(clock)
+                        .atZoneSameInstant(BUSINESS_ZONE)
+                        .toOffsetDateTime());
+    }
+
+    private static boolean isBlank(String value) {
+        return value == null || value.trim().isEmpty();
+    }
+
+    private static class RuntimeState {
+        private final CockpitCurrentEventsSnapshot snapshot;
+
+        private RuntimeState(CockpitCurrentEventsSnapshot snapshot) {
+            this.snapshot = snapshot;
+        }
+    }
+
+    private static class EventRow {
+        private final String eventNo;
+        private final String eventName;
+        private final String eventType;
+        private final Integer level;
+        private final String status;
+        private final String eventTime;
+        private final String updatedTime;
+        private final BigDecimal value;
+        private final String unit;
+        private final String deviceCode;
+        private final String deviceName;
+        private final String location;
+        private final String industry;
+        private final BigDecimal longitude;
+        private final BigDecimal latitude;
+
+        private EventRow(
+                String eventNo,
+                String eventName,
+                String eventType,
+                Integer level,
+                String status,
+                String eventTime,
+                String updatedTime,
+                BigDecimal value,
+                String unit,
+                String deviceCode,
+                String deviceName,
+                String location,
+                String industry,
+                BigDecimal longitude,
+                BigDecimal latitude) {
+            this.eventNo = eventNo;
+            this.eventName = eventName;
+            this.eventType = eventType;
+            this.level = level;
+            this.status = status;
+            this.eventTime = eventTime;
+            this.updatedTime = updatedTime;
+            this.value = value;
+            this.unit = unit;
+            this.deviceCode = deviceCode;
+            this.deviceName = deviceName;
+            this.location = location;
+            this.industry = industry;
+            this.longitude = longitude;
+            this.latitude = latitude;
+        }
+
+        private String getEventNo() {
+            return eventNo;
+        }
+
+        private CockpitCurrentEventsResponse.EventItem toItem(
+                Map<String, CockpitCurrentEventsResponse.WorkOrderSummary> summaries) {
+            String key = eventNo == null ? null : eventNo.trim();
+            return new CockpitCurrentEventsResponse.EventItem(
+                    eventNo,
+                    eventName,
+                    eventType,
+                    level,
+                    CockpitEventLevelColor.resolve(level),
+                    status,
+                    eventTime,
+                    updatedTime,
+                    value,
+                    unit,
+                    deviceCode,
+                    deviceName,
+                    location,
+                    industry,
+                    longitude,
+                    latitude,
+                    key == null ? null : summaries.get(key));
+        }
+    }
+}

+ 383 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/CockpitCurrentEventsSnapshot.java

@@ -0,0 +1,383 @@
+package com.zksy.cockpit.event;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Read snapshot for 预警处置 / 当前预警.
+ *
+ * <p>Delivers both event families separately (warnings and alarms) plus optional
+ * work-order links. A null workOrders list means the work-order section is
+ * unavailable and must fail independently without hiding the event list.
+ */
+public class CockpitCurrentEventsSnapshot {
+    private final String updatedAt;
+    private final List<SourceType> types;
+    private final List<SourceWarning> warnings;
+    private final List<SourceAlarm> alarms;
+    private final List<SourceWorkOrderLink> workOrders;
+
+    public CockpitCurrentEventsSnapshot(
+            String updatedAt,
+            List<SourceType> types,
+            List<SourceWarning> warnings,
+            List<SourceAlarm> alarms,
+            List<SourceWorkOrderLink> workOrders) {
+        this.updatedAt = updatedAt;
+        this.types = types;
+        this.warnings = warnings;
+        this.alarms = alarms;
+        this.workOrders = workOrders;
+    }
+
+    public static CockpitCurrentEventsSnapshot empty(String updatedAt) {
+        return new CockpitCurrentEventsSnapshot(
+                updatedAt,
+                Collections.emptyList(),
+                Collections.emptyList(),
+                Collections.emptyList(),
+                Collections.emptyList());
+    }
+
+    public String getUpdatedAt() {
+        return updatedAt;
+    }
+
+    public List<SourceType> getTypes() {
+        return types;
+    }
+
+    public List<SourceWarning> getWarnings() {
+        return warnings;
+    }
+
+    public List<SourceAlarm> getAlarms() {
+        return alarms;
+    }
+
+    public List<SourceWorkOrderLink> getWorkOrders() {
+        return workOrders;
+    }
+
+    public static class SourceType {
+        private final String code;
+        private final String name;
+        private final String industry;
+        private final String objectKind;
+
+        public SourceType(String code, String name, String industry, String objectKind) {
+            this.code = code;
+            this.name = name;
+            this.industry = industry;
+            this.objectKind = objectKind;
+        }
+
+        public String getCode() {
+            return code;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public String getIndustry() {
+            return industry;
+        }
+
+        public String getObjectKind() {
+            return objectKind;
+        }
+    }
+
+    public static class SourceWarning {
+        private final String warningNo;
+        private final String warningName;
+        private final String warningType;
+        private final Integer level;
+        private final String status;
+        private final String eventTime;
+        private final String updatedTime;
+        private final BigDecimal value;
+        private final String unit;
+        private final String deviceCode;
+        private final String deviceName;
+        private final String location;
+        private final String typeCode;
+        private final BigDecimal longitude;
+        private final BigDecimal latitude;
+
+        public SourceWarning(
+                String warningNo,
+                String warningName,
+                String warningType,
+                Integer level,
+                String status,
+                String eventTime,
+                String updatedTime,
+                BigDecimal value,
+                String unit,
+                String deviceCode,
+                String deviceName,
+                String location,
+                String typeCode,
+                BigDecimal longitude,
+                BigDecimal latitude) {
+            this.warningNo = warningNo;
+            this.warningName = warningName;
+            this.warningType = warningType;
+            this.level = level;
+            this.status = status;
+            this.eventTime = eventTime;
+            this.updatedTime = updatedTime;
+            this.value = value;
+            this.unit = unit;
+            this.deviceCode = deviceCode;
+            this.deviceName = deviceName;
+            this.location = location;
+            this.typeCode = typeCode;
+            this.longitude = longitude;
+            this.latitude = latitude;
+        }
+
+        public String getWarningNo() {
+            return warningNo;
+        }
+
+        public String getWarningName() {
+            return warningName;
+        }
+
+        public String getWarningType() {
+            return warningType;
+        }
+
+        public Integer getLevel() {
+            return level;
+        }
+
+        public String getStatus() {
+            return status;
+        }
+
+        public String getEventTime() {
+            return eventTime;
+        }
+
+        public String getUpdatedTime() {
+            return updatedTime;
+        }
+
+        public BigDecimal getValue() {
+            return value;
+        }
+
+        public String getUnit() {
+            return unit;
+        }
+
+        public String getDeviceCode() {
+            return deviceCode;
+        }
+
+        public String getDeviceName() {
+            return deviceName;
+        }
+
+        public String getLocation() {
+            return location;
+        }
+
+        public String getTypeCode() {
+            return typeCode;
+        }
+
+        public BigDecimal getLongitude() {
+            return longitude;
+        }
+
+        public BigDecimal getLatitude() {
+            return latitude;
+        }
+    }
+
+    public static class SourceAlarm {
+        private final String alarmId;
+        private final String warningName;
+        private final String warningType;
+        private final Integer level;
+        private final String status;
+        private final String eventTime;
+        private final String updatedTime;
+        private final BigDecimal value;
+        private final String unit;
+        private final String deviceCode;
+        private final String deviceName;
+        private final String location;
+        private final String typeCode;
+        private final BigDecimal longitude;
+        private final BigDecimal latitude;
+
+        public SourceAlarm(
+                String alarmId,
+                String warningName,
+                String warningType,
+                Integer level,
+                String status,
+                String eventTime,
+                String updatedTime,
+                BigDecimal value,
+                String unit,
+                String deviceCode,
+                String deviceName,
+                String location,
+                String typeCode,
+                BigDecimal longitude,
+                BigDecimal latitude) {
+            this.alarmId = alarmId;
+            this.warningName = warningName;
+            this.warningType = warningType;
+            this.level = level;
+            this.status = status;
+            this.eventTime = eventTime;
+            this.updatedTime = updatedTime;
+            this.value = value;
+            this.unit = unit;
+            this.deviceCode = deviceCode;
+            this.deviceName = deviceName;
+            this.location = location;
+            this.typeCode = typeCode;
+            this.longitude = longitude;
+            this.latitude = latitude;
+        }
+
+        public String getAlarmId() {
+            return alarmId;
+        }
+
+        public String getWarningName() {
+            return warningName;
+        }
+
+        public String getWarningType() {
+            return warningType;
+        }
+
+        public Integer getLevel() {
+            return level;
+        }
+
+        public String getStatus() {
+            return status;
+        }
+
+        public String getEventTime() {
+            return eventTime;
+        }
+
+        public String getUpdatedTime() {
+            return updatedTime;
+        }
+
+        public BigDecimal getValue() {
+            return value;
+        }
+
+        public String getUnit() {
+            return unit;
+        }
+
+        public String getDeviceCode() {
+            return deviceCode;
+        }
+
+        public String getDeviceName() {
+            return deviceName;
+        }
+
+        public String getLocation() {
+            return location;
+        }
+
+        public String getTypeCode() {
+            return typeCode;
+        }
+
+        public BigDecimal getLongitude() {
+            return longitude;
+        }
+
+        public BigDecimal getLatitude() {
+            return latitude;
+        }
+    }
+
+    public static class SourceWorkOrderLink {
+        private final String eventNo;
+        private final String workOrderCode;
+        private final Integer orderLevel;
+        private final String orderDesc;
+        private final Integer stateCode;
+        private final String createTime;
+        private final String updateTime;
+        private final String planFinishTime;
+        private final String realFinishTime;
+
+        public SourceWorkOrderLink(
+                String eventNo,
+                String workOrderCode,
+                Integer orderLevel,
+                String orderDesc,
+                Integer stateCode,
+                String createTime,
+                String updateTime,
+                String planFinishTime,
+                String realFinishTime) {
+            this.eventNo = eventNo;
+            this.workOrderCode = workOrderCode;
+            this.orderLevel = orderLevel;
+            this.orderDesc = orderDesc;
+            this.stateCode = stateCode;
+            this.createTime = createTime;
+            this.updateTime = updateTime;
+            this.planFinishTime = planFinishTime;
+            this.realFinishTime = realFinishTime;
+        }
+
+        public String getEventNo() {
+            return eventNo;
+        }
+
+        public String getWorkOrderCode() {
+            return workOrderCode;
+        }
+
+        public Integer getOrderLevel() {
+            return orderLevel;
+        }
+
+        public String getOrderDesc() {
+            return orderDesc;
+        }
+
+        public Integer getStateCode() {
+            return stateCode;
+        }
+
+        public String getCreateTime() {
+            return createTime;
+        }
+
+        public String getUpdateTime() {
+            return updateTime;
+        }
+
+        public String getPlanFinishTime() {
+            return planFinishTime;
+        }
+
+        public String getRealFinishTime() {
+            return realFinishTime;
+        }
+    }
+}

+ 26 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/EmptyCockpitCurrentEventsAdapter.java

@@ -0,0 +1,26 @@
+package com.zksy.cockpit.event;
+
+import org.springframework.stereotype.Service;
+
+import java.time.Clock;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+
+@Service
+public class EmptyCockpitCurrentEventsAdapter implements CockpitCurrentEventsAdapter {
+    private final Clock clock;
+
+    public EmptyCockpitCurrentEventsAdapter(Clock clock) {
+        this.clock = clock;
+    }
+
+    @Override
+    public CockpitCurrentEventsSnapshot load() {
+        return CockpitCurrentEventsSnapshot.empty(businessTime());
+    }
+
+    private String businessTime() {
+        return DateTimeFormatter.ISO_OFFSET_DATE_TIME
+                .format(clock.instant().atZone(ZoneId.of("Asia/Shanghai")));
+    }
+}