12
0

2 Коммиты 05fe019ccc ... a57e14cc35

Автор SHA1 Сообщение Дата
  刘云鑫 a57e14cc35 Merge remote-tracking branch 'origin/sit-new-checkstand' into sit-new-checkstand 1 неделя назад
  刘云鑫 2882949aa5 feat:微信服务商支付 1 неделя назад

+ 24 - 0
alien-dining/src/main/java/shop/alien/dining/controller/PaymentController.java

@@ -10,9 +10,12 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
+import shop.alien.dining.service.StoreOrderService;
 import shop.alien.dining.strategy.payment.PaymentStrategyFactory;
 import shop.alien.entity.result.R;
 import shop.alien.util.common.constant.PaymentEnum;
@@ -35,6 +38,7 @@ import java.util.Map;
 public class PaymentController {
 
     private final PaymentStrategyFactory paymentStrategyFactory;
+    private final StoreOrderService storeOrderService;
 
 
     @ApiOperation("创建预支付订单")
@@ -122,6 +126,26 @@ public class PaymentController {
     }
 
     /**
+     * 支付成功后重置餐桌与购物车(内部接口,供 alien-store 微信 APP 服务商回调等调用,与 {@link StoreOrderService#resetTableAfterPayment} 一致)
+     */
+    @PostMapping("/internal/resetTableAfterPayment")
+    public R<Void> internalResetTableAfterPayment(
+            @RequestParam("tableId") Integer tableId,
+            @RequestParam(value = "menuType", required = false) Integer menuType) {
+        log.info("[PaymentController] internalResetTableAfterPayment tableId={}, menuType={}", tableId, menuType);
+        try {
+            if (tableId == null) {
+                return R.fail("tableId 不能为空");
+            }
+            storeOrderService.resetTableAfterPayment(tableId, menuType);
+            return R.success("OK");
+        } catch (Exception e) {
+            log.error("[PaymentController] internalResetTableAfterPayment 失败 tableId={}", tableId, e);
+            return R.fail(e.getMessage() != null ? e.getMessage() : "重置失败");
+        }
+    }
+
+    /**
      * 查询订单状态
      * @param transactionId 交易订单号(微信支付订单号/商户订单号)
      * @param payType 支付类型

+ 40 - 4
alien-store/src/main/java/shop/alien/store/controller/PaymentController.java

@@ -7,6 +7,8 @@ import io.swagger.annotations.ApiImplicitParams;
 import io.swagger.annotations.ApiOperation;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.CrossOrigin;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -15,8 +17,10 @@ import org.springframework.web.bind.annotation.RestController;
 import shop.alien.entity.result.R;
 import shop.alien.store.feign.DiningServiceFeign;
 import shop.alien.store.strategy.payment.PaymentStrategyFactory;
+import shop.alien.util.common.constant.PaymentEnum;
 
 import javax.servlet.http.HttpServletRequest;
+import java.util.HashMap;
 import java.util.Map;
 
 /**
@@ -71,6 +75,12 @@ public class PaymentController {
                         authHeader(request), price, subject, payType, payer, orderNo, storeId,
                         couponId, payerId, tablewareFee, discountAmount, payAmount);
             }
+            // 微信服务商 APP:传入 orderNo 时与小程序策略一致,绑定/刷新 store_order 与微信商户单号
+            if ("wechatPayPartner".equals(payType) && storeId != null && orderNo != null) {
+                return paymentStrategyFactory.getStrategy(payType).createPrePayOrder(
+                        price, subject, payer, orderNo, storeId, couponId, payerId,
+                        tablewareFee, discountAmount, payAmount);
+            }
             return paymentStrategyFactory.getStrategy(payType).createPrePayOrder(price, subject, storeId);
         } catch (Exception e) {
             return R.fail(e.getMessage());
@@ -78,13 +88,39 @@ public class PaymentController {
     }
 
     /**
-     * 通知接口 之后可能会用
-     * @param notifyData
-     * @return
+     * 微信服务商 APP 支付回调(需在商户平台将 notify_url 配置为本地址;验签通过后返回 204)
+     */
+    @RequestMapping("/weChatPartnerNotify")
+    public ResponseEntity<?> weChatPartnerNotify(@RequestBody String notifyData, HttpServletRequest request) throws Exception {
+        log.info("[微信服务商APP回调] 收到请求, Content-Length={}, Wechatpay-Serial={}",
+                request.getContentLength(), request.getHeader("Wechatpay-Serial"));
+        try {
+            R<?> result = paymentStrategyFactory.getStrategy(PaymentEnum.WECHAT_PAY_PARTNER.getType())
+                    .handleNotify(notifyData, request);
+            if (R.isSuccess(result)) {
+                return ResponseEntity.noContent().build();
+            }
+            String message = result != null && result.getMsg() != null ? result.getMsg() : "失败";
+            Map<String, String> failBody = new HashMap<>(2);
+            failBody.put("code", "FAIL");
+            failBody.put("message", message);
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(failBody);
+        } catch (Exception e) {
+            log.error("[微信服务商APP回调] 处理异常", e);
+            String msg = e.getMessage() != null ? e.getMessage() : "失败";
+            Map<String, String> failBody = new HashMap<>(2);
+            failBody.put("code", "FAIL");
+            failBody.put("message", msg);
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(failBody);
+        }
+    }
+
+    /**
+     * 预留:非微信 V3 回调(当前未使用)
      */
     @RequestMapping("/notify")
     public R notify(String notifyData) {
-        return  null;
+        return null;
     }
 
     /**

+ 8 - 0
alien-store/src/main/java/shop/alien/store/feign/DiningServiceFeign.java

@@ -614,6 +614,14 @@ public interface DiningServiceFeign {
             @RequestParam("payType") String payType,
             @RequestParam("storeId") Integer storeId);
 
+    /**
+     * 支付成功后重置餐桌与购物车(与 alien-dining {@code StoreOrderService.resetTableAfterPayment} 一致),供微信回调等业务使用。
+     */
+    @PostMapping("/payment/internal/resetTableAfterPayment")
+    R<Void> resetTableAfterPaymentInternal(
+            @RequestParam("tableId") Integer tableId,
+            @RequestParam(value = "menuType", required = false) Integer menuType);
+
     // ==================== 门店信息接口 ====================
 
     @GetMapping("/store/info/detail/{storeId}")

+ 28 - 0
alien-store/src/main/java/shop/alien/store/strategy/payment/PaymentStrategy.java

@@ -3,6 +3,7 @@ package shop.alien.store.strategy.payment;
 
 import shop.alien.entity.result.R;
 
+import javax.servlet.http.HttpServletRequest;
 import java.util.Map;
 
 /**
@@ -31,6 +32,25 @@ public interface PaymentStrategy {
         return createPrePayOrder(price, subject);
     }
 
+    /**
+     * 创建预支付订单(扩展参数:与点餐小程序服务商策略对齐,用于绑定业务订单号、回写 store_order)
+     * <p>
+     * 默认实现忽略扩展字段,仅调用 {@link #createPrePayOrder(String, String, Integer)}。
+     * </p>
+     *
+     * @param payer         预留(APP 服务商下单可不传)
+     * @param orderNo       业务订单号,有值时尝试关联 store_order
+     * @param couponId      优惠券
+     * @param payerId       支付用户
+     * @param serviceFee    服务费(与接口层 tablewareFee 等含义对齐时由调用方传入)
+     * @param discountAmount 优惠金额
+     * @param payAmount     实付金额
+     */
+    default R createPrePayOrder(String price, String subject, String payer, String orderNo, Integer storeId,
+                                Integer couponId, Integer payerId, String serviceFee, String discountAmount,
+                                String payAmount) throws Exception {
+        return createPrePayOrder(price, subject, storeId);
+    }
 
     /**
      * 处理支付通知
@@ -41,6 +61,14 @@ public interface PaymentStrategy {
      */
     R handleNotify(String notifyData) throws Exception;
 
+    /**
+     * 处理支付通知(微信 APIv3 需从请求头读取 Wechatpay-Serial 等参与验签)
+     * <p>默认实现转调 {@link #handleNotify(String)}。</p>
+     */
+    default R handleNotify(String notifyData, HttpServletRequest request) throws Exception {
+        return handleNotify(notifyData);
+    }
+
      /**
      * 查询订单状态
      *

+ 270 - 12
alien-store/src/main/java/shop/alien/store/strategy/payment/impl/WeChatPartnerPaymentStrategyImpl.java

@@ -1,5 +1,7 @@
 package shop.alien.store.strategy.payment.impl;
 
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.google.gson.annotations.SerializedName;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -9,16 +11,25 @@ import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.StoreInfo;
+import shop.alien.entity.store.StoreOrder;
+import shop.alien.entity.store.StorePaymentConfig;
+import shop.alien.entity.store.LifeDiscountCouponUser;
 import shop.alien.entity.store.RefundRecord;
+import shop.alien.mapper.LifeDiscountCouponUserMapper;
 import shop.alien.mapper.StoreInfoMapper;
+import shop.alien.mapper.StoreOrderMapper;
+import shop.alien.store.feign.DiningServiceFeign;
 import shop.alien.store.service.RefundRecordService;
+import shop.alien.store.service.StorePaymentConfigService;
 import shop.alien.store.strategy.payment.PaymentStrategy;
 import shop.alien.store.util.WXPayUtility;
 import shop.alien.util.common.UniqueRandomNumGenerator;
+import shop.alien.util.common.constant.DiscountCouponEnum;
 import shop.alien.util.common.constant.PaymentEnum;
 import shop.alien.util.system.OSUtil;
 
 import javax.annotation.PostConstruct;
+import javax.servlet.http.HttpServletRequest;
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.math.BigDecimal;
@@ -27,8 +38,11 @@ import java.security.PrivateKey;
 import java.security.PublicKey;
 import java.security.Signature;
 import java.util.Base64;
+import java.util.Date;
+import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.concurrent.CompletableFuture;
 
 /**
  * 微信支付 — 服务商模式(特约商户 / partner)
@@ -55,6 +69,14 @@ public class WeChatPartnerPaymentStrategyImpl implements PaymentStrategy {
     private final RefundRecordService refundRecordService;
     /** 按门店读取 store_info.wechat_sub_mchid(特约商户号),与前端传入的 storeId 对应 */
     private final StoreInfoMapper storeInfoMapper;
+    /** 预下单时按业务订单号更新 store_order */
+    private final StoreOrderMapper storeOrderMapper;
+    /** 按门店解析子商户 AppID(sub_appid),与小程序策略一致 */
+    private final StorePaymentConfigService storePaymentConfigService;
+    /** 支付成功回调:核销优惠券(与小程序服务商策略一致) */
+    private final LifeDiscountCouponUserMapper lifeDiscountCouponUserMapper;
+    /** 支付成功回调:重置餐桌/购物车逻辑在 alien-dining,通过 Feign 调用 */
+    private final DiningServiceFeign diningServiceFeign;
     /** 复用直连实现中的退款单模型、HTTP 工具与 RefundRecord 构建逻辑(构建后覆盖 payType 为服务商类型) */
     private final WeChatPaymentStrategyImpl weChatPaymentStrategy;
 
@@ -121,6 +143,10 @@ public class WeChatPartnerPaymentStrategyImpl implements PaymentStrategy {
     @Value("${payment.wechatPartnerPay.business.refundNotifyUrl}")
     private String refundNotifyUrl;
 
+    /** 支付结果通知 resource 解密(与小程序服务商策略一致,回调验签通过后解密) */
+    @Value("${payment.wechatPartnerPay.business.apiV3Key:}")
+    private String apiV3Key;
+
     private PrivateKey privateKey;
     private PublicKey wechatPayPublicKey;
 
@@ -145,14 +171,28 @@ public class WeChatPartnerPaymentStrategyImpl implements PaymentStrategy {
 
     @Override
     public R createPrePayOrder(String price, String subject) throws Exception {
-        return createPrePayOrder(price, subject, null);
+        return createPrePayOrder(price, subject, null, null, null, null, null, null, null, null);
     }
 
     @Override
     public R createPrePayOrder(String price, String subject, Integer storeId) throws Exception {
+        return createPrePayOrder(price, subject, null, null, storeId, null, null, null, null, null);
+    }
+
+    /**
+     * 与 {}
+     * 参数对齐:有 orderNo 时刷新 {@link StoreOrder} 的 pay_trade_no 及费用字段,商户单号与微信 out_trade_no 一致。
+     */
+    @Override
+    public R createPrePayOrder(String price, String subject, String payer, String orderNo, Integer storeId,
+                               Integer couponId, Integer payerId, String serviceFee, String discountAmount,
+                               String payAmount) throws Exception {
         String subMchid = resolveSubMchidFromStore(storeId);
-        log.info("[WeChatPartner] 创建预支付订单,price={}, subject={}, spMchId={}, storeId={}, subMchid={}",
-                price, subject, spMchId, storeId, subMchid != null ? subMchid : "(未解析)");
+        log.info("[WeChatPartner] 创建预支付订单,price={}, subject={}, spMchId={}, storeId={}, orderNo={}, subMchid={}",
+                price, subject, spMchId, storeId, orderNo, subMchid != null ? subMchid : "(未解析)");
+        // 与小程序策略入参一致;服务商 APP 下单无需 sp_openid,仅记录便于与前端联调对照
+        log.debug("[WeChatPartner] 扩展参数 payer(APP 不使用)={}, couponId={}, payerId={}, serviceFee={}, discountAmount={}, payAmount={}",
+                payer, couponId, payerId, serviceFee, discountAmount, payAmount);
         if (subMchid == null) {
             return R.fail("请传入门店 storeId,且门店需在 store_info 中维护微信特约商户号 wechat_sub_mchid");
         }
@@ -171,15 +211,58 @@ public class WeChatPartnerPaymentStrategyImpl implements PaymentStrategy {
             return R.fail("价格格式不正确");
         }
 
+        String wechatOutTradeNo;
+        if (StringUtils.isNotBlank(orderNo)) {
+            wechatOutTradeNo = orderNo.trim();
+            LambdaQueryWrapper<StoreOrder> ow = new LambdaQueryWrapper<>();
+            ow.eq(StoreOrder::getOrderNo, wechatOutTradeNo);
+            ow.eq(StoreOrder::getDeleteFlag, 0);
+            StoreOrder storeOrder = storeOrderMapper.selectOne(ow);
+            log.info("[WeChatPartner] createPrePayOrder orderNo={}, storeOrderFound={}, subMchid={}", orderNo, storeOrder != null, subMchid);
+
+            if (storeOrder != null) {
+                if (storeOrder.getPayStatus() != null && storeOrder.getPayStatus() == 1) {
+                    return R.fail("订单已支付");
+                }
+                if (storeOrder.getPayTradeNo() != null) {
+                    try {
+                        WeChatPaymentStrategyImpl.DirectAPIv3QueryResponse wxOrder =
+                                partnerSearchOrderRun(storeOrder.getPayTradeNo(), subMchid);
+                        if (wxOrder != null && "SUCCESS".equals(wxOrder.tradeState)) {
+                            return R.fail("该支付单已在微信侧支付成功,请勿重复发起支付");
+                        }
+                    } catch (WXPayUtility.ApiException e) {
+                        if (e.getStatusCode() != 404 && !"ORDER_NOT_EXIST".equals(e.getErrorCode())) {
+                            log.error("[WeChatPartner] 预支付前查单失败 payTradeNo={}, code={}, msg={}",
+                                    storeOrder.getPayTradeNo(), e.getErrorCode(), e.getErrorMessage());
+                            return R.fail("查询微信支付订单失败:" + (e.getErrorMessage() != null ? e.getErrorMessage() : e.getMessage()));
+                        }
+                    }
+                }
+                String newPayTradeNo = "WX" + storeOrder.getId() + "_" + System.currentTimeMillis();
+                storeOrder.setPayTradeNo(newPayTradeNo);
+                wechatOutTradeNo = newPayTradeNo;
+                log.info("[WeChatPartner] 换新商户单号 orderNo={}, payTradeNo={}", orderNo, newPayTradeNo);
+                storeOrder.setCouponId(couponId);
+                storeOrder.setPayUserId(payerId);
+                storeOrder.setServiceFee(parseAmountOrZero(serviceFee));
+                storeOrder.setDiscountAmount(parseAmountOrZero(discountAmount));
+                storeOrder.setPayAmount(parseAmountOrZero(payAmount));
+                if (storeOrderMapper.updateById(storeOrder) <= 0) {
+                    log.error("[WeChatPartner] 更新订单失败 orderNo={}", orderNo);
+                    return R.fail("更新订单失败");
+                }
+            }
+        } else {
+            wechatOutTradeNo = UniqueRandomNumGenerator.generateUniqueCode(19);
+        }
+
         PartnerAppPrepayRequest request = new PartnerAppPrepayRequest();
         request.spAppid = spAppId;
         request.spMchid = spMchId;
         request.subMchid = subMchid;
-        if (StringUtils.isNotBlank(subAppId)) {
-            request.subAppid = subAppId;
-        }
         request.description = subject;
-        request.outTradeNo = UniqueRandomNumGenerator.generateUniqueCode(19);
+        request.outTradeNo = wechatOutTradeNo;
         request.notifyUrl = prePayNotifyUrl;
         request.amount = new WeChatPaymentStrategyImpl.CommonAmountInfo();
         request.amount.total = new BigDecimal(price).multiply(new BigDecimal(100)).longValue();
@@ -189,12 +272,10 @@ public class WeChatPartnerPaymentStrategyImpl implements PaymentStrategy {
             WeChatPaymentStrategyImpl.DirectAPIv3AppPrepayResponse response = partnerPrePayOrderRun(request);
             log.info("[WeChatPartner] 预下单成功 prepayId={}, outTradeNo={}", response.prepayId, request.outTradeNo);
 
-            String clientAppId = StringUtils.isNotBlank(subAppId) ? subAppId : spAppId;
             Map<String, String> result = new HashMap<>();
             result.put("prepayId", response.prepayId);
-            result.put("appId", clientAppId);
+            result.put("appId", spAppId);
             result.put("spAppId", spAppId);
-            result.put("subAppId", subAppId);
             result.put("spMchId", spMchId);
             result.put("subMchId", subMchid);
             result.put("orderNo", request.outTradeNo);
@@ -202,7 +283,7 @@ public class WeChatPartnerPaymentStrategyImpl implements PaymentStrategy {
             long timestamp = System.currentTimeMillis() / 1000;
             String nonce = WXPayUtility.createNonce(32);
             String prepayId = response.prepayId;
-            String message = String.format("%s\n%s\n%s\n%s\n", clientAppId, timestamp, nonce, prepayId);
+            String message = String.format("%s\n%s\n%s\n", timestamp, nonce, prepayId);
             Signature sign = Signature.getInstance("SHA256withRSA");
             sign.initSign(privateKey);
             sign.update(message.getBytes(StandardCharsets.UTF_8));
@@ -216,9 +297,186 @@ public class WeChatPartnerPaymentStrategyImpl implements PaymentStrategy {
         }
     }
 
+    /**
+     * 子商户 AppID:门店支付配置优先,否则使用全局 {@code payment.wechatPartnerPay.business.subAppId}。
+     */
+    private String resolveSubAppId(StorePaymentConfig config) {
+        if (config != null) {
+            if (org.springframework.util.StringUtils.hasText(config.getWechatMiniAppId())) {
+                return config.getWechatMiniAppId().trim();
+            }
+            if (org.springframework.util.StringUtils.hasText(config.getWechatAppId())) {
+                return config.getWechatAppId().trim();
+            }
+        }
+        if (StringUtils.isNotBlank(subAppId)) {
+            return subAppId.trim();
+        }
+        return null;
+    }
+
+    /** 解析金额字符串,空或非法时记日志并返回 0,避免更新订单时 NPE */
+    private static BigDecimal parseAmountOrZero(String raw) {
+        if (raw == null || raw.trim().isEmpty()) {
+            return BigDecimal.ZERO;
+        }
+        try {
+            return new BigDecimal(raw.trim());
+        } catch (NumberFormatException e) {
+            return BigDecimal.ZERO;
+        }
+    }
+
     @Override
     public R handleNotify(String notifyData) throws Exception {
-        return null;
+        log.warn("[WeChatPartner回调] 缺少 HttpServletRequest,无法进行 APIv3 验签,请使用 POST /payment/weChatPartnerNotify");
+        return R.fail("请使用微信支付回调专用接口(需携带 Wechatpay-* 请求头)");
+    }
+
+    /**
+     * 与 {} 对齐:
+     * 验签、解密 resource,异步更新订单/优惠券,并通过 Feign 调用 dining 重置餐桌。
+     */
+    @Override
+    public R handleNotify(String notifyData, HttpServletRequest request) throws Exception {
+        log.info("[WeChatPartner回调] 进入 handleNotify, len={}", notifyData != null ? notifyData.length() : 0);
+        if (request == null) {
+            return R.fail("请求上下文缺失");
+        }
+        String serial = request.getHeader("Wechatpay-Serial");
+        String signature = request.getHeader("Wechatpay-Signature");
+        String timestamp = request.getHeader("Wechatpay-Timestamp");
+        String nonce = request.getHeader("Wechatpay-Nonce");
+
+        if (serial == null || signature == null || timestamp == null || nonce == null) {
+            log.warn("[WeChatPartner回调] 验签参数缺失 serial={}, signature={}, timestamp={}, nonce={}",
+                    serial, signature != null, timestamp, nonce);
+            return R.fail("验签参数缺失");
+        }
+
+        if (signature.startsWith("WECHATPAY/SIGNTEST/")) {
+            log.info("[WeChatPartner回调] 签名探测请求,直接成功");
+            return R.success("OK");
+        }
+
+        if (!StringUtils.equals(serial, wechatPayPublicKeyId)) {
+            log.warn("[WeChatPartner回调] Wechatpay-Serial 与配置不符 serial={}, expected={}", serial, wechatPayPublicKeyId);
+            return R.fail("公钥序列号不匹配");
+        }
+        if (wechatPayPublicKey == null) {
+            log.error("[WeChatPartner回调] 平台公钥未加载");
+            return R.fail("平台公钥未就绪");
+        }
+
+        Headers okHeaders = buildOkHttpHeaders(request);
+
+        if (!org.springframework.util.StringUtils.hasText(apiV3Key)) {
+            log.error("[WeChatPartner回调] 未配置 payment.wechatPartnerPay.business.apiV3Key,无法解密");
+            return R.fail("APIv3 密钥未配置");
+        }
+
+        try {
+            WXPayUtility.Notification parsed = WXPayUtility.parseNotification(apiV3Key, wechatPayPublicKeyId,
+                    wechatPayPublicKey, okHeaders, notifyData);
+            String plaintext = parsed.getPlaintext();
+            if (plaintext == null || plaintext.isEmpty()) {
+                log.warn("[WeChatPartner回调] 解密后业务数据为空");
+                return R.fail("解密结果为空");
+            }
+            final String plainCopy = plaintext;
+            CompletableFuture.runAsync(() -> processPartnerNotifyBusiness(plainCopy));
+            return R.success("OK");
+        } catch (IllegalArgumentException e) {
+            log.error("[WeChatPartner回调] 验签或解密失败: {}", e.getMessage());
+            return R.fail(e.getMessage() != null ? e.getMessage() : "处理失败");
+        } catch (Exception e) {
+            log.error("[WeChatPartner回调] 处理异常", e);
+            return R.fail(e.getMessage() != null ? e.getMessage() : "处理异常");
+        }
+    }
+
+    private static Headers buildOkHttpHeaders(HttpServletRequest request) {
+        Headers.Builder b = new Headers.Builder();
+        Enumeration<String> names = request.getHeaderNames();
+        while (names != null && names.hasMoreElements()) {
+            String name = names.nextElement();
+            Enumeration<String> values = request.getHeaders(name);
+            while (values != null && values.hasMoreElements()) {
+                b.add(name, values.nextElement());
+            }
+        }
+        return b.build();
+    }
+
+    /**
+     * 异步:解析支付成功报文,更新 store_order、优惠券,并通知 dining 重置餐桌(与小程序服务商回调业务一致)
+     */
+    private void processPartnerNotifyBusiness(String plaintext) {
+        try {
+            JSONObject jsonObject = JSONObject.parseObject(plaintext);
+            String tradeState = jsonObject.getString("trade_state");
+            if (!"SUCCESS".equals(tradeState)) {
+                log.info("[WeChatPartner回调] trade_state 非 SUCCESS: {}", tradeState);
+                return;
+            }
+            String outTradeNo = jsonObject.getString("out_trade_no");
+            if (outTradeNo == null || outTradeNo.isEmpty()) {
+                log.warn("[WeChatPartner回调] 缺少 out_trade_no");
+                return;
+            }
+
+            LambdaQueryWrapper<StoreOrder> byOrderNo = new LambdaQueryWrapper<>();
+            byOrderNo.eq(StoreOrder::getOrderNo, outTradeNo).eq(StoreOrder::getDeleteFlag, 0);
+            StoreOrder storeOrder = storeOrderMapper.selectOne(byOrderNo);
+            if (storeOrder == null) {
+                LambdaQueryWrapper<StoreOrder> byPayTrade = new LambdaQueryWrapper<>();
+                byPayTrade.eq(StoreOrder::getPayTradeNo, outTradeNo).eq(StoreOrder::getDeleteFlag, 0);
+                storeOrder = storeOrderMapper.selectOne(byPayTrade);
+            }
+
+            if (storeOrder != null && !Integer.valueOf(1).equals(storeOrder.getPayStatus())) {
+                storeOrder.setPayStatus(1);
+                storeOrder.setOrderStatus(1);
+                storeOrder.setPayType(1);
+                storeOrder.setPayTime(new Date());
+                int rows = storeOrderMapper.updateById(storeOrder);
+                if (rows > 0) {
+                    log.info("[WeChatPartner回调] 更新订单支付成功 outTradeNo={}", outTradeNo);
+                    if (storeOrder.getCouponId() != null && storeOrder.getPayUserId() != null) {
+                        LambdaQueryWrapper<LifeDiscountCouponUser> cw = new LambdaQueryWrapper<>();
+                        cw.eq(LifeDiscountCouponUser::getUserId, storeOrder.getPayUserId());
+                        cw.eq(LifeDiscountCouponUser::getCouponId, storeOrder.getCouponId());
+                        cw.eq(LifeDiscountCouponUser::getStatus, Integer.parseInt(DiscountCouponEnum.WAITING_USED.getValue()));
+                        cw.eq(LifeDiscountCouponUser::getDeleteFlag, 0);
+                        cw.orderByDesc(LifeDiscountCouponUser::getCreatedTime);
+                        cw.last("LIMIT 1");
+                        LifeDiscountCouponUser couponUser = lifeDiscountCouponUserMapper.selectOne(cw);
+                        if (couponUser != null) {
+                            couponUser.setStatus(Integer.parseInt(DiscountCouponEnum.HAVE_BEEN_USED.getValue()));
+                            couponUser.setUseTime(new Date());
+                            lifeDiscountCouponUserMapper.updateById(couponUser);
+                            log.info("[WeChatPartner回调] 优惠券已使用 id={}", couponUser.getId());
+                        }
+                    }
+                    try {
+                        R<Void> feignRet =
+                                diningServiceFeign.resetTableAfterPaymentInternal(storeOrder.getTableId(), storeOrder.getMenuType());
+                        if (!R.isSuccess(feignRet)) {
+                            log.error("[WeChatPartner回调] Feign 重置餐桌失败 tableId={}, msg={}",
+                                    storeOrder.getTableId(), feignRet != null ? feignRet.getMsg() : "null");
+                        } else {
+                            log.info("[WeChatPartner回调] 已请求 dining 重置餐桌 tableId={}", storeOrder.getTableId());
+                        }
+                    } catch (Exception e) {
+                        log.error("[WeChatPartner回调] Feign 重置餐桌异常 tableId={}", storeOrder.getTableId(), e);
+                    }
+                } else {
+                    log.warn("[WeChatPartner回调] 更新订单影响行数为 0, orderId={}", storeOrder.getId());
+                }
+            }
+        } catch (Exception e) {
+            log.error("[WeChatPartner回调] 异步处理异常", e);
+        }
     }
 
     @Override