Quellcode durchsuchen

feat(cockpit): add work order nine states and timelines (#25)

Kazerin vor 1 Monat
Ursprung
Commit
108b646c98
16 geänderte Dateien mit 1153 neuen und 0 gelöschten Zeilen
  1. 126 0
      docs/cockpit/work-order-nine-states-demo.html
  2. 103 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/cockpit/CockpitWorkOrderReadController.java
  3. 275 0
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitWorkOrderReadControllerTest.java
  4. 157 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/BusinessCockpitWorkOrderReadAdapter.java
  5. 20 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/CockpitWorkOrderEventAssociation.java
  6. 9 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/CockpitWorkOrderReadAdapter.java
  7. 50 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/CockpitWorkOrderReadResponse.java
  8. 119 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/CockpitWorkOrderReadService.java
  9. 26 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/CockpitWorkOrderReadSnapshot.java
  10. 28 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/CockpitWorkOrderState.java
  11. 39 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/CockpitWorkOrderStateDictionary.java
  12. 72 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/CockpitWorkOrderSummary.java
  13. 52 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/CockpitWorkOrderTimelineEntry.java
  14. 38 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/CockpitWorkOrderTimelineResponse.java
  15. 18 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/CockpitWorkOrderTimelineSnapshot.java
  16. 21 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/EmptyCockpitWorkOrderReadAdapter.java

+ 126 - 0
docs/cockpit/work-order-nine-states-demo.html

@@ -0,0 +1,126 @@
+<!doctype html>
+<html lang="zh-CN">
+<head>
+  <meta charset="utf-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1" />
+  <title>Cockpit Work Order Nine States Demo</title>
+  <style>
+    body { margin:0; min-height:100vh; background:#08131f; color:#e8f1f8; font:15px/1.5 "Microsoft YaHei",sans-serif; }
+    main { width:min(1280px,calc(100vw - 32px)); margin:28px auto; display:grid; gap:16px; }
+    header, section { background:#0e1e2d; border:1px solid #21465f; padding:20px; }
+    h1,h2,h3 { margin:0 0 10px; } h1 { font-size:22px; } h2 { font-size:17px; } h3 { font-size:15px; }
+    .endpoint { color:#78a2b8; font-family:Consolas,monospace; margin:0; }
+    .toolbar { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:14px; }
+    button { border:1px solid #2f7098; background:#123247; color:#dff2ff; padding:8px 14px; cursor:pointer; }
+    button.active { outline:2px solid #61c2ff; }
+    .layout { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:16px; align-items:start; }
+    .orders { display:grid; gap:12px; }
+    .order { border:1px solid #275b74; background:#0b1922; padding:12px; }
+    .order.alarm { border-color:#ff7a7a; } .order.warning { border-color:#f2c94c; }
+    .title { display:flex; justify-content:space-between; gap:8px; align-items:flex-start; }
+    .code { color:#78a2b8; font-family:Consolas,monospace; font-size:12px; overflow-wrap:anywhere; }
+    .badge { font-size:12px; padding:3px 8px; border:1px solid; white-space:nowrap; }
+    .active { color:#68d095; border-color:#68d095; } .legacy { color:#ff7a7a; border-color:#ff7a7a; } .finished { color:#a9b7c0; border-color:#a9b7c0; }
+    .timeline { margin-top:10px; display:grid; gap:8px; }
+    .entry { border-left:3px solid #61c2ff; background:#0e1e2d; padding:8px 10px; }
+    .entry.legacy { border-left-color:#ff7a7a; } .entry.finished { border-left-color:#a9b7c0; }
+    .meta { color:#8fb3c7; font-size:13px; margin:6px 0 0; overflow-wrap:anywhere; }
+    .dictionary { display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:8px; }
+    .state { border:1px solid #275b74; background:#0b1922; padding:8px; } .state.legacy { border-color:#ff7a7a; }
+    .mono { font-family:Consolas,monospace; overflow-wrap:anywhere; } pre { max-height:340px; overflow:auto; background:#071019; border:1px solid #275b74; padding:12px; }
+    .note { color:#8fb3c7; font-size:13px; }
+  </style>
+</head>
+<body>
+<main>
+  <header>
+    <h1>驾驶舱事件关联工单九态演示</h1>
+    <p class="endpoint">GET /api/cockpit/v1/equipments/{deviceCode}/work-orders</p>
+    <p class="endpoint">GET /api/cockpit/v1/events/{eventSource}/{eventCode}/work-orders</p>
+    <p class="endpoint">GET /api/cockpit/v1/work-orders/{workOrderCode}/timeline</p>
+  </header>
+  <section>
+    <div class="toolbar">
+      <button type="button" data-mode="all" class="active">九态总览</button>
+      <button type="button" data-mode="multi">同设备多类型报警</button>
+    </div>
+    <div class="layout">
+      <div>
+        <h2>工单列表</h2>
+        <div id="orders" class="orders"></div>
+      </div>
+      <div>
+        <h2>状态字典</h2>
+        <div id="dictionary" class="dictionary"></div>
+      </div>
+    </div>
+  </section>
+  <section>
+    <h2>响应契约</h2>
+    <p class="note">legacyState / legacyName 保留现网九态兼容编码;authoritativeState 是 ADR-0008 七态权威映射,旧 8/9 标记 deprecated,避免新功能依赖已废止状态。</p>
+    <pre id="payload" class="mono"></pre>
+  </section>
+</main>
+<script>
+  const stateNames = ['待派单','待接单','已接单','处理中','待验收','已结案','已驳回','延期审核','已延期'];
+  const authoritative = [
+    {code:1,name:'待派单'}, {code:2,name:'待接单'}, {code:3,name:'已接单'}, {code:4,name:'处理中'},
+    {code:5,name:'待验收'}, {code:6,name:'已结案'}, {code:7,name:'已取消'}, null, null
+  ];
+  const stateDictionary = stateNames.map((name, index) => ({
+    legacyCode:index + 1, legacyName:name,
+    authoritativeCode:authoritative[index]?.code ?? null, authoritativeName:authoritative[index]?.name ?? null,
+    deprecated:index >= 6
+  }));
+  const summary = (code, state, source, eventCode, desc) => ({
+    workOrderCode:code, deviceCode:'TC-PS-WS-202609020012', orderType:1, orderTypeName:'故障维修',
+    orderLevel:2, orderDesc:desc,
+    legacyState:state, legacyStateName:stateNames[state - 1],
+    authoritativeState:authoritative[state - 1]?.code ?? null, authoritativeStateName:authoritative[state - 1]?.name ?? null,
+    deprecatedState:state >= 7,
+    createTime:'2026-09-02T09:00:00+08:00', updateTime:'2026-09-02T10:00:00+08:00',
+    planFinishTime:'2026-09-03T09:00:00+08:00', realFinishTime:state === 6 ? '2026-09-02T10:00:00+08:00' : null,
+    eventAssociation:{ eventSource:source, eventCode, associatedAt:'2026-09-02T10:00:00+08:00' }
+  });
+  const allOrders = stateNames.map((name, index) => summary('WO-202609020000' + (index + 1), index + 1, 'ALARM', 'ALM-20260902-00' + (index + 1), '演示第 ' + (index + 1) + ' 态'));
+  const multiOrders = [
+    summary('WO-2026090200101', 4, 'ALARM', 'ALM-WS_ENV_HIGH-001', '污水COD超标维修'),
+    summary('WO-2026090200102', 5, 'ALARM', 'ALM-WS_ENV_PH-002', '污水pH越界维修')
+  ];
+  const timelines = {
+    'WO-2026090200101': ['创建:待派单','派单:待接单','开始处置:处理中'],
+    'WO-2026090200102': ['创建:待派单','派单:待接单','提交验收:待验收']
+  };
+  function renderOrder(order) {
+    const timeline = timelines[order.workOrderCode] || ['创建:' + order.legacyStateName];
+    return `<article class="order ${order.eventAssociation.eventSource === 'ALARM' ? 'alarm' : 'warning'}">
+      <div class="title">
+        <div><h3>${order.orderDesc}</h3><div class="code">${order.workOrderCode}</div></div>
+        <span class="badge ${order.deprecatedState ? 'legacy' : order.legacyState === 6 ? 'finished' : 'active'}">${order.legacyState} ${order.legacyStateName}</span>
+      </div>
+      <p class="meta">权威状态:${order.authoritativeState ?? '已废止'} ${order.authoritativeStateName ?? ''}<br />事件:${order.eventAssociation.eventSource} ${order.eventAssociation.eventCode}</p>
+      <div class="timeline">${timeline.map(item => `<div class="entry ${order.deprecatedState ? 'legacy' : order.legacyState === 6 ? 'finished' : ''}">${item}</div>`).join('')}</div>
+    </article>`;
+  }
+  function render(mode) {
+    document.querySelectorAll('button[data-mode]').forEach(button => button.classList.toggle('active', button.dataset.mode === mode));
+    const orders = mode === 'all' ? allOrders : multiOrders;
+    document.getElementById('orders').innerHTML = orders.map(renderOrder).join('');
+    document.getElementById('dictionary').innerHTML = stateDictionary.map(state => `
+      <div class="state ${state.deprecated ? 'legacy' : ''}"><strong>${state.legacyCode} ${state.legacyName}</strong>
+        <p class="meta">${state.authoritativeCode ? '权威:' + state.authoritativeCode + ' ' + state.authoritativeName : '已废止'}</p>
+      </div>`).join('');
+    document.getElementById('payload').textContent = JSON.stringify({
+      code:200, msg:'操作成功', data:{
+        requestId:'work-order-demo', apiVersion:'v1', readOnly:true,
+        deviceCode:'TC-PS-WS-202609020012', eventSource:mode === 'all' ? null : 'ALARM',
+        eventCode:mode === 'all' ? null : 'WS_ENV_MULTI', state:'READY', stateReason:'READY',
+        stateDictionary, workOrders:orders
+      }
+    }, null, 2);
+  }
+  document.querySelectorAll('button[data-mode]').forEach(button => button.addEventListener('click', () => render(button.dataset.mode)));
+  render('all');
+</script>
+</body>
+</html>

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

@@ -0,0 +1,103 @@
+package com.zksy.web.controller.cockpit;
+
+import com.zksy.cockpit.foundation.CockpitDeviceNotFoundException;
+import com.zksy.cockpit.foundation.CockpitErrorPayload;
+import com.zksy.cockpit.foundation.CockpitInvalidRequestException;
+import com.zksy.cockpit.foundation.CockpitReadException;
+import com.zksy.cockpit.workorder.CockpitWorkOrderReadResponse;
+import com.zksy.cockpit.workorder.CockpitWorkOrderReadService;
+import com.zksy.cockpit.workorder.CockpitWorkOrderTimelineResponse;
+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.PathVariable;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.util.UriUtils;
+
+import javax.servlet.http.HttpServletRequest;
+import java.nio.charset.StandardCharsets;
+
+@RestController
+public class CockpitWorkOrderReadController {
+    private static final String REQUEST_ID_HEADER = "X-Request-Id";
+
+    private final CockpitWorkOrderReadService workOrderService;
+
+    public CockpitWorkOrderReadController(CockpitWorkOrderReadService workOrderService) {
+        this.workOrderService = workOrderService;
+    }
+
+    @GetMapping("/api/cockpit/v1/equipments/{deviceCode}/work-orders")
+    @PreAuthorize("isAuthenticated()")
+    public ResponseEntity<AjaxResult> deviceWorkOrders(
+            @PathVariable("deviceCode") String deviceCode,
+            @RequestHeader(value = REQUEST_ID_HEADER, required = false) String requestId) {
+        CockpitWorkOrderReadResponse response = workOrderService.loadByDevice(
+                resolveRequestId(requestId), decodePathSegment(deviceCode));
+        return ResponseEntity.ok(AjaxResult.success(response));
+    }
+
+    @GetMapping("/api/cockpit/v1/events/{eventSource}/{eventCode}/work-orders")
+    @PreAuthorize("isAuthenticated()")
+    public ResponseEntity<AjaxResult> eventWorkOrders(
+            @PathVariable("eventSource") String eventSource,
+            @PathVariable("eventCode") String eventCode,
+            @RequestHeader(value = REQUEST_ID_HEADER, required = false) String requestId) {
+        CockpitWorkOrderReadResponse response = workOrderService.loadByEvent(
+                resolveRequestId(requestId),
+                decodePathSegment(eventSource),
+                decodePathSegment(eventCode));
+        return ResponseEntity.ok(AjaxResult.success(response));
+    }
+
+    @GetMapping("/api/cockpit/v1/work-orders/{workOrderCode}/timeline")
+    @PreAuthorize("isAuthenticated()")
+    public ResponseEntity<AjaxResult> workOrderTimeline(
+            @PathVariable("workOrderCode") String workOrderCode,
+            @RequestHeader(value = REQUEST_ID_HEADER, required = false) String requestId) {
+        CockpitWorkOrderTimelineResponse response = workOrderService.loadTimeline(
+                resolveRequestId(requestId), decodePathSegment(workOrderCode));
+        return ResponseEntity.ok(AjaxResult.success(response));
+    }
+
+    @ExceptionHandler(CockpitInvalidRequestException.class)
+    public ResponseEntity<AjaxResult> handleInvalidRequest(HttpServletRequest request) {
+        return error(HttpStatus.BAD_REQUEST, workOrderService.invalidRequest(
+                resolveRequestId(request.getHeader(REQUEST_ID_HEADER))));
+    }
+
+    @ExceptionHandler(CockpitDeviceNotFoundException.class)
+    public ResponseEntity<AjaxResult> handleNotFound(HttpServletRequest request) {
+        return error(HttpStatus.NOT_FOUND, workOrderService.notFound(
+                resolveRequestId(request.getHeader(REQUEST_ID_HEADER))));
+    }
+
+    @ExceptionHandler(CockpitReadException.class)
+    public ResponseEntity<AjaxResult> handleReadFailure(HttpServletRequest request) {
+        return error(HttpStatus.INTERNAL_SERVER_ERROR, workOrderService.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();
+    }
+
+    private String decodePathSegment(String value) {
+        try {
+            return UriUtils.decode(value, StandardCharsets.UTF_8);
+        } catch (IllegalArgumentException exception) {
+            throw new CockpitInvalidRequestException("invalid path segment encoding");
+        }
+    }
+}

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

@@ -0,0 +1,275 @@
+package com.zksy.web.controller.cockpit;
+
+import com.zksy.cockpit.foundation.CockpitDeviceNotFoundException;
+import com.zksy.cockpit.foundation.CockpitReadException;
+import com.zksy.cockpit.workorder.CockpitWorkOrderEventAssociation;
+import com.zksy.cockpit.workorder.CockpitWorkOrderReadAdapter;
+import com.zksy.cockpit.workorder.CockpitWorkOrderReadService;
+import com.zksy.cockpit.workorder.CockpitWorkOrderReadSnapshot;
+import com.zksy.cockpit.workorder.CockpitWorkOrderState;
+import com.zksy.cockpit.workorder.CockpitWorkOrderStateDictionary;
+import com.zksy.cockpit.workorder.CockpitWorkOrderSummary;
+import com.zksy.cockpit.workorder.CockpitWorkOrderTimelineEntry;
+import com.zksy.cockpit.workorder.CockpitWorkOrderTimelineSnapshot;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import java.lang.reflect.Method;
+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.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 CockpitWorkOrderReadControllerTest {
+    private static final String DEVICE_CODE = "TC-PS-WS-202609020012";
+
+    private CockpitWorkOrderReadAdapter adapter;
+    private MockMvc mockMvc;
+
+    @BeforeEach
+    void setUp() {
+        adapter = mock(CockpitWorkOrderReadAdapter.class);
+        CockpitWorkOrderReadService service = new CockpitWorkOrderReadService(
+                adapter,
+                Clock.fixed(Instant.parse("2026-09-02T02:00:00Z"), ZoneOffset.UTC));
+        mockMvc = MockMvcBuilders
+                .standaloneSetup(new CockpitWorkOrderReadController(service))
+                .build();
+    }
+
+    @Test
+    void deviceWorkOrdersReturnNineStateDictionaryAndAuthoritativeMapping() throws Exception {
+        when(adapter.loadByDevice(DEVICE_CODE)).thenReturn(nineStateFixture());
+
+        mockMvc.perform(get("/api/cockpit/v1/equipments/" + DEVICE_CODE + "/work-orders")
+                        .header("X-Request-Id", "work-order-device-001"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data.requestId").value("work-order-device-001"))
+                .andExpect(jsonPath("$.data.apiVersion").value("v1"))
+                .andExpect(jsonPath("$.data.readOnly").value(true))
+                .andExpect(jsonPath("$.data.deviceCode").value(DEVICE_CODE))
+                .andExpect(jsonPath("$.data.stateDictionary", hasSize(9)))
+                .andExpect(jsonPath("$.data.stateDictionary[0].legacyCode").value(1))
+                .andExpect(jsonPath("$.data.stateDictionary[0].legacyName").value("待派单"))
+                .andExpect(jsonPath("$.data.stateDictionary[5].legacyCode").value(6))
+                .andExpect(jsonPath("$.data.stateDictionary[5].legacyName").value("已结案"))
+                .andExpect(jsonPath("$.data.stateDictionary[6].legacyCode").value(7))
+                .andExpect(jsonPath("$.data.stateDictionary[6].legacyName").value("已驳回"))
+                .andExpect(jsonPath("$.data.stateDictionary[6].authoritativeCode").value(7))
+                .andExpect(jsonPath("$.data.stateDictionary[6].authoritativeName").value("已取消"))
+                .andExpect(jsonPath("$.data.stateDictionary[7].deprecated").value(true))
+                .andExpect(jsonPath("$.data.stateDictionary[8].deprecated").value(true))
+                .andExpect(jsonPath("$.data.workOrders", hasSize(9)))
+                .andExpect(jsonPath("$.data.workOrders[0].legacyState").value(1))
+                .andExpect(jsonPath("$.data.workOrders[0].authoritativeState").value(1))
+                .andExpect(jsonPath("$.data.workOrders[8].legacyState").value(9))
+                .andExpect(jsonPath("$.data.workOrders[8].authoritativeState").doesNotExist());
+    }
+
+    @Test
+    void eventWorkOrdersPreserveAssociationsAndSeparateEventFamilies() throws Exception {
+        when(adapter.loadByEvent("ALARM", "ALM-20260902-001")).thenReturn(singleAlarmFixture());
+        when(adapter.loadByEvent("WARNING", "WRN-20260902-001")).thenReturn(singleWarningFixture());
+
+        mockMvc.perform(get("/api/cockpit/v1/events/ALARM/ALM-20260902-001/work-orders")
+                        .header("X-Request-Id", "work-order-alarm-001"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.eventSource").value("ALARM"))
+                .andExpect(jsonPath("$.data.eventCode").value("ALM-20260902-001"))
+                .andExpect(jsonPath("$.data.workOrders", hasSize(1)))
+                .andExpect(jsonPath("$.data.workOrders[0].eventAssociation.eventSource").value("ALARM"))
+                .andExpect(jsonPath("$.data.workOrders[0].eventAssociation.eventCode").value("ALM-20260902-001"));
+
+        mockMvc.perform(get("/api/cockpit/v1/events/WARNING/WRN-20260902-001/work-orders")
+                        .header("X-Request-Id", "work-order-warning-001"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.eventSource").value("WARNING"))
+                .andExpect(jsonPath("$.data.eventCode").value("WRN-20260902-001"))
+                .andExpect(jsonPath("$.data.workOrders", hasSize(1)))
+                .andExpect(jsonPath("$.data.workOrders[0].eventAssociation.eventSource").value("WARNING"))
+                .andExpect(jsonPath("$.data.workOrders[0].eventAssociation.eventCode").value("WRN-20260902-001"));
+    }
+
+    @Test
+    void sameDeviceDifferentAlarmTypesCanHaveSeparateWorkOrders() throws Exception {
+        when(adapter.loadByDevice(DEVICE_CODE)).thenReturn(new CockpitWorkOrderReadSnapshot(
+                DEVICE_CODE,
+                null,
+                null,
+                Arrays.asList(
+                        summary("WO-2026090200101", 4, "ALM-WS_ENV_HIGH-001"),
+                        summary("WO-2026090200102", 5, "ALM-WS_ENV_PH-002"))));
+
+        mockMvc.perform(get("/api/cockpit/v1/equipments/" + DEVICE_CODE + "/work-orders"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.workOrders", hasSize(2)))
+                .andExpect(jsonPath("$.data.workOrders[0].eventAssociation.eventCode").value("ALM-WS_ENV_HIGH-001"))
+                .andExpect(jsonPath("$.data.workOrders[1].eventAssociation.eventCode").value("ALM-WS_ENV_PH-002"));
+    }
+
+    @Test
+    void timelinesAreIndependentBetweenWorkOrders() throws Exception {
+        when(adapter.loadTimeline("WO-2026090200001")).thenReturn(timelineFixture("WO-2026090200001", "3"));
+        when(adapter.loadTimeline("WO-2026090200002")).thenReturn(timelineFixture("WO-2026090200002", "6"));
+
+        mockMvc.perform(get("/api/cockpit/v1/work-orders/WO-2026090200001/timeline")
+                        .header("X-Request-Id", "timeline-001"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.requestId").value("timeline-001"))
+                .andExpect(jsonPath("$.data.workOrderCode").value("WO-2026090200001"))
+                .andExpect(jsonPath("$.data.stateDictionary", hasSize(9)))
+                .andExpect(jsonPath("$.data.timelineEntries", hasSize(3)))
+                .andExpect(jsonPath("$.data.timelineEntries[0].workOrderCode").value("WO-2026090200001"))
+                .andExpect(jsonPath("$.data.timelineEntries[0].legacyState").value(1))
+                .andExpect(jsonPath("$.data.timelineEntries[0].authoritativeState").value(1))
+                .andExpect(jsonPath("$.data.timelineEntries[2].legacyState").value(3))
+                .andExpect(jsonPath("$.data.timelineEntries[2].authoritativeState").value(3));
+
+        mockMvc.perform(get("/api/cockpit/v1/work-orders/WO-2026090200002/timeline")
+                        .header("X-Request-Id", "timeline-002"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.workOrderCode").value("WO-2026090200002"))
+                .andExpect(jsonPath("$.data.timelineEntries", hasSize(1)))
+                .andExpect(jsonPath("$.data.timelineEntries[0].workOrderCode").value("WO-2026090200002"))
+                .andExpect(jsonPath("$.data.timelineEntries[0].legacyState").value(6))
+                .andExpect(jsonPath("$.data.timelineEntries[0].authoritativeState").value(6));
+    }
+
+    @Test
+    void emptyDeviceWorkOrdersReturnSuccessfulEmptyState() throws Exception {
+        when(adapter.loadByDevice(DEVICE_CODE)).thenReturn(new CockpitWorkOrderReadSnapshot(
+                DEVICE_CODE, null, null, Collections.emptyList()));
+
+        mockMvc.perform(get("/api/cockpit/v1/equipments/" + DEVICE_CODE + "/work-orders"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.state").value("EMPTY"))
+                .andExpect(jsonPath("$.data.stateReason").value("NO_WORK_ORDERS"))
+                .andExpect(jsonPath("$.data.workOrders", hasSize(0)));
+    }
+
+    @Test
+    void missingDeviceAndAdapterFailureUseTypedReadOnlyErrors() throws Exception {
+        when(adapter.loadByDevice("MISSING")).thenThrow(new CockpitDeviceNotFoundException("device not found"));
+        when(adapter.loadByDevice("BROKEN")).thenThrow(new CockpitReadException("read failed"));
+
+        mockMvc.perform(get("/api/cockpit/v1/equipments/MISSING/work-orders"))
+                .andExpect(status().isNotFound())
+                .andExpect(jsonPath("$.data.category").value("NOT_FOUND"));
+        mockMvc.perform(get("/api/cockpit/v1/equipments/BROKEN/work-orders"))
+                .andExpect(status().isInternalServerError())
+                .andExpect(jsonPath("$.data.category").value("SERVER_ERROR"));
+    }
+
+    @Test
+    void controllerExposesReadOnlyMappedMethodsOnly() throws Exception {
+        Method[] methods = CockpitWorkOrderReadController.class.getDeclaredMethods();
+        for (Method method : methods) {
+            if (method.isAnnotationPresent(org.springframework.web.bind.annotation.GetMapping.class)) {
+                assertTrue(!method.isAnnotationPresent(org.springframework.web.bind.annotation.PostMapping.class)
+                        && !method.isAnnotationPresent(org.springframework.web.bind.annotation.PutMapping.class)
+                        && !method.isAnnotationPresent(org.springframework.web.bind.annotation.PatchMapping.class)
+                        && !method.isAnnotationPresent(org.springframework.web.bind.annotation.DeleteMapping.class));
+            }
+        }
+
+        mockMvc.perform(post("/api/cockpit/v1/equipments/" + DEVICE_CODE + "/work-orders"))
+                .andExpect(status().isMethodNotAllowed());
+        verifyNoInteractions(adapter);
+    }
+
+    private void assertTrue(boolean condition) {
+        org.junit.jupiter.api.Assertions.assertTrue(condition);
+    }
+
+    private CockpitWorkOrderReadSnapshot nineStateFixture() {
+        List<CockpitWorkOrderSummary> summaries = new java.util.ArrayList<>();
+        for (int state = 1; state <= 9; state++) {
+            summaries.add(summary("WO-202609020000" + state, state, "ALM-20260902-00" + state));
+        }
+        return new CockpitWorkOrderReadSnapshot(DEVICE_CODE, null, null, summaries);
+    }
+
+    private CockpitWorkOrderReadSnapshot singleAlarmFixture() {
+        return new CockpitWorkOrderReadSnapshot(
+                DEVICE_CODE,
+                "ALARM",
+                "ALM-20260902-001",
+                Collections.singletonList(summary("WO-2026090200001", 1, "ALM-20260902-001")));
+    }
+
+    private CockpitWorkOrderReadSnapshot singleWarningFixture() {
+        return new CockpitWorkOrderReadSnapshot(
+                DEVICE_CODE,
+                "WARNING",
+                "WRN-20260902-001",
+                Collections.singletonList(new CockpitWorkOrderSummary(
+                        "WO-2026090200002", DEVICE_CODE, 1, "故障维修", 2, "处理异常", 2, "待接单", 2, "待接单", false,
+                        "2026-09-02T09:59:30+08:00", "2026-09-02T10:00:00+08:00", "2026-09-02T09:00:00+08:00", "2026-09-02T09:30:00+08:00",
+                        new CockpitWorkOrderEventAssociation("WARNING", "WRN-20260902-001", "2026-09-02T10:00:00+08:00"))));
+    }
+
+    private CockpitWorkOrderSummary summary(String workOrderCode, int legacyState, String eventCode) {
+        CockpitWorkOrderState state = CockpitWorkOrderStateDictionary.byLegacyCode(legacyState);
+        return new CockpitWorkOrderSummary(
+                workOrderCode,
+                DEVICE_CODE,
+                1,
+                "故障维修",
+                2,
+                "处理异常",
+                state.getLegacyCode(),
+                state.getLegacyName(),
+                state.getAuthoritativeCode(),
+                state.getAuthoritativeName(),
+                state.isDeprecated(),
+                "2026-09-02T09:59:30+08:00",
+                "2026-09-02T10:00:00+08:00",
+                "2026-09-02T09:00:00+08:00",
+                "2026-09-02T09:30:00+08:00",
+                new CockpitWorkOrderEventAssociation("ALARM", eventCode, "2026-09-02T10:00:00+08:00"));
+    }
+
+    private CockpitWorkOrderTimelineSnapshot timelineFixture(String workOrderCode, String lastState) {
+        int state = Integer.parseInt(lastState);
+        CockpitWorkOrderSummary summary = summary(workOrderCode, state, "ALM-20260902-001");
+        if (state >= 6) {
+            return new CockpitWorkOrderTimelineSnapshot(
+                    summary,
+                    Collections.singletonList(entry(workOrderCode, 6, 6)));
+        }
+        return new CockpitWorkOrderTimelineSnapshot(
+                summary,
+                Arrays.asList(
+                        entry(workOrderCode, 1, 1),
+                        entry(workOrderCode, 2, 2),
+                        entry(workOrderCode, 3, 3)));
+    }
+    private CockpitWorkOrderTimelineEntry entry(String workOrderCode, int legacyState, int authoritativeState) {
+        return new CockpitWorkOrderTimelineEntry(
+                (long) legacyState,
+                workOrderCode,
+                legacyState,
+                CockpitWorkOrderStateDictionary.byLegacyCode(legacyState).getLegacyName(),
+                authoritativeState,
+                CockpitWorkOrderStateDictionary.byLegacyCode(legacyState).getAuthoritativeName(),
+                false,
+                "创建",
+                "创建工单",
+                "系统",
+                "2026-09-02T09:00:00+08:00");
+    }
+}

+ 157 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/BusinessCockpitWorkOrderReadAdapter.java

@@ -0,0 +1,157 @@
+package com.zksy.cockpit.workorder;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.zksy.base.domain.WorkOrder;
+import com.zksy.base.domain.WorkOrderLog;
+import com.zksy.base.service.BusinessWorkOrderService;
+import com.zksy.base.warning.domain.AlarmWarningRel;
+import com.zksy.base.warning.domain.EarlyWarning;
+import com.zksy.base.warning.mapper.AlarmWarningRelMapper;
+import com.zksy.base.warning.service.EarlyWarningService;
+import org.springframework.context.annotation.Primary;
+import org.springframework.stereotype.Component;
+
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+@Component
+@Primary
+public class BusinessCockpitWorkOrderReadAdapter implements CockpitWorkOrderReadAdapter {
+    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
+
+    private final BusinessWorkOrderService workOrderService;
+    private final EarlyWarningService earlyWarningService;
+    private final AlarmWarningRelMapper alarmWarningRelMapper;
+
+    public BusinessCockpitWorkOrderReadAdapter(
+            BusinessWorkOrderService workOrderService,
+            EarlyWarningService earlyWarningService,
+            AlarmWarningRelMapper alarmWarningRelMapper) {
+        this.workOrderService = workOrderService;
+        this.earlyWarningService = earlyWarningService;
+        this.alarmWarningRelMapper = alarmWarningRelMapper;
+    }
+
+    @Override
+    public CockpitWorkOrderReadSnapshot loadByDevice(String deviceCode) {
+        List<WorkOrder> orders = workOrderService.lambdaQuery()
+                .eq(WorkOrder::getDeviceCode, deviceCode)
+                .orderByDesc(WorkOrder::getCreateTime)
+                .list();
+        List<CockpitWorkOrderSummary> summaries = orders.stream()
+                .map(order -> toSummary(order, null, null))
+                .collect(Collectors.toList());
+        return new CockpitWorkOrderReadSnapshot(deviceCode, null, null, summaries);
+    }
+
+    @Override
+    public CockpitWorkOrderReadSnapshot loadByEvent(String eventSource, String eventCode) {
+        if ("ALARM".equals(eventSource)) {
+            List<WorkOrder> orders = workOrderService.lambdaQuery()
+                    .eq(WorkOrder::getAlarmId, eventCode)
+                    .orderByDesc(WorkOrder::getCreateTime)
+                    .list();
+            return new CockpitWorkOrderReadSnapshot(null, eventSource, eventCode,
+                    orders.stream().map(order -> toSummary(order, eventSource, eventCode)).collect(Collectors.toList()));
+        }
+        if ("WARNING".equals(eventSource)) {
+            EarlyWarning warning = earlyWarningService.lambdaQuery()
+                    .eq(EarlyWarning::getWarningNo, eventCode)
+                    .one();
+            if (warning == null) {
+                return null;
+            }
+            List<AlarmWarningRel> relations = alarmWarningRelMapper.selectList(
+                    new LambdaQueryWrapper<AlarmWarningRel>()
+                            .eq(AlarmWarningRel::getWarningId, warning.getWarningId()));
+            List<String> alarmIds = relations.stream()
+                    .map(AlarmWarningRel::getAlarmId)
+                    .filter(Objects::nonNull)
+                    .distinct()
+                    .collect(Collectors.toList());
+            List<WorkOrder> orders = alarmIds.isEmpty()
+                    ? Collections.emptyList()
+                    : workOrderService.lambdaQuery()
+                            .in(WorkOrder::getAlarmId, alarmIds)
+                            .orderByDesc(WorkOrder::getCreateTime)
+                            .list();
+            List<CockpitWorkOrderSummary> summaries = orders.stream()
+                    .map(order -> toSummary(order, eventSource, eventCode))
+                    .collect(Collectors.toList());
+            return new CockpitWorkOrderReadSnapshot(null, eventSource, eventCode, summaries);
+        }
+        return null;
+    }
+
+    @Override
+    public CockpitWorkOrderTimelineSnapshot loadTimeline(String workOrderCode) {
+        WorkOrder order = workOrderService.lambdaQuery()
+                .eq(WorkOrder::getOrderNo, workOrderCode)
+                .one();
+        if (order == null) {
+            return null;
+        }
+        List<WorkOrderLog> logs = workOrderService.getWorkOrderLogs(order.getOrderId());
+        List<CockpitWorkOrderTimelineEntry> entries = logs.stream()
+                .map(log -> toTimelineEntry(order, log))
+                .collect(Collectors.toList());
+        return new CockpitWorkOrderTimelineSnapshot(toSummary(order, null, null), entries);
+    }
+
+    private CockpitWorkOrderSummary toSummary(WorkOrder order, String eventSource, String eventCode) {
+        CockpitWorkOrderState state = CockpitWorkOrderStateDictionary.byLegacyCode(order.getOrderStatus());
+        return new CockpitWorkOrderSummary(
+                order.getOrderNo(),
+                order.getDeviceCode(),
+                order.getOrderType(),
+                orderTypeName(order.getOrderType()),
+                order.getOrderLevel(),
+                order.getOrderDesc(),
+                state.getLegacyCode(),
+                state.getLegacyName(),
+                state.getAuthoritativeCode(),
+                state.getAuthoritativeName(),
+                state.isDeprecated(),
+                format(order.getCreateTime()),
+                format(order.getUpdateTime()),
+                format(order.getPlanFinishTime()),
+                format(order.getRealFinishTime()),
+                eventSource == null ? null : new CockpitWorkOrderEventAssociation(
+                        eventSource, eventCode, format(order.getCreateTime())));
+    }
+
+    private CockpitWorkOrderTimelineEntry toTimelineEntry(WorkOrder order, WorkOrderLog log) {
+        CockpitWorkOrderState state = CockpitWorkOrderStateDictionary.byLegacyCode(log.getOperType());
+        return new CockpitWorkOrderTimelineEntry(
+                log.getLogId(),
+                order.getOrderNo(),
+                state.getLegacyCode(),
+                state.getLegacyName(),
+                state.getAuthoritativeCode(),
+                state.getAuthoritativeName(),
+                state.isDeprecated(),
+                state.getLegacyName(),
+                log.getOperDesc(),
+                log.getOperUserName(),
+                format(log.getOperTime()));
+    }
+
+    private String orderTypeName(Integer orderType) {
+        if (orderType == null) return "未知";
+        switch (orderType) {
+            case 1: return "故障维修";
+            case 2: return "日常巡检";
+            case 3: return "设备保养";
+            default: return "未知";
+        }
+    }
+
+    private String format(Date value) {
+        return value == null ? null : TIME_FORMATTER.format(value.toInstant().atZone(ZoneId.of("Asia/Shanghai")));
+    }
+}

+ 20 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/CockpitWorkOrderEventAssociation.java

@@ -0,0 +1,20 @@
+package com.zksy.cockpit.workorder;
+
+public class CockpitWorkOrderEventAssociation {
+    private final String eventSource;
+    private final String eventCode;
+    private final String associatedAt;
+
+    public CockpitWorkOrderEventAssociation(
+            String eventSource,
+            String eventCode,
+            String associatedAt) {
+        this.eventSource = eventSource;
+        this.eventCode = eventCode;
+        this.associatedAt = associatedAt;
+    }
+
+    public String getEventSource() { return eventSource; }
+    public String getEventCode() { return eventCode; }
+    public String getAssociatedAt() { return associatedAt; }
+}

+ 9 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/CockpitWorkOrderReadAdapter.java

@@ -0,0 +1,9 @@
+package com.zksy.cockpit.workorder;
+
+public interface CockpitWorkOrderReadAdapter {
+    CockpitWorkOrderReadSnapshot loadByDevice(String deviceCode);
+
+    CockpitWorkOrderReadSnapshot loadByEvent(String eventSource, String eventCode);
+
+    CockpitWorkOrderTimelineSnapshot loadTimeline(String workOrderCode);
+}

+ 50 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/CockpitWorkOrderReadResponse.java

@@ -0,0 +1,50 @@
+package com.zksy.cockpit.workorder;
+
+import java.util.List;
+
+public class CockpitWorkOrderReadResponse {
+    private final String requestId;
+    private final String apiVersion;
+    private final boolean readOnly;
+    private final String deviceCode;
+    private final String eventSource;
+    private final String eventCode;
+    private final String state;
+    private final String stateReason;
+    private final List<CockpitWorkOrderState> stateDictionary;
+    private final List<CockpitWorkOrderSummary> workOrders;
+
+    public CockpitWorkOrderReadResponse(
+            String requestId,
+            String apiVersion,
+            boolean readOnly,
+            String deviceCode,
+            String eventSource,
+            String eventCode,
+            String state,
+            String stateReason,
+            List<CockpitWorkOrderState> stateDictionary,
+            List<CockpitWorkOrderSummary> workOrders) {
+        this.requestId = requestId;
+        this.apiVersion = apiVersion;
+        this.readOnly = readOnly;
+        this.deviceCode = deviceCode;
+        this.eventSource = eventSource;
+        this.eventCode = eventCode;
+        this.state = state;
+        this.stateReason = stateReason;
+        this.stateDictionary = stateDictionary;
+        this.workOrders = workOrders;
+    }
+
+    public String getRequestId() { return requestId; }
+    public String getApiVersion() { return apiVersion; }
+    public boolean isReadOnly() { return readOnly; }
+    public String getDeviceCode() { return deviceCode; }
+    public String getEventSource() { return eventSource; }
+    public String getEventCode() { return eventCode; }
+    public String getState() { return state; }
+    public String getStateReason() { return stateReason; }
+    public List<CockpitWorkOrderState> getStateDictionary() { return stateDictionary; }
+    public List<CockpitWorkOrderSummary> getWorkOrders() { return workOrders; }
+}

+ 119 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/CockpitWorkOrderReadService.java

@@ -0,0 +1,119 @@
+package com.zksy.cockpit.workorder;
+
+import com.zksy.cockpit.foundation.CockpitDeviceNotFoundException;
+import com.zksy.cockpit.foundation.CockpitErrorCategory;
+import com.zksy.cockpit.foundation.CockpitErrorPayload;
+import com.zksy.cockpit.foundation.CockpitInvalidRequestException;
+import com.zksy.cockpit.foundation.CockpitReadException;
+import org.springframework.stereotype.Service;
+
+import java.time.Clock;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@Service
+public class CockpitWorkOrderReadService {
+    private static final List<String> VALID_EVENT_SOURCES = Arrays.asList("ALARM", "WARNING");
+
+    private final CockpitWorkOrderReadAdapter adapter;
+    private final Clock clock;
+
+    public CockpitWorkOrderReadService(CockpitWorkOrderReadAdapter adapter, Clock clock) {
+        this.adapter = adapter;
+        this.clock = clock;
+    }
+
+    public CockpitWorkOrderReadResponse loadByDevice(String requestId, String deviceCode) {
+        validateCode(deviceCode, "device code is required");
+        CockpitWorkOrderReadSnapshot snapshot = adapter.loadByDevice(deviceCode.trim());
+        if (snapshot == null) {
+            throw new CockpitDeviceNotFoundException("device not found");
+        }
+        return response(requestId, snapshot);
+    }
+
+    public CockpitWorkOrderReadResponse loadByEvent(
+            String requestId,
+            String eventSource,
+            String eventCode) {
+        if (eventSource == null || !VALID_EVENT_SOURCES.contains(eventSource.trim())) {
+            throw new CockpitInvalidRequestException("event source must be ALARM or WARNING");
+        }
+        validateCode(eventCode, "event code is required");
+        CockpitWorkOrderReadSnapshot snapshot = adapter.loadByEvent(
+                eventSource.trim(), eventCode.trim());
+        if (snapshot == null) {
+            throw new CockpitDeviceNotFoundException("event not found");
+        }
+        return response(requestId, snapshot);
+    }
+
+    public CockpitWorkOrderTimelineResponse loadTimeline(String requestId, String workOrderCode) {
+        validateCode(workOrderCode, "work order code is required");
+        CockpitWorkOrderTimelineSnapshot snapshot = adapter.loadTimeline(workOrderCode.trim());
+        if (snapshot == null) {
+            throw new CockpitDeviceNotFoundException("work order not found");
+        }
+        return timelineResponse(requestId, snapshot);
+    }
+
+    public CockpitErrorPayload invalidRequest(String requestId) {
+        return error(requestId, CockpitErrorCategory.INVALID_REQUEST, "工单查询参数不合法");
+    }
+
+    public CockpitErrorPayload notFound(String requestId) {
+        return error(requestId, CockpitErrorCategory.NOT_FOUND, "工单或关联对象不存在");
+    }
+
+    public CockpitErrorPayload serverError(String requestId) {
+        return error(requestId, CockpitErrorCategory.SERVER_ERROR, "驾驶舱工单数据暂不可用");
+    }
+
+    private CockpitWorkOrderReadResponse response(String requestId, CockpitWorkOrderReadSnapshot snapshot) {
+        List<CockpitWorkOrderSummary> workOrders = snapshot.getWorkOrders() == null
+                ? new ArrayList<>() : snapshot.getWorkOrders();
+        String state = workOrders.isEmpty() ? "EMPTY" : "READY";
+        return new CockpitWorkOrderReadResponse(
+                requestId,
+                "v1",
+                true,
+                snapshot.getDeviceCode(),
+                snapshot.getEventSource(),
+                snapshot.getEventCode(),
+                state,
+                workOrders.isEmpty() ? "NO_WORK_ORDERS" : "READY",
+                CockpitWorkOrderStateDictionary.states(),
+                workOrders);
+    }
+
+    private CockpitWorkOrderTimelineResponse timelineResponse(
+            String requestId,
+            CockpitWorkOrderTimelineSnapshot snapshot) {
+        return new CockpitWorkOrderTimelineResponse(
+                requestId,
+                "v1",
+                true,
+                snapshot.getWorkOrder().getWorkOrderCode(),
+                CockpitWorkOrderStateDictionary.states(),
+                snapshot.getWorkOrder(),
+                snapshot.getTimelineEntries() == null ? new ArrayList<>() : snapshot.getTimelineEntries());
+    }
+
+    private void validateCode(String value, String message) {
+        if (value == null || value.trim().isEmpty()) {
+            throw new CockpitInvalidRequestException(message);
+        }
+    }
+
+    private CockpitErrorPayload error(String requestId, CockpitErrorCategory category, String message) {
+        return new CockpitErrorPayload(
+                category,
+                message,
+                requestId,
+                DateTimeFormatter.ISO_OFFSET_DATE_TIME
+                        .format(clock.instant().atZone(ZoneId.of("Asia/Shanghai"))));
+    }
+}

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

@@ -0,0 +1,26 @@
+package com.zksy.cockpit.workorder;
+
+import java.util.List;
+
+public class CockpitWorkOrderReadSnapshot {
+    private final String deviceCode;
+    private final String eventSource;
+    private final String eventCode;
+    private final List<CockpitWorkOrderSummary> workOrders;
+
+    public CockpitWorkOrderReadSnapshot(
+            String deviceCode,
+            String eventSource,
+            String eventCode,
+            List<CockpitWorkOrderSummary> workOrders) {
+        this.deviceCode = deviceCode;
+        this.eventSource = eventSource;
+        this.eventCode = eventCode;
+        this.workOrders = workOrders;
+    }
+
+    public String getDeviceCode() { return deviceCode; }
+    public String getEventSource() { return eventSource; }
+    public String getEventCode() { return eventCode; }
+    public List<CockpitWorkOrderSummary> getWorkOrders() { return workOrders; }
+}

+ 28 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/CockpitWorkOrderState.java

@@ -0,0 +1,28 @@
+package com.zksy.cockpit.workorder;
+
+public class CockpitWorkOrderState {
+    private final int legacyCode;
+    private final String legacyName;
+    private final Integer authoritativeCode;
+    private final String authoritativeName;
+    private final boolean deprecated;
+
+    public CockpitWorkOrderState(
+            int legacyCode,
+            String legacyName,
+            Integer authoritativeCode,
+            String authoritativeName,
+            boolean deprecated) {
+        this.legacyCode = legacyCode;
+        this.legacyName = legacyName;
+        this.authoritativeCode = authoritativeCode;
+        this.authoritativeName = authoritativeName;
+        this.deprecated = deprecated;
+    }
+
+    public int getLegacyCode() { return legacyCode; }
+    public String getLegacyName() { return legacyName; }
+    public Integer getAuthoritativeCode() { return authoritativeCode; }
+    public String getAuthoritativeName() { return authoritativeName; }
+    public boolean isDeprecated() { return deprecated; }
+}

+ 39 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/CockpitWorkOrderStateDictionary.java

@@ -0,0 +1,39 @@
+package com.zksy.cockpit.workorder;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+public final class CockpitWorkOrderStateDictionary {
+    private static final List<CockpitWorkOrderState> STATES = Collections.unmodifiableList(Arrays.asList(
+            new CockpitWorkOrderState(1, "待派单", 1, "待派单", false),
+            new CockpitWorkOrderState(2, "待接单", 2, "待接单", false),
+            new CockpitWorkOrderState(3, "已接单", 3, "已接单", false),
+            new CockpitWorkOrderState(4, "处理中", 4, "处理中", false),
+            new CockpitWorkOrderState(5, "待验收", 5, "待验收", false),
+            new CockpitWorkOrderState(6, "已结案", 6, "已结案", false),
+            new CockpitWorkOrderState(7, "已驳回", 7, "已取消", true),
+            new CockpitWorkOrderState(8, "延期审核", null, null, true),
+            new CockpitWorkOrderState(9, "已延期", null, null, true)
+    ));
+
+    private static final Map<Integer, CockpitWorkOrderState> BY_LEGACY_CODE = STATES.stream()
+            .collect(Collectors.toMap(CockpitWorkOrderState::getLegacyCode, Function.identity()));
+
+    private CockpitWorkOrderStateDictionary() {
+    }
+
+    public static List<CockpitWorkOrderState> states() {
+        return STATES;
+    }
+
+    public static CockpitWorkOrderState byLegacyCode(Integer code) {
+        if (code == null) {
+            return BY_LEGACY_CODE.get(7);
+        }
+        return BY_LEGACY_CODE.get(code);
+    }
+}

+ 72 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/CockpitWorkOrderSummary.java

@@ -0,0 +1,72 @@
+package com.zksy.cockpit.workorder;
+
+public class CockpitWorkOrderSummary {
+    private final String workOrderCode;
+    private final String deviceCode;
+    private final Integer orderType;
+    private final String orderTypeName;
+    private final Integer orderLevel;
+    private final String orderDesc;
+    private final Integer legacyState;
+    private final String legacyStateName;
+    private final Integer authoritativeState;
+    private final String authoritativeStateName;
+    private final boolean deprecatedState;
+    private final String createTime;
+    private final String updateTime;
+    private final String planFinishTime;
+    private final String realFinishTime;
+    private final CockpitWorkOrderEventAssociation eventAssociation;
+
+    public CockpitWorkOrderSummary(
+            String workOrderCode,
+            String deviceCode,
+            Integer orderType,
+            String orderTypeName,
+            Integer orderLevel,
+            String orderDesc,
+            Integer legacyState,
+            String legacyStateName,
+            Integer authoritativeState,
+            String authoritativeStateName,
+            boolean deprecatedState,
+            String createTime,
+            String updateTime,
+            String planFinishTime,
+            String realFinishTime,
+            CockpitWorkOrderEventAssociation eventAssociation) {
+        this.workOrderCode = workOrderCode;
+        this.deviceCode = deviceCode;
+        this.orderType = orderType;
+        this.orderTypeName = orderTypeName;
+        this.orderLevel = orderLevel;
+        this.orderDesc = orderDesc;
+        this.legacyState = legacyState;
+        this.legacyStateName = legacyStateName;
+        this.authoritativeState = authoritativeState;
+        this.authoritativeStateName = authoritativeStateName;
+        this.deprecatedState = deprecatedState;
+        this.createTime = createTime;
+        this.updateTime = updateTime;
+        this.planFinishTime = planFinishTime;
+        this.realFinishTime = realFinishTime;
+        this.eventAssociation = eventAssociation;
+    }
+
+    public String getWorkOrderCode() { return workOrderCode; }
+    public String getDeviceCode() { return deviceCode; }
+    public Integer getOrderType() { return orderType; }
+    public String getOrderTypeName() { return orderTypeName; }
+    public Integer getOrderLevel() { return orderLevel; }
+    public String getOrderDesc() { return orderDesc; }
+    public Integer getLegacyState() { return legacyState; }
+    public String getLegacyStateName() { return legacyStateName; }
+    public Integer getAuthoritativeState() { return authoritativeState; }
+    public String getAuthoritativeStateName() { return authoritativeStateName; }
+    public boolean isDeprecatedState() { return deprecatedState; }
+    public String getCreateTime() { return createTime; }
+    public String getUpdateTime() { return updateTime; }
+    public String getPlanFinishTime() { return planFinishTime; }
+    public String getRealFinishTime() { return realFinishTime; }
+    public CockpitWorkOrderEventAssociation getEventAssociation() { return eventAssociation; }
+}

+ 52 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/CockpitWorkOrderTimelineEntry.java

@@ -0,0 +1,52 @@
+package com.zksy.cockpit.workorder;
+
+public class CockpitWorkOrderTimelineEntry {
+    private final Long logId;
+    private final String workOrderCode;
+    private final Integer legacyState;
+    private final String legacyStateName;
+    private final Integer authoritativeState;
+    private final String authoritativeStateName;
+    private final boolean deprecatedState;
+    private final String operationName;
+    private final String operationDesc;
+    private final String operatorName;
+    private final String operatedAt;
+
+    public CockpitWorkOrderTimelineEntry(
+            Long logId,
+            String workOrderCode,
+            Integer legacyState,
+            String legacyStateName,
+            Integer authoritativeState,
+            String authoritativeStateName,
+            boolean deprecatedState,
+            String operationName,
+            String operationDesc,
+            String operatorName,
+            String operatedAt) {
+        this.logId = logId;
+        this.workOrderCode = workOrderCode;
+        this.legacyState = legacyState;
+        this.legacyStateName = legacyStateName;
+        this.authoritativeState = authoritativeState;
+        this.authoritativeStateName = authoritativeStateName;
+        this.deprecatedState = deprecatedState;
+        this.operationName = operationName;
+        this.operationDesc = operationDesc;
+        this.operatorName = operatorName;
+        this.operatedAt = operatedAt;
+    }
+
+    public Long getLogId() { return logId; }
+    public String getWorkOrderCode() { return workOrderCode; }
+    public Integer getLegacyState() { return legacyState; }
+    public String getLegacyStateName() { return legacyStateName; }
+    public Integer getAuthoritativeState() { return authoritativeState; }
+    public String getAuthoritativeStateName() { return authoritativeStateName; }
+    public boolean isDeprecatedState() { return deprecatedState; }
+    public String getOperationName() { return operationName; }
+    public String getOperationDesc() { return operationDesc; }
+    public String getOperatorName() { return operatorName; }
+    public String getOperatedAt() { return operatedAt; }
+}

+ 38 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/CockpitWorkOrderTimelineResponse.java

@@ -0,0 +1,38 @@
+package com.zksy.cockpit.workorder;
+
+import java.util.List;
+
+public class CockpitWorkOrderTimelineResponse {
+    private final String requestId;
+    private final String apiVersion;
+    private final boolean readOnly;
+    private final String workOrderCode;
+    private final List<CockpitWorkOrderState> stateDictionary;
+    private final CockpitWorkOrderSummary workOrder;
+    private final List<CockpitWorkOrderTimelineEntry> timelineEntries;
+
+    public CockpitWorkOrderTimelineResponse(
+            String requestId,
+            String apiVersion,
+            boolean readOnly,
+            String workOrderCode,
+            List<CockpitWorkOrderState> stateDictionary,
+            CockpitWorkOrderSummary workOrder,
+            List<CockpitWorkOrderTimelineEntry> timelineEntries) {
+        this.requestId = requestId;
+        this.apiVersion = apiVersion;
+        this.readOnly = readOnly;
+        this.workOrderCode = workOrderCode;
+        this.stateDictionary = stateDictionary;
+        this.workOrder = workOrder;
+        this.timelineEntries = timelineEntries;
+    }
+
+    public String getRequestId() { return requestId; }
+    public String getApiVersion() { return apiVersion; }
+    public boolean isReadOnly() { return readOnly; }
+    public String getWorkOrderCode() { return workOrderCode; }
+    public List<CockpitWorkOrderState> getStateDictionary() { return stateDictionary; }
+    public CockpitWorkOrderSummary getWorkOrder() { return workOrder; }
+    public List<CockpitWorkOrderTimelineEntry> getTimelineEntries() { return timelineEntries; }
+}

+ 18 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/CockpitWorkOrderTimelineSnapshot.java

@@ -0,0 +1,18 @@
+package com.zksy.cockpit.workorder;
+
+import java.util.List;
+
+public class CockpitWorkOrderTimelineSnapshot {
+    private final CockpitWorkOrderSummary workOrder;
+    private final List<CockpitWorkOrderTimelineEntry> timelineEntries;
+
+    public CockpitWorkOrderTimelineSnapshot(
+            CockpitWorkOrderSummary workOrder,
+            List<CockpitWorkOrderTimelineEntry> timelineEntries) {
+        this.workOrder = workOrder;
+        this.timelineEntries = timelineEntries;
+    }
+
+    public CockpitWorkOrderSummary getWorkOrder() { return workOrder; }
+    public List<CockpitWorkOrderTimelineEntry> getTimelineEntries() { return timelineEntries; }
+}

+ 21 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/EmptyCockpitWorkOrderReadAdapter.java

@@ -0,0 +1,21 @@
+package com.zksy.cockpit.workorder;
+
+import org.springframework.stereotype.Component;
+
+@Component
+public class EmptyCockpitWorkOrderReadAdapter implements CockpitWorkOrderReadAdapter {
+    @Override
+    public CockpitWorkOrderReadSnapshot loadByDevice(String deviceCode) {
+        return null;
+    }
+
+    @Override
+    public CockpitWorkOrderReadSnapshot loadByEvent(String eventSource, String eventCode) {
+        return null;
+    }
+
+    @Override
+    public CockpitWorkOrderTimelineSnapshot loadTimeline(String workOrderCode) {
+        return null;
+    }
+}