自动化立体仓库 - WMS系统
#
zwl
17 小时以前 60f504b1f7308bcf8d1079a25d0db6922926ebae
#
11个文件已添加
953 ■■■■■ 已修改文件
skills/build-asrs-solutions/SKILL.md 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
skills/build-asrs-solutions/agents/openai.yaml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
skills/build-asrs-solutions/references/asrs-delivery-playbook.md 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/api/controller/params/StopOutTaskParams.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/param/ErpPakinReportParam.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/param/ErpPakoutReportParam.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/param/OpenOrderPakoutPauseParam.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/task/WorkErpReportScheduler.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/task/WorkOutErpReportScheduler.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/task/handler/WorkErpReportHandler.java 334 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/task/handler/WorkOutErpReportHandler.java 281 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
skills/build-asrs-solutions/SKILL.md
New file
@@ -0,0 +1,75 @@
---
name: build-asrs-solutions
description: Plan, design, implement, and communicate automated warehouse upper-level systems for ASRS projects, including WMS, WCS, ERP/MES integration, customer-facing requirement clarification, product scoping, interface definition, troubleshooting, and delivery coordination. Use when Codex supports 自动化立体仓库上位机系统、WMS/WCS 功能设计、ERP/MES/第三方系统对接、需求拆解、接口文档、流程梳理、客户沟通纪要、异常分析或项目交付推进。
---
# Build ASRS Solutions
## Overview
作为自动化立体仓库项目的方案型工程搭档,先把业务流、设备流和系统边界讲清,再输出可执行的设计、接口、排期和沟通材料。
始终把这个技能当作“全栈工程 + 产品经理 + 客户集成接口人”的工作辅助,而不是单纯的代码生成器。
## Core Workflow
1. 先建立上下文
- 明确项目阶段、行业场景、库区/巷道/工位结构、货物属性,以及入库、出库、补货、移库、盘点等作业类型。
- 列出参与系统和责任方:ERP、MES、WMS、WCS、PLC/设备层、PDA、报表平台、第三方系统。
- 确认主数据、订单、库存、任务、设备状态各自的 source of truth。
2. 再划清系统边界
- 明确哪些规则属于 ERP/MES,哪些属于 WMS,哪些属于 WCS,哪些属于 PLC 或设备控制器。
- 区分业务完成、库存完成、任务完成、设备完成,避免把不同层级状态压成一个状态字段。
- 为每个异步接口补齐触发时机、请求/回执、重试、幂等、超时、补偿、对账规则。
3. 把问题转换成交付物
- 当需求不清晰时,先输出澄清问题、边界假设、风险点,不要直接进入实现。
- 当任务偏设计时,输出责任矩阵、时序图、状态机、接口协议、字段映射、数据模型或 API 草案。
- 当任务偏执行时,输出开发拆分、联调计划、测试范围、上线切换和验收清单。
4. 用场景反推方案质量
- 走通正常流程、高峰流程、异常流程、人工干预、失败重试、回滚恢复。
- 检查库存差异、设备报警、消息重复、反馈延迟、上游阻塞、下游不可用时方案是否仍成立。
## Working Rules
- 先绑定具体对象:仓库、库区、设备、订单类型、任务类型、状态码、异常码、接口方向。
- 优先用表格、时序、状态迁移、责任矩阵表达跨系统逻辑,不要只给抽象描述。
- 把分析链路固定到业务单号、任务号、设备指令号、消息流水号。
- 先暴露假设和缺失输入,再给方案;不要把关键不确定性藏在实现细节里。
- 默认输出可以直接给研发、测试、实施、客户或第三方厂商评审的材料。
## Default Output Shapes
- 需求澄清:输出 `背景/范围/边界/关键问题/待确认项/风险`。
- 接口设计:输出 `接口目标/责任归属/触发时机/字段映射/幂等与重试/异常回传/验收口径`。
- 方案评审:输出 `现状/问题拆解/候选方案/取舍/风险/推荐方案`。
- 故障分析:输出 `现象/影响范围/链路定位/怀疑点/取证步骤/临时止血/永久修复/防回归措施`。
- 客户沟通:输出 `业务结论/系统边界/需对方提供内容/我方动作/时间点/未决问题`。
## Common Deliverables
- 需求澄清清单
- WMS/WCS/ERP/MES 责任矩阵
- 接口文档和字段映射表
- 业务流程图、异常流程图、状态机
- 数据库设计建议、SQL 草案、API 草案、事件模型
- 联调计划、测试案例、上线切换清单
- 客户会议纪要、行动项、对外澄清邮件草稿
- 故障复盘和根因分析报告
## Default Heuristics
- 先把库存差异看成“归属错误、时序错误、人工干预、幂等失效、补偿缺失”问题,而不是单纯数据错。
- 先把重复下发、任务卡死、设备反复报警看成“WMS/WCS/设备状态分叉”问题,直到证据证明不是。
- 为所有外部接口默认设计重试、去重、回查、对账和人工兜底。
- 把异常链路和人工恢复设计成产品能力,不要当作上线后临时补丁。
- 默认用“WMS 偏业务策略,WCS 偏执行编排”的原则建模;项目有偏差时再显式写明。
## Reference
当任务需要更细的调研提纲、责任矩阵模板、接口检查清单、故障排查顺序或会议纪要骨架时,读取 [references/asrs-delivery-playbook.md](references/asrs-delivery-playbook.md)。
skills/build-asrs-solutions/agents/openai.yaml
New file
@@ -0,0 +1,4 @@
interface:
  display_name: "ASRS Solutions"
  short_description: "WMS/WCS and ERP/MES delivery playbook"
  default_prompt: "Use $build-asrs-solutions to scope a warehouse-system request, map WMS/WCS and ERP/MES boundaries, and produce an actionable delivery plan."
skills/build-asrs-solutions/references/asrs-delivery-playbook.md
New file
@@ -0,0 +1,92 @@
# ASRS Delivery Playbook
## Table of Contents
- Context checklist
- Responsibility matrix template
- Integration checklist
- Scenario review set
- Troubleshooting workflow
- Communication templates
## Context Checklist
在开始设计或排查前,先补齐下面的信息:
- 项目阶段:售前、蓝图、开发、联调、上线、运维。
- 场景边界:整库、单库区、单设备线、单客户、单工厂,还是集团多基地。
- 货物属性:托盘、料箱、周转箱、单件,是否批次、效期、序列号管理。
- 作业类型:入库、出库、补货、移库、越库、盘点、冻结、解冻、空托管理。
- 设备组成:堆垛机、穿梭车、提升机、输送线、机械臂、AGV、RGV、电子标签、扫描器。
- 外部系统:ERP、MES、OMS、SRM、QMS、财务系统、BI、第三方仓配平台。
- 核心指标:吞吐、库存准确率、任务时效、波次完成率、设备可用率。
- 当前问题:需求新增、上线延期、库存不准、任务卡死、接口反复失败、客户口径不一致。
## Responsibility Matrix Template
用这个表先做责任归属,不要直接写实现。
| Domain | ERP/MES | WMS | WCS | Device/PLC | Notes |
| --- | --- | --- | --- | --- | --- |
| 主数据 | 主数据来源/审批 | 主数据落地和校验 | 设备侧必要映射 | 只保留设备执行必需配置 | 明确编码、版本、同步频率 |
| 单据/生产指令 | 下发业务需求 | 拆单、库存校验、波次/策略 | 接收执行任务 | 接收控制指令 | 明确谁可以撤销和改派 |
| 库存真值 | 财务或生产口径 | 仓储真值 | 缓存执行态 | 不作为长期真值 | 明确差异回写路径 |
| 任务状态 | 关注业务结果 | 管理业务任务状态 | 管理执行任务状态 | 返回动作结果 | 区分完成、失败、取消、挂起 |
| 异常处理 | 处理业务阻塞 | 处理库存和业务异常 | 处理设备和执行异常 | 输出报警和反馈 | 明确人工介入点 |
## Integration Checklist
每个接口都检查以下项目:
- 接口方向:谁发起、谁确认、谁回写、谁重试。
- 触发时机:创建、下发、开始、部分完成、完成、取消、异常、恢复。
- 唯一键:订单号、行号、任务号、容器号、设备任务号、消息流水号。
- 字段映射:编码体系、单位、状态码、时间格式、批次/效期/序列号口径。
- 幂等机制:幂等键、重复报文识别、去重窗口、重复回执处理。
- 重试策略:超时阈值、重试次数、退避规则、人工补发方式。
- 异常闭环:错误码、错误描述、通知对象、升级路径、恢复动作。
- 对账机制:日切、班次、增量回查、全量校验、补偿逻辑。
- 审计追踪:请求日志、回执日志、状态迁移日志、操作人、时间戳。
- 切换策略:存量数据初始化、灰度范围、回滚条件、双写或冻结窗口。
## Scenario Review Set
方案至少覆盖下面这些场景:
- 正常入库:预约到货、收货、质检、上架、库存确认。
- 正常出库:波次、分配、拣选、出库、装车、回写。
- 库存调整:盘点差异、冻结、解冻、报损、报溢。
- 人工干预:强制完成、取消任务、改库位、改容器、线边补录。
- 异常恢复:设备报警后重下、任务拆分、任务合并、断点续传。
- 跨系统失败:ERP 已回写但 WMS 未完成、WMS 已完成但 WCS 未回执、设备已动作但状态未同步。
## Troubleshooting Workflow
排查时按这个顺序推进:
1. 锁定业务主键:订单号、任务号、容器号、设备指令号。
2. 拉齐时间线:ERP/MES、WMS、WCS、设备日志的时间戳。
3. 找最后一次一致状态,确认问题发生在“下发前、接口中、执行中、回写中”的哪一段。
4. 检查是否有重复消息、漏回执、人工改数、时钟漂移、状态回滚失败。
5. 区分临时止血和永久修复:先恢复作业,再补监控、补校验、补补偿机制。
6. 输出可验证结论:现象、证据、根因假设、验证结果、修复动作、预防措施。
## Communication Templates
### Requirement Clarification
- 业务目标是什么,成功口径是什么。
- 哪个系统先发起,哪个系统作为最终确认口径。
- 哪些状态需要回传,哪些状态只在本系统内部可见。
- 异常时由谁处理,允许哪些人工操作。
- 客户必须提供哪些主数据、编码规则、接口样例、测试环境、验收案例。
### Meeting Summary
- 背景和目标
- 已确认范围
- 未确认范围
- 系统边界结论
- 双方行动项
- 风险和阻塞项
- 下次检查点
src/main/java/com/zy/api/controller/params/StopOutTaskParams.java
New file
@@ -0,0 +1,40 @@
package com.zy.api.controller.params;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@Data
@ApiModel(value = "StopOutTaskParams", description = "pause out task params")
public class StopOutTaskParams implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty("order no")
    private String orderNo;
    @ApiModelProperty("pause reason")
    private String reason;
    @ApiModelProperty("task list")
    private List<TaskItem> tasks = new ArrayList<>();
    @Data
    public static class TaskItem implements Serializable {
        private static final long serialVersionUID = 1L;
        @ApiModelProperty("task no")
        private String taskNo;
        @ApiModelProperty("station no")
        private String staNo;
        @ApiModelProperty("loc no")
        private String locNo;
    }
}
src/main/java/com/zy/asrs/entity/param/ErpPakinReportParam.java
New file
@@ -0,0 +1,21 @@
package com.zy.asrs.entity.param;
import lombok.Data;
@Data
public class ErpPakinReportParam {
    private String palletId;
    private Double anfme;
    private String locId;
    private Double weight;
    private String createTime;
    private String BizNo;
    private String startTime;
}
src/main/java/com/zy/asrs/entity/param/ErpPakoutReportParam.java
New file
@@ -0,0 +1,13 @@
package com.zy.asrs.entity.param;
import lombok.Data;
@Data
public class ErpPakoutReportParam {
    private String palletId;
    private String createTime;
    private String startTime;
}
src/main/java/com/zy/asrs/entity/param/OpenOrderPakoutPauseParam.java
New file
@@ -0,0 +1,11 @@
package com.zy.asrs.entity.param;
import lombok.Data;
@Data
public class OpenOrderPakoutPauseParam {
    private String orderNo;
    private String reason;
}
src/main/java/com/zy/asrs/task/WorkErpReportScheduler.java
New file
@@ -0,0 +1,41 @@
package com.zy.asrs.task;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.zy.asrs.entity.WrkMast;
import com.zy.asrs.service.WrkMastService;
import com.zy.asrs.task.core.ReturnT;
import com.zy.asrs.task.handler.WorkErpReportHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.List;
@Slf4j
@Component
public class WorkErpReportScheduler {
    @Autowired
    private WrkMastService wrkMastService;
    @Autowired
    private WorkErpReportHandler workErpReportHandler;
    @Scheduled(cron = "0/10 * * * * ? ")
    private void execute() {
        List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<WrkMast>()
                .eq("wrk_sts", WorkErpReportHandler.ERP_REPORT_PENDING_WRK_STS)
                .orderBy("io_time", true)
                .orderBy("wrk_no", true));
        if (wrkMasts.isEmpty()) {
            return;
        }
        for (WrkMast wrkMast : wrkMasts) {
            ReturnT<String> result = workErpReportHandler.start(wrkMast);
            if (!result.isSuccess()) {
                log.error("workNo={} erp report failed: {}", wrkMast.getWrkNo(), result.getMsg());
            }
        }
    }
}
src/main/java/com/zy/asrs/task/WorkOutErpReportScheduler.java
New file
@@ -0,0 +1,41 @@
package com.zy.asrs.task;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.zy.asrs.entity.WrkMast;
import com.zy.asrs.service.WrkMastService;
import com.zy.asrs.task.core.ReturnT;
import com.zy.asrs.task.handler.WorkOutErpReportHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.List;
@Slf4j
@Component
public class WorkOutErpReportScheduler {
    @Autowired
    private WrkMastService wrkMastService;
    @Autowired
    private WorkOutErpReportHandler workOutErpReportHandler;
    @Scheduled(cron = "0/10 * * * * ? ")
    private void execute() {
        List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<WrkMast>()
                .eq("wrk_sts", WorkOutErpReportHandler.ERP_REPORT_PENDING_WRK_STS)
                .orderBy("io_time", true)
                .orderBy("wrk_no", true));
        if (wrkMasts.isEmpty()) {
            return;
        }
        for (WrkMast wrkMast : wrkMasts) {
            ReturnT<String> result = workOutErpReportHandler.start(wrkMast);
            if (!result.isSuccess()) {
                log.error("workNo={} outbound erp report failed: {}", wrkMast.getWrkNo(), result.getMsg());
            }
        }
    }
}
src/main/java/com/zy/asrs/task/handler/WorkErpReportHandler.java
New file
@@ -0,0 +1,334 @@
package com.zy.asrs.task.handler;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.core.common.Cools;
import com.zy.asrs.entity.WaitPakin;
import com.zy.asrs.entity.WrkDetl;
import com.zy.asrs.entity.WrkMast;
import com.zy.asrs.entity.param.ErpPakinReportParam;
import com.zy.asrs.service.ApiLogService;
import com.zy.asrs.service.WaitPakinService;
import com.zy.asrs.service.WrkDetlService;
import com.zy.asrs.service.WrkMastService;
import com.zy.asrs.task.AbstractHandler;
import com.zy.asrs.task.core.ReturnT;
import com.zy.common.entity.Parameter;
import com.zy.common.utils.HttpHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
@Slf4j
@Service
public class WorkErpReportHandler extends AbstractHandler<String> {
    public static final long ERP_REPORT_PENDING_WRK_STS = 16L;
    public static final long ERP_REPORT_FINISHED_WRK_STS = 5L;
    public static final int ERP_REPORT_MAX_RETRY_TIMES = 3;
    public static final String ERP_REPORT_PENDING_FLAG = "P";
    public static final String ERP_REPORT_SUCCESS_FLAG = "Y";
    public static final String ERP_REPORT_FAIL_FLAG = "F";
    private static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
    @Autowired
    private WrkMastService wrkMastService;
    @Autowired
    private WrkDetlService wrkDetlService;
    @Autowired
    private WaitPakinService waitPakinService;
    @Autowired
    private ApiLogService apiLogService;
    @Value("${erp.switch.ErpReportOld}")
    private boolean erpReportOld;
    @Value("${erp.address.URL}")
    private String erpUrl;
    @Value("${erp.address.Inaddress}")
    private String erpInAddress;
    @Transactional(rollbackFor = Exception.class)
    public ReturnT<String> start(WrkMast source) {
        WrkMast wrkMast = wrkMastService.selectById(source.getWrkNo());
        if (wrkMast == null || !Long.valueOf(ERP_REPORT_PENDING_WRK_STS).equals(wrkMast.getWrkSts())) {
            return SUCCESS;
        }
        if (!isErpReportEnabled()) {
            finishReport(wrkMast, false, getRetryTimes(wrkMast), "ERP reporting is disabled", false);
            return FAIL.setMsg("ERP reporting is disabled");
        }
        List<WrkDetl> wrkDetls = wrkDetlService.selectByWrkNo(wrkMast.getWrkNo());
        if (Cools.isEmpty(wrkDetls)) {
            finishReport(wrkMast, false, getRetryTimes(wrkMast), "work details are empty", false);
            return FAIL.setMsg("work details are empty");
        }
        WaitPakin waitPakin = findWaitPakin(wrkMast.getBarcode());
        ErpPakinReportParam param = buildParam(wrkMast, wrkDetls, waitPakin);
        String request = JSON.toJSONString(param);
        String response = "";
        boolean success = false;
        String errorMsg = null;
        String requestUrl = buildRequestUrl();
        try {
            response = new HttpHandler.Builder()
                    .setUri(erpUrl)
                    .setPath(erpInAddress)
                    .setJson(request)
                    .build()
                    .doPost();
            success = isErpReportSuccess(response);
            if (!success) {
                errorMsg = extractErrorMsg(response);
            }
            finishReport(wrkMast, success, getRetryTimes(wrkMast), errorMsg, true);
        } catch (Exception e) {
            errorMsg = e.getMessage();
            finishReport(wrkMast, false, getRetryTimes(wrkMast), errorMsg, true);
        } finally {
            try {
                apiLogService.save(
                        "Inbound ERP Report",
                        requestUrl,
                        null,
                        "127.0.0.1",
                        request,
                        response,
                        success,
                        "workNo=" + wrkMast.getWrkNo()
                );
            } catch (Exception logEx) {
                log.error("save erp api log failed", logEx);
            }
        }
        if (success) {
            return SUCCESS;
        }
        return FAIL.setMsg(errorMsg);
    }
    private boolean isErpReportEnabled() {
        if (!erpReportOld) {
            return false;
        }
        String erpReport = Parameter.get().getErpReport();
        return Cools.isEmpty(erpReport) || "true".equalsIgnoreCase(erpReport);
    }
    private WaitPakin findWaitPakin(String barcode) {
        if (Cools.isEmpty(barcode)) {
            return null;
        }
        return waitPakinService.selectOne(new EntityWrapper<WaitPakin>().eq("zpallet", barcode));
    }
    private ErpPakinReportParam buildParam(WrkMast wrkMast, List<WrkDetl> wrkDetls, WaitPakin waitPakin) {
        ErpPakinReportParam param = new ErpPakinReportParam();
        param.setPalletId(resolvePalletId(wrkMast, wrkDetls));
        param.setAnfme(sumAnfme(wrkDetls));
        param.setLocId(wrkMast.getLocNo());
        param.setWeight(sumWeight(wrkDetls));
        param.setCreateTime(formatDate(resolveCreateTime(wrkMast)));
        param.setBizNo(resolveBizNo(wrkDetls, waitPakin));
        param.setStartTime(formatDate(resolveStartTime(wrkMast, waitPakin)));
        return param;
    }
    private String resolvePalletId(WrkMast wrkMast, List<WrkDetl> wrkDetls) {
        if (!Cools.isEmpty(wrkMast.getBarcode())) {
            return wrkMast.getBarcode();
        }
        for (WrkDetl wrkDetl : wrkDetls) {
            if (!Cools.isEmpty(wrkDetl.getZpallet())) {
                return wrkDetl.getZpallet();
            }
        }
        return null;
    }
    private Double sumAnfme(List<WrkDetl> wrkDetls) {
        double total = 0D;
        for (WrkDetl wrkDetl : wrkDetls) {
            if (!Cools.isEmpty(wrkDetl.getAnfme())) {
                total += wrkDetl.getAnfme();
            }
        }
        return total;
    }
    private Double sumWeight(List<WrkDetl> wrkDetls) {
        double total = 0D;
        for (WrkDetl wrkDetl : wrkDetls) {
            if (Cools.isEmpty(wrkDetl.getWeight())) {
                continue;
            }
            double qty = Cools.isEmpty(wrkDetl.getAnfme()) ? 1D : wrkDetl.getAnfme();
            total += wrkDetl.getWeight() * qty;
        }
        return total;
    }
    private Date resolveCreateTime(WrkMast wrkMast) {
        if (!Cools.isEmpty(wrkMast.getCrnEndTime())) {
            return wrkMast.getCrnEndTime();
        }
        if (!Cools.isEmpty(wrkMast.getModiTime())) {
            return wrkMast.getModiTime();
        }
        if (!Cools.isEmpty(wrkMast.getIoTime())) {
            return wrkMast.getIoTime();
        }
        return new Date();
    }
    private String resolveBizNo(List<WrkDetl> wrkDetls, WaitPakin waitPakin) {
        for (WrkDetl wrkDetl : wrkDetls) {
            if (!Cools.isEmpty(wrkDetl.getThreeCode())) {
                return wrkDetl.getThreeCode();
            }
        }
        return waitPakin == null ? null : waitPakin.getThreeCode();
    }
    private Date resolveStartTime(WrkMast wrkMast, WaitPakin waitPakin) {
        if (waitPakin != null && !Cools.isEmpty(waitPakin.getAppeTime())) {
            return waitPakin.getAppeTime();
        }
        if (!Cools.isEmpty(wrkMast.getAppeTime())) {
            return wrkMast.getAppeTime();
        }
        if (!Cools.isEmpty(wrkMast.getIoTime())) {
            return wrkMast.getIoTime();
        }
        return new Date();
    }
    private String formatDate(Date date) {
        if (date == null) {
            return null;
        }
        return new SimpleDateFormat(DATE_TIME_PATTERN).format(date);
    }
    private boolean isErpReportSuccess(String response) {
        if (Cools.isEmpty(response)) {
            return false;
        }
        try {
            JSONObject jsonObject = JSON.parseObject(response);
            if (jsonObject == null) {
                return false;
            }
            Boolean success = jsonObject.getBoolean("success");
            if (success != null) {
                return success;
            }
            Integer code = jsonObject.getInteger("code");
            if (code != null) {
                return code == 200 || code == 0;
            }
            Integer status = jsonObject.getInteger("status");
            if (status != null) {
                return status == 200 || status == 0;
            }
            String result = jsonObject.getString("result");
            if (!Cools.isEmpty(result)) {
                return "success".equalsIgnoreCase(result) || "ok".equalsIgnoreCase(result) || "true".equalsIgnoreCase(result);
            }
        } catch (Exception ignore) {
            return response.toLowerCase().contains("success") && response.toLowerCase().contains("true");
        }
        return false;
    }
    private String extractErrorMsg(String response) {
        if (Cools.isEmpty(response)) {
            return "empty erp response";
        }
        try {
            JSONObject jsonObject = JSON.parseObject(response);
            if (jsonObject == null) {
                return response;
            }
            String[] keys = new String[]{"msg", "message", "error", "errMsg"};
            for (String key : keys) {
                String value = jsonObject.getString(key);
                if (!Cools.isEmpty(value)) {
                    return value;
                }
            }
        } catch (Exception ignore) {
        }
        return response;
    }
    private int getRetryTimes(WrkMast wrkMast) {
        if (wrkMast.getExpTime() == null) {
            return 0;
        }
        return wrkMast.getExpTime().intValue();
    }
    private void finishReport(WrkMast wrkMast, boolean success, int currentRetryTimes, String errorMsg, boolean countCurrentAttempt) {
        int retryTimes = currentRetryTimes + (countCurrentAttempt ? 1 : 0);
        Date now = new Date();
        wrkMast.setExpTime((double) retryTimes);
        wrkMast.setModiTime(now);
        if (success) {
            wrkMast.setWrkSts(ERP_REPORT_FINISHED_WRK_STS);
            wrkMast.setLogMk(ERP_REPORT_SUCCESS_FLAG);
            wrkMast.setLogErrMemo(null);
            wrkMast.setLogErrTime(null);
        } else {
            wrkMast.setLogErrMemo(truncate(errorMsg, 500));
            wrkMast.setLogErrTime(now);
            if (retryTimes >= ERP_REPORT_MAX_RETRY_TIMES || !countCurrentAttempt) {
                wrkMast.setWrkSts(ERP_REPORT_FINISHED_WRK_STS);
                wrkMast.setLogMk(ERP_REPORT_FAIL_FLAG);
            } else {
                wrkMast.setWrkSts(ERP_REPORT_PENDING_WRK_STS);
                wrkMast.setLogMk(ERP_REPORT_PENDING_FLAG);
            }
        }
        if (!wrkMastService.updateById(wrkMast)) {
            throw new IllegalStateException("update erp report status failed, workNo=" + wrkMast.getWrkNo());
        }
    }
    private String truncate(String message, int maxLength) {
        if (message == null || message.length() <= maxLength) {
            return message;
        }
        return message.substring(0, maxLength);
    }
    private String buildRequestUrl() {
        if (Cools.isEmpty(erpUrl)) {
            return erpInAddress;
        }
        if (erpInAddress == null) {
            return erpUrl;
        }
        if (erpUrl.endsWith("/") && erpInAddress.startsWith("/")) {
            return erpUrl + erpInAddress.substring(1);
        }
        if (!erpUrl.endsWith("/") && !erpInAddress.startsWith("/")) {
            return erpUrl + "/" + erpInAddress;
        }
        return erpUrl + erpInAddress;
    }
}
src/main/java/com/zy/asrs/task/handler/WorkOutErpReportHandler.java
New file
@@ -0,0 +1,281 @@
package com.zy.asrs.task.handler;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.core.common.Cools;
import com.zy.asrs.entity.WrkDetl;
import com.zy.asrs.entity.WrkMast;
import com.zy.asrs.entity.param.ErpPakoutReportParam;
import com.zy.asrs.service.ApiLogService;
import com.zy.asrs.service.WrkDetlService;
import com.zy.asrs.service.WrkMastService;
import com.zy.asrs.task.AbstractHandler;
import com.zy.asrs.task.core.ReturnT;
import com.zy.common.entity.Parameter;
import com.zy.common.utils.HttpHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
@Slf4j
@Service
public class WorkOutErpReportHandler extends AbstractHandler<String> {
    public static final long ERP_REPORT_PENDING_WRK_STS = 17L;
    public static final long ERP_REPORT_FINISHED_WRK_STS = 15L;
    public static final int ERP_REPORT_MAX_RETRY_TIMES = 3;
    public static final String ERP_REPORT_PENDING_FLAG = "P";
    public static final String ERP_REPORT_SUCCESS_FLAG = "Y";
    public static final String ERP_REPORT_FAIL_FLAG = "F";
    private static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
    @Autowired
    private WrkMastService wrkMastService;
    @Autowired
    private WrkDetlService wrkDetlService;
    @Autowired
    private ApiLogService apiLogService;
    @Value("${erp.switch.ErpReportOld}")
    private boolean erpReportOld;
    @Value("${erp.address.URL}")
    private String erpUrl;
    @Value("${erp.address.Outaddress}")
    private String erpOutAddress;
    @Transactional(rollbackFor = Exception.class)
    public ReturnT<String> start(WrkMast source) {
        WrkMast wrkMast = wrkMastService.selectById(source.getWrkNo());
        if (wrkMast == null || !Long.valueOf(ERP_REPORT_PENDING_WRK_STS).equals(wrkMast.getWrkSts())) {
            return SUCCESS;
        }
        if (!isErpReportEnabled()) {
            finishReport(wrkMast, false, getRetryTimes(wrkMast), "ERP reporting is disabled", false);
            return FAIL.setMsg("ERP reporting is disabled");
        }
        ErpPakoutReportParam param = buildParam(wrkMast);
        String request = JSON.toJSONString(param);
        String response = "";
        boolean success = false;
        String errorMsg = null;
        String requestUrl = buildRequestUrl();
        try {
            response = new HttpHandler.Builder()
                    .setUri(erpUrl)
                    .setPath(erpOutAddress)
                    .setJson(request)
                    .build()
                    .doPost();
            success = isErpReportSuccess(response);
            if (!success) {
                errorMsg = extractErrorMsg(response);
            }
            finishReport(wrkMast, success, getRetryTimes(wrkMast), errorMsg, true);
        } catch (Exception e) {
            errorMsg = e.getMessage();
            finishReport(wrkMast, false, getRetryTimes(wrkMast), errorMsg, true);
        } finally {
            try {
                apiLogService.save(
                        "Outbound ERP Report",
                        requestUrl,
                        null,
                        "127.0.0.1",
                        request,
                        response,
                        success,
                        "workNo=" + wrkMast.getWrkNo()
                );
            } catch (Exception logEx) {
                log.error("save outbound erp api log failed", logEx);
            }
        }
        if (success) {
            return SUCCESS;
        }
        return FAIL.setMsg(errorMsg);
    }
    private boolean isErpReportEnabled() {
        if (!erpReportOld) {
            return false;
        }
        String erpReport = Parameter.get().getErpReport();
        return Cools.isEmpty(erpReport) || "true".equalsIgnoreCase(erpReport);
    }
    private ErpPakoutReportParam buildParam(WrkMast wrkMast) {
        ErpPakoutReportParam param = new ErpPakoutReportParam();
        param.setPalletId(resolvePalletId(wrkMast));
        param.setCreateTime(formatDate(resolveCreateTime(wrkMast)));
        param.setStartTime(formatDate(resolveStartTime(wrkMast)));
        return param;
    }
    private String resolvePalletId(WrkMast wrkMast) {
        if (!Cools.isEmpty(wrkMast.getBarcode())) {
            return wrkMast.getBarcode();
        }
        List<WrkDetl> wrkDetls = wrkDetlService.selectByWrkNo(wrkMast.getWrkNo());
        for (WrkDetl wrkDetl : wrkDetls) {
            if (!Cools.isEmpty(wrkDetl.getZpallet())) {
                return wrkDetl.getZpallet();
            }
        }
        return null;
    }
    private Date resolveCreateTime(WrkMast wrkMast) {
        if (!Cools.isEmpty(wrkMast.getCrnEndTime())) {
            return wrkMast.getCrnEndTime();
        }
        if (!Cools.isEmpty(wrkMast.getModiTime())) {
            return wrkMast.getModiTime();
        }
        if (!Cools.isEmpty(wrkMast.getIoTime())) {
            return wrkMast.getIoTime();
        }
        return new Date();
    }
    private Date resolveStartTime(WrkMast wrkMast) {
        if (!Cools.isEmpty(wrkMast.getCrnStrTime())) {
            return wrkMast.getCrnStrTime();
        }
        if (!Cools.isEmpty(wrkMast.getPlcStrTime())) {
            return wrkMast.getPlcStrTime();
        }
        if (!Cools.isEmpty(wrkMast.getIoTime())) {
            return wrkMast.getIoTime();
        }
        return new Date();
    }
    private String formatDate(Date date) {
        if (date == null) {
            return null;
        }
        return new SimpleDateFormat(DATE_TIME_PATTERN).format(date);
    }
    private boolean isErpReportSuccess(String response) {
        if (Cools.isEmpty(response)) {
            return false;
        }
        try {
            JSONObject jsonObject = JSON.parseObject(response);
            if (jsonObject == null) {
                return false;
            }
            Boolean success = jsonObject.getBoolean("success");
            if (success != null) {
                return success;
            }
            Integer code = jsonObject.getInteger("code");
            if (code != null) {
                return code == 200 || code == 0;
            }
            Integer status = jsonObject.getInteger("status");
            if (status != null) {
                return status == 200 || status == 0;
            }
            String result = jsonObject.getString("result");
            if (!Cools.isEmpty(result)) {
                return "success".equalsIgnoreCase(result) || "ok".equalsIgnoreCase(result) || "true".equalsIgnoreCase(result);
            }
        } catch (Exception ignore) {
            return response.toLowerCase().contains("success") && response.toLowerCase().contains("true");
        }
        return false;
    }
    private String extractErrorMsg(String response) {
        if (Cools.isEmpty(response)) {
            return "empty erp response";
        }
        try {
            JSONObject jsonObject = JSON.parseObject(response);
            if (jsonObject == null) {
                return response;
            }
            String[] keys = new String[]{"msg", "message", "error", "errMsg"};
            for (String key : keys) {
                String value = jsonObject.getString(key);
                if (!Cools.isEmpty(value)) {
                    return value;
                }
            }
        } catch (Exception ignore) {
        }
        return response;
    }
    private int getRetryTimes(WrkMast wrkMast) {
        if (wrkMast.getExpTime() == null) {
            return 0;
        }
        return wrkMast.getExpTime().intValue();
    }
    private void finishReport(WrkMast wrkMast, boolean success, int currentRetryTimes, String errorMsg, boolean countCurrentAttempt) {
        int retryTimes = currentRetryTimes + (countCurrentAttempt ? 1 : 0);
        Date now = new Date();
        wrkMast.setExpTime((double) retryTimes);
        wrkMast.setModiTime(now);
        if (success) {
            wrkMast.setWrkSts(ERP_REPORT_FINISHED_WRK_STS);
            wrkMast.setLogMk(ERP_REPORT_SUCCESS_FLAG);
            wrkMast.setLogErrMemo(null);
            wrkMast.setLogErrTime(null);
        } else {
            wrkMast.setLogErrMemo(truncate(errorMsg, 500));
            wrkMast.setLogErrTime(now);
            if (retryTimes >= ERP_REPORT_MAX_RETRY_TIMES || !countCurrentAttempt) {
                wrkMast.setWrkSts(ERP_REPORT_FINISHED_WRK_STS);
                wrkMast.setLogMk(ERP_REPORT_FAIL_FLAG);
            } else {
                wrkMast.setWrkSts(ERP_REPORT_PENDING_WRK_STS);
                wrkMast.setLogMk(ERP_REPORT_PENDING_FLAG);
            }
        }
        if (!wrkMastService.updateById(wrkMast)) {
            throw new IllegalStateException("update outbound erp report status failed, workNo=" + wrkMast.getWrkNo());
        }
    }
    private String truncate(String message, int maxLength) {
        if (message == null || message.length() <= maxLength) {
            return message;
        }
        return message.substring(0, maxLength);
    }
    private String buildRequestUrl() {
        if (Cools.isEmpty(erpUrl)) {
            return erpOutAddress;
        }
        if (erpOutAddress == null) {
            return erpUrl;
        }
        if (erpUrl.endsWith("/") && erpOutAddress.startsWith("/")) {
            return erpUrl + erpOutAddress.substring(1);
        }
        if (!erpUrl.endsWith("/") && !erpOutAddress.startsWith("/")) {
            return erpUrl + "/" + erpOutAddress;
        }
        return erpUrl + erpOutAddress;
    }
}