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