找回密码
 立即注册

QQ登录

只需一步,快速开始

参考文章

关注:2

所属分类: 微信开发 微信公众号开发资源 参考文章

微信公众号开发相关参考文章,不定期更新

微信 JS API 微信支付教程

[复制链接]
查看: 730|回复: 4
最佳答案
0 

11

主题

11

帖子

250

积分

新人求带

积分
250
 楼主| 发表于 2017-11-16 14:30:02 | 显示全部楼层 |阅读模式
最近一个项目中用到了微信开发,之前没有做过支付相关的东西,算是拿这个来练练手,刚开始接触支付时候很懵逼,加上微信支付开发文档本来就讲得不清楚,我是彻底蒙圈了,参考了很多代码之后,算是有一点思路了。

用户认证获取openId

如果你知识关注支付流程,这块可以跳过,因为我知道这些你已经做过了,在开始所有的流程之前,我觉得你应该把所有微信相关的配置放到一个properties文件中去,这样不仅显得更规范,而且会避免犯很多错误,真是一个完美的选择!
  1. ######## 配置文件
  2. ######## 公众号开发配置中的token(自定义)
  3. wechat.token=
  4. ######## 应用id
  5. wechat.appId=
  6. ######## 密钥(同token查看地址)
  7. wechat.appSecret=
  8. ######## 静默授权微信回调url
  9. wechat.callBackSlientUrl=
  10. ######## 商户Id(支付相关)
  11. wechat.MCHID=
  12. ######## 微信下单地址
  13. wechat.wxorder=https://api.mch.weixin.qq.com/pay/unifiedorder
  14. ######## 支付api密钥
  15. wechat.KEY=
  16. ######## 支付结果回调地址
  17. wechat.NOTIFYURL=
复制代码

接着你可以考虑把这个properties注入到一个bean中,使用更方便,当然你还可以选择使用java来读取properties的配置,对比这两个方法,我更喜欢第一个,我就使用第一种方法来演示一下(这里使用spring boot框架,spring mvc类似)
  1. /**
  2. * <p>Created on 2017/3/13.</p>
  3. *
  4. * [url=home.php?mod=space&uid=49329]@author[/url] StormMma
  5. *
  6. * @Description: 微信相关常量
  7. */
  8. @Component
  9. @ConfigurationProperties(locations = {"classpath:config/wechat.properties"}, prefix = "wechat")
  10. public class WeChatConfigBean {
  11.     /**
  12.      * token
  13.      */
  14.     private String token;
  15.     /**
  16.      * app id
  17.      */
  18.     private String appId;
  19.     /**
  20.      * app secret
  21.      */
  22.     private String appSecret;
  23.     /**
  24.      * 静默授权回调地址
  25.      */
  26.     private String callBackSlientUrl;
  27.     /**
  28.      * 商户id
  29.      */
  30.     private String MCHID;
  31.     /**
  32.      * 异步回调地址
  33.      */
  34.     private String NOTIFYURL;
  35.     /**
  36.      * 微信统一下单地址
  37.      */
  38.     private String wxorder;
  39.     /**
  40.      * key
  41.      */
  42.     private String KEY;
  43.     public String getToken() {
  44.         return token;
  45.     }
  46.     public void setToken(String token) {
  47.         this.token = token;
  48.     }
  49.     public String getAppId() {
  50.         return appId;
  51.     }
  52.     public void setAppId(String appId) {
  53.         this.appId = appId;
  54.     }
  55.     public String getAppSecret() {
  56.         return appSecret;
  57.     }
  58.     public void setAppSecret(String appSecret) {
  59.         this.appSecret = appSecret;
  60.     }
  61.     public String getCallBackSlientUrl() {
  62.         return callBackSlientUrl;
  63.     }
  64.     public void setCallBackSlientUrl(String callBackSlientUrl) {
  65.         this.callBackSlientUrl = callBackSlientUrl;
  66.     }
  67.     public String getMCHID() {
  68.         return MCHID;
  69.     }
  70.     public void setMCHID(String MCHID) {
  71.         this.MCHID = MCHID;
  72.     }
  73.     public String getNOTIFYURL() {
  74.         return NOTIFYURL;
  75.     }
  76.     public void setNOTIFYURL(String NOTIFYURL) {
  77.         this.NOTIFYURL = NOTIFYURL;
  78.     }
  79.     public String getWxorder() {
  80.         return wxorder;
  81.     }
  82.     public void setWxorder(String wxorder) {
  83.         this.wxorder = wxorder;
  84.     }
  85.     public String getKEY() {
  86.         return KEY;
  87.     }
  88.     public void setKEY(String KEY) {
  89.         this.KEY = KEY;
  90.     }
复制代码

封装请求工具(这次我选择使用HttpClient, 此处的json工具我选择了ali的fastjson)

RequestUtil.java
  1. /**
  2.     * 发送Get请求到url,获得response的json实体
  3.     * @param url
  4.     * @return
  5.     * @throws IOException
  6.     */
  7.    private JSONObject doGetUrl(String url) throws WechatException, ServerSystemException {
  8.        CloseableHttpClient httpclient = HttpClients.createDefault();
  9.        HttpGet httpGet = new HttpGet(url);
  10.        CloseableHttpResponse response;
  11.        String result;
  12.        try {
  13.            response = httpclient.execute(httpGet);
  14.            HttpEntity entity = response.getEntity();
  15.            result = EntityUtils.toString(entity, "UTF-8");
  16.            httpclient.close();
  17.        } catch (IOException e) {
  18.            logger.error("执行GET请求发生错误!", e);
  19.            throw new ServerSystemException("执行GET请求发生错误!{}", e);
  20.        }
  21.        return JSONObject.parseObject(result);
  22.    }
  23.    /**
  24.     * 发送post请求
  25.     * @param url
  26.     * @param param
  27.     * @return
  28.     * @throws ServerSystemException
  29.     */
  30.    private JSONObject doPostUrl(String url, String param) throws ServerSystemException {
  31.        final String CONTENT_TYPE_TEXT_JSON = "application/json";
  32.        DefaultHttpClient httpClient = new DefaultHttpClient(new PoolingClientConnectionManager());
  33.        HttpPost httpPost = new HttpPost(url);
  34.        HttpResponse response;
  35.        String result;
  36.        try {
  37.            StringEntity stringEntity = new StringEntity(param);
  38.            stringEntity.setContentType(CONTENT_TYPE_TEXT_JSON);
  39.            stringEntity.setContentEncoding("UTF-8");
  40.            httpPost.setEntity(stringEntity);
  41.            response = httpClient.execute(httpPost);
  42.            HttpEntity entity = response.getEntity();
  43.            result = EntityUtils.toString(entity, "UTF-8");
  44.            httpClient.close();
  45.        } catch (IOException e) {
  46.            logger.error("执行POST请求发生错误!", e);
  47.            throw new ServerSystemException("执行POST请求发生错误!{}", e);
  48.        }
  49.        return JSONObject.parseObject(result);
  50.    }
复制代码

获取code

在此之前,我想我们应该抽出一个微信工具类,专门来封装各种请求和RequestUtil来结合使用,是的,这是一个很好的选择。
WxRequestUtil.java
  1. public calss WxRequestUtil {
  2.     @AutoWired
  3.     private WechatConfigBean config;
  4.     /**
  5.      * <p>获得静默授权的url</p>
  6.      * @return
  7.      */
  8.     public String getSlientUrl() {
  9.         String url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=" + config.getAppId() +
  10.                 "&redirect_uri=" + URLEncoder.encode(config.getCallBackSlientUrl()) +
  11.                 "&response_type=code" +
  12.                 "&scope=snsapi_base" +
  13.                 "&state=STATE#wechat_redirect";
  14.         return url;
  15.     }
  16. }
复制代码

接着我想我们应该参照开发文档来重定向到这个url,然后微信服务器会检查参数接着重定向到我们的回调地址,嗯嗯,你猜对了,就是参数带的那个redirect_uri,那么我们应该补充一下回调接口

获取openId

WechatController.java
  1. /**
  2.     * 获得openId,静默授权
  3.     * @param code
  4.     * @param session
  5.     * @param response
  6.     */
  7.    @RequestMapping(value = "/slient/check")
  8.    public RequestResult<String> callBackBase(@RequestParam(value = "code", required = false) String code, HttpServletResponse response) {
  9.        String openId = wechatService.getOpenIdBySlientAuthy(
  10.        return ResultUtil.success(openId);
  11.    }
复制代码

我想我应该解释一下,控制器层我用的都是规范化的请求响应,不知道的可以参考我前面的博文。另外一点我需要说明的就是我们还需要一个service来处理获取openId的逻辑。

WechatService.java
  1. /**
  2.     * 静默授权获得openId
  3.     * @param code
  4.     * @return
  5.     */
  6.    public String getOpenIdBySlientAuthy(String code) {
  7.        String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=" + config.getAppId() +
  8.                "&secret=" + config.getAppSecret() +
  9.                "&code=" + code +
  10.                "&grant_type=authorization_code";
  11.     //为了代码简便,此处省略异常处理
  12.        JSONObject jsonObject = doGetUrl(url);
  13.        return jsonObject.getString("openid");
  14.    }
复制代码

至此,我们获得了openId,那么接着我们回到支付的话题上

微信支付

首先,我需要说明的是,微信支付的一个流程,至于为什么呢,我的目的很明确就是要描述清楚微信支付。我做支付的时候看过很多资料,有一个很深的体会就是代码复制来复制去,一大片一大片的代码看着心碎。在这里,我就不贴微信官方的流程图了,我相信你看着流程图会吓一跳,所以我选择不残害你。回到正题,微信支付最重要的就是三个步骤。

统一下单,得到预支付id, 次数需要你提供商户的信息以及商品的信息,然后得到一个预支付id(请相信我,其他返回的数据并没有什么实际的意义)
组装调起支付参数(我不知道叫什么名字更贴切,索性就这么叫吧,这个步骤其实就是使用预支付id,和其他的配置信息签名生成请求数据,返回至前台调用)
调起支付(使用jssdk或者h5接口调起支付)
其他的步骤就不是那么重要了,比如支付接口通知接口,可以根据自己的需求进行改写,这里我就不多说了。

统一下单

PayService.java
  1.     /**
  2.      * 获得统一下单参数
  3.      * @param openId
  4.      * @param totalFee
  5.      * @param ip
  6.      * @param body
  7.      * @return
  8.      */
  9. public String getPayParam(String openId, String totalFee, String ip, String body) {
  10.         Map<String, String> datas = new TreeMap<>();
  11.         datas.put("appid", weChatConfigBean.getAppId());
  12.         datas.put("mch_id", weChatConfigBean.getMCHID());
  13.         //设备
  14.         datas.put("device_info", "WEB");
  15.         //商品描述
  16.         datas.put("body", body);
  17.         //支付类型,这里使用公众号支付,所以是JSAPI
  18.         datas.put("trade_type", "JSAPI");
  19.         //随机字符串,32字符以内
  20.         datas.put("nonce_str", WXUtil.getNonceStr());
  21.         //支付结果通知地址
  22.         datas.put("notify_url", config.getNOTIFYURL());
  23.         //订单号,自己生成一个唯一的订单号就行
  24.         datas.put("out_trade_no", createOutTradeNO());
  25.         //支付金额,以分为单位
  26.         datas.put("total_fee", totalFee);
  27.         //用户openId
  28.         datas.put("openid", openId);
  29.         //ip
  30.         datas.put("spbill_create_ip", ip);
  31.         String sign = SignatureUtils.signature(datas, config.getKEY());
  32.         datas.put("sign", sign);
复制代码

看到这里,你可能有点懵逼,我想我需要解释一下,开始之前我们用Map把所有的参数封装起来,至于为什么用TreeMapp,因为我们后面的签名要将Map的参数转换成一个字符串的形式(字段名=字段值&字段名=字段值)并且字段名字典序排序,这样,我们就只需要关注签名算法的实现,官方文档有解释签名算法,就像我前面说的,我们需要把Map转换成字符串的形式,并且后面要追加一个&key=#{key}(注意:#{key}是你的字段值)的参数,然后进行加密。我想此处我应该给出我的签名:

SignatureUtils.java
  1. /**
  2. * 微信支付加密工具
  3. * @param key
  4. * @param map
  5. */
  6. public static String signature(Map<String, String> map, String key) {
  7.     Set<String> keySet = map.keySet();
  8.     String[] str = new String<script>jQuery(function($) {$("#google-maps-1").gMap({controls: false,scrollwheel: false,markers: [{address: "",icon: {image: "http://www.importnew.com/wp-content/themes/jobbolev4blog/_assets/img/_colors/red/pin.png",iconsize: [32, 32],iconanchor: [16,27],infowindowanchor: [16, 27]}}],address: "",zoom: 15,icon: {image: "http://www.importnew.com/wp-content/themes/jobbolev4blog/_assets/img/_colors/red/pin.png",iconsize: [32, 32],iconanchor: [16,27],infowindowanchor: [16, 27]}});});</script><div id="google-maps-1" class="google-maps" style="width: 100%; height: 200px;"></div>;
  9.     StringBuilder tmp = new StringBuilder();
  10.     str = keySet.toArray(str);
  11.     for (int i = 0; i < str.length; i++) {
  12.         String t = str[i] + "=" + map.get(str[i]) + "&";
  13.         tmp.append(t);
  14.     }
  15.     if (StringUtils.isNotBlank(key)) {
  16.         tmp.append("key=" + key);
  17.     }
  18.     String tosend = tmp.toString();
  19.     MessageDigest md = null;
  20.     byte[] bytes = null;
  21.     try {
  22.         md = MessageDigest.getInstance("MD5");
  23.         bytes = md.digest(tosend.getBytes("utf-8"));
  24.     } catch (Exception e) {
  25.         e.printStackTrace();
  26.     }
  27.     String singe = byteToStr(bytes);
  28.     return singe.toUpperCase();
  29. }
  30. /**
  31. * 字节数组转换为字符串
  32. * @param byteArray
  33. * @return
  34. */
  35. public static String byteToStr(byte[] byteArray) {
  36.     String strDigest = "";
  37.     for (int i = 0; i < byteArray.length; i++) {
  38.         strDigest += byteToHexStr(byteArray[i]);
  39.     }
  40.     return strDigest;
  41. }
  42. /**
  43. * 字节转换为字符串
  44. * @param mByte
  45. * @return
  46. */
  47. public static String byteToHexStr(byte mByte) {
  48.     char[] Digit = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A',
  49.             'B', 'C', 'D', 'E', 'F' };
  50.     char[] tempArr = new char[2];
  51.     tempArr[0] = Digit[(mByte >>> 4) & 0X0F];
  52.     tempArr[1] = Digit[mByte & 0X0F];
  53.     String s = new String(tempArr);
  54.     return s;
  55. }
复制代码

这个工具类,具体我就不多介绍了,可以查看一下官方文档,了解一下签名算法,然后回来看代码,我相信你可以看懂。

我想我应该说一声对不起,我忘了解释其实我们最终下单的参数是一个xml的String类型,所以我们还要把Map转换成xml,这个就很简单了。我们可以考虑把它加到PayService里面(其他地方用不着,你可以考虑私有,相信我,这样会更优雅)。
  1. /**
  2.     * 得到统一下单参数的xml形式
  3.     *
  4.     * @param parameters
  5.     * @return
  6.     */
  7.    public static String getRequestXml(Map<String, String> parameters) {
  8.        StringBuffer sb = new StringBuffer();
  9.        sb.append("<xml>");
  10.        Set es = parameters.entrySet();
  11.        Iterator it = es.iterator();
  12.        while (it.hasNext()) {
  13.            Map.Entry entry = (Map.Entry) it.next();
  14.            String k = (String) entry.getKey();
  15.            String v = (String) entry.getValue();
  16.            if ("attach".equalsIgnoreCase(k) || "body".equalsIgnoreCase(k) || "sign".equalsIgnoreCase(k)) {
  17.                sb.append("<" + k + ">" + "<![CDATA[" + v + "]]></" + k + ">");
  18.            } else {
  19.                sb.append("<" + k + ">" + v + "</" + k + ">");
  20.            }
  21.        }
  22.        sb.append("</xml>");
  23.        return sb.toString();
  24.    }
复制代码

于是,我们得到了统一下单的参数,接下来就是去请求微信服务器了。
  1. /**
  2.      * 支付接口
  3.      * @param body
  4.      * @param totalFee
  5.      * @param user
  6.      * @param response
  7.      * @return
  8.      * @throws Exception
  9.      */
  10.     @PostMapping(value = "/pay")
  11.     public RequestResult<Map<String, String>> order(@RequestParam("body")String body,
  12.                                                     @RequestParam("totalFee")String totalFee,
  13.                                                     @SessionAttribute(name = "user", required = false)User user,
  14.                                                     HttpServletResponse response) throws Exception {
  15.         //之前我们获得了openId,这里我使用假数据测试
  16.         String openId = "oxxjlv1dWSkielTGFfWQGNK-RHSc";
  17.         String ip = this.getIpAddress();
  18.         String requestParam = payService.getPayParam(openId, totalFee, ip, body);
  19.         //stop here ,下面我会讲
  20.         Map<String, String> result = payService.requestWechatPayServer(requestParam);
  21.         Map<String, String> datas = new TreeMap<>();
  22.         if (result.get("return_code").equals("SUCCESS")) {
  23.             String prepayId = result.get("prepay_id");
  24.             datas.put("appId", weChatConfigBean.getAppId());
  25.             datas.put("package", "prepay_id=" + prepayId);
  26.             datas.put("signType", "MD5");
  27.             datas.put("timeStamp", Long.toString(new Date().getTime()).substring(0, 10));
  28.             datas.put("nonceStr", WXUtil.getNonceStr());
  29.             String sign = SignatureUtils.signature(datas, weChatConfigBean.getKEY());
  30.             datas.put("paySign", sign);
  31.             return ResultUtil.success(datas);
  32.         }
  33.         return ResultUtil.fail();
  34.     }
复制代码

组装调起支付参数

继续上面的控制器,我们已经得到了预支付id,那么我们离成功不远了 请相信我,我没有骗你。然后我们要封装调起支付参数,我们先看一下jssdk调起支付需要的参数。
  1. wx.chooseWXPay({
  2.     timestamp: 0, // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
  3.     nonceStr: '', // 支付签名随机串,不长于 32 位
  4.     package: '', // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=***)
  5.     signType: '', // 签名方式,默认为'SHA1',使用新版支付需传入'MD5'
  6.     paySign: '', // 支付签名
  7.     success: function (res) {
  8.         // 支付成功后的回调函数
  9.     }
  10. });
复制代码

那么我们就根据这个参数列表来生成参数,不过我很好奇,为什么timeStamp一阵大写一阵小写的,我想估计脑子抽了吧。现在我们看看上面的控制器剩余的代码,其实就是组装这个参数到Map,我想这个应该没有疑惑的地方吧。说到这,微信开发基本结束了,剩下的就是js调起支付,输入密码,微信服务器判断,给你返回结果的过程,处理结果的接口我就不贴了,简单到不行。

结尾

在做微信支付的时候,我有时候真的很无奈,没有好的官方文档,更没有好的博文,这篇博客旨在能讲清楚微信支付的步骤,我知道在这么短的时间讲清楚显然不可能,希望各位多多指正,有问题的可以发邮件给我,StormMaybin@gmail.com。哦对了,最后别忘记配置支付目录,不然会显示url未注册。应部分人的要求,最后写了一个demo,附上链接:https://github.com/StormMaybin/wxpay.git
回复

使用道具 举报

最佳答案
0 

0

主题

10

帖子

108

积分

新人求带

积分
108
发表于 2017-11-28 09:46:24 | 显示全部楼层
感谢楼主的帖子!
回复 支持 反对

使用道具 举报

最佳答案
0 

0

主题

54

帖子

736

积分

专家路上

积分
736
发表于 2017-11-28 12:56:53 | 显示全部楼层
感谢楼主
回复

使用道具 举报

最佳答案
0 

0

主题

77

帖子

863

积分

专家路上

积分
863
发表于 2017-11-29 00:59:29 | 显示全部楼层
谢谢楼主,楼主辛苦了......
回复 支持 反对

使用道具 举报

最佳答案
0 

0

主题

8

帖子

44

积分

新人求带

积分
44
发表于 3 天前 | 显示全部楼层
2785295756 发表于 2017-11-28 09:46
感谢楼主的帖子!

啊//hm.baidu.com/hm.js?cbf20f554ba43aeba396a009eb4ab5f7
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



henkuai.com是专业的第三方微信开发者平台,为生态而生。


本站为第三方微信开发者平台,非腾讯官方网站。

天津市滨海新区
中新生态城中成大道生态建设公寓9号楼3层301

欢迎来这里一起喝喝茶,
聊聊你的产品。

微信公众号gongzhongkaifa

工作日12小时内回复。

广告推广
zhongcong@henkuai.com

工作日12小时内回复。

市场合作
songchang@henkuai.com