|
|
@@ -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
|