Ver Fonte

feat:小程序支付成功回调修改//TODO联调看问题

刘云鑫 há 2 meses atrás
pai
commit
dfc2620ac6

+ 9 - 3
alien-dining/src/main/java/shop/alien/dining/controller/PaymentController.java

@@ -13,7 +13,9 @@ import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 import shop.alien.dining.strategy.payment.PaymentStrategyFactory;
 import shop.alien.entity.result.R;
+import shop.alien.util.common.constant.PaymentEnum;
 
+import javax.servlet.http.HttpServletRequest;
 import java.util.Map;
 
 /**
@@ -55,9 +57,13 @@ public class PaymentController {
      * @param notifyData
      * @return
      */
-    @RequestMapping("/notify")
-    public R notify(String notifyData) {
-        return  null;
+    @RequestMapping("/weChatMininNotify")
+    public R notify(@RequestBody String notifyData, HttpServletRequest request) throws Exception {
+        try {
+            return paymentStrategyFactory.getStrategy(PaymentEnum.WECHAT_PAY_MININ_PROGRAM.getType()).handleNotify(notifyData,request);
+        } catch (Exception e) {
+            return R.fail(e.getMessage());
+        }
     }
 
     /**

+ 2 - 1
alien-dining/src/main/java/shop/alien/dining/strategy/payment/PaymentStrategy.java

@@ -2,6 +2,7 @@ package shop.alien.dining.strategy.payment;
 
 import shop.alien.entity.result.R;
 
+import javax.servlet.http.HttpServletRequest;
 import java.util.Map;
 
 /**
@@ -29,7 +30,7 @@ public interface PaymentStrategy {
      * @return 处理结果
      * @throws Exception 处理异常
      */
-    R handleNotify(String notifyData) throws Exception;
+    R handleNotify(String notifyData, HttpServletRequest request) throws Exception;
 
     /**
      * 查询订单状态

+ 105 - 3
alien-dining/src/main/java/shop/alien/dining/strategy/payment/impl/WeChatPaymentMininProgramStrategyImpl.java

@@ -1,6 +1,9 @@
 package shop.alien.dining.strategy.payment.impl;
 
-
+import com.alibaba.fastjson2.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.gson.annotations.Expose;
 import com.google.gson.annotations.SerializedName;
 import com.wechat.pay.java.service.refund.model.QueryByOutRefundNoRequest;
@@ -13,13 +16,17 @@ import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.cloud.context.config.annotation.RefreshScope;
 import org.springframework.stereotype.Component;
+import shop.alien.dining.service.StoreOrderService;
 import shop.alien.dining.strategy.payment.PaymentStrategy;
 import shop.alien.dining.util.WXPayUtility;
+import shop.alien.dining.util.WeChatPayUtil;
 import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreOrder;
 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.nio.charset.StandardCharsets;
@@ -117,6 +124,12 @@ public class WeChatPaymentMininProgramStrategyImpl implements PaymentStrategy {
     @Value("${payment.wechatPay.searchOrderByOutTradeNoPath}")
     private String searchOrderByOutTradeNoPath;
 
+    @Value("${payment.wechatPay.business.apiV3key}")
+    private String apiV3key;
+
+    private final StoreOrderService storeOrderService;
+
+    private final ObjectMapper objectMapper;
 
     private static String POSTMETHOD = "POST";
     private static String GETMETHOD = "GET";
@@ -212,8 +225,97 @@ public class WeChatPaymentMininProgramStrategyImpl implements PaymentStrategy {
     }
 
     @Override
-    public R handleNotify(String notifyData) throws Exception {
-        return null;
+    public R handleNotify(String notifyData, HttpServletRequest request) throws Exception {
+        /*{
+    "id": "EV-2018022511223320873",
+    "create_time": "2015-05-20T13:29:35+08:00",
+    "resource_type": "encrypt-resource",
+    "event_type": "TRANSACTION.SUCCESS",
+    "summary": "支付成功",
+    "resource": {
+        "original_type": "transaction",
+        "algorithm": "AEAD_AES_256_GCM",
+        "ciphertext": "",
+        "associated_data": "",
+        "nonce": ""
+    }
+}*/
+        // 1. 提取微信支付回调请求头中的验签参数(对应图片里的4个核心参数)
+        String serial = request.getHeader("Wechatpay-Serial"); // 证书序列号/公钥ID
+        String signature = request.getHeader("Wechatpay-Signature"); // 签名值
+        String timestamp = request.getHeader("Wechatpay-Timestamp"); // 时间戳
+        String nonce = request.getHeader("Wechatpay-Nonce"); // 随机字符串
+
+        // 2. 校验核心验签参数是否为空
+        if (serial == null || signature == null || timestamp == null || nonce == null) {
+            log.warn("微信支付回调验签失败:核心头参数缺失 serial={}, signature={}, timestamp={}, nonce={}",
+                    serial, signature, timestamp, nonce);
+            return R.fail("验签参数缺失");
+        }
+
+        StringBuilder signStr = new StringBuilder();
+        signStr.append(timestamp).append("\n"); // 第一行:应答时间戳
+        signStr.append(nonce).append("\n");     // 第二行:应答随机串
+        signStr.append(notifyData).append("\n");      // 第三行:应答报文主体(末尾必须加\n
+        Signature sign = Signature.getInstance("SHA256withRSA");
+        // 步骤2:Base64解码签名串(对应命令行openssl base64 -d逻辑)
+        byte[] signatureBytes = Base64.getDecoder().decode(signature);
+
+        // 步骤3:初始化SHA256withRSA签名验证器(对应命令行-sha256参数)
+        sign.initVerify(wechatPayPublicKey);
+
+        // 步骤4:传入验签名串(编码为UTF-8,对应命令行EOF里的内容)
+        sign.update(signStr.toString().getBytes(StandardCharsets.UTF_8));
+
+        // 步骤5:执行验签(对应命令行-verify逻辑)
+        if (sign.verify(signatureBytes)) {
+            // 更新订单状态 TODO 改为异步
+            // ========== 第二步:解析回调报文,提取加密的resource字段 ==========
+            JsonNode rootNode = objectMapper.readTree(notifyData);
+            JsonNode resourceNode = rootNode.get("resource");
+            if (resourceNode == null) {
+                log.warn("微信支付回调报文无resource字段");
+                return R.fail("回调数据格式异常");
+            }
+
+            // 提取resource下的加密相关参数
+            String encryptAlgorithm = resourceNode.get("algorithm").asText();
+            String resourceNonce = resourceNode.get("nonce").asText();
+            String associatedData = resourceNode.get("associated_data").asText();
+            String ciphertext = resourceNode.get("ciphertext").asText();
+
+            // 校验加密算法(目前仅支持AEAD_AES_256_GCM)
+            if (!"AEAD_AES_256_GCM".equals(encryptAlgorithm)) {
+                log.warn("不支持的加密算法:{}", encryptAlgorithm);
+                return R.fail("不支持的加密算法");
+            }
+            // ========== 第三步:AES-256-GCM解密,获取明文业务信息 ==========
+            String plainBusinessData = WeChatPayUtil.decrypt(
+                    apiV3key,
+                    resourceNonce,
+                    associatedData,
+                    ciphertext
+            );
+            log.info("微信支付回调解密后的业务信息:{}", plainBusinessData);
+            JSONObject jsonObject = JSONObject.parseObject(plainBusinessData);
+            String tradeState = jsonObject.getString("trade_state");
+            // 如果支付成功
+            if(tradeState.equals("SUCCESS")){
+                String transactionId = jsonObject.getString("transaction_id");
+                StoreOrder storeOrder = storeOrderService.getOne(new QueryWrapper<StoreOrder>().eq("transaction_id", transactionId));
+                if(storeOrder.getPayStatus() != 1){
+                    storeOrder.setPayStatus(1);
+                    if(storeOrderService.updateById(storeOrder)){
+                        log.info("小程序更新订单成功,订单号transactionId:{}", transactionId);
+                    };
+                }
+            }
+            return R.success("Verified OK");
+        } else {
+            return R.fail("Verified error");
+        }
+
+
     }
 
     @Override

+ 49 - 0
alien-dining/src/main/java/shop/alien/dining/util/WeChatPayUtil.java

@@ -1,6 +1,12 @@
 package shop.alien.dining.util;
 
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.codec.binary.Base64;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
 
 /**
  * 微信支付工具类
@@ -12,5 +18,48 @@ import lombok.extern.slf4j.Slf4j;
 @Slf4j
 public class WeChatPayUtil {
     // TODO: 实现微信支付相关工具方法
+
+    // AES-GCM模式的TAG长度(微信固定为128位/16字节)
+    private static final int GCM_TAG_LENGTH = 128;
+    // AES-256算法要求密钥长度为32字节(APIv3密钥必须是32位)
+    private static final int API_V3_KEY_LENGTH = 32;
+
+    /**
+     * 解密微信支付resource字段密文
+     * @param apiV3Key 商户平台设置的APIv3密钥(32位字符串)
+     * @param nonce resource.nonce(加密随机串)
+     * @param associatedData resource.associated_data(关联数据)
+     * @param ciphertext resource.ciphertext(Base64编码的密文)
+     * @return 解密后的明文(JSON格式业务信息)
+     * @throws Exception 解密异常(密钥错误、参数非法等)
+     */
+    public static String decrypt(String apiV3Key, String nonce, String associatedData, String ciphertext) throws Exception {
+        // 1. 校验APIv3密钥长度(必须32字节)
+        if (apiV3Key == null || apiV3Key.length() != API_V3_KEY_LENGTH) {
+            throw new IllegalArgumentException("APIv3密钥必须是32位字符串");
+        }
+
+        // 2. 转换密钥为SecretKeySpec(AES-256)
+        SecretKeySpec secretKeySpec = new SecretKeySpec(apiV3Key.getBytes(StandardCharsets.UTF_8), "AES");
+
+        // 3. Base64解密密文
+        byte[] ciphertextBytes = Base64.decodeBase64(ciphertext);
+
+        // 4. 初始化GCM参数(nonce为16字节?微信实际是12字节,符合rfc5116)
+        GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, nonce.getBytes(StandardCharsets.UTF_8));
+
+        // 5. 初始化AES-GCM解密器
+        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
+        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, gcmParameterSpec);
+
+        // 6. 设置关联数据(AEAD附加数据)
+        cipher.updateAAD(associatedData.getBytes(StandardCharsets.UTF_8));
+
+        // 7. 执行解密
+        byte[] plaintextBytes = cipher.doFinal(ciphertextBytes);
+
+        // 8. 转换为UTF-8明文(JSON格式)
+        return new String(plaintextBytes, StandardCharsets.UTF_8);
+    }
 }