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; } }