微信支付之Native扫码支付功能

作者:陈惠,叩丁狼教育高级讲师。原创文章,转载请注明出处。

上一篇微信支付文章:https://www.jianshu.com/p/9c322b1a5274
实现了微信公众号内H5页面进行支付的功能,但是这种方式的缺点就是必须在微信中打开付款页面才能实现,所以并不适合所有的场景。那么本篇文章,会以另外一种方式实现,使用扫码的方式来进行支付。

需要注意的是,扫码支付分两种形式:

线下的扫码支付:
这种方式非常简单,商户在微信申请付款二维码贴纸,贴到收银台附近位置,客户购买商品直接使用手机扫码二维码,输入付款金额即可支付,这种方式不需要编程人员,大大减少商户成本,比如常见的便利店,商场线下店等等。

叩丁狼教育.png

线上的扫码支付:
这是本文选择实现的方式,也叫做Native支付,这种方式适合PC端的网站,比如大众点评、携程、优酷等,如果使用的是电脑来访问网页,并需要支付相关费用,比如商品付款,充值VIP这些都是比较常见的场景,很多网站都会选用这种方式,总之最后付款的那一刻,就在网页上展示付款码,让用户去扫并付款即可,因为这种方式不是面对面付款,所以必须要保证客户付款的金额是准确的,所以这个二维码不是固定的,是根据账单金额生成的,用户扫码之后就可以马上看到需要付款的金额,确认无误再进行付款。

叩丁狼教育.png

本文使用的是线上的扫码支付方式,框架使用SpringMVC来实现。

准备工作

一.注册商户号
到微信支付商户平台https://pay.weixin.qq.com
提交商家相关资料,注册一个商户账号,并开通Native支付功能。

叩丁狼教育.png

二.绑定公众号或小程序
目的是要得到一个授权的APPID。

叩丁狼教育.png

三.设置API密钥,登录商户平台——>账户中心——>API安全——>API密钥
该密钥在后面的代码中计算支付签名的时候需要使用到。

叩丁狼教育.png

Native支付实现模式

官方提供了两种模式实现,第一种模式比较复杂,还需要自己处理二维码地址相关信息,第二种比较简单,微信可以直接把生成好的二维码地址返回给我们,所以我们使用第二种实现。
具体区别可参考:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_3

实现的大致流程可参考官方提供的时序图:

叩丁狼教育.png

但是流程有很多,不一一演示,我们选取核心的部分来实现即可。

开发流程

文中的例子为:开通叩丁狼VIP会员服务,并实现扫码支付。

一.准备一个可以触发下单操作的页面

叩丁狼教育.png

二.点击"开通VIP"按钮后进入controller的方法,接收商品参数并调用微信支付统一下单接口
正常的业务流程是在该方法中,获取商品id,再通过id去查询数据库该商品的相关属性,比如名称,价格等等,然后再创建应用自身的业务订单,再去调用微信支付的统一下单接口(让微信生成预支付单,后续才可以进行支付),但此处重点在支付流程,商品的属性值和订单相关值,暂且先使用假数据。

接口以及参数可参考微信官方提供的统一下单文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1

根据文档介绍,使用Native方式调用统一下单接口时需要带上相关必填的参数如下:

叩丁狼教育.png

代码如下:
把必填的参数封装成对应的实体类:

/**
 * 微信统一下单实体类
 */
@Setter
@Getter
@XmlRootElement(name = "xml")
@XmlAccessorType(XmlAccessType.FIELD)
public class WxOrderEntity {
  private String appid;
    private String body;
    private String device_info;
    private String mch_id;
    private String nonce_str;
    private String sign;
    private String out_trade_no;
    private int total_fee;
    private String trade_type;
    private String spbill_create_ip;
    private String openid;
    private String notify_url;
    private Long product_id;
}

调用接口成功后返回的结果也封装成实体类:

/**
 * 微信统一下单返回结果实体类
 */
@Setter
@Getter
@XmlRootElement(name = "xml")
@XmlAccessorType(XmlAccessType.FIELD)
public class WxOrderResultEntity {
    private String return_code;
    private String return_msg;
    private String result_code;
    private String appid;
    private String nonce_str;
    private String sign;
    private String trade_type;
    private String prepay_id;
    private String code_url;
}

该结果中最重要的是code_url参数,在生成付款二维码时需要用到。

叩丁狼教育.png

注意:下单的业务逻辑,正常是需要抽取到业务层的,但是此处为了方便阅读代码,直接写到了控制器上。

@Controller
public class OrderController {

    @RequestMapping("order")
    public String save(Long productId, Model model, HttpServletRequest request) throws Exception {
        //根据商品id查询商品详细信息(假数据)
        double price = 0.01;//(0.01元)
        String productName = "叩丁狼VIP会员";
        //生成订单编号
        int number = (int)((Math.random()*9)*1000);//随机数
        DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");//时间
        String orderNumber = dateFormat.format(new Date()) + number;
        //准备调用接口需要的参数
        WxOrderEntity order = new WxOrderEntity();
        //appid
        order.setAppid(WeChatUtil.APPID);
        //商户号
        order.setMch_id(WeChatUtil.MCH_ID);
        //商品描述
        order.setBody(productName);
        //交易类型
        order.setTrade_type("NATIVE");
        //商户订单号
        order.setOut_trade_no(orderNumber);
        //支付金额(单位:分)
        order.setTotal_fee((int)(price*100));
        //用户ip地址
        order.setSpbill_create_ip(RequestUtil.getIPAddress(request));
        //设置商品id
        order.setProduct_id(productId);
        //接收支付结果的地址
        order.setNotify_url("http:/www.xxxx.com/receive.do");
        //32位随机数(UUID去掉-就是32位的)
        String uuid = UUID.randomUUID().toString().replace("-", "");
        order.setNonce_str(uuid);
        //生成签名
        String sign = WeChatUtil.getPaySign(order);
        order.setSign(sign);
        //调用微信支付统一下单接口,让微信也生成一个预支付订单
        String xmlResult = HttpUtil.post(GET_PAY_URL, XMLUtil.toXmlString(order));
        //把返回的xml字符串转成对象
        WxOrderResultEntity entity = XMLUtil.toObject(xmlResult,WxOrderResultEntity.class);
        //微信预支付单成功创建
        if(entity.getReturn_code().equals("SUCCESS")&&entity.getResult_code().equals("SUCCESS")){
            //使用二维码生成工具,把微信返回的codeUrl转为二维码图片,保存到磁盘
            String codeUrl = entity.getCode_url();
            //使用订单号来作为二维码的图片名称
            File file = new File(QRCodeUtil.PAY_PATH,orderNumber+".jpg");
            QRCodeUtil.createImage(codeUrl,new FileOutputStream(file));
            //把订单号传到支付页面
            model.addAttribute("orderNumber",orderNumber);
        }
        //跳转到支付页进行支付
        return  "pay";
    }
}

统一下单接口调用成功后,微信会创建一个预支付单,此时即为未付款状态:

叩丁狼教育.png

同时,微信还会返回codeUrl给我们,这个就是二维码的链接,我们需要利用工具把该链接转为二维码

叩丁狼教育.png

二维码生成工具,我使用的是google的Zxing
maven依赖:

 <dependency>
      <groupId>com.google.zxing</groupId>
      <artifactId>core</artifactId>
      <version>3.3.3</version>
    </dependency>

工具类:

public class QRCodeUtil {

    // 二维码尺寸
    public static final int QRCODE_SIZE = 300;

    // 存放二维码的路径
    public static final String PAY_PATH = "c://pay";

    /**
     * 生成二维码
     * @param content   源内容
     * @param outputStream 输出流
     * @throws Exception
     */
    public static void createImage(String content, OutputStream outputStream) throws Exception {
        Hashtable hints = new Hashtable();
        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
        hints.put(EncodeHintType.CHARACTER_SET, "utf-8");
        hints.put(EncodeHintType.MARGIN, 1);
        BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, QRCODE_SIZE, QRCODE_SIZE,
                hints);
        int width = bitMatrix.getWidth();
        int height = bitMatrix.getHeight();
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                image.setRGB(x, y, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF);
            }
        }
        // 存到磁盘
        ImageIO.write(image, "jpg", outputStream);
    }

}

生成二维码网上也有很多工具,有的人还会在二维码中间加入logo,自己去找就可以啦。

下面是刚才统一下单的接口使用到的签名算法代码。

官方文档参考:
pay签名:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3

 
    /**
     * 计算微信支付的签名
     * @param obj
     * @return
     * @throws IllegalAccessException
     */
    public static String getPaySign(Object obj) throws IllegalAccessException, IOException {
        StringBuilder sb = new StringBuilder();
        //把对象转为TreeMap集合(按照key的ASCII 码从小到大排序)
        TreeMap<String, Object> map;
        if(!(obj instanceof Map)) {
            map = ObjectUtils.objectToMap(obj);
        }else{
            map = (TreeMap)obj;
        }
        Set<Map.Entry<String, Object>> entrySet = map.entrySet();
        //遍历键值对
        for (Map.Entry<String, Object> entry : entrySet) {
            //如果值为空,不参与签名
            if(entry.getValue()!=null) {
                //格式key1=value1&key2=value2…
                sb.append(entry.getKey() + "=" + entry.getValue() + "&");
            }
        }
        //最后拼接商户的API密钥
        String stringSignTemp = sb.toString()+"key="+WeChatUtil.KEY;
        //进行md5加密并转为大写
        return SecurityUtil.MD5(stringSignTemp).toUpperCase();
    }

三.支付页面,展示付款二维码
效果如下:

叩丁狼教育.png

页面代码:

<div class="mod_layer_wxopen" style="display:block;">
    <iframe frameborder="0" class="iframe_mask"></iframe>
    <div class="ly_content">
        <div class="ly_bd cf">
            <div class="ly_ct">
                <div class="qr_list">
                    <h3 class="qr_tit">正在给微信帐号<span class="user js_wx_name">H</span><span class="js_txt">开通</span>VIP会员</h3>
                    <div class="qr_pic">
                        <img id="qr_img"  src="/getQrCode.do?orderNumber=${orderNumber}">
                        <span class="icon_wx"></span>
                    </div>
                    <p class="qr_txt js_qr_txt">使用微信扫描二维码</p>
                    <p class="qr_tips">请使用微信扫码支付</p>
                </div>
            </div>
        </div>
    </div>
</div>

重点的是img标签,二维码图片是经过controller去找的。

获取付款二维码:

    @RequestMapping("getQrCode")
    public void getQrCode(String orderNumber,HttpServletResponse response) throws IOException {
        //从磁盘中获取付款二维码并输出给response
        File file = new File(QRCodeUtil.PAY_PATH,orderNumber+".jpg");
        if(file.exists()){
            IOUtils.copy(new FileInputStream(file),response.getOutputStream());
        }
    }

有了付款二维码,客户就可以使用微信扫码,并支付了。

四.支付结果的处理
当用户支付后,微信会把支付结果发送到我们之前指定的notify_url地址,我们可以根据支付结果来做相关的业务逻辑

把微信支付通知结果封装成实体类

@Setter
@Getter
@XmlRootElement(name="xml")
@XmlAccessorType(XmlAccessType.FIELD)
public class WxPayResultEntity {
    private String return_code;
    private String result_code;
    private String return_msg;
    private String transaction_id;//微信支付订单号
    private String out_trade_no;//商户订单号
    private String total_fee;//订单金额
    private String cash_fee;//现金支付金额
}

具体支付通知结果的参数可参考官方文章:
https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_7&index=8

接收支付结果并处理业务:

    @RequestMapping("receive")
    @ResponseBody
    public BaseResult receive(@RequestBody WxPayResultEntity result) throws IOException {
        //判断是否支付成功
        if(result.getReturn_code().equals("SUCCESS")&&result.getResult_code().equals("SUCCESS")){
            //避免结果出现差异,安全起见,会再调用预支付订单查询的接口,检查该订单的状态是否是已支付
            //代码省略
            //.....
        }
        //通知微信我们收到了,如果微信没有收到回复,会间隔一段时间又通知一遍,这样的话容易出现业务重复处理操作
        BaseResult resp = new BaseResult();
        resp.setReturn_code("SUCCESS");
        resp.setReturn_msg("OK");
        return resp;
    }

主要逻辑是:
判断微信返回的支付结果是否支付成功,如果是支付成功,还应调用查询预支付订单的接口,再次确认支付结果,如果确认无误,我们就可以执行支付成功的业务逻辑,比如设置业务订单状态为已付款,商品发货,或者设置为VIP会员等等,最后需要给微信返回应答,通知微信我们收到并处理了这个结果,如果微信没有收到我们的回复,会间隔一段时间又再次通知一遍,这样的话容易出现业务重复处理的问题,可能导致商家资金损失。

查询预支付订单参考:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_2
处理支付结果参考:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_7&index=8

五.页面轮询检查支付结果
为了用户体验更好,页面需要定时去获取支付的结果,及时跳转页面或者显示支付结果给客户。

页面带上订单号轮询检查:

  <script type="text/javascript">
        //每两秒检查一次
        setInterval(function() {
            $.get("/checkOrder.do?orderNumber=${orderNumber}",function(data) {
                if (data.success) {
                   alert("支付成功!");
                }
            });
        },2000)
  </script>

后台查询该订单的支付状态:
在该方法中,应查询数据库,检查该业务订单是否为已支付的状态,如果是,返回success:true,页面接收到结果,即可马上提示支付成功或者跳转到我的订单页面之类的业务处理。

    @RequestMapping("checkOrder")
    @ResponseBody
    public JSONObject checkOrder(String orderNumber) throws IOException {
        //检查订单状态,确认已支付,返回success:true
        //逻辑省略....
        JSONObject json = new JSONObject();
        json.put("success",true);
        return json;
    }

想获取更多技术干货,请前往叩丁狼官网:http://www.wolfcode.cn/all_article.html

猜你喜欢

转载自blog.csdn.net/wolfcode_cn/article/details/84950406