微信公众号支付的那些坑

在之前记录了一下做微信公众号支付的过程,但是有些混乱,之前做的内个也不是直接接的微信官方,而是转接的别人在接的微信官方,他们赚个手续费,在这之后因为app停用了一段时间,上游公司把我们的appid给关掉了,所以打算从新接,直接接微信官方,好了这是背景。

我们做的是公众号支付,也就是在微信网页内部进行调取支付插件进行支付的一个过程

所以需要到微信官方开通公众号支付 微信官网:https://pay.weixin.qq.com

1、登录后点击产品中心, 点击公众号支付


进入后就会看到这个页面


因为我的已经开通所以就不需要了

这是官方文档 : https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_3

点击开发配置

进行配置支付授权目录:也就是你的支付页面所在的目录

一定是生产环境的,微信不支持 ip +端口 形式的地址   异步通知也不支持,

所以测试都需要线上真实环境的域名+支付页面所在目录


登录公众号平台进行配置

https://mp.weixin.qq.com 

公众号的按钮在下面

其次设置你的JS接口安全域名:也就是完整域名如:www.baidu.com


配置到这里基本就算完成了

现在我们需要获取几个必须的参数

appid,mch_id ,加密key

基本配置按钮也在下面

显示appid  点击基本配置就会看到了 如:wxf8xxxxxxxxxfca


mch_id 就是你登录微信商户后台的内个号,如:1594xxxxxxxxx98 


key 获取,也是在微信商户后台

这个是自己设置的,看你自己设置了,


1.        appid APPID (已有)

wxfxxxxxxxxxxxxxca

2.        mch_id 商户ID (已有)

147xxxxxxxxxxx54

3.        nonce_str 随机字符串 , 生成UUID就可以了;

 UUID.randomUUID().toString().trim().replaceAll("-", "");

4.        sign 签名 用WXPayUtil中的generateSignature(finalMap<String, String> data, String key)方法,data是将除了sign外,其他10个参数放到map中,key是四大配置参数中的API秘钥(paternerKey)(这里不要着急管它,最后处理它);

5.        body 所支付的名称

6.        out_trade_no 自己后台生成的订单号,只要保证唯一就好:如“pay2018062521331”

7.        total_fee 支付金额 单位:分,为了测试此值给1,表示支付1分钱

8.        spbill_create_ip IP地址 网上很多ip的方法,自己找,此处测试给“127.0.0.1”

9.        notify_url 回调地址:这是微信支付成功后,微信那边会带着一大堆参数(XML格式)请求这个地址多次,这个地址做我们业务处理如:修改订单状态,赠送积分等。Ps:支付还没成功还想这么远干嘛,最后再说。地址要公网可以访问。

10.    trade_type 支付类型 咱们是公众号支付此处给“JSAPI”

11.    openid 支付人的微信公众号对应的唯一标识,每个人的openid在不同的公众号是不一样的,这11个参数里,最费劲的就是他了,其他的几乎都已经解决,现在开发得到这个参数。

    获得openid的部分内容应该不属于微信支付的范畴,属于微信公众号网页授权的东西,详情请参考微信网页授权:

https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842

获得openid步骤:

第一步:用户同意授权,获取code

https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect

注意:1. redirect_uri参数:授权后重定向的回调链接地址请使用 urlEncode 对链接进行处理。

2. scope:用snsapi_base 

    通过此链接可以获取code,可以在一个空页面设置一个a标签,链接至其redirect_uri的地址。点击a标签,即可链接到redirect_uri的地址,并携带code。

[html]  view plain  copy
  1. <a href="https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx15c*********&redirect_uri=http%3a%2f%2fwww.***.com%2fpay.jsp&response_type=code&cope=snsapi_base#wechat_redirect">去支付页面pay.jsp并携带code</a>  

第二步:通过code换取网页授权access_token(其实微信支付就没有必要获取access_token,咱们只要其中openid,不是要用户信息,此步结果已经就含有咱们需要的openid了)

获取code,请求以下链接获取access_token https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

上一步的code有了,对于此链接的参数就容易了。可是在页面上如何处理是个问题,我是在pay.jsp页面加载完成后将获取code当做参数传异步到后台,在后台中用http相关类发送get请求(可以自行网上查找)。返回的JSON结果为:

[html]  view plain  copy
  1. { "access_token":"ACCESS_TOKEN",  
  2. "expires_in":7200,  
  3. "refresh_token":"REFRESH_TOKEN",  
  4. "openid":"OPENID",//就是它,只要这个值  
  5. "scope":"SCOPE" }  

好了都有了,我们就可以开始写拼装参数了, 参数填写修改成你自己的就可以了

   HashMap<String, Object> reqMap = new HashMap<String, Object>()
            reqMap.put("appid", appid);
            reqMap.put("mch_id", mchid);
            reqMap.put("nonce_str", getUUid());
            reqMap.put("out_trade_no", "订单号");
            reqMap.put("total_fee", 金额);
            reqMap.put("body", "所支付的名称");
            reqMap.put("spbill_create_ip", getIp);
            reqMap.put("notify_url", notifyUrl);
            // reqMap.put("device_info", "WEB");
            reqMap.put("trade_type", "JSAPI");
            reqMap.put("openid", openId);

下面是工具类

/** 生成32位编码
 * @return string
 */
    public static String getUUid() {
        String uuid = UUID.randomUUID().toString().trim().replaceAll("-", "");
        return uuid;
    }
/**
 * 获取IP
 * @param request
 * @return
 */
    public String getRemortIP(HttpServletRequest request) {
        String remoteAddr = request.getRemoteAddr();
        String forwarded = request.getHeader("X-Forwarded-For");
        String realIp = request.getHeader("X-Real-IP");
        String ip = null;
        if (realIp == null) {
            if (forwarded == null) {
                ip = remoteAddr;
            } else {
                ip = remoteAddr + "/" + forwarded.split(",")[0];
            }
        } else {
            if (realIp.equals(forwarded)) {
                ip = realIp;
            } else {
                if (forwarded != null) {
                    forwarded = forwarded.split(",")[0];
                }
                ip = realIp + "/" + forwarded;
            }
        }
        return ip
    }

下面进行签名,MD5工具类会贴出了的

 def md5str = createLinkString(reqMap);//排序
            System.out.println("---> md5str:" + md5str + "&key=" + wxapiKey);
            def sign = MD5Tool.md5(md5str + "&key=" + apiKey, charset).toUpperCase();签名并转大写
            reqMap.put("sign", sign)
/**
 *  SignStr 待签名字符串
 * @param params
 * @return
 */
    public static String createLinkString(Map<String, Object> map) {
        System.out.println("带签名排序params:::::" + map.toString())
        List<String> keys = new ArrayList<String>(map.keySet());
        Collections.sort(keys);
        System.out.println("排序完Keys:::::" + keys.toString())
        def sb = new StringBuffer()
        for (int i = 0; i < keys.size(); i++) {
            String key = keys.get(i);
            Object value = map.get(key)
            System.out.println("key::" + key + "::value::" + value)
            if ("sign".equals(key) || "sign_type".equals(key) || value == null || value == "") {
                continue;
            }
            if (i == keys.size() - 1) {
                sb.append(key).append("=").append(value)
            } else {
                sb.append(key).append("=").append(value).append("&")
            }
        }
        def sbStr = sb.toString()
        System.out.println("===========签名串:" + sbStr)
        return sbStr;
    }
package eims;

import org.apache.commons.codec.digest.DigestUtils;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
//MD5工具类
public class MD5Tool {
    public static String md5(byte[] data) {
        MessageDigest md5 = null;
        try {
            md5 = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            throw new IllegalStateException("System doesn't support MD5 algorithm.");
        }
        md5.update(data);

        byte[] encoded = md5.digest();
        StringBuffer buf = new StringBuffer();
        for (int i = 0; i < encoded.length; i++) {
            if ((encoded[i] & 0xff) < 0x10) {
                buf.append("0");
            }
            buf.append(Long.toString(encoded[i] & 0xff, 16));
        }

        return buf.toString();
    }
    /**
     * 签名字符
     *
     * @param text
     *            要签名的字符
     * @param key
     *            密钥
     *            编码格式
     * @return 签名结果
     */
    public static String sign(String text, String key, String charset) throws Exception {
        text = text + key;
        return DigestUtils.md5Hex(getContentBytes(text, charset));
    }
    /**
     * @param content
     * @param charset
     * @return
     * @throws java.io.UnsupportedEncodingException
     */
    private static byte[] getContentBytes(String content, String charset) {
        if (charset == null || "".equals(charset)) {
            return content.getBytes();
        }
        try {
            return content.getBytes(charset);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("签名过程中出现错,指定的编码集不对,您目前指定的编码集是:" + charset);
        }
    }
    public static String md5(String str, String charset) {
        if (str == null) return null;
        try {
            return md5( str.getBytes(charset) );
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            throw new IllegalStateException("System doesn't support Charset '" + charset + "'");
        }
    }
}

到这里所有的参数都拼装好了,微信要的是XML 格式的所以我们需要进行转换

def requestXML = mapGetXML(reqMap);
System.out.println("req xml :\n" + requestXML);
String response = post(wxUrl, requestXML);

/**
 * map 转 XML
 * @param map
 * @return
 */
    public static String mapGetXML(Map<String, String> map) {
        String xmlStr = null;
        StringBuffer sbf = new StringBuffer("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>");
        sbf.append("<xml>");
        for (Map.Entry<String, String> s : map.entrySet()) {
            sbf.append("<")
                    .append(s.getKey())
                    .append(">")
                    .append(s.getValue())
                    .append("</")
                    .append(s.getKey())
                    .append(">");
        }
        sbf.append("</xml>");
        xmlStr = sbf.toString();
        return xmlStr;
    }

XML也转换好了,我们现在可以直接发送请求了 格式xml

请求地址  :https://api.mch.weixin.qq.com/pay/unifiedorder

 String response = post(wxUrl, requestXML);

post方法

/**
 * 发送xml数据请求到server端
 *
 * @param url
 *            xml请求数据地址
 * @param xmlString
 *            发送的xml数据流
 * @return null发送失败,否则返回响应内容
 */
    public static String post(String url, String xmlFileName) {
        // 关闭
        System.setProperty("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.SimpleLog");
        System.setProperty("org.apache.commons.logging.simplelog.showdatetime", "true");
        System.setProperty("org.apache.commons.logging.simplelog.log.org.apache.commons.httpclient", "stdout");

        // 创建httpclient工具对象
        HttpClient client = new HttpClient();
        // 创建post请求方法
        PostMethod myPost = new PostMethod(url);
        // 设置请求超时时间
        client.setConnectionTimeout(300 * 1000);
        String responseString = null;
        try {
            // 设置请求头部类型
            myPost.setRequestHeader("Content-Type", "text/xml");
            myPost.setRequestHeader("charset", "utf-8");

            // 设置请求体,即xml文本内容,注:这里写了两种方式,一种是直接获取xml内容字符串,一种是读取xml文件以流的形式
            // myPost.setRequestBody(xmlString);

            // InputStream
            // body=this.getClass().getResourceAsStream("/"+xmlFileName);
            // myPost.setRequestBody(body);
            myPost.setRequestEntity(new StringRequestEntity(xmlFileName, "text/xml", "utf-8"));
            int statusCode = client.executeMethod(myPost);
            if (statusCode == HttpStatus.SC_OK) {
                BufferedInputStream bis = new BufferedInputStream(myPost.getResponseBodyAsStream());
                byte[] bytes = new byte[1024];
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                int count = 0;
                while ((count = bis.read(bytes)) != -1) {
                    bos.write(bytes, 0, count);
                }
                byte[] strByte = bos.toByteArray();
                responseString = new String(strByte, 0, strByte.length, "utf-8");
                bos.close();
                bis.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        myPost.releaseConnection();
        return responseString;
    }

搞了这么多终于看到点结果了


请求过后 微信端返回的也是XML 不利于我们处理,所以继续转map
  Map<String, Object> respMap = xmlString2Map(response);

xml 转map

/**
 * Xml string转换成Map
 * @param xmlStr
 * @return
 */
    public static Map<String, Object> xmlString2Map(String xmlStr) {
        Map<String, Object> map = new HashMap<String, Object>();
        Document doc;
        try {
            doc = DocumentHelper.parseText(xmlStr);
            Element el = doc.getRootElement();
            map = recGetXmlElementValue(el, map);
        } catch (DocumentException e) {
            e.printStackTrace();
        }
        return map;
    }

转好map 后我们就开始取 prepay_id

搞了这么久就是为了丫的, 取出来后我们还需要把参数拼装一遍,扔到页面,掉起 JS 插件进行支付

参数:

appid 也有

timeStamp 时间戳 你们new Date();即可,因为我语言是 Groovy 所以需要getTime 才是秒数

package   prepay_id   已有了

nonceStr    随机数  getuuid方法就可以了

signType     固定值  MD5

sign           上面5个参数的签名结果

这里值得注意的是package 参数, 这个参数可不是简单的吧prepay_id 放进去

,要把 “prepay_id=”这个拼接上里面不能有多余的"或者'符号

之前没有拼接 ,微信支付的时候返回 缺少total_fee参数, 可是上一步给微信传的时候并没有少,微信返回的都成功了
所以还是抛页面的时候出现的错误,害我整了好久。



 String retCode = respMap.get("result_code");
            if ("SUCCESS".equals(retCode)) {
               String prepay_id = respMap.get("prepay_id");
                Map map = new HashMap();
                map.put("appId", wxappid);
                map.put("timeStamp", new Date().getTime());
                map.put("package", "prepay_id=" + prepay_id);
                map.put("nonceStr", getUUid());
                map.put("signType", "MD5");
                def newmd5str = createLinkString(map);
                System.out.println("ShoppingCartService---> md5str:" + newmd5str + "&key=" + wxapiKey);
                def newSign = MD5Tool.md5(newmd5str + "&key=" + wxapiKey, charset).toUpperCase();
                map.put("paySign", newSign);
            }

参数装完后直接传到页面,看自己框架了,我就不贴了

下面直接把页面贴出了 值得注意的是

package 这个参数 ,在页面是一个域 ,所以在后台传的时候重新改个名 我改的是paypackage 

<%@ page contentType="text/html;charset=UTF-8" %>
<html>
<head>
    <title>微信支付</title>
    <meta http-equiv="content-type" content="text/html;charset=utf-8"/>
</head>
<script>
    function onBridgeReady() {
        <%
            System.out.println("incoming...");
            System.out.println(request.getParameter("appId"));
            System.out.println(request.getParameter("timeStamp"));
            System.out.println(request.getParameter("nonceStr"));
            System.out.println(request.getParameter("paypackage"));
            System.out.println(request.getParameter("signType"));
            System.out.println(request.getParameter("paySign"));
            System.out.println('deviceType:::'+request.getParameter("deviceType"));
            System.out.println("incoming...");
        %>
        WeixinJSBridge.invoke(
            'getBrandWCPayRequest', {
                "appId": "<%= request.getParameter("appId") %>",     //公众号名称,由商户传入
                "timeStamp": "<%= request.getParameter("timeStamp") %>",         //时间戳,自1970年以来的秒数
                "nonceStr": "<%= request.getParameter("nonceStr") %>", //随机串
                "package": "<%= request.getParameter("paypackage") %>",
                "signType": "<%= request.getParameter("signType") %>",         //微信签名方式:
                "paySign": "<%= request.getParameter("paySign") %>" //微信签名,paySign 采用统一的微信支付 Sign 签名生成方法,注意这里 appId 也要参与签名,appId 与 config 中传入的 appId 一致,即最后参与签名的参数有appId, timeStamp, nonceStr, package, signType。
            },
            function (res) {
                if (res.err_msg == "get_brand_wcpay_request:ok") {     // 使用以上方式判断前端返回,微信团队郑重提示:res.err_msg将在用户支付成功后返回    ok,但并不保证它绝对可靠。
                    alert('支付成功!');
                   
                } else if (res.err_msg == "get_brand_wcpay_request:cancel") {
                    alert('已取消微信支付!');
                } else {
                    alert('支付失败!' + res.err_msg)
                }
                /*WeixinJSBridge.call('closeWindow');*/  //关闭微信端窗口
            }
        );
    }

    if (typeof WeixinJSBridge == "undefined") {
        if (document.addEventListener) {
            document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
        } else if (document.attachEvent) {
            document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
            document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
        }
    } else {
        onBridgeReady();
    }
</script>
</html>


到这里就完成了,微信支付只能去生产上面测试,这是比较操蛋的
如果按照以上的步骤一步步走的话不会出现问题的,如有问题请留言,看到会及时答复(有人看吗,哈哈哈哈哈够呛!!



评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值