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 获取实现(按租户)。 + * + *

复用现有缓存结构: + *

+ *

+ */ +@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 "订单商品"; + } + } +}