#
Junjie
18 小时以前 3adcbff31fdece77269744c8741f237e7a57348e
#
10个文件已添加
14个文件已修改
2724 ■■■■■ 已修改文件
src/main/java/com/zy/asrs/controller/WrkAnalysisController.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/WrkAnalysis.java 155 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/mapper/WrkAnalysisMapper.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/WrkAnalysisService.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/PlannerServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/WrkAnalysisServiceImpl.java 909 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/WrkMastLogServiceImpl.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/task/WrkAnalysisStationArrivalScanner.java 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/task/WrkMastScheduler.java 48 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/common/service/CommonService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/enums/WrkStsType.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/plugin/FakeProcess.java 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/plugin/GslProcess.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/plugin/NormalProcess.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/plugin/XiaosongProcess.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/service/WrkCommandRollbackService.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/trace/StationTaskTraceRegistry.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/utils/CrnOperateProcessUtils.java 46 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/utils/DualCrnOperateProcessUtils.java 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/utils/StationOperateProcessUtils.java 23 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/WrkAnalysisMapper.xml 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/sql/20260322_add_wrk_analysis_page_and_inbound_status_migration.sql 332 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/static/js/wrkAnalysis/wrkAnalysis.js 477 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/wrkAnalysis/wrkAnalysis.html 443 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/WrkAnalysisController.java
New file
@@ -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));
    }
}
src/main/java/com/zy/asrs/entity/WrkAnalysis.java
New file
@@ -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;
}
src/main/java/com/zy/asrs/mapper/WrkAnalysisMapper.java
New file
@@ -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> {
}
src/main/java/com/zy/asrs/service/WrkAnalysisService.java
New file
@@ -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);
}
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());
src/main/java/com/zy/asrs/service/impl/WrkAnalysisServiceImpl.java
New file
@@ -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;
    }
}
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) {
src/main/java/com/zy/asrs/task/WrkAnalysisStationArrivalScanner.java
New file
@@ -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());
            }
        }
    }
}
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();
    }
}
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("移库任务 --- 保存工作档失败!");
            throw new CoolException("保存工作档失败");
        }
        wrkAnalysisService.initForTask(wrkMast);
        sourceLocMast.setLocSts("R");
        sourceLocMast.setModiTime(new Date());
@@ -358,6 +361,7 @@
            News.error("入库任务 --- 保存工作档失败!");
            throw new CoolException("保存工作档失败");
        }
        wrkAnalysisService.initForTask(wrkMast);
        locMast.setLocSts("S");
        locMast.setModiTime(new Date());
@@ -475,6 +479,7 @@
            News.error("出库任务 --- 保存工作档失败!");
            throw new CoolException("保存工作档失败");
        }
        wrkAnalysisService.initForTask(wrkMast);
        locMast.setLocSts("R");
        locMast.setModiTime(new Date());
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, "入库库存更新"),
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());
src/main/java/com/zy/core/plugin/GslProcess.java
@@ -65,10 +65,6 @@
        stationOperateProcessUtils.stationInExecute();
        //执行输送站点出库任务
        stationOperateProcessUtils.crnStationOutExecute();
        //检测输送站点出库任务执行完成
        stationOperateProcessUtils.stationOutExecuteFinish();
        // 检测任务转完成
        stationOperateProcessUtils.checkTaskToComplete();
        // 检测出库排序
        stationOperateProcessUtils.checkStationOutOrder();
        // 监控绕圈站点
src/main/java/com/zy/core/plugin/NormalProcess.java
@@ -75,8 +75,6 @@
        stationOperateProcessUtils.stationInExecute();
        //执行输送站点出库任务
        stationOperateProcessUtils.crnStationOutExecute();
        //检测输送站点出库任务执行完成
        stationOperateProcessUtils.stationOutExecuteFinish();
        //检测输送站点是否运行堵塞
        stationOperateProcessUtils.checkStationRunBlock();
src/main/java/com/zy/core/plugin/XiaosongProcess.java
@@ -80,10 +80,6 @@
        stationOperateProcessUtils.crnStationOutExecute();
        //执行双工位堆垛机输送站点出库任务
        stationOperateProcessUtils.dualCrnStationOutExecute();
        //检测输送站点出库任务执行完成
        stationOperateProcessUtils.stationOutExecuteFinish();
        // 检测任务转完成
        stationOperateProcessUtils.checkTaskToComplete();
        //检测输送站点是否运行堵塞
        stationOperateProcessUtils.checkStationRunBlock();
        //检测输送站点任务停留超时后重新计算路径
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;
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);
    }
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));
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);
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);
    }
src/main/resources/mapper/WrkAnalysisMapper.xml
New file
@@ -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>
src/main/resources/sql/20260322_add_wrk_analysis_page_and_inbound_status_migration.sql
New file
@@ -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;
src/main/webapp/static/js/wrkAnalysis/wrkAnalysis.js
New file
@@ -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";
            }
        }
    });
})();
src/main/webapp/views/wrkAnalysis/wrkAnalysis.html
New file
@@ -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>