From 3adcbff31fdece77269744c8741f237e7a57348e Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期日, 22 三月 2026 17:17:58 +0800
Subject: [PATCH] #

---
 src/main/webapp/static/js/wrkAnalysis/wrkAnalysis.js                                   |  477 ++++++++++
 src/main/java/com/zy/core/trace/StationTaskTraceRegistry.java                          |    2 
 src/main/java/com/zy/core/utils/StationOperateProcessUtils.java                        |   23 
 src/main/java/com/zy/asrs/service/impl/WrkAnalysisServiceImpl.java                     |  909 +++++++++++++++++++
 src/main/java/com/zy/asrs/entity/WrkAnalysis.java                                      |  155 +++
 src/main/java/com/zy/core/utils/DualCrnOperateProcessUtils.java                        |   27 
 src/main/java/com/zy/core/utils/CrnOperateProcessUtils.java                            |   46 
 src/main/java/com/zy/asrs/task/WrkMastScheduler.java                                   |   48 
 src/main/java/com/zy/core/service/WrkCommandRollbackService.java                       |    4 
 src/main/java/com/zy/core/plugin/NormalProcess.java                                    |    2 
 src/main/java/com/zy/core/enums/WrkStsType.java                                        |    7 
 src/main/java/com/zy/core/plugin/XiaosongProcess.java                                  |    4 
 src/main/webapp/views/wrkAnalysis/wrkAnalysis.html                                     |  443 +++++++++
 src/main/java/com/zy/asrs/controller/WrkAnalysisController.java                        |   41 
 src/main/java/com/zy/asrs/mapper/WrkAnalysisMapper.java                                |   11 
 src/main/java/com/zy/asrs/service/WrkAnalysisService.java                              |   40 
 src/main/java/com/zy/asrs/service/impl/PlannerServiceImpl.java                         |    2 
 src/main/java/com/zy/asrs/service/impl/WrkMastLogServiceImpl.java                      |    8 
 src/main/resources/mapper/WrkAnalysisMapper.xml                                        |   40 
 src/main/java/com/zy/common/service/CommonService.java                                 |    5 
 src/main/resources/sql/20260322_add_wrk_analysis_page_and_inbound_status_migration.sql |  332 +++++++
 src/main/java/com/zy/asrs/task/WrkAnalysisStationArrivalScanner.java                   |   81 +
 src/main/java/com/zy/core/plugin/FakeProcess.java                                      |   13 
 src/main/java/com/zy/core/plugin/GslProcess.java                                       |    4 
 24 files changed, 2,666 insertions(+), 58 deletions(-)

diff --git a/src/main/java/com/zy/asrs/controller/WrkAnalysisController.java b/src/main/java/com/zy/asrs/controller/WrkAnalysisController.java
new file mode 100644
index 0000000..22ff5d3
--- /dev/null
+++ b/src/main/java/com/zy/asrs/controller/WrkAnalysisController.java
@@ -0,0 +1,41 @@
+package com.zy.asrs.controller;
+
+import com.alibaba.fastjson.JSONObject;
+import com.core.annotations.ManagerAuth;
+import com.core.common.R;
+import com.zy.asrs.service.WrkAnalysisService;
+import com.zy.common.web.BaseController;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+@RestController
+public class WrkAnalysisController extends BaseController {
+
+    private final WrkAnalysisService wrkAnalysisService;
+
+    public WrkAnalysisController(WrkAnalysisService wrkAnalysisService) {
+        this.wrkAnalysisService = wrkAnalysisService;
+    }
+
+    @RequestMapping("/wrkAnalysis/options/auth")
+    @ManagerAuth
+    public R options() {
+        return R.ok(wrkAnalysisService.queryOptions());
+    }
+
+    @RequestMapping("/wrkAnalysis/list/auth")
+    @ManagerAuth
+    public R list(@RequestParam(defaultValue = "1") Integer curr,
+                  @RequestParam(defaultValue = "20") Integer limit,
+                  @RequestParam Map<String, Object> param) {
+        excludeTrash(param);
+        return R.ok(wrkAnalysisService.queryList(curr, limit, param));
+    }
+
+    @PostMapping("/wrkAnalysis/analyze/auth")
+    @ManagerAuth
+    public R analyze(@RequestBody(required = false) JSONObject param) {
+        return R.ok(wrkAnalysisService.analyze(param));
+    }
+}
diff --git a/src/main/java/com/zy/asrs/entity/WrkAnalysis.java b/src/main/java/com/zy/asrs/entity/WrkAnalysis.java
new file mode 100644
index 0000000..e17b96b
--- /dev/null
+++ b/src/main/java/com/zy/asrs/entity/WrkAnalysis.java
@@ -0,0 +1,155 @@
+package com.zy.asrs.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("asr_wrk_analysis")
+public class WrkAnalysis implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "宸ヤ綔鍙�")
+    @TableId(value = "wrk_no", type = IdType.INPUT)
+    private Integer wrkNo;
+
+    @ApiModelProperty(value = "WMS浠诲姟鍙�")
+    @TableField("wms_wrk_no")
+    private String wmsWrkNo;
+
+    @ApiModelProperty(value = "鍏ュ嚭搴撶被鍨�")
+    @TableField("io_type")
+    private Integer ioType;
+
+    @ApiModelProperty(value = "鏈�缁堝伐浣滅姸鎬�")
+    @TableField("final_wrk_sts")
+    private Long finalWrkSts;
+
+    @ApiModelProperty(value = "婧愮珯")
+    @TableField("source_sta_no")
+    private Integer sourceStaNo;
+
+    @ApiModelProperty(value = "鐩爣绔�")
+    @TableField("sta_no")
+    private Integer staNo;
+
+    @ApiModelProperty(value = "婧愬簱浣�")
+    @TableField("source_loc_no")
+    private String sourceLocNo;
+
+    @ApiModelProperty(value = "鐩爣搴撲綅")
+    @TableField("loc_no")
+    private String locNo;
+
+    @ApiModelProperty(value = "鍫嗗灈鏈哄彿")
+    @TableField("crn_no")
+    private Integer crnNo;
+
+    @ApiModelProperty(value = "鍙屽伐浣嶅爢鍨涙満鍙�")
+    @TableField("dual_crn_no")
+    private Integer dualCrnNo;
+
+    @ApiModelProperty(value = "RGV鍙�")
+    @TableField("rgv_no")
+    private Integer rgvNo;
+
+    @ApiModelProperty(value = "鍒涘缓鏃堕棿")
+    @TableField("appe_time")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date appeTime;
+
+    @ApiModelProperty(value = "瀹屾垚鏃堕棿")
+    @TableField("finish_time")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date finishTime;
+
+    @ApiModelProperty(value = "鎬昏�楁椂姣")
+    @TableField("total_duration_ms")
+    private Long totalDurationMs;
+
+    @ApiModelProperty(value = "绔欑偣寮�濮嬫椂闂�")
+    @TableField("station_start_time")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date stationStartTime;
+
+    @ApiModelProperty(value = "绔欑偣缁撴潫鏃堕棿")
+    @TableField("station_end_time")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date stationEndTime;
+
+    @ApiModelProperty(value = "绔欑偣鑰楁椂姣")
+    @TableField("station_duration_ms")
+    private Long stationDurationMs;
+
+    @ApiModelProperty(value = "鍫嗗灈鏈哄紑濮嬫椂闂�")
+    @TableField("crane_start_time")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date craneStartTime;
+
+    @ApiModelProperty(value = "鍫嗗灈鏈虹粨鏉熸椂闂�")
+    @TableField("crane_end_time")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date craneEndTime;
+
+    @ApiModelProperty(value = "鍫嗗灈鏈鸿�楁椂姣")
+    @TableField("crane_duration_ms")
+    private Long craneDurationMs;
+
+    @ApiModelProperty(value = "鏄惁鏁呴殰")
+    @TableField("has_fault")
+    private Integer hasFault;
+
+    @ApiModelProperty(value = "鏁呴殰娆℃暟")
+    @TableField("fault_count")
+    private Integer faultCount;
+
+    @ApiModelProperty(value = "鏁呴殰鑰楁椂姣")
+    @TableField("fault_duration_ms")
+    private Long faultDurationMs;
+
+    @ApiModelProperty(value = "鍗曞爢鍨涙満鏁呴殰娆℃暟")
+    @TableField("crn_fault_count")
+    private Integer crnFaultCount;
+
+    @ApiModelProperty(value = "鍗曞爢鍨涙満鏁呴殰鑰楁椂姣")
+    @TableField("crn_fault_duration_ms")
+    private Long crnFaultDurationMs;
+
+    @ApiModelProperty(value = "鍙屽伐浣嶅爢鍨涙満鏁呴殰娆℃暟")
+    @TableField("dual_crn_fault_count")
+    private Integer dualCrnFaultCount;
+
+    @ApiModelProperty(value = "鍙屽伐浣嶅爢鍨涙満鏁呴殰鑰楁椂姣")
+    @TableField("dual_crn_fault_duration_ms")
+    private Long dualCrnFaultDurationMs;
+
+    @ApiModelProperty(value = "RGV鏁呴殰娆℃暟")
+    @TableField("rgv_fault_count")
+    private Integer rgvFaultCount;
+
+    @ApiModelProperty(value = "RGV鏁呴殰鑰楁椂姣")
+    @TableField("rgv_fault_duration_ms")
+    private Long rgvFaultDurationMs;
+
+    @ApiModelProperty(value = "鏁版嵁瀹屾暣鎬�")
+    @TableField("metric_completeness")
+    private String metricCompleteness;
+
+    @ApiModelProperty(value = "鍒涘缓鏃堕棿")
+    @TableField("create_time")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    @ApiModelProperty(value = "鏇存柊鏃堕棿")
+    @TableField("update_time")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+}
diff --git a/src/main/java/com/zy/asrs/mapper/WrkAnalysisMapper.java b/src/main/java/com/zy/asrs/mapper/WrkAnalysisMapper.java
new file mode 100644
index 0000000..8a06a89
--- /dev/null
+++ b/src/main/java/com/zy/asrs/mapper/WrkAnalysisMapper.java
@@ -0,0 +1,11 @@
+package com.zy.asrs.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zy.asrs.entity.WrkAnalysis;
+import org.apache.ibatis.annotations.Mapper;
+import org.springframework.stereotype.Repository;
+
+@Mapper
+@Repository
+public interface WrkAnalysisMapper extends BaseMapper<WrkAnalysis> {
+}
diff --git a/src/main/java/com/zy/asrs/service/WrkAnalysisService.java b/src/main/java/com/zy/asrs/service/WrkAnalysisService.java
new file mode 100644
index 0000000..219035e
--- /dev/null
+++ b/src/main/java/com/zy/asrs/service/WrkAnalysisService.java
@@ -0,0 +1,40 @@
+package com.zy.asrs.service;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zy.asrs.entity.WrkAnalysis;
+import com.zy.asrs.entity.WrkMast;
+
+import java.util.Collection;
+import java.util.Date;
+import java.util.Map;
+
+public interface WrkAnalysisService extends IService<WrkAnalysis> {
+
+    String METRIC_COMPLETE = "COMPLETE";
+    String METRIC_PARTIAL = "PARTIAL";
+
+    void initForTask(WrkMast wrkMast);
+
+    void markInboundStationStart(WrkMast wrkMast, Date operateTime);
+
+    void markOutboundStationStart(WrkMast wrkMast, Date operateTime);
+
+    boolean completeInboundStationRun(WrkMast wrkMast, Date operateTime);
+
+    void markOutboundStationComplete(WrkMast wrkMast, Date operateTime);
+
+    void markCraneStart(WrkMast wrkMast, Date operateTime);
+
+    void markCraneComplete(WrkMast wrkMast, Date operateTime, Long finalWrkSts);
+
+    void finishTask(WrkMast wrkMast, Date finishTime);
+
+    Map<Integer, WrkAnalysis> mapByWrkNos(Collection<Integer> wrkNos);
+
+    Map<String, Object> queryOptions();
+
+    Map<String, Object> queryList(Integer curr, Integer limit, Map<String, Object> param);
+
+    Map<String, Object> analyze(JSONObject param);
+}
diff --git a/src/main/java/com/zy/asrs/service/impl/PlannerServiceImpl.java b/src/main/java/com/zy/asrs/service/impl/PlannerServiceImpl.java
index a73721a..29cbe1f 100644
--- a/src/main/java/com/zy/asrs/service/impl/PlannerServiceImpl.java
+++ b/src/main/java/com/zy/asrs/service/impl/PlannerServiceImpl.java
@@ -352,7 +352,7 @@
             taskDataList.add(t);
         }
 
-        List<WrkMast> inTasks = wrkMastService.list(new QueryWrapper<WrkMast>().eq("wrk_sts", WrkStsType.INBOUND_DEVICE_RUN.sts));
+        List<WrkMast> inTasks = wrkMastService.list(new QueryWrapper<WrkMast>().eq("wrk_sts", WrkStsType.INBOUND_STATION_RUN_COMPLETE.sts));
         for (WrkMast wrkMast : inTasks) {
             HashMap<String, Object> t = new HashMap<>();
             t.put("taskId", wrkMast.getWrkNo());
diff --git a/src/main/java/com/zy/asrs/service/impl/WrkAnalysisServiceImpl.java b/src/main/java/com/zy/asrs/service/impl/WrkAnalysisServiceImpl.java
new file mode 100644
index 0000000..7b3e214
--- /dev/null
+++ b/src/main/java/com/zy/asrs/service/impl/WrkAnalysisServiceImpl.java
@@ -0,0 +1,909 @@
+package com.zy.asrs.service.impl;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.core.common.Cools;
+import com.core.common.DateUtils;
+import com.zy.asrs.entity.*;
+import com.zy.asrs.mapper.WrkAnalysisMapper;
+import com.zy.asrs.service.*;
+import com.zy.core.enums.WrkIoType;
+import com.zy.core.enums.WrkStsType;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service("wrkAnalysisService")
+public class WrkAnalysisServiceImpl extends ServiceImpl<WrkAnalysisMapper, WrkAnalysis> implements WrkAnalysisService {
+
+    private static final List<Long> DEFAULT_FINAL_STATUSES = Arrays.asList(
+            WrkStsType.COMPLETE_INBOUND.sts,
+            WrkStsType.SETTLE_INBOUND.sts,
+            WrkStsType.COMPLETE_OUTBOUND.sts,
+            WrkStsType.SETTLE_OUTBOUND.sts,
+            WrkStsType.COMPLETE_LOC_MOVE.sts
+    );
+    private static final String MODE_TASK = "TASK";
+    private static final String MODE_TIME = "TIME";
+    private static final String TIME_FIELD_FINISH = "finish_time";
+    private static final String TIME_FIELD_APPE = "appe_time";
+    private static final String DEVICE_TYPE_CRN = "CRN";
+    private static final String DEVICE_TYPE_DUAL_CRN = "DUAL_CRN";
+    private static final String DEVICE_TYPE_RGV = "RGV";
+    private static final int DURATION_COMPARE_LIMIT = 20;
+
+    private final WrkMastLogService wrkMastLogService;
+    private final BasCrnpErrLogService basCrnpErrLogService;
+    private final BasDualCrnpErrLogService basDualCrnpErrLogService;
+    private final BasRgvErrLogService basRgvErrLogService;
+    private final BasStationService basStationService;
+    private final BasWrkStatusService basWrkStatusService;
+    private final WrkMastService wrkMastService;
+
+    public WrkAnalysisServiceImpl(WrkMastLogService wrkMastLogService,
+                                  BasCrnpErrLogService basCrnpErrLogService,
+                                  BasDualCrnpErrLogService basDualCrnpErrLogService,
+                                  BasRgvErrLogService basRgvErrLogService,
+                                  BasStationService basStationService,
+                                  BasWrkStatusService basWrkStatusService,
+                                  WrkMastService wrkMastService) {
+        this.wrkMastLogService = wrkMastLogService;
+        this.basCrnpErrLogService = basCrnpErrLogService;
+        this.basDualCrnpErrLogService = basDualCrnpErrLogService;
+        this.basRgvErrLogService = basRgvErrLogService;
+        this.basStationService = basStationService;
+        this.basWrkStatusService = basWrkStatusService;
+        this.wrkMastService = wrkMastService;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void initForTask(WrkMast wrkMast) {
+        if (wrkMast == null || wrkMast.getWrkNo() == null) {
+            return;
+        }
+        WrkAnalysis entity = this.getById(wrkMast.getWrkNo());
+        Date now = new Date();
+        if (entity == null) {
+            entity = new WrkAnalysis();
+            entity.setWrkNo(wrkMast.getWrkNo());
+            entity.setCreateTime(now);
+        }
+        syncBaseFields(entity, wrkMast);
+        entity.setHasFault(defaultInt(entity.getHasFault()));
+        entity.setFaultCount(defaultInt(entity.getFaultCount()));
+        entity.setFaultDurationMs(defaultLong(entity.getFaultDurationMs()));
+        entity.setCrnFaultCount(defaultInt(entity.getCrnFaultCount()));
+        entity.setCrnFaultDurationMs(defaultLong(entity.getCrnFaultDurationMs()));
+        entity.setDualCrnFaultCount(defaultInt(entity.getDualCrnFaultCount()));
+        entity.setDualCrnFaultDurationMs(defaultLong(entity.getDualCrnFaultDurationMs()));
+        entity.setRgvFaultCount(defaultInt(entity.getRgvFaultCount()));
+        entity.setRgvFaultDurationMs(defaultLong(entity.getRgvFaultDurationMs()));
+        entity.setMetricCompleteness(METRIC_PARTIAL);
+        entity.setUpdateTime(now);
+        this.saveOrUpdate(entity);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void markInboundStationStart(WrkMast wrkMast, Date operateTime) {
+        WrkAnalysis entity = ensureRecord(wrkMast);
+        if (entity == null) {
+            return;
+        }
+        Date time = safeDate(operateTime);
+        entity.setStationStartTime(time);
+        entity.setSourceStaNo(wrkMast.getSourceStaNo());
+        entity.setStaNo(wrkMast.getStaNo());
+        entity.setFinalWrkSts(WrkStsType.INBOUND_STATION_RUN.sts);
+        entity.setUpdateTime(time);
+        this.updateById(entity);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void markOutboundStationStart(WrkMast wrkMast, Date operateTime) {
+        WrkAnalysis entity = ensureRecord(wrkMast);
+        if (entity == null) {
+            return;
+        }
+        Date time = safeDate(operateTime);
+        entity.setStationStartTime(time);
+        entity.setFinalWrkSts(WrkStsType.STATION_RUN.sts);
+        entity.setUpdateTime(time);
+        this.updateById(entity);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean completeInboundStationRun(WrkMast wrkMast, Date operateTime) {
+        if (wrkMast == null || wrkMast.getWrkNo() == null || !Objects.equals(wrkMast.getWrkSts(), WrkStsType.INBOUND_STATION_RUN.sts)) {
+            return false;
+        }
+        WrkMast updateEntity = new WrkMast();
+        updateEntity.setWrkNo(wrkMast.getWrkNo());
+        updateEntity.setWrkSts(WrkStsType.INBOUND_STATION_RUN_COMPLETE.sts);
+        Date now = safeDate(operateTime);
+        updateEntity.setIoTime(now);
+        updateEntity.setModiTime(now);
+        boolean updated = wrkMast.getWrkNo() != null && wrkMastService.update(updateEntity, new QueryWrapper<WrkMast>()
+                .eq("wrk_no", wrkMast.getWrkNo())
+                .eq("wrk_sts", WrkStsType.INBOUND_STATION_RUN.sts));
+        if (!updated) {
+            return false;
+        }
+        WrkAnalysis entity = ensureRecord(wrkMast);
+        if (entity != null) {
+            entity.setFinalWrkSts(WrkStsType.INBOUND_STATION_RUN_COMPLETE.sts);
+            entity.setStationEndTime(now);
+            if (entity.getStationStartTime() != null) {
+                entity.setStationDurationMs(durationMs(entity.getStationStartTime(), now));
+            }
+            entity.setUpdateTime(now);
+            this.updateById(entity);
+        }
+        wrkMast.setWrkSts(WrkStsType.INBOUND_STATION_RUN_COMPLETE.sts);
+        wrkMast.setIoTime(now);
+        wrkMast.setModiTime(now);
+        return true;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void markOutboundStationComplete(WrkMast wrkMast, Date operateTime) {
+        WrkAnalysis entity = ensureRecord(wrkMast);
+        if (entity == null) {
+            return;
+        }
+        Date time = safeDate(operateTime);
+        entity.setFinalWrkSts(WrkStsType.STATION_RUN_COMPLETE.sts);
+        entity.setStationEndTime(time);
+        if (entity.getStationStartTime() != null) {
+            entity.setStationDurationMs(durationMs(entity.getStationStartTime(), time));
+        }
+        entity.setUpdateTime(time);
+        this.updateById(entity);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void markCraneStart(WrkMast wrkMast, Date operateTime) {
+        WrkAnalysis entity = ensureRecord(wrkMast);
+        if (entity == null) {
+            return;
+        }
+        Date time = safeDate(operateTime);
+        entity.setCraneStartTime(time);
+        entity.setCrnNo(wrkMast.getCrnNo());
+        entity.setDualCrnNo(wrkMast.getDualCrnNo());
+        entity.setRgvNo(wrkMast.getRgvNo());
+        entity.setFinalWrkSts(wrkMast.getWrkSts());
+        entity.setUpdateTime(time);
+        if (Objects.equals(wrkMast.getIoType(), WrkIoType.LOC_MOVE.id) && entity.getStationDurationMs() == null) {
+            entity.setStationDurationMs(0L);
+        }
+        this.updateById(entity);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void markCraneComplete(WrkMast wrkMast, Date operateTime, Long finalWrkSts) {
+        WrkAnalysis entity = ensureRecord(wrkMast);
+        if (entity == null) {
+            return;
+        }
+        Date time = safeDate(operateTime);
+        entity.setCraneEndTime(time);
+        if (entity.getCraneStartTime() != null) {
+            entity.setCraneDurationMs(durationMs(entity.getCraneStartTime(), time));
+        }
+        entity.setFinalWrkSts(finalWrkSts);
+        entity.setUpdateTime(time);
+        this.updateById(entity);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void finishTask(WrkMast wrkMast, Date finishTime) {
+        if (wrkMast == null || wrkMast.getWrkNo() == null) {
+            return;
+        }
+        WrkAnalysis entity = ensureRecord(wrkMast);
+        if (entity == null) {
+            return;
+        }
+        Date time = safeDate(finishTime);
+        syncBaseFields(entity, wrkMast);
+        entity.setFinishTime(time);
+        if (entity.getAppeTime() != null) {
+            entity.setTotalDurationMs(durationMs(entity.getAppeTime(), time));
+        }
+        if (Objects.equals(wrkMast.getIoType(), WrkIoType.LOC_MOVE.id) && entity.getStationDurationMs() == null) {
+            entity.setStationDurationMs(0L);
+        }
+        FaultSummary faultSummary = buildFaultSummary(wrkMast.getWrkNo(), time);
+        entity.setHasFault(faultSummary.hasFault);
+        entity.setFaultCount(faultSummary.totalCount);
+        entity.setFaultDurationMs(faultSummary.totalDurationMs);
+        entity.setCrnFaultCount(faultSummary.crnCount);
+        entity.setCrnFaultDurationMs(faultSummary.crnDurationMs);
+        entity.setDualCrnFaultCount(faultSummary.dualCount);
+        entity.setDualCrnFaultDurationMs(faultSummary.dualDurationMs);
+        entity.setRgvFaultCount(faultSummary.rgvCount);
+        entity.setRgvFaultDurationMs(faultSummary.rgvDurationMs);
+        entity.setMetricCompleteness(resolveMetricCompleteness(wrkMast, entity));
+        entity.setUpdateTime(time);
+        this.saveOrUpdate(entity);
+    }
+
+    @Override
+    public Map<Integer, WrkAnalysis> mapByWrkNos(Collection<Integer> wrkNos) {
+        Map<Integer, WrkAnalysis> result = new LinkedHashMap<>();
+        if (wrkNos == null || wrkNos.isEmpty()) {
+            return result;
+        }
+        List<WrkAnalysis> list = this.listByIds(new LinkedHashSet<>(wrkNos));
+        for (WrkAnalysis item : list) {
+            if (item != null && item.getWrkNo() != null) {
+                result.put(item.getWrkNo(), item);
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public Map<String, Object> queryOptions() {
+        Map<String, Object> result = new LinkedHashMap<>();
+        List<Map<String, Object>> ioTypes = new ArrayList<>();
+        ioTypes.add(option("1", "IN", "鍏ュ簱", WrkIoType.IN.id));
+        ioTypes.add(option("2", "OUT", "鍑哄簱", WrkIoType.OUT.id));
+        ioTypes.add(option("3", "LOC_MOVE", "绉诲簱", WrkIoType.LOC_MOVE.id));
+        List<Map<String, Object>> timeFields = new ArrayList<>();
+        timeFields.add(option(TIME_FIELD_FINISH, TIME_FIELD_FINISH, "瀹屾垚鏃堕棿", TIME_FIELD_FINISH));
+        timeFields.add(option(TIME_FIELD_APPE, TIME_FIELD_APPE, "鍒涘缓鏃堕棿", TIME_FIELD_APPE));
+        List<Map<String, Object>> deviceTypes = new ArrayList<>();
+        deviceTypes.add(option(DEVICE_TYPE_CRN, DEVICE_TYPE_CRN, "鍗曞爢鍨涙満", DEVICE_TYPE_CRN));
+        deviceTypes.add(option(DEVICE_TYPE_DUAL_CRN, DEVICE_TYPE_DUAL_CRN, "鍙屽伐浣嶅爢鍨涙満", DEVICE_TYPE_DUAL_CRN));
+        deviceTypes.add(option(DEVICE_TYPE_RGV, DEVICE_TYPE_RGV, "RGV", DEVICE_TYPE_RGV));
+        List<Map<String, Object>> statusList = basWrkStatusService.list(new QueryWrapper<BasWrkStatus>().orderByAsc("wrk_sts"))
+                .stream()
+                .map(item -> option(String.valueOf(item.getWrkSts()), String.valueOf(item.getWrkSts()), item.getWrkDesc(), item.getWrkSts()))
+                .collect(Collectors.toList());
+        List<Map<String, Object>> stations = basStationService.list(new QueryWrapper<BasStation>().orderByAsc("station_id"))
+                .stream()
+                .map(item -> {
+                    String label = item.getStationAlias();
+                    if (Cools.isEmpty(label)) {
+                        label = "绔欑偣" + item.getStationId();
+                    } else {
+                        label = item.getStationId() + " - " + label;
+                    }
+                    return option(String.valueOf(item.getStationId()), String.valueOf(item.getStationId()), label, item.getStationId());
+                })
+                .collect(Collectors.toList());
+        result.put("ioTypes", ioTypes);
+        result.put("timeFields", timeFields);
+        result.put("deviceTypes", deviceTypes);
+        result.put("statuses", statusList);
+        result.put("stations", stations);
+        return result;
+    }
+
+    @Override
+    public Map<String, Object> queryList(Integer curr, Integer limit, Map<String, Object> param) {
+        int pageNo = curr == null || curr <= 0 ? 1 : curr;
+        int pageSize = limit == null || limit <= 0 ? 20 : limit;
+        QueryWrapper<WrkMastLog> wrapper = buildHistoryLogWrapper(param);
+        Page<WrkMastLog> page = wrkMastLogService.page(new Page<>(pageNo, pageSize), wrapper);
+        List<WrkMastLog> logList = page.getRecords();
+        List<Integer> wrkNos = logList.stream().map(WrkMastLog::getWrkNo).filter(Objects::nonNull).collect(Collectors.toList());
+        Map<Integer, WrkAnalysis> analysisMap = mapByWrkNos(wrkNos);
+        List<Map<String, Object>> records = new ArrayList<>();
+        for (WrkMastLog log : logList) {
+            records.add(toListItem(log, analysisMap.get(log.getWrkNo())));
+        }
+        Map<String, Object> data = new LinkedHashMap<>();
+        data.put("records", records);
+        data.put("total", page.getTotal());
+        data.put("size", page.getSize());
+        data.put("current", page.getCurrent());
+        return data;
+    }
+
+    @Override
+    public Map<String, Object> analyze(JSONObject param) {
+        JSONObject request = param == null ? new JSONObject() : param;
+        String mode = upperTrim(request.getString("mode"));
+        if (Cools.isEmpty(mode)) {
+            mode = MODE_TIME;
+        }
+        QueryWrapper<WrkAnalysis> wrapper = buildAnalysisWrapper(request, mode);
+        List<WrkAnalysis> list = this.list(wrapper);
+        list.sort(Comparator.comparing(WrkAnalysis::getFinishTime, Comparator.nullsLast(Date::compareTo))
+                .thenComparing(WrkAnalysis::getWrkNo, Comparator.nullsLast(Integer::compareTo)));
+        Collections.reverse(list);
+        String timeField = TIME_FIELD_FINISH;
+        if (TIME_FIELD_APPE.equalsIgnoreCase(request.getString("timeField"))) {
+            timeField = TIME_FIELD_APPE;
+        }
+        return buildAnalysisResult(list, mode, timeField, request);
+    }
+
+    private QueryWrapper<WrkMastLog> buildHistoryLogWrapper(Map<String, Object> param) {
+        QueryWrapper<WrkMastLog> wrapper = new QueryWrapper<>();
+        String keyword = stringValue(param.get("keyword"));
+        if (!Cools.isEmpty(keyword)) {
+            wrapper.and(w -> w.like("wrk_no", keyword)
+                    .or().like("wms_wrk_no", keyword)
+                    .or().like("loc_no", keyword)
+                    .or().like("source_loc_no", keyword)
+                    .or().like("barcode", keyword));
+        }
+        Integer ioType = parseInteger(param.get("ioType"));
+        if (ioType != null) {
+            wrapper.eq("io_type", ioType);
+        }
+        Long finalWrkSts = parseLong(param.get("finalWrkSts"));
+        if (finalWrkSts != null) {
+            wrapper.eq("wrk_sts", finalWrkSts);
+        } else {
+            wrapper.in("wrk_sts", DEFAULT_FINAL_STATUSES);
+        }
+        Integer sourceStaNo = parseInteger(param.get("sourceStaNo"));
+        if (sourceStaNo != null) {
+            wrapper.eq("source_sta_no", sourceStaNo);
+        }
+        Integer staNo = parseInteger(param.get("staNo"));
+        if (staNo != null) {
+            wrapper.eq("sta_no", staNo);
+        }
+        applyDeviceTypeFilter(wrapper, upperTrim(stringValue(param.get("deviceType"))));
+        applyRange(wrapper, "appe_time", stringValue(param.get("appeTimeRange")));
+        applyRange(wrapper, "modi_time", stringValue(param.get("finishTimeRange")));
+        wrapper.orderByDesc("modi_time", "wrk_no");
+        return wrapper;
+    }
+
+    private QueryWrapper<WrkAnalysis> buildAnalysisWrapper(JSONObject request, String mode) {
+        QueryWrapper<WrkAnalysis> wrapper = new QueryWrapper<>();
+        Integer ioType = parseInteger(request.get("ioType"));
+        if (ioType != null) {
+            wrapper.eq("io_type", ioType);
+        }
+        Long finalWrkSts = parseLong(request.get("finalWrkSts"));
+        if (finalWrkSts != null) {
+            wrapper.eq("final_wrk_sts", finalWrkSts);
+        } else {
+            wrapper.in("final_wrk_sts", DEFAULT_FINAL_STATUSES);
+        }
+        Integer sourceStaNo = parseInteger(request.get("sourceStaNo"));
+        if (sourceStaNo != null) {
+            wrapper.eq("source_sta_no", sourceStaNo);
+        }
+        Integer staNo = parseInteger(request.get("staNo"));
+        if (staNo != null) {
+            wrapper.eq("sta_no", staNo);
+        }
+        String deviceType = upperTrim(request.getString("deviceType"));
+        if (DEVICE_TYPE_CRN.equals(deviceType)) {
+            wrapper.isNotNull("crn_no");
+        } else if (DEVICE_TYPE_DUAL_CRN.equals(deviceType)) {
+            wrapper.isNotNull("dual_crn_no");
+        } else if (DEVICE_TYPE_RGV.equals(deviceType)) {
+            wrapper.isNotNull("rgv_no");
+        }
+        if (MODE_TASK.equals(mode)) {
+            List<Integer> wrkNos = parseWrkNos(request.get("wrkNos"));
+            if (wrkNos.isEmpty()) {
+                wrapper.eq("wrk_no", -1);
+                return wrapper;
+            }
+            wrapper.in("wrk_no", wrkNos);
+        } else {
+            String timeField = TIME_FIELD_APPE.equalsIgnoreCase(request.getString("timeField")) ? TIME_FIELD_APPE : TIME_FIELD_FINISH;
+            Long startTime = parseLong(request.get("startTime"));
+            Long endTime = parseLong(request.get("endTime"));
+            if (startTime != null && endTime != null) {
+                wrapper.ge(timeField, new Date(startTime));
+                wrapper.le(timeField, new Date(endTime));
+            } else {
+                wrapper.eq("wrk_no", -1);
+                return wrapper;
+            }
+        }
+        return wrapper;
+    }
+
+    private void applyDeviceTypeFilter(QueryWrapper<WrkMastLog> wrapper, String deviceType) {
+        if (Cools.isEmpty(deviceType)) {
+            return;
+        }
+        QueryWrapper<WrkAnalysis> analysisWrapper = new QueryWrapper<>();
+        analysisWrapper.select("wrk_no");
+        if (DEVICE_TYPE_CRN.equals(deviceType)) {
+            analysisWrapper.isNotNull("crn_no");
+        } else if (DEVICE_TYPE_DUAL_CRN.equals(deviceType)) {
+            analysisWrapper.isNotNull("dual_crn_no");
+        } else if (DEVICE_TYPE_RGV.equals(deviceType)) {
+            analysisWrapper.isNotNull("rgv_no");
+        } else {
+            return;
+        }
+        List<Integer> wrkNos = this.list(analysisWrapper).stream()
+                .map(WrkAnalysis::getWrkNo)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+        if (wrkNos.isEmpty()) {
+            wrapper.eq("wrk_no", -1);
+            return;
+        }
+        wrapper.in("wrk_no", wrkNos);
+    }
+
+    private Map<String, Object> buildAnalysisResult(List<WrkAnalysis> list, String mode, String timeField, JSONObject request) {
+        Map<String, Object> result = new LinkedHashMap<>();
+        Map<String, Object> summary = new LinkedHashMap<>();
+        summary.put("taskCount", list.size());
+        summary.put("avgTotalDurationMs", average(list, item -> item.getTotalDurationMs() != null, WrkAnalysis::getTotalDurationMs));
+        summary.put("avgStationDurationMs", average(list, item -> !METRIC_PARTIAL.equals(item.getMetricCompleteness()) && item.getStationDurationMs() != null, WrkAnalysis::getStationDurationMs));
+        summary.put("avgCraneDurationMs", average(list, item -> !METRIC_PARTIAL.equals(item.getMetricCompleteness()) && item.getCraneDurationMs() != null, WrkAnalysis::getCraneDurationMs));
+        summary.put("faultTaskCount", list.stream().filter(this::hasFault).count());
+        summary.put("faultDurationMs", list.stream().map(WrkAnalysis::getFaultDurationMs).filter(Objects::nonNull).reduce(0L, Long::sum));
+        summary.put("partialTaskCount", list.stream().filter(item -> METRIC_PARTIAL.equals(item.getMetricCompleteness())).count());
+        result.put("summary", summary);
+        result.put("durationCompare", buildDurationCompare(list));
+        result.put("trend", buildTrend(list, mode, timeField, request));
+        result.put("faultPie", buildFaultPie(list));
+        result.put("faultDuration", buildFaultDuration(list));
+        result.put("detail", buildDetailRows(list));
+        result.put("timeField", timeField);
+        return result;
+    }
+
+    private List<Map<String, Object>> buildDurationCompare(List<WrkAnalysis> list) {
+        return list.stream()
+                .limit(DURATION_COMPARE_LIMIT)
+                .map(this::toDetailItem)
+                .collect(Collectors.toList());
+    }
+
+    private List<Map<String, Object>> buildTrend(List<WrkAnalysis> list, String mode, String timeField, JSONObject request) {
+        List<Map<String, Object>> trend = new ArrayList<>();
+        if (list.isEmpty()) {
+            return trend;
+        }
+        long startMs = Long.MAX_VALUE;
+        long endMs = Long.MIN_VALUE;
+        if (MODE_TIME.equals(mode)) {
+            Long reqStart = parseLong(request.get("startTime"));
+            Long reqEnd = parseLong(request.get("endTime"));
+            if (reqStart != null && reqEnd != null) {
+                startMs = reqStart;
+                endMs = reqEnd;
+            }
+        }
+        if (startMs == Long.MAX_VALUE || endMs == Long.MIN_VALUE) {
+            for (WrkAnalysis item : list) {
+                Date date = resolveBucketTime(item, timeField);
+                if (date == null) {
+                    continue;
+                }
+                long time = date.getTime();
+                startMs = Math.min(startMs, time);
+                endMs = Math.max(endMs, time);
+            }
+        }
+        if (startMs == Long.MAX_VALUE || endMs == Long.MIN_VALUE) {
+            return trend;
+        }
+        boolean hourly = endMs - startMs <= 72L * 60L * 60L * 1000L;
+        SimpleDateFormat keyFormat = new SimpleDateFormat(hourly ? "yyyy-MM-dd HH:00" : "yyyy-MM-dd");
+        keyFormat.setTimeZone(TimeZone.getDefault());
+        Map<String, BucketAccumulator> bucketMap = new LinkedHashMap<>();
+        List<WrkAnalysis> sorted = new ArrayList<>(list);
+        sorted.sort(Comparator.comparing(item -> resolveBucketTime(item, timeField), Comparator.nullsLast(Date::compareTo)));
+        for (WrkAnalysis item : sorted) {
+            Date bucketTime = resolveBucketTime(item, timeField);
+            if (bucketTime == null) {
+                continue;
+            }
+            String key = keyFormat.format(bucketTime);
+            BucketAccumulator accumulator = bucketMap.computeIfAbsent(key, k -> new BucketAccumulator());
+            accumulator.taskCount++;
+            if (item.getTotalDurationMs() != null) {
+                accumulator.totalDurationMs += item.getTotalDurationMs();
+                accumulator.totalDurationCount++;
+            }
+            if (!METRIC_PARTIAL.equals(item.getMetricCompleteness()) && item.getStationDurationMs() != null) {
+                accumulator.stationDurationMs += item.getStationDurationMs();
+                accumulator.stationDurationCount++;
+            }
+            if (!METRIC_PARTIAL.equals(item.getMetricCompleteness()) && item.getCraneDurationMs() != null) {
+                accumulator.craneDurationMs += item.getCraneDurationMs();
+                accumulator.craneDurationCount++;
+            }
+        }
+        for (Map.Entry<String, BucketAccumulator> entry : bucketMap.entrySet()) {
+            Map<String, Object> item = new LinkedHashMap<>();
+            item.put("bucketLabel", entry.getKey());
+            item.put("taskCount", entry.getValue().taskCount);
+            item.put("avgTotalDurationMs", entry.getValue().totalDurationCount == 0 ? null : entry.getValue().totalDurationMs / entry.getValue().totalDurationCount);
+            item.put("avgStationDurationMs", entry.getValue().stationDurationCount == 0 ? null : entry.getValue().stationDurationMs / entry.getValue().stationDurationCount);
+            item.put("avgCraneDurationMs", entry.getValue().craneDurationCount == 0 ? null : entry.getValue().craneDurationMs / entry.getValue().craneDurationCount);
+            trend.add(item);
+        }
+        return trend;
+    }
+
+    private List<Map<String, Object>> buildFaultPie(List<WrkAnalysis> list) {
+        long fault = list.stream().filter(this::hasFault).count();
+        long normal = Math.max(0, list.size() - fault);
+        List<Map<String, Object>> result = new ArrayList<>();
+        result.add(slice("鏁呴殰浠诲姟", fault));
+        result.add(slice("鏃犳晠闅滀换鍔�", normal));
+        return result;
+    }
+
+    private List<Map<String, Object>> buildFaultDuration(List<WrkAnalysis> list) {
+        List<Map<String, Object>> result = new ArrayList<>();
+        result.add(slice("鍗曞爢鍨涙満", list.stream().map(WrkAnalysis::getCrnFaultDurationMs).filter(Objects::nonNull).reduce(0L, Long::sum)));
+        result.add(slice("鍙屽伐浣嶅爢鍨涙満", list.stream().map(WrkAnalysis::getDualCrnFaultDurationMs).filter(Objects::nonNull).reduce(0L, Long::sum)));
+        result.add(slice("RGV", list.stream().map(WrkAnalysis::getRgvFaultDurationMs).filter(Objects::nonNull).reduce(0L, Long::sum)));
+        return result;
+    }
+
+    private List<Map<String, Object>> buildDetailRows(List<WrkAnalysis> list) {
+        return list.stream().map(this::toDetailItem).collect(Collectors.toList());
+    }
+
+    private Map<String, Object> toListItem(WrkMastLog log, WrkAnalysis analysis) {
+        Map<String, Object> item = new LinkedHashMap<>();
+        item.put("wrkNo", log.getWrkNo());
+        item.put("wmsWrkNo", log.getWmsWrkNo());
+        item.put("wrkSts", log.getWrkSts());
+        item.put("wrkSts$", log.getWrkSts$());
+        item.put("ioType", log.getIoType());
+        item.put("ioType$", log.getIoType$());
+        item.put("sourceStaNo", log.getSourceStaNo());
+        item.put("staNo", log.getStaNo());
+        item.put("sourceLocNo", log.getSourceLocNo());
+        item.put("locNo", log.getLocNo());
+        item.put("barcode", log.getBarcode());
+        item.put("appeTime", log.getAppeTime());
+        item.put("appeTime$", formatDate(log.getAppeTime()));
+        item.put("finishTime", analysis == null ? null : analysis.getFinishTime());
+        item.put("finishTime$", analysis == null ? "" : formatDate(analysis.getFinishTime()));
+        fillMetrics(item, analysis);
+        return item;
+    }
+
+    private Map<String, Object> toDetailItem(WrkAnalysis item) {
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("wrkNo", item.getWrkNo());
+        result.put("wmsWrkNo", item.getWmsWrkNo());
+        result.put("ioType", item.getIoType());
+        result.put("ioType$", resolveIoTypeDesc(item.getIoType()));
+        result.put("finalWrkSts", item.getFinalWrkSts());
+        result.put("finalWrkSts$", resolveWrkStsDesc(item.getFinalWrkSts()));
+        result.put("sourceStaNo", item.getSourceStaNo());
+        result.put("staNo", item.getStaNo());
+        result.put("sourceLocNo", item.getSourceLocNo());
+        result.put("locNo", item.getLocNo());
+        result.put("appeTime", item.getAppeTime());
+        result.put("appeTime$", formatDate(item.getAppeTime()));
+        result.put("finishTime", item.getFinishTime());
+        result.put("finishTime$", formatDate(item.getFinishTime()));
+        fillMetrics(result, item);
+        return result;
+    }
+
+    private void fillMetrics(Map<String, Object> target, WrkAnalysis analysis) {
+        target.put("totalDurationMs", analysis == null ? null : analysis.getTotalDurationMs());
+        target.put("stationDurationMs", analysis == null ? null : analysis.getStationDurationMs());
+        target.put("craneDurationMs", analysis == null ? null : analysis.getCraneDurationMs());
+        target.put("faultCount", analysis == null ? 0 : defaultInt(analysis.getFaultCount()));
+        target.put("faultDurationMs", analysis == null ? 0L : defaultLong(analysis.getFaultDurationMs()));
+        target.put("hasFault", analysis == null ? 0 : defaultInt(analysis.getHasFault()));
+        target.put("crnFaultDurationMs", analysis == null ? 0L : defaultLong(analysis.getCrnFaultDurationMs()));
+        target.put("dualCrnFaultDurationMs", analysis == null ? 0L : defaultLong(analysis.getDualCrnFaultDurationMs()));
+        target.put("rgvFaultDurationMs", analysis == null ? 0L : defaultLong(analysis.getRgvFaultDurationMs()));
+        target.put("metricCompleteness", analysis == null ? METRIC_PARTIAL : analysis.getMetricCompleteness());
+    }
+
+    private FaultSummary buildFaultSummary(Integer wrkNo, Date finishTime) {
+        FaultSummary summary = new FaultSummary();
+        if (wrkNo == null) {
+            return summary;
+        }
+        List<BasCrnpErrLog> crnList = basCrnpErrLogService.list(new QueryWrapper<BasCrnpErrLog>().eq("wrk_no", wrkNo));
+        List<BasDualCrnpErrLog> dualList = basDualCrnpErrLogService.list(new QueryWrapper<BasDualCrnpErrLog>().eq("wrk_no", wrkNo));
+        List<BasRgvErrLog> rgvList = basRgvErrLogService.list(new QueryWrapper<BasRgvErrLog>().eq("task_no", wrkNo));
+        summary.crnCount = crnList.size();
+        summary.crnDurationMs = durationMs(crnList, finishTime);
+        summary.dualCount = dualList.size();
+        summary.dualDurationMs = durationMs(dualList, finishTime);
+        summary.rgvCount = rgvList.size();
+        summary.rgvDurationMs = durationMs(rgvList, finishTime);
+        summary.totalCount = summary.crnCount + summary.dualCount + summary.rgvCount;
+        summary.totalDurationMs = summary.crnDurationMs + summary.dualDurationMs + summary.rgvDurationMs;
+        summary.hasFault = summary.totalCount > 0 ? 1 : 0;
+        return summary;
+    }
+
+    private Long durationMs(List<? extends Object> list, Date finishTime) {
+        long total = 0L;
+        for (Object item : list) {
+            Date startTime = null;
+            Date endTime = null;
+            if (item instanceof BasCrnpErrLog) {
+                startTime = ((BasCrnpErrLog) item).getStartTime();
+                endTime = ((BasCrnpErrLog) item).getEndTime();
+            } else if (item instanceof BasDualCrnpErrLog) {
+                startTime = ((BasDualCrnpErrLog) item).getStartTime();
+                endTime = ((BasDualCrnpErrLog) item).getEndTime();
+            } else if (item instanceof BasRgvErrLog) {
+                startTime = ((BasRgvErrLog) item).getStartTime();
+                endTime = ((BasRgvErrLog) item).getEndTime();
+            }
+            if (startTime == null) {
+                continue;
+            }
+            total += durationMs(startTime, endTime == null ? finishTime : endTime);
+        }
+        return total;
+    }
+
+    private String resolveMetricCompleteness(WrkMast wrkMast, WrkAnalysis entity) {
+        if (wrkMast == null) {
+            return METRIC_PARTIAL;
+        }
+        if (Objects.equals(wrkMast.getIoType(), WrkIoType.LOC_MOVE.id)) {
+            return entity.getCraneStartTime() != null && entity.getCraneEndTime() != null ? METRIC_COMPLETE : METRIC_PARTIAL;
+        }
+        return entity.getStationStartTime() != null
+                && entity.getStationEndTime() != null
+                && entity.getCraneStartTime() != null
+                && entity.getCraneEndTime() != null ? METRIC_COMPLETE : METRIC_PARTIAL;
+    }
+
+    private WrkAnalysis ensureRecord(WrkMast wrkMast) {
+        if (wrkMast == null || wrkMast.getWrkNo() == null) {
+            return null;
+        }
+        WrkAnalysis entity = this.getById(wrkMast.getWrkNo());
+        if (entity != null) {
+            syncBaseFields(entity, wrkMast);
+            return entity;
+        }
+        initForTask(wrkMast);
+        return this.getById(wrkMast.getWrkNo());
+    }
+
+    private void syncBaseFields(WrkAnalysis entity, WrkMast wrkMast) {
+        entity.setWrkNo(wrkMast.getWrkNo());
+        entity.setWmsWrkNo(wrkMast.getWmsWrkNo());
+        entity.setIoType(wrkMast.getIoType());
+        entity.setFinalWrkSts(wrkMast.getWrkSts());
+        entity.setSourceStaNo(wrkMast.getSourceStaNo());
+        entity.setStaNo(wrkMast.getStaNo());
+        entity.setSourceLocNo(wrkMast.getSourceLocNo());
+        entity.setLocNo(wrkMast.getLocNo());
+        entity.setCrnNo(wrkMast.getCrnNo());
+        entity.setDualCrnNo(wrkMast.getDualCrnNo());
+        entity.setRgvNo(wrkMast.getRgvNo());
+        entity.setAppeTime(wrkMast.getAppeTime());
+    }
+
+    private Date resolveBucketTime(WrkAnalysis item, String timeField) {
+        if (TIME_FIELD_APPE.equals(timeField)) {
+            return item.getAppeTime();
+        }
+        return item.getFinishTime();
+    }
+
+    private Map<String, Object> option(String key, String code, String label, Object value) {
+        Map<String, Object> item = new LinkedHashMap<>();
+        item.put("key", key);
+        item.put("code", code);
+        item.put("label", label);
+        item.put("value", value);
+        return item;
+    }
+
+    private Map<String, Object> slice(String name, Object value) {
+        Map<String, Object> item = new LinkedHashMap<>();
+        item.put("name", name);
+        item.put("value", value);
+        return item;
+    }
+
+    private String formatDate(Date date) {
+        if (date == null) {
+            return "";
+        }
+        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date);
+    }
+
+    private String resolveWrkStsDesc(Long wrkSts) {
+        if (wrkSts == null) {
+            return "";
+        }
+        BasWrkStatus basWrkStatus = basWrkStatusService.getById(wrkSts);
+        if (basWrkStatus != null && !Cools.isEmpty(basWrkStatus.getWrkDesc())) {
+            return basWrkStatus.getWrkDesc();
+        }
+        try {
+            return WrkStsType.query(wrkSts).desc;
+        } catch (Exception ignore) {
+            return String.valueOf(wrkSts);
+        }
+    }
+
+    private String resolveIoTypeDesc(Integer ioType) {
+        if (ioType == null) {
+            return "";
+        }
+        if (Objects.equals(ioType, WrkIoType.IN.id)) {
+            return "鍏ュ簱";
+        }
+        if (Objects.equals(ioType, WrkIoType.OUT.id)) {
+            return "鍑哄簱";
+        }
+        if (Objects.equals(ioType, WrkIoType.LOC_MOVE.id)) {
+            return "绉诲簱";
+        }
+        return String.valueOf(ioType);
+    }
+
+    private boolean hasFault(WrkAnalysis item) {
+        return item != null && (defaultInt(item.getHasFault()) > 0 || defaultInt(item.getFaultCount()) > 0);
+    }
+
+    private Long average(List<WrkAnalysis> list,
+                         java.util.function.Predicate<WrkAnalysis> predicate,
+                         java.util.function.Function<WrkAnalysis, Long> valueFn) {
+        long total = 0L;
+        long count = 0L;
+        for (WrkAnalysis item : list) {
+            if (!predicate.test(item)) {
+                continue;
+            }
+            Long value = valueFn.apply(item);
+            if (value == null) {
+                continue;
+            }
+            total += value;
+            count++;
+        }
+        return count == 0 ? null : total / count;
+    }
+
+    private void applyRange(QueryWrapper<WrkMastLog> wrapper, String column, String rawValue) {
+        if (Cools.isEmpty(rawValue) || !rawValue.contains("~")) {
+            return;
+        }
+        String[] parts = rawValue.split("~");
+        if (parts.length != 2) {
+            return;
+        }
+        wrapper.ge(column, DateUtils.convert(parts[0].trim()));
+        wrapper.le(column, DateUtils.convert(parts[1].trim()));
+    }
+
+    private List<Integer> parseWrkNos(Object value) {
+        List<Integer> result = new ArrayList<>();
+        if (value == null) {
+            return result;
+        }
+        if (value instanceof JSONArray) {
+            JSONArray array = (JSONArray) value;
+            for (int i = 0; i < array.size(); i++) {
+                Integer item = parseInteger(array.get(i));
+                if (item != null) {
+                    result.add(item);
+                }
+            }
+            return result;
+        }
+        if (value instanceof Collection) {
+            for (Object item : (Collection<?>) value) {
+                Integer parsed = parseInteger(item);
+                if (parsed != null) {
+                    result.add(parsed);
+                }
+            }
+            return result;
+        }
+        String text = String.valueOf(value).trim();
+        if (text.startsWith("[") && text.endsWith("]")) {
+            return parseWrkNos(JSONArray.parseArray(text));
+        }
+        if (!Cools.isEmpty(text)) {
+            for (String part : text.split(",")) {
+                Integer parsed = parseInteger(part);
+                if (parsed != null) {
+                    result.add(parsed);
+                }
+            }
+        }
+        return result;
+    }
+
+    private Integer parseInteger(Object value) {
+        if (value == null || Cools.isEmpty(value)) {
+            return null;
+        }
+        try {
+            return Integer.valueOf(String.valueOf(value).trim());
+        } catch (Exception ignore) {
+            return null;
+        }
+    }
+
+    private Long parseLong(Object value) {
+        if (value == null || Cools.isEmpty(value)) {
+            return null;
+        }
+        try {
+            return Long.valueOf(String.valueOf(value).trim());
+        } catch (Exception ignore) {
+            return null;
+        }
+    }
+
+    private String stringValue(Object value) {
+        return value == null ? null : String.valueOf(value).trim();
+    }
+
+    private String upperTrim(String value) {
+        return value == null ? null : value.trim().toUpperCase(Locale.ROOT);
+    }
+
+    private Date safeDate(Date date) {
+        return date == null ? new Date() : date;
+    }
+
+    private long durationMs(Date startTime, Date endTime) {
+        if (startTime == null || endTime == null) {
+            return 0L;
+        }
+        return Math.max(0L, endTime.getTime() - startTime.getTime());
+    }
+
+    private Integer defaultInt(Integer value) {
+        return value == null ? 0 : value;
+    }
+
+    private Long defaultLong(Long value) {
+        return value == null ? 0L : value;
+    }
+
+    private static class FaultSummary {
+        private int hasFault;
+        private int totalCount;
+        private long totalDurationMs;
+        private int crnCount;
+        private long crnDurationMs;
+        private int dualCount;
+        private long dualDurationMs;
+        private int rgvCount;
+        private long rgvDurationMs;
+    }
+
+    private static class BucketAccumulator {
+        private long taskCount;
+        private long totalDurationMs;
+        private long totalDurationCount;
+        private long stationDurationMs;
+        private long stationDurationCount;
+        private long craneDurationMs;
+        private long craneDurationCount;
+    }
+
+}
diff --git a/src/main/java/com/zy/asrs/service/impl/WrkMastLogServiceImpl.java b/src/main/java/com/zy/asrs/service/impl/WrkMastLogServiceImpl.java
index e75f8e2..1092423 100644
--- a/src/main/java/com/zy/asrs/service/impl/WrkMastLogServiceImpl.java
+++ b/src/main/java/com/zy/asrs/service/impl/WrkMastLogServiceImpl.java
@@ -6,15 +6,17 @@
 import com.zy.asrs.mapper.WrkMastLogMapper;
 import com.zy.asrs.service.WrkMastLogService;
 import com.zy.asrs.service.WrkMastService;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 
 @Service("wrkMastLogService")
 public class WrkMastLogServiceImpl extends ServiceImpl<WrkMastLogMapper, WrkMastLog> implements WrkMastLogService {
 
-    @Autowired
-    private WrkMastService wrkMastService;
+    private final WrkMastService wrkMastService;
+
+    public WrkMastLogServiceImpl(WrkMastService wrkMastService) {
+        this.wrkMastService = wrkMastService;
+    }
 
     @Override
     public boolean save(Integer wrkNo) {
diff --git a/src/main/java/com/zy/asrs/task/WrkAnalysisStationArrivalScanner.java b/src/main/java/com/zy/asrs/task/WrkAnalysisStationArrivalScanner.java
new file mode 100644
index 0000000..9740c55
--- /dev/null
+++ b/src/main/java/com/zy/asrs/task/WrkAnalysisStationArrivalScanner.java
@@ -0,0 +1,81 @@
+package com.zy.asrs.task;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.zy.asrs.entity.BasStation;
+import com.zy.asrs.entity.WrkMast;
+import com.zy.asrs.service.BasStationService;
+import com.zy.asrs.service.WrkAnalysisService;
+import com.zy.asrs.service.WrkMastService;
+import com.zy.core.News;
+import com.zy.core.cache.SlaveConnection;
+import com.zy.core.enums.SlaveType;
+import com.zy.core.enums.WrkStsType;
+import com.zy.core.model.protocol.StationProtocol;
+import com.zy.core.thread.StationThread;
+import com.zy.core.utils.StationOperateProcessUtils;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@Component
+public class WrkAnalysisStationArrivalScanner {
+
+    private final WrkMastService wrkMastService;
+    private final BasStationService basStationService;
+    private final WrkAnalysisService wrkAnalysisService;
+    private final StationOperateProcessUtils stationOperateProcessUtils;
+
+    public WrkAnalysisStationArrivalScanner(WrkMastService wrkMastService,
+                                            BasStationService basStationService,
+                                            WrkAnalysisService wrkAnalysisService,
+                                            StationOperateProcessUtils stationOperateProcessUtils) {
+        this.wrkMastService = wrkMastService;
+        this.basStationService = basStationService;
+        this.wrkAnalysisService = wrkAnalysisService;
+        this.stationOperateProcessUtils = stationOperateProcessUtils;
+    }
+
+    @Scheduled(fixedDelay = 1000L)
+    public void scanOutboundStationFlow() {
+        stationOperateProcessUtils.stationOutExecuteFinish();
+        stationOperateProcessUtils.checkTaskToComplete();
+    }
+
+    @Scheduled(fixedDelay = 1000L)
+    public void scanInboundStationArrival() {
+        List<WrkMast> wrkMasts = wrkMastService.list(new QueryWrapper<WrkMast>()
+                .eq("io_type", 1)
+                .eq("wrk_sts", WrkStsType.INBOUND_STATION_RUN.sts)
+                .isNotNull("sta_no"));
+        for (WrkMast wrkMast : wrkMasts) {
+            if (wrkMast == null || wrkMast.getWrkNo() == null || wrkMast.getStaNo() == null) {
+                continue;
+            }
+            BasStation basStation = basStationService.getOne(new QueryWrapper<BasStation>()
+                    .eq("station_id", wrkMast.getStaNo())
+                    .last("limit 1"));
+            if (basStation == null || basStation.getDeviceNo() == null) {
+                continue;
+            }
+            StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basStation.getDeviceNo());
+            if (stationThread == null) {
+                continue;
+            }
+            Map<Integer, StationProtocol> statusMap = stationThread.getStatusMap();
+            StationProtocol stationProtocol = statusMap == null ? null : statusMap.get(basStation.getStationId());
+            if (stationProtocol == null) {
+                continue;
+            }
+            if (!wrkMast.getWrkNo().equals(stationProtocol.getTaskNo()) || !stationProtocol.isLoading()) {
+                continue;
+            }
+            boolean updated = wrkAnalysisService.completeInboundStationRun(wrkMast, new Date());
+            if (updated) {
+                News.info("鍏ュ簱绔欑偣鍒拌揪鎵弿鍛戒腑锛屽伐浣滃彿={}锛岀洰鏍囩珯={}", wrkMast.getWrkNo(), wrkMast.getStaNo());
+            }
+        }
+    }
+}
diff --git a/src/main/java/com/zy/asrs/task/WrkMastScheduler.java b/src/main/java/com/zy/asrs/task/WrkMastScheduler.java
index e9d01c5..87dfaea 100644
--- a/src/main/java/com/zy/asrs/task/WrkMastScheduler.java
+++ b/src/main/java/com/zy/asrs/task/WrkMastScheduler.java
@@ -6,6 +6,7 @@
 import com.zy.asrs.entity.LocMast;
 import com.zy.asrs.entity.WrkMast;
 import com.zy.asrs.service.LocMastService;
+import com.zy.asrs.service.WrkAnalysisService;
 import com.zy.asrs.service.WrkMastLogService;
 import com.zy.asrs.service.WrkMastService;
 import com.zy.asrs.utils.NotifyUtils;
@@ -13,7 +14,6 @@
 import com.zy.core.enums.WrkIoType;
 import com.zy.core.enums.WrkStsType;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 import org.springframework.transaction.annotation.Transactional;
@@ -26,14 +26,23 @@
 @Slf4j
 public class WrkMastScheduler {
 
-    @Autowired
-    private WrkMastService wrkMastService;
-    @Autowired
-    private WrkMastLogService wrkMastLogService;
-    @Autowired
-    private LocMastService locMastService;
-    @Autowired
-    private NotifyUtils notifyUtils;
+    private final WrkMastService wrkMastService;
+    private final WrkMastLogService wrkMastLogService;
+    private final WrkAnalysisService wrkAnalysisService;
+    private final LocMastService locMastService;
+    private final NotifyUtils notifyUtils;
+
+    public WrkMastScheduler(WrkMastService wrkMastService,
+                            WrkMastLogService wrkMastLogService,
+                            WrkAnalysisService wrkAnalysisService,
+                            LocMastService locMastService,
+                            NotifyUtils notifyUtils) {
+        this.wrkMastService = wrkMastService;
+        this.wrkMastLogService = wrkMastLogService;
+        this.wrkAnalysisService = wrkAnalysisService;
+        this.locMastService = locMastService;
+        this.notifyUtils = notifyUtils;
+    }
 
     @Scheduled(cron = "0/1 * * * * ? ")
     @Transactional
@@ -68,6 +77,8 @@
             // 淇濆瓨宸ヤ綔涓绘。鍘嗗彶妗�
             if (!wrkMastLogService.save(wrkMast.getWrkNo())) {
                 log.info("淇濆瓨宸ヤ綔鍘嗗彶妗workNo={}]澶辫触", wrkMast.getWrkNo());
+            } else {
+                wrkAnalysisService.finishTask(wrkMast, resolveFinishTime(wrkMast));
             }
             // 鍒犻櫎宸ヤ綔涓绘。
             if (!wrkMastService.removeById(wrkMast.getWrkNo())) {
@@ -112,6 +123,8 @@
             // 淇濆瓨宸ヤ綔涓绘。鍘嗗彶妗�
             if (!wrkMastLogService.save(wrkMast.getWrkNo())) {
                 log.info("淇濆瓨宸ヤ綔鍘嗗彶妗workNo={}]澶辫触", wrkMast.getWrkNo());
+            } else {
+                wrkAnalysisService.finishTask(wrkMast, resolveFinishTime(wrkMast));
             }
             // 鍒犻櫎宸ヤ綔涓绘。
             if (!wrkMastService.removeById(wrkMast.getWrkNo())) {
@@ -177,6 +190,8 @@
             // 淇濆瓨宸ヤ綔涓绘。鍘嗗彶妗�
             if (!wrkMastLogService.save(wrkMast.getWrkNo())) {
                 log.info("淇濆瓨宸ヤ綔鍘嗗彶妗workNo={}]澶辫触", wrkMast.getWrkNo());
+            } else {
+                wrkAnalysisService.finishTask(wrkMast, resolveFinishTime(wrkMast));
             }
             // 鍒犻櫎宸ヤ綔涓绘。
             if (!wrkMastService.removeById(wrkMast.getWrkNo())) {
@@ -200,6 +215,8 @@
             // 淇濆瓨宸ヤ綔涓绘。鍘嗗彶妗�
             if (!wrkMastLogService.save(wrkMast.getWrkNo())) {
                 log.info("淇濆瓨宸ヤ綔鍘嗗彶妗workNo={}]澶辫触", wrkMast.getWrkNo());
+            } else {
+                wrkAnalysisService.finishTask(wrkMast, resolveFinishTime(wrkMast));
             }
             // 鍒犻櫎宸ヤ綔涓绘。
             if (!wrkMastService.removeById(wrkMast.getWrkNo())) {
@@ -273,4 +290,17 @@
         }
     }
 
+    private Date resolveFinishTime(WrkMast wrkMast) {
+        if (wrkMast == null) {
+            return new Date();
+        }
+        if (wrkMast.getModiTime() != null) {
+            return wrkMast.getModiTime();
+        }
+        if (wrkMast.getIoTime() != null) {
+            return wrkMast.getIoTime();
+        }
+        return new Date();
+    }
+
 }
diff --git a/src/main/java/com/zy/common/service/CommonService.java b/src/main/java/com/zy/common/service/CommonService.java
index 771bdeb..bb00969 100644
--- a/src/main/java/com/zy/common/service/CommonService.java
+++ b/src/main/java/com/zy/common/service/CommonService.java
@@ -48,6 +48,8 @@
     private BasOutStationAreaService basOutStationAreaService;
     @Autowired
     private WrkCommandRollbackService wrkCommandRollbackService;
+    @Autowired
+    private WrkAnalysisService wrkAnalysisService;
 
     /**
      * 鐢熸垚宸ヤ綔鍙�
@@ -290,6 +292,7 @@
             News.error("绉诲簱浠诲姟 --- 淇濆瓨宸ヤ綔妗eけ璐ワ紒");
             throw new CoolException("淇濆瓨宸ヤ綔妗eけ璐�");
         }
+        wrkAnalysisService.initForTask(wrkMast);
 
         sourceLocMast.setLocSts("R");
         sourceLocMast.setModiTime(new Date());
@@ -358,6 +361,7 @@
             News.error("鍏ュ簱浠诲姟 --- 淇濆瓨宸ヤ綔妗eけ璐ワ紒");
             throw new CoolException("淇濆瓨宸ヤ綔妗eけ璐�");
         }
+        wrkAnalysisService.initForTask(wrkMast);
 
         locMast.setLocSts("S");
         locMast.setModiTime(new Date());
@@ -475,6 +479,7 @@
             News.error("鍑哄簱浠诲姟 --- 淇濆瓨宸ヤ綔妗eけ璐ワ紒");
             throw new CoolException("淇濆瓨宸ヤ綔妗eけ璐�");
         }
+        wrkAnalysisService.initForTask(wrkMast);
 
         locMast.setLocSts("R");
         locMast.setModiTime(new Date());
diff --git a/src/main/java/com/zy/core/enums/WrkStsType.java b/src/main/java/com/zy/core/enums/WrkStsType.java
index 143e36f..1c713c1 100644
--- a/src/main/java/com/zy/core/enums/WrkStsType.java
+++ b/src/main/java/com/zy/core/enums/WrkStsType.java
@@ -5,9 +5,10 @@
 public enum WrkStsType {
 
     NEW_INBOUND(1, "鐢熸垚鍏ュ簱浠诲姟"),
-    INBOUND_DEVICE_RUN(2, "璁惧涓婅蛋"),
-    INBOUND_RUN(3, "璁惧鎼繍涓�"),
-    INBOUND_RUN_COMPLETE(4, "璁惧鎼繍瀹屾垚"),
+    INBOUND_STATION_RUN(2, "绔欑偣杩愯涓�"),
+    INBOUND_STATION_RUN_COMPLETE(3, "绔欑偣杩愯瀹屾垚"),
+    INBOUND_RUN(4, "璁惧鎼繍涓�"),
+    INBOUND_RUN_COMPLETE(5, "璁惧鎼繍瀹屾垚"),
     INBOUND_MANUAL(6, "鍏ュ簱寰呬汉宸ュ洖婊�"),
     COMPLETE_INBOUND(9, "鍏ュ簱瀹屾垚"),
     SETTLE_INBOUND(10, "鍏ュ簱搴撳瓨鏇存柊"),
diff --git a/src/main/java/com/zy/core/plugin/FakeProcess.java b/src/main/java/com/zy/core/plugin/FakeProcess.java
index c93863d..a136dd6 100644
--- a/src/main/java/com/zy/core/plugin/FakeProcess.java
+++ b/src/main/java/com/zy/core/plugin/FakeProcess.java
@@ -88,6 +88,8 @@
     @Autowired
     private WmsOperateUtils wmsOperateUtils;
     @Autowired
+    private WrkAnalysisService wrkAnalysisService;
+    @Autowired
     private DualCrnOperateProcessUtils dualCrnOperateProcessUtils;
     @Autowired
     private StoreInTaskGenerationService storeInTaskGenerationService;
@@ -128,10 +130,6 @@
         stationOperateProcessUtils.stationInExecute();
         // 鎵ц杈撻�佺珯鐐瑰嚭搴撲换鍔�
         stationOperateProcessUtils.crnStationOutExecute();
-        // 妫�娴嬭緭閫佺珯鐐瑰嚭搴撲换鍔℃墽琛屽畬鎴�
-        stationOperateProcessUtils.stationOutExecuteFinish();
-        // 妫�娴嬩换鍔¤浆瀹屾垚
-        stationOperateProcessUtils.checkTaskToComplete();
         // 妫�娴嬪嚭搴撴帓搴�
         stationOperateProcessUtils.checkStationOutOrder();
         // 鐩戞帶缁曞湀绔欑偣
@@ -594,7 +592,7 @@
                             JSON.toJSONString(command));
                 } else {
                     if (wrkMast.getWrkSts() != WrkStsType.NEW_INBOUND.sts
-                            && wrkMast.getWrkSts() != WrkStsType.INBOUND_DEVICE_RUN.sts) {
+                            && wrkMast.getWrkSts() != WrkStsType.INBOUND_STATION_RUN.sts) {
                         Integer crnNo = wrkMast.getCrnNo();
                         if (crnNo != null) {
                             CrnThread crnThread = (CrnThread) SlaveConnection.get(SlaveType.Crn, crnNo);
@@ -711,10 +709,13 @@
                     continue;
                 }
 
+                Date now = new Date();
                 wrkMast.setWrkSts(updateWrkSts);
                 wrkMast.setSystemMsg("");
-                wrkMast.setIoTime(new Date());
+                wrkMast.setIoTime(now);
+                wrkMast.setModiTime(now);
                 if (wrkMastService.updateById(wrkMast)) {
+                    wrkAnalysisService.markCraneComplete(wrkMast, now, updateWrkSts);
                     CrnCommand resetCommand = crnThread.getResetCommand(crnProtocol.getTaskNo(), crnProtocol.getCrnNo());
                     MessageQueue.offer(SlaveType.Crn, crnProtocol.getCrnNo(), new Task(2, resetCommand));
                     News.info("鍫嗗灈鏈轰换鍔$姸鎬佹洿鏂版垚鍔燂紝鍫嗗灈鏈哄彿={}锛屽伐浣滃彿={}", basCrnp.getCrnNo(), crnProtocol.getTaskNo());
diff --git a/src/main/java/com/zy/core/plugin/GslProcess.java b/src/main/java/com/zy/core/plugin/GslProcess.java
index c38b7d8..2f2f632 100644
--- a/src/main/java/com/zy/core/plugin/GslProcess.java
+++ b/src/main/java/com/zy/core/plugin/GslProcess.java
@@ -65,10 +65,6 @@
         stationOperateProcessUtils.stationInExecute();
         //鎵ц杈撻�佺珯鐐瑰嚭搴撲换鍔�
         stationOperateProcessUtils.crnStationOutExecute();
-        //妫�娴嬭緭閫佺珯鐐瑰嚭搴撲换鍔℃墽琛屽畬鎴�
-        stationOperateProcessUtils.stationOutExecuteFinish();
-        // 妫�娴嬩换鍔¤浆瀹屾垚
-        stationOperateProcessUtils.checkTaskToComplete();
         // 妫�娴嬪嚭搴撴帓搴�
         stationOperateProcessUtils.checkStationOutOrder();
         // 鐩戞帶缁曞湀绔欑偣
diff --git a/src/main/java/com/zy/core/plugin/NormalProcess.java b/src/main/java/com/zy/core/plugin/NormalProcess.java
index 3216f4d..826f1e5 100644
--- a/src/main/java/com/zy/core/plugin/NormalProcess.java
+++ b/src/main/java/com/zy/core/plugin/NormalProcess.java
@@ -75,8 +75,6 @@
         stationOperateProcessUtils.stationInExecute();
         //鎵ц杈撻�佺珯鐐瑰嚭搴撲换鍔�
         stationOperateProcessUtils.crnStationOutExecute();
-        //妫�娴嬭緭閫佺珯鐐瑰嚭搴撲换鍔℃墽琛屽畬鎴�
-        stationOperateProcessUtils.stationOutExecuteFinish();
 
         //妫�娴嬭緭閫佺珯鐐规槸鍚﹁繍琛屽牭濉�
         stationOperateProcessUtils.checkStationRunBlock();
diff --git a/src/main/java/com/zy/core/plugin/XiaosongProcess.java b/src/main/java/com/zy/core/plugin/XiaosongProcess.java
index bfed9a4..5fcf9d6 100644
--- a/src/main/java/com/zy/core/plugin/XiaosongProcess.java
+++ b/src/main/java/com/zy/core/plugin/XiaosongProcess.java
@@ -80,10 +80,6 @@
         stationOperateProcessUtils.crnStationOutExecute();
         //鎵ц鍙屽伐浣嶅爢鍨涙満杈撻�佺珯鐐瑰嚭搴撲换鍔�
         stationOperateProcessUtils.dualCrnStationOutExecute();
-        //妫�娴嬭緭閫佺珯鐐瑰嚭搴撲换鍔℃墽琛屽畬鎴�
-        stationOperateProcessUtils.stationOutExecuteFinish();
-        // 妫�娴嬩换鍔¤浆瀹屾垚
-        stationOperateProcessUtils.checkTaskToComplete();
         //妫�娴嬭緭閫佺珯鐐规槸鍚﹁繍琛屽牭濉�
         stationOperateProcessUtils.checkStationRunBlock();
         //妫�娴嬭緭閫佺珯鐐逛换鍔″仠鐣欒秴鏃跺悗閲嶆柊璁$畻璺緞
diff --git a/src/main/java/com/zy/core/service/WrkCommandRollbackService.java b/src/main/java/com/zy/core/service/WrkCommandRollbackService.java
index bd6ac11..580fded 100644
--- a/src/main/java/com/zy/core/service/WrkCommandRollbackService.java
+++ b/src/main/java/com/zy/core/service/WrkCommandRollbackService.java
@@ -173,7 +173,7 @@
 
     private Long getRollbackStatus(Long wrkSts) {
         if (Long.valueOf(WrkStsType.INBOUND_RUN.sts).equals(wrkSts)) {
-            return WrkStsType.INBOUND_DEVICE_RUN.sts;
+            return WrkStsType.INBOUND_STATION_RUN_COMPLETE.sts;
         }
         if (Long.valueOf(WrkStsType.OUTBOUND_RUN.sts).equals(wrkSts)) {
             return WrkStsType.NEW_OUTBOUND.sts;
@@ -199,7 +199,7 @@
 
     private Long getRollbackStatusFromManual(Long wrkSts) {
         if (Long.valueOf(WrkStsType.INBOUND_MANUAL.sts).equals(wrkSts)) {
-            return WrkStsType.INBOUND_DEVICE_RUN.sts;
+            return WrkStsType.INBOUND_STATION_RUN_COMPLETE.sts;
         }
         if (Long.valueOf(WrkStsType.OUTBOUND_MANUAL.sts).equals(wrkSts)) {
             return WrkStsType.NEW_OUTBOUND.sts;
diff --git a/src/main/java/com/zy/core/trace/StationTaskTraceRegistry.java b/src/main/java/com/zy/core/trace/StationTaskTraceRegistry.java
index 1751be6..e9eb870 100644
--- a/src/main/java/com/zy/core/trace/StationTaskTraceRegistry.java
+++ b/src/main/java/com/zy/core/trace/StationTaskTraceRegistry.java
@@ -259,7 +259,7 @@
     }
 
     private boolean isStationTraceActiveWrkStatus(Long wrkSts) {
-        return Objects.equals(wrkSts, WrkStsType.INBOUND_DEVICE_RUN.sts)
+        return Objects.equals(wrkSts, WrkStsType.INBOUND_STATION_RUN.sts)
                 || Objects.equals(wrkSts, WrkStsType.STATION_RUN.sts);
     }
 
diff --git a/src/main/java/com/zy/core/utils/CrnOperateProcessUtils.java b/src/main/java/com/zy/core/utils/CrnOperateProcessUtils.java
index ee1b8f6..eb997b1 100644
--- a/src/main/java/com/zy/core/utils/CrnOperateProcessUtils.java
+++ b/src/main/java/com/zy/core/utils/CrnOperateProcessUtils.java
@@ -15,6 +15,7 @@
 import com.zy.asrs.service.BasCrnpService;
 import com.zy.asrs.service.BasStationService;
 import com.zy.asrs.service.LocMastService;
+import com.zy.asrs.service.WrkAnalysisService;
 import com.zy.asrs.service.WrkMastService;
 import com.zy.asrs.utils.NotifyUtils;
 import com.zy.asrs.utils.Utils;
@@ -63,6 +64,8 @@
     private NotifyUtils notifyUtils;
     @Autowired
     private StationOperateProcessUtils stationOperateProcessUtils;
+    @Autowired
+    private WrkAnalysisService wrkAnalysisService;
 
     private static final String CRN_OUT_REQUIRE_STATION_OUT_ENABLE_CONFIG = "crnOutRequireStationOutEnable";
 
@@ -127,7 +130,7 @@
         List<WrkMast> taskQueue = wrkMastService.list(new QueryWrapper<WrkMast>()
                 .in("crn_no", new ArrayList<>(dispatchCrnMap.keySet()))
                 .in("wrk_sts",
-                        WrkStsType.INBOUND_DEVICE_RUN.sts,
+                        WrkStsType.INBOUND_STATION_RUN_COMPLETE.sts,
                         WrkStsType.NEW_OUTBOUND.sts,
                         WrkStsType.NEW_LOC_MOVE.sts));
         taskQueue.sort(Comparator
@@ -150,7 +153,7 @@
                 continue;
             }
 
-            if (wrkMast.getWrkSts() != null && wrkMast.getWrkSts() == WrkStsType.INBOUND_DEVICE_RUN.sts) {
+            if (wrkMast.getWrkSts() != null && wrkMast.getWrkSts() == WrkStsType.INBOUND_STATION_RUN_COMPLETE.sts) {
                 boolean result = this.crnExecuteInPlanner(basCrnp, crnThread, wrkMast);
                 if (result) {
                     crnProtocol.setLastIo("O");
@@ -275,7 +278,7 @@
                 continue;
             }
 
-            if(wrkMast.getWrkSts() != WrkStsType.INBOUND_DEVICE_RUN.sts){
+            if(wrkMast.getWrkSts() != WrkStsType.INBOUND_STATION_RUN_COMPLETE.sts){
                 continue;
             }
 
@@ -302,11 +305,14 @@
 
             CrnCommand command = crnThread.getPickAndPutCommand(sourceLocNo, wrkMast.getLocNo(), wrkMast.getWrkNo(), crnNo);
 
+            Date now = new Date();
             wrkMast.setWrkSts(WrkStsType.INBOUND_RUN.sts);
             wrkMast.setCrnNo(crnNo);
             wrkMast.setSystemMsg("");
-            wrkMast.setIoTime(new Date());
+            wrkMast.setIoTime(now);
+            wrkMast.setModiTime(now);
             if (wrkMastService.updateById(wrkMast)) {
+                wrkAnalysisService.markCraneStart(wrkMast, now);
                 MessageQueue.offer(SlaveType.Crn, crnNo, new Task(2, command));
                 notifyUtils.notify(String.valueOf(SlaveType.Crn), crnNo, String.valueOf(wrkMast.getWrkNo()), wrkMast.getWmsWrkNo(), NotifyMsgType.CRN_IN_TASK_RUN, null);
                 News.info("鍫嗗灈鏈哄懡浠や笅鍙戞垚鍔燂紝鍫嗗灈鏈哄彿={}锛屼换鍔℃暟鎹�={}", crnNo, JSON.toJSON(command));
@@ -404,11 +410,14 @@
 
                 CrnCommand command = crnThread.getPickAndPutCommand(wrkMast.getSourceLocNo(), targetLocNo, wrkMast.getWrkNo(), crnNo);
 
+                Date now = new Date();
                 wrkMast.setWrkSts(WrkStsType.OUTBOUND_RUN.sts);
                 wrkMast.setCrnNo(crnNo);
                 wrkMast.setSystemMsg("");
-                wrkMast.setIoTime(new Date());
+                wrkMast.setIoTime(now);
+                wrkMast.setModiTime(now);
                 if (wrkMastService.updateById(wrkMast)) {
+                    wrkAnalysisService.markCraneStart(wrkMast, now);
                     MessageQueue.offer(SlaveType.Crn, crnNo, new Task(2, command));
                     notifyUtils.notify(String.valueOf(SlaveType.Crn), crnNo, String.valueOf(wrkMast.getWrkNo()), wrkMast.getWmsWrkNo(), NotifyMsgType.CRN_OUT_TASK_RUN, null);
                     News.info("鍫嗗灈鏈哄懡浠や笅鍙戞垚鍔燂紝鍫嗗灈鏈哄彿={}锛屼换鍔℃暟鎹�={}", crnNo, JSON.toJSON(command));
@@ -471,7 +480,7 @@
                 continue;
             }
 
-            if (wrkMast.getWrkSts() != WrkStsType.INBOUND_DEVICE_RUN.sts) {
+            if (wrkMast.getWrkSts() != WrkStsType.INBOUND_STATION_RUN_COMPLETE.sts) {
                 continue;
             }
 
@@ -498,11 +507,14 @@
 
             CrnCommand command = crnThread.getPickAndPutCommand(sourceLocNo, wrkMast.getLocNo(), wrkMast.getWrkNo(), crnNo);
 
+            Date now = new Date();
             wrkMast.setWrkSts(WrkStsType.INBOUND_RUN.sts);
             wrkMast.setCrnNo(crnNo);
             wrkMast.setSystemMsg("");
-            wrkMast.setIoTime(new Date());
+            wrkMast.setIoTime(now);
+            wrkMast.setModiTime(now);
             if (wrkMastService.updateById(wrkMast)) {
+                wrkAnalysisService.markCraneStart(wrkMast, now);
                 MessageQueue.offer(SlaveType.Crn, crnNo, new Task(2, command));
                 notifyUtils.notify(String.valueOf(SlaveType.Crn), crnNo, String.valueOf(wrkMast.getWrkNo()), wrkMast.getWmsWrkNo(), NotifyMsgType.CRN_IN_TASK_RUN, null);
                 News.info("鍫嗗灈鏈哄懡浠や笅鍙戞垚鍔燂紝鍫嗗灈鏈哄彿={}锛屼换鍔℃暟鎹�={}", crnNo, JSON.toJSON(command));
@@ -595,11 +607,14 @@
 
             CrnCommand command = crnThread.getPickAndPutCommand(wrkMast.getSourceLocNo(), targetLocNo, wrkMast.getWrkNo(), crnNo);
 
+            Date now = new Date();
             wrkMast.setWrkSts(WrkStsType.OUTBOUND_RUN.sts);
             wrkMast.setCrnNo(crnNo);
             wrkMast.setSystemMsg("");
-            wrkMast.setIoTime(new Date());
+            wrkMast.setIoTime(now);
+            wrkMast.setModiTime(now);
             if (wrkMastService.updateById(wrkMast)) {
+                wrkAnalysisService.markCraneStart(wrkMast, now);
                 MessageQueue.offer(SlaveType.Crn, crnNo, new Task(2, command));
                 notifyUtils.notify(String.valueOf(SlaveType.Crn), crnNo, String.valueOf(wrkMast.getWrkNo()), wrkMast.getWmsWrkNo(), NotifyMsgType.CRN_OUT_TASK_RUN, null);
                 News.info("鍫嗗灈鏈哄懡浠や笅鍙戞垚鍔燂紝鍫嗗灈鏈哄彿={}锛屼换鍔℃暟鎹�={}", crnNo, JSON.toJSON(command));
@@ -833,11 +848,14 @@
 
             CrnCommand command = crnThread.getPickAndPutCommand(wrkMast.getSourceLocNo(), wrkMast.getLocNo(), wrkMast.getWrkNo(), crnNo);
 
+            Date now = new Date();
             wrkMast.setWrkSts(WrkStsType.LOC_MOVE_RUN.sts);
             wrkMast.setCrnNo(crnNo);
             wrkMast.setSystemMsg("");
-            wrkMast.setIoTime(new Date());
+            wrkMast.setIoTime(now);
+            wrkMast.setModiTime(now);
             if (wrkMastService.updateById(wrkMast)) {
+                wrkAnalysisService.markCraneStart(wrkMast, now);
                 MessageQueue.offer(SlaveType.Crn, crnNo, new Task(2, command));
                 notifyUtils.notify(String.valueOf(SlaveType.Crn), crnNo, String.valueOf(wrkMast.getWrkNo()), wrkMast.getWmsWrkNo(), NotifyMsgType.CRN_TRANSFER_TASK_RUN, null);
                 News.info("鍫嗗灈鏈哄懡浠や笅鍙戞垚鍔燂紝鍫嗗灈鏈哄彿={}锛屼换鍔℃暟鎹�={}", crnNo, JSON.toJSON(command));
@@ -878,6 +896,7 @@
                 }
 
                 Long updateWrkSts = null;
+                Date now = new Date();
                 if(wrkMast.getWrkSts() == WrkStsType.INBOUND_RUN.sts){
                     updateWrkSts = WrkStsType.COMPLETE_INBOUND.sts;
                     notifyUtils.notify(String.valueOf(SlaveType.Crn), crnProtocol.getCrnNo(), String.valueOf(wrkMast.getWrkNo()), wrkMast.getWmsWrkNo(), NotifyMsgType.CRN_IN_TASK_COMPLETE, null);
@@ -909,8 +928,10 @@
 
                 wrkMast.setWrkSts(updateWrkSts);
                 wrkMast.setSystemMsg("");
-                wrkMast.setIoTime(new Date());
+                wrkMast.setIoTime(now);
+                wrkMast.setModiTime(now);
                 if (wrkMastService.updateById(wrkMast)) {
+                    wrkAnalysisService.markCraneComplete(wrkMast, now, updateWrkSts);
                     CrnCommand resetCommand = crnThread.getResetCommand(crnProtocol.getTaskNo(), crnProtocol.getCrnNo());
                     MessageQueue.offer(SlaveType.Crn, crnProtocol.getCrnNo(), new Task(2, resetCommand));
                     News.info("鍫嗗灈鏈轰换鍔$姸鎬佹洿鏂版垚鍔燂紝鍫嗗灈鏈哄彿={}锛屽伐浣滃彿={}", basCrnp.getCrnNo(), crnProtocol.getTaskNo());
@@ -1056,11 +1077,14 @@
 
         CrnCommand command = crnThread.getPickAndPutCommand(wrkMast.getSourceLocNo(), wrkMast.getLocNo(), wrkMast.getWrkNo(), crnNo);
 
+        Date now = new Date();
         wrkMast.setWrkSts(WrkStsType.LOC_MOVE_RUN.sts);
         wrkMast.setCrnNo(crnNo);
         wrkMast.setSystemMsg("");
-        wrkMast.setIoTime(new Date());
+        wrkMast.setIoTime(now);
+        wrkMast.setModiTime(now);
         if (wrkMastService.updateById(wrkMast)) {
+            wrkAnalysisService.markCraneStart(wrkMast, now);
             MessageQueue.offer(SlaveType.Crn, crnNo, new Task(2, command));
             notifyUtils.notify(String.valueOf(SlaveType.Crn), crnNo, String.valueOf(wrkMast.getWrkNo()), wrkMast.getWmsWrkNo(), NotifyMsgType.CRN_TRANSFER_TASK_RUN, null);
             News.info("鍫嗗灈鏈哄懡浠や笅鍙戞垚鍔燂紝鍫嗗灈鏈哄彿={}锛屼换鍔℃暟鎹�={}", crnNo, JSON.toJSON(command));
diff --git a/src/main/java/com/zy/core/utils/DualCrnOperateProcessUtils.java b/src/main/java/com/zy/core/utils/DualCrnOperateProcessUtils.java
index e4ee5c4..951c437 100644
--- a/src/main/java/com/zy/core/utils/DualCrnOperateProcessUtils.java
+++ b/src/main/java/com/zy/core/utils/DualCrnOperateProcessUtils.java
@@ -14,6 +14,7 @@
 import com.zy.asrs.service.BasDualCrnpService;
 import com.zy.asrs.service.BasStationService;
 import com.zy.asrs.service.LocMastService;
+import com.zy.asrs.service.WrkAnalysisService;
 import com.zy.asrs.service.WrkMastService;
 import com.zy.asrs.utils.NotifyUtils;
 import com.zy.asrs.utils.Utils;
@@ -66,6 +67,8 @@
     private NotifyUtils notifyUtils;
     @Autowired
     private StationOperateProcessUtils stationOperateProcessUtils;
+    @Autowired
+    private WrkAnalysisService wrkAnalysisService;
 
     private static final String CRN_OUT_REQUIRE_STATION_OUT_ENABLE_CONFIG = "crnOutRequireStationOutEnable";
 
@@ -354,7 +357,7 @@
                 .in("wrk_no", taskList)
         );
         for (WrkMast wrkMast : wrkMasts) {
-            if(wrkMast.getWrkSts() != WrkStsType.INBOUND_DEVICE_RUN.sts){
+            if(wrkMast.getWrkSts() != WrkStsType.INBOUND_STATION_RUN_COMPLETE.sts){
                 continue;
             }
             list.add(wrkMast);
@@ -412,7 +415,7 @@
 
         Integer crnNo = basDualCrnp.getCrnNo();
 
-        if (wrkMast.getWrkSts() != WrkStsType.INBOUND_DEVICE_RUN.sts) {
+        if (wrkMast.getWrkSts() != WrkStsType.INBOUND_STATION_RUN_COMPLETE.sts) {
             return null;
         }
 
@@ -509,11 +512,14 @@
         commandList.add(pickCommand);
         commandList.add(putCommand);
 
+        Date now = new Date();
         wrkMast.setWrkSts(WrkStsType.INBOUND_RUN.sts);
         wrkMast.setDualCrnNo(crnNo);
         wrkMast.setSystemMsg("");
-        wrkMast.setIoTime(new Date());
+        wrkMast.setIoTime(now);
+        wrkMast.setModiTime(now);
         if (wrkMastService.updateById(wrkMast)) {
+            wrkAnalysisService.markCraneStart(wrkMast, now);
             SendDualCrnCommandParam sendDualCrnCommandParam = new SendDualCrnCommandParam();
             sendDualCrnCommandParam.setCrnNo(crnNo);
             sendDualCrnCommandParam.setStation(station);
@@ -621,11 +627,14 @@
             commandList.add(pickCommand);
             commandList.add(putCommand);
 
+            Date now = new Date();
             wrkMast.setWrkSts(WrkStsType.OUTBOUND_RUN.sts);
             wrkMast.setDualCrnNo(crnNo);
             wrkMast.setSystemMsg("");
-            wrkMast.setIoTime(new Date());
+            wrkMast.setIoTime(now);
+            wrkMast.setModiTime(now);
             if (wrkMastService.updateById(wrkMast)) {
+                wrkAnalysisService.markCraneStart(wrkMast, now);
                 redisUtil.set(RedisKeyType.DUAL_CRN_OUT_TASK_STATION_INFO.key + wrkMast.getWrkNo(), JSON.toJSONString(stationObjModel, SerializerFeature.DisableCircularReferenceDetect), 60 * 60 * 24);
 
                 SendDualCrnCommandParam sendDualCrnCommandParam = new SendDualCrnCommandParam();
@@ -706,11 +715,14 @@
         commandList.add(pickCommand);
         commandList.add(putCommand);
 
+        Date now = new Date();
         wrkMast.setWrkSts(WrkStsType.LOC_MOVE_RUN.sts);
         wrkMast.setDualCrnNo(crnNo);
         wrkMast.setSystemMsg("");
-        wrkMast.setIoTime(new Date());
+        wrkMast.setIoTime(now);
+        wrkMast.setModiTime(now);
         if (wrkMastService.updateById(wrkMast)) {
+            wrkAnalysisService.markCraneStart(wrkMast, now);
             SendDualCrnCommandParam sendDualCrnCommandParam = new SendDualCrnCommandParam();
             sendDualCrnCommandParam.setCrnNo(crnNo);
             sendDualCrnCommandParam.setStation(station);
@@ -778,6 +790,7 @@
 
         if (idx >= 2) {
             Long updateWrkSts = null;
+            Date now = new Date();
             if (wrkMast.getWrkSts() == WrkStsType.INBOUND_RUN.sts) {
                 updateWrkSts = WrkStsType.COMPLETE_INBOUND.sts;
                 notifyUtils.notify(String.valueOf(SlaveType.DualCrn), basDualCrnp.getCrnNo(), String.valueOf(wrkMast.getWrkNo()), wrkMast.getWmsWrkNo(), NotifyMsgType.DUAL_CRN_IN_TASK_COMPLETE, null);
@@ -827,8 +840,10 @@
 
                 wrkMast.setWrkSts(updateWrkSts);
                 wrkMast.setSystemMsg("");
-                wrkMast.setIoTime(new Date());
+                wrkMast.setIoTime(now);
+                wrkMast.setModiTime(now);
                 if (wrkMastService.updateById(wrkMast)) {
+                    wrkAnalysisService.markCraneComplete(wrkMast, now, updateWrkSts);
                     News.info("鍙屽伐浣嶅爢鍨涙満浠诲姟鐘舵�佹洿鏂版垚鍔燂紝鍫嗗灈鏈哄彿={}锛屽伐浣滃彿={}", basDualCrnp.getCrnNo(), taskNo);
                 }
                 redisUtil.set(RedisKeyType.DUAL_CRN_IO_EXECUTE_FINISH_LIMIT.key + basDualCrnp.getCrnNo() + "_" + taskNo, "lock", 10);
diff --git a/src/main/java/com/zy/core/utils/StationOperateProcessUtils.java b/src/main/java/com/zy/core/utils/StationOperateProcessUtils.java
index 550ad32..f988dd1 100644
--- a/src/main/java/com/zy/core/utils/StationOperateProcessUtils.java
+++ b/src/main/java/com/zy/core/utils/StationOperateProcessUtils.java
@@ -70,6 +70,8 @@
     private BasStationOptService basStationOptService;
     @Autowired
     private StationTaskLoopService stationTaskLoopService;
+    @Autowired
+    private WrkAnalysisService wrkAnalysisService;
 
     //鎵ц杈撻�佺珯鐐瑰叆搴撲换鍔�
     public synchronized void stationInExecute() {
@@ -115,7 +117,7 @@
                             continue;
                         }
 
-                        if (wrkMast.getWrkSts() == WrkStsType.INBOUND_DEVICE_RUN.sts) {
+                        if (!Objects.equals(wrkMast.getWrkSts(), WrkStsType.NEW_INBOUND.sts)) {
                             continue;
                         }
 
@@ -145,12 +147,15 @@
                             continue;
                         }
 
-                        wrkMast.setWrkSts(WrkStsType.INBOUND_DEVICE_RUN.sts);
+                        Date now = new Date();
+                        wrkMast.setWrkSts(WrkStsType.INBOUND_STATION_RUN.sts);
                         wrkMast.setSourceStaNo(stationProtocol.getStationId());
                         wrkMast.setStaNo(targetStationId);
                         wrkMast.setSystemMsg("");
-                        wrkMast.setIoTime(new Date());
+                        wrkMast.setIoTime(now);
+                        wrkMast.setModiTime(now);
                         if (wrkMastService.updateById(wrkMast)) {
+                            wrkAnalysisService.markInboundStationStart(wrkMast, now);
                             MessageQueue.offer(SlaveType.Devp, basDevp.getDevpNo(), new Task(2, command));
                             News.info("杈撻�佺珯鐐瑰叆搴撳懡浠や笅鍙戞垚鍔燂紝绔欑偣鍙�={}锛屽伐浣滃彿={}锛屽懡浠ゆ暟鎹�={}", stationId, wrkMast.getWrkNo(), JSON.toJSONString(command));
                             redisUtil.set(RedisKeyType.STATION_IN_EXECUTE_LIMIT.key + stationId, "lock", 5);
@@ -238,10 +243,13 @@
                         continue;
                     }
 
+                    Date now = new Date();
                     wrkMast.setWrkSts(WrkStsType.STATION_RUN.sts);
                     wrkMast.setSystemMsg("");
-                    wrkMast.setIoTime(new Date());
+                    wrkMast.setIoTime(now);
+                    wrkMast.setModiTime(now);
                     if (wrkMastService.updateById(wrkMast)) {
+                        wrkAnalysisService.markOutboundStationStart(wrkMast, now);
                         MessageQueue.offer(SlaveType.Devp, stationObjModel.getDeviceNo(), new Task(2, command));
                         News.info("杈撻�佺珯鐐瑰嚭搴撳懡浠や笅鍙戞垚鍔燂紝绔欑偣鍙�={}锛屽伐浣滃彿={}锛屽懡浠ゆ暟鎹�={}", stationProtocol.getStationId(), wrkMast.getWrkNo(), JSON.toJSONString(command));
                         redisUtil.set(RedisKeyType.STATION_OUT_EXECUTE_LIMIT.key + stationProtocol.getStationId(), "lock", 5);
@@ -362,9 +370,12 @@
         if (wrkMast == null || wrkMast.getWrkNo() == null) {
             return;
         }
+        Date now = new Date();
         wrkMast.setWrkSts(WrkStsType.STATION_RUN_COMPLETE.sts);
-        wrkMast.setIoTime(new Date());
+        wrkMast.setIoTime(now);
+        wrkMast.setModiTime(now);
         wrkMastService.updateById(wrkMast);
+        wrkAnalysisService.markOutboundStationComplete(wrkMast, now);
         if (deviceNo != null) {
             notifyUtils.notify(String.valueOf(SlaveType.Devp), deviceNo, String.valueOf(wrkMast.getWrkNo()), wrkMast.getWmsWrkNo(), NotifyMsgType.STATION_OUT_TASK_RUN_COMPLETE, null);
         }
@@ -1345,7 +1356,7 @@
         if (Objects.equals(currentStationId, wrkMast.getStaNo())) {
             return false;
         }
-        return Objects.equals(wrkMast.getWrkSts(), WrkStsType.INBOUND_DEVICE_RUN.sts)
+        return Objects.equals(wrkMast.getWrkSts(), WrkStsType.INBOUND_STATION_RUN.sts)
                 || Objects.equals(wrkMast.getWrkSts(), WrkStsType.STATION_RUN.sts);
     }
 
diff --git a/src/main/resources/mapper/WrkAnalysisMapper.xml b/src/main/resources/mapper/WrkAnalysisMapper.xml
new file mode 100644
index 0000000..92bf472
--- /dev/null
+++ b/src/main/resources/mapper/WrkAnalysisMapper.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.zy.asrs.mapper.WrkAnalysisMapper">
+
+    <resultMap id="BaseResultMap" type="com.zy.asrs.entity.WrkAnalysis">
+        <id column="wrk_no" property="wrkNo" />
+        <result column="wms_wrk_no" property="wmsWrkNo" />
+        <result column="io_type" property="ioType" />
+        <result column="final_wrk_sts" property="finalWrkSts" />
+        <result column="source_sta_no" property="sourceStaNo" />
+        <result column="sta_no" property="staNo" />
+        <result column="source_loc_no" property="sourceLocNo" />
+        <result column="loc_no" property="locNo" />
+        <result column="crn_no" property="crnNo" />
+        <result column="dual_crn_no" property="dualCrnNo" />
+        <result column="rgv_no" property="rgvNo" />
+        <result column="appe_time" property="appeTime" />
+        <result column="finish_time" property="finishTime" />
+        <result column="total_duration_ms" property="totalDurationMs" />
+        <result column="station_start_time" property="stationStartTime" />
+        <result column="station_end_time" property="stationEndTime" />
+        <result column="station_duration_ms" property="stationDurationMs" />
+        <result column="crane_start_time" property="craneStartTime" />
+        <result column="crane_end_time" property="craneEndTime" />
+        <result column="crane_duration_ms" property="craneDurationMs" />
+        <result column="has_fault" property="hasFault" />
+        <result column="fault_count" property="faultCount" />
+        <result column="fault_duration_ms" property="faultDurationMs" />
+        <result column="crn_fault_count" property="crnFaultCount" />
+        <result column="crn_fault_duration_ms" property="crnFaultDurationMs" />
+        <result column="dual_crn_fault_count" property="dualCrnFaultCount" />
+        <result column="dual_crn_fault_duration_ms" property="dualCrnFaultDurationMs" />
+        <result column="rgv_fault_count" property="rgvFaultCount" />
+        <result column="rgv_fault_duration_ms" property="rgvFaultDurationMs" />
+        <result column="metric_completeness" property="metricCompleteness" />
+        <result column="create_time" property="createTime" />
+        <result column="update_time" property="updateTime" />
+    </resultMap>
+
+</mapper>
diff --git a/src/main/resources/sql/20260322_add_wrk_analysis_page_and_inbound_status_migration.sql b/src/main/resources/sql/20260322_add_wrk_analysis_page_and_inbound_status_migration.sql
new file mode 100644
index 0000000..8d8350f
--- /dev/null
+++ b/src/main/resources/sql/20260322_add_wrk_analysis_page_and_inbound_status_migration.sql
@@ -0,0 +1,332 @@
+CREATE TABLE IF NOT EXISTS `asr_wrk_analysis` (
+  `wrk_no` int NOT NULL COMMENT '宸ヤ綔鍙�',
+  `wms_wrk_no` varchar(64) DEFAULT NULL COMMENT 'WMS浠诲姟鍙�',
+  `io_type` int DEFAULT NULL COMMENT '鍏ュ嚭搴撶被鍨�',
+  `final_wrk_sts` bigint DEFAULT NULL COMMENT '鏈�缁堝伐浣滅姸鎬�',
+  `source_sta_no` int DEFAULT NULL COMMENT '婧愮珯',
+  `sta_no` int DEFAULT NULL COMMENT '鐩爣绔�',
+  `source_loc_no` varchar(64) DEFAULT NULL COMMENT '婧愬簱浣�',
+  `loc_no` varchar(64) DEFAULT NULL COMMENT '鐩爣搴撲綅',
+  `crn_no` int DEFAULT NULL COMMENT '鍫嗗灈鏈哄彿',
+  `dual_crn_no` int DEFAULT NULL COMMENT '鍙屽伐浣嶅爢鍨涙満鍙�',
+  `rgv_no` int DEFAULT NULL COMMENT 'RGV鍙�',
+  `appe_time` datetime DEFAULT NULL COMMENT '鍒涘缓鏃堕棿',
+  `finish_time` datetime DEFAULT NULL COMMENT '瀹屾垚鏃堕棿',
+  `total_duration_ms` bigint DEFAULT NULL COMMENT '鎬昏�楁椂姣',
+  `station_start_time` datetime DEFAULT NULL COMMENT '绔欑偣寮�濮嬫椂闂�',
+  `station_end_time` datetime DEFAULT NULL COMMENT '绔欑偣缁撴潫鏃堕棿',
+  `station_duration_ms` bigint DEFAULT NULL COMMENT '绔欑偣鑰楁椂姣',
+  `crane_start_time` datetime DEFAULT NULL COMMENT '鍫嗗灈鏈哄紑濮嬫椂闂�',
+  `crane_end_time` datetime DEFAULT NULL COMMENT '鍫嗗灈鏈虹粨鏉熸椂闂�',
+  `crane_duration_ms` bigint DEFAULT NULL COMMENT '鍫嗗灈鏈鸿�楁椂姣',
+  `has_fault` int DEFAULT '0' COMMENT '鏄惁鏁呴殰',
+  `fault_count` int DEFAULT '0' COMMENT '鏁呴殰娆℃暟',
+  `fault_duration_ms` bigint DEFAULT '0' COMMENT '鏁呴殰鑰楁椂姣',
+  `crn_fault_count` int DEFAULT '0' COMMENT '鍗曞爢鍨涙満鏁呴殰娆℃暟',
+  `crn_fault_duration_ms` bigint DEFAULT '0' COMMENT '鍗曞爢鍨涙満鏁呴殰鑰楁椂姣',
+  `dual_crn_fault_count` int DEFAULT '0' COMMENT '鍙屽伐浣嶅爢鍨涙満鏁呴殰娆℃暟',
+  `dual_crn_fault_duration_ms` bigint DEFAULT '0' COMMENT '鍙屽伐浣嶅爢鍨涙満鏁呴殰鑰楁椂姣',
+  `rgv_fault_count` int DEFAULT '0' COMMENT 'RGV鏁呴殰娆℃暟',
+  `rgv_fault_duration_ms` bigint DEFAULT '0' COMMENT 'RGV鏁呴殰鑰楁椂姣',
+  `metric_completeness` varchar(16) DEFAULT 'PARTIAL' COMMENT '鏁版嵁瀹屾暣鎬�',
+  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '鏇存柊鏃堕棿',
+  PRIMARY KEY (`wrk_no`),
+  KEY `idx_wrk_analysis_finish_time` (`finish_time`),
+  KEY `idx_wrk_analysis_appe_time` (`appe_time`),
+  KEY `idx_wrk_analysis_io_type` (`io_type`),
+  KEY `idx_wrk_analysis_final_wrk_sts` (`final_wrk_sts`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='浠诲姟鎵ц鍒嗘瀽姹囨�昏〃';
+
+SET @wrk_analysis_inbound_status_migrated := CASE
+    WHEN EXISTS (
+        SELECT 1
+        FROM `asr_bas_wrk_status`
+        WHERE `wrk_sts` = 3
+          AND `wrk_desc` = '绔欑偣杩愯瀹屾垚'
+    ) THEN 1
+    ELSE 0
+END;
+
+UPDATE `asr_wrk_mast`
+SET `wrk_sts` = CASE
+    WHEN `wrk_sts` = 3 THEN 4
+    WHEN `wrk_sts` = 4 THEN 5
+    ELSE `wrk_sts`
+END
+WHERE @wrk_analysis_inbound_status_migrated = 0
+  AND `wrk_sts` IN (3, 4);
+
+UPDATE `asr_wrk_mast_log`
+SET `wrk_sts` = CASE
+    WHEN `wrk_sts` = 3 THEN 4
+    WHEN `wrk_sts` = 4 THEN 5
+    ELSE `wrk_sts`
+END
+WHERE @wrk_analysis_inbound_status_migrated = 0
+  AND `wrk_sts` IN (3, 4);
+
+UPDATE `asr_bas_crnp_err_log`
+SET `wrk_sts` = CASE
+    WHEN `wrk_sts` = 3 THEN 4
+    WHEN `wrk_sts` = 4 THEN 5
+    ELSE `wrk_sts`
+END
+WHERE @wrk_analysis_inbound_status_migrated = 0
+  AND `wrk_sts` IN (3, 4);
+
+UPDATE `asr_bas_dual_crnp_err_log`
+SET `wrk_sts` = CASE
+    WHEN `wrk_sts` = 3 THEN 4
+    WHEN `wrk_sts` = 4 THEN 5
+    ELSE `wrk_sts`
+END
+WHERE @wrk_analysis_inbound_status_migrated = 0
+  AND `wrk_sts` IN (3, 4);
+
+UPDATE `asr_bas_rgv_err_log`
+SET `wrk_sts` = CASE
+    WHEN `wrk_sts` = 3 THEN 4
+    WHEN `wrk_sts` = 4 THEN 5
+    ELSE `wrk_sts`
+END
+WHERE @wrk_analysis_inbound_status_migrated = 0
+  AND `wrk_sts` IN (3, 4);
+
+INSERT INTO `asr_bas_wrk_status` (`wrk_sts`, `wrk_desc`, `appe_time`, `modi_time`)
+VALUES (3, '绔欑偣杩愯瀹屾垚', NOW(), NOW())
+ON DUPLICATE KEY UPDATE
+`wrk_desc` = VALUES(`wrk_desc`),
+`modi_time` = NOW();
+
+INSERT INTO `asr_bas_wrk_status` (`wrk_sts`, `wrk_desc`, `appe_time`, `modi_time`)
+VALUES (4, '璁惧鎼繍涓�', NOW(), NOW())
+ON DUPLICATE KEY UPDATE
+`wrk_desc` = VALUES(`wrk_desc`),
+`modi_time` = NOW();
+
+INSERT INTO `asr_bas_wrk_status` (`wrk_sts`, `wrk_desc`, `appe_time`, `modi_time`)
+VALUES (5, '璁惧鎼繍瀹屾垚', NOW(), NOW())
+ON DUPLICATE KEY UPDATE
+`wrk_desc` = VALUES(`wrk_desc`),
+`modi_time` = NOW();
+
+UPDATE `asr_bas_wrk_status`
+SET `wrk_desc` = '绔欑偣杩愯涓�',
+    `modi_time` = NOW()
+WHERE `wrk_sts` = 2;
+
+INSERT INTO `asr_wrk_analysis` (
+    `wrk_no`,
+    `wms_wrk_no`,
+    `io_type`,
+    `final_wrk_sts`,
+    `source_sta_no`,
+    `sta_no`,
+    `source_loc_no`,
+    `loc_no`,
+    `crn_no`,
+    `dual_crn_no`,
+    `rgv_no`,
+    `appe_time`,
+    `finish_time`,
+    `total_duration_ms`,
+    `station_duration_ms`,
+    `crane_duration_ms`,
+    `has_fault`,
+    `fault_count`,
+    `fault_duration_ms`,
+    `crn_fault_count`,
+    `crn_fault_duration_ms`,
+    `dual_crn_fault_count`,
+    `dual_crn_fault_duration_ms`,
+    `rgv_fault_count`,
+    `rgv_fault_duration_ms`,
+    `metric_completeness`,
+    `create_time`,
+    `update_time`
+)
+SELECT
+    l.`wrk_no`,
+    l.`wms_wrk_no`,
+    l.`io_type`,
+    l.`wrk_sts`,
+    l.`source_sta_no`,
+    l.`sta_no`,
+    l.`source_loc_no`,
+    l.`loc_no`,
+    l.`crn_no`,
+    l.`dual_crn_no`,
+    l.`rgv_no`,
+    l.`appe_time`,
+    COALESCE(l.`modi_time`, l.`io_time`, l.`appe_time`) AS `finish_time`,
+    CASE
+        WHEN l.`appe_time` IS NULL OR COALESCE(l.`modi_time`, l.`io_time`, l.`appe_time`) IS NULL THEN NULL
+        ELSE GREATEST(TIMESTAMPDIFF(MICROSECOND, l.`appe_time`, COALESCE(l.`modi_time`, l.`io_time`, l.`appe_time`)), 0) DIV 1000
+    END AS `total_duration_ms`,
+    CASE WHEN l.`io_type` = 201 THEN 0 ELSE NULL END AS `station_duration_ms`,
+    NULL AS `crane_duration_ms`,
+    CASE WHEN COALESCE(c.`fault_count`, 0) + COALESCE(d.`fault_count`, 0) + COALESCE(r.`fault_count`, 0) > 0 THEN 1 ELSE 0 END AS `has_fault`,
+    COALESCE(c.`fault_count`, 0) + COALESCE(d.`fault_count`, 0) + COALESCE(r.`fault_count`, 0) AS `fault_count`,
+    COALESCE(c.`fault_duration_ms`, 0) + COALESCE(d.`fault_duration_ms`, 0) + COALESCE(r.`fault_duration_ms`, 0) AS `fault_duration_ms`,
+    COALESCE(c.`fault_count`, 0) AS `crn_fault_count`,
+    COALESCE(c.`fault_duration_ms`, 0) AS `crn_fault_duration_ms`,
+    COALESCE(d.`fault_count`, 0) AS `dual_crn_fault_count`,
+    COALESCE(d.`fault_duration_ms`, 0) AS `dual_crn_fault_duration_ms`,
+    COALESCE(r.`fault_count`, 0) AS `rgv_fault_count`,
+    COALESCE(r.`fault_duration_ms`, 0) AS `rgv_fault_duration_ms`,
+    'PARTIAL' AS `metric_completeness`,
+    NOW(),
+    NOW()
+FROM `asr_wrk_mast_log` l
+LEFT JOIN (
+    SELECT
+        e.`wrk_no`,
+        COUNT(*) AS `fault_count`,
+        SUM(
+            CASE
+                WHEN e.`start_time` IS NULL THEN 0
+                ELSE GREATEST(
+                    TIMESTAMPDIFF(
+                        MICROSECOND,
+                        e.`start_time`,
+                        COALESCE(e.`end_time`, wl.`modi_time`, wl.`io_time`, wl.`appe_time`, e.`start_time`)
+                    ),
+                    0
+                ) DIV 1000
+            END
+        ) AS `fault_duration_ms`
+    FROM `asr_bas_crnp_err_log` e
+    LEFT JOIN `asr_wrk_mast_log` wl ON wl.`wrk_no` = e.`wrk_no`
+    GROUP BY e.`wrk_no`
+) c ON c.`wrk_no` = l.`wrk_no`
+LEFT JOIN (
+    SELECT
+        e.`wrk_no`,
+        COUNT(*) AS `fault_count`,
+        SUM(
+            CASE
+                WHEN e.`start_time` IS NULL THEN 0
+                ELSE GREATEST(
+                    TIMESTAMPDIFF(
+                        MICROSECOND,
+                        e.`start_time`,
+                        COALESCE(e.`end_time`, wl.`modi_time`, wl.`io_time`, wl.`appe_time`, e.`start_time`)
+                    ),
+                    0
+                ) DIV 1000
+            END
+        ) AS `fault_duration_ms`
+    FROM `asr_bas_dual_crnp_err_log` e
+    LEFT JOIN `asr_wrk_mast_log` wl ON wl.`wrk_no` = e.`wrk_no`
+    GROUP BY e.`wrk_no`
+) d ON d.`wrk_no` = l.`wrk_no`
+LEFT JOIN (
+    SELECT
+        e.`task_no` AS `wrk_no`,
+        COUNT(*) AS `fault_count`,
+        SUM(
+            CASE
+                WHEN e.`start_time` IS NULL THEN 0
+                ELSE GREATEST(
+                    TIMESTAMPDIFF(
+                        MICROSECOND,
+                        e.`start_time`,
+                        COALESCE(e.`end_time`, wl.`modi_time`, wl.`io_time`, wl.`appe_time`, e.`start_time`)
+                    ),
+                    0
+                ) DIV 1000
+            END
+        ) AS `fault_duration_ms`
+    FROM `asr_bas_rgv_err_log` e
+    LEFT JOIN `asr_wrk_mast_log` wl ON wl.`wrk_no` = e.`task_no`
+    GROUP BY e.`task_no`
+) r ON r.`wrk_no` = l.`wrk_no`
+ON DUPLICATE KEY UPDATE
+    `wms_wrk_no` = VALUES(`wms_wrk_no`),
+    `io_type` = VALUES(`io_type`),
+    `final_wrk_sts` = VALUES(`final_wrk_sts`),
+    `source_sta_no` = VALUES(`source_sta_no`),
+    `sta_no` = VALUES(`sta_no`),
+    `source_loc_no` = VALUES(`source_loc_no`),
+    `loc_no` = VALUES(`loc_no`),
+    `crn_no` = VALUES(`crn_no`),
+    `dual_crn_no` = VALUES(`dual_crn_no`),
+    `rgv_no` = VALUES(`rgv_no`),
+    `appe_time` = VALUES(`appe_time`),
+    `finish_time` = VALUES(`finish_time`),
+    `total_duration_ms` = VALUES(`total_duration_ms`),
+    `station_duration_ms` = VALUES(`station_duration_ms`),
+    `crane_duration_ms` = VALUES(`crane_duration_ms`),
+    `has_fault` = VALUES(`has_fault`),
+    `fault_count` = VALUES(`fault_count`),
+    `fault_duration_ms` = VALUES(`fault_duration_ms`),
+    `crn_fault_count` = VALUES(`crn_fault_count`),
+    `crn_fault_duration_ms` = VALUES(`crn_fault_duration_ms`),
+    `dual_crn_fault_count` = VALUES(`dual_crn_fault_count`),
+    `dual_crn_fault_duration_ms` = VALUES(`dual_crn_fault_duration_ms`),
+    `rgv_fault_count` = VALUES(`rgv_fault_count`),
+    `rgv_fault_duration_ms` = VALUES(`rgv_fault_duration_ms`),
+    `metric_completeness` = VALUES(`metric_completeness`),
+    `update_time` = NOW();
+
+SET @wrk_analysis_parent_id := COALESCE(
+  (
+    SELECT `id`
+    FROM `sys_resource`
+    WHERE `code` = 'logReport' AND `level` = 1
+    ORDER BY `id`
+    LIMIT 1
+  ),
+  (
+    SELECT `id`
+    FROM `sys_resource`
+    WHERE `code` = 'develop' AND `level` = 1
+    ORDER BY `id`
+    LIMIT 1
+  )
+);
+
+INSERT INTO `sys_resource`(`code`, `name`, `resource_id`, `level`, `sort`, `status`)
+SELECT 'wrkAnalysis/wrkAnalysis.html', '浠诲姟鎵ц鍒嗘瀽', @wrk_analysis_parent_id, 2, 996, 1
+FROM dual
+WHERE @wrk_analysis_parent_id IS NOT NULL
+  AND NOT EXISTS (
+    SELECT 1
+    FROM `sys_resource`
+    WHERE `code` = 'wrkAnalysis/wrkAnalysis.html' AND `level` = 2
+  );
+
+UPDATE `sys_resource`
+SET `name` = '浠诲姟鎵ц鍒嗘瀽',
+    `resource_id` = @wrk_analysis_parent_id,
+    `level` = 2,
+    `sort` = 996,
+    `status` = 1
+WHERE `code` = 'wrkAnalysis/wrkAnalysis.html' AND `level` = 2;
+
+SET @wrk_analysis_id := (
+  SELECT `id`
+  FROM `sys_resource`
+  WHERE `code` = 'wrkAnalysis/wrkAnalysis.html' AND `level` = 2
+  ORDER BY `id`
+  LIMIT 1
+);
+
+INSERT INTO `sys_resource`(`code`, `name`, `resource_id`, `level`, `sort`, `status`)
+SELECT 'wrkAnalysis/wrkAnalysis.html#view', '鏌ョ湅', @wrk_analysis_id, 3, 1, 1
+FROM dual
+WHERE @wrk_analysis_id IS NOT NULL
+  AND NOT EXISTS (
+    SELECT 1
+    FROM `sys_resource`
+    WHERE `code` = 'wrkAnalysis/wrkAnalysis.html#view' AND `level` = 3
+  );
+
+UPDATE `sys_resource`
+SET `name` = '鏌ョ湅',
+    `resource_id` = @wrk_analysis_id,
+    `level` = 3,
+    `sort` = 1,
+    `status` = 1
+WHERE `code` = 'wrkAnalysis/wrkAnalysis.html#view' AND `level` = 3;
diff --git a/src/main/webapp/static/js/wrkAnalysis/wrkAnalysis.js b/src/main/webapp/static/js/wrkAnalysis/wrkAnalysis.js
new file mode 100644
index 0000000..12c5835
--- /dev/null
+++ b/src/main/webapp/static/js/wrkAnalysis/wrkAnalysis.js
@@ -0,0 +1,477 @@
+(function () {
+    "use strict";
+
+    function nowDate() {
+        return new Date();
+    }
+
+    function startOfToday() {
+        var date = new Date();
+        date.setHours(0, 0, 0, 0);
+        return date;
+    }
+
+    function createDefaultFilters() {
+        return {
+            mode: "TASK",
+            keyword: "",
+            ioType: "",
+            finalWrkSts: "",
+            sourceStaNo: "",
+            staNo: "",
+            deviceType: "",
+            timeField: "finish_time",
+            timeRange: [startOfToday(), nowDate()]
+        };
+    }
+
+    function createEmptyAnalysis() {
+        return {
+            summary: {
+                taskCount: 0,
+                avgTotalDurationMs: null,
+                avgStationDurationMs: null,
+                avgCraneDurationMs: null,
+                faultTaskCount: 0,
+                faultDurationMs: 0,
+                partialTaskCount: 0
+            },
+            durationCompare: [],
+            trend: [],
+            faultPie: [],
+            faultDuration: [],
+            detail: []
+        };
+    }
+
+    new Vue({
+        el: "#app",
+        data: function () {
+            return {
+                options: {
+                    ioTypes: [],
+                    statuses: [],
+                    stations: [],
+                    deviceTypes: [],
+                    timeFields: []
+                },
+                filters: createDefaultFilters(),
+                tableData: [],
+                currentPage: 1,
+                pageSize: 20,
+                pageTotal: 0,
+                listLoading: false,
+                analyzeLoading: false,
+                selectedWrkNoMap: {},
+                analysis: createEmptyAnalysis(),
+                analysisReady: false,
+                charts: {
+                    duration: null,
+                    trend: null,
+                    faultPie: null,
+                    faultDuration: null
+                },
+                resizeHandler: null
+            };
+        },
+        computed: {
+            selectedWrkNos: function () {
+                return Object.keys(this.selectedWrkNoMap).map(function (key) {
+                    return Number(key);
+                }).filter(function (value) {
+                    return !!value;
+                });
+            }
+        },
+        mounted: function () {
+            var self = this;
+            this.loadOptions();
+            this.loadList();
+            this.resizeHandler = function () {
+                self.resizeCharts();
+            };
+            window.addEventListener("resize", this.resizeHandler);
+        },
+        beforeDestroy: function () {
+            if (this.resizeHandler) {
+                window.removeEventListener("resize", this.resizeHandler);
+            }
+            this.disposeCharts();
+        },
+        methods: {
+            loadOptions: function () {
+                var self = this;
+                $.ajax({
+                    url: baseUrl + "/wrkAnalysis/options/auth",
+                    headers: { token: localStorage.getItem("token") },
+                    method: "GET",
+                    success: function (res) {
+                        if (res && res.code === 200) {
+                            self.options = Object.assign(self.options, res.data || {});
+                            return;
+                        }
+                        self.$message.error((res && res.msg) || "鍒嗘瀽閫夐」鍔犺浇澶辫触");
+                    },
+                    error: function () {
+                        self.$message.error("鍒嗘瀽閫夐」鍔犺浇澶辫触");
+                    }
+                });
+            },
+            buildListParams: function () {
+                var params = {
+                    curr: this.currentPage,
+                    limit: this.pageSize,
+                    keyword: this.filters.keyword,
+                    ioType: this.filters.ioType,
+                    finalWrkSts: this.filters.finalWrkSts,
+                    sourceStaNo: this.filters.sourceStaNo,
+                    staNo: this.filters.staNo,
+                    deviceType: this.filters.deviceType
+                };
+                if (this.filters.timeRange && this.filters.timeRange.length === 2) {
+                    if (this.filters.timeField === "appe_time") {
+                        params.appeTimeRange = this.formatRange(this.filters.timeRange);
+                    } else {
+                        params.finishTimeRange = this.formatRange(this.filters.timeRange);
+                    }
+                }
+                return this.cleanParams(params);
+            },
+            loadList: function () {
+                var self = this;
+                this.listLoading = true;
+                $.ajax({
+                    url: baseUrl + "/wrkAnalysis/list/auth",
+                    headers: { token: localStorage.getItem("token") },
+                    method: "GET",
+                    data: self.buildListParams(),
+                    success: function (res) {
+                        if (res && res.code === 200) {
+                            var data = res.data || {};
+                            self.tableData = data.records || [];
+                            self.pageTotal = data.total || 0;
+                            self.$nextTick(function () {
+                                self.restoreSelection();
+                            });
+                            return;
+                        }
+                        self.$message.error((res && res.msg) || "鍘嗗彶浠诲姟鍔犺浇澶辫触");
+                    },
+                    error: function () {
+                        self.$message.error("鍘嗗彶浠诲姟鍔犺浇澶辫触");
+                    },
+                    complete: function () {
+                        self.listLoading = false;
+                    }
+                });
+            },
+            handleSearch: function () {
+                this.currentPage = 1;
+                this.loadList();
+            },
+            handleReset: function () {
+                this.filters = createDefaultFilters();
+                this.currentPage = 1;
+                this.pageSize = 20;
+                this.selectedWrkNoMap = {};
+                this.analysis = createEmptyAnalysis();
+                this.analysisReady = false;
+                this.disposeCharts();
+                this.loadList();
+            },
+            handleSizeChange: function (size) {
+                this.pageSize = size;
+                this.currentPage = 1;
+                this.loadList();
+            },
+            handleCurrentChange: function (page) {
+                this.currentPage = page;
+                this.loadList();
+            },
+            restoreSelection: function () {
+                var table = this.$refs.historyTable;
+                var self = this;
+                if (!table) {
+                    return;
+                }
+                table.clearSelection();
+                (this.tableData || []).forEach(function (row) {
+                    if (self.selectedWrkNoMap[row.wrkNo]) {
+                        table.toggleRowSelection(row, true);
+                    }
+                });
+            },
+            syncCurrentPageSelection: function (selection) {
+                var selectedMap = {};
+                (selection || []).forEach(function (row) {
+                    selectedMap[row.wrkNo] = true;
+                });
+                (this.tableData || []).forEach(function (row) {
+                    delete this.selectedWrkNoMap[row.wrkNo];
+                }, this);
+                Object.keys(selectedMap).forEach(function (key) {
+                    this.selectedWrkNoMap[key] = true;
+                }, this);
+            },
+            runAnalysis: function () {
+                var self = this;
+                var request = {
+                    mode: this.filters.mode,
+                    ioType: this.filters.ioType,
+                    finalWrkSts: this.filters.finalWrkSts,
+                    sourceStaNo: this.filters.sourceStaNo,
+                    staNo: this.filters.staNo,
+                    deviceType: this.filters.deviceType
+                };
+                if (this.filters.mode === "TASK") {
+                    if (!this.selectedWrkNos.length) {
+                        this.$message.warning("璇峰厛鍕鹃�夎鍒嗘瀽鐨勪换鍔�");
+                        return;
+                    }
+                    request.wrkNos = this.selectedWrkNos;
+                    request.timeField = this.filters.timeField;
+                } else {
+                    if (!this.filters.timeRange || this.filters.timeRange.length !== 2) {
+                        this.$message.warning("璇峰厛閫夋嫨鍒嗘瀽鏃堕棿鑼冨洿");
+                        return;
+                    }
+                    request.timeField = this.filters.timeField;
+                    request.startTime = this.filters.timeRange[0].getTime();
+                    request.endTime = this.filters.timeRange[1].getTime();
+                }
+                this.analyzeLoading = true;
+                $.ajax({
+                    url: baseUrl + "/wrkAnalysis/analyze/auth",
+                    headers: {
+                        token: localStorage.getItem("token"),
+                        "Content-Type": "application/json"
+                    },
+                    method: "POST",
+                    data: JSON.stringify(this.cleanParams(request)),
+                    success: function (res) {
+                        if (res && res.code === 200) {
+                            self.analysis = Object.assign(createEmptyAnalysis(), res.data || {});
+                            self.analysisReady = true;
+                            self.$nextTick(function () {
+                                self.updateCharts();
+                            });
+                            return;
+                        }
+                        self.$message.error((res && res.msg) || "鍒嗘瀽澶辫触");
+                    },
+                    error: function () {
+                        self.$message.error("鍒嗘瀽澶辫触");
+                    },
+                    complete: function () {
+                        self.analyzeLoading = false;
+                    }
+                });
+            },
+            updateCharts: function () {
+                if (!this.analysisReady) {
+                    this.disposeCharts();
+                    return;
+                }
+                this.ensureCharts();
+                this.renderDurationChart();
+                this.renderTrendChart();
+                this.renderFaultPieChart();
+                this.renderFaultDurationChart();
+            },
+            ensureCharts: function () {
+                if (this.$refs.durationChart && !this.charts.duration) {
+                    this.charts.duration = echarts.init(this.$refs.durationChart);
+                }
+                if (this.$refs.trendChart && !this.charts.trend) {
+                    this.charts.trend = echarts.init(this.$refs.trendChart);
+                }
+                if (this.$refs.faultPieChart && !this.charts.faultPie) {
+                    this.charts.faultPie = echarts.init(this.$refs.faultPieChart);
+                }
+                if (this.$refs.faultDurationChart && !this.charts.faultDuration) {
+                    this.charts.faultDuration = echarts.init(this.$refs.faultDurationChart);
+                }
+            },
+            renderDurationChart: function () {
+                if (!this.charts.duration) {
+                    return;
+                }
+                var rows = this.analysis.durationCompare || [];
+                this.charts.duration.setOption({
+                    tooltip: { trigger: "axis" },
+                    legend: { data: ["绔欑偣鑰楁椂", "鍫嗗灈鏈鸿�楁椂", "鎬昏�楁椂"] },
+                    grid: { left: 50, right: 20, top: 40, bottom: 70 },
+                    xAxis: {
+                        type: "category",
+                        data: rows.map(function (item) { return String(item.wrkNo); }),
+                        axisLabel: { rotate: rows.length > 8 ? 30 : 0 }
+                    },
+                    yAxis: {
+                        type: "value",
+                        axisLabel: {
+                            formatter: function (value) {
+                                return Math.round((value || 0) / 1000) + "s";
+                            }
+                        }
+                    },
+                    series: [
+                        { name: "绔欑偣鑰楁椂", type: "bar", barMaxWidth: 28, data: rows.map(function (item) { return item.stationDurationMs || 0; }) },
+                        { name: "鍫嗗灈鏈鸿�楁椂", type: "bar", barMaxWidth: 28, data: rows.map(function (item) { return item.craneDurationMs || 0; }) },
+                        { name: "鎬昏�楁椂", type: "bar", barMaxWidth: 28, data: rows.map(function (item) { return item.totalDurationMs || 0; }) }
+                    ]
+                }, true);
+            },
+            renderTrendChart: function () {
+                if (!this.charts.trend) {
+                    return;
+                }
+                var rows = this.analysis.trend || [];
+                this.charts.trend.setOption({
+                    tooltip: { trigger: "axis" },
+                    legend: { data: ["骞冲潎鎬昏�楁椂", "骞冲潎绔欑偣鑰楁椂", "骞冲潎鍫嗗灈鏈鸿�楁椂"] },
+                    grid: { left: 50, right: 20, top: 40, bottom: 70 },
+                    xAxis: {
+                        type: "category",
+                        data: rows.map(function (item) { return item.bucketLabel; }),
+                        axisLabel: { rotate: rows.length > 8 ? 25 : 0 }
+                    },
+                    yAxis: {
+                        type: "value",
+                        axisLabel: {
+                            formatter: function (value) {
+                                return Math.round((value || 0) / 1000) + "s";
+                            }
+                        }
+                    },
+                    series: [
+                        { name: "骞冲潎鎬昏�楁椂", type: "line", smooth: true, data: rows.map(function (item) { return item.avgTotalDurationMs || 0; }) },
+                        { name: "骞冲潎绔欑偣鑰楁椂", type: "line", smooth: true, data: rows.map(function (item) { return item.avgStationDurationMs || 0; }) },
+                        { name: "骞冲潎鍫嗗灈鏈鸿�楁椂", type: "line", smooth: true, data: rows.map(function (item) { return item.avgCraneDurationMs || 0; }) }
+                    ]
+                }, true);
+            },
+            renderFaultPieChart: function () {
+                if (!this.charts.faultPie) {
+                    return;
+                }
+                this.charts.faultPie.setOption({
+                    tooltip: { trigger: "item" },
+                    legend: { bottom: 0 },
+                    series: [{
+                        type: "pie",
+                        radius: ["42%", "68%"],
+                        center: ["50%", "46%"],
+                        label: { formatter: "{b}\n{d}%" },
+                        data: this.analysis.faultPie || []
+                    }]
+                }, true);
+            },
+            renderFaultDurationChart: function () {
+                if (!this.charts.faultDuration) {
+                    return;
+                }
+                var rows = this.analysis.faultDuration || [];
+                this.charts.faultDuration.setOption({
+                    tooltip: { trigger: "axis" },
+                    grid: { left: 50, right: 20, top: 20, bottom: 40 },
+                    xAxis: {
+                        type: "category",
+                        data: rows.map(function (item) { return item.name; })
+                    },
+                    yAxis: {
+                        type: "value",
+                        axisLabel: {
+                            formatter: function (value) {
+                                return Math.round((value || 0) / 1000) + "s";
+                            }
+                        }
+                    },
+                    series: [{
+                        name: "鏁呴殰鑰楁椂",
+                        type: "bar",
+                        barMaxWidth: 36,
+                        data: rows.map(function (item) { return item.value || 0; })
+                    }]
+                }, true);
+            },
+            resizeCharts: function () {
+                Object.keys(this.charts).forEach(function (key) {
+                    if (this.charts[key]) {
+                        this.charts[key].resize();
+                    }
+                }, this);
+            },
+            disposeCharts: function () {
+                Object.keys(this.charts).forEach(function (key) {
+                    if (this.charts[key]) {
+                        this.charts[key].dispose();
+                        this.charts[key] = null;
+                    }
+                }, this);
+            },
+            formatRange: function (range) {
+                return this.formatDateTime(range[0]) + " ~ " + this.formatDateTime(range[1]);
+            },
+            formatDateTime: function (date) {
+                if (!date) {
+                    return "";
+                }
+                var year = date.getFullYear();
+                var month = this.pad(date.getMonth() + 1);
+                var day = this.pad(date.getDate());
+                var hour = this.pad(date.getHours());
+                var minute = this.pad(date.getMinutes());
+                var second = this.pad(date.getSeconds());
+                return year + "-" + month + "-" + day + " " + hour + ":" + minute + ":" + second;
+            },
+            pad: function (value) {
+                return value < 10 ? "0" + value : String(value);
+            },
+            cleanParams: function (params) {
+                var result = {};
+                Object.keys(params || {}).forEach(function (key) {
+                    var value = params[key];
+                    if (value === "" || value === null || value === undefined) {
+                        return;
+                    }
+                    if (Array.isArray(value) && !value.length) {
+                        return;
+                    }
+                    result[key] = value;
+                });
+                return result;
+            },
+            formatNumber: function (value) {
+                var num = Number(value || 0);
+                if (!isFinite(num)) {
+                    return "0";
+                }
+                return num.toLocaleString("zh-CN");
+            },
+            formatDuration: function (value) {
+                if (value === null || value === undefined || value === "") {
+                    return "--";
+                }
+                var ms = Number(value);
+                if (!isFinite(ms)) {
+                    return "--";
+                }
+                if (ms < 1000) {
+                    return Math.round(ms) + " ms";
+                }
+                var totalSeconds = Math.floor(ms / 1000);
+                var hours = Math.floor(totalSeconds / 3600);
+                var minutes = Math.floor((totalSeconds % 3600) / 60);
+                var seconds = totalSeconds % 60;
+                if (hours > 0) {
+                    return hours + "h " + this.pad(minutes) + "m " + this.pad(seconds) + "s";
+                }
+                if (minutes > 0) {
+                    return minutes + "m " + this.pad(seconds) + "s";
+                }
+                return seconds + "s";
+            }
+        }
+    });
+
+})();
diff --git a/src/main/webapp/views/wrkAnalysis/wrkAnalysis.html b/src/main/webapp/views/wrkAnalysis/wrkAnalysis.html
new file mode 100644
index 0000000..ee6ac39
--- /dev/null
+++ b/src/main/webapp/views/wrkAnalysis/wrkAnalysis.html
@@ -0,0 +1,443 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>浠诲姟鎵ц鍒嗘瀽</title>
+    <link rel="stylesheet" href="../../static/vue/element/element.css">
+    <style>
+        [v-cloak] { display: none; }
+        * { box-sizing: border-box; }
+        html, body {
+            margin: 0;
+            min-height: 100%;
+            font-family: "Avenir Next", "PingFang SC", "Microsoft YaHei", sans-serif;
+            color: #22364a;
+            background:
+                radial-gradient(960px 400px at 0% -10%, rgba(33, 91, 168, 0.10), transparent 60%),
+                radial-gradient(880px 360px at 100% 0%, rgba(16, 142, 120, 0.10), transparent 60%),
+                linear-gradient(180deg, #f3f7fb 0%, #edf3f8 100%);
+        }
+        .page-shell {
+            max-width: 1720px;
+            margin: 0 auto;
+            padding: 16px;
+        }
+        .page-head {
+            display: flex;
+            align-items: flex-end;
+            justify-content: space-between;
+            gap: 12px;
+            margin-bottom: 14px;
+            flex-wrap: wrap;
+        }
+        .page-title {
+            font-size: 30px;
+            font-weight: 700;
+            color: #1e3245;
+        }
+        .page-subtitle {
+            margin-top: 6px;
+            font-size: 13px;
+            color: #7b8ca0;
+        }
+        .panel {
+            margin-top: 16px;
+            border-radius: 24px;
+            border: 1px solid rgba(216, 226, 235, 0.96);
+            background: rgba(255, 255, 255, 0.92);
+            box-shadow: 0 16px 36px rgba(38, 60, 88, 0.08);
+            overflow: hidden;
+        }
+        .panel-head {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            gap: 12px;
+            padding: 18px 20px 0;
+            flex-wrap: wrap;
+        }
+        .panel-title {
+            font-size: 18px;
+            font-weight: 700;
+            color: #22364a;
+        }
+        .panel-body {
+            padding: 16px 20px 20px;
+        }
+        .filter-grid {
+            display: grid;
+            grid-template-columns: repeat(6, minmax(0, 1fr));
+            gap: 12px;
+        }
+        .filter-item.span-2 {
+            grid-column: span 2;
+        }
+        .toolbar-row {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            gap: 12px;
+            margin-bottom: 14px;
+            flex-wrap: wrap;
+        }
+        .toolbar-actions {
+            display: flex;
+            gap: 10px;
+            flex-wrap: wrap;
+        }
+        .selection-meta {
+            font-size: 13px;
+            color: #698096;
+        }
+        .summary-grid {
+            display: grid;
+            grid-template-columns: repeat(6, minmax(0, 1fr));
+            gap: 12px;
+        }
+        .summary-card {
+            min-height: 92px;
+            padding: 14px 16px;
+            border-radius: 18px;
+            border: 1px solid #dfe8f2;
+            background: linear-gradient(180deg, #fcfdff 0%, #f5f9fd 100%);
+        }
+        .summary-label {
+            font-size: 12px;
+            color: #7d8ea2;
+        }
+        .summary-value {
+            margin-top: 8px;
+            font-size: 28px;
+            line-height: 1.1;
+            font-weight: 700;
+            color: #22364a;
+        }
+        .summary-sub {
+            margin-top: 8px;
+            font-size: 12px;
+            color: #8a99ab;
+        }
+        .quality-banner {
+            margin-bottom: 14px;
+            padding: 10px 14px;
+            border-radius: 14px;
+            border: 1px solid rgba(230, 187, 88, 0.35);
+            background: rgba(255, 246, 226, 0.92);
+            color: #8b5d10;
+            font-size: 13px;
+        }
+        .chart-grid {
+            display: grid;
+            grid-template-columns: repeat(2, minmax(0, 1fr));
+            gap: 16px;
+        }
+        .chart-card {
+            padding: 16px;
+            border-radius: 20px;
+            border: 1px solid #dfe7f0;
+            background: #fbfdff;
+        }
+        .chart-title {
+            margin-bottom: 12px;
+            font-size: 16px;
+            font-weight: 700;
+            color: #22364a;
+        }
+        .chart-box {
+            width: 100%;
+            height: 320px;
+        }
+        .empty-shell {
+            padding: 52px 16px;
+            border-radius: 18px;
+            border: 1px dashed #d5e0ec;
+            background: #fafcff;
+            text-align: center;
+            color: #8a99ab;
+            font-size: 14px;
+        }
+        .tag-complete {
+            color: #187c5a;
+            background: rgba(24, 162, 105, 0.16);
+            border-color: rgba(24, 162, 105, 0.18);
+        }
+        .tag-partial {
+            color: #b56c05;
+            background: rgba(245, 163, 74, 0.16);
+            border-color: rgba(245, 163, 74, 0.18);
+        }
+        @media (max-width: 1460px) {
+            .filter-grid,
+            .summary-grid {
+                grid-template-columns: repeat(3, minmax(0, 1fr));
+            }
+            .chart-grid {
+                grid-template-columns: 1fr;
+            }
+        }
+        @media (max-width: 860px) {
+            .page-shell { padding: 12px; }
+            .filter-grid,
+            .summary-grid {
+                grid-template-columns: repeat(2, minmax(0, 1fr));
+            }
+            .filter-item.span-2 {
+                grid-column: span 2;
+            }
+            .page-title {
+                font-size: 24px;
+            }
+        }
+    </style>
+</head>
+<body>
+<div id="app" v-cloak class="page-shell">
+    <div class="page-head">
+        <div>
+            <div class="page-title">浠诲姟鎵ц鍒嗘瀽</div>
+            <div class="page-subtitle">鏀寔鎸変换鍔℃壒閲忓垎鏋愬拰鎸夋椂闂磋寖鍥村垎鏋愶紝缁熶竴鏌ョ湅绔欑偣鑰楁椂銆佸爢鍨涙満鑰楁椂銆佹�昏�楁椂鍜屾晠闅滆�楁椂銆�</div>
+        </div>
+    </div>
+
+    <section class="panel">
+        <div class="panel-head">
+            <div class="panel-title">绛涢�夋潯浠�</div>
+        </div>
+        <div class="panel-body">
+            <div class="toolbar-row">
+                <el-radio-group v-model="filters.mode" size="small">
+                    <el-radio-button label="TASK">鎸変换鍔℃壒閲忓垎鏋�</el-radio-button>
+                    <el-radio-button label="TIME">鎸夋椂闂磋寖鍥村垎鏋�</el-radio-button>
+                </el-radio-group>
+                <div class="toolbar-actions">
+                    <el-button size="small" @click="handleReset">閲嶇疆</el-button>
+                    <el-button size="small" type="primary" @click="handleSearch">鏌ヨ鍘嗗彶浠诲姟</el-button>
+                    <el-button size="small" type="success" :loading="analyzeLoading" @click="runAnalysis">寮�濮嬪垎鏋�</el-button>
+                </div>
+            </div>
+
+            <div class="filter-grid">
+                <div class="filter-item span-2">
+                    <el-input v-model.trim="filters.keyword" clearable placeholder="鍏抽敭瀛楋細宸ヤ綔鍙� / WMS浠诲姟鍙� / 搴撲綅 / 鏉$爜"></el-input>
+                </div>
+                <div class="filter-item">
+                    <el-select v-model="filters.ioType" clearable placeholder="浠诲姟绫诲瀷" style="width: 100%;">
+                        <el-option label="鍏ㄩ儴" value=""></el-option>
+                        <el-option v-for="item in options.ioTypes" :key="item.code" :label="item.label" :value="item.value"></el-option>
+                    </el-select>
+                </div>
+                <div class="filter-item">
+                    <el-select v-model="filters.finalWrkSts" clearable placeholder="鏈�缁堢姸鎬�" style="width: 100%;">
+                        <el-option label="榛樿瀹屾垚鎬�" value=""></el-option>
+                        <el-option v-for="item in options.statuses" :key="item.code" :label="item.label" :value="item.value"></el-option>
+                    </el-select>
+                </div>
+                <div class="filter-item">
+                    <el-select v-model="filters.sourceStaNo" clearable placeholder="婧愮珯" style="width: 100%;">
+                        <el-option label="鍏ㄩ儴婧愮珯" value=""></el-option>
+                        <el-option v-for="item in options.stations" :key="'source-' + item.code" :label="item.label" :value="item.value"></el-option>
+                    </el-select>
+                </div>
+                <div class="filter-item">
+                    <el-select v-model="filters.staNo" clearable placeholder="鐩爣绔�" style="width: 100%;">
+                        <el-option label="鍏ㄩ儴鐩爣绔�" value=""></el-option>
+                        <el-option v-for="item in options.stations" :key="'target-' + item.code" :label="item.label" :value="item.value"></el-option>
+                    </el-select>
+                </div>
+                <div class="filter-item">
+                    <el-select v-model="filters.deviceType" clearable placeholder="璁惧绫诲瀷" style="width: 100%;">
+                        <el-option label="鍏ㄩ儴璁惧" value=""></el-option>
+                        <el-option v-for="item in options.deviceTypes" :key="item.code" :label="item.label" :value="item.value"></el-option>
+                    </el-select>
+                </div>
+                <div class="filter-item">
+                    <el-select v-model="filters.timeField" placeholder="鏃堕棿瀛楁" style="width: 100%;">
+                        <el-option v-for="item in options.timeFields" :key="item.code" :label="item.label" :value="item.value"></el-option>
+                    </el-select>
+                </div>
+                <div class="filter-item span-2">
+                    <el-date-picker
+                            v-model="filters.timeRange"
+                            type="datetimerange"
+                            unlink-panels
+                            range-separator="鑷�"
+                            start-placeholder="寮�濮嬫椂闂�"
+                            end-placeholder="缁撴潫鏃堕棿"
+                            style="width: 100%;">
+                    </el-date-picker>
+                </div>
+            </div>
+        </div>
+    </section>
+
+    <section class="panel">
+        <div class="panel-head">
+            <div class="panel-title">鍘嗗彶浠诲姟鍒楄〃</div>
+            <div class="selection-meta">宸查�変换鍔� {{ selectedWrkNos.length }} 鏉�</div>
+        </div>
+        <div class="panel-body">
+            <el-table
+                    ref="historyTable"
+                    :data="tableData"
+                    border
+                    stripe
+                    size="mini"
+                    row-key="wrkNo"
+                    @selection-change="syncCurrentPageSelection"
+                    v-loading="listLoading"
+                    style="width: 100%;">
+                <el-table-column type="selection" width="48" reserve-selection></el-table-column>
+                <el-table-column prop="wrkNo" label="宸ヤ綔鍙�" width="100" align="center"></el-table-column>
+                <el-table-column prop="wmsWrkNo" label="WMS浠诲姟鍙�" min-width="150"></el-table-column>
+                <el-table-column prop="ioType$" label="浠诲姟绫诲瀷" width="90" align="center"></el-table-column>
+                <el-table-column prop="wrkSts$" label="鏈�缁堢姸鎬�" min-width="120"></el-table-column>
+                <el-table-column prop="sourceStaNo" label="婧愮珯" width="90" align="center"></el-table-column>
+                <el-table-column prop="staNo" label="鐩爣绔�" width="90" align="center"></el-table-column>
+                <el-table-column prop="appeTime$" label="鍒涘缓鏃堕棿" width="165"></el-table-column>
+                <el-table-column prop="finishTime$" label="瀹屾垚鏃堕棿" width="165"></el-table-column>
+                <el-table-column label="鎬昏�楁椂" width="130" align="right">
+                    <template slot-scope="scope">{{ formatDuration(scope.row.totalDurationMs) }}</template>
+                </el-table-column>
+                <el-table-column label="绔欑偣鑰楁椂" width="130" align="right">
+                    <template slot-scope="scope">{{ formatDuration(scope.row.stationDurationMs) }}</template>
+                </el-table-column>
+                <el-table-column label="鍫嗗灈鏈鸿�楁椂" width="130" align="right">
+                    <template slot-scope="scope">{{ formatDuration(scope.row.craneDurationMs) }}</template>
+                </el-table-column>
+                <el-table-column prop="faultCount" label="鏁呴殰娆℃暟" width="90" align="center"></el-table-column>
+                <el-table-column label="鏁呴殰鑰楁椂" width="130" align="right">
+                    <template slot-scope="scope">{{ formatDuration(scope.row.faultDurationMs) }}</template>
+                </el-table-column>
+                <el-table-column label="鏁版嵁瀹屾暣鎬�" width="110" align="center">
+                    <template slot-scope="scope">
+                        <el-tag size="mini" :class="scope.row.metricCompleteness === 'COMPLETE' ? 'tag-complete' : 'tag-partial'">
+                            {{ scope.row.metricCompleteness === 'COMPLETE' ? 'COMPLETE' : 'PARTIAL' }}
+                        </el-tag>
+                    </template>
+                </el-table-column>
+            </el-table>
+
+            <div style="margin-top: 14px; text-align: right;">
+                <el-pagination
+                        background
+                        layout="total, sizes, prev, pager, next"
+                        :current-page="currentPage"
+                        :page-size="pageSize"
+                        :page-sizes="[20, 50, 100, 200]"
+                        :total="pageTotal"
+                        @size-change="handleSizeChange"
+                        @current-change="handleCurrentChange">
+                </el-pagination>
+            </div>
+        </div>
+    </section>
+
+    <section class="panel">
+        <div class="panel-head">
+            <div class="panel-title">鍒嗘瀽缁撴灉</div>
+        </div>
+        <div class="panel-body">
+            <div v-if="analysis.summary.partialTaskCount > 0" class="quality-banner">
+                褰撳墠缁撴灉涓湁 {{ analysis.summary.partialTaskCount }} 鏉″巻鍙蹭换鍔$己灏戦樁娈甸噰闆嗭紝浠呭弬涓庢�昏�楁椂鍜屾晠闅滅粺璁°��
+            </div>
+
+            <div v-if="!analysisReady" class="empty-shell">鍏堜粠涓婃柟绛涢�変换鍔℃垨鏃堕棿鑼冨洿锛岀劧鍚庢墽琛屽垎鏋愩��</div>
+            <template v-else>
+                <div class="summary-grid">
+                    <div class="summary-card">
+                        <div class="summary-label">浠诲姟鏁�</div>
+                        <div class="summary-value">{{ formatNumber(analysis.summary.taskCount) }}</div>
+                        <div class="summary-sub">鏈鍒嗘瀽鍛戒腑浠诲姟鎬绘暟</div>
+                    </div>
+                    <div class="summary-card">
+                        <div class="summary-label">骞冲潎鎬昏�楁椂</div>
+                        <div class="summary-value">{{ formatDuration(analysis.summary.avgTotalDurationMs) }}</div>
+                        <div class="summary-sub">鍒涘缓鍒板畬鎴愮殑骞冲潎鑰楁椂</div>
+                    </div>
+                    <div class="summary-card">
+                        <div class="summary-label">骞冲潎绔欑偣鑰楁椂</div>
+                        <div class="summary-value">{{ formatDuration(analysis.summary.avgStationDurationMs) }}</div>
+                        <div class="summary-sub">浠呯粺璁″畬鏁撮樁娈垫暟鎹�</div>
+                    </div>
+                    <div class="summary-card">
+                        <div class="summary-label">骞冲潎鍫嗗灈鏈鸿�楁椂</div>
+                        <div class="summary-value">{{ formatDuration(analysis.summary.avgCraneDurationMs) }}</div>
+                        <div class="summary-sub">浠呯粺璁″畬鏁撮樁娈垫暟鎹�</div>
+                    </div>
+                    <div class="summary-card">
+                        <div class="summary-label">鏁呴殰浠诲姟鏁�</div>
+                        <div class="summary-value">{{ formatNumber(analysis.summary.faultTaskCount) }}</div>
+                        <div class="summary-sub">鍑虹幇璁惧鏁呴殰鐨勪换鍔℃暟</div>
+                    </div>
+                    <div class="summary-card">
+                        <div class="summary-label">鎬绘晠闅滆�楁椂</div>
+                        <div class="summary-value">{{ formatDuration(analysis.summary.faultDurationMs) }}</div>
+                        <div class="summary-sub">鍗曞爢鍨涙満 / 鍙屽伐浣� / RGV</div>
+                    </div>
+                </div>
+
+                <div class="chart-grid" style="margin-top: 16px;">
+                    <div class="chart-card">
+                        <div class="chart-title">浠诲姟鑰楁椂瀵规瘮</div>
+                        <div ref="durationChart" class="chart-box"></div>
+                    </div>
+                    <div class="chart-card">
+                        <div class="chart-title">鑰楁椂瓒嬪娍</div>
+                        <div ref="trendChart" class="chart-box"></div>
+                    </div>
+                    <div class="chart-card">
+                        <div class="chart-title">鏁呴殰浠诲姟鍗犳瘮</div>
+                        <div ref="faultPieChart" class="chart-box"></div>
+                    </div>
+                    <div class="chart-card">
+                        <div class="chart-title">鏁呴殰鑰楁椂鍒嗗竷</div>
+                        <div ref="faultDurationChart" class="chart-box"></div>
+                    </div>
+                </div>
+
+                <div style="margin-top: 16px;">
+                    <el-table :data="analysis.detail" border stripe size="mini" max-height="360" style="width: 100%;">
+                        <el-table-column prop="wrkNo" label="宸ヤ綔鍙�" width="100" align="center"></el-table-column>
+                        <el-table-column prop="wmsWrkNo" label="WMS浠诲姟鍙�" min-width="150"></el-table-column>
+                        <el-table-column prop="ioType$" label="浠诲姟绫诲瀷" width="90" align="center"></el-table-column>
+                        <el-table-column prop="finalWrkSts$" label="鏈�缁堢姸鎬�" min-width="120"></el-table-column>
+                        <el-table-column prop="sourceStaNo" label="婧愮珯" width="90" align="center"></el-table-column>
+                        <el-table-column prop="staNo" label="鐩爣绔�" width="90" align="center"></el-table-column>
+                        <el-table-column prop="appeTime$" label="鍒涘缓鏃堕棿" width="165"></el-table-column>
+                        <el-table-column prop="finishTime$" label="瀹屾垚鏃堕棿" width="165"></el-table-column>
+                        <el-table-column label="鎬昏�楁椂" width="130" align="right">
+                            <template slot-scope="scope">{{ formatDuration(scope.row.totalDurationMs) }}</template>
+                        </el-table-column>
+                        <el-table-column label="绔欑偣鑰楁椂" width="130" align="right">
+                            <template slot-scope="scope">{{ formatDuration(scope.row.stationDurationMs) }}</template>
+                        </el-table-column>
+                        <el-table-column label="鍫嗗灈鏈鸿�楁椂" width="130" align="right">
+                            <template slot-scope="scope">{{ formatDuration(scope.row.craneDurationMs) }}</template>
+                        </el-table-column>
+                        <el-table-column prop="faultCount" label="鏁呴殰娆℃暟" width="90" align="center"></el-table-column>
+                        <el-table-column label="鏁呴殰鑰楁椂" width="130" align="right">
+                            <template slot-scope="scope">{{ formatDuration(scope.row.faultDurationMs) }}</template>
+                        </el-table-column>
+                        <el-table-column label="鏁版嵁瀹屾暣鎬�" width="110" align="center">
+                            <template slot-scope="scope">
+                                <el-tag size="mini" :class="scope.row.metricCompleteness === 'COMPLETE' ? 'tag-complete' : 'tag-partial'">
+                                    {{ scope.row.metricCompleteness === 'COMPLETE' ? 'COMPLETE' : 'PARTIAL' }}
+                                </el-tag>
+                            </template>
+                        </el-table-column>
+                    </el-table>
+                </div>
+            </template>
+        </div>
+    </section>
+</div>
+
+<script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script>
+<script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script>
+<script type="text/javascript" src="../../static/vue/js/vue.min.js"></script>
+<script type="text/javascript" src="../../static/vue/element/element.js"></script>
+<script type="text/javascript" src="../../static/js/echarts/echarts.min.js"></script>
+<script type="text/javascript" src="../../static/js/wrkAnalysis/wrkAnalysis.js?v=20260322_01" charset="utf-8"></script>
+</body>
+</html>

--
Gitblit v1.9.1