diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml
index 23abb4d..f79b0cf 100644
--- a/.idea/sqldialects.xml
+++ b/.idea/sqldialects.xml
@@ -1,6 +1,6 @@
-
+
\ 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 b4dd11c..b16bbff 100644
--- a/src/main/java/com/project/logistics/config/WebDavProperties.java
+++ b/src/main/java/com/project/logistics/config/WebDavProperties.java
@@ -42,7 +42,7 @@ public class WebDavProperties {
* 签收回单(POD)的存放根路径
* 例如: /U9_Orders/Signed_Receipts
*/
- private String podRoot;
+ private String podRoot = "/电子签收单";
/**
* 连接超时时间 (毫秒),默认 10000
diff --git a/src/main/java/com/project/logistics/domain/enums/ShipmentOrderInfoFieldEnum.java b/src/main/java/com/project/logistics/domain/enums/ShipmentOrderInfoFieldEnum.java
index 43a084b..64fb533 100644
--- a/src/main/java/com/project/logistics/domain/enums/ShipmentOrderInfoFieldEnum.java
+++ b/src/main/java/com/project/logistics/domain/enums/ShipmentOrderInfoFieldEnum.java
@@ -19,6 +19,7 @@ public enum ShipmentOrderInfoFieldEnum {
expressType("DescFlexField_PrivateDescSeg20","寄付方式") ,
fee("DescFlexField_PrivateDescSeg3" , "运费") ,
resourceCode("DescFlexField_PrivateDescSeg18" , "资源编码"),
+ quantity("SM_Ship.DescFlexField_PrivateDescSeg2" , "件数") ,
;
private final String fieldName;
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 2d5d108..7aacc7d 100644
--- a/src/main/java/com/project/logistics/domain/scheduler/ApiRetryJob.java
+++ b/src/main/java/com/project/logistics/domain/scheduler/ApiRetryJob.java
@@ -1,5 +1,7 @@
package com.project.logistics.domain.scheduler;
+import cn.hutool.core.util.StrUtil;
+import com.project.base.config.ScheduledTaskProperties;
import com.project.logistics.domain.entity.ApiRetryTaskEntity;
import com.project.logistics.domain.entity.LogisticsOrderEntity;
import com.project.logistics.domain.enums.TaskStatusEnum;
@@ -29,6 +31,9 @@ public class ApiRetryJob {
// Spring 会自动把所有实现了 ApiTaskHandler 的类注入到这个 List 里
private final Map handlerMap;
+ @Autowired
+ private ScheduledTaskProperties scheduledTaskProperties;
+
@Autowired
public ApiRetryJob(List handlers) {
// 将 List 转换成 Map,方便通过 ActionCode 快速查找对应的处理器
@@ -42,6 +47,10 @@ public class ApiRetryJob {
*/
@Scheduled(fixedDelay = 10000)
public void executePendingTasks() throws Exception {
+ if (StrUtil.equals(scheduledTaskProperties.getOwner() , "local")) {
+ log.error("跳过ApiRetryJob定时任务");
+ return;
+ }
// 1. 从数据库捞取所有 状态=PENDING/FAILED 且 执行时间<=当前时间 的任务
List pendingTasks = apiRetryTaskService.listPendingTasks();
diff --git a/src/main/java/com/project/logistics/domain/scheduler/WebDavDirectoryInitJob.java b/src/main/java/com/project/logistics/domain/scheduler/WebDavDirectoryInitJob.java
new file mode 100644
index 0000000..0e923d8
--- /dev/null
+++ b/src/main/java/com/project/logistics/domain/scheduler/WebDavDirectoryInitJob.java
@@ -0,0 +1,55 @@
+package com.project.logistics.domain.scheduler;
+
+import com.project.logistics.config.WebDavProperties;
+import com.project.logistics.domain.service.WebDavService;
+import com.project.logistics.domain.utils.FilePathUtil;
+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.Date;
+
+@Slf4j
+@Component
+public class WebDavDirectoryInitJob {
+
+ @Autowired
+ private WebDavService webDavService;
+
+ @Autowired
+ private WebDavProperties webDavProperties;
+
+ /**
+ * 每天凌晨 00:00:01 执行
+ * 预先创建好当天所有的业务文件夹
+ */
+ @Scheduled(cron = "8 0 0 * * ?")
+ public void initTodayDirectories() {
+ log.info(">>> [系统预热] 开始初始化今日 WebDAV 业务目录...");
+
+ // 1. 生成今日层级路径: 2026年/2026年3月/2026年3月30日
+ String datePath = FilePathUtil.getHierarchicalPath(new Date());
+
+ try {
+ // 2. 初始化【待处理出货单】目录 (用户上班要往里放文件)
+ webDavService.initFolder(webDavProperties.getShipmentRoot(), datePath);
+
+ // 3. 初始化【已自动下单出货单】目录 (下单成功后文件会自动移入)
+ webDavService.initFolder(webDavProperties.getProcessedFolderName(), datePath);
+
+ // 4. 初始化【顺丰面单】目录
+ // 假设你在 WebDavProperties 中定义了 waybillRoot
+ webDavService.initFolder("顺丰面单", datePath);
+
+ // 5. 初始化【签收回单】目录
+ webDavService.initFolder(webDavProperties.getPodRoot(), datePath);
+
+ log.info(">>> [系统预热] 今日所有目录初始化完成!");
+
+ } catch (Exception e) {
+ log.error(">>> [系统预热] 自动创建今日目录失败: {}", e.getMessage());
+ // 注意:这里失败没关系,业务运行时的 ensureDirectoryExists 还有一次兜底机会
+ }
+ }
+}
\ 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 efd7fe5..1396e90 100644
--- a/src/main/java/com/project/logistics/domain/service/WebDavService.java
+++ b/src/main/java/com/project/logistics/domain/service/WebDavService.java
@@ -145,4 +145,22 @@ public class WebDavService {
String normalized = rawUrl.replaceAll("(?>> 正在初始化业务目录: {}", rootName + "/" + datePath);
+ // 调用之前实现的递归创建逻辑
+ ensureDirectoryExists(sardine, encodedUrl);
+ }
}
\ No newline at end of file
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 5631bba..ebb99fa 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
@@ -2,6 +2,8 @@ package com.project.logistics.domain.service.base;
import cn.hutool.json.JSONObject;
+import java.math.BigDecimal;
+
public interface ErpService {
@@ -11,4 +13,8 @@ public interface ErpService {
* 回写运单号到 U9
*/
void updateWaybillToErp(String orderNo, String waybillNo , String resourceCode) throws Exception;
+
+ void updateStatusToErp(String orderNo, String statusDesc) throws Exception;
+
+ void updateFeeToErp(String orderNo, Integer qty, BigDecimal amount) throws Exception;
}
diff --git a/src/main/java/com/project/logistics/domain/service/base/ApiRetryTaskServiceImpl.java b/src/main/java/com/project/logistics/domain/service/base/impl/ApiRetryTaskServiceImpl.java
similarity index 95%
rename from src/main/java/com/project/logistics/domain/service/base/ApiRetryTaskServiceImpl.java
rename to src/main/java/com/project/logistics/domain/service/base/impl/ApiRetryTaskServiceImpl.java
index 95a801c..84d6991 100644
--- a/src/main/java/com/project/logistics/domain/service/base/ApiRetryTaskServiceImpl.java
+++ b/src/main/java/com/project/logistics/domain/service/base/impl/ApiRetryTaskServiceImpl.java
@@ -1,9 +1,10 @@
-package com.project.logistics.domain.service.base;
+package com.project.logistics.domain.service.base.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.project.logistics.domain.entity.ApiRetryTaskEntity;
import com.project.logistics.domain.enums.RetryActionEnum;
import com.project.logistics.domain.enums.TaskStatusEnum;
+import com.project.logistics.domain.service.base.ApiRetryTaskService;
import com.project.logistics.mapper.ApiRetryTaskMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
diff --git a/src/main/java/com/project/logistics/domain/service/base/ErpServiceImpl.java b/src/main/java/com/project/logistics/domain/service/base/impl/ErpServiceImpl.java
similarity index 73%
rename from src/main/java/com/project/logistics/domain/service/base/ErpServiceImpl.java
rename to src/main/java/com/project/logistics/domain/service/base/impl/ErpServiceImpl.java
index 6d52b3a..12bb7bc 100644
--- a/src/main/java/com/project/logistics/domain/service/base/ErpServiceImpl.java
+++ b/src/main/java/com/project/logistics/domain/service/base/impl/ErpServiceImpl.java
@@ -1,13 +1,15 @@
-package com.project.logistics.domain.service.base;
+package com.project.logistics.domain.service.base.impl;
import cn.hutool.json.JSONObject;
import com.project.logistics.domain.enums.ShipmentOrderInfoFieldEnum;
+import com.project.logistics.domain.service.base.ErpService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
+import java.math.BigDecimal;
import java.util.Map;
@Service
@@ -70,4 +72,35 @@ public class ErpServiceImpl implements ErpService {
throw e; // 抛出异常,触发 ApiRetryJob 的重试机制
}
}
+
+ /**
+ * 通用的状态回写方法 (揽收、签收)
+ */
+ @Override
+ public void updateStatusToErp(String orderNo, String statusDesc) throws Exception {
+ log.info(">>> [U9回写状态] 单号: {}, 目标状态: {}", orderNo, statusDesc);
+
+ // 假设 U9 的状态字段是 StatusDesc,或者是自定义字段 DescFlexField_Pub_2
+ String sql = "UPDATE SM_Ship SET DescFlexField_Pub_2 = ? WHERE DocNo = ?";
+
+ int rows = u9JdbcTemplate.update(sql, statusDesc, orderNo);
+ if (rows == 0) {
+ throw new RuntimeException("U9单据不存在,回写状态失败");
+ }
+ }
+
+ @Override
+ public void updateFeeToErp(String orderNo, Integer qty, BigDecimal amount) throws Exception {
+ // 财务安全:确保回写的金额 100% 是两位小数
+ BigDecimal formattedAmount = amount.setScale(2, java.math.RoundingMode.HALF_UP);
+
+ log.info(">>> [U9回写费用] 单号: {}, 最终金额: {}", orderNo, formattedAmount);
+
+ String sql = String.format("UPDATE SM_Ship SET %s = ?, %s = ? WHERE DocNo = ?" ,
+ ShipmentOrderInfoFieldEnum.quantity.getFieldName() ,
+ ShipmentOrderInfoFieldEnum.fee.getFieldName());
+
+ // 执行更新
+ u9JdbcTemplate.update(sql, qty, formattedAmount, orderNo);
+ }
}
diff --git a/src/main/java/com/project/logistics/domain/service/base/LogisticsOrderServiceImpl.java b/src/main/java/com/project/logistics/domain/service/base/impl/LogisticsOrderServiceImpl.java
similarity index 93%
rename from src/main/java/com/project/logistics/domain/service/base/LogisticsOrderServiceImpl.java
rename to src/main/java/com/project/logistics/domain/service/base/impl/LogisticsOrderServiceImpl.java
index e1e3dfc..b7fb27e 100644
--- a/src/main/java/com/project/logistics/domain/service/base/LogisticsOrderServiceImpl.java
+++ b/src/main/java/com/project/logistics/domain/service/base/impl/LogisticsOrderServiceImpl.java
@@ -1,4 +1,4 @@
-package com.project.logistics.domain.service.base;
+package com.project.logistics.domain.service.base.impl;
import cn.hutool.json.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -8,6 +8,8 @@ import com.project.logistics.domain.enums.OrderStatusEnum;
import com.project.logistics.domain.enums.OrderTypeEnum;
import com.project.logistics.domain.enums.RetryActionEnum;
import com.project.logistics.domain.enums.TaskStatusEnum;
+import com.project.logistics.domain.service.base.ApiRetryTaskService;
+import com.project.logistics.domain.service.base.LogisticsOrderService;
import com.project.logistics.mapper.LogisticsOrderMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
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 a18aa69..6abf71a 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
@@ -79,6 +79,12 @@ public class SfCreateOrderHandler implements ApiTaskHandler {
terminateBusinessTask(task, order, "匹配字段失败:单据产品类别 (" + expressType + ") 未匹配到顺丰业务类型");
return;
}
+
+ if (StrUtil.equals(order.getOrderType() , OrderTypeEnum.SHIPMENT.name()) &&
+ StrUtil.isBlank(order.getResourceCode())) {
+ terminateBusinessTask(task, order, "常规出货单缺少资源编码");
+ return;
+ }
msgData.put("cargoDesc", "交换机");
JSONArray cargoDetails = new JSONArray();
JSONObject item = new JSONObject();
@@ -102,7 +108,7 @@ public class SfCreateOrderHandler implements ApiTaskHandler {
// 收件方 (从 U9 快照中动态获取)
JSONObject receiver = new JSONObject();
- receiver.put("contactType", SfContactTypeEnum.SENDER.getCode());
+ receiver.put("contactType", SfContactTypeEnum.RECIPIENT.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()));
diff --git a/src/main/java/com/project/receive/controller/ReceiveController.java b/src/main/java/com/project/receive/controller/ReceiveController.java
index 87fcae6..92f2204 100644
--- a/src/main/java/com/project/receive/controller/ReceiveController.java
+++ b/src/main/java/com/project/receive/controller/ReceiveController.java
@@ -1,162 +1,87 @@
package com.project.receive.controller;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.project.receive.dto.*;
-import com.project.receive.utils.SfDecryptUtil;
+
+import com.project.receive.domain.dto.SfRoutePushRequest;
+import com.project.receive.domain.service.ReceiveService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
-import java.io.FileOutputStream;
-import java.nio.file.Path;
-import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
-@RestController
@Slf4j
-@RequestMapping("/api/")
+@RestController
+@RequestMapping("/api/sf/callback")
public class ReceiveController {
- private static final String SF_CHECKWORD = "ZXmoWOQdSd2UTBmSP6Kv3VW9Q4N5dJqz";
+ @Autowired
+ private ReceiveService receiveService;
+
/**
- * 接收顺丰订单状态推送接口
- * 对应文档 2.6 节 JSON 示例
+ * 1. 路由状态回调 (揽收/签收)
+ * 对应接口: PushOrderState
*/
- @PostMapping("/pushOrderState")
- public SfPushResponse receiveOrderState(@RequestBody SfPushRequest request) {
- // 1. 打印接收到的原始数据日志
- log.info("==== 收到顺丰状态推送 ====");
- log.info("Request ID: {}", request.getRequestId());
- log.info("Timestamp: {}", request.getTimestamp());
+ @PostMapping("/route")
+ public Map handleRoutePush(@RequestBody SfRoutePushRequest request) {
+ log.info(">>> 收到顺丰路由推送,包含节点数: {}",
+ (request.getBody() != null && request.getBody().getWaybillRoute() != null) ?
+ request.getBody().getWaybillRoute().size() : 0);
- if (request.getOrderState() != null) {
- request.getOrderState().forEach(state -> {
- log.info("订单号: {}, 运单号: {}, 状态码: {}, 描述: {}",
- state.getOrderNo(),
- state.getWaybillNo(),
- state.getOrderStateCode(),
- state.getOrderStateDesc());
- });
- }
+ try {
+ // 业务处理逻辑
+ receiveService.processRoutePush(request);
- // 2. 根据文档 2.7 节,返回成功响应
- return SfPushResponse.ok();
+ // 返回顺丰要求的成功格式
+ return successResponse();
+ } catch (Exception e) {
+ log.error(">>> 路由解析处理失败", e);
+ return failResponse(e.getMessage());
+ }
}
-
-
/**
- * 接收顺丰路由推送
- * 对应文档 2.5 节 JSON 示例
+ * 2. 费用与重量回调
+ * 对应接口: EXP_RECE_WAYBILLS_FEE_PUSH
*/
- @PostMapping("/pushRoute")
- public SfRouteResponse receiveRoute(@RequestBody SfRoutePushRequest request) {
- log.info(">>>> 收到顺丰路由信息推送 <<<<");
-
- if (request.getBody() != null && request.getBody().getWaybillRoute() != null) {
- for (WaybillRoute route : request.getBody().getWaybillRoute()) {
- log.info("运单号: {}, 订单号: {}, 时间: {}, 状态: {}, 备注: {}",
- route.getMailno(),
- route.getOrderid(),
- route.getAcceptTime(),
- route.getOpCode(),
- route.getRemark());
- }
- } else {
- log.warn("收到的路由数据内容为空");
+ @PostMapping("/fee")
+ public Map handleFeePush(@RequestParam("content") String content) {
+ log.info(">>> 收到顺丰【运费重量】推送: {}", content);
+ try {
+ receiveService.saveFeeLog(content);
+ return successResponse();
+ } catch (Exception e) {
+ log.error("运费处理异常", e);
+ return failResponse(e.getMessage());
}
-
- // 根据文档 2.6,必须返回 0000 告知顺丰接收成功,否则顺丰会重复推送
- return SfRouteResponse.ok();
}
-
- @RequestMapping("/pushOrderStateRaw")
- public String receiveRaw(@RequestBody String rawBody) {
- log.info("收到原始报文: {}", rawBody);
- // 返回 JSON 格式的成功字符串
- return "{\"success\":\"true\",\"code\":\"0\",\"msg\":\"\"}";
- }
-
- @Autowired
- private ObjectMapper objectMapper;
-
/**
- * 接收顺丰运费推送
- * 报文类型: application/x-www-form-urlencoded
+ * 3. 电子回单图片回调
+ * 对应接口: 图片注册及推送接口
*/
- @PostMapping(value = "/pushFee", consumes = "application/x-www-form-urlencoded")
- public SfFeeResponse receiveFee(
- @RequestParam("content") String content,
- @RequestParam(value = "sign", required = false) String sign) {
-
- log.info(">>>> 收到顺丰运费推送 <<<<");
- log.info("签名(sign): {}", sign);
- log.info("原始内容(content): {}", content);
-
+ @PostMapping("/pod-picture")
+ public Map handlePodPicturePush(@RequestParam("content") String content) {
+ log.info(">>> 收到顺丰【电子回单图片】推送 (内容较长,不完整打印)");
try {
- // 将 content 字符串解析为 Java 对象
- SfFeeContent feeData = objectMapper.readValue(content, SfFeeContent.class);
-
- log.info("解析成功 - 运单号: {}, 订单号: {}, 计费重量: {}",
- feeData.getWaybillNo(),
- feeData.getOrderNo(),
- feeData.getMeterageWeightQty());
-
- if (feeData.getFeeList() != null) {
- feeData.getFeeList().forEach(fee -> {
- log.info("费用项 - 类型: {}, 金额: {}", fee.getFeeTypeCode(), fee.getFeeAmt());
- });
- }
-
- // 返回成功响应 (code 必须为 200)
- return SfFeeResponse.ok("your_partner_code");
-
+ receiveService.savePodPictureLog(content);
+ return successResponse();
} catch (Exception e) {
- log.error("解析顺丰运费推送失败", e);
- SfFeeResponse error = new SfFeeResponse();
- error.setCode(400);
- error.setMessage("解析异常");
- return error;
+ log.error("回单图片处理异常", e);
+ return failResponse(e.getMessage());
}
}
- @PostMapping("/pushElectronicReceipt")
- public SfPicturePushResponse receiveReceipt(@RequestBody SfPicturePushRequest request) {
- log.info(">>>> 收到顺丰电子回单(IN149)推送, 运单号: {} <<<<", request.getWaybillNo());
-
- try {
- // 步骤 1:解密最外层的 content 得到内部 JSON 字符串
- byte[] firstLevelDecrypted = SfDecryptUtil.decrypt(request.getContent(), SF_CHECKWORD);
- String innerJsonStr = new String(firstLevelDecrypted, "UTF-8");
- log.info("第一层解密成功");
-
- // 步骤 2:解析内部 JSON 获取文件密文
- SfInnerContent innerContent = objectMapper.readValue(innerJsonStr, SfInnerContent.class);
- String encryptedFileBase64 = innerContent.getContent();
-
- // 步骤 3:对文件密文进行第二层 AES 解密 (根据文档示例,文件内容也是加密的)
- // 先 Base64 解码,再 AES 解密
- byte[] pdfBytes = SfDecryptUtil.decrypt(encryptedFileBase64, SF_CHECKWORD);
- log.info("第二层解密(文件流)成功,大小: {} bytes", pdfBytes.length);
-
- // 步骤 4:保存为 PDF 文件到当前目录
- String fileName = request.getWaybillNo() + "_receipt.pdf";
- Path path = Paths.get(System.getProperty("user.dir"), fileName);
-
- try (FileOutputStream fos = new FileOutputStream(path.toFile())) {
- fos.write(pdfBytes);
- fos.flush();
- }
-
- log.info("电子回单已保存至: {}", path.toAbsolutePath());
-
- return SfPicturePushResponse.ok();
+ private Map successResponse() {
+ Map res = new HashMap<>();
+ res.put("success", true);
+ res.put("errorCode", "S0000");
+ return res;
+ }
- } catch (Exception e) {
- log.error("处理电子回单推送失败", e);
- SfPicturePushResponse error = new SfPicturePushResponse();
- error.setReturnCode("1000");
- error.setReturnMsg("解析保存失败: " + e.getMessage());
- return error;
- }
+ private Map failResponse(String msg) {
+ Map res = new HashMap<>();
+ res.put("success", false);
+ res.put("errorMsg", msg);
+ return res;
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/project/receive/domain/dto/SfRoutePushRequest.java b/src/main/java/com/project/receive/domain/dto/SfRoutePushRequest.java
new file mode 100644
index 0000000..047b9e2
--- /dev/null
+++ b/src/main/java/com/project/receive/domain/dto/SfRoutePushRequest.java
@@ -0,0 +1,29 @@
+package com.project.receive.domain.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import java.util.List;
+
+@Data
+public class SfRoutePushRequest {
+
+ @JsonProperty("Body") // 必须对应顺丰的大写 Body
+ private RouteBody body;
+
+ @Data
+ public static class RouteBody {
+ @JsonProperty("WaybillRoute") // 必须对应顺丰的大写 WaybillRoute
+ private List waybillRoute;
+ }
+
+ @Data
+ public static class WaybillRouteDetail {
+ private String mailno; // 顺丰运单号
+ private String orderid; // 客户订单号 (U9单号)
+ private String acceptAddress; // 发生地址
+ private String acceptTime; // 发生时间
+ private String remark; // 备注内容
+ private String opCode; // 操作码 (50:揽收, 80:签收)
+ private String id; // 推送ID
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/project/logistics/domain/entity/FeePushLogEntity.java b/src/main/java/com/project/receive/domain/entity/FeePushLogEntity.java
similarity index 91%
rename from src/main/java/com/project/logistics/domain/entity/FeePushLogEntity.java
rename to src/main/java/com/project/receive/domain/entity/FeePushLogEntity.java
index d446c29..bd75948 100644
--- a/src/main/java/com/project/logistics/domain/entity/FeePushLogEntity.java
+++ b/src/main/java/com/project/receive/domain/entity/FeePushLogEntity.java
@@ -1,4 +1,4 @@
-package com.project.logistics.domain.entity;
+package com.project.receive.domain.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
@@ -30,6 +30,11 @@ public class FeePushLogEntity extends BaseEntity {
@TableField("order_no")
private String orderNo;
+ @Comment("件数")
+ @Column(name = "item_count")
+ @TableField("item_count")
+ private Integer itemCount;
+
@Comment("关联顺丰运单号")
@Column(name = "waybill_no", length = 64, nullable = false)
@TableField("waybill_no")
diff --git a/src/main/java/com/project/logistics/domain/entity/PodPushLogEntity.java b/src/main/java/com/project/receive/domain/entity/PodPushLogEntity.java
similarity index 97%
rename from src/main/java/com/project/logistics/domain/entity/PodPushLogEntity.java
rename to src/main/java/com/project/receive/domain/entity/PodPushLogEntity.java
index 562d578..8dbbb3c 100644
--- a/src/main/java/com/project/logistics/domain/entity/PodPushLogEntity.java
+++ b/src/main/java/com/project/receive/domain/entity/PodPushLogEntity.java
@@ -1,4 +1,4 @@
-package com.project.logistics.domain.entity;
+package com.project.receive.domain.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
diff --git a/src/main/java/com/project/logistics/domain/entity/RoutePushLogEntity.java b/src/main/java/com/project/receive/domain/entity/RoutePushLogEntity.java
similarity index 78%
rename from src/main/java/com/project/logistics/domain/entity/RoutePushLogEntity.java
rename to src/main/java/com/project/receive/domain/entity/RoutePushLogEntity.java
index 81ec32e..7b4aeac 100644
--- a/src/main/java/com/project/logistics/domain/entity/RoutePushLogEntity.java
+++ b/src/main/java/com/project/receive/domain/entity/RoutePushLogEntity.java
@@ -1,4 +1,4 @@
-package com.project.logistics.domain.entity;
+package com.project.receive.domain.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
@@ -33,15 +33,20 @@ public class RoutePushLogEntity extends BaseEntity {
@TableField("order_no")
private String orderNo;
- @Comment("顺丰路由状态代码")
- @Column(name = "order_state_code", length = 32)
- @TableField("order_state_code")
- private String orderStateCode;
+ @Comment("操作时间")
+ @Column(name = "accept_time", length = 200)
+ @TableField("accept_time")
+ private String acceptTime;
+
+ @Comment("路由操作码 (顺丰opCode, 京东status等)")
+ @Column(name = "op_code", length = 32)
+ @TableField("op_code")
+ private String opCode;
@Comment("顺丰路由状态描述(如:已收取、派件中、签收成功等)")
- @Column(name = "order_state_desc", length = 255)
- @TableField("order_state_desc")
- private String orderStateDesc;
+ @Column(name = "remark", length = 2000)
+ @TableField("remark")
+ private String remark;
@Comment("收派员工号")
@Column(name = "emp_code", length = 100)
diff --git a/src/main/java/com/project/receive/domain/service/ReceiveService.java b/src/main/java/com/project/receive/domain/service/ReceiveService.java
new file mode 100644
index 0000000..e0bfde7
--- /dev/null
+++ b/src/main/java/com/project/receive/domain/service/ReceiveService.java
@@ -0,0 +1,181 @@
+package com.project.receive.domain.service;
+
+import com.jayway.jsonpath.JsonPath;
+import com.project.logistics.domain.entity.LogisticsOrderEntity;
+
+import com.project.logistics.domain.enums.*;
+import com.project.logistics.domain.service.base.*;
+import com.project.receive.domain.dto.SfRoutePushRequest;
+import com.project.receive.domain.entity.FeePushLogEntity;
+import com.project.receive.domain.entity.PodPushLogEntity;
+import com.project.receive.domain.entity.RoutePushLogEntity;
+import com.project.receive.domain.service.base.FeePushLogService;
+import com.project.receive.domain.service.base.PodPushLogService;
+import com.project.receive.domain.service.base.RoutePushLogService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+@Slf4j
+@Service
+public class ReceiveService {
+
+ @Autowired
+ private LogisticsOrderService logisticsOrderService;
+ @Autowired
+ private ApiRetryTaskService apiRetryTaskService;
+
+ @Autowired
+ private RoutePushLogService routePushLogService;
+ @Autowired
+ private FeePushLogService feePushLogService;
+ @Autowired
+ private PodPushLogService podPushLogService;
+
+ /**
+ * 处理路由推送:识别揽收与签收
+ */
+ public void processRoutePush(SfRoutePushRequest request) throws Exception {
+ if (request.getBody() == null || request.getBody().getWaybillRoute() == null) {
+ return;
+ }
+
+ // 循环处理顺丰推过来的每一个路由节点
+ for (SfRoutePushRequest.WaybillRouteDetail detail : request.getBody().getWaybillRoute()) {
+
+ log.info(">>> 正在解析路由: 单号={}, 操作码={}, 描述={}",
+ detail.getOrderid(), detail.getOpCode(), detail.getRemark());
+
+ // 1. 存入流水日志
+ saveToLogTable(detail);
+
+ // 2. 状态机流转 (仅针对揽收50和签收80)
+ handleStatusMachine(detail);
+ }
+ }
+
+ private void handleStatusMachine(SfRoutePushRequest.WaybillRouteDetail detail) throws Exception {
+ String orderNo = detail.getOrderid();
+ String opCode = detail.getOpCode();
+
+ // 尝试获取本地订单
+ LogisticsOrderEntity order = logisticsOrderService.getByOrderNo(orderNo);
+ if (order == null) {
+ log.warn(">>> 收到路由推送,但本地库找不到单号: {}", orderNo);
+ return;
+ }
+
+ // 判断操作码
+ if (SfRouteOpCodeEnum.PICKED_UP.getCode().equals(opCode)) {
+ // 已揽收
+ order.setOrderStatus(OrderStatusEnum.PICKED_UP.getCode());
+ logisticsOrderService.updateById(order);
+ // 触发回写 ERP 任务
+ apiRetryTaskService.createNextTask(orderNo, RetryActionEnum.ERP_UPDATE_PICKED_UP);
+ }
+ else if (SfRouteOpCodeEnum.DELIVERED.getCode().equals(opCode)) {
+ // 已签收
+ order.setOrderStatus(OrderStatusEnum.DELIVERED.getCode());
+ logisticsOrderService.updateById(order);
+ // 触发回写 ERP 任务
+ apiRetryTaskService.createNextTask(orderNo, RetryActionEnum.ERP_UPDATE_DELIVERED);
+ }
+ }
+
+ private void saveToLogTable(SfRoutePushRequest.WaybillRouteDetail detail) {
+ RoutePushLogEntity logEntity = new RoutePushLogEntity();
+ logEntity.setOrderNo(detail.getOrderid());
+ logEntity.setWaybillNo(detail.getMailno());
+ logEntity.setOpCode(detail.getOpCode());
+ logEntity.setRemark(detail.getRemark());
+ logEntity.setAcceptTime(detail.getAcceptTime());
+ routePushLogService.save(logEntity);
+ }
+ /**
+ * 处理运费推送
+ */
+ @Transactional(rollbackFor = Exception.class)
+ public void saveFeeLog(String rawJson) {
+ try {
+ // 1. 提取基础信息
+ String orderNo = JsonPath.read(rawJson, "$.orderNo");
+ String waybillNo = JsonPath.read(rawJson, "$.waybillNo");
+
+ // 提取件数 (quantity)
+ Object qtyObj = JsonPath.read(rawJson, "$.quantity");
+ Integer itemCount = new java.math.BigDecimal(String.valueOf(qtyObj)).intValue();
+
+ // 提取重量 (meterageWeightQty)
+ Object weightObj = JsonPath.read(rawJson, "$.meterageWeightQty");
+ BigDecimal weight = new java.math.BigDecimal(String.valueOf(weightObj))
+ .setScale(3, java.math.RoundingMode.HALF_UP);
+ // 2. 【核心】计算总费用
+ // 从 feeList 数组中提取所有 feeAmt 并求和
+ List fees = JsonPath.read(rawJson, "$.feeList[*].feeAmt");
+ BigDecimal totalFee = BigDecimal.ZERO;
+ if (fees != null) {
+ for (Object f : fees) {
+ totalFee = totalFee.add(new BigDecimal(String.valueOf(f)));
+ }
+ }
+ totalFee = totalFee.setScale(2, java.math.RoundingMode.HALF_UP);
+
+ log.info(">>> 解析运费报文: 单号={}, 件数={}, 重量={}, 总费={}",
+ orderNo, itemCount, weight, totalFee);
+
+ // 3. 存入数据库
+ FeePushLogEntity feeEntity = new FeePushLogEntity();
+ feeEntity.setOrderNo(orderNo);
+ feeEntity.setWaybillNo(waybillNo);
+ feeEntity.setItemCount(itemCount);
+ feeEntity.setRealWeightQty(weight);
+ feeEntity.setTotalFeeAmt(totalFee);
+ feeEntity.setSyncStatus(SyncStatusEnum.WAIT.getCode());
+ feeEntity.setRawPushData(rawJson);
+ feePushLogService.save(feeEntity);
+
+ // 4. 驱动状态机与回写任务
+ // 找到主订单,如果当前状态是已揽收,则推进到“运费已清点”状态
+ LogisticsOrderEntity order = logisticsOrderService.getByOrderNo(orderNo);
+ if (order != null) {
+ // 只有状态处于 PICKED_UP 时才更新主表状态,避免覆盖更高级的 DELIVERED 状态
+ if (OrderStatusEnum.PICKED_UP.getCode().equals(order.getOrderStatus())) {
+ order.setOrderStatus(OrderStatusEnum.ERP_FEE_UPDATED.getCode());
+ logisticsOrderService.updateById(order);
+ }
+
+ // 无论主表状态如何,都要触发一次“回写运费到ERP”的任务
+ apiRetryTaskService.createNextTask(orderNo, RetryActionEnum.ERP_UPDATE_FEE);
+ }
+
+ } catch (Exception e) {
+ log.error(">>> 运费报文解析失败: {}", rawJson, e);
+ throw new RuntimeException("FEE_PARSE_ERROR");
+ }
+ }
+ /**
+ * 处理 POD 图片推送
+ */
+ public void savePodPictureLog(String content) throws Exception {
+ // 关键:POD 推送包含巨大的 Base64 字节,这里只做存库。
+ // 具体的解析、下载、传 WebDAV 交由后续的 SF_DOWNLOAD_POD 任务去重试执行。
+ String waybillNo = JsonPath.read(content, "$.waybillNo");
+
+ PodPushLogEntity podLog = new PodPushLogEntity();
+ podLog.setWaybillNo(waybillNo);
+ podLog.setRawPushData(content); // 存入 LONGTEXT 字段
+ podLog.setProcessStatus(SyncStatusEnum.WAIT.getCode());
+ podPushLogService.save(podLog);
+
+ // 查找订单号并触发“处理图片”的异步任务
+ LogisticsOrderEntity order = logisticsOrderService.lambdaQuery()
+ .eq(LogisticsOrderEntity::getSfWaybillNo, waybillNo).one();
+ if (order != null) {
+ apiRetryTaskService.createNextTask(order.getOrderNo(), RetryActionEnum.SF_DOWNLOAD_POD);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/project/receive/domain/service/base/FeePushLogService.java b/src/main/java/com/project/receive/domain/service/base/FeePushLogService.java
new file mode 100644
index 0000000..d2c8eb1
--- /dev/null
+++ b/src/main/java/com/project/receive/domain/service/base/FeePushLogService.java
@@ -0,0 +1,7 @@
+package com.project.receive.domain.service.base;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.project.receive.domain.entity.FeePushLogEntity;
+
+public interface FeePushLogService extends IService {
+}
diff --git a/src/main/java/com/project/receive/domain/service/base/PodPushLogService.java b/src/main/java/com/project/receive/domain/service/base/PodPushLogService.java
new file mode 100644
index 0000000..2819880
--- /dev/null
+++ b/src/main/java/com/project/receive/domain/service/base/PodPushLogService.java
@@ -0,0 +1,7 @@
+package com.project.receive.domain.service.base;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.project.receive.domain.entity.PodPushLogEntity;
+
+public interface PodPushLogService extends IService {
+}
diff --git a/src/main/java/com/project/receive/domain/service/base/RoutePushLogService.java b/src/main/java/com/project/receive/domain/service/base/RoutePushLogService.java
new file mode 100644
index 0000000..7941daa
--- /dev/null
+++ b/src/main/java/com/project/receive/domain/service/base/RoutePushLogService.java
@@ -0,0 +1,7 @@
+package com.project.receive.domain.service.base;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.project.receive.domain.entity.RoutePushLogEntity;
+
+public interface RoutePushLogService extends IService {
+}
diff --git a/src/main/java/com/project/receive/domain/service/base/impl/FeePushLogServiceImpl.java b/src/main/java/com/project/receive/domain/service/base/impl/FeePushLogServiceImpl.java
new file mode 100644
index 0000000..6cdb3f2
--- /dev/null
+++ b/src/main/java/com/project/receive/domain/service/base/impl/FeePushLogServiceImpl.java
@@ -0,0 +1,12 @@
+package com.project.receive.domain.service.base.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.project.receive.domain.entity.FeePushLogEntity;
+import com.project.receive.domain.service.base.FeePushLogService;
+import com.project.receive.mapper.FeePushLogMapper;
+import org.springframework.stereotype.Service;
+
+@Service
+public class FeePushLogServiceImpl extends ServiceImpl
+ implements FeePushLogService {
+}
diff --git a/src/main/java/com/project/receive/domain/service/base/impl/PodPushLogServiceImpl.java b/src/main/java/com/project/receive/domain/service/base/impl/PodPushLogServiceImpl.java
new file mode 100644
index 0000000..21b7f67
--- /dev/null
+++ b/src/main/java/com/project/receive/domain/service/base/impl/PodPushLogServiceImpl.java
@@ -0,0 +1,13 @@
+package com.project.receive.domain.service.base.impl;
+
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.project.receive.domain.entity.PodPushLogEntity;
+import com.project.receive.domain.service.base.PodPushLogService;
+import com.project.receive.mapper.PodPushLogMapper;
+import org.springframework.stereotype.Service;
+
+@Service
+public class PodPushLogServiceImpl extends ServiceImpl
+ implements PodPushLogService {
+}
diff --git a/src/main/java/com/project/receive/domain/service/base/impl/RoutePushLogServiceImpl.java b/src/main/java/com/project/receive/domain/service/base/impl/RoutePushLogServiceImpl.java
new file mode 100644
index 0000000..b28008d
--- /dev/null
+++ b/src/main/java/com/project/receive/domain/service/base/impl/RoutePushLogServiceImpl.java
@@ -0,0 +1,13 @@
+package com.project.receive.domain.service.base.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.project.receive.domain.entity.RoutePushLogEntity;
+import com.project.receive.domain.service.base.RoutePushLogService;
+import com.project.receive.mapper.RoutePushLogMapper;
+import org.springframework.stereotype.Service;
+
+
+@Service
+public class RoutePushLogServiceImpl extends ServiceImpl
+ implements RoutePushLogService {
+}
diff --git a/src/main/java/com/project/receive/mapper/FeePushLogMapper.java b/src/main/java/com/project/receive/mapper/FeePushLogMapper.java
new file mode 100644
index 0000000..7ceae64
--- /dev/null
+++ b/src/main/java/com/project/receive/mapper/FeePushLogMapper.java
@@ -0,0 +1,10 @@
+package com.project.receive.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.project.receive.domain.entity.FeePushLogEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+
+public interface FeePushLogMapper extends BaseMapper {
+}
diff --git a/src/main/java/com/project/receive/mapper/PodPushLogMapper.java b/src/main/java/com/project/receive/mapper/PodPushLogMapper.java
new file mode 100644
index 0000000..8bdb591
--- /dev/null
+++ b/src/main/java/com/project/receive/mapper/PodPushLogMapper.java
@@ -0,0 +1,10 @@
+package com.project.receive.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.project.receive.domain.entity.PodPushLogEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+
+public interface PodPushLogMapper extends BaseMapper {
+}
diff --git a/src/main/java/com/project/receive/mapper/RoutePushLogMapper.java b/src/main/java/com/project/receive/mapper/RoutePushLogMapper.java
new file mode 100644
index 0000000..c2ac84c
--- /dev/null
+++ b/src/main/java/com/project/receive/mapper/RoutePushLogMapper.java
@@ -0,0 +1,10 @@
+package com.project.receive.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.project.receive.domain.entity.RoutePushLogEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+
+public interface RoutePushLogMapper extends BaseMapper {
+}
diff --git a/src/main/java/com/project/receive/strategy/ErpUpdateFeeHandler.java b/src/main/java/com/project/receive/strategy/ErpUpdateFeeHandler.java
new file mode 100644
index 0000000..f75b1cf
--- /dev/null
+++ b/src/main/java/com/project/receive/strategy/ErpUpdateFeeHandler.java
@@ -0,0 +1,58 @@
+package com.project.receive.strategy;
+
+import com.project.logistics.domain.entity.ApiRetryTaskEntity;
+import com.project.logistics.domain.entity.LogisticsOrderEntity;
+import com.project.logistics.domain.enums.RetryActionEnum;
+import com.project.logistics.domain.enums.SyncStatusEnum;
+import com.project.logistics.domain.service.base.ErpService;
+import com.project.logistics.domain.strategy.ApiTaskHandler;
+import com.project.receive.domain.entity.FeePushLogEntity;
+import com.project.receive.domain.service.base.FeePushLogService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+public class ErpUpdateFeeHandler implements ApiTaskHandler {
+
+ @Autowired
+ private ErpService erpService;
+
+ @Autowired
+ private FeePushLogService feePushLogService;
+
+ @Override
+ public String getActionCode() {
+ return RetryActionEnum.ERP_UPDATE_FEE.getCode();
+ }
+
+ @Override
+ public void handle(ApiRetryTaskEntity task, LogisticsOrderEntity order) throws Exception {
+ log.info(">>> 开始回写运费件数到 ERP,单号: {}", order.getOrderNo());
+
+ // 1. 获取顺丰推过来的费用明细 (取最新的一条)
+ FeePushLogEntity feeLog = feePushLogService.lambdaQuery()
+ .eq(FeePushLogEntity::getOrderNo, order.getOrderNo())
+ .orderByDesc(FeePushLogEntity::getCreateTime)
+ .last("limit 1")
+ .one();
+
+ if (feeLog == null) {
+ throw new RuntimeException("尚未收到顺丰的计费推送报文,任务等待重试");
+ }
+
+ // 2. 调用 U9 专线执行更新
+ erpService.updateFeeToErp(
+ order.getOrderNo(),
+ feeLog.getItemCount(),
+ feeLog.getTotalFeeAmt()
+ );
+
+ // 3. 更新费用日志的同步状态
+ feeLog.setSyncStatus(SyncStatusEnum.SUCCESS.getCode());
+ feePushLogService.updateById(feeLog);
+
+ log.info(">>> ERP 费用回写成功,单号: {}", order.getOrderNo());
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/project/receivedemo/controller/ReceiveDemoController.java b/src/main/java/com/project/receivedemo/controller/ReceiveDemoController.java
new file mode 100644
index 0000000..3356d33
--- /dev/null
+++ b/src/main/java/com/project/receivedemo/controller/ReceiveDemoController.java
@@ -0,0 +1,162 @@
+package com.project.receivedemo.controller;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.project.receivedemo.dto.*;
+import com.project.receivedemo.utils.SfDecryptUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.FileOutputStream;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+@RestController
+@Slf4j
+@RequestMapping("/api/")
+public class ReceiveDemoController {
+
+ private static final String SF_CHECKWORD = "ZXmoWOQdSd2UTBmSP6Kv3VW9Q4N5dJqz";
+ /**
+ * 接收顺丰订单状态推送接口
+ * 对应文档 2.6 节 JSON 示例
+ */
+ @PostMapping("/pushOrderState")
+ public SfPushResponse receiveOrderState(@RequestBody SfPushRequest request) {
+ // 1. 打印接收到的原始数据日志
+ log.info("==== 收到顺丰状态推送 ====");
+ log.info("Request ID: {}", request.getRequestId());
+ log.info("Timestamp: {}", request.getTimestamp());
+
+ if (request.getOrderState() != null) {
+ request.getOrderState().forEach(state -> {
+ log.info("订单号: {}, 运单号: {}, 状态码: {}, 描述: {}",
+ state.getOrderNo(),
+ state.getWaybillNo(),
+ state.getOrderStateCode(),
+ state.getOrderStateDesc());
+ });
+ }
+
+ // 2. 根据文档 2.7 节,返回成功响应
+ return SfPushResponse.ok();
+ }
+
+
+ /**
+ * 接收顺丰路由推送
+ * 对应文档 2.5 节 JSON 示例
+ */
+ @PostMapping("/pushRoute")
+ public SfRouteResponse receiveRoute(@RequestBody SfRoutePushRequest request) {
+ log.info(">>>> 收到顺丰路由信息推送 <<<<");
+
+ if (request.getBody() != null && request.getBody().getWaybillRoute() != null) {
+ for (WaybillRoute route : request.getBody().getWaybillRoute()) {
+ log.info("运单号: {}, 订单号: {}, 时间: {}, 状态: {}, 备注: {}",
+ route.getMailno(),
+ route.getOrderid(),
+ route.getAcceptTime(),
+ route.getOpCode(),
+ route.getRemark());
+ }
+ } else {
+ log.warn("收到的路由数据内容为空");
+ }
+
+ // 根据文档 2.6,必须返回 0000 告知顺丰接收成功,否则顺丰会重复推送
+ return SfRouteResponse.ok();
+ }
+
+
+ @RequestMapping("/pushOrderStateRaw")
+ public String receiveRaw(@RequestBody String rawBody) {
+ log.info("收到原始报文: {}", rawBody);
+ // 返回 JSON 格式的成功字符串
+ return "{\"success\":\"true\",\"code\":\"0\",\"msg\":\"\"}";
+ }
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ /**
+ * 接收顺丰运费推送
+ * 报文类型: application/x-www-form-urlencoded
+ */
+ @PostMapping(value = "/pushFee", consumes = "application/x-www-form-urlencoded")
+ public SfFeeResponse receiveFee(
+ @RequestParam("content") String content,
+ @RequestParam(value = "sign", required = false) String sign) {
+
+ log.info(">>>> 收到顺丰运费推送 <<<<");
+ log.info("签名(sign): {}", sign);
+ log.info("原始内容(content): {}", content);
+
+ try {
+ // 将 content 字符串解析为 Java 对象
+ SfFeeContent feeData = objectMapper.readValue(content, SfFeeContent.class);
+
+ log.info("解析成功 - 运单号: {}, 订单号: {}, 计费重量: {}",
+ feeData.getWaybillNo(),
+ feeData.getOrderNo(),
+ feeData.getMeterageWeightQty());
+
+ if (feeData.getFeeList() != null) {
+ feeData.getFeeList().forEach(fee -> {
+ log.info("费用项 - 类型: {}, 金额: {}", fee.getFeeTypeCode(), fee.getFeeAmt());
+ });
+ }
+
+ // 返回成功响应 (code 必须为 200)
+ return SfFeeResponse.ok("your_partner_code");
+
+ } catch (Exception e) {
+ log.error("解析顺丰运费推送失败", e);
+ SfFeeResponse error = new SfFeeResponse();
+ error.setCode(400);
+ error.setMessage("解析异常");
+ return error;
+ }
+ }
+
+ @PostMapping("/pushElectronicReceipt")
+ public SfPicturePushResponse receiveReceipt(@RequestBody SfPicturePushRequest request) {
+ log.info(">>>> 收到顺丰电子回单(IN149)推送, 运单号: {} <<<<", request.getWaybillNo());
+
+ try {
+ // 步骤 1:解密最外层的 content 得到内部 JSON 字符串
+ byte[] firstLevelDecrypted = SfDecryptUtil.decrypt(request.getContent(), SF_CHECKWORD);
+ String innerJsonStr = new String(firstLevelDecrypted, "UTF-8");
+ log.info("第一层解密成功");
+
+ // 步骤 2:解析内部 JSON 获取文件密文
+ SfInnerContent innerContent = objectMapper.readValue(innerJsonStr, SfInnerContent.class);
+ String encryptedFileBase64 = innerContent.getContent();
+
+ // 步骤 3:对文件密文进行第二层 AES 解密 (根据文档示例,文件内容也是加密的)
+ // 先 Base64 解码,再 AES 解密
+ byte[] pdfBytes = SfDecryptUtil.decrypt(encryptedFileBase64, SF_CHECKWORD);
+ log.info("第二层解密(文件流)成功,大小: {} bytes", pdfBytes.length);
+
+ // 步骤 4:保存为 PDF 文件到当前目录
+ String fileName = request.getWaybillNo() + "_receipt.pdf";
+ Path path = Paths.get(System.getProperty("user.dir"), fileName);
+
+ try (FileOutputStream fos = new FileOutputStream(path.toFile())) {
+ fos.write(pdfBytes);
+ fos.flush();
+ }
+
+ log.info("电子回单已保存至: {}", path.toAbsolutePath());
+
+ return SfPicturePushResponse.ok();
+
+ } catch (Exception e) {
+ log.error("处理电子回单推送失败", e);
+ SfPicturePushResponse error = new SfPicturePushResponse();
+ error.setReturnCode("1000");
+ error.setReturnMsg("解析保存失败: " + e.getMessage());
+ return error;
+ }
+ }
+}
diff --git a/src/main/java/com/project/receive/dto/FeeInfo.java b/src/main/java/com/project/receivedemo/dto/FeeInfo.java
similarity index 91%
rename from src/main/java/com/project/receive/dto/FeeInfo.java
rename to src/main/java/com/project/receivedemo/dto/FeeInfo.java
index c4cfe17..2cdd572 100644
--- a/src/main/java/com/project/receive/dto/FeeInfo.java
+++ b/src/main/java/com/project/receivedemo/dto/FeeInfo.java
@@ -1,4 +1,4 @@
-package com.project.receive.dto;
+package com.project.receivedemo.dto;
import lombok.Data;
diff --git a/src/main/java/com/project/receive/dto/OrderStateDetail.java b/src/main/java/com/project/receivedemo/dto/OrderStateDetail.java
similarity index 91%
rename from src/main/java/com/project/receive/dto/OrderStateDetail.java
rename to src/main/java/com/project/receivedemo/dto/OrderStateDetail.java
index 5eb2f58..603be13 100644
--- a/src/main/java/com/project/receive/dto/OrderStateDetail.java
+++ b/src/main/java/com/project/receivedemo/dto/OrderStateDetail.java
@@ -1,4 +1,4 @@
-package com.project.receive.dto;
+package com.project.receivedemo.dto;
import lombok.Data;
diff --git a/src/main/java/com/project/receive/dto/RouteBody.java b/src/main/java/com/project/receivedemo/dto/RouteBody.java
similarity index 85%
rename from src/main/java/com/project/receive/dto/RouteBody.java
rename to src/main/java/com/project/receivedemo/dto/RouteBody.java
index bdd4f1d..265eeef 100644
--- a/src/main/java/com/project/receive/dto/RouteBody.java
+++ b/src/main/java/com/project/receivedemo/dto/RouteBody.java
@@ -1,4 +1,4 @@
-package com.project.receive.dto;
+package com.project.receivedemo.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
diff --git a/src/main/java/com/project/receive/dto/SfFeeContent.java b/src/main/java/com/project/receivedemo/dto/SfFeeContent.java
similarity index 94%
rename from src/main/java/com/project/receive/dto/SfFeeContent.java
rename to src/main/java/com/project/receivedemo/dto/SfFeeContent.java
index 6faf1b6..b845b4f 100644
--- a/src/main/java/com/project/receive/dto/SfFeeContent.java
+++ b/src/main/java/com/project/receivedemo/dto/SfFeeContent.java
@@ -1,4 +1,4 @@
-package com.project.receive.dto;
+package com.project.receivedemo.dto;
import lombok.Data;
diff --git a/src/main/java/com/project/receive/dto/SfFeeResponse.java b/src/main/java/com/project/receivedemo/dto/SfFeeResponse.java
similarity index 92%
rename from src/main/java/com/project/receive/dto/SfFeeResponse.java
rename to src/main/java/com/project/receivedemo/dto/SfFeeResponse.java
index be3dfa9..45bb335 100644
--- a/src/main/java/com/project/receive/dto/SfFeeResponse.java
+++ b/src/main/java/com/project/receivedemo/dto/SfFeeResponse.java
@@ -1,4 +1,4 @@
-package com.project.receive.dto;
+package com.project.receivedemo.dto;
import lombok.Data;
diff --git a/src/main/java/com/project/receive/dto/SfInnerContent.java b/src/main/java/com/project/receivedemo/dto/SfInnerContent.java
similarity index 84%
rename from src/main/java/com/project/receive/dto/SfInnerContent.java
rename to src/main/java/com/project/receivedemo/dto/SfInnerContent.java
index 2d49eeb..e2c2b08 100644
--- a/src/main/java/com/project/receive/dto/SfInnerContent.java
+++ b/src/main/java/com/project/receivedemo/dto/SfInnerContent.java
@@ -1,4 +1,4 @@
-package com.project.receive.dto;
+package com.project.receivedemo.dto;
import lombok.Data;
diff --git a/src/main/java/com/project/receive/dto/SfPicturePushRequest.java b/src/main/java/com/project/receivedemo/dto/SfPicturePushRequest.java
similarity index 90%
rename from src/main/java/com/project/receive/dto/SfPicturePushRequest.java
rename to src/main/java/com/project/receivedemo/dto/SfPicturePushRequest.java
index 5e9267d..dcef394 100644
--- a/src/main/java/com/project/receive/dto/SfPicturePushRequest.java
+++ b/src/main/java/com/project/receivedemo/dto/SfPicturePushRequest.java
@@ -1,4 +1,4 @@
-package com.project.receive.dto;
+package com.project.receivedemo.dto;
import lombok.Data;
diff --git a/src/main/java/com/project/receive/dto/SfPicturePushResponse.java b/src/main/java/com/project/receivedemo/dto/SfPicturePushResponse.java
similarity index 95%
rename from src/main/java/com/project/receive/dto/SfPicturePushResponse.java
rename to src/main/java/com/project/receivedemo/dto/SfPicturePushResponse.java
index eacd394..aaa939c 100644
--- a/src/main/java/com/project/receive/dto/SfPicturePushResponse.java
+++ b/src/main/java/com/project/receivedemo/dto/SfPicturePushResponse.java
@@ -1,4 +1,4 @@
-package com.project.receive.dto;
+package com.project.receivedemo.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
diff --git a/src/main/java/com/project/receive/dto/SfPushRequest.java b/src/main/java/com/project/receivedemo/dto/SfPushRequest.java
similarity index 86%
rename from src/main/java/com/project/receive/dto/SfPushRequest.java
rename to src/main/java/com/project/receivedemo/dto/SfPushRequest.java
index e4c857e..eeaaf08 100644
--- a/src/main/java/com/project/receive/dto/SfPushRequest.java
+++ b/src/main/java/com/project/receivedemo/dto/SfPushRequest.java
@@ -1,4 +1,4 @@
-package com.project.receive.dto;
+package com.project.receivedemo.dto;
import lombok.Data;
diff --git a/src/main/java/com/project/receive/dto/SfPushResponse.java b/src/main/java/com/project/receivedemo/dto/SfPushResponse.java
similarity index 90%
rename from src/main/java/com/project/receive/dto/SfPushResponse.java
rename to src/main/java/com/project/receivedemo/dto/SfPushResponse.java
index f8f2801..9867c7d 100644
--- a/src/main/java/com/project/receive/dto/SfPushResponse.java
+++ b/src/main/java/com/project/receivedemo/dto/SfPushResponse.java
@@ -1,4 +1,4 @@
-package com.project.receive.dto;
+package com.project.receivedemo.dto;
import lombok.Data;
diff --git a/src/main/java/com/project/receive/dto/SfRoutePushRequest.java b/src/main/java/com/project/receivedemo/dto/SfRoutePushRequest.java
similarity index 82%
rename from src/main/java/com/project/receive/dto/SfRoutePushRequest.java
rename to src/main/java/com/project/receivedemo/dto/SfRoutePushRequest.java
index fb91a07..67fdd11 100644
--- a/src/main/java/com/project/receive/dto/SfRoutePushRequest.java
+++ b/src/main/java/com/project/receivedemo/dto/SfRoutePushRequest.java
@@ -1,4 +1,4 @@
-package com.project.receive.dto;
+package com.project.receivedemo.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
diff --git a/src/main/java/com/project/receive/dto/SfRouteResponse.java b/src/main/java/com/project/receivedemo/dto/SfRouteResponse.java
similarity index 92%
rename from src/main/java/com/project/receive/dto/SfRouteResponse.java
rename to src/main/java/com/project/receivedemo/dto/SfRouteResponse.java
index 72ae18a..4b21143 100644
--- a/src/main/java/com/project/receive/dto/SfRouteResponse.java
+++ b/src/main/java/com/project/receivedemo/dto/SfRouteResponse.java
@@ -1,4 +1,4 @@
-package com.project.receive.dto;
+package com.project.receivedemo.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
diff --git a/src/main/java/com/project/receive/dto/WaybillRoute.java b/src/main/java/com/project/receivedemo/dto/WaybillRoute.java
similarity index 95%
rename from src/main/java/com/project/receive/dto/WaybillRoute.java
rename to src/main/java/com/project/receivedemo/dto/WaybillRoute.java
index bf98fa6..6301376 100644
--- a/src/main/java/com/project/receive/dto/WaybillRoute.java
+++ b/src/main/java/com/project/receivedemo/dto/WaybillRoute.java
@@ -1,4 +1,4 @@
-package com.project.receive.dto;
+package com.project.receivedemo.dto;
import lombok.Data;
diff --git a/src/main/java/com/project/receive/utils/SfDecryptUtil.java b/src/main/java/com/project/receivedemo/utils/SfDecryptUtil.java
similarity index 95%
rename from src/main/java/com/project/receive/utils/SfDecryptUtil.java
rename to src/main/java/com/project/receivedemo/utils/SfDecryptUtil.java
index 98b6cb0..06aa183 100644
--- a/src/main/java/com/project/receive/utils/SfDecryptUtil.java
+++ b/src/main/java/com/project/receivedemo/utils/SfDecryptUtil.java
@@ -1,4 +1,4 @@
-package com.project.receive.utils;
+package com.project.receivedemo.utils;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 330df37..3c66a8c 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -97,4 +97,6 @@ fixed-rule:
senderPayContact: '王盛荣'
senderMobile: '13480155048'
senderAddress: '广东省广州市番禺区石碁镇南荔东路56号'
- monthlyCard: '7551234567'
\ No newline at end of file
+ monthlyCard: '7551234567'
+scheduled-task:
+ owner: local
\ No newline at end of file