Browse Source

调接口逻辑

master
luoweijian 4 weeks ago
parent
commit
46baa97609
  1. 6
      .idea/sqldialects.xml
  2. 17
      src/main/java/com/project/logistics/config/FixedRuleProperties.java
  3. 4
      src/main/java/com/project/logistics/config/SfApiProperties.java
  4. 2
      src/main/java/com/project/logistics/config/WebDavProperties.java
  5. 18
      src/main/java/com/project/logistics/domain/enums/SfContactTypeEnum.java
  6. 30
      src/main/java/com/project/logistics/domain/enums/SfExpressTypeEnum.java
  7. 30
      src/main/java/com/project/logistics/domain/enums/SfPayMethodEnum.java
  8. 27
      src/main/java/com/project/logistics/domain/enums/ShipmentOrderInfoFieldEnum.java
  9. 10
      src/main/java/com/project/logistics/domain/scheduler/ApiRetryJob.java
  10. 63
      src/main/java/com/project/logistics/domain/service/SfApiService.java
  11. 34
      src/main/java/com/project/logistics/domain/service/WebDavService.java
  12. 5
      src/main/java/com/project/logistics/domain/service/base/ErpService.java
  13. 37
      src/main/java/com/project/logistics/domain/service/base/ErpServiceImpl.java
  14. 55
      src/main/java/com/project/logistics/domain/strategy/handler/ErpUpdateWaybillHandler.java
  15. 91
      src/main/java/com/project/logistics/domain/strategy/handler/SfCreateOrderHandler.java
  16. 71
      src/main/java/com/project/logistics/domain/strategy/handler/SfDownloadWaybillHandler.java
  17. 5
      src/main/resources/application.yml

6
.idea/sqldialects.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/src/main/java/com/project/logistics/domain/service/base/ErpServiceImpl.java" dialect="GenericSQL" />
</component>
</project>

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

4
src/main/java/com/project/logistics/config/SfApiProperties.java

@ -44,4 +44,8 @@ public class SfApiProperties {
* 正式环境: 由顺丰提供
*/
private String channelCode;
/**
* 面单模板编码
*/
private String templateCode;
}

2
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

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

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

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

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

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

63
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<String, String> 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<String, String> 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<Void> requestEntity = new HttpEntity<>(headers);
// 使用 RestTemplate 的 exchange 方法发起 GET,并接收 byte[]
ResponseEntity<byte[]> 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");
}
}
}

34
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)

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

37
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<String, Object> 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 的重试机制
}
}
}

55
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 模块根据推送内容生成新的异步任务。
}
}

91
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 认为执行“结束”了(但状态已改)。
}
}

71
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<String, String> 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);
}
}

5
src/main/resources/application.yml

@ -93,3 +93,8 @@ u9-source:
username: sa
password: 'Liujun1928374650'
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
fixed-rule:
senderPayContact: '王盛荣'
senderMobile: '13480155048'
senderAddress: '广东省广州市番禺区石碁镇南荔东路56号'
monthlyCard: '7551234567'
Loading…
Cancel
Save