IDEA SPringBoot 整合H5微信支付

前言

​ 上周由于项目需要开通H5微信支付功能,于是在网上参考了很多例子,由于数据缺失,实用性不高,所以在此特地将SpringBoot整合H5微信支付的流程整理成文档,测试可用。

场景介绍

​ H5支付是指商户在微信客户端外的移动端网页展示商品或服务,用户在前述页面确认使用微信支付时,商户发起本服务呼起微信客户端进行支付。

​ 主要用于触屏版的手机浏览器请求微信支付的场景。可以方便的从外部浏览器唤起微信支付。

​ 申请入口:登录商户平台–>产品中心–>我的产品–>支付产品–>H5支付

​ 注意:需要开通H5支付,并且做一些配置

​ 微信官方体验链接:https://wxpay.wxutil.com/mch/pay/h5.v2.php,请在微信外浏览器打开。

IDEA SPringBoot 整合H5微信支付_第1张图片

1、用户在商户侧完成下单,使用微信支付进行支付

2、由商户后台向微信支付发起下单请求(调用统一下单接口)注:交易类型trade_type=MWEB

3、统一下单接口返回支付相关参数给商户后台,如支付跳转url(参数名“mweb_url”),商户通过mweb_url调起微信支付中间页

4、中间页进行H5权限的校验,安全性检查(此处常见错误请见下文)

5、如支付成功,商户后台会接收到微信侧的异步通知

6、用户在微信支付收银台完成支付或取消支付,返回商户页面(默认为返回支付发起页面)

7、商户在展示页面,引导用户主动发起支付结果的查询

8、商户后台判断是否接到收微信侧的支付结果通知,如没有,后台调用我们的订单查询接口确认订单状态

9、展示最终的订单支付结果给用户

H5支付文档

集成H5微信支付

1.导入依赖jar包

<dependency>
    <groupId>com.github.wxpaygroupId>
    <artifactId>wxpay-sdkartifactId>
    <version>0.0.3version>
dependency>

<dependency>
    <groupId>org.webjarsgroupId>
    <artifactId>jqueryartifactId>
    <version>3.3.1version>
dependency>

2. application.yml

# 测试账号
pay:
  wxpay:
     appID: wxab8acb865bb1637e
     mchID: 11473623
     key: 2ab9071b06b9f739b950ddb41db2690d
     sandboxKey: 3639bc1370e105aa65f10cd4fef2a3ef
     certPath: /var/local/cert/apiclient_cert.p12
     notifyUrl: http://65ta5j.natappfree.cc/wxpay/refund/notify
     useSandbox: true
spring:
  thymeleaf:
    prefix: classpath:/templates/
    suffix: .html
    mode: HTML5
    encoding: UTF-8   

3. WebMvcConfiguration

@Configuration
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
    @Override
    protected void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/gotoWapPage").setViewName("gotoWapPay");
        registry.addViewController("/gotoPagePage").setViewName("gotoPagePay");
        registry.addViewController("/gotoH5Page").setViewName("gotoH5Page");
        registry.addViewController("/h5PaySuccess").setViewName("h5PaySuccess");

        super.addViewControllers(registry);
    }

    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
        super.addResourceHandlers(registry);
    }
}

4. MyWXPayConfig

/**
 * 微信支付的参数配置
 *
 * @author mengday zhang
 */
@Data
@Slf4j
@ConfigurationProperties(prefix = "pay.wxpay")
public class MyWXPayConfig implements WXPayConfig{

    /** 公众账号ID */
    private String appID;

    /** 商户号 */
    private String mchID;

    /** API 密钥 */
    private String key;

    /** API 沙箱环境密钥 */
    private String sandboxKey;

    /** API证书绝对路径 */
    private String certPath;

    /** 退款异步通知地址 */
    private String notifyUrl;

    private Boolean useSandbox;

    /** HTTP(S) 连接超时时间,单位毫秒 */
    private int httpConnectTimeoutMs = 8000;

    /** HTTP(S) 读数据超时时间,单位毫秒 */
    private int httpReadTimeoutMs = 10000;


    /**
     * 获取商户证书内容
     *
     * @return 商户证书内容
     */
    @Override
    public InputStream getCertStream()  {
        File certFile = new File(certPath);
        InputStream inputStream = null;
        try {
            inputStream = new FileInputStream(certFile);
        } catch (FileNotFoundException e) {
            log.error("cert file not found, path={}, exception is:{}", certPath, e);
        }
        return inputStream;
    }

    @Override
    public String getKey(){
        if (useSandbox) {
            return sandboxKey;
        }
        return key;
    }

}

5. WXPayClient

/**
 * WXPayClient
 * 

* 对WXPay的简单封装,处理支付密切相关的逻辑. * * @author Mengday Zhang * @version 1.0 * @since 2018/6/16 */ @Slf4j public class WXPayClient extends WXPay { /** 密钥算法 */ private static final String ALGORITHM = "AES"; /** 加解密算法/工作模式/填充方式 */ private static final String ALGORITHM_MODE_PADDING = "AES/ECB/PKCS5Padding"; /** 用户支付中,需要输入密码 */ private static final String ERR_CODE_USERPAYING = "USERPAYING"; private static final String ERR_CODE_AUTHCODEEXPIRE = "AUTHCODEEXPIRE"; /** 交易状态: 未支付 */ private static final String TRADE_STATE_NOTPAY = "NOTPAY"; /** 用户输入密码,尝试30秒内去查询支付结果 */ private static Integer remainingTimeMs = 10000; private WXPayConfig config; public WXPayClient(WXPayConfig config, WXPayConstants.SignType signType, boolean useSandbox) { super(config, signType, useSandbox); this.config = config; } /** * * 刷卡支付 * * 对WXPay#microPay(Map)增加了当支付结果为USERPAYING时去轮询查询支付结果的逻辑处理 * * 注意:该方法没有处理return_code=FAIL的情况,暂时不考虑网络问题,这种情况直接返回错误 * * @param reqData * @return * @throws Exception */ public Map<String, String> microPayWithPOS(Map<String, String> reqData) throws Exception { // 开始时间(毫秒) long startTimestampMs = System.currentTimeMillis(); Map<String, String> responseMapForPay = super.microPay(reqData); log.info(responseMapForPay.toString()); // // 先判断 协议字段返回(return_code),再判断 业务返回,最后判断 交易状态(trade_state) // 通信标识,非交易标识 String returnCode = responseMapForPay.get("return_code"); if (WXPayConstants.SUCCESS.equals(returnCode)) { String errCode = responseMapForPay.get("err_code"); // 余额不足,信用卡失效 if (ERR_CODE_USERPAYING.equals(errCode) || "SYSTEMERROR".equals(errCode) || "BANKERROR".equals(errCode)) { Map<String, String> orderQueryMap = null; Map<String, String> requestData = new HashMap<>(); requestData.put("out_trade_no", reqData.get("out_trade_no")); // 用户支付中,需要输入密码或系统错误则去重新查询订单API err_code, result_code, err_code_des // 每次循环时的当前系统时间 - 开始时记录的时间 > 设定的30秒时间就退出 while (System.currentTimeMillis() - startTimestampMs < remainingTimeMs) { // 商户收银台得到USERPAYING状态后,经过商户后台系统调用【查询订单API】查询实际支付结果。 orderQueryMap = super.orderQuery(requestData); String returnCodeForQuery = orderQueryMap.get("return_code"); if (WXPayConstants.SUCCESS.equals(returnCodeForQuery)) { // 通讯成功 String tradeState = orderQueryMap.get("trade_state"); if (WXPayConstants.SUCCESS.equals(tradeState)) { // 如果成功了直接将查询结果返回 return orderQueryMap; } // 如果支付结果仍为USERPAYING,则每隔5秒循环调用【查询订单API】判断实际支付结果 Thread.sleep(1000); } } // 如果用户取消支付或累计30秒用户都未支付,商户收银台退出查询流程后继续调用【撤销订单API】撤销支付交易。 String tradeState = orderQueryMap.get("trade_state"); if (TRADE_STATE_NOTPAY.equals(tradeState) || ERR_CODE_USERPAYING.equals(tradeState) || ERR_CODE_AUTHCODEEXPIRE.equals(tradeState)) { Map<String, String> reverseMap = this.reverse(requestData); String returnCodeForReverse = reverseMap.get("return_code"); String resultCode = reverseMap.get("result_code"); if (WXPayConstants.SUCCESS.equals(returnCodeForReverse) && WXPayConstants.SUCCESS.equals(resultCode)) { // 如果撤销成功,需要告诉客户端已经撤销订单了 responseMapForPay.put("err_code_des", "用户取消支付或尚未支付,后台已经撤销该订单,请重新支付!"); } } } } return responseMapForPay; } /** * 从request的inputStream中获取参数 * @param request * @return * @throws Exception */ public Map<String, String> getNotifyParameter(HttpServletRequest request) throws Exception { InputStream inputStream = request.getInputStream(); ByteArrayOutputStream outSteam = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int length = 0; while ((length = inputStream.read(buffer)) != -1) { outSteam.write(buffer, 0, length); } outSteam.close(); inputStream.close(); // 获取微信调用我们notify_url的返回信息 String resultXml = new String(outSteam.toByteArray(), "utf-8"); Map<String, String> notifyMap = WXPayUtil.xmlToMap(resultXml); return notifyMap; } /** * 解密退款通知 * * public Map<String, String> decodeRefundNotify(HttpServletRequest request) throws Exception { // 从request的流中获取参数 Map<String, String> notifyMap = this.getNotifyParameter(request); log.info(notifyMap.toString()); String reqInfo = notifyMap.get("req_info"); //(1)对加密串A做base64解码,得到加密串B byte[] bytes = new BASE64Decoder().decodeBuffer(reqInfo); //(2)对商户key做md5,得到32位小写key* ( key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置 ) Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING); SecretKeySpec key = new SecretKeySpec(WXPayUtil.MD5(config.getKey()).toLowerCase().getBytes(), ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, key); //(3)用key*对加密串B做AES-256-ECB解密(PKCS7Padding) // java.security.InvalidKeyException: Illegal key size or default parameters // https://www.cnblogs.com/yaks/p/5608358.html String responseXml = new String(cipher.doFinal(bytes),"UTF-8"); Map<String, String> responseMap = WXPayUtil.xmlToMap(responseXml); return responseMap; } /** * 获取沙箱环境验签秘钥API * 获取验签秘钥API文档 * @return * @throws Exception */ public Map<String, String> getSignKey() throws Exception { Map<String, String> reqData = new HashMap<>(); reqData.put("mch_id", config.getMchID()); reqData.put("nonce_str", WXPayUtil.generateNonceStr()); String sign = WXPayUtil.generateSignature(reqData, config.getKey(), WXPayConstants.SignType.MD5); reqData.put("sign", sign); String responseXml = this.requestWithoutCert("https://api.mch.weixin.qq.com/sandboxnew/pay/getsignkey", reqData, config.getHttpConnectTimeoutMs(), config.getHttpReadTimeoutMs()); Map<String, String> responseMap = WXPayUtil.xmlToMap(responseXml); return responseMap; } }

6. WXPayConfiguration

/**
 * 微信支付配置
 *
 * @author mengday zhang
 */
@Configuration
@EnableConfigurationProperties(MyWXPayConfig.class)
public class WXPayConfiguration {

    @Autowired
    private MyWXPayConfig wxPayConfig;

    /**
     * useSandbox 沙盒环境
     * @return
     */
    @Bean
    public WXPay wxPay() {
        return new WXPay(wxPayConfig, WXPayConstants.SignType.MD5, wxPayConfig.getUseSandbox() );
    }

    @Bean
    public WXPayClient wxPayClient() {
        return new WXPayClient(wxPayConfig, WXPayConstants.SignType.MD5, wxPayConfig.getUseSandbox());
    }
}

7. gotoH5Page.html


<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Titletitle>
head>
<body style="font-size: 30px">

<h3>购买商品:可口可乐h3>
<h3>价格:4h3>
<h3>数量:10个h3>

<button style="width: 100%; height: 60px; alignment: center; background: #b49e8f" onclick="commitOrder()">提交订单button>

<script src="http://localhost:8080/webjars/jquery/3.3.1/jquery.js">script>
<script>
    function commitOrder() {
        $.ajax({
            type: "POST",
            url: "http://localhost:8080/wxpay/h5pay/order",
            data: null,
            success: function(data) {
                console.log(data);
                var redirectUrl = "http://localhost:8080/h5PaySuccess";
                var mwebUrl = data.mweb_url+"&redirect_url="+encodeURIComponent(redirectUrl);
                window.location.href=mwebUrl;
            }

        })
    }
script>

body>
html>

8. h5PaySuccess.html


<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Titletitle>
head>
<body>

<h1>微信支付-H5支付成功h1>

body>
html>

9. WXPayH5PayController

/**
 * 微信支付-H5支付.
 * 

* detailed description * * @author Mengday Zhang * @version 1.0 * @since 2018/6/18 */ @Slf4j @RestController @RequestMapping("/wxpay/h5pay") public class WXPayH5PayController { @Autowired private WXPay wxPay; @Autowired private WXPayClient wxPayClient; /** * 使用沙箱支付的金额必须是用例中指定的金额,也就是 1.01 元,1.02元等,不能是你自己的商品的实际价格,必须是这个数。 * 否则会报错:沙箱支付金额(2000)无效,请检查需要验收的case * @return * @throws Exception */ @PostMapping("/order") public Object h5pay() throws Exception { Map<String, String> reqData = new HashMap<>(); reqData.put("out_trade_no", String.valueOf(System.nanoTime())); reqData.put("trade_type", "MWEB"); reqData.put("product_id", "1"); reqData.put("body", "商户下单"); // 订单总金额,单位为分 reqData.put("total_fee", "101"); // APP和网页支付提交用户端ip,Native支付填调用微信支付API的机器IP。 reqData.put("spbill_create_ip", "14.23.150.211"); // 异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 reqData.put("notify_url", "http://3sbqi7.natappfree.cc/wxpay/h5pay/notify"); // 自定义参数, 可以为终端设备号(门店号或收银设备ID),PC网页或公众号内支付可以传"WEB" reqData.put("device_info", ""); // 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用。 reqData.put("attach", ""); reqData.put("scene_info", "{\"h5_info\": {\"type\":\"Wap\",\"wap_url\": \"http://3sbqi7.natappfree.cc\",\"wap_name\": \"腾讯充值\"}}"); Map<String, String> responseMap = wxPay.unifiedOrder(reqData); log.info(responseMap.toString()); String returnCode = responseMap.get("return_code"); String resultCode = responseMap.get("result_code"); if (WXPayConstants.SUCCESS.equals(returnCode) && WXPayConstants.SUCCESS.equals(resultCode)) { // 预支付交易会话标识 String prepayId = responseMap.get("prepay_id"); // 支付跳转链接(前端需要在该地址上拼接redirect_url,该参数不是必须的) // 正常流程用户支付完成后会返回至发起支付的页面,如需返回至指定页面,则可以在MWEB_URL后拼接上redirect_url参数,来指定回调页面 // 需对redirect_url进行urlencode处理 // TODO 正常情况下这里应该是普通的链接,不知道这里为何是weixin://这样的链接,不知道是不是微信公众平台上的配置少配置了; // 由于没有实际账号,还没找到为啥不是普通链接的原因 String mwebUrl = responseMap.get("mweb_url"); } return responseMap; } /** * 注意:如果是沙箱环境,一提交订单就会立即异步通知,而无需拉起微信支付收银台的中间页面 * @param request * @throws Exception */ @RequestMapping("/notify") public void payNotify(HttpServletRequest request, HttpServletResponse response) throws Exception{ Map<String, String> reqData = wxPayClient.getNotifyParameter(request); log.info(reqData.toString()); String returnCode = reqData.get("return_code"); String resultCode = reqData.get("result_code"); if (WXPayConstants.SUCCESS.equals(returnCode) && WXPayConstants.SUCCESS.equals(resultCode)) { boolean signatureValid = wxPay.isPayResultNotifySignatureValid(reqData); if (signatureValid) { // TODO 业务处理 Map<String, String> responseMap = new HashMap<>(2); responseMap.put("return_code", "SUCCESS"); responseMap.put("return_msg", "OK"); String responseXml = WXPayUtil.mapToXml(responseMap); response.setContentType("text/xml"); response.getWriter().write(responseXml); response.flushBuffer(); } } } }

常见问题

1.@Data @Slf4j标签的引用

参考 IntelliJ IDEA lombok插件的安装和使用

2.微信支付常见错误

参考 微信官方文档 微信支付常见问题 微信返回错误提示

3.沙箱环境说明

参考 浅析微信支付:如何使用沙箱环境测试

注意:本文档中 useSandbox: true 为沙箱测试环境,返回微信地址为 weixin://这样的链接,提交订单就会立即异步通知,不会拉起微信支付收银台的中间页面,当 useSandbox: false 时,切换为生产环境,浏览器会跳转至微信支付收银台的中间页面。

回调页面

​ 正常流程用户支付完成后会返回至发起支付的页面,如需返回至指定页面,则可以在MWEB_URL后拼接上redirect_url参数,来指定回调页面。

如:

​ 您希望用户支付完成后跳转至https://www.wechatpay.com.cn,则可以做如下处理:

​ 假设您通过统一下单接口获到的MWEB_URL= https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx20161110163838f231619da20804912345&package=1037687096

​ 则拼接后的地址为MWEB_URL= https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx20161110163838f231619da20804912345&package=1037687096&redirect_url=https%3A%2F%2Fwww.wechatpay.com.cn

注意:

注意MWEB_URL是普通的链接,不是微信weixin://短链接(useSandbox: false)
需对redirect_url进行urlencode处理
由于设置redirect_url后,回跳指定页面的操作可能发生在:

微信支付中间页调起微信收银台后超过5秒

用户点击“取消支付“或支付完成后点“完成”按钮。因此无法保证页面回跳时,支付流程已结束,所以商户设置的redirect_url地址不能自动执行查单操作,应让用户去点击按钮触发查单操作。

本博客参考 :https://blog.csdn.net/vbirdbest/article/details/80726616 原创文档
作者博客网址:IDEA SPringBoot 整合H5微信支付

你可能感兴趣的:(支付模块,微信支付,H5支付,支付集成,支付模块,支付)