From 804a5a7bef5200ad61208847e58a824656752c63 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com>
Date: Fri, 6 Feb 2026 01:09:41 +0800
Subject: [PATCH] =?UTF-8?q?feat(shop):=20=E6=B7=BB=E5=8A=A0=E5=BE=AE?=
=?UTF-8?q?=E4=BF=A1=E5=B0=8F=E7=A8=8B=E5=BA=8F=E5=8F=91=E8=B4=A7=E4=BF=A1?=
=?UTF-8?q?=E6=81=AF=E8=87=AA=E5=8A=A8=E5=90=8C=E6=AD=A5=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 ShopWechatShippingSyncService 接口及实现类
- 在订单发货时自动同步实物快递和无需物流的发货信息到微信后台
- 添加微信小程序 access_token 获取服务及缓存机制
- 优化订单发货逻辑,支持无需物流/自提订单的自动同步处理
- 添加详细的日志记录和异常处理机制
- 实现发货信息同步失败时的容错处理
---
.../service/WxMiniappAccessTokenService.java | 18 ++
.../impl/WxMiniappAccessTokenServiceImpl.java | 108 ++++++++
.../shop/controller/ShopOrderController.java | 51 +++-
.../ShopWechatShippingSyncService.java | 24 ++
.../impl/ShopOrderDeliveryServiceImpl.java | 49 +---
.../ShopWechatShippingSyncServiceImpl.java | 231 ++++++++++++++++++
6 files changed, 433 insertions(+), 48 deletions(-)
create mode 100644 src/main/java/com/gxwebsoft/common/system/service/WxMiniappAccessTokenService.java
create mode 100644 src/main/java/com/gxwebsoft/common/system/service/impl/WxMiniappAccessTokenServiceImpl.java
create mode 100644 src/main/java/com/gxwebsoft/shop/service/ShopWechatShippingSyncService.java
create mode 100644 src/main/java/com/gxwebsoft/shop/service/impl/ShopWechatShippingSyncServiceImpl.java
diff --git a/src/main/java/com/gxwebsoft/common/system/service/WxMiniappAccessTokenService.java b/src/main/java/com/gxwebsoft/common/system/service/WxMiniappAccessTokenService.java
new file mode 100644
index 0000000..3a3a44b
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/system/service/WxMiniappAccessTokenService.java
@@ -0,0 +1,18 @@
+package com.gxwebsoft.common.system.service;
+
+/**
+ * 微信小程序 access_token 获取服务(按租户)。
+ *
+ *
用于调用微信小程序开放接口(例如:上传发货信息)。
+ */
+public interface WxMiniappAccessTokenService {
+
+ /**
+ * 获取指定租户的小程序 access_token(内部带缓存)。
+ *
+ * @param tenantId 租户ID
+ * @return access_token
+ */
+ String getAccessToken(Integer tenantId);
+}
+
diff --git a/src/main/java/com/gxwebsoft/common/system/service/impl/WxMiniappAccessTokenServiceImpl.java b/src/main/java/com/gxwebsoft/common/system/service/impl/WxMiniappAccessTokenServiceImpl.java
new file mode 100644
index 0000000..b6dbe7a
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/system/service/impl/WxMiniappAccessTokenServiceImpl.java
@@ -0,0 +1,108 @@
+package com.gxwebsoft.common.system.service.impl;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.gxwebsoft.common.core.exception.BusinessException;
+import com.gxwebsoft.common.core.utils.RedisUtil;
+import com.gxwebsoft.common.system.service.WxMiniappAccessTokenService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.concurrent.TimeUnit;
+
+import static com.gxwebsoft.common.core.constants.RedisConstants.ACCESS_TOKEN_KEY;
+import static com.gxwebsoft.common.core.constants.RedisConstants.MP_WX_KEY;
+
+/**
+ * 微信小程序 access_token 获取实现(按租户)。
+ *
+ * 复用现有缓存结构:
+ *
+ * - 小程序配置:Redis key = {@code mp-weixin:{tenantId}},value 为 JSON,包含 appId/appSecret
+ * - access_token:Redis key = {@code access-token:{tenantId}},value 为微信返回的 JSON 字符串
+ *
+ *
+ */
+@Slf4j
+@Service
+public class WxMiniappAccessTokenServiceImpl implements WxMiniappAccessTokenService {
+
+ @Resource
+ private RedisUtil redisUtil;
+
+ @Override
+ public String getAccessToken(Integer tenantId) {
+ if (tenantId == null) {
+ throw new BusinessException("tenantId 不能为空");
+ }
+
+ final String tokenCacheKey = ACCESS_TOKEN_KEY + ":" + tenantId;
+
+ // 1) 优先从缓存取(兼容 JSON 或纯字符串 token 的历史格式)
+ String cachedValue = redisUtil.get(tokenCacheKey);
+ if (StrUtil.isNotBlank(cachedValue)) {
+ try {
+ JSONObject cachedJson = JSON.parseObject(cachedValue);
+ String accessToken = cachedJson.getString("access_token");
+ if (StrUtil.isNotBlank(accessToken)) {
+ return accessToken;
+ }
+ } catch (Exception ignore) {
+ // 旧格式:直接存 token
+ return cachedValue;
+ }
+ }
+
+ // 2) 缓存没有则从租户配置获取 appId/appSecret
+ final String wxConfigKey = MP_WX_KEY + tenantId;
+ final String wxConfigValue = redisUtil.get(wxConfigKey);
+ if (StrUtil.isBlank(wxConfigValue)) {
+ throw new BusinessException("未找到微信小程序配置,请检查缓存key: " + wxConfigKey);
+ }
+
+ JSONObject wxConfig;
+ try {
+ wxConfig = JSON.parseObject(wxConfigValue);
+ } catch (Exception e) {
+ throw new BusinessException("微信小程序配置格式错误: " + e.getMessage());
+ }
+
+ final String appId = wxConfig.getString("appId");
+ final String appSecret = wxConfig.getString("appSecret");
+ if (StrUtil.isBlank(appId) || StrUtil.isBlank(appSecret)) {
+ throw new BusinessException("微信小程序配置不完整(appId/appSecret)");
+ }
+
+ // 3) 调用微信接口获取 token
+ final String apiUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential"
+ + "&appid=" + appId + "&secret=" + appSecret;
+ String result = HttpUtil.get(apiUrl);
+
+ JSONObject json = JSON.parseObject(result);
+ if (json.containsKey("errcode") && json.getIntValue("errcode") != 0) {
+ Integer errcode = json.getInteger("errcode");
+ String errmsg = json.getString("errmsg");
+ throw new BusinessException("获取小程序access_token失败: " + errmsg + " (errcode: " + errcode + ")");
+ }
+
+ String accessToken = json.getString("access_token");
+ Integer expiresIn = json.getInteger("expires_in");
+ if (StrUtil.isBlank(accessToken)) {
+ throw new BusinessException("获取小程序access_token失败: access_token为空");
+ }
+
+ // 4) 缓存微信原始 JSON(与现有实现保持一致),提前5分钟过期
+ long ttlSeconds = 7000L;
+ if (expiresIn != null && expiresIn > 300) {
+ ttlSeconds = expiresIn - 300L;
+ }
+ redisUtil.set(tokenCacheKey, result, ttlSeconds, TimeUnit.SECONDS);
+
+ log.info("获取小程序access_token成功 - tenantId={}, ttlSeconds={}", tenantId, ttlSeconds);
+ return accessToken;
+ }
+}
+
diff --git a/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java b/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java
index 9456ebb..1ebcb7c 100644
--- a/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java
+++ b/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java
@@ -95,6 +95,8 @@ public class ShopOrderController extends BaseController {
@Resource
private ShopOrderDeliveryService shopOrderDeliveryService;
@Resource
+ private ShopWechatShippingSyncService shopWechatShippingSyncService;
+ @Resource
private PaymentService paymentService;
@Operation(summary = "分页查询订单")
@@ -274,16 +276,47 @@ public class ShopOrderController extends BaseController {
}
// 发货状态从“未发货(10)”变更为“已发货(20)”时,记录发货信息
if (Objects.equals(shopOrderNow.getDeliveryStatus(), 10) && Objects.equals(shopOrder.getDeliveryStatus(), 20)) {
- ShopOrderDelivery shopOrderDelivery = new ShopOrderDelivery();
- shopOrderDelivery.setOrderId(shopOrder.getOrderId());
- shopOrderDelivery.setDeliveryMethod(30);
- shopOrderDelivery.setExpressId(shopOrder.getExpressId());
- shopOrderDelivery.setSendName(shopOrder.getSendName());
- shopOrderDelivery.setSendPhone(shopOrder.getSendPhone());
- shopOrderDelivery.setSendAddress(shopOrder.getSendAddress());
- shopOrderDeliveryService.save(shopOrderDelivery);
+ // 1) 无需物流/自提:不走快递100下单,直接置为已发货并同步到微信后台
+ if (shopOrder.getExpressId() == null || shopOrder.getExpressId() == 0) {
+ ShopOrderDelivery shopOrderDelivery = new ShopOrderDelivery();
+ shopOrderDelivery.setOrderId(shopOrder.getOrderId());
+ shopOrderDelivery.setDeliveryMethod(20);
+ shopOrderDelivery.setSendName(shopOrder.getSendName());
+ shopOrderDelivery.setSendPhone(shopOrder.getSendPhone());
+ shopOrderDelivery.setSendAddress(shopOrder.getSendAddress());
+ shopOrderDeliveryService.save(shopOrderDelivery);
- shopOrderDeliveryService.setExpress(getLoginUser(), shopOrderDelivery, shopOrder);
+ ShopOrder patch = new ShopOrder();
+ patch.setOrderId(shopOrder.getOrderId());
+ patch.setDeliveryStatus(20);
+ patch.setDeliveryTime(LocalDateTime.now());
+ shopOrderService.updateById(patch);
+
+ // 同步到微信后台(发货信息录入)
+ ShopOrder syncOrder = shopOrderNow;
+ syncOrder.setDeliveryStatus(20);
+ syncOrder.setDeliveryTime(patch.getDeliveryTime());
+ try {
+ shopWechatShippingSyncService.uploadNoLogisticsShippingInfo(syncOrder);
+ } catch (Exception e) {
+ logger.warn("同步微信发货信息失败(无需物流,不影响发货成功) - orderId={}", shopOrder.getOrderId(), e);
+ }
+ } else {
+ // 2) 实物快递:创建发货单并走快递100电子面单,发货成功后同步微信后台
+ ShopOrderDelivery shopOrderDelivery = new ShopOrderDelivery();
+ shopOrderDelivery.setOrderId(shopOrder.getOrderId());
+ shopOrderDelivery.setDeliveryMethod(30);
+ shopOrderDelivery.setExpressId(shopOrder.getExpressId());
+ shopOrderDelivery.setSendName(shopOrder.getSendName());
+ shopOrderDelivery.setSendPhone(shopOrder.getSendPhone());
+ shopOrderDelivery.setSendAddress(shopOrder.getSendAddress());
+ shopOrderDeliveryService.save(shopOrderDelivery);
+
+ // 需要用订单的持久化字段(例如 addressId/tenantId/userId/transactionId),同时补齐临时的发货地址
+ ShopOrder orderForDelivery = shopOrderNow;
+ orderForDelivery.setSendAddress(shopOrder.getSendAddress());
+ shopOrderDeliveryService.setExpress(getLoginUser(), shopOrderDelivery, orderForDelivery);
+ }
}
if (shopOrderService.updateById(shopOrder)) {
diff --git a/src/main/java/com/gxwebsoft/shop/service/ShopWechatShippingSyncService.java b/src/main/java/com/gxwebsoft/shop/service/ShopWechatShippingSyncService.java
new file mode 100644
index 0000000..cebc186
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/shop/service/ShopWechatShippingSyncService.java
@@ -0,0 +1,24 @@
+package com.gxwebsoft.shop.service;
+
+import com.gxwebsoft.shop.entity.ShopExpress;
+import com.gxwebsoft.shop.entity.ShopOrder;
+import com.gxwebsoft.shop.entity.ShopOrderDelivery;
+
+/**
+ * 微信小程序“发货信息管理”同步服务。
+ *
+ * 用于将系统内发货/无需物流状态同步到微信小程序后台,避免人工在后台录入。
+ */
+public interface ShopWechatShippingSyncService {
+
+ /**
+ * 实物快递发货同步到微信后台(上传运单号/快递公司)。
+ */
+ boolean uploadExpressShippingInfo(ShopOrder order, ShopOrderDelivery orderDelivery, ShopExpress express);
+
+ /**
+ * 无需物流/自提发货同步到微信后台(上传无需物流)。
+ */
+ boolean uploadNoLogisticsShippingInfo(ShopOrder order);
+}
+
diff --git a/src/main/java/com/gxwebsoft/shop/service/impl/ShopOrderDeliveryServiceImpl.java b/src/main/java/com/gxwebsoft/shop/service/impl/ShopOrderDeliveryServiceImpl.java
index 41cd996..2b404df 100644
--- a/src/main/java/com/gxwebsoft/shop/service/impl/ShopOrderDeliveryServiceImpl.java
+++ b/src/main/java/com/gxwebsoft/shop/service/impl/ShopOrderDeliveryServiceImpl.java
@@ -12,6 +12,7 @@ import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.kuaidi100.sdk.pojo.HttpResult;
import com.kuaidi100.sdk.request.BOrderReq;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@@ -27,6 +28,7 @@ import java.util.Map;
* @since 2025-01-11 10:45:12
*/
@Service
+@Slf4j
public class ShopOrderDeliveryServiceImpl extends ServiceImpl implements ShopOrderDeliveryService {
@Resource
private ShopExpressService expressService;
@@ -40,6 +42,8 @@ public class ShopOrderDeliveryServiceImpl extends ServiceImpl pageRel(ShopOrderDeliveryParam param) {
@@ -111,45 +115,12 @@ public class ShopOrderDeliveryServiceImpl extends ServiceImpl orderGoodsList = shopOrderGoodsService.getListByOrderId(order.getOrderId());
- // 上传小程序发货信息
-// WxMaOrderShippingInfoUploadRequest uploadRequest = new WxMaOrderShippingInfoUploadRequest();
-// uploadRequest.setLogisticsType(1);
-// uploadRequest.setDeliveryMode(1);
-//
-// OrderKeyBean orderKeyBean = new OrderKeyBean();
-// orderKeyBean.setOrderNumberType(2);
-// orderKeyBean.setTransactionId(order.getTransactionId());
-// uploadRequest.setOrderKey(orderKeyBean);
-//
-// List shippingList = new ArrayList<>();
-// ShippingListBean shippingListBean = new ShippingListBean();
-// shippingListBean.setTrackingNo((String) bOrderData.get("kuaidinum"));
-// shippingListBean.setExpressCompany(express.getWxCode());
-// ContactBean contactBean = new ContactBean();
-// contactBean.setReceiverContact(user.getMobile());
-// shippingListBean.setContact(contactBean);
-//
-// ShopGoods shopGoods = shopGoodsService.getById(orderGoodsList.get(0).getGoodsId());
-//
-// String itemDesc = shopGoods.getName();
-// if (orderGoodsList.size() > 1) itemDesc += "等" + orderGoodsList.size() + "件商品";
-// shippingListBean.setItemDesc(itemDesc);
-// shippingList.add(shippingListBean);
-// uploadRequest.setShippingList(shippingList);
-//
-// uploadRequest.setUploadTime(new DateTime().toString(DatePattern.UTC_WITH_ZONE_OFFSET_PATTERN));
-//
-// PayerBean payerBean = new PayerBean();
-//
-// payerBean.setOpenid(user.getOpenid());
-// uploadRequest.setPayer(payerBean);
-//
-// WxMaService wxMaService = weChatController.wxMaService();
-// WxMaOrderShippingService wxMaOrderShippingService = new WxMaOrderShippingServiceImpl(wxMaService);
-// WxMaOrderShippingInfoBaseResponse response = wxMaOrderShippingService.upload(uploadRequest);
-// System.out.println("response" + response);
+ // 同步发货信息到微信小程序后台(发货信息录入),避免人工录入
+ try {
+ shopWechatShippingSyncService.uploadExpressShippingInfo(order, orderDelivery, express);
+ } catch (Exception e) {
+ // 不影响本地发货流程,记录日志即可(可配合定时任务后补偿重试)
+ log.warn("同步微信发货信息失败(不影响发货成功) - orderId={}", order.getOrderId(), e);
}
return new HashMap<>() {{
put("res", true);
diff --git a/src/main/java/com/gxwebsoft/shop/service/impl/ShopWechatShippingSyncServiceImpl.java b/src/main/java/com/gxwebsoft/shop/service/impl/ShopWechatShippingSyncServiceImpl.java
new file mode 100644
index 0000000..663682c
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/shop/service/impl/ShopWechatShippingSyncServiceImpl.java
@@ -0,0 +1,231 @@
+package com.gxwebsoft.shop.service.impl;
+
+import cn.binarywang.wx.miniapp.bean.shop.request.shipping.OrderKeyBean;
+import cn.binarywang.wx.miniapp.bean.shop.request.shipping.PayerBean;
+import cn.binarywang.wx.miniapp.bean.shop.request.shipping.ShippingListBean;
+import cn.binarywang.wx.miniapp.bean.shop.request.shipping.WxMaOrderShippingInfoUploadRequest;
+import cn.binarywang.wx.miniapp.constant.WxMaApiUrlConstants;
+import cn.hutool.core.date.DatePattern;
+import cn.hutool.core.date.DateTime;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpRequest;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.gxwebsoft.common.core.utils.RedisUtil;
+import com.gxwebsoft.common.system.entity.Payment;
+import com.gxwebsoft.common.system.entity.User;
+import com.gxwebsoft.common.system.service.UserService;
+import com.gxwebsoft.common.system.service.WxMiniappAccessTokenService;
+import com.gxwebsoft.shop.entity.ShopExpress;
+import com.gxwebsoft.shop.entity.ShopGoods;
+import com.gxwebsoft.shop.entity.ShopOrder;
+import com.gxwebsoft.shop.entity.ShopOrderDelivery;
+import com.gxwebsoft.shop.entity.ShopOrderGoods;
+import com.gxwebsoft.shop.service.ShopGoodsService;
+import com.gxwebsoft.shop.service.ShopOrderGoodsService;
+import com.gxwebsoft.shop.service.ShopWechatShippingSyncService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 同步系统发货信息到微信小程序后台(发货信息录入)。
+ */
+@Slf4j
+@Service
+public class ShopWechatShippingSyncServiceImpl implements ShopWechatShippingSyncService {
+
+ private static final int ORDER_NUMBER_TYPE_OUT_TRADE_NO = 1;
+ private static final int ORDER_NUMBER_TYPE_TRANSACTION_ID = 2;
+
+ // 这两个值在项目原有注释代码中已经使用过(实物快递)。
+ private static final int LOGISTICS_TYPE_PHYSICAL = 1;
+ private static final int DELIVERY_MODE_EXPRESS = 1;
+
+ // 无需物流/自提:微信侧会在“发货信息录入”里变为已发货(具体枚举以微信接口为准)。
+ private static final int LOGISTICS_TYPE_NO_LOGISTICS = 3;
+ private static final int DELIVERY_MODE_NO_LOGISTICS = 3;
+
+ private static final Gson GSON = new Gson();
+
+ @Resource
+ private WxMiniappAccessTokenService wxMiniappAccessTokenService;
+ @Resource
+ private UserService userService;
+ @Resource
+ private ShopOrderGoodsService shopOrderGoodsService;
+ @Resource
+ private ShopGoodsService shopGoodsService;
+ @Resource
+ private RedisUtil redisUtil;
+
+ @Override
+ public boolean uploadExpressShippingInfo(ShopOrder order, ShopOrderDelivery orderDelivery, ShopExpress express) {
+ if (order == null || order.getOrderId() == null) {
+ return false;
+ }
+ if (orderDelivery == null || StrUtil.isBlank(orderDelivery.getExpressNo())) {
+ log.warn("上传微信发货信息跳过:缺少运单号 - orderId={}", order.getOrderId());
+ return false;
+ }
+ if (express == null || StrUtil.isBlank(express.getWxCode())) {
+ log.warn("上传微信发货信息跳过:缺少微信快递公司编码(wxCode) - orderId={}", order.getOrderId());
+ return false;
+ }
+
+ List shippingList = new ArrayList<>();
+ ShippingListBean item = new ShippingListBean();
+ item.setTrackingNo(orderDelivery.getExpressNo());
+ item.setExpressCompany(express.getWxCode());
+ item.setItemDesc(buildItemDesc(order.getOrderId()));
+ shippingList.add(item);
+
+ return doUpload(order, LOGISTICS_TYPE_PHYSICAL, DELIVERY_MODE_EXPRESS, shippingList);
+ }
+
+ @Override
+ public boolean uploadNoLogisticsShippingInfo(ShopOrder order) {
+ if (order == null || order.getOrderId() == null) {
+ return false;
+ }
+ // 无需物流情况下通常不需要 shipping_list
+ return doUpload(order, LOGISTICS_TYPE_NO_LOGISTICS, DELIVERY_MODE_NO_LOGISTICS, Collections.emptyList());
+ }
+
+ private boolean doUpload(ShopOrder order, int logisticsType, int deliveryMode, List shippingList) {
+ // 仅对微信支付订单尝试同步(微信后台“待发货”来自微信支付交易)
+ if (!ObjectUtil.equals(order.getPayType(), 1) && !ObjectUtil.equals(order.getPayType(), 102)) {
+ return false;
+ }
+ if (!Boolean.TRUE.equals(order.getPayStatus())) {
+ return false;
+ }
+ if (order.getTenantId() == null) {
+ return false;
+ }
+
+ // payer openid:必须是下单用户,不是后台操作员
+ User buyer = userService.getByIdIgnoreTenant(order.getUserId());
+ if (buyer == null || StrUtil.isBlank(buyer.getOpenid())) {
+ log.warn("上传微信发货信息失败:买家openid为空 - orderId={}, userId={}", order.getOrderId(), order.getUserId());
+ return false;
+ }
+
+ String accessToken;
+ try {
+ accessToken = wxMiniappAccessTokenService.getAccessToken(order.getTenantId());
+ } catch (Exception e) {
+ log.error("获取小程序access_token失败 - orderId={}, tenantId={}", order.getOrderId(), order.getTenantId(), e);
+ return false;
+ }
+
+ OrderKeyBean orderKey = buildOrderKey(order);
+ if (orderKey == null) {
+ log.warn("上传微信发货信息跳过:无法构建order_key - orderId={}", order.getOrderId());
+ return false;
+ }
+
+ WxMaOrderShippingInfoUploadRequest uploadRequest = new WxMaOrderShippingInfoUploadRequest();
+ uploadRequest.setOrderKey(orderKey);
+ uploadRequest.setLogisticsType(logisticsType);
+ uploadRequest.setDeliveryMode(deliveryMode);
+ uploadRequest.setIsAllDelivered(true);
+ if (shippingList != null && !shippingList.isEmpty()) {
+ uploadRequest.setShippingList(shippingList);
+ }
+ uploadRequest.setUploadTime(new DateTime().toString(DatePattern.UTC_WITH_ZONE_OFFSET_PATTERN));
+
+ PayerBean payerBean = new PayerBean();
+ payerBean.setOpenid(buyer.getOpenid());
+ uploadRequest.setPayer(payerBean);
+
+ String url = WxMaApiUrlConstants.OrderShipping.UPLOAD_SHIPPING_INFO + "?access_token=" + accessToken;
+ String body = GSON.toJson(uploadRequest);
+
+ try {
+ String resp = HttpRequest.post(url)
+ .header("Content-Type", "application/json")
+ .body(body)
+ .timeout(10000)
+ .execute()
+ .body();
+ JsonObject json = JsonParser.parseString(resp).getAsJsonObject();
+ int errcode = json.has("errcode") ? json.get("errcode").getAsInt() : -1;
+ String errmsg = json.has("errmsg") ? json.get("errmsg").getAsString() : resp;
+ if (errcode == 0) {
+ log.info("✅ 微信发货信息同步成功 - orderId={}, logisticsType={}, deliveryMode={}",
+ order.getOrderId(), logisticsType, deliveryMode);
+ return true;
+ }
+ log.error("❌ 微信发货信息同步失败 - orderId={}, errcode={}, errmsg={}, req={}",
+ order.getOrderId(), errcode, errmsg, body);
+ return false;
+ } catch (Exception e) {
+ log.error("❌ 微信发货信息同步异常 - orderId={}, req={}", order.getOrderId(), body, e);
+ return false;
+ }
+ }
+
+ private OrderKeyBean buildOrderKey(ShopOrder order) {
+ if (StrUtil.isNotBlank(order.getTransactionId())) {
+ OrderKeyBean key = new OrderKeyBean();
+ key.setOrderNumberType(ORDER_NUMBER_TYPE_TRANSACTION_ID);
+ key.setTransactionId(order.getTransactionId());
+ return key;
+ }
+
+ // transactionId 为空时,尝试使用 out_trade_no + mchid
+ if (StrUtil.isBlank(order.getOrderNo())) {
+ return null;
+ }
+
+ Payment payment = loadWechatPaymentConfig(order.getTenantId());
+ if (payment == null || StrUtil.isBlank(payment.getMchId())) {
+ return null;
+ }
+
+ OrderKeyBean key = new OrderKeyBean();
+ key.setOrderNumberType(ORDER_NUMBER_TYPE_OUT_TRADE_NO);
+ key.setOutTradeNo(order.getOrderNo());
+ key.setMchId(payment.getMchId());
+ return key;
+ }
+
+ private Payment loadWechatPaymentConfig(Integer tenantId) {
+ if (tenantId == null) {
+ return null;
+ }
+ // 与微信支付回调一致:Payment:1:{tenantId}
+ String key = "Payment:1:" + tenantId;
+ try {
+ return redisUtil.get(key, Payment.class);
+ } catch (Exception e) {
+ log.warn("读取支付配置失败 - key={}", key, e);
+ return null;
+ }
+ }
+
+ private String buildItemDesc(Integer orderId) {
+ try {
+ List orderGoodsList = shopOrderGoodsService.getListByOrderId(orderId);
+ if (orderGoodsList == null || orderGoodsList.isEmpty()) {
+ return "订单商品";
+ }
+ ShopGoods shopGoods = shopGoodsService.getById(orderGoodsList.get(0).getGoodsId());
+ String itemDesc = shopGoods != null && StrUtil.isNotBlank(shopGoods.getName()) ? shopGoods.getName() : "订单商品";
+ if (orderGoodsList.size() > 1) {
+ itemDesc += "等" + orderGoodsList.size() + "件商品";
+ }
+ return itemDesc;
+ } catch (Exception e) {
+ log.warn("构建微信发货 item_desc 失败 - orderId={}", orderId, e);
+ return "订单商品";
+ }
+ }
+}