微信各种支付(完整开发流程)


最近做了几个关于微信支付的项目,现在刚好有点时间,来总结一下容易踩的坑。

首先微信支付主要有这几种方式(微信公众号支付、微信H5支付、微信扫码支付、微信APP支付)

所有支付的第一步都是请求统一下单,统一下单,统一下单,请求URL地址:https://api.mch.weixin.qq.com/pay/unifiedorder。统一下单的目的是拿到预支付交易会话标识prepay_id,这个是必须的。所有的支付调用都是通过prepay_id来识别。

还有一个重要的就是,微信支付接口签名校验工具(网址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=20_1),此工具旨在帮助开发者检测调用【微信支付接口API】时发送的请求参数中生成的签名是否正确,提交相关信息后可获得签名校验结果。签名正确基本就没什么问题了。

一、微信资料准备

1、注册一个微信公众号,并年审(年审需要交300费用,然后等待微信那边审核通过),这个跟着微信步骤弄就行了

微信公众平台(https://mp.weixin.qq.com/

2、注册微信商户号,成为微信商家

微信支付(https://pay.weixin.qq.com/

3、把公众号与商户号绑定

4、需要用到的信息位置

①、公众号(AppID)

②、商户号(MCH_ID,API_KEY)

③、商户授权目录、扫码回调地址

④、商户号关联的APPID

⑤、商户号支付功能开通

二、微信公众号支付

微信公众号支付需要的参数有:APP_ID(微信公众号开发者ID)、APP_SECRET(微信公众号开发者密码)、MCH_ID(商户ID)、API_KEY(商户密钥)。

微信公众号支付应用的场景是在微信内部的H5环境中是用的支付方式。因为要通过网页授权获取用户的openId,所以必须要配置网页授权域名。同时要配置JS接口安全域名。

1、工具类

 1 /**
 2  *    公众号支付配置信息
 3  */
 4 public class PayConfigUtil {
 5     public static String APP_ID="公众号APPID";
 6     public static String MCH_ID="商户号";
 7     public static String API_KEY="API密钥";
 8     public static String CREATE_IP="127.0.0.1";
 9     public static String NOTIFY_URL="微信回调地址";//公众号支付回调-与商户里配置的支付回调无关
10       //微信统一下单地址
11     public static String UFDODER_URL="https://api.mch.weixin.qq.com/pay/unifiedorder";
12 }
  1 /**
  2  *    微信支付常用方法
  3  */
  4 public class PayCommonUtil {
  5        
  6      /** 
  7        * 是否签名正确,规则是:按参数名称a-z排序,遇到空值的参数不参加签名。 
  8        * @return boolean 
  9        */  
 10      public static boolean isTenpaySign(String characterEncoding, SortedMap<Object, Object> packageParams, String API_KEY) {  
 11          StringBuffer sb = new StringBuffer();  
 12          Set es = packageParams.entrySet();  
 13          Iterator it = es.iterator();  
 14          while(it.hasNext()) {  
 15            Map.Entry entry = (Map.Entry)it.next();  
 16            String k = (String)entry.getKey();  
 17            String v = (String)entry.getValue();  
 18            if(!"sign".equals(k) && null != v && !"".equals(v)) {  
 19                sb.append(k + "=" + v + "&");  
 20            }  
 21          }  
 22        
 23          sb.append("key=" + API_KEY);  
 24        
 25        //算出摘要  
 26        String mysign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toLowerCase();  
 27        String tenpaySign = ((String)packageParams.get("sign")).toLowerCase();  
 28 
 29        //System.out.println(tenpaySign + "    " + mysign);  
 30        return tenpaySign.equals(mysign);  
 31      }  
 32 
 33      /**  
 34       * @Description:sign签名 
 35       * @param characterEncoding 编码格式
 36       * @param packageParams  请求参数
 37       * @param API_KEY
 38       * @return 
 39       */  
 40      public static String createSign(String characterEncoding, SortedMap<Object, Object> packageParams, String API_KEY) {  
 41        StringBuffer sb = new StringBuffer();  
 42        Set es = packageParams.entrySet();  
 43        Iterator it = es.iterator();  
 44        while (it.hasNext()) {  
 45            Map.Entry entry = (Map.Entry) it.next();  
 46            String k = (String) entry.getKey();  
 47            String v = (String) entry.getValue();  
 48            if (null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)) {  
 49                sb.append(k + "=" + v + "&");  
 50            }  
 51        }  
 52        sb.append("key=" + API_KEY);  
 53        String sign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toUpperCase();  
 54        return sign;  
 55      }  
 56 
 57    /** 
 58     * @Description:将请求参数转换为xml格式的string 
 59     * @param parameters 请求参数 
 60     * @return 
 61     */  
 62    public static String getRequestXml(SortedMap<Object, Object> parameters) {  
 63        StringBuffer sb = new StringBuffer();  
 64        sb.append("<xml>");  
 65        Set es = parameters.entrySet();  
 66        Iterator it = es.iterator();  
 67        while (it.hasNext()) {  
 68            Map.Entry entry = (Map.Entry) it.next();  
 69            String k = (String) entry.getKey();  
 70            String v = (String) entry.getValue();  
 71            if ("attach".equalsIgnoreCase(k) || "body".equalsIgnoreCase(k) || "sign".equalsIgnoreCase(k)) {  
 72                sb.append("<" + k + ">" + "<![CDATA[" + v + "]]></" + k + ">");  
 73            } else {  
 74                sb.append("<" + k + ">" + v + "</" + k + ">");  
 75            }  
 76        }  
 77        sb.append("</xml>");  
 78        return sb.toString();  
 79    }  
 80 
 81    /** 
 82     * 取出一个指定长度大小的随机正整数
 83     * @param length int 设定所取出随机数的长度。length小于11 
 84     * @return int 返回生成的随机数。 
 85     */  
 86    public static int buildRandom(int length) {  
 87        int num = 1;  
 88        double random = Math.random();  
 89        if (random < 0.1) {  
 90            random = random + 0.1;  
 91        }  
 92        for (int i = 0; i < length; i++) {  
 93            num = num * 10;  
 94        }  
 95        return (int) ((random * num));  
 96    }  
 97 
 98    /** 
 99     * 获取当前时间 yyyyMMddHHmmss 
100     * @return String 
101     */  
102    public static String getCurrTime() {  
103        Date now = new Date();  
104        SimpleDateFormat outFormat = new SimpleDateFormat("yyyyMMddHHmmss");  
105        String s = outFormat.format(now);  
106        return s;  
107    }  
108 }
 1 /**
 2  *    发送支付请求
 3  */
 4 public class HttpUtil {
 5     private final static int CONNECT_TIMEOUT = 5000; // in milliseconds  
 6     private final static String DEFAULT_ENCODING = "UTF-8";  
 7       
 8     public static String postData(String urlStr, String data){  
 9         return postData(urlStr, data, null);  
10     }  
11       
12     public static String postData(String urlStr, String data, String contentType){  
13         BufferedReader reader = null;  
14         try {  
15             URL url = new URL(urlStr);  
16             URLConnection conn = url.openConnection();  
17             conn.setDoOutput(true);  
18             conn.setConnectTimeout(CONNECT_TIMEOUT);  
19             conn.setReadTimeout(CONNECT_TIMEOUT);  
20             if(contentType != null)  
21                 conn.setRequestProperty("content-type", contentType);  
22             OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream(), DEFAULT_ENCODING);  
23             if(data == null)  
24                 data = "";  
25             writer.write(data);   
26             writer.flush();  
27             writer.close();    
28   
29             reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), DEFAULT_ENCODING));  
30             StringBuilder sb = new StringBuilder();  
31             String line = null;  
32             while ((line = reader.readLine()) != null) {  
33                 sb.append(line);  
34                 sb.append("\r\n");  
35             }  
36             return sb.toString();  
37         } catch (IOException e) {  
38          System.out.println("Error connecting to " + urlStr + ": " + e.getMessage());  
39         } finally {  
40             try {  
41                 if (reader != null)  
42                     reader.close();  
43             } catch (IOException e) {  
44             }  
45         }  
46         return null;  
47     }
48 }
 1 /**
 2  *    微信返回结果解析
 3  */
 4 public class XMLUtil {
 5     /**
 6      * 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。
 7      * 
 8      * @param strxml
 9      * @return
10      * @throws JDOMException
11      * @throws IOException
12      */
13     public static Map<String, String> doXMLParse(String strxml) throws JDOMException, IOException {
14         strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");
15 
16         if (null == strxml || "".equals(strxml)) {
17             return null;
18         }
19 
20         Map<String, String> m = new HashMap<>();
21 
22         InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8"));
23         SAXBuilder builder = new SAXBuilder();
24         Document doc = builder.build(in);
25         Element root = doc.getRootElement();
26         List list = root.getChildren();
27         Iterator it = list.iterator();
28         while (it.hasNext()) {
29             Element e = (Element) it.next();
30             String k = e.getName();
31             String v = "";
32             List children = e.getChildren();
33             if (children.isEmpty()) {
34                 v = e.getTextNormalize();
35             } else {
36                 v = XMLUtil.getChildrenText(children);
37             }
38             m.put(k, v);
39         }
40         // 关闭流
41         in.close();
42         return m;
43     }
44 
45     /**
46      * 获取子结点的xml
47      * @param children
48      * @return String
49      */
50     public static String getChildrenText(List children) {
51         StringBuffer sb = new StringBuffer();
52         if (!children.isEmpty()) {
53             Iterator it = children.iterator();
54             while (it.hasNext()) {
55                 Element e = (Element) it.next();
56                 String name = e.getName();
57                 String value = e.getTextNormalize();
58                 List list = e.getChildren();
59                 sb.append("<" + name + ">");
60                 if (!list.isEmpty()) {
61                     sb.append(XMLUtil.getChildrenText(list));
62                 }
63                 sb.append(value);
64                 sb.append("</" + name + ">");
65             }
66         }
67         return sb.toString();
68     }
69 }

2、微信支付

①、页面请求

 1 $.ajax({
 2   url:"weChatPay",
 3   type:"post",
 4   data: {
 5     "用于生成订单的商品信息"
 6   },
 7   success:function(res){
 8     var data = res.data;
 9     if(data.msg != null && data.msg != ""){
10       $.toast(data.msg, "cancel");
11     }else {
12       WeixinJSBridge.invoke(
13         'getBrandWCPayRequest', {
14           "appId":data.appId,     //公众号名称,由商户传入
15           "timeStamp":data.timeStamp,         //时间戳,自1970年以来的秒数
16           "nonceStr":data.nonceStr, //随机串
17           "package":data.package,
18           "signType":"MD5",         //微信签名方式:
19           "paySign":data.paySign //微信签名
20         },
21         function(re){
22           if(re.err_msg == "get_brand_wcpay_request:fail" )
23           {
24             // 使用以上方式判断前端返回,微信团队郑重提示:
25             //res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。
26             $.toast("支付失败,请稍后再试!", "cancel");
27           }
28           else if(re.err_msg == "get_brand_wcpay_request:cancel")
29           {
30             $.toast("已取消支付!", "cancel");
31           }
32           else if(re.err_msg == "get_brand_wcpay_request:ok")
33           {
34             $.toast("支付成功,请手动刷新订单查看!");
35           }
36         });
37     }
38   },
39   error:function(e){
40     $.toast("系统错误,支付失败!", "cancel");
41   }
42 });

②、controller层

 1 /**
 2  * 微信公众号支付
 3  * @param session
 4  * @return
 5  */
 6 @PostMapping("weChatPay")
 7 public Response weChatPay(Object 订单需要信息,HttpSession session){
 8   //获取微信用户的openId---这个是用户访问页面的时候已经从微信获取保存在session里面了,可以通过微信授权的时候取到
 9   String openId = session.getAttribute("openId").toString();
10   //生成待支付订单
11   String orderCode = "订单唯一值";
12     。。。此处省略了插入数据库模块
13   Map<String,Object> map = this.weChatService.weChatPay(openId,orderCode);
14   return new Response().success().data(map);
15 }

③、service层

 1 public Map<String, Object> weChatPay(String openId,String orderCode) {
 2   Map<String,Object> wechatmap = new HashMap<>();
 3   String msg= "";
 4   try{
 5     //通过订单取到支付金额即商品名称
 6     。。。通过订单编号取订单信息
 7     //订单金额
 8     BigDecimal ordersAmount = null;
 9     //商品名称
10     String body = "";
11     //微信支付金额单位统一转换成分
12     BigDecimal fen = ordersAmount.multiply(new BigDecimal(100));
13     fen = fen.setScale(0, BigDecimal.ROUND_HALF_UP);
14     String amount = fen.toString();
15 
16     //微信支付配置
17     String appid = PayConfigUtil.APP_ID; // appid
18     String mch_id = PayConfigUtil.MCH_ID; // 商业号
19     String key = PayConfigUtil.API_KEY; // key
20 
21     String currTime = PayCommonUtil.getCurrTime();
22     String strTime = currTime.substring(8, currTime.length());
23     String strRandom = PayCommonUtil.buildRandom(4) + "";
24     String nonce_str = strTime + strRandom;
25 
26     String spbill_create_ip = PayConfigUtil.CREATE_IP;// 获取发起电脑 ip
27     String notify_url = PayConfigUtil.NOTIFY_URL;// 回调接口
28     String trade_type = "JSAPI";//公众号支付方式
29 
30     SortedMap<Object, Object> packageParams = new TreeMap<Object, Object>();
31     packageParams.put("appid", appid);
32     packageParams.put("openid",openId);//用户标识
33     packageParams.put("mch_id", mch_id);
34     packageParams.put("nonce_str", nonce_str);
35     packageParams.put("body", body);
36     packageParams.put("out_trade_no", orderCode);
37     packageParams.put("total_fee", order_price);
38     packageParams.put("spbill_create_ip", spbill_create_ip);
39     packageParams.put("notify_url", notify_url);
40     packageParams.put("trade_type", trade_type);
41     //附加类型-这里可以附加自己需要的特殊属性
42     packageParams.put("attach", "公众号一支付");
43 
44     //计算签名
45     String sign = PayCommonUtil.createSign("UTF-8", packageParams, key);
46     packageParams.put("sign", sign);
47     //把支付参数转换xml格式
48     String requestXML = PayCommonUtil.getRequestXml(packageParams);
49     //发起微信支付请求
50     String resXml = HttpUtil.postData(PayConfigUtil.UFDODER_URL, requestXML);
51   
52     //解析返回结果
53     Map<String, String> map = XMLUtil.doXMLParse(resXml);
54 
55     if(map.get("return_code").equals("FAIL")){
56       msg = "下单错误";
57       wechatmap.put("msg",msg);
58       wechatmap.put("error_code", map.get("return_msg"));
59       return wechatmap;
60     }else if(map.get("return_code").equals("SUCCESS")){
61       if(map.get("result_code").equals("FAIL")
62          || (map.get("err_code") != null && !map.get("err_code").equals(""))
63          || (map.get("err_code_des") != null && !map.get("err_code_des").equals(""))){
64         msg = "其他的错误参数值";
65         wechatmap.put("msg",msg);
66         wechatmap.put("error_code", map.get("err_code_des"));
67         return wechatmap;
68       }
69     }
70     
71     //预支付交易会话标识---这个值很关键
72     String prepay_id = (String) map.get("prepay_id");
73 
74     SortedMap<Object, Object> mapp = new TreeMap<Object, Object>();
75 
76     currTime = PayCommonUtil.getCurrTime();
77     strTime = currTime.substring(8, currTime.length());
78     strRandom = PayCommonUtil.buildRandom(4) + "";
79     nonce_str = strTime + strRandom;
80 
81     mapp.put("appId", appid);
82     mapp.put("timeStamp", System.currentTimeMillis());//重新计算时间戳
83     mapp.put("nonceStr", nonce_str);//重新计算随机字符串,长度要求在32位以内。
84 
85     if(!prepay_id.equals("")) {
86       mapp.put("package", "prepay_id="+prepay_id);
87     }
88 
89     mapp.put("signType", "MD5");
90     wechatmap = createSign("UTF-8", mapp, key);
91   }catch (IOException e){
92     msg = "系统异常,暂时无法使用微信支付,请联系客服!!";
93   }
94   wechatmap.put("msg",msg);
95   return wechatmap;
96 }

④、util(公众号用户授权)

 1 /**
 2  * 微信授权重定向后进入的方法获取用户在本公众号的 唯一标示
 3  * 能否授权成功并取到用户的openId是公众号各类操作成功的前提
 4  */
 5 public static String[] obtainUnionid(HttpServletRequest request)
 6 {
 7     String CODE = request.getParameter("code");
 8 
 9     String[] openid = {"",""};
10 
11     if(CODE == null || CODE.equals(""))
12     {
13         openid[0] = "未授权!";
14     }
15     else if(CODE.equals("10009"))//操作频繁
16     {
17         openid[0] = "操作频繁!";
18     }
19     else if(CODE.equals("10004"))//此公众号被封禁
20     {
21         openid[0] = "此公众号被封禁!";
22     }
23     else if(CODE.equals("10006"))//必须关注此测试号
24     {
25         openid[0] = "必须先关注!";
26     }
27     else if(CODE.equals("10015"))//公众号未授权第三方平台,请检查授权状态
28     {
29         openid[0] = "此公众不支持!";
30     }
31     else if (CODE.equals("10003") || CODE.equals("10005") || CODE.equals("10007") || CODE.equals("10008")
32             || CODE.equals("10010") || CODE.equals("10011") || CODE.equals("10012") || CODE.equals("10013")
33             || CODE.equals("10014") || CODE.equals("10016"))
34     {
35         openid[0] = "系统错误,请稍后再试!";
36     }
37     else
38     {
39         String APPID = WeChatConfig.getInstance().getAppId();
40         String SECRET = WeChatConfig.getInstance().getAppSecret();
41 
42         String URL = WeChatConsts.URL_OAUTH2_GET_ACCESSTOKEN.replace("APPID", APPID).replace("SECRET", SECRET).replace("CODE", CODE);
43 
44         JSONObject jsonStr = WeChatRealization.httpRequest(URL,"GET",null);
45 
46         if(jsonStr.get("errcode") != null && !jsonStr.get("errcode").equals(""))
47         {
48             //失败
49             openid[0] = "此公众不支持!!!";
50         }
51         else
52         {
53             openid[1] = jsonStr.get("openid").toString();//只有在用户将公众号绑定到微信开放平台帐号后,才会出现该字段。
54         }
55     }
56     return openid;
57 }

3、微信回调

①、controller层

1 /**
2  * 微信支付回调
3  * @param request
4  * @param response
5  */
6 @RequestMapping(value = "payBackWx", method = {RequestMethod.GET,RequestMethod.POST})
7 public void payBackWx(HttpServletRequest request, HttpServletResponse response){
8   weChatService.payBackWx(request,response);
9 }

②、service层

 1 public void payBackWx(HttpServletRequest request, HttpServletResponse response) {
 2       response.setContentType("text/xml");
 3       InputStream inputStream;// 读取参数
 4       StringBuffer sb = new StringBuffer();
 5       try{
 6           inputStream = request.getInputStream();
 7           String s;
 8          BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
 9         while ((s = in.readLine()) != null){
10             sb.append(s);
11         }
12           in.close();
13         inputStream.close();
14         // 解析xml成map
15         Map<String, String> m = new HashMap<String, String>();
16         m = XMLUtil.doXMLParse(sb.toString());
17 
18         // 过滤空 设置 TreeMap
19         SortedMap<Object, Object> packageParams = new TreeMap<Object, Object>();
20         Iterator it = m.keySet().iterator();
21         while (it.hasNext()){
22             String parameter = (String) it.next();
23             String parameterValue = m.get(parameter);
24             String v = "";
25             if (null != parameterValue){
26                   v = parameterValue.trim();
27             }
28               packageParams.put(parameter, v);
29         }
30 
31         String key = PayConfigUtil.API_KEY; // key
32         String resXml = "";
33 
34         // 判断签名是否正确
35         if (PayCommonUtil.isTenpaySign("UTF-8", packageParams, key)){
36               // 处理业务开始
37               if ("SUCCESS".equals((String) packageParams.get("result_code"))){
38                 // 这里是支付成功
39                 ////////// 自己的业务逻辑////////////////
40                 String out_trade_no = packageParams.get("out_trade_no").toString();//商户订单号
41                 String mch_id = packageParams.get("mch_id").toString();//商户号
42                 String openid = packageParams.get("openid").toString();//用户标识
43                 String total_fee = packageParams.get("total_fee").toString();//金额 为 分
44                 String attach = packageParams.get("attach").toString();//自己的附加属性
45                 // 分 换成元
46                 Double total_fee2 = Double.valueOf(total_fee) / 100;
47                 //微信支付订单号
48                 String transaction_id = packageParams.get("transaction_id").toString();
49                 。。。修改订单状态为支付成功
50                 // 通知微信.异步确认成功.必写.不然会一直通知后台.八次之后就认为交易失败了.
51                 resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>" + "" + "</xml> ";
52             }else{
53                 resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
54               + "<return_msg><![CDATA[失败]]></return_msg>" + "</xml> ";
55               }
56             response.getWriter().write(resXml);
57             response.getWriter().flush();
58         }else{
59               //log通知签名验证失败
60         }
61       }catch (IOException e){
62         //log获取微信充值返回参数错误
63       }catch(JDOMException e){
64         //log微信充值返回数据xml转换为map错误
65       }
66 }

4、常见问题

①、此公众号并没有这些scope的权限,错误码:10005

建议检查一下公众号的功能。比如是不是在订阅号/未认证的公众号里面尝试调用认证服务号的功能。

微信支付认证过期或者APPID填写错误。

请使用snsapi_userinfo的授权登录方式即可解决。

②、商家暂时没有此类交易权限,请联系商家客服

请检查你的下单接口是否指定了支付用户的身份,需单独开通指定身份支付权限方可使用。

请确认你使用的商户号是否有jsapi支付的权限,可登录商户平台-产品中心查看。

③、当前页面的URL未注册:http://www.weixin.qq.com/pay.do

请检查下单接口中使用的商户号是否在商户平台(http://pay.weixin.qq.com)配置了对应的支付目录。

④、redirect_url域名与后台配置不一致,错误码:10003

本错误是公众号获取openid接口报的错误,接口文档:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842

检查下单接口传的appid与获取openid接口的appid是否同一个(需一致)。

检查appid对应的公众号后台(mp.weixin.qq.com),是否配置的授权域名和获取openid的域名一致。授权域名配置路径:公众平台--设置--公众号设置--功能设置--网页授权域名。

⑤、该商户暂不支持通过外部拉起微信完成支付

Jsapi支付只能从微信浏览器内发起支付请求。

⑥、使用公众号时需要在公众号内设置IP白名单,需要把自己的公网IP/服务器IP加到白名单内。

⑦、安全域名、业务域名

都有每月设置次数限制,测试的时候不要经常去换域名信息,要不然上线的时候就没机会换域名了。(设置域名时注意,公众号内有个用户验证的MP_verify_xxxxxxxxx.txt文件,微信提示说要放在根目录下面,其实只要把文件内容返回出来,访问名称设置成文件名称加后缀就能通过验证了)。

三、微信扫码支付

微信扫码支付需要的参数有:APP_ID(微信公众号开发者ID)、MCH_ID(商户ID)、API_KEY(商户密钥)。

微信扫码支付一般应用的场景是PC端电脑支付。微信扫码支付可分为两种模式,根据支付场景选择相应模式。一般情况下的PC端扫码支付选择的是模式二,需要注意的是模式二无回调函数。

【模式一】商户后台系统根据微信支付规则链接生成二维码,链接中带固定参数productid(可定义为产品标识或订单号)。用户扫码后,微信支付系统将productid和用户唯一标识(openid)回调商户后台系统(需要设置支付回调URL),商户后台系统根据productid生成支付交易,最后微信支付系统发起用户支付流程。

【模式二】商户后台系统调用微信支付【统一下单API】生成预付交易,将接口返回的链接生成二维码,用户扫码后输入密码完成支付交易。注意:该模式的预付单有效期为2小时,过期后无法支付。

注意:需要回调时需要在商户里面配置扫码回调地址(可以在前面的资料准备-信息位置里面看图)

1、工具类

需要的工具类与上面微信公众号支付的一致

2、微信支付

这里我们使用的是微信扫码支付的【模式二】,使用链接生成二维码方式

①、页面请求

 1 //引用二维码生成JS---直接网上下载一个就行了
 2 <script src="qrcode.js"></script>
 3 
 4 $.ajax({
 5     url : 'weChatPayment',
 6     type : 'post',
 7     async: true,
 8     data : formData,
 9     success : function(data) {
10         if(data!=null && data!="" && data.length > 0){
11               if(data[0] != ""){
12                 if(data[0] == 1){
13                       //页面中间显示图片
14                     $("#qrcode").html("");
15                     var qrcode = new QRCode('qrcode');
16                     qrcode.makeCode(data[1]);
17                 }else{
18                       layer.msg(data[0], {icon: 2});
19                 }
20               }else{
21                 layer.msg("错误!!!", {icon: 2});
22               }
23         }else{
24               layer.msg("错误!!!", {icon: 2});
25         }
26       },
27       error:function(e){
28         layer.msg("网络错误!!!", {icon: 2});
29       }
30 });

②、controller层

 1 /**
 2  * 微信充值前
 3  * @param 订单需要信息
 4  * @return
 5  */
 6 @PostMapping("weChatPayment")
 7 public Object weChatPayment(Object 订单需要信息){
 8     //生成待支付订单
 9     String orderCode = "订单唯一值";
10     。。。此处省略了插入数据库模块
11     return paymentService.weChatPayment(orderCode);
12 }

③、service层

 1 @Transactional
 2 public String[] weChatPayment(String orderCode) {
 3     String[] str = new String[2];
 4     try{
 5           //通过订单取到支付金额即商品名称
 6           。。。通过订单编号取订单信息
 7           //订单金额
 8           BigDecimal ordersAmount = null;
 9           BigDecimal fen = new BigDecimal(ordersAmount).multiply(new BigDecimal(100));
10           fen = fen.setScale(0, BigDecimal.ROUND_HALF_UP);
11           String amount = fen.toString();
12 
13         //微信支付配置
14         String appid = PayConfigUtil.APP_ID; // appid
15         String mch_id = PayConfigUtil.MCH_ID; // 商业号
16         String key = PayConfigUtil.API_KEY; // key
17 
18         String currTime = PayCommonUtil.getCurrTime();
19         String strTime = currTime.substring(8, currTime.length());
20         String strRandom = PayCommonUtil.buildRandom(4) + "";
21         String nonce_str = strTime + strRandom;
22 
23         String spbill_create_ip = PayConfigUtil.CREATE_IP;// 获取发起电脑 ip
24         String notify_url = PayConfigUtil.NOTIFY_URL;// 回调接口
25         String trade_type = "NATIVE";//扫码支付方式
26 
27         String order_price = amount; // 价格 注意:价格的单位是分
28         String body = "账户充值"; // 商品名称
29 
30         SortedMap<Object, Object> packageParams = new TreeMap<Object, Object>();
31         packageParams.put("appid", appid);
32         packageParams.put("mch_id", mch_id);
33         packageParams.put("nonce_str", nonce_str);
34         packageParams.put("body", body);
35         packageParams.put("out_trade_no", orderCode);
36         packageParams.put("total_fee", order_price);
37         packageParams.put("spbill_create_ip", spbill_create_ip);
38         packageParams.put("notify_url", notify_url);
39         packageParams.put("trade_type", trade_type);
40 
41         //计算签名
42         String sign = PayCommonUtil.createSign("UTF-8", packageParams, key);
43         packageParams.put("sign", sign);
44         //把支付参数转换xml格式
45         String requestXML = PayCommonUtil.getRequestXml(packageParams);
46         //发起微信支付请求
47         String resXml = HttpUtil.postData(PayConfigUtil.UFDODER_URL, requestXML);
48         Map<String, String> map = XMLUtil.doXMLParse(resXml);
49       
50         //获取得到充值url-前台生成二维码使用
51         String urlCode = (String) map.get("code_url");
52         if(urlCode != null && !urlCode.equals("")){
53               str[0] = "1";
54               str[1] = urlCode;
55               return str;
56         }else{
57               //系统异常,暂时无法使用微信支付,请联系客服
58               str[0] = "系统异常,暂时无法使用微信支付,请联系客服";
59         }
60     }catch (IOException e){
61         str[0] = "系统异常,暂时无法使用微信支付,请联系客服!!";
62     }
63       return str;
64 }

3、微信回调

回调和解析方式都与微信公众号支付回调一致

四、微信H5支付

微信H5支付需要的参数有:APP_ID(微信公众号开发者ID)、MCH_ID(商户ID)、API_KEY(商户密钥)。

微信H5支付是微信官方2017年上半年刚刚对外开放的支付模式,它主要应用于在手机网站在移动浏览器(非微信环境)调用微信支付的场景。底层的技术以及支付链接本质上是财付通。

微信H5支付的流程比较简单,就是拼接请求的xml数据,进行统一下单,获取到支付的mweb_url,然后请求这个url网址就行。请求使用curl函数,使用的时候需要注意设置header参数。

注意:微信H5支付需要在微信支付商户平台单独申请开通,否则无法使用。(可以在前面的资料准备-信息位置里面看图)

1、工具类

需要的工具类与上面微信公众号支付的一致

2、微信支付

①、页面请求

 1 $.ajax({
 2   url: 'pay',
 3   type: 'POST',
 4   dataType: "json",
 5   data:{},
 6   success: function (res) {
 7     $.hideLoading();
 8     var data = res.data;
 9     if(data!=null && data!="" && data.length > 0){
10       if(data[0] != ""){
11         if(data[0] == 1){
12           $("#submit_z").text("正在支付");
13           //原页面显示一个需手动确认的支付提示信息
14           layer.open({
15             type: 1
16             ,title: false //不显示标题栏
17             ,closeBtn: false
18             ,area:['240px','150px']
19             ,shade: 0.3
20             ,offset: 'auto'
21             ,id: 'LAY_layuipro' //设定一个id,防止重复弹出
22             ,content: '<div style="width: 100%;text-align: center;font-size: 14px;border-radius: 20px;">\n' +
23             '    <div style="width: 100%;height: 59px;line-height: 59px;border-bottom: 1px #ccc solid;">\n' +
24             '        请确认微信支付是否已完成\n' +
25             '    </div>\n' +
26             '    <div onclick="pay_true();" style="width: 100%;font-weight: 500;border-bottom: 1px #ccc solid;line-height: 44px;height: 44px;color: red;">\n' +
27             '        已完成支付\n' +
28             '    </div>\n' +
29             '    <div onclick="pay_false();" style="color: #9c9c9c;width: 100%;line-height: 45px;height: 45px;">\n' +
30             '        支付遇到问题,重新支付\n' +
31             '    </div>\n' +
32             '</div>'
33           });
34         }else{
35           layer.msg(data[0], {icon: 2});
36         }
37       }
38       else{
39         layer.msg("错误!!!", {icon: 2});
40       }
41     }
42     else{
43       layer.msg("错误!!!", {icon: 2});
44     }
45   },
46   error:function(e){
47     layer.msg("网络错误!!!", {icon: 2});
48   }
49 });
50 
51 //跳转到订单详情
52 function pay_true() {
53     //跳转到订单页
54       window.location.href = [[${url}]];
55 }
56 //回到支付前状态
57 function pay_false() {
58     layer.closeAll();
59     $("#submit_z").text("提交订单");
60 }

②、controller层

 1 @PostMapping("pay")
 2 public Response pay(Object obj,HttpServletRequest request) {
 3       String request_url = request.getRequestURL().toString().split("indent/pay")[0];
 4     // 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址
 5       // 一定要取到真实IP,不然后面支付会报错
 6     String ip = null;
 7     try {
 8         ip = request.getHeader("x-forwarded-for");
 9         if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
10               ip = request.getHeader("Proxy-Client-IP");
11         }
12         if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
13               ip = request.getHeader("WL-Proxy-Client-IP");
14         }
15         if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
16               ip = request.getHeader("HTTP_CLIENT_IP");
17         }
18         if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
19               ip = request.getHeader("HTTP_X_FORWARDED_FOR");
20         }
21         if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
22               ip = request.getRemoteAddr();
23               if (ip.equals("127.0.0.1")) {
24                 // 根据网卡取本机配置的IP
25                 InetAddress inet = null;
26                 try {
27                     inet = InetAddress.getLocalHost();
28                 } catch (UnknownHostException e) {
29                   //LOGGER_ERROR.error("抛出异常 # ",e);
30                 }
31                 ip = inet.getHostAddress();
32             }
33         }
34 
35         // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
36         if (ip != null && ip.length() > 15) { // "***.***.***.***".length() = 15
37               if (ip.indexOf(",") > 0) {
38                 ip = ip.substring(0, ip.indexOf(","));
39               }
40         }
41       } catch (Exception ex) {
42     // LOGGER_ERROR.error("NetworkUtil # getIpAddress # 抛出异常 # ", ex);
43       }
44     //生成待支付订单
45     String orderCode = "订单唯一值";
46     。。。此处省略了插入数据库模块
47       return new Response().success().data(this.payService.pay(orderCode,request_url,ip));
48 }

③、service层

 1 @Transactional
 2 public String[] pay(String orderCode, String url,String ip) {
 3     String[] str = new String[2];
 4     String message = "";
 5     try {
 6       
 7           //通过订单取到支付金额即商品名称
 8           。。。通过订单编号取订单信息
 9           //订单金额
10           BigDecimal ordersAmount = null;
11           // 商品名称
12           String body = "";
13       
14         //获取支付配置
15         String appid = PayConfigUtil.APP_ID;// appid
16         String mch_id = PayConfigUtil.MCH_ID;// 商业号
17         String key = PayConfigUtil.API_KEY;// 商户API-key
18 
19         String currTime = PayCommonUtil.getCurrTime();
20         String strTime = currTime.substring(8, currTime.length());
21         String strRandom = PayCommonUtil.buildRandom(4) + "";
22         String nonce_str = strTime + strRandom;
23       
24         // 价格 注意:价格的单位是分
25         String order_price = new BigDecimal(String.valueOf(ordersAmount)).multiply(new BigDecimal("100")).setScale(0,BigDecimal.ROUND_HALF_UP).toString(); 
26 
27         // 获取发起电脑 ip----这里的IP限制跟别的支付不一样,这里需要真实IP
28         String spbill_create_ip = ip;
29 
30         // 回调接口
31         String notify_url = PayConfigUtil.NOTIFY_URL;
32 
33           //JSAPI NATIVE 代表公众号支付 MWEB 代表H5
34         String trade_type = "MWEB";
35         
36         SortedMap<Object, Object> packageParams = new TreeMap<Object, Object>();
37 
38         packageParams.put("appid", appid);//公众账号ID
39         
40         packageParams.put("mch_id", mch_id);//商户号
41         
42         //packageParams.put("device_info", "WEB");
43         //自定义参数,可以为终端设备号(门店号或收银设备ID),PC网页或公众号内支付可以传"WEB"
44         
45         packageParams.put("nonce_str", nonce_str);
46         //随机字符串,长度要求在32位以内。
47         packageParams.put("body", body);
48         //商品简单描述
49         packageParams.put("out_trade_no", orderCode);
50         //商户订单号
51         packageParams.put("total_fee", order_price);
52         //标价金额 单位为分
53         packageParams.put("spbill_create_ip", spbill_create_ip);
54         //APP和网页支付提交用户端ip
55         packageParams.put("notify_url", notify_url);
56         //异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。
57         packageParams.put("trade_type", trade_type);
58         packageParams.put("scene_info","'h5_info':{'type':'Wap','wap_url':'"+url+"/shop','wap_name': "+body+"");
59 
60         String sign = PayCommonUtil.createSign("UTF-8", packageParams, key);
61         packageParams.put("sign", sign);
62         //通过签名算法计算得出的签名值
63 
64         String requestXML = PayCommonUtil.getRequestXml(packageParams);
65 
66         String resXml = HttpUtil.postData(PayConfigUtil.UFDODER_URL, requestXML);
67 
68         
69         Map<String, String> map = XMLUtil.doXMLParse(resXml);
70 
71         String urlCode = (String) map.get("code_url");//获取得到充值url
72 
73         // 确认支付过后跳的地址,需要经过urlencode处理
74         String mweb_url = map.get("mweb_url") + "&redirect_url=" + PayConfigUtil.NOTIFY_URL;
75 
76         if(mweb_url != null && !mweb_url.equals("")){
77           str[0] = "1";
78           str[1] = mweb_url;
79           return str;
80         }else{
81           //系统异常,暂时无法使用微信支付,请联系客服
82           str[0] = "系统异常,暂时无法使用微信支付,请联系客服";
83         }
84     }catch (Exception e){
85           str[0] = "系统异常,暂时无法使用微信支付,请联系客服!!";
86     }
87     return str;
88 }

3、微信回调

回调和解析方式都与微信公众号支付回调一致

4、常见问题

①、网络环境未能通过安全验证,请稍后再试

商户侧统一下单传的终端IP(spbill_create_ip)与用户实际调起支付时微信侧检测到的终端IP不一致导致的,这个问题一般是商户在统一下单时没有传递正确的终端IP到spbill_create_ip导致,详细可参见客户端ip获取指引(本文已经提供了一个获取用户真实IP的方法)。

统一下单与调起支付时的网络有变动,如统一下单时是WIFI网络,下单成功后切换成4G网络再调起支付,这样可能会引发我们的正常拦截,请保持网络环境一致的情况下重新发起支付流程。

②、商家参数格式有误,请联系商家解决

当前调起H5支付的referer为空导致,一般是因为直接访问页面调起H5支付,请按正常流程进行页面跳转后发起支付,或自行抓包确认referer值是否为空。

如果是APP里调起H5支付,需要在webview中手动设置referer,如(Map extraHeaders = new HashMap();extraHeaders.put("Referer", "商户申请H5时提交的授权域名");//例如 http://www.baidu.com ))。

③、商家存在未配置的参数,请联系商家解决

当前调起H5支付的域名(微信侧从referer中获取)与申请H5支付时提交的授权域名不一致,如需添加或修改授权域名,请登陆商户号对应的商户平台--"产品中心"--"开发配置"自行配置。

如果设置了回跳地址redirect_url,请确认设置的回跳地址的域名与申请H5支付时提交的授权域名是否一致。

④、支付请求已失效,请重新发起支付

统一下单返回的MWEB_URL生成后,有效期为5分钟,如超时请重新生成MWEB_URL后再发起支付。

⑤、请在微信外打开订单,进行支付

H5支付不能直接在微信客户端内调起,请在外部浏览器调起

⑥、IOS:签名验证失败、安卓:系统繁忙,请稍后再试

请确认同一个MWEB_URL只被一个微信号调起,如果不同微信号调起请重新下单生成新的MWEB_URL。

如MWEB_URL有添加redirect_url,请确认参数拼接格式是否有误,是否有对redirect_url的值做urlencode,可对比以下例子格式:https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx20161110163838f231619da20804912345&package=1037687096&redirect_url=https%3A%2F%2Fwww.wechatpay.com.cn

五、微信APP支付、微信小程序支付

结合前面几种支付,都是套娃式的

六、注意

①、测试支付时,别老用同一订单编号。

②、微信提供了签名校验工具,https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=20_1

③、扫码支付需要在商户号里面设置回调地址,其它都是在请求支付的时候把回调地址带过去。

④、微信的回调会执行多次,注意不要重复操作订单

⑤、微信支付时,金额单位必须是分

⑥、H5支付时,支付请求传入的spbill_create_ip,必须是用户的真实IP

⑦、遇到问题不要急,先检查参数有没有问题,再看微信返回的内容,有错误码就去微信开发文档里面查看具体问题


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM