From 46baa976095fae88bce4dce969764049156cbadd Mon Sep 17 00:00:00 2001 From: luoweijian <1329394916@qq.com> Date: Mon, 30 Mar 2026 15:26:53 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=8E=A5=E5=8F=A3=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/sqldialects.xml | 6 ++ .../logistics/config/FixedRuleProperties.java | 17 ++++ .../logistics/config/SfApiProperties.java | 4 + .../logistics/config/WebDavProperties.java | 2 + .../domain/enums/SfContactTypeEnum.java | 18 ++++ .../domain/enums/SfExpressTypeEnum.java | 30 ++++++ .../domain/enums/SfPayMethodEnum.java | 30 ++++++ .../enums/ShipmentOrderInfoFieldEnum.java | 27 ++++++ .../domain/scheduler/ApiRetryJob.java | 10 +- .../domain/service/SfApiService.java | 63 +++++++++++++ .../domain/service/WebDavService.java | 34 ++++--- .../domain/service/base/ErpService.java | 5 + .../domain/service/base/ErpServiceImpl.java | 37 +++++++- .../handler/ErpUpdateWaybillHandler.java | 55 +++++++++++ .../handler/SfCreateOrderHandler.java | 93 +++++++++++++++---- .../handler/SfDownloadWaybillHandler.java | 71 ++++++++++++++ src/main/resources/application.yml | 7 +- 17 files changed, 474 insertions(+), 35 deletions(-) create mode 100644 .idea/sqldialects.xml create mode 100644 src/main/java/com/project/logistics/config/FixedRuleProperties.java create mode 100644 src/main/java/com/project/logistics/domain/enums/SfContactTypeEnum.java create mode 100644 src/main/java/com/project/logistics/domain/enums/SfExpressTypeEnum.java create mode 100644 src/main/java/com/project/logistics/domain/enums/SfPayMethodEnum.java create mode 100644 src/main/java/com/project/logistics/domain/enums/ShipmentOrderInfoFieldEnum.java create mode 100644 src/main/java/com/project/logistics/domain/strategy/handler/ErpUpdateWaybillHandler.java create mode 100644 src/main/java/com/project/logistics/domain/strategy/handler/SfDownloadWaybillHandler.java diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..23abb4d --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/main/java/com/project/logistics/config/FixedRuleProperties.java b/src/main/java/com/project/logistics/config/FixedRuleProperties.java new file mode 100644 index 0000000..e4db647 --- /dev/null +++ b/src/main/java/com/project/logistics/config/FixedRuleProperties.java @@ -0,0 +1,17 @@ +package com.project.logistics.config; + + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "fixed-rule") +public class FixedRuleProperties { + private String senderPayContact; + private String senderMobile; + private String senderAddress; + private String monthlyCard; + +} diff --git a/src/main/java/com/project/logistics/config/SfApiProperties.java b/src/main/java/com/project/logistics/config/SfApiProperties.java index 03b712d..01a587a 100644 --- a/src/main/java/com/project/logistics/config/SfApiProperties.java +++ b/src/main/java/com/project/logistics/config/SfApiProperties.java @@ -44,4 +44,8 @@ public class SfApiProperties { * 正式环境: 由顺丰提供 */ private String channelCode; + /** + * 面单模板编码 + */ + private String templateCode; } \ No newline at end of file diff --git a/src/main/java/com/project/logistics/config/WebDavProperties.java b/src/main/java/com/project/logistics/config/WebDavProperties.java index aea74d2..b4dd11c 100644 --- a/src/main/java/com/project/logistics/config/WebDavProperties.java +++ b/src/main/java/com/project/logistics/config/WebDavProperties.java @@ -36,6 +36,8 @@ public class WebDavProperties { */ private String processedFolderName = "/已自动下单出货单"; + private String sfWaybillFolderName = "/顺丰面单"; + /** * 签收回单(POD)的存放根路径 * 例如: /U9_Orders/Signed_Receipts diff --git a/src/main/java/com/project/logistics/domain/enums/SfContactTypeEnum.java b/src/main/java/com/project/logistics/domain/enums/SfContactTypeEnum.java new file mode 100644 index 0000000..725ae7c --- /dev/null +++ b/src/main/java/com/project/logistics/domain/enums/SfContactTypeEnum.java @@ -0,0 +1,18 @@ +package com.project.logistics.domain.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum SfContactTypeEnum { + + SENDER(1, "寄件方"), + RECIPIENT(2, "到件方"); + + @EnumValue + private final Integer code; + private final String desc; + +} diff --git a/src/main/java/com/project/logistics/domain/enums/SfExpressTypeEnum.java b/src/main/java/com/project/logistics/domain/enums/SfExpressTypeEnum.java new file mode 100644 index 0000000..8b62ae0 --- /dev/null +++ b/src/main/java/com/project/logistics/domain/enums/SfExpressTypeEnum.java @@ -0,0 +1,30 @@ +package com.project.logistics.domain.enums; + +import cn.hutool.core.util.StrUtil; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum SfExpressTypeEnum { + SF_GROUND_FREIGHT_EXPRESS(1,"顺丰卡航" ,255) , + SF_STANDARD_EXPRESS(2, "顺丰标快" , 2) , + SF_EXPRESS(3, "顺丰特快" , 1); + + + private final Integer code; + private final String desc; + + private final Integer sfCode; + + public static SfExpressTypeEnum getSfStandardExpressByCode(String str) { + for (SfExpressTypeEnum value : SfExpressTypeEnum.values()) { + if (StrUtil.equals(value.code.toString() , str) || + StrUtil.equals(value.getDesc() , str)) { + return value; + } + } + return null; + } + +} diff --git a/src/main/java/com/project/logistics/domain/enums/SfPayMethodEnum.java b/src/main/java/com/project/logistics/domain/enums/SfPayMethodEnum.java new file mode 100644 index 0000000..13171ed --- /dev/null +++ b/src/main/java/com/project/logistics/domain/enums/SfPayMethodEnum.java @@ -0,0 +1,30 @@ +package com.project.logistics.domain.enums; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.annotation.EnumValue; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum SfPayMethodEnum { + SENDER_PAY(1, "寄付" , "公司"), + RECIPIENT_PAY(2, "到付" , "客户"); + + @EnumValue + private final Integer code; + private final String desc; + + private final String u9Desc; + + + public static SfPayMethodEnum getSenderPayByU9Desc(String desc) { + for (SfPayMethodEnum value : SfPayMethodEnum.values()) { + if (StrUtil.equals(value.getU9Desc() , desc)) { + return value; + } + } + return null; + } + +} diff --git a/src/main/java/com/project/logistics/domain/enums/ShipmentOrderInfoFieldEnum.java b/src/main/java/com/project/logistics/domain/enums/ShipmentOrderInfoFieldEnum.java new file mode 100644 index 0000000..43a084b --- /dev/null +++ b/src/main/java/com/project/logistics/domain/enums/ShipmentOrderInfoFieldEnum.java @@ -0,0 +1,27 @@ +package com.project.logistics.domain.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ShipmentOrderInfoFieldEnum { + + orderNo("DocNo", "出货单号"), + salesman("DescFlexField_PrivateDescSeg15", "CRM业务员") , + transportMethod("DescFlexField_PrivateDescSeg4" , "运输方式及货场名称") , + piece("DescFlexField_PrivateDescSeg2" , "物流件数") , + recipientContact("DescFlexField_PrivateDescSeg8" , "收货人") , + recipientMobile("DescFlexField_PrivateDescSeg5" , "收货人联系电话") , + recipientAddress("DescFlexField_PrivateDescSeg7" , "收货地址") , + payer("DescFlexField_PrivateDescSeg10" , "运费承担") , + waybillNo("DescFlexField_PrivateDescSeg1" , "货运单号") , + expressType("DescFlexField_PrivateDescSeg20","寄付方式") , + fee("DescFlexField_PrivateDescSeg3" , "运费") , + resourceCode("DescFlexField_PrivateDescSeg18" , "资源编码"), + ; + + private final String fieldName; + + private final String desc; +} diff --git a/src/main/java/com/project/logistics/domain/scheduler/ApiRetryJob.java b/src/main/java/com/project/logistics/domain/scheduler/ApiRetryJob.java index 8214623..2d5d108 100644 --- a/src/main/java/com/project/logistics/domain/scheduler/ApiRetryJob.java +++ b/src/main/java/com/project/logistics/domain/scheduler/ApiRetryJob.java @@ -82,10 +82,16 @@ public class ApiRetryJob { handler.handle(task, order); // 如果没抛异常,说明执行成功 - task.setTaskStatus(TaskStatusEnum.SUCCESS.getCode()); - task.setErrorMessage(""); + if (TaskStatusEnum.EXECUTING.getCode().equals(task.getTaskStatus())) { + task.setTaskStatus(TaskStatusEnum.SUCCESS.getCode()); + task.setErrorMessage(""); + log.info(">>> 任务 [{}] 执行成功,单号: {}", task.getActionCode(), task.getOrderNo()); + } else if (TaskStatusEnum.CANCELLED.getCode().equals(task.getTaskStatus())) { + log.warn(">>> 任务 [{}] 已被业务主动终止,原因: {}", task.getActionCode(), task.getErrorMessage()); + } apiRetryTaskService.updateById(task); + } catch (Exception e) { handleTaskFailure(task, e); } diff --git a/src/main/java/com/project/logistics/domain/service/SfApiService.java b/src/main/java/com/project/logistics/domain/service/SfApiService.java index 059f8b7..85dcdc0 100644 --- a/src/main/java/com/project/logistics/domain/service/SfApiService.java +++ b/src/main/java/com/project/logistics/domain/service/SfApiService.java @@ -4,7 +4,9 @@ import cn.hutool.core.codec.Base64; import cn.hutool.core.util.IdUtil; import cn.hutool.crypto.digest.DigestUtil; import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; +import com.jayway.jsonpath.JsonPath; import com.project.logistics.config.SfApiProperties; import com.project.logistics.domain.utils.SfTokenUtil; import lombok.extern.slf4j.Slf4j; @@ -134,4 +136,65 @@ public class SfApiService { } return resultData; } + + public Map getWaybillPrintFile(String waybillNo) { + JSONObject msgData = new JSONObject(); + msgData.put("version", "2.0"); + msgData.put("templateCode", "fm_76130_standard_" + sfApiProperties.getPartnerId()); + msgData.put("fileType", "pdf"); + msgData.put("sync", true); // 同步获取 + + JSONArray documents = new JSONArray(); + JSONObject doc = new JSONObject(); + doc.put("masterWaybillNo", waybillNo); + documents.add(doc); + msgData.put("documents", documents); + + String response = callSfApi("COM_RECE_CLOUD_PRINT_WAYBILLS", msgData, null); + + String apiCode = JsonPath.read(response, "$.apiResultCode"); + if ("A1000".equals(apiCode)) { + String innerJson = JsonPath.read(response, "$.apiResultData"); + boolean success = JsonPath.read(innerJson, "$.success"); + + if (success) { + // 1. 同时读取 URL 和 Token + String pdfUrl = JsonPath.read(innerJson, "$.obj.files[0].url"); + String token = JsonPath.read(innerJson, "$.obj.files[0].token"); + + Map result = new HashMap<>(); + result.put("url", pdfUrl); + result.put("token", token); + return result; + } + } + throw new RuntimeException("顺丰面单获取失败: " + response); + } + /** + * 5.2 【面单文件下载专用】:携带 X-Auth-token 进行 HTTP GET + */ + public byte[] downloadWaybillPdfWithAuth(String url, String fileToken) { + try { + HttpHeaders headers = new HttpHeaders(); + // 关键点:按照文档要求,注入 X-Auth-token 请求头 + headers.set("X-Auth-token", fileToken); + + HttpEntity requestEntity = new HttpEntity<>(headers); + + // 使用 RestTemplate 的 exchange 方法发起 GET,并接收 byte[] + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + requestEntity, + byte[].class + ); + + log.info(">>> 面单 PDF 下载成功,大小 {} KB", response.getBody().length / 1024); + return response.getBody(); + + } catch (Exception e) { + log.error(">>> 面单文件下载异常, URL: {}", url, e); + throw new RuntimeException("WAYBILL_DOWNLOAD_HTTP_ERROR"); + } + } } \ No newline at end of file diff --git a/src/main/java/com/project/logistics/domain/service/WebDavService.java b/src/main/java/com/project/logistics/domain/service/WebDavService.java index 45d2508..efd7fe5 100644 --- a/src/main/java/com/project/logistics/domain/service/WebDavService.java +++ b/src/main/java/com/project/logistics/domain/service/WebDavService.java @@ -3,6 +3,7 @@ package com.project.logistics.domain.service; import cn.hutool.core.io.IoUtil; import cn.hutool.core.net.url.UrlBuilder; import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.URLUtil; import com.github.sardine.DavResource; import com.github.sardine.Sardine; @@ -76,28 +77,37 @@ public class WebDavService { public void moveFileToProcessed(String fullFilePath) throws IOException { Sardine sardine = getSardine(); - // 1. 从完整路径中截取 目录 和 文件名 - int lastSlashIdx = fullFilePath.lastIndexOf("/"); - if (lastSlashIdx == -1) throw new IllegalArgumentException("文件路径格式错误: " + fullFilePath); + // 1. 构造源文件完整 URL + String sourceUrl = getEncodedUrl(properties.getUrl() + "/" + fullFilePath); - String directoryPath = fullFilePath.substring(0, lastSlashIdx + 1); // 包含末尾斜杠 - String fileName = fullFilePath.substring(lastSlashIdx + 1); + // 2. 构造目标路径:将路径中的 shipmentRoot 替换为已下单根目录名 + // 使用 replaceFirst 确保只替换开头的根目录 + String shipmentRootWithSlash = StrUtil.addPrefixIfNot(properties.getShipmentRoot(), "/"); + String processedRootWithSlash = StrUtil.addPrefixIfNot(properties.getProcessedFolderName(), "/"); - // 2. 构造源 URL 和 目标 URL - String sourceUrl = getEncodedUrl(properties.getUrl() + "/" + fullFilePath); - String targetFolderUrl = getEncodedUrl(properties.getUrl() + "/" + directoryPath + properties.getProcessedFolderName() + "/"); - String targetFileUrl = getEncodedUrl(properties.getUrl() + "/" + directoryPath + properties.getProcessedFolderName() + "/" + fileName); + // 得到目标相对路径: /已自动下单出货单/2026年/2026年3月/2026年3月30日/单号.pdf + String targetRelativePath = fullFilePath.replaceFirst(shipmentRootWithSlash, processedRootWithSlash); + + String targetFileUrl = getEncodedUrl(properties.getUrl() + "/" + targetRelativePath); + // 获取目标文件夹 URL (截取到最后一个斜杠) + String targetFolderUrl = targetFileUrl.substring(0, targetFileUrl.lastIndexOf("/") + 1); + // 3. 检查源文件是否存在 if (!sardine.exists(sourceUrl)) { - log.warn(">>> 移动跳过: 源文件已不存在 {}", URLUtil.decode(sourceUrl)); + log.warn(">>> 移动跳过: 源文件已不存在(可能已移走) {}", URLUtil.decode(sourceUrl)); return; } + // 4. 【关键自动化】确保目标根目录下的“年/月/日”层级全部存在,不存在则自动创建 ensureDirectoryExists(sardine, targetFolderUrl); + + // 5. 执行物理移动 sardine.move(sourceUrl, targetFileUrl); - log.info(">>> 文件已移入归档目录: {}", URLUtil.decode(targetFileUrl)); - } + log.info(">>> 文件归档成功!"); + log.info(" 源: {}", URLUtil.decode(sourceUrl)); + log.info(" 目: {}", URLUtil.decode(targetFileUrl)); + } /** * 4. 【通用】上传文件到指定路径 * @param targetFilePath 完整的目标文件路径 (如: /签收回单/2026年/.../单号_POD.pdf) diff --git a/src/main/java/com/project/logistics/domain/service/base/ErpService.java b/src/main/java/com/project/logistics/domain/service/base/ErpService.java index 55382b5..5631bba 100644 --- a/src/main/java/com/project/logistics/domain/service/base/ErpService.java +++ b/src/main/java/com/project/logistics/domain/service/base/ErpService.java @@ -6,4 +6,9 @@ public interface ErpService { JSONObject getShipmentOrderInfo(String orderNo) throws Exception; + + /** + * 回写运单号到 U9 + */ + void updateWaybillToErp(String orderNo, String waybillNo , String resourceCode) throws Exception; } diff --git a/src/main/java/com/project/logistics/domain/service/base/ErpServiceImpl.java b/src/main/java/com/project/logistics/domain/service/base/ErpServiceImpl.java index f80508b..6d52b3a 100644 --- a/src/main/java/com/project/logistics/domain/service/base/ErpServiceImpl.java +++ b/src/main/java/com/project/logistics/domain/service/base/ErpServiceImpl.java @@ -1,6 +1,7 @@ package com.project.logistics.domain.service.base; import cn.hutool.json.JSONObject; +import com.project.logistics.domain.enums.ShipmentOrderInfoFieldEnum; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -23,7 +24,18 @@ public class ErpServiceImpl implements ErpService { log.info(">>> 正在通过专属 U9 管道查询单据: {}", orderNo); // 这里不再需要 @DS 注解,也不需要 push/poll,因为 u9JdbcTemplate 内部绑定的就是 SQL Server - String sql = "SELECT TOP 1 * FROM SM_Ship WITH(NOLOCK) WHERE DocNo = ?"; + String sql = "SELECT TOP 1 " + + String.format("%s as %s," , ShipmentOrderInfoFieldEnum.orderNo.getFieldName() , ShipmentOrderInfoFieldEnum.orderNo.name()) + + String.format("%s as %s," , ShipmentOrderInfoFieldEnum.salesman.getFieldName() , ShipmentOrderInfoFieldEnum.salesman.name()) + + String.format("%s as %s," , ShipmentOrderInfoFieldEnum.transportMethod.getFieldName() , ShipmentOrderInfoFieldEnum.transportMethod.name()) + + String.format("%s as %s," , ShipmentOrderInfoFieldEnum.piece.getFieldName() , ShipmentOrderInfoFieldEnum.piece.name()) + + String.format("%s as %s," , ShipmentOrderInfoFieldEnum.recipientContact.getFieldName() , ShipmentOrderInfoFieldEnum.recipientContact.name()) + + String.format("%s as %s," , ShipmentOrderInfoFieldEnum.recipientMobile.getFieldName() , ShipmentOrderInfoFieldEnum.recipientMobile.name()) + + String.format("%s as %s," , ShipmentOrderInfoFieldEnum.recipientAddress.getFieldName() , ShipmentOrderInfoFieldEnum.recipientAddress.name()) + + String.format("%s as %s," , ShipmentOrderInfoFieldEnum.payer.getFieldName() , ShipmentOrderInfoFieldEnum.payer.name()) + + String.format("%s as %s," , ShipmentOrderInfoFieldEnum.waybillNo.getFieldName() , ShipmentOrderInfoFieldEnum.waybillNo.name()) + + String.format("%s as %s" , ShipmentOrderInfoFieldEnum.expressType.getFieldName() , ShipmentOrderInfoFieldEnum.expressType.name()) + + " FROM SM_Ship WITH(NOLOCK) WHERE DocNo = ?"; try { Map result = u9JdbcTemplate.queryForMap(sql, orderNo); @@ -35,4 +47,27 @@ public class ErpServiceImpl implements ErpService { throw e; } } + + @Override + public void updateWaybillToErp(String orderNo, String waybillNo, String resourceCode) throws Exception { + log.info(">>> [U9回写] 准备将运单号 {} 写入 U9 单据 {}", waybillNo, orderNo); + + // SQL 说明:根据 U9 实际情况,更新扩展字段(如 DescFlexField_Pub_1)和同步状态 + String sql = String.format("UPDATE SM_Ship SET %s = ?,%s = ? WHERE DocNo = ?" , ShipmentOrderInfoFieldEnum.waybillNo.getFieldName() , + ShipmentOrderInfoFieldEnum.resourceCode.getFieldName()); + + try { + int rows = u9JdbcTemplate.update(sql, waybillNo , resourceCode , orderNo); + + if (rows > 0) { + log.info(">>> [U9回写成功] 单号: {}", orderNo); + } else { + // 如果更新了 0 行,说明 U9 里的单号不存在,这在重试中可能是致命的 + throw new RuntimeException("U9 中未找到对应的出货单据,回写失败"); + } + } catch (Exception e) { + log.error(">>> [U9回写异常]: {}", e.getMessage()); + throw e; // 抛出异常,触发 ApiRetryJob 的重试机制 + } + } } diff --git a/src/main/java/com/project/logistics/domain/strategy/handler/ErpUpdateWaybillHandler.java b/src/main/java/com/project/logistics/domain/strategy/handler/ErpUpdateWaybillHandler.java new file mode 100644 index 0000000..1cc46a4 --- /dev/null +++ b/src/main/java/com/project/logistics/domain/strategy/handler/ErpUpdateWaybillHandler.java @@ -0,0 +1,55 @@ +package com.project.logistics.domain.strategy.handler; + +import cn.hutool.core.util.StrUtil; +import com.project.logistics.domain.entity.ApiRetryTaskEntity; +import com.project.logistics.domain.entity.LogisticsOrderEntity; +import com.project.logistics.domain.enums.OrderStatusEnum; +import com.project.logistics.domain.enums.RetryActionEnum; +import com.project.logistics.domain.service.base.ErpService; +import com.project.logistics.domain.service.base.LogisticsOrderService; +import com.project.logistics.domain.strategy.ApiTaskHandler; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Component +public class ErpUpdateWaybillHandler implements ApiTaskHandler { + + @Autowired + private ErpService erpService; + + @Autowired + private LogisticsOrderService logisticsOrderService; + + @Override + public String getActionCode() { + return RetryActionEnum.ERP_UPDATE_WAYBILL.getCode(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void handle(ApiRetryTaskEntity task, LogisticsOrderEntity order) throws Exception { + log.info(">>> 开始处理 ERP 运单回写任务,单号: {}", order.getOrderNo()); + + // 1. 安全检查:确保本地已经存有顺丰运单号 + String waybillNo = order.getSfWaybillNo(); + if (StrUtil.isBlank(waybillNo)) { + throw new RuntimeException("本地订单缺少顺丰运单号,无法回写 ERP"); + } + + // 2. 调用 U9 专线执行更新 + erpService.updateWaybillToErp(order.getOrderNo(), waybillNo , order.getResourceCode()); + + // 3. 更新本地订单状态机 + order.setOrderStatus(OrderStatusEnum.ERP_WAYBILL_UPDATED.getCode()); + logisticsOrderService.updateById(order); + + log.info(">>> ERP 运单号回写任务完成,单号: {}", order.getOrderNo()); + + // 💡 提示:这里不需要主动触发下一个任务了。 + // 因为后续的“已揽收”、“已签收”都是由顺丰主动推送到我们的 receive 模块, + // 再由 receive 模块根据推送内容生成新的异步任务。 + } +} \ No newline at end of file diff --git a/src/main/java/com/project/logistics/domain/strategy/handler/SfCreateOrderHandler.java b/src/main/java/com/project/logistics/domain/strategy/handler/SfCreateOrderHandler.java index dfba94e..a18aa69 100644 --- a/src/main/java/com/project/logistics/domain/strategy/handler/SfCreateOrderHandler.java +++ b/src/main/java/com/project/logistics/domain/strategy/handler/SfCreateOrderHandler.java @@ -1,14 +1,15 @@ package com.project.logistics.domain.strategy.handler; +import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.jayway.jsonpath.JsonPath; +import com.project.logistics.config.FixedRuleProperties; import com.project.logistics.domain.entity.ApiRetryTaskEntity; import com.project.logistics.domain.entity.LogisticsOrderEntity; -import com.project.logistics.domain.enums.OrderStatusEnum; -import com.project.logistics.domain.enums.RetryActionEnum; +import com.project.logistics.domain.enums.*; import com.project.logistics.domain.service.SfApiService; import com.project.logistics.domain.service.WebDavService; import com.project.logistics.domain.service.base.ApiRetryTaskService; @@ -19,6 +20,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; +import java.util.Objects; + @Slf4j @Component public class SfCreateOrderHandler implements ApiTaskHandler { @@ -32,6 +35,9 @@ public class SfCreateOrderHandler implements ApiTaskHandler { @Autowired private WebDavService webDavService; + @Autowired + private FixedRuleProperties fixedRuleProperties; + @Override public String getActionCode() { return RetryActionEnum.SF_CREATE_ORDER.getCode(); @@ -50,40 +56,74 @@ public class SfCreateOrderHandler implements ApiTaskHandler { msgData.put("language", "zh-CN"); msgData.put("orderId", order.getOrderNo()); // 客户订单号 + + String transportMethod = u9Data.getString(ShipmentOrderInfoFieldEnum.transportMethod.name()); + if (StrUtil.isBlank(transportMethod) || !transportMethod.contains("顺丰")) { + // 需要终止任务,匹配字段失败,失败原因:运输方式及货场名称不为顺丰 + terminateBusinessTask(task, order, "匹配字段失败:运输方式及货场名称不为顺丰 (" + transportMethod + ")"); + return; + } + + String payer = u9Data.getString(ShipmentOrderInfoFieldEnum.payer.name()); + SfPayMethodEnum senderPayByU9Desc = SfPayMethodEnum.getSenderPayByU9Desc(payer); + if (Objects.isNull(senderPayByU9Desc)) { + // 需要终止任务,匹配字段失败,失败原因:运费承担字段匹配失败 + terminateBusinessTask(task, order, "匹配字段失败:运费承担字段 (" + payer + ") 无法匹配顺丰付款方式"); + return; + } + + String expressType = u9Data.getString(ShipmentOrderInfoFieldEnum.expressType.name()); + SfExpressTypeEnum sfStandardExpressByCode = SfExpressTypeEnum.getSfStandardExpressByCode(expressType); + if (Objects.isNull(sfStandardExpressByCode)) { + // 需要终止任务,匹配字段失败,失败原因:未匹配快件产品类别 + terminateBusinessTask(task, order, "匹配字段失败:单据产品类别 (" + expressType + ") 未匹配到顺丰业务类型"); + return; + } + msgData.put("cargoDesc", "交换机"); + JSONArray cargoDetails = new JSONArray(); + JSONObject item = new JSONObject(); + item.put("name", "交换机"); + cargoDetails.add(item); + msgData.put("cargoDetails", cargoDetails); + // 2.1 构造收寄双方信息 (根据你之前 SELECT * 看到的 U9 字段名来取值) JSONArray contactInfoList = new JSONArray(); - + // 寄件方 (通常写死公司信息,或从配置取) JSONObject sender = new JSONObject(); - sender.put("contactType", 1); - sender.put("contact", "您的公司名"); - sender.put("mobile", "13800000000"); - sender.put("address", "公司详细地址"); + sender.put("contactType", SfContactTypeEnum.SENDER.getCode()); + // 寄付取业务的名字,到付取固定 + sender.put("contact", SfPayMethodEnum.SENDER_PAY.equals(senderPayByU9Desc) ? + u9Data.getString(ShipmentOrderInfoFieldEnum.payer.name()) : + fixedRuleProperties.getSenderPayContact()); + sender.put("mobile", fixedRuleProperties.getSenderMobile()); + sender.put("address", fixedRuleProperties.getSenderAddress()); contactInfoList.add(sender); // 收件方 (从 U9 快照中动态获取) JSONObject receiver = new JSONObject(); - receiver.put("contactType", 2); - receiver.put("contact", u9Data.getString("Receiver_Contact")); // 假设 U9 字段名是这个 - receiver.put("mobile", u9Data.getString("Receiver_Phone")); - receiver.put("province", u9Data.getString("Receiver_Province")); - receiver.put("city", u9Data.getString("Receiver_City")); - receiver.put("address", u9Data.getString("Receiver_Address")); + receiver.put("contactType", SfContactTypeEnum.SENDER.getCode()); + receiver.put("contact", u9Data.getString(ShipmentOrderInfoFieldEnum.recipientContact.name())); // 假设 U9 字段名是这个 + receiver.put("mobile", u9Data.getString(ShipmentOrderInfoFieldEnum.recipientMobile.name())); + receiver.put("address", u9Data.getString(ShipmentOrderInfoFieldEnum.recipientAddress.name())); contactInfoList.add(receiver); msgData.put("contactInfoList", contactInfoList); // 2.2 货物信息与支付方式 - msgData.put("monthlyCard", u9Data.getString("MonthlyCard")); // 月结卡号 - msgData.put("payMethod", u9Data.getInteger("PayMethod")); // 付款方式 - msgData.put("expressTypeId", "1"); // 标准快递 + // 月结卡号 + if (SfPayMethodEnum.SENDER_PAY.equals(senderPayByU9Desc)) { + msgData.put("monthlyCard" , fixedRuleProperties.getMonthlyCard()); + } + msgData.put("expressTypeId", sfStandardExpressByCode.getSfCode()); // 标准快递 // 2.3 【关键步骤】绑定电子回单增值服务 - if (order.getResourceCode() != null) { + if (StrUtil.isNotBlank(order.getResourceCode())) { JSONArray serviceList = new JSONArray(); JSONObject receiptService = new JSONObject(); receiptService.put("name", "IN149"); // 电子回单服务代码 - receiptService.put("value", order.getResourceCode()); + receiptService.put("value", "3"); + receiptService.put("value1", order.getResourceCode()); serviceList.add(receiptService); msgData.put("serviceList", serviceList); } @@ -121,8 +161,9 @@ public class SfCreateOrderHandler implements ApiTaskHandler { log.error(">>> 顺丰下单成功但文件移位失败: {}", e.getMessage()); } - // 7. 触发下一个任务:回写 U9 运单号 + // 7. 触发下阶段任务:回写 U9 运单号 + 下载面单(可并行) apiRetryTaskService.createNextTask(order.getOrderNo(), RetryActionEnum.ERP_UPDATE_WAYBILL); + apiRetryTaskService.createNextTask(order.getOrderNo(), RetryActionEnum.SF_DOWNLOAD_WAYBILL); } else { String errorMsg = JsonPath.read(innerJsonStr, "$.errorMsg"); @@ -133,4 +174,18 @@ public class SfCreateOrderHandler implements ApiTaskHandler { throw e; } } + private void terminateBusinessTask(ApiRetryTaskEntity task, LogisticsOrderEntity order, String reason) { + log.error(">>> 订单 {} 业务校验不通过: {}", order.getOrderNo(), reason); + + // 1. 更新主表为 ERROR 状态 + order.setOrderStatus(OrderStatusEnum.ERROR.getCode()); + logisticsOrderService.updateById(order); + + // 2. 将当前任务设为取消/死信状态,防止定时任务继续重试 + task.setTaskStatus(TaskStatusEnum.CANCELLED.getCode()); + task.setErrorMessage(reason); + // 注意:任务表的最终 update 会由 ApiRetryJob 在 handle 执行完后统一处理 + // 此处抛出一个特定异常或正常返回即可。由于 ApiRetryJob 会根据异常重试, + // 我们这里选择不抛异常,让 ApiRetryJob 认为执行“结束”了(但状态已改)。 + } } \ No newline at end of file diff --git a/src/main/java/com/project/logistics/domain/strategy/handler/SfDownloadWaybillHandler.java b/src/main/java/com/project/logistics/domain/strategy/handler/SfDownloadWaybillHandler.java new file mode 100644 index 0000000..6d43dff --- /dev/null +++ b/src/main/java/com/project/logistics/domain/strategy/handler/SfDownloadWaybillHandler.java @@ -0,0 +1,71 @@ +package com.project.logistics.domain.strategy.handler; + +import com.project.logistics.config.WebDavProperties; +import com.project.logistics.domain.entity.ApiRetryTaskEntity; +import com.project.logistics.domain.entity.LogisticsOrderEntity; +import com.project.logistics.domain.enums.OrderStatusEnum; +import com.project.logistics.domain.enums.RetryActionEnum; +import com.project.logistics.domain.service.SfApiService; +import com.project.logistics.domain.service.WebDavService; +import com.project.logistics.domain.service.base.ApiRetryTaskService; +import com.project.logistics.domain.service.base.LogisticsOrderService; +import com.project.logistics.domain.strategy.ApiTaskHandler; +import com.project.logistics.domain.utils.FilePathUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Date; +import java.util.Map; + +@Slf4j +@Component +public class SfDownloadWaybillHandler implements ApiTaskHandler { + + @Autowired + private SfApiService sfApiService; + @Autowired + private WebDavService webDavService; + @Autowired + private LogisticsOrderService logisticsOrderService; + @Autowired + private ApiRetryTaskService apiRetryTaskService; + @Autowired + private WebDavProperties webDavProperties; + + @Override + public String getActionCode() { + return RetryActionEnum.SF_DOWNLOAD_WAYBILL.getCode(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void handle(ApiRetryTaskEntity task, LogisticsOrderEntity order) throws Exception { + log.info(">>> 开始获取顺丰面单,单号: {}", order.getOrderNo()); + + // 1. 调用顺丰云打印接口,获取 PDF 的临时下载链接 + Map waybillPrintFile = sfApiService.getWaybillPrintFile(order.getSfWaybillNo()); + + // 2. 通过 URL 下载 PDF 二进制流 + byte[] pdfBytes = sfApiService.downloadWaybillPdfWithAuth(waybillPrintFile.get("url") , + waybillPrintFile.get("token")); + + // 3. 将面单上传到 WebDAV 的指定目录下 + // 路径规则:/面单文件根目录/2026年/2026年3月/2026年3月30日/单号_面单.pdf + String datePath = FilePathUtil.getHierarchicalPath(new Date()); + String fileName = order.getOrderNo() + ".pdf"; + + // 假设你在 WebDavProperties 里定义了 waybillRoot + String fullSavePath = webDavProperties.getSfWaybillFolderName() + "/" + datePath + "/" + fileName; + + webDavService.uploadFile(fullSavePath, pdfBytes); + + // 4. 更新数据库状态 + order.setWaybillPdfPath(fullSavePath); + order.setOrderStatus(OrderStatusEnum.WAYBILL_DOWNLOADED.getCode()); + logisticsOrderService.updateById(order); + + log.info(">>> 顺丰面单处理完成并已存入 WebDAV: {}", fileName); + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index dc076ea..330df37 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -92,4 +92,9 @@ u9-source: url: jdbc:sqlserver://192.168.4.202:1433;databaseName=20241030;encrypt=false;trustServerCertificate=true username: sa password: 'Liujun1928374650' - driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver \ No newline at end of file + driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver +fixed-rule: + senderPayContact: '王盛荣' + senderMobile: '13480155048' + senderAddress: '广东省广州市番禺区石碁镇南荔东路56号' + monthlyCard: '7551234567' \ No newline at end of file