一:开发文档场景介绍
H5支付是指商户在微信客户端外的移动端网页展示商品或服务,用户在前述页面确认使用微信支付时,商户发起本服务呼起微信客户端进行支付。
主要用于触屏版的手机浏览器请求微信支付的场景。可以方便的从外部浏览器唤起微信支付。
申请入口:登录商户平台-->产品中心-->我的产品-->支付产品-->H5支付 注意:需要开通H5支付,并且做一些配置
微信官方体验链接:http://wxpay.wxutil.com/mch/pay/h5.v2.php,请在微信外浏览器打开。
1、用户在商户侧完成下单,使用微信支付进行支付
2、由商户后台向微信支付发起下单请求(调用统一下单接口)注:交易类型trade_type=MWEB
3、统一下单接口返回支付相关参数给商户后台,如支付跳转url(参数名“mweburl”),商户通过mweburl调起微信支付中间页
4、中间页进行H5权限的校验,安全性检查(此处常见错误请见下文)
5、如支付成功,商户后台会接收到微信侧的异步通知
6、用户在微信支付收银台完成支付或取消支付,返回商户页面(默认为返回支付发起页面)
7、商户在展示页面,引导用户主动发起支付结果的查询
8,9、商户后台判断是否接到收微信侧的支付结果通知,如没有,后台调用我们的订单查询接口确认订单状态
10、展示最终的订单支付结果给用户
H5支付文档
二:集成步骤1. 引入依赖
<dependency> <groupId>com.github.wxpay</groupId> <artifactId>wxpay-sdk</artifactId> <version>0.0.3</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>3.3.1</version> </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 * <p> * 对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; } /** * 解密退款通知 * * <a href="https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_16&index=11>退款结果通知文档</a> * @return * @throws Exception */ 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 * <a href="https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=23_1">获取验签秘钥API文档</a> * @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
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body style="font-size: 30px"> <h3>购买商品:越南新娘</h3> <h3>价格:20000</h3> <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> </htm
8. h5PaySuccess.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>微信支付-H5支付成功</h1> </body> </html>
9. WXPayH5PayController
三: 常见问题
/** * 微信支付-H5支付. * <p> * 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(); } } } }
一、回调页面
正常流程用户支付完成后会返回至发起支付的页面,如需返回至指定页面,则可以在MWEBURL后拼接上redirecturl参数,来指定回调页面。
如,您希望用户支付完成后跳转至https://www.wechatpay.com.cn,则可以做如下处理:
假设您通过统一下单接口获到的MWEBURL= https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepayid=wx20161110163838f231619da20804912345&package=1037687096
则拼接后的地址为MWEBURL= https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepayid=wx20161110163838f231619da20804912345&package=1037687096&redirect_url=https://www.wechatpay.com.cn
注意:
- 注意MWEB_URL是普通的链接,不是微信那种短链接(我在使用测试账号时就返回了短链接,没找到什么原因,估计使用真实账号,申请开通H5支付,然后做一些开发配置估计就正常了)
- 需对redirect_url进行urlencode处理
- 由于设置redirect_url后,回跳指定页面的操作可能发生在:
获取源码
- 微信支付中间页调起微信收银台后超过5秒
- 用户点击“取消支付“或支付完成后点“完成”按钮。因此无法保证页面回跳时,支付流程已结束,所以商户设置的redirect_url地址不能自动执行查单操作,应让用户去点击按钮触发查单操作。
关注并回复关键字“微信支付H5支付”获取源码。
,