From 946fe4de4c234fd3080d7d4d6b972ecf9e7c2227 Mon Sep 17 00:00:00 2001 From: gong <1157756119@qq.com> Date: Wed, 14 Jan 2026 14:41:30 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9B=9E=E8=B0=83=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/czg/controller/NotifyController.java | 52 +++++++- .../com/czg/dto/req/WechatNotifyReqDto.java | 33 +++++ .../czg/dto/req/WechatPayNotifyDataDto.java | 118 ++++++++++++++++++ .../czg/third/alipay/AlipayIsvPayManager.java | 2 +- .../com/czg/third/wechat/WechatAesUtil.java | 60 +++++++++ .../czg/third/wechat/WechatPayManager.java | 5 +- .../com/czg/third/wechat/WechatReqUtils.java | 26 +++- 7 files changed, 285 insertions(+), 11 deletions(-) create mode 100644 cash-sdk/aggregation-pay/src/main/java/com/czg/dto/req/WechatNotifyReqDto.java create mode 100644 cash-sdk/aggregation-pay/src/main/java/com/czg/dto/req/WechatPayNotifyDataDto.java create mode 100644 cash-sdk/aggregation-pay/src/main/java/com/czg/third/wechat/WechatAesUtil.java diff --git a/cash-api/order-server/src/main/java/com/czg/controller/NotifyController.java b/cash-api/order-server/src/main/java/com/czg/controller/NotifyController.java index af97cc9c7..479293715 100644 --- a/cash-api/order-server/src/main/java/com/czg/controller/NotifyController.java +++ b/cash-api/order-server/src/main/java/com/czg/controller/NotifyController.java @@ -4,8 +4,12 @@ import cn.hutool.core.date.DateUtil; import cn.hutool.core.io.IoUtil; import com.alibaba.fastjson2.JSONObject; import com.czg.CzgPayUtils; +import com.czg.PayCst; import com.czg.constants.PayTypeConstants; +import com.czg.dto.req.WechatNotifyReqDto; +import com.czg.dto.req.WechatPayNotifyDataDto; import com.czg.entity.CzgBaseRespParams; +import com.czg.exception.CzgException; import com.czg.market.entity.MkShopConsumeDiscountRecord; import com.czg.market.service.MkDistributionUserService; import com.czg.market.service.MkShopConsumeDiscountRecordService; @@ -15,15 +19,13 @@ import com.czg.order.entity.OrderPayment; import com.czg.order.service.OrderInfoCustomService; import com.czg.order.service.OrderPaymentService; import com.czg.service.market.service.impl.AppWxServiceImpl; +import com.czg.third.wechat.WechatReqUtils; import com.czg.utils.AssertUtil; import com.mybatisflex.core.query.QueryWrapper; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; -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 org.springframework.web.bind.annotation.*; import java.io.IOException; @@ -59,6 +61,48 @@ public class NotifyController { return "success"; } + /** + * 原生支付回调 + */ + @RequestMapping("/native/pay/{platform}") + public String pay(@PathVariable String platform, @RequestBody JSONObject json) { + if (PayCst.Platform.WECHAT.equalsIgnoreCase(platform)) { + // 微信 + WechatNotifyReqDto reqDto = JSONObject.parseObject(json.toJSONString(), WechatNotifyReqDto.class); + log.info("【微信支付回调】收到微信支付回调 data: {}", JSONObject.toJSONString(reqDto)); + + String decrypted = WechatReqUtils.decryptRespParam(null, reqDto); + log.info("【微信支付回调】解密数据 {}", decrypted); + + WechatPayNotifyDataDto dataDto = JSONObject.parseObject(decrypted, WechatPayNotifyDataDto.class); + return "success"; + } else if (PayCst.Platform.ALIPAY.equalsIgnoreCase(platform)) { + // 支付宝 + return "success"; + } + throw new CzgException("不支持的支付平台"); + } + + /** + * 原生退款回调 + */ + @RequestMapping("/native/refund/{platform}") + public String refund(@PathVariable String platform, @RequestBody JSONObject json) { + if (PayCst.Platform.WECHAT.equalsIgnoreCase(platform)) { + // 微信 + WechatNotifyReqDto reqDto = JSONObject.parseObject(json.toJSONString(), WechatNotifyReqDto.class); + log.info("【微信退款回调】收到微信退款回调 data: {}", JSONObject.toJSONString(reqDto)); + String decrypted = WechatReqUtils.decryptRespParam(null, reqDto); + log.info("【微信退款回调】解密数据 {}", decrypted); + + return "success"; + } else if (PayCst.Platform.ALIPAY.equalsIgnoreCase(platform)) { + // 支付宝 + return "success"; + } + throw new CzgException("不支持的支付平台"); + } + @RequestMapping("/payCallBack") public String notifyCallBack(@RequestBody CzgBaseRespParams respParams) { diff --git a/cash-sdk/aggregation-pay/src/main/java/com/czg/dto/req/WechatNotifyReqDto.java b/cash-sdk/aggregation-pay/src/main/java/com/czg/dto/req/WechatNotifyReqDto.java new file mode 100644 index 000000000..71efa434e --- /dev/null +++ b/cash-sdk/aggregation-pay/src/main/java/com/czg/dto/req/WechatNotifyReqDto.java @@ -0,0 +1,33 @@ +package com.czg.dto.req; + +import com.alibaba.fastjson2.annotation.JSONField; +import lombok.Data; + +/** + * @author yjjie + * @date 2025/12/23 21:47 + */ +@Data +public class WechatNotifyReqDto { + + private String id; + @JSONField(name = "create_time") + private String createTime; + @JSONField(name = "resource_type") + private String resourceType; + @JSONField(name = "event_type") + private String eventType; + private String summary; + private Resource resource; + + @Data + public static class Resource { + @JSONField(name = "resource_type") + private String originalType; + private String algorithm; + private String ciphertext; + @JSONField(name = "associated_data") + private String associatedData; + private String nonce; + } +} diff --git a/cash-sdk/aggregation-pay/src/main/java/com/czg/dto/req/WechatPayNotifyDataDto.java b/cash-sdk/aggregation-pay/src/main/java/com/czg/dto/req/WechatPayNotifyDataDto.java new file mode 100644 index 000000000..321eb4fb4 --- /dev/null +++ b/cash-sdk/aggregation-pay/src/main/java/com/czg/dto/req/WechatPayNotifyDataDto.java @@ -0,0 +1,118 @@ +package com.czg.dto.req; + +import com.alibaba.fastjson2.annotation.JSONField; +import lombok.Data; + +/** + * @author yjjie + * @date 2025/12/23 22:21 + */ +@Data +public class WechatPayNotifyDataDto { + + /** + * 公众账号ID + */ + private String appid; + + /** + * 商户号 + */ + private String mchid; + + /** + * 商户订单号 + */ + @JSONField(name = "out_trade_no") + private String outTradeNo; + + /** + * 微信支付订单号 + */ + @JSONField(name = "transaction_id") + private String transactionId; + + /** + * 交易类型 + * JSAPI:公众号支付、小程序支付 + * NATIVE:Native支付 + * APP:APP支付 + * MICROPAY:付款码支付 + * MWEB:H5支付 + * FACEPAY:刷脸支付 + */ + @JSONField(name = "trade_type") + private String tradeType; + + /** + * 交易状态 + * SUCCESS—支付成功 + * REFUND—转入退款 + * NOTPAY—未支付 + * CLOSED—已关闭 + * REVOKED—已撤销(刷卡支付) + * USERPAYING--用户支付中 + * PAYERROR--支付失败(其他原因,如银行返回失败) + */ + @JSONField(name = "trade_state") + private String tradeState; + + /** + * 交易状态描述 + */ + @JSONField(name = "trade_state_desc") + private String tradeStateDesc; + + /** + * 付款银行 + */ + @JSONField(name = "bank_type") + private String bankType; + + /** + * 商户数据包 + */ + private String attach; + + /** + * 支付完成时间 + */ + @JSONField(name = "success_time") + private String successTime; + + /** + * 金额 + */ + private Amount amount; + + @Data + public static class Amount { + /** + * 用户支付金额 + */ + @JSONField(name = "payer_total") + private Integer payerTotal; + + /** + * 货币类型 + */ + private String currency; + + /** + * 总金额 + */ + private Integer total; + } + + public boolean isSuccess() { + return "SUCCESS".equals(tradeState); + } + + public boolean isClosed() { + return "CLOSED".equals(tradeState); + } + + public Long getPayAmount() { + return Long.valueOf(amount.getTotal()); + } +} diff --git a/cash-sdk/aggregation-pay/src/main/java/com/czg/third/alipay/AlipayIsvPayManager.java b/cash-sdk/aggregation-pay/src/main/java/com/czg/third/alipay/AlipayIsvPayManager.java index 493d9ae8e..3ed81c0f0 100644 --- a/cash-sdk/aggregation-pay/src/main/java/com/czg/third/alipay/AlipayIsvPayManager.java +++ b/cash-sdk/aggregation-pay/src/main/java/com/czg/third/alipay/AlipayIsvPayManager.java @@ -43,7 +43,7 @@ public class AlipayIsvPayManager { model.setTotalAmount(getYuanAmountByFen(paramsDto.getAmount())); model.setSubject(paramsDto.getTitle()); model.setBody(paramsDto.getBody()); - model.setNotifyUrl(paramsDto.getNotifyUrl()); + model.setNotifyUrl(paramsDto.getNotifyUrl() + "/" + PayCst.Platform.ALIPAY); model.setExtendParams(new ExtendParams()); CustomizedParams customizedParams = new CustomizedParams(); diff --git a/cash-sdk/aggregation-pay/src/main/java/com/czg/third/wechat/WechatAesUtil.java b/cash-sdk/aggregation-pay/src/main/java/com/czg/third/wechat/WechatAesUtil.java new file mode 100644 index 000000000..4e83075cd --- /dev/null +++ b/cash-sdk/aggregation-pay/src/main/java/com/czg/third/wechat/WechatAesUtil.java @@ -0,0 +1,60 @@ +package com.czg.third.wechat; + +import javax.crypto.AEADBadTagException; +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +/** + * 微信参数 AES 解密 + * @author GYJoker + */ +public class WechatAesUtil { + static final int KEY_LENGTH_BYTE = 32; + static final int TAG_LENGTH_BIT = 128; + private final byte[] aesKey; + public WechatAesUtil(byte[] key) { + if (key.length != KEY_LENGTH_BYTE) { + throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节"); + } + this.aesKey = key; + } + public String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext) + throws GeneralSecurityException { + try { + + // 调试信息:打印参数信息 + System.out.println("associatedData长度: " + (associatedData != null ? associatedData.length : 0)); + System.out.println("nonce长度: " + (nonce != null ? nonce.length : 0)); + System.out.println("ciphertext长度: " + ciphertext.length()); + + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + SecretKeySpec key = new SecretKeySpec(aesKey, "AES"); + GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce); + cipher.init(Cipher.DECRYPT_MODE, key, spec); + + // 处理 associatedData,确保使用空数组而非 null + cipher.updateAAD(associatedData != null ? associatedData : new byte[0]); + + byte[] decodedCiphertext = Base64.getDecoder().decode(ciphertext); + System.out.println("解码后密文长度: " + decodedCiphertext.length); + + byte[] plaintext = cipher.doFinal(decodedCiphertext); + return new String(plaintext, StandardCharsets.UTF_8); + } catch (AEADBadTagException e) { + // 捕获标签不匹配异常,提供更详细的错误信息 + throw new GeneralSecurityException("解密失败: 标签不匹配,可能是密钥错误、数据被篡改或参数不匹配", e); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new IllegalStateException(e); + } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { + throw new IllegalArgumentException("无效的密钥或参数", e); + } + } +} diff --git a/cash-sdk/aggregation-pay/src/main/java/com/czg/third/wechat/WechatPayManager.java b/cash-sdk/aggregation-pay/src/main/java/com/czg/third/wechat/WechatPayManager.java index 2fac42701..a99c698f1 100644 --- a/cash-sdk/aggregation-pay/src/main/java/com/czg/third/wechat/WechatPayManager.java +++ b/cash-sdk/aggregation-pay/src/main/java/com/czg/third/wechat/WechatPayManager.java @@ -13,7 +13,6 @@ import com.wechat.pay.java.core.Config; import com.wechat.pay.java.core.cipher.Signer; import com.wechat.pay.java.core.util.NonceUtil; import lombok.extern.slf4j.Slf4j; -import org.aspectj.apache.bcel.generic.RET; import org.dom4j.Document; import org.dom4j.DocumentHelper; import org.dom4j.Element; @@ -58,7 +57,7 @@ public class WechatPayManager { reqData.put("sub_mchid", paramsDto.getMerchantId()); reqData.put("description", paramsDto.getTitle()); reqData.put("out_trade_no", paramsDto.getOrderNo()); - reqData.put("notify_url", paramsDto.getNotifyUrl()); + reqData.put("notify_url", paramsDto.getNotifyUrl() + "/" + PayCst.Platform.WECHAT); reqData.put("attach", paramsDto.getExtData()); JSONObject amount = new JSONObject(); @@ -268,7 +267,7 @@ public class WechatPayManager { refundParam.put("out_trade_no", paramsDto.getOrderNo()); refundParam.put("out_refund_no", paramsDto.getRefundOrderNo()); refundParam.put("reason", paramsDto.getRefundReason()); - refundParam.put("notify_url", paramsDto.getRefundNotifyUrl()); + refundParam.put("notify_url", paramsDto.getRefundNotifyUrl() + "/" + PayCst.Platform.WECHAT); JSONObject amount = new JSONObject(); amount.put("total", paramsDto.getOrderAmount()); diff --git a/cash-sdk/aggregation-pay/src/main/java/com/czg/third/wechat/WechatReqUtils.java b/cash-sdk/aggregation-pay/src/main/java/com/czg/third/wechat/WechatReqUtils.java index adc8a79f3..8c1ed17cd 100644 --- a/cash-sdk/aggregation-pay/src/main/java/com/czg/third/wechat/WechatReqUtils.java +++ b/cash-sdk/aggregation-pay/src/main/java/com/czg/third/wechat/WechatReqUtils.java @@ -3,12 +3,11 @@ package com.czg.third.wechat; import cn.hutool.http.HttpRequest; import cn.hutool.http.HttpResponse; import cn.hutool.http.HttpUtil; +import com.czg.dto.req.WechatNotifyReqDto; import com.czg.exception.CzgException; import com.czg.third.wechat.dto.config.WechatPayConfigDto; import com.wechat.pay.java.core.Config; import com.wechat.pay.java.core.cipher.Signer; -import com.wechat.pay.java.core.http.HttpClient; -import com.wechat.pay.java.core.http.HttpMethod; import com.wechat.pay.java.core.util.NonceUtil; import lombok.extern.slf4j.Slf4j; @@ -16,7 +15,6 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Map; -import java.util.UUID; import java.util.stream.Collectors; /** @@ -102,6 +100,28 @@ public class WechatReqUtils { return signature; } + /** + * 解密微信回调报文 + * + * @param configDto 配置 + * @param reqDto 请求报文 + * @return 解密后的报文 + */ + public static String decryptRespParam(WechatPayConfigDto configDto, WechatNotifyReqDto reqDto) { + if (configDto == null) { + configDto = WechatPayConfigDto.getDefaultConfig(); + } + WechatAesUtil aesUtil = new WechatAesUtil(configDto.getApiV3Key().getBytes(StandardCharsets.UTF_8)); + try { + return aesUtil.decryptToString( + reqDto.getResource().getAssociatedData() == null ? new byte[0] : reqDto.getResource().getAssociatedData().getBytes(StandardCharsets.UTF_8), + reqDto.getResource().getNonce().getBytes(StandardCharsets.UTF_8), reqDto.getResource().getCiphertext()); + } catch (Exception e) { + log.error("【微信支付回调】解密失败 {}", reqDto); + throw new CzgException("解密失败"); + } + } + /** * 获取随机串 */