微信公众号支付开发 --Java

公众号支付是用户在微信中打开商户的H5页面,商户在H5页面通过调用微信支付提供的JSAPI接口调起微信支付模块完成支付。应用场景有:

  1. ◆ 用户在微信公众账号内进入商家公众号,打开某个主页面,完成支付
  2. ◆ 用户的好友在朋友圈、聊天窗口等分享商家页面连接,用户点击链接打开商家页面,完成支付
  3. ◆ 将商户页面转换成二维码,用户扫描二维码后在微信浏览器中打开页面后完成支付

  4. 项目描述:本系统采用SpringMVC 、AngularJs框架进行开发,开发的系统依托于微信,即在微信公众账号内进入商家公众号,并打开相应的支付页面,完成支付。因此需要采用公众号支付。

项目开发流程:
一、微信网页授权,即获取code
    首先前端通过url转到login方法,并携带相应的参数如PayVO类,如totalFee等。而后调用getOauthConnectUrl方法获得拼接的地址,并把拼接后的地址传给前端,在前端转向改链接。
    1.拼接的地址传给前端触发,原因不清楚,后端触发不了,报“跨域”的错误
    2.只能在微信中打开才能获得code,并且打开连接的微信账号必须关注相应的公众号才可以获得
    3. 获得code的url如下图,可以看到有两种方式。应用授权作用域scope取snsapi_base 时,不弹出授权页面,直接跳转,只能获取用户openid;当scope取snsapi_userinfo 时弹出授权页面,可通过openid拿到昵称、性别、所在地,即使在未关注的情况下,只要用户授权,也能获取其信息。





    /**
     * 获得用户的code,并转向支付页面(pay/pay)
     * @return
     */
    @RequestMapping("/login")
    @ResponseBody
    public JsonApi login(PayVO payVO) {
        LogUtils.trace("--------------------/pay/login-------------------------------");

        String oauthUrl;                 //重定向链接
        oauthUrl = PayUtil.getOauthConnectUrl(WechatConts.OauthScope.BASE, payVO.getTotalFee());

        LogUtils.trace("oauthUrl:" + oauthUrl);

        Map<String, String> map = new HashMap<String, String>() ;
        map.put("url", oauthUrl);

        return new JsonApi(map);
    }

public class PayVO {
    private String totalFee;

    public String getTotalFee() {
        return totalFee;
    }

    public void setTotalFee(String totalFee) {
        this.totalFee = totalFee;
    }
}



/**
     * 获得授权URL
     * @param scope
     * @param state
     * @return
     */
    public static String getOauthConnectUrl(WechatConts.OauthScope scope, String state) {
        /**
         * 获得访问授权所需要的信息
         * 如:appId, Redirect_uri, scope
         */
        String appId = WxConfigure.AppId;
        String redirectUrl = "http://" + WxConfigure.Redirect_uri;
        String connectUrl = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect";
        String oauthScope = scope.getScope();

        if (state.length() > 100) {
            LogUtils.info("stateUrl too Long:" + state);
            state = state.substring(0, 100);
        }
        String stateUrl = UrlUtil.urlEncode(state);

        if (stateUrl.length() >= 128) {
            //微信允许的最大state长度是128位
            LogUtils.info("stateUrl too Long" + stateUrl);
            stateUrl = UrlUtil.urlEncode(state.substring(0, 80));
        }
<pre name="code" class="java">public class WxOauthAccessToken {
    int errCode;
    String errMsg;
    String accessToken; //公众号的唯一标识
    int expiresIn;
    String refreshToken;
    String openId;
    String scope;

    public WxOauthAccessToken(ErrorMng.ErrorCode errorCode) {
        this.errCode = errorCode.getErrorID();
        this.errMsg = errorCode.getErrorMsg();
    }

    public WxOauthAccessToken(JSONObject jsonObject) {
        if (jsonObject.getInteger("errcode") == null) {
            this.accessToken = (String) jsonObject.get("access_token");
            this.expiresIn = (int) jsonObject.get("expires_in");
            this.refreshToken = (String) jsonObject.get("refresh_token");
            this.openId = (String) jsonObject.get("openid");
            this.scope = (String) jsonObject.get("scope");
        } else {
            this.errCode = jsonObject.getInteger("errcode");
            this.errMsg = jsonObject.getString("errmsg");
        }

    }

return connectUrl.replace("APPID", appId) .replace("REDIRECT_URI", UrlUtil.urlEncode(redirectUrl)) .replace("SCOPE", oauthScope) .replace("STATE", stateUrl); }
 
 
二、通过code换取网页授权access_token,继而获得openid
     第一步中的redirect_url指向下面的地址,当成功时会携带code和相应的参数跳转到该方法下,在该方法里调用getOauthAccessToken方法获得相应的内容,如WxOauthAccessToken类,get和set方法隐藏掉了。
public class WxOauthAccessToken {
    int errCode;
    String errMsg;
    String accessToken; //公众号的唯一标识
    int expiresIn;
    String refreshToken;
    String openId;
    String scope;

    public WxOauthAccessToken(ErrorMng.ErrorCode errorCode) {
        this.errCode = errorCode.getErrorID();
        this.errMsg = errorCode.getErrorMsg();
    }

    public WxOauthAccessToken(JSONObject jsonObject) {
        if (jsonObject.getInteger("errcode") == null) {
            this.accessToken = (String) jsonObject.get("access_token");
            this.expiresIn = (int) jsonObject.get("expires_in");
            this.refreshToken = (String) jsonObject.get("refresh_token");
            this.openId = (String) jsonObject.get("openid");
            this.scope = (String) jsonObject.get("scope");
        } else {
            this.errCode = jsonObject.getInteger("errcode");
            this.errMsg = jsonObject.getString("errmsg");
        }

    }

    /**
     * 付款页面
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @RequestMapping("/callback")
    public String pay(HttpServletRequest request, HttpServletResponse response) throws Exception {
        LogUtils.trace("-----------------------/pay/callback/--------------------------------");

        /**
         * 获得code和state字段
         */
        String code = request.getParameter("code");
        String state = request.getParameter("state");
        int total = (int)(Float.parseFloat(state)*100);

        /**
         * 获取openId
         */
        WxOauthAccessToken oauthAccessToken = PayUtil.getOauthAccessToken(code);
        String openId = oauthAccessToken.getOpenId();

    /**
     * 需要第一步引导获得用户的code,才能拿到该用户的accessToken
     * @param code
     * @return
     */
    public static WxOauthAccessToken getOauthAccessToken(String code) {
        String oauthUrl = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code";
        try {
            JSONObject oauthResult = JsonApi.getJson(oauthUrl.replace("APPID", AccessInfo.AppId.VALUE.getAppId())
                    .replace("SECRET", AccessInfo.AppSecret.VALUE.getAppSecret())
                    .replace("CODE", code));
            return new WxOauthAccessToken(oauthResult);
        } catch (IOException e) {
            LogUtils.error("Wechat Get Oauth AccessToken Fail" + e.getStackTrace());
            return new WxOauthAccessToken(ErrorMng.ErrorCode.WECHAT_API_INVOKE_FAIL);
        }
    }


三、执行统一下单接口
     首先初始化统一接口需要的各个必须参数(公众号支付统一下单接口),而后调用JsApiReqUtils类的httpsRequest方法请求微信的统一下单接口。其次将返回的值根据前端JsAPi需要的值(发起一个微信支付请求)进行封装并返回给前端,此时是采取的是url带值的方式。
  /**
     * 付款页面
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @RequestMapping("/callback")
    public String pay(HttpServletRequest request, HttpServletResponse response) throws Exception {
        LogUtils.trace("-----------------------/pay/callback/--------------------------------");

        /**
         * 获得code和state字段
         */
        String code = request.getParameter("code");
        String state = request.getParameter("state");
        int total = (int)(Float.parseFloat(state)*100);

        /**
         * 获取openId
         */
        WxOauthAccessToken oauthAccessToken = PayUtil.getOauthAccessToken(code);
        String openId = oauthAccessToken.getOpenId();


        /**
         * 执行统一下单接口,初始化各参数
         */
        Date now = new Date();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
        String outTradeNo = "NO" + dateFormat.format( now );
        String body = "测试";
        String nonceStr = StringUtil.generateRandomString(16);
//        String spBillCreateIP = ReqUtils.getRealIp(request);
        String spBillCreateIP = "127.0.0.1";

        //初始化传入参数
        JsApiReqData jsApiReqData =
                new JsApiReqData(body,nonceStr,outTradeNo,total,spBillCreateIP, openId);

        String info = MobiMessage.JsApiReqData2xml(jsApiReqData).replaceAll("__", "_");
        LogUtils.trace(info);

        String url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
        StringBuffer sb = JsApiReqUtils.httpsRequest(url, "POST", info);
        if (!"".equals(sb.toString())) {
            /**
             * 得到预支付ID,即prepay_id
             */
            Map<String, String> getMap = MobiMessage.parseXml(new String(sb.toString().getBytes(), "utf-8"));
            LogUtils.trace(getMap);
            String prepay_id = getMap.get("prepay_id");
            LogUtils.trace(prepay_id);

            // 生成支付签名,这个签名给微信支付的调用使用
            Integer timeStamp = DateUtils.getCurrentTimestamp();
            JsApiToJsData jsApiToJsData = new JsApiToJsData(timeStamp, nonceStr,prepay_id);

            String redirectUrl = "/#pay?appId=APPID&timeStamp=TIMESTAMP&nonceStr=NONCESTR&prepay_id=PREPAYID&signType=MD5&paySign=SIGN";
            redirectUrl = redirectUrl.replace("APPID", jsApiToJsData.getAppid())
                    .replace("TIMESTAMP", jsApiToJsData.getTimeStamp() + "")
                    .replace("NONCESTR", jsApiToJsData.getNonce_str())
                    .replace("PREPAYID", jsApiToJsData.getPrepay_id())
                    .replace("SIGN", jsApiToJsData.getSign());
            LogUtils.trace(redirectUrl);

            return "redirect:" + redirectUrl ;
        } else {
            LogUtils.trace("统一下单失败!");
            return "";
        }
    }

/**
 * 请求公众号支付API需要提交的数据
 * @author Created by fenghui.
 * @date Created on 2016/09/06/9:28
 **/
public class JsApiReqData {
    //每个字段具体的意思请查看API文档
    private String appid = "";
    private String mch_id = "";
    private String body = "";
    private String nonce_str = "";
    private String sign = "";
    private String notify_url = "";
    private String out_trade_no = "";
    private String spbill_create_ip = "";
    private Integer total_fee = 0;
    private String trade_type = "";
    private String openid = "";

    /**
     * @param body   要支付的商品的描述信息,用户会在支付成功页面里看到这个信息
     * @param nonceStr  随机字符串,不长于32 位
     * @param outTradeNo  商户系统内部的订单号,32个字符内可包含字母, 确保在商户系统唯一
     * @param totalFee   订单总金额,单位为“分”,只能整数
     * @param spBillCreateIP  订单生成的机器IP
     * @param openid  用户标识,trade_type=JSAPI,此参数必传,用户在商户appid下的唯一标识。
     */
    public JsApiReqData(String body,String nonceStr,String outTradeNo,Integer totalFee,String spBillCreateIP,
                        String openid){

        //微信分配的公众号ID(开通公众号之后可以获取到)
        setAppid(WxConfigure.AppId);

        //微信支付分配的商户号ID(开通公众号的微信支付功能之后可以获取到)
        setMch_id(WxConfigure.Mch_id);

        //接收微信支付异步通知后的回调地址
        setNotify_url(WxConfigure.Notify_url);

        //交易类型,取值如下:JSAPI,NATIVE,APP,
        setTrade_type(WxConfigure.Trade_type);

        //要支付的商品的描述信息,用户会在支付成功页面里看到这个信息
        setBody(body);

        //商户系统内部的订单号,32个字符内可包含字母, 确保在商户系统唯一
        setOut_trade_no(outTradeNo);

        //订单总金额,单位为“分”,只能整数
        setTotal_fee(totalFee);

        //订单生成的机器IP
        setSpbill_create_ip(spBillCreateIP);

        //随机字符串,不长于32 位
        setNonce_str(nonceStr);

        //用户标识,trade_type=JSAPI,此参数必传,用户在商户appid下的唯一标识。
        setOpenid(openid);


        //根据API给的签名规则进行签名
        SortedMap<Object, Object> parameters = new TreeMap<Object, Object>();
        parameters.put("appid", appid);
        parameters.put("mch_id", mch_id);
        parameters.put("nonce_str", nonce_str);
        parameters.put("body", body);
        parameters.put("out_trade_no", out_trade_no);
        parameters.put("notify_url", notify_url);
        parameters.put("total_fee", total_fee);
        parameters.put("spbill_create_ip", spbill_create_ip);
        parameters.put("trade_type", trade_type);
        parameters.put("openid", openid);

        String sign = DictionarySort.createSign(parameters);
        //根据给的签名规则进行签名
        setSign(sign);//把签名数据设置到Sign这个属性中

    }

public class JsApiToJsData {
    //每个字段具体的意思请查看API文档
    private String appid = "";
    private Integer timeStamp = 0;
    private String nonce_str = "";
    private String sign = "";
    private String prepay_id = "";
    private String signType = "";

    /**
     * @param nonceStr  随机字符串,不长于32 位
     */
    public JsApiToJsData(Integer timeStamp,String nonceStr,String prepay_id){

        //微信分配的公众号ID(开通公众号之后可以获取到)
        setAppid(WxConfigure.AppId);

        setTimeStamp(timeStamp);

        setNonce_str(nonceStr);

        setPrepay_id(prepay_id);

        setSignType("MD5");

        //根据API给的签名规则进行签名
        SortedMap<Object,Object> parameters = new TreeMap<Object,Object>();
        parameters.put("appId", appid);
        parameters.put("timeStamp", timeStamp);
        parameters.put("nonceStr", nonceStr);
        parameters.put("package", "prepay_id=" + prepay_id);
        parameters.put("signType",signType);

        //根据给的签名规则进行签名
        String sign = DictionarySort.createSign(parameters);
        setSign(sign);//把签名数据设置到Sign这个属性中
    }

    类StringUtil主要用于产生随机字符串。
public class StringUtil {

    public static String generateRandomString() {
        return generateRandomString(16);
    }

    public static String generateRandomString(int length) {
        String seekChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        int seekLength = seekChars.length();
        String str = "";
        for (int i = 0; i < length; i++) {
            str += seekChars.charAt((int) (Math.random() * seekLength));
        }
        return str;
    }

    public static String generateRandomNumber() {
        return generateRandomNumber(6);
    }

    public static String generateRandomNumber(int length) {
        String seekChars = "0123456789";
        int seekLength = seekChars.length();
        String str = "";
        for (int i = 0; i < length; i++) {
            str += seekChars.charAt((int) (Math.random() * seekLength));
        }
        return str;
    }
}

    类JsApiReqUtils用于获得 网页支付提交用户端ip和触发http请求。
public class JsApiReqUtils {

    //获取真实IP
    public static String getRealIp(HttpServletRequest request) {
        if (request == null) {
            LogUtils.error("getRealIp request null");
            return "0.0.0.0";
        }
        String realIp = request.getHeader("X-Real-IP");
        LogUtils.trace("realIp:" + realIp);
        if (realIp != null && realIp.length() > 0) {
            return realIp;
        }
        realIp = request.getHeader("X-Forwarded-For");
        LogUtils.trace("realIp2:" + realIp);
        if (realIp != null && realIp.length() > 0) {
            return realIp;
        }
        return request.getRemoteAddr();
    }

    public static StringBuffer httpsRequest(String requestUrl, String requestMethod, String output)
            throws Exception {
        HttpURLConnection conn = (HttpURLConnection) new URL(requestUrl).openConnection();
        //加入数据
        conn.setRequestMethod(requestMethod);
        conn.setDoOutput(true);

        BufferedOutputStream buffOutStr = new BufferedOutputStream(conn.getOutputStream());
        buffOutStr.write(output.getBytes("utf-8"));
        buffOutStr.flush();
        buffOutStr.close();

        //获取输入流
        BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));

        String line = null;
        StringBuffer sb = new StringBuffer();
        while((line = reader.readLine())!= null){
            sb.append(line);
        }
        return sb;
    }

}

    MobiMessage类主要用于把json格式的数据转换为微信需要的xml格式的数据,并把返回值从xml格式解析为map格式。
public class MobiMessage {

    public static Map<String,String> xml2map(HttpServletRequest request) throws IOException, DocumentException {
        Map<String,String> map = new HashMap<String, String>();
        SAXReader reader = new SAXReader();
        InputStream inputStream = request.getInputStream();
        Document document = reader.read(inputStream);
        Element root = document.getRootElement();
        List<Element> list = root.elements();
        for(Element e:list){
            map.put(e.getName(), e.getText());
        }
        inputStream.close();
        return map;
    }


    //订单转换成xml
    public static String JsApiReqData2xml(JsApiReqData jsApiReqData){
        /*XStream xStream = new XStream();
        xStream.alias("xml",productInfo.getClass());
        return xStream.toXML(productInfo);*/
        MobiMessage.xstream.alias("xml",jsApiReqData.getClass());
        return MobiMessage.xstream.toXML(jsApiReqData);
    }

    public static String RefundReqData2xml(RefundReqData refundReqData){
        /*XStream xStream = new XStream();
        xStream.alias("xml",productInfo.getClass());
        return xStream.toXML(productInfo);*/
        MobiMessage.xstream.alias("xml",refundReqData.getClass());
        return MobiMessage.xstream.toXML(refundReqData);
    }

    public static String class2xml(Object object){

        return "";
    }
    public static Map<String, String> parseXml(String xml) throws Exception {
        Map<String, String> map = new HashMap<String, String>();
        Document document = DocumentHelper.parseText(xml);
        Element root = document.getRootElement();
        List<Element> elementList = root.elements();
        for (Element e : elementList)
            map.put(e.getName(), e.getText());
        return map;
    }

    //扩展xstream,使其支持CDATA块
    private static XStream xstream = new XStream(new XppDriver() {
        public HierarchicalStreamWriter createWriter(Writer out) {
            return new PrettyPrintWriter(out) {
                // 对所有xml节点的转换都增加CDATA标记
                boolean cdata = true;

                //@SuppressWarnings("unchecked")
                public void startNode(String name, Class clazz) {
                    super.startNode(name, clazz);
                }

                protected void writeText(QuickWriter writer, String text) {
                    if (cdata) {
                        writer.write("<![CDATA[");
                        writer.write(text);
                        writer.write("]]>");
                    } else {
                        writer.write(text);
                    }
                }
            };
        }
    });


}

    DateUtils类主要用于获得当前的时间戳。
public class DateUtils {


    public static int minTimestamp = 0; //最小的时间戳
    public static int maxTimestamp = 1999999999;//最大的时间戳
    public static int offset = 0;

    /**
     * 按照yyyy-MM-dd HH:mm:ss的格式,日期转字符串
     *
     * @param date
     * @return yyyy-MM-dd HH:mm:ss
     */
    public static String date2Str(Date date) {
        return date2Str(date, "yyyy-MM-dd HH:mm:ss");
    }

    /**
     * 按照参数format的格式,日期转字符串
     *
     * @param date
     * @param format
     * @return
     */
    public static String date2Str(Date date, String format) {
        if (date != null) {
            SimpleDateFormat sdf = new SimpleDateFormat(format);
            return sdf.format(date);
        } else {
            return "";
        }
    }

    public static int getCurrentTimestamp() {
        return (int) (System.currentTimeMillis() / 1000 + offset);
    }

    public static boolean setCurrentTimestamp(int timestamp) {
        int systemTimestamp = (int) (System.currentTimeMillis() / 1000);
        offset = timestamp - systemTimestamp;
        return true;

    }
}

    类DictionarySort主要根据微信通过的签名规则用于签名,其中key设置路径:微信商户平台-->账户中心-->API安全-->API密钥-->设置API密钥。



public class DictionarySort {

    /**
     * 签名算法
     * @param parameters
     * @return
     */
    public static String createSign(SortedMap<Object, Object> parameters) {
        StringBuffer sb = new StringBuffer();
        Set es = parameters.entrySet();//所有参与传参的参数按照accsii排序(升序)
        Iterator it = es.iterator();
        String key = "1082ae0cd40043df9c4531b597970646";
        while (it.hasNext()) {
            Map.Entry entry = (Map.Entry) it.next();
            String k = (String) entry.getKey();
            Object v = entry.getValue();
            if (null != v && !"".equals(v)
                    && !"sign".equals(k) && !"key".equals(k)) {
                sb.append(k + "=" + v + "&");
            }
        }
        sb.append("key=" + key);
        LogUtils.trace(sb.toString());
        String sign = CryptoUtils.MD5(sb.toString()).toUpperCase();
        return sign;
    }

}


四、汇款后的通知页面
统一下单的参数notify_url决定了付款后的通知地址,应该把起设为改地址,如

//付款成功后的通知地址
    public static final String Notify_url = "***/pay/notification";

注意:此时微信会重复发送通知给应用,因此此时接受通知业务的逻辑必须能够识别是否为第一次通知,不然会重复处理接到的通知。官方文档上说可以给微信返回处理成功的通知,但经过多次试验都没能够解决该问题。因此最后采取在业务逻辑上处理通知,即第一次通知进行处理,其余的通知一律忽视。
 /**
     * 付款后通知跳轉頁面
     * @param request
     * @param response
     * @return
     * @throws IOException
     * @throws DocumentException
     */
    @RequestMapping("/notification")
    @ResponseBody
    public JsonApi notification(HttpServletRequest request,HttpServletResponse response) throws IOException, DocumentException {
        Map<String, String> map = MobiMessage.xml2map(request);
        LogUtils.trace(map);

        //根据测试成功后的返回数据来进行业务逻辑操作,比如存数据库等

        return new JsonApi();
    }

五、设置测试目录
    必须设置公众号支付的测试授权目录和支付授权目录,该目录必须和发起支付请求的页面保持一致,比如发起支付请求的url是http://***/#/pay,只需要把该目录设为http://***/即可。




  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值