微信支付商户系统接入Native支付

简易demo演示

Native支付介绍

目前微信支付有以下几种场景

  • JSAPi支付,适合微信公众号及微信小程序
  • APP支付
  • H5支付
  • Native支付,适合PC网站页面支付
    微信支付商户平台
    微信支付Native接口文档
    Native支付是指商户系统按照微信支付协议生成支付二维码,用户再用微信“扫一扫”实现支付

前提准备

微信商家号、微信小程序或者微信公众号appId、商户证书
需要提前开启Native支付

需要将公众号或者小程序的appId在微信支付后台关联起来

在微信支付——账户中心——API安全,生成商户APIV3证书

业务流程图


主要步骤为,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;

    @Override
    public 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();
    }

    @Override
    public String createPayOrder(String clientIp, OrderRequest orderRequest) {
        final String orderNo = OrderUtils.generateOrderNo();
        // 一个商户号只能初始化一个配置,否则会因为重复的下载任务报错
        // 构建service
        NativePayService 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;

    @Data
    public 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;

    @Data
    public static final class Payer {
        private String openid;
    }

    @Data
    public 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("支付完成");

在手机端查看订单详情信息可以看到订单号,关联的appid以及其他的信息

同一个二维码也就是生成的native链接如果已经被支付过会提示订单已经支付,请勿重复发起支付

posted @ 2024-11-01 14:17  木马不是马  阅读(18)  评论(0编辑  收藏  举报