美文网首页
Java微信扫码支付

Java微信扫码支付

作者: asing1elife | 来源:发表于2018-10-28 15:29 被阅读0次

    以下内容是基于模式二开发
    在开发之前需要先到微信支付官网注册账号,并获取到以下信息
    appid:wx1137939101111111公众账号id
    mch_id:1438111111 商户号
    key:4Inn0va1eSxOnl1neqsxwuhan1111111密钥
    send_url:https://api.mch.weixin.qq.com/pay/unifiedorder统一下单API
    notify_url:http://127.0.0.1:8080/sop/order/notify/wechat支付成功回调地址

    官网

    1. 微信支付官网
    2. 扫码支付开发者文档

    模式

    1. 需要在公众平台后台设置支付回调URL ,用于接收用户扫码后微信支付系统回调的productid和openid
    2. 直接调用统一下单API 即可,相对于模式一更为简洁

    定义接口对象

    1. 根据 统一下单接口API 定义四个对象,用于发送和接收数据
      UnifiedOrderRequest.java 统一下单请求参数-必填项
    public class UnifiedOrderRequest {
    
        // 公众账号id
        private String appid;
    
        // 商户号
        private String mch_id;
    
        // 随机字符串,32位以内
        private String nonce_str;
    
        // 签名,遵循签名算法
        private String sign;
    
        // 商品描述,浏览器打开的网站主页title名称-商品概述
        private String body;
    
        // 商户订单号,32位以内,不重复
        private String out_trade_no;
    
        // 标价金额,单位分
        private Integer total_fee;
    
        // 终端ip,填写调用端的ip
        private String spbill_create_ip;
    
        // 通知地址,接收支付结果的会掉地址,必须外网可访问
        private String notify_url;
    
        // 交易类型,JSAPI--公众号支付、NATIVE--原生扫码支付、APP--app支付
        private String trade_type;
    }
    

    UnifiedOrderRequestExt.java 统一下单请求参数-非必填项

    public class UnifiedOrderRequestExt extends UnifiedOrderRequest {
    
        // 设备号,网页端填写WEB
        private String device_info;
    
        // 签名类型,默认MD5
        private String sign_type;
    
        // 商品详情,JSON格式
        private String detail;
    
        // 附加数据,可作为自定义参数使用
        private String attach;
    
        // 标价币种,默认CNY
        private String fee_type;
    
        // 交易起始时间,格式为yyyyMMddHHmmss
        private String time_start;
    
        // 交易结束时间,最短失效时间必须间隔5分钟
        private String time_expire;
    
        // 商品id,trade_type=NATIVE时,必填
        private String product_id;
    
        // 指定支付方式,no_credit可限制使用信用卡
        private String limit_pay;
    
        // 用户标识,trade_type=JSAPI时,必填
        private String openid;
    }
    

    UnifiedOrderResponse.java 统一下单返回参数-必填项

    public class UnifiedOrderResponse {
    
        // 返回状态码,通信标识,SUCCESS/FAIL
        private String return_code;
    
        // 公众账号id
        private String appid;
    
        // 商户号
        private String mch_id;
    
        // 随机字符串
        private String nonce_str;
    
        // 签名
        private String sign;
    
        // 业务结果,交易标识,SUCCESS/FAIL
        private String result_code;
    
        // 交易类型,JSAPI,NATIVE,APP
        private String trade_type;
    
        // 预支付交易会话标识,有效值2小时
        private String prepay_id;
    }
    

    UnifiedOrderResponseExt.java 统一下单返回参数-非必填项

    public class UnifiedOrderResponseExt extends UnifiedOrderResponse {
    
        // 返回信息,非空则表示返回了错误信息
        private String return_msg;
    
        // 设备号
        private String device_info;
    
        // 错误代码
        private String err_code;
    
        // 错误代码描述
        private String err_code_des;
    
        // 二维码连接,trade_type=NATIVE时返回
        private String code_url;
    }
    

    定义一个<img>标签用于显示二维码

    • 调用统一下单API成功后,会返回一系列XML数据,其中code_url表示返回的预支付交易链接,可将其生成二维码图片
    <div class="order-pay-panel order-wechat-panel">
        <div class="modal-header">
            <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
            <h4 class="modal-title">
                <i class="icon-th-large"></i> 微信支付
            </h4>
        </div>
        <div class="modal-body">
            <div class="wechat-qrcode-panel margin-bottom-10">
                <img src="${ctx}/api/order/pay/wechat/qrcode?orderId=${order.hexId}">
            </div>
            <div class="wechat-description-panel">
                <p class="text-muted">使用微信扫描二维码完成支付</p>
                <p class="text-danger">¥${order.price}</p>
            </div>
        </div>
    </div>
    

    根据统一下单API的要求生成订单

    • 将系统内部订单号传入请求参数的out_trade_no中,用于后续操作的唯一标识符
    • 请求参数中的sign是验证参数合法性的唯一标识,需要根据 微信支付签名算法 来生成
    • 使用XStream将对象转换为XML,由于微信的请求参数中大量使用下划线,但下划线在XStream中是关键字,因此需要把下划线转换为双下划线,避免报错
    private String generateOrderInfo(Long orderId) throws Exception {
        // 获取订单信息
        OrderDTO order = orderManageService.getOrder(orderId);
    
        // 生成订单
        UnifiedOrderRequestExt ext = new UnifiedOrderRequestExt();
        ext.setAppid(SOPConstants.WECHAT_PAY_APP_ID);
        ext.setMch_id(SOPConstants.WECHAT_PAY_MCH_ID);
        ext.setBody("轻实训-" + order.getName());
        ext.setOut_trade_no(order.getCode());
        ext.setTotal_fee(order.getPrice() * 100);
        ext.setSpbill_create_ip(super.getClientIP());
        ext.setNotify_url(SOPConstants.WECHAT_PAY_NOTIFY_URL);
        ext.setTrade_type("NATIVE");
        ext.setProduct_id(order.getHexId());
    
      // 生成32位随机数
        ext.setNonce_str(makeNonceStr());
    
      // 签名,按照指定签名算法生成
        ext.setSign(makeSign(ext));
    
        // 格式转换为XML
        XStream xStream = new XStream(new XppDriver(new XmlFriendlyReplacer("_-", "_")));
        xStream.alias("xml", UnifiedOrderRequestExt.class);
    
        return xStream.toXML(ext);
    }
    
    • 生成32位随机数,方式为当前时间加随机数
    private String makeNonceStr() {
        StringBuffer str = new StringBuffer(DateUtil.getSysDateString("yyyyMMddHHmmssS"));
        str.append((new Random().nextInt(900) + 100));
    
        return str.toString();
    }
    
    • 拼接签名数据
    private String makeSign(UnifiedOrderRequestExt ext) throws Exception {
        // 根据规则创建可排序的map集合
        SortedMap<String, String> signMaps = Maps.newTreeMap();
        signMaps.put("appid", ext.getAppid());
        signMaps.put("body", ext.getBody());
        signMaps.put("mch_id", ext.getMch_id());
        signMaps.put("nonce_str", ext.getNonce_str());
        signMaps.put("notify_url", ext.getNotify_url());
        signMaps.put("out_trade_no", ext.getOut_trade_no());
        signMaps.put("spbill_create_ip", ext.getSpbill_create_ip());
        signMaps.put("trade_type", ext.getTrade_type());
        signMaps.put("total_fee", ext.getTotal_fee().toString());
        signMaps.put("product_id", ext.getProduct_id());
    
        // 生成签名
        return generateSign(signMaps);
    }
    
    • 按照签名算法生成签名
    private String generateSign(SortedMap<String, String> signMaps) throws Exception {
        StringBuffer sb = new StringBuffer();
    
        // 字典序
        for (Map.Entry signMap : signMaps.entrySet()) {
            String key = (String) signMap.getKey();
            String value = (String) signMap.getValue();
    
            // 为空不参与签名、参数名区分大小写
            if (null != value && !"".equals(value) && !"sign".equals(key) && !"key".equals(key)) {
                sb.append(key + "=" + value + "&");
            }
        }
    
        // 拼接key
        sb.append("key=" + SOPConstants.WECHAT_PAY_KEY);
    
        // MD5加密
        return encoderByMd5(sb.toString()).toUpperCase();
    }
    

    调用统一下单API

    • 将生成的订单发送给微信,同时接收微信的返回参数,读取其中的code_url
    • 如果发送的订单信息不符合要求,则会在返回参数中告知问题
    • 订单合法,返回参数中return_code=SUCCESS return_msg=OK result_code=SUCCESS
    • 订单不合法,返回参数中return_code=FAIL return_msg=具体错误原因
    private String sendHttpRequest(String orderInfo) throws IOException {
        // 建立连接
        HttpURLConnection conn = (HttpURLConnection) new URL(SOPConstants.WECHAT_PAY_SEND_URL).openConnection();
        conn.setRequestMethod("POST");
        conn.setDoOutput(true);
    
        // 发送数据
        BufferedOutputStream bos = new BufferedOutputStream(conn.getOutputStream());
        bos.write(orderInfo.getBytes());
        bos.flush();
        bos.close();
    
        // 获取数据
        BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
    
        // 接收数据
        String line;
        StringBuffer str = new StringBuffer();
        while ((line = reader.readLine()) != null) {
            str.append(line);
        }
    
        // XML数据转换为对象
        XStream xStream = new XStream(new XppDriver(new XmlFriendlyReplacer("_-", "_")));
        xStream.alias("xml", UnifiedOrderResponseExt.class);
    
        UnifiedOrderResponseExt ext = (UnifiedOrderResponseExt) xStream.fromXML(str.toString());
    
        // 判断数据有效性
        if (null != ext && "SUCCESS".equals(ext.getReturn_code()) && "SUCCESS".equals(ext.getResult_code())) {
            return ext.getCode_url();
        }
    
        return null;
    }
    
    image

    根据返回的code_url生成二维码图片

    @RequestMapping(value = “/wechat/qrcode”, method = RequestMethod.GET)
    public void wechatQRCode(HttpServletResponse response, @RequestParam("orderId") String orderId) {
        try {
            // 初始化数据
            int width = 240;
            int height = 240;
            String format = "png";
      // 获取二维码链接
            String codeUrl = orderPayService.getQRCodeUrl(IdEncoder.decodeId(orderId));
    
            Hashtable htable = new Hashtable();
            htable.put(EncodeHintType.CHARACTER_SET, "UTF-8");
    
            // 生成图片
            BitMatrix matrix = new MultiFormatWriter().encode(codeUrl, BarcodeFormat.QR_CODE, width, height, htable);
    
            OutputStream out = response.getOutputStream();
    
            // 输出图片
            MatrixToImageWriter.writeToStream(matrix, format, out);
    
            out.flush();
            out.close();
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
    }
    
    • 需要准备的包信息
    <dependency>
        <groupId>com.google.zxing</groupId>
        <artifactId>core</artifactId>
        <version>3.3.0</version>
    </dependency>
    <dependency>
        <groupId>com.google.zxing</groupId>
        <artifactId>javase</artifactId>
        <version>3.3.0</version>
    </dependency>
    

    接收回调

    • 用户通过微信扫描二维码并支付成功后,微信会根据之前订单中的notify_url回调地址进行回执
    • 此处提供给微信的回调地址必须是外网可访问的,否则无法正常接收回执信息
    • 由于回执时并没有携带用户信息,所以如果使用了诸如shiro等安全框架的,需要给予该回执地址一个访问许可,否则会被安全框架屏蔽
    • 发送回执是异步进行,由于网络等不确定因素,微信不保证回执一定成功
    • 微信会通过一定的策略定期重启发送通知,通知频率为15/15/30/180/1800/1800/1800/1800/3600,单位:秒
    • 虽然是异步回执,但并不需要采用ajax异步接收的方式来接收数据
    @RequestMapping(value = "/wechat")
    public String wechatNotify(HttpServletResponse response, HttpServletRequest request) throws Exception {
        orderPayService.notify(response, request);
    
      // 此处的返回值无效,需要在支付页面通过轮询获取支付结果,微信支付本身无法实现自动跳转
        return "/redirect:/login";
    }
    

    处理回执内容

    • 数据是通过IO流发送,所以也需要通过IO流接收
    • 微信发送回执用户接收后,需要通过IO流的方式告知微信接收成功,否则微信认为回执失败
    • 接收到回执信息后,最关键是验证签名来确保信息的有效性和安全性,验签的方式和发送订单签名的方式一致
    • 验签成功,且回执信息中result_code=SUCCESS,则表示回执信息有效
    • 从回执信息中可获取到out_trade_no,这是之前发送的用户订单唯一标识符,通过该信息可以继续处理用户订单
    • 所有流程处理完毕后,必须以XML格式编写回执信息,并通过IO流的方式告知微信回执接收成功
    public void notify(HttpServletResponse response, HttpServletRequest request) throws Exception {
        // 读取回执数据
        HashMap<String, String> notifyMaps = readNotify(request);
    
        // 回执数据验证
        if (notifyMaps == null || notifyMaps.isEmpty()) {
            logger.error("未收到回执数据!");
            throw new TSharkException("未收到回执数据!");
        }
    
        // 挑选数据
        SortedMap<String, String> notifySorts = sortNotify(notifyMaps);
    
        // 重新签名
        String sign = generateSign(notifySorts);
        // 获取回执签名
        String notifySign = notifySorts.get("sign").toUpperCase();
    
        // 验证签名
        if (!sign.equals(notifySign)) {
            logger.error("签名验证失败!");
            throw new TSharkException("签名验证失败!");
        }
    
        String resXml;
    
        // 验证回执
        if ("SUCCESS".equals(notifySorts.get("result_code"))) {
            // 更新订单信息
            updateOrderInfo(notifySorts.get("out_trade_no"));
    
            // 微信回执
            resXml = "<xml>" +
                    "<return_code><![CDATA[SUCCESS]]></return_code>" +
                    "<return_msg><![CDATA[OK]]></return_msg>" +
                    "</xml> ";
        } else {
            resXml = "<xml>" +
                    "<return_code><![CDATA[FAIL]]></return_code>" +
                    "<return_msg><![CDATA[报文为空]]></return_msg>" +
                    "</xml> ";
        }
    
        BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
        out.write(resXml.getBytes());
        out.flush();
        out.close();
    }
    
    • 从IO流中读取回执信息
    private HashMap<String, String> readNotify(HttpServletRequest request) throws Exception {
        // 读取参数
        InputStream inputStream = request.getInputStream();
        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
    
        // 接收数据
        String line;
        StringBuffer str = new StringBuffer();
        while ((line = reader.readLine()) != null) {
            str.append(line);
        }
    
        reader.close();
        inputStream.close();
    
        // XML转换Map
        return fromXml(str.toString());
    }
    
    • 读取的回执信息时XML格式,需要通过jDom的SAXBuilder解析为Map
    private HashMap<String, String> fromXml(String xml) throws Exception {
        xml = xml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");
    
        if (null == xml || "".equals(xml)) {
            return null;
        }
    
        HashMap<String, String> m = Maps.newHashMap();
    
        InputStream in = new ByteArrayInputStream(xml.getBytes("UTF-8"));
    
        SAXBuilder builder = new SAXBuilder();
    
        Document doc = builder.build(in);
    
        Element root = doc.getRootElement();
    
        List list = root.getChildren();
    
        Iterator it = list.iterator();
    
        while (it.hasNext()) {
            Element e = (Element) it.next();
    
            String k = e.getName();
            String v = "";
            List children = e.getChildren();
    
            if (children.isEmpty()) {
                v = e.getTextNormalize();
            } else {
                v = getXmlChildren(children);
            }
    
            m.put(k, v);
        }
    
        //关闭流
        in.close();
    
        return m;
    }
    
    private String getXmlChildren(List children) {
        StringBuffer sb = new StringBuffer();
    
        if (!children.isEmpty()) {
            Iterator it = children.iterator();
    
            while (it.hasNext()) {
                Element e = (Element) it.next();
    
                String name = e.getName();
                String value = e.getTextNormalize();
    
                List list = e.getChildren();
    
                sb.append("<" + name + ">");
    
                if (!list.isEmpty()) {
                    sb.append(getXmlChildren(list));
                }
    
                sb.append(value);
                sb.append("</" + name + ">");
            }
        }
    
        return sb.toString();
    }
    

    轮询订单状态,实现支付完成后页面自动跳转

    • 由于支付回执是异步的,所以即使捕获到异步回执也无法实现支付页面的自动跳转
    • 所以需要在支付页面打开时设置一个ajax轮询订单状态,一旦订单状态更新,则进行页面跳转
    // 页面关闭
    $(".modal-header button.close:last").click(function () {
        // 停止轮询
        clearInterval(checkTimer);
    });
    
    // 轮询订单状态
    checkTimer = setInterval(function () {
        // 窗口是否打开
        if ($(".order-pay-panel").length <= 0) {
            clearInterval(checkTimer);
        }
    
        // 获取订单状态
        $.ts.doAction("/api/order/review/check/", {
            orderId: orderId
        }, function () {
            // 订单已支付
            if (!this.data) {
                $.ts.closeWindow();
    
                g_index.loadMainContentWithState("/order/manage");
    
          $.ts.toastr.success("订单已支付成功!");
            }
        }, "", "", "");
    }, 3000);
    

    相关文章

      网友评论

          本文标题:Java微信扫码支付

          本文链接:https://www.haomeiwen.com/subject/raoptqtx.html