本文来自作者 javen 在 GitChat 上分享「微信支付接入的那点事儿」,「阅读原文」查看交流实录
「文末高能」
编辑 | 嘉仔
前言
本次 Chat 中涉及到的图片、统计数据均来自于网络,截图均为 macOS 和 Chrome 以及手机上的截图,如有侵权请联系删除。
微信是由张小龙所带领的腾讯广州研发中心产品团队打造于2011年1月21日推出的。
截止6月底微信月活跃数已远远超越QQ,微信月活跃账户数已达到9.63亿而QQ智能终端月活6.62亿。
微信红包与2015年春节联欢晚会的互动,让其成为了年夜饭的主菜单,小小的红包甚至不小心抢了春晚的风头让人记忆深刻。现在大到商场超市小到街边摊贩都用到了微信支付。
现在可以说微信支付在整个微信体系中承担着相当重要的一个角色,微信以及支付宝甚至开辟了国内乃至世界独有的全民支付模式。
前不久网上流传的一个话题 你最想把中国的什么带回祖国?
这里说的支付宝其实就是移动支付。这里有一份2017上半年中国第三方移动支付市场研究报告 感兴趣的点击这里查看详情(http://www.chinaz.com/news/2017/0814/796147.shtml)。
这么大的用户量这好的支付环境我想没有那个公司不为之动心吧。涉及到支付必定会接入大厂的财付通(微信支付、QQ钱包)、支付宝支付甚至是银联支付。
那作为开发人员的我们是不是应该需要了解一下支付行业呢?
好了,现在开始我们的探索之旅。
不同行业使用微信支付的费率是多少?
参考资料:
-
结算规则、费率、周期说明(http://kf.qq.com/faq/140225MveaUz1504092YFjeM.html)
-
商户类目对应资质、费率、结算周期(http://kf.qq.com/faq/140225MveaUz1501077rEfqI.html)
以下是部分截图方便移动设备查看:
微信提供了哪些支付方式
对于开发人员而言接触到一个新的平台或者一个新的知识点一般都会看看开发者文档(说文艺一点就是Wiki),如果没有这玩意怎么办呢?最直接方式就是查看官方提供的 Demo 源码并运行看预期效果。
微信支付以及公众号开发官方提供的Demo大部分都是PHP的,让我们Java开发的情何以堪!!!
只能耐着寂寞默默的啃文档。
微信支付开发文档( https://pay.weixin.qq.com/wiki/doc/api/index.html )
说到微信的开发文档,这里不得不说下微信关于支付、公众号开发这块的文档有很多网友吐槽。
但到目前为止官方也没有说要优化文档的意思,只能靠我们自己一遍一遍的试错和查资料了。
微信的支付方式主要包括:
-
刷卡支付
-
公众号支付
-
扫码支付(微信买单)
-
APP支付
-
H5支付
-
小程序支付
微信支付的版本主要包括:
-
普通商户版
-
服务商版
-
银行服务商版
那么问题来了,普通商户版、服务商版、银行服务商版有什么区别?
微信支付不同的版本有什么区别
-
普通商户版:公司注册微信服务号认证并申请开通微信支付自己使用即为微信的普通商户。
-
服务商版:公司注册微信服务号认证申请开通微信支付并申请开发服务商,邀请第三方公司入驻签约即为微信的服务商。
-
银行服务商版:与服务商版类似只是正对银行资质的。
有人要问服务商可以申请自己作为子商户吗?
回答:不能, 服务商的子商户的主体必须和服务商不一致,单独申请普通商户支付就好了,微信公众平台>微信支付>支付申请。
服务商版下如何添加商户、如何实现盈利?
如何添加商户
这里为大家找到了一份详细的资料可以点击这里查看( http://kf.qq.com/faq/1612203AfMba161220eU3AjY.html )。
以下是部分截图方便移动设备查看:
成为服务商有什么好处?
成功申请为微信支付服务商后,可以为没有开发能力的商户提供对接系统的生产、安装和维护服务,自行与商家进行业务洽谈,服务商信息会在我司备案,消除了商家质疑心理,使它更具备官方性、真实性、正规性。
如何实现盈利
-
微信支付官方的活动(智慧餐厅推广活动、鼓励金等)
-
不定期的服务奖励金
-
与商家洽谈的费率差
这里说明一下,上文提到的费率有些行业费率是零。试问服务商还有费率差吗?
回答:有的,服务商保底收入的费率为0.05,餐饮行业目前微信有做线下活动对商户来说费率零但微信会给服务商拿0.2,一般行业如果费率是0.6那么微信拿0.2服务商拿0.4。
不同场景下的支付方式如何实现快速接入?
接入参考资料:
-
刷卡支付:http://blog.csdn.net/zyw_java/article/details/54024198
-
公众号支付:http://blog.csdn.net/zyw_java/article/details/54023968
-
扫码支付:http://blog.csdn.net/zyw_java/article/details/54024162
-
APP支付:http://blog.csdn.net/zyw_java/article/details/54024232
-
H5支付:http://blog.csdn.net/zyw_java/article/details/77507835
-
小程序支付:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_sl_api.php?chapter=7_7&index=5(与公众号支付类似只是下单与唤起支付方式与二次签名不一样)
关于“小程序支付”,这里有你最关心的7个问题
说明:如果你想同时使用公众号支付、APP支付、小程序支付每年的微信认证费得¥900。因为都需要在不同的平台申请账号并进行微信认证。
总体来看微信支付主要提供以下几个接口
-
统一下单接口
-
提交刷卡支付接口(刷卡支付)
-
查询订单接口
-
关闭订单接口
-
撤销订单接口
-
申请退款接口
-
查询退款接口
-
下载对账单接口
-
调起支付接口
-
支付结果通知、退款结果通知
-
其他辅助接口(拉取订单评价数据、交易保障、转换短链接)
以上所有的接口在我的支付开源项目中都有实现。IJPay让支付触手可及 ( https://github.com/Javen205/IJPay )
以上接口都是需要 Https 的支持而且都会涉及到签名( 签名生成算法:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=4_3 )很多人都会遇到问题。这里贴一下实现的代码。
签名实现
/** * 生成签名 * * @param params * 参数 * @param partnerKey * 支付密钥 * @return {String} * */ public static String createSign(Map<String, String> params, String partnerKey) { // 生成签名前先去除sign params.remove("sign"); String stringA = packageSign(params, false); String stringSignTemp = stringA + "&key=" + partnerKey; return HashKit.md5(stringSignTemp).toUpperCase(); } /** * 组装签名的字段 * * @param params * 参数 * @param urlEncoder * 是否urlEncoder * @return {String} */ public static String packageSign(Map<String, String> params, boolean urlEncoder) { // 先将参数以其参数名的字典序升序进行排序 TreeMap<String, String> sortedParams = new TreeMap<String, String>(params); // 遍历排序后的字典,将所有参数按"key=value"格式拼接在一起 StringBuilder sb = new StringBuilder(); boolean first = true; for (Entry<String, String> param : sortedParams.entrySet()) { String value = param.getValue(); if (StrKit.isBlank(value)) { continue; } if (first) { first = false; } else { sb.append("&"); } sb.append(param.getKey()).append("="); if (urlEncoder) { try { value = urlEncode(value); } catch (UnsupportedEncodingException e) { } } sb.append(value); } return sb.toString(); } /** * 预付订单再次签名 * @param prepay_id * @return <Map<String, String>> */ public static Map<String, String> prepayIdCreateSign(String prepay_id) { Map<String, String> packageParams = new HashMap<String, String>(); packageParams.put("appId", WxPayApiConfigKit.getWxPayApiConfig().getAppId()); packageParams.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000)); packageParams.put("nonceStr", String.valueOf(System.currentTimeMillis())); packageParams.put("package", "prepay_id=" + prepay_id); packageParams.put("signType", "MD5"); String packageSign = PaymentKit.createSign(packageParams, WxPayApiConfigKit.getWxPayApiConfig().getPaternerKey()); packageParams.put("paySign", packageSign); return packageParams; }
Https工具类
https://github.com/Javen205/IJPay/blob/master/src/main/java/com/jpay/util/HttpUtils.java
public final class HttpUtils { private HttpUtils() {} public static String get(String url) { return delegate.get(url); } public static String get(String url, Map<String, String> queryParas) { return delegate.get(url, queryParas); } public static String post(String url, String data) { return delegate.post(url, data); } public static String post(String url, Map<String, String> queryParas) { return delegate.post(url, queryParas); } public static String postSSL(String url, String data, String certPath, String certPass) { return delegate.postSSL(url, data, certPath, certPass); } public static InputStream download(String url, String params){ return delegate.download(url, params); } public static String upload(String url, File file, String params) { return delegate.upload(url, file, params); } /** * http请求工具 委托 * 优先使用OkHttp * 最后使用JFinal HttpKit */ private interface HttpDelegate { String get(String url); String get(String url, Map<String, String> queryParas); String post(String url, String data); String post(String url, Map<String, String> queryParas); String postSSL(String url, String data, String certPath, String certPass); InputStream download(String url, String params); String upload(String url, File file, String params); } // http请求工具代理对象 private static final HttpDelegate delegate; static { HttpDelegate delegateToUse = null; // okhttp3.OkHttpClient? if (ClassUtils.isPresent("okhttp3.OkHttpClient", HttpUtils.class.getClassLoader())) { delegateToUse = new OkHttp3Delegate(); } // com.squareup.okhttp.OkHttpClient? else if (ClassUtils.isPresent("com.squareup.okhttp.OkHttpClient", HttpUtils.class.getClassLoader())) { delegateToUse = new OkHttpDelegate(); } delegate = delegateToUse; } /** * OkHttp2代理 */ private static class OkHttpDelegate implements HttpDelegate { private final com.squareup.okhttp.OkHttpClient httpClient; private final com.squareup.okhttp.OkHttpClient httpsClient; Lock lock = new ReentrantLock(); public OkHttpDelegate() { httpClient = new com.squareup.okhttp.OkHttpClient(); // 分别设置Http的连接,写入,读取的超时时间 httpClient.setConnectTimeout(10, TimeUnit.SECONDS); httpClient.setWriteTimeout(10, TimeUnit.SECONDS); httpClient.setReadTimeout(30, TimeUnit.SECONDS); httpsClient = httpClient.clone(); } private static final com.squareup.okhttp.MediaType CONTENT_TYPE_FORM = com.squareup.okhttp.MediaType.parse("application/x-www-form-urlencoded"); private String exec(com.squareup.okhttp.Request request) { try { com.squareup.okhttp.Response response = httpClient.newCall(request).execute(); if (!response.isSuccessful()) throw new RuntimeException("Unexpected code " + response); return response.body().string(); } catch (IOException e) { throw new RuntimeException(e); } } @Override public String get(String url) { com.squareup.okhttp.Request request = new com.squareup.okhttp.Request.Builder().url(url).get().build(); return exec(request); } @Override public String get(String url, Map<String, String> queryParas) { com.squareup.okhttp.HttpUrl.Builder urlBuilder = com.squareup.okhttp.HttpUrl.parse(url).newBuilder(); for (Entry<String, String> entry : queryParas.entrySet()) { urlBuilder.addQueryParameter(entry.getKey(), entry.getValue()); } com.squareup.okhttp.HttpUrl httpUrl = urlBuilder.build(); com.squareup.okhttp.Request request = new com.squareup.okhttp.Request.Builder().url(httpUrl).get().build(); return exec(request); } @Override public String post(String url, String params) { com.squareup.okhttp.RequestBody body = com.squareup.okhttp.RequestBody.create(CONTENT_TYPE_FORM, params); com.squareup.okhttp.Request request = new com.squareup.okhttp.Request.Builder() .url(url) .post(body) .build(); return exec(request); } @Override public String post(String url, Map<String, String> queryParas) { com.squareup.okhttp.FormEncodingBuilder builder = new com.squareup.okhttp.FormEncodingBuilder(); for (Entry<String, String> entry : queryParas.entrySet()) { builder.add(entry.getKey(), entry.getValue()); } com.squareup.okhttp.Request request = new com.squareup.okhttp.Request.Builder() .url(url) .post(builder.build()) .build(); return exec(request); } @Override public String postSSL(String url, String data, String certPath, String certPass) { com.squareup.okhttp.RequestBody body = com.squareup.okhttp.RequestBody.create(CONTENT_TYPE_FORM, data); com.squareup.okhttp.Request request = new com.squareup.okhttp.Request.Builder() .url(url) .post(body) .build(); InputStream inputStream = null; try { // 移动到最开始,certPath io异常unlock会报错 lock.lock(); KeyStore clientStore = KeyStore.getInstance("PKCS12"); inputStream = new FileInputStream(certPath); clientStore.load(inputStream, certPass.toCharArray()); KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(clientStore, certPass.toCharArray()); KeyManager[] kms = kmf.getKeyManagers(); SSLContext sslContext = SSLContext.getInstance("TLSv1"); sslContext.init(kms, null, new SecureRandom()); httpsClient.setSslSocketFactory(sslContext.getSocketFactory()); com.squareup.okhttp.Response response = httpsClient.newCall(request).execute(); if (!response.isSuccessful()) throw new RuntimeException("Unexpected code " + response); return response.body().string(); } catch (Exception e) { throw new RuntimeException(e); } finally { IOUtils.closeQuietly(inputStream); lock.unlock(); } } @Override public InputStream download(String url, String params) { com.squareup.okhttp.Request request; if (StrKit.notBlank(params)) { com.squareup.okhttp.RequestBody body = com.squareup.okhttp.RequestBody.create(CONTENT_TYPE_FORM, params); request = new com.squareup.okhttp.Request.Builder().url(url).post(body).build(); } else { request = new com.squareup.okhttp.Request.Builder().url(url).get().build(); } try { com.squareup.okhttp.Response response = httpClient.newCall(request).execute(); if (!response.isSuccessful()) throw new RuntimeException("Unexpected code " + response); return response.body().byteStream(); } catch (IOException e) { throw new RuntimeException(e); } } @Override public String upload(String url, File file, String params) { com.squareup.okhttp.RequestBody fileBody = com.squareup.okhttp.RequestBody .create(com.squareup.okhttp.MediaType.parse("application/octet-stream"), file); com.squareup.okhttp.MultipartBuilder builder = new com.squareup.okhttp.MultipartBuilder() .type(com.squareup.okhttp.MultipartBuilder.FORM) .addFormDataPart("media", file.getName(), fileBody); if (StrKit.notBlank(params)) { builder.addFormDataPart("description", params); } com.squareup.okhttp.RequestBody requestBody = builder.build(); com.squareup.okhttp.Request request = new com.squareup.okhttp.Request.Builder() .url(url) .post(requestBody) .build(); return exec(request); } } /** * OkHttp3代理 */ private static class OkHttp3Delegate implements HttpDelegate { private okhttp3.OkHttpClient httpClient; public OkHttp3Delegate() { // 分别设置Http的连接,写入,读取的超时时间 httpClient = new okhttp3.OkHttpClient().newBuilder() .connectTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build(); } private static final okhttp3.MediaType CONTENT_TYPE_FORM = okhttp3.MediaType.parse("application/x-www-form-urlencoded"); private String exec(okhttp3.Request request) { try { okhttp3.Response response = httpClient.newCall(request).execute(); if (!response.isSuccessful()) throw new RuntimeException("Unexpected code " + response); return response.body().string(); } catch (IOException e) { throw new RuntimeException(e); } } @Override public String get(String url) { okhttp3.Request request = new okhttp3.Request.Builder().url(url).get().build(); return exec(request); } @Override public String get(String url, Map<String, String> queryParas) { okhttp3.HttpUrl.Builder urlBuilder = okhttp3.HttpUrl.parse(url).newBuilder(); for (Entry<String, String> entry : queryParas.entrySet()) { urlBuilder.addQueryParameter(entry.getKey(), entry.getValue()); } okhttp3.HttpUrl httpUrl = urlBuilder.build(); okhttp3.Request request = new okhttp3.Request.Builder().url(httpUrl).get().build(); return exec(request); } @Override public String post(String url, String params) { okhttp3.RequestBody body = okhttp3.RequestBody.create(CONTENT_TYPE_FORM, params); okhttp3.Request request = new okhttp3.Request.Builder() .url(url) .post(body) .build(); return exec(request); } @Override public String postSSL(String url, String data, String certPath, String certPass) { okhttp3.RequestBody body = okhttp3.RequestBody.create(CONTENT_TYPE_FORM, data); okhttp3.Request request = new okhttp3.Request.Builder() .url(url) .post(body) .build(); InputStream inputStream = null; try { KeyStore clientStore = KeyStore.getInstance("PKCS12"); inputStream = new FileInputStream(certPath); char[] passArray = certPass.toCharArray(); clientStore.load(inputStream, passArray); KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(clientStore, passArray); KeyManager[] kms = kmf.getKeyManagers(); SSLContext sslContext = SSLContext.getInstance("TLSv1"); sslContext.init(kms, null, new SecureRandom()); @SuppressWarnings("deprecation") okhttp3.OkHttpClient httpsClient = new okhttp3.OkHttpClient() .newBuilder() .connectTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .sslSocketFactory(sslContext.getSocketFactory()) .build(); okhttp3.Response response = httpsClient.newCall(request).execute(); if (!response.isSuccessful()) throw new RuntimeException("Unexpected code " + response); return response.body().string(); } catch (Exception e) { throw new RuntimeException(e); } finally { IOUtils.closeQuietly(inputStream); } } @Override public InputStream download(String url, String params) { okhttp3.Request request; if (StrKit.notBlank(params)) { okhttp3.RequestBody body = okhttp3.RequestBody.create(CONTENT_TYPE_FORM, params); request = new okhttp3.Request.Builder().url(url).post(body).build(); } else { request = new okhttp3.Request.Builder().url(url).get().build(); } try { okhttp3.Response response = httpClient.newCall(request).execute(); if (!response.isSuccessful()) throw new RuntimeException("Unexpected code " + response); return response.body().byteStream(); } catch (IOException e) { throw new RuntimeException(e); } } @Override public String upload(String url, File file, String params) { okhttp3.RequestBody fileBody = okhttp3.RequestBody .create(okhttp3.MediaType.parse("application/octet-stream"), file); okhttp3.MultipartBody.Builder builder = new okhttp3.MultipartBody.Builder() .setType(okhttp3.MultipartBody.FORM) .addFormDataPart("media", file.getName(), fileBody); if (StrKit.notBlank(params)) { builder.addFormDataPart("description", params); } okhttp3.RequestBody requestBody = builder.build(); okhttp3.Request request = new okhttp3.Request.Builder() .url(url) .post(requestBody) .build(); return exec(request); } @Override public String post(String url, Map<String, String> params) { okhttp3.FormBody.Builder builder = new okhttp3.FormBody.Builder(); if(params == null){ params = new HashMap<String, String>(); } if(params != null){ Set<Map.Entry<String,String>> entries = params.entrySet(); for(Map.Entry<String,String> entry:entries){ builder.add(entry.getKey(),entry.getValue()); } } okhttp3.Request request = new okhttp3.Request.Builder().url(url).post(builder.build()).build(); return exec(request); } } }
支付时如何获取用户的openId?
通过微信网页授权获取用户信息接口来获取 openId。
-
微信网页授权官方文档( https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842 )
-
具体代码实现可以参考之前我写的 CSDN 博客( http://blog.csdn.net/zyw_java/article/details/61415123 )
如何实现一键导出所有用户信息?
公众号可通过获取用户列( https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140840 )表的接口来获取帐号的关注者列表,关注者列表由一串 OpenID(加密后的微信号,每个用户对每个公众号的 OpenID 是唯一的)组成。
一次拉取调用最多拉取10000个关注者的OpenID,可以通过多次拉取的方式来满足需求。
获取到OpenID之后可以使用获取用户基本信息(https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140839)接口来获取用户的详细信息。
具体代码实现可以参考之前我写的CSDN博客-微信公众号开发之如何一键导出微信所有用户信息到Excel(http://blog.csdn.net/zyw_java/article/details/61415146)。
如何实现关注微信公众号时发送随机红包
开启开发者模式在关注事件中异步发生红包即可。
微信公众号接收事件推送( https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140454 )文档。
发送随机红包具体代码实现可以参考之前我写的CSDN博客-微信开发之现金红包(http://blog.csdn.net/zyw_java/article/details/54024211)。
近期热文
《GitChat 达人课:Express & Reveal PPT 开发?》
「阅读原文」看交流实录,你想知道的都在这里