微信支付之Native扫码支付功能
作者:陈惠,叩丁狼教育高级讲师。原创文章,转载请注明出处。
上一篇微信支付文章:https://www.jianshu.com/p/9c322b1a5274
实现了微信公众号内H5页面进行支付的功能,但是这种方式的缺点就是必须在微信中打开付款页面才能实现,所以并不适合所有的场景。那么本篇文章,会以另外一种方式实现,使用扫码的方式来进行支付。
需要注意的是,扫码支付分两种形式:
线下的扫码支付:
这种方式非常简单,商户在微信申请付款二维码贴纸,贴到收银台附近位置,客户购买商品直接使用手机扫码二维码,输入付款金额即可支付,这种方式不需要编程人员,大大减少商户成本,比如常见的便利店,商场线下店等等。
线上的扫码支付:
这是本文选择实现的方式,也叫做Native支付,这种方式适合PC端的网站,比如大众点评、携程、优酷等,如果使用的是电脑来访问网页,并需要支付相关费用,比如商品付款,充值VIP这些都是比较常见的场景,很多网站都会选用这种方式,总之最后付款的那一刻,就在网页上展示付款码,让用户去扫并付款即可,因为这种方式不是面对面付款,所以必须要保证客户付款的金额是准确的,所以这个二维码不是固定的,是根据账单金额生成的,用户扫码之后就可以马上看到需要付款的金额,确认无误再进行付款。
本文使用的是线上的扫码支付方式,框架使用SpringMVC来实现。
准备工作
一.注册商户号
到微信支付商户平台https://pay.weixin.qq.com
提交商家相关资料,注册一个商户账号,并开通Native支付功能。
二.绑定公众号或小程序
目的是要得到一个授权的APPID。
三.设置API密钥,登录商户平台——>账户中心——>API安全——>API密钥
该密钥在后面的代码中计算支付签名的时候需要使用到。
Native支付实现模式
官方提供了两种模式实现,第一种模式比较复杂,还需要自己处理二维码地址相关信息,第二种比较简单,微信可以直接把生成好的二维码地址返回给我们,所以我们使用第二种实现。
具体区别可参考:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_3
实现的大致流程可参考官方提供的时序图:
叩丁狼教育.png
但是流程有很多,不一一演示,我们选取核心的部分来实现即可。
开发流程
文中的例子为:开通叩丁狼VIP会员服务,并实现扫码支付。
一.准备一个可以触发下单操作的页面
二.点击"开通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();
}
三.支付页面,展示付款二维码
效果如下:
页面代码:
<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