简易demo演示
demo演示
点击体验
业务流程图
主要步骤为,pc端生成订单调用Native下单接口生成微信的native 跳转链接,再生成二维码返回到页面,用户微信扫一扫完成支付,微信支付后台回调支付成功的链接。
支付接入
引入微信支付的SDK
<dependency><groupId>com.github.wechatpay-apiv3</groupId><artifactId>wechatpay-java</artifactId><version>0.2.14</version></dependency>
生成二维码的工具,这里我选择了zxing
<dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>3.4.1</version></dependency><dependency><groupId>com.google.zxing</groupId><artifactId>javase</artifactId><version>3.4.1</version></dependency>
创建订单接口https://pay.weixin.qq.com/doc/v3/merchant/4012525095
需要商户号、商户APIV3密钥、商户证书序列号、商户API秘钥文件
正常秘钥文件放在服务器上读取,这里先直接放在项目文件resources下面
@Slf4j
@Service
public class WechatPayService extends AbstractPayService implements InitializingBean {/*** 商户号*/public static String merchantId = "xxx";/*** 商户证书序列号*/public static String merchantSerialNumber = "xxxx";/*** 商户APIV3密钥*/public static String apiV3Key = "xxxx";private Config config;/*** 商户API私钥*/private String privateKeyPem;@Overridepublic void afterPropertiesSet() throws Exception {ClassPathResource resource = new ClassPathResource("wechat/apiclient_key.pem");privateKeyPem = IOUtils.toString(resource.getInputStream());config = new RSAAutoCertificateConfig.Builder().merchantId(merchantId).privateKey(privateKeyPem).merchantSerialNumber(merchantSerialNumber).apiV3Key(apiV3Key).build();}@Overridepublic String createPayOrder(String clientIp, OrderRequest orderRequest) {final String orderNo = OrderUtils.generateOrderNo();// 一个商户号只能初始化一个配置,否则会因为重复的下载任务报错// 构建serviceNativePayService service = new NativePayService.Builder().config(config).build();// request.setXxx(val)设置所需参数,具体参数可见Request定义PrepayRequest request = new PrepayRequest();Amount amount = new Amount();amount.setTotal(1);request.setAmount(amount);request.setAppid("xxxx");request.setMchid(merchantId);request.setDescription("VIP体验卡");request.setNotifyUrl("http://myrkfh.natappfree.cc/mark_day/wechat/pay/notify");request.setOutTradeNo(orderNo);// 调用下单方法,得到应答PrepayResponse response = service.prepay(request);// 使用微信扫描 code_url 对应的二维码,即可体验Native支付log.info("调用微信生成订单返回结果:", response.toString());if (StrUtil.isBlank(response.getCodeUrl())) {throw new BaseException("创建订单失败");}String codeUrl = response.getCodeUrl();// 生成二维码QrConfig qrConfig = QrConfig.create();String qrBase64Text = QrCodeUtil.generateAsBase64(codeUrl, qrConfig, "svg");// 这里可以记录订单信息return qrBase64Text;}
orderNo在本系统中要唯一,不能有重复的订单号,否则下单失败
private static final String DATE_FORMAT = "yyyyMMddHHmmss";public static String generateOrderNo() {LocalDateTime now = LocalDateTime.now();DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT);String timestamp = now.format(formatter);return timestamp + RandomUtil.randomNumbers(18);}
其中notifyUrl为支付成功回调地址,本地调试可以借助内网穿透工具,其他参数接口文档https://pay.weixin.qq.com/doc/v3/merchant/4012525095
微信返回的codeUrl为weixin://wxpay/bizpayurl?pr=Vd8RvS7z4格式的,放在手机上可以直接打开,我们要做的就是将codeUrl放入二维码的内容,也可以直接 使用Hutool,返回的二维码可以保存图片,将图片地址返回前端,也可以直接返回base64的图片地址,这里一般会记录订单数据为待支付状态数据,最后根据自己需要生成二维码的内容如下,有效期为两个小时
将地址返回过后后面就是等待微信支付的回调,
微信回调通知
微信支付通知接口文档 https://pay.weixin.qq.com/doc/v3/merchant/4012084431
如果处理成功了,需要返回HTTP状态码为200或204,否则微信会根据一定的策略进行重试
/*** 微信支付回调实体** @author liufuqiang* @Date 2024-10-21 17:33:00*/
@Data
public class WechatPayNotifyDTO {private String id;/*** 通知创建的时间*/@JsonAlias("create_time")private String createTime;/*** 通知的资源数据类型,支付成功通知为encrypt-resource。*/@JsonAlias("resource_type")private String resourceType;/*** 通知的类型,支付成功通知的类型为TRANSACTION.SUCCESS*/@JsonAlias("event_type")private String eventType;/*** 通知资源数据*/private Resource resource;@Datapublic static class Resource {/*** 加密算法AEAD_AES_256_GCM*/private String algorithm;/*** Base64编码后的开启/停用结果数据密文。*/private String ciphertext;/*** 附加数据。*/@JsonAlias("associated_data")private String associatedData;/*** 原始回调类型,为transaction*/@JsonAlias("original_type")private String originalType;/*** 加密使用的随机串。*/private String nonce;}/*** 回调摘要*/private String summary;
接受回调
@PostMapping("/notify")public void payNotify(HttpServletResponse response,HttpServletRequest request,@RequestHeader(Constant.WECHAT_PAY_SIGNATURE) String signature,@RequestHeader(Constant.WECHAT_PAY_NONCE) String nonce,@RequestHeader(Constant.WECHAT_PAY_TIMESTAMP) String timestamp,@RequestHeader(Constant.WECHAT_PAY_SERIAL) String serialNo,@RequestBody String requestBody) {HttpHeaders httpHeaders = new HttpHeaders();httpHeaders.addHeader(Constant.WECHAT_PAY_SIGNATURE, signature);httpHeaders.addHeader(Constant.WECHAT_PAY_NONCE, nonce);httpHeaders.addHeader(Constant.WECHAT_PAY_TIMESTAMP, timestamp);httpHeaders.addHeader(Constant.WECHAT_PAY_SERIAL, serialNo);// 验证签名boolean isSignValid = wechatPayService.validSignature(httpHeaders, requestBody);if (!isSignValid) {response.setStatus(HttpServletResponse.SC_BAD_REQUEST);}log.info("签名验证通过");WechatPayNotifyDTO notifyDTO = JsonUtils.genBeanByJson(requestBody, WechatPayNotifyDTO.class);wechatPayService.paymentHandle(notifyDTO);response.setStatus(HttpServletResponse.SC_OK);}
验签
防止伪造支付的回调,需要会返回的内容进行签名验证,
非文件/下载验证签名文档https://pay.weixin.qq.com/doc/v3/merchant/4012365350
如果嫌弃麻烦,也可以直接使用WechatPay2Validator
验证签名的时候尽量不要全部取出请求头的内容,根据HttpServletRequest全部取出的HeaderMap里面的key值全部是小写,签名的时候避免取到空值
签名的第三个参数为全部requestBody的内容,所以这里不能用实体对象接受数据,直接改用字符串接受数据
解密
根据API商户秘钥以及接口返回的associatedData、nonce对密文ciphertext进行解密,是AES对称加密解密
static final int TAG_LENGTH_BIT = 128;
public static String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext) {try {Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");SecretKeySpec key = new SecretKeySpec(apiV3Key.getBytes(), "AES");GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);cipher.init(Cipher.DECRYPT_MODE, key, spec);cipher.updateAAD(associatedData);return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), "utf-8");} catch (NoSuchAlgorithmException | NoSuchPaddingException | UnsupportedEncodingException e) {} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException |BadPaddingException e) {}return "";}
解析完成是json字符串,这俩使用jackjson转为对象
@Data
public class OutOrderDTO {private String appid;@JsonAlias("out_trade_no")private String outTradeNo;@JsonAlias("trade_type")private String tradeType;@JsonAlias("trade_state")private String tradeState;@JsonAlias("trade_state_desc")private String tradeStateDesc;@JsonAlias("bank_ype")private String bankType;@JsonAlias("success_time")private String payTime;private Payer payer;private Amount amount;@Datapublic static final class Payer {private String openid;}@Datapublic static final class Amount {private Integer total;@JsonAlias("payer_total")private String payerTotal;}
创建订单时的OutTradeNo订单号也会返回,为了避免重复处理,在订单表加状态判断或者加分布式锁
WechatPayNotifyDTO.Resource resource = wechatPayNotifyDTO.getResource();String jsonStr = decryptToString(resource.getAssociatedData().getBytes(StandardCharsets.UTF_8),resource.getNonce().getBytes(StandardCharsets.UTF_8), resource.getCiphertext());log.info("微信支付回调信息:{}", jsonStr);OutOrderDTO outOrderDTO = JsonUtils.genBeanByJson(jsonStr, OutOrderDTO.class);// 订单处理log.info("支付完成");
同一个二维码也就是生成的native链接如果已经被支付过会提示订单已经支付,请勿重复发起支付