开发准备
参考文档 JSAPI支付开发文档
支付方式
目前微信主流的支付方式有以下6种
方式 | 说明 |
---|---|
付款码支付 | 付款码支付是用户展示微信钱包内的“刷卡条码/二维码”给商户系统扫描后直接完成支付的模式。主要应用线下面对面收银的场景。 |
Native支付 | Native支付是商户系统按微信支付协议生成支付二维码,用户再用微信“扫一扫”完成支付的模式。该模式适用于PC网站支付、实体店单品或订单支付、媒体广告支付等场景。 |
JSAPI支付 | JSAPI支付是用户在微信中打开商户的H5页面,商户在H5页面通过调用微信支付提供的JSAPI接口调起微信支付模块完成支付。 |
APP支付 | APP支付又称移动端支付,是商户通过在移动端应用APP中集成开放SDK调起微信支付模块完成支付的模式。 |
H5支付 | H5支付主要是在手机、ipad等移动设备中通过浏览器来唤起微信支付的支付产品。 |
小程序支付 | 小程序支付是专门被定义使用在小程序中的支付产品。目前在小程序中能且只能使用小程序支付的方式来唤起微信支付。 |
因为前面做过关于公众号的文章,因此这里主要介绍JSAPI支付,后面的开发等也围绕于此。
JSAPI应用场景有:
- 用户在微信公众账号内进入商家公众号,打开某个主页面,完成支付
- 用户的好友在朋友圈、聊天窗口等分享商家页面连接,用户点击链接打开商家页面,完成支付
- 将商户页面转换成二维码,用户扫描二维码后在微信浏览器中打开页面后完成支付
核心名词
不同于微信公众号的测试开发,可以使用内网穿透,和普通的测试账号等。微信支付要求开发者,必须要有一个已通过验证的真实商户号,且该商户号开通支付功能,以及该商户下有真实的公众号等。
-
【微信商户平台】
微信商户平台是微信支付相关的商户功能集合,包括参数配置、支付数据查询与统计、在线退款、代金券或立减优惠运营等功能
平台入口:http://pay.weixin.qq.com。 -
【微信公众平台】
微信公众平台是微信公众账号申请入口和管理后台。商户可以在公众平台提交基本资料、业务资料、财务资料申请开通微信支付功能。
平台入口:http://mp.weixin.qq.com。 -
【微信支付系统】
微信支付系统是指完成微信支付流程中涉及的API接口、后台业务处理系统、账务系统、回调通知等系统的总称。 -
【商户证书】
商户证书是微信提供的二进制文件,商户系统发起与微信支付后台服务器通信请求的时候,作为微信支付后台识别商户真实身份的凭据。 -
【商户后台系统】
商户后台系统是商户后台处理业务系统的总称,例如:商户网站、收银系统、进销存系统、发货系统、客服系统等,一般关联开发者自己的数据库。 -
【签名】
商户后台和微信支付后台根据相同的密钥和算法生成一个结果,用于校验双方身份合法性。签名的算法由微信支付制定并公开,常用的签名方式有:MD5、SHA1、SHA256、HMAC等。 -
【支付密码】
支付密码是用户开通微信支付时单独设置的密码,用于确认支付完成交易授权。该密码与微信登录密码不同。 -
【Openid】
用户在公众号内的身份标识,不同公众号拥有不同的openid。商户后台系统通过登录授权、支付通知、查询订单等API可获取到用户的openid。主要用途是判断同一个用户,对用户发送客服消息、模版消息等。
申请的核心账户参数:
账户参数说明
邮件中参数 | API参数名 | 详细说明 |
---|---|---|
APPID | appid | appid是微信公众账号或开放平台APP的唯一标识,在公众平台申请公众账号或者在开放平台申请APP账号后,微信会自动分配对应的appid,用于标识该应用。可在微信公众平台–>开发–>基本配置里面查看,商户的微信支付审核通过邮件中也会包含该字段值。 |
微信支付商户号 | mch_id | 商户申请微信支付后,由微信支付分配的商户收款账号。 |
API密钥 | key | 交易过程生成签名的密钥,仅保留在商户系统和微信支付后台,不会在网络中传播。商户妥善保管该Key,切勿在网络中传输,不能在其他客户端中存储,保证key不会被泄漏。商户可根据邮件提示登录微信商户平台进行设置。也可按以下路径设置:微信商户平台(pay.weixin.qq.com)–>账户中心–>账户设置–>API安全–>密钥设置 |
Appsecret | secret | AppSecret是APPID对应的接口密码,用于获取接口调用凭证access_token时使用。在微信支付中,先通过OAuth2.0接口获取用户openid,此openid用于微信内网页支付模式下单接口使用。可登录公众平台–>微信支付,获取AppSecret(需成为开发者且帐号没有异常状态)。 |
协议规则
商户接入微信支付,调用API必须遵循以下规则:
传输方式 | 为保证交易安全性,采用HTTPS传输 |
---|---|
提交方式 | 采用POST方法提交 |
数据格式 | 提交和返回数据都为XML格式,根节点名为xml |
字符编码 | 统一采用UTF-8字符编码 |
签名算法 | MD5/HMAC-SHA256 |
签名要求 | 请求和接收数据均需要校验签名,详细方法请参考安全规范-签名算法 |
证书要求 | 调用申请退款、撤销订单、红包接口等需要商户api证书,各api接口文档均有说明。 |
判断逻辑 | 先判断协议字段返回,再判断业务返回,最后判断交易状态 |
开发中代码配置的参数(实际开发中建议直接在属性文件中配置,便于环境切换)
// 公众号、小程序appid
public static String APP_ID = "xxxxxxxxx";
// AppSecret
public static String SECRET = "xxxxxxxxx";
// 商户号
public static final String MCH_ID = "xxxxxxxxx";
// API密钥
public static final String API_KEY = "xxxxxxxxx";
// 网页授权域名,JSAPI支付授权目录,JS接口安全域名
public static final String AUTH_URL = "xxxxxxxxx";
以上参数不便公开。如果公司有现成的支付账户最好,没有的话恐怕只能在某宝租用一下了,但没有这些不影响前期的业务开发。
业务梳理
业务流程时序图
对于开发者来说,发起支付的过程中,
后端:主要调用了JSAPI支付中的三个接口:【统一下单API】、【支付结果通知API】、【查询订单API】
前端:
前端微信内H5调起支付,提供用户触发微信支付的button和JSON数据传输。
开始开发
项目搭建
一、采用SpringBoot+Thymeleaf结构,参考微信公众号快速开发(二)项目搭建与被动回复
二、引入官方SDK工具包
阅读文档后发现,对于xml解析,加密算法等其实都时常用的方法,微信为我们直接提供了常用工具类方法的半成品,注意,这些只能是半成品,使用时需要做适当的更改。
链接:SDK与DEMO下载,选择JAVA版本下载后解压即可
代码开发
公众号配置
一、将公众号和商户的信息注入到Bean中
@Component
public class WXPayConfigExtend extends WXPayConfig {private byte[] certData;private WXPayConfigExtend() throws Exception {
// String certPath = WXPayConstants.APICLIENT_CERT;
// File file = new File(certPath);
// InputStream certStream = new FileInputStream(file);
// this.certData = new byte[(int) file.length()];
// certStream.read(this.certData);
// certStream.close();}@Overridepublic String getAppID() {return WXPayConstants.APP_ID;}@Overridepublic String getMchID() {return WXPayConstants.MCH_ID;}@Overridepublic String getKey() {return WXPayConstants.API_KEY;}@Overridepublic InputStream getCertStream() {ByteArrayInputStream certBis = new ByteArrayInputStream(this.certData);return certBis;}@Overridepublic int getHttpConnectTimeoutMs() {return 2000;}@Overridepublic int getHttpReadTimeoutMs() {return 10000;}@Overridepublic IWXPayDomain getWXPayDomain() {return WXPayDomainSimpleImpl.instance();}public String getPrimaryDomain() {return "api.mch.weixin.qq.com";}public String getAlternateDomain() {return "api2.mch.weixin.qq.com";}@Overridepublic int getReportWorkerNum() {return 1;}@Overridepublic int getReportBatchSize() {return 2;}
}
获取openid
需页面提供网页授权,以获取openid,关于微信网页授权可参考:微信公众号快速开发(四)微信网页授权
页面:
页面这里直接设计了一个可以发起预支付的按钮的静态页面:templates/preOrder.html
里面包含了跳转到后端支付接口的表单:
<form name=wexinpayment action='http://chety.mynatapp.cc/api/v1/wechat1/placeOrder' method=post target="_blank">...
Thymeleaf下页面转发的控制器:
@Controller
@RequestMapping("/api/v1/wechat1")
public class IndexController {// 用于thymeleaf环境下,跳转到字符串相应的html页面@RequestMapping("/{path}")public String webPath(@PathVariable String path) {return path;}
}
网页授权的入口控制器:
@Controller
@RequestMapping("/api/v1/wechat1")
public class IndexController {...@RequestMapping("/index")public void index(String code, Model model, HttpServletRequest request, HttpServletResponse response) throws IOException {// 显式授权,获得codeif (code != null) {JSONObject json = WeChatUtil.getWebAccessToken(code);WXPayUtil.getLogger().info("code: ",json.toJSONString());String openid = json.getString(("openid"));request.getSession().setAttribute("openid", openid);WXPayUtil.getLogger().info("index openid={}",openid);// 重定向到预下单页面response.sendRedirect("preOrder"); // 重定向到预支付页面} else {StringBuffer url = RequestUtil.getRequestURL(request);WXPayUtil.getLogger().info("index 请求路径:{}"+url);String path = WeChatUtil.WEB_REDIRECT_URL.replace("APPID", WeChatConstants.APP_ID).replace("REDIRECT_URI", url).replace("SCOPE", "snsapi_userinfo");WXPayUtil.getLogger().info("index 重定向:{}",path);// 重定向到授权获取code的页面response.sendRedirect(path);}}
}
启动项目,请求接口:
一、 微信开发者工具的地址栏输入:{网页授权域名}//api/v1/wechat1/index
二、确认【同意】授权,(这里目的是为了获取openid,也可以使用base静默授权的模式,不用显示的提示授权),跳转到预支付页面,如图:
发起支付
当用户确认预支付页面的订单时,将请求【/placeOrder】接口,该业务将调用微信的【统一下单】接口:
一、微信统一下单实体类
@Setter
@Getter
@ToString
@XmlRootElement(name = "xml")
@XmlAccessorType(XmlAccessType.FIELD)
public class WxOrderEntity {private String appid;private String mchId;private String deviceInfo;private String nonceStr;private String sign;private String body;private String outTradeNo;private int totalFee;private String spbillCreateIp;private String notifyUrl;private String tradeType;private String openid;
}
二、微信支付的业务层
@Service
public class WxBackendServiceImpl {@AutowiredWXPayConfigExtend wxPayConfigExtend;// 统一下单public Map<String, Object> unifiedorder(Model model, HttpServletRequest request) throws Exception {WXPayUtil.getLogger().info("进入下单控制器...");Map<String,Object> data = null;try {//生成订单编号WXPay wxpay = new WXPay(wxPayConfigExtend);WxOrderEntity order = new WxOrderEntity();double price = 0.01;String orderName = "xxx--微信支付";int number = (int)((Math.random()*9)*1000);//随机数DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");//时间String orderNumber = dateFormat.format(new Date()) + number;String nonceStr = WXPayUtil.generateNonceStr();String openId = (String) request.getSession().getAttribute("openid");openId = openId == null ? "o4036jqo2PN9isV6N2FHGRsGRVqg" : openId; // 前一个openid,是chet在xxx公众号下的openidorder.setBody(orderName);order.setOutTradeNo(orderNumber);order.setTotalFee(MoneyUtil.Yuan2Fen(price));order.setSpbillCreateIp(IpUtils.getIpAddr(request));order.setOpenid(openId);order.setNotifyUrl(WXPayConstants.NOTIFY_URL);order.setTradeType(WXPayConstants.TRADE_TYPE_JSAPI);order.setNonceStr(nonceStr);WXPayUtil.getLogger().info("save 统一下单接口调用,order:{}",order);// 利用sdk统一下单,已自动调用wxpay.fillRequestData(data);Map<String, String> response = wxpay.doWxPayApi(order,WXPayConstants.UNIFIEDORDER);WXPayUtil.getLogger().info("save 下单结果,response:{}",response);if(response.get(WXPayConstants.RETURN_CODE).equals("SUCCESS")&&response.get(WXPayConstants.RESULT_CODE).equals("SUCCESS")){String url = request.getQueryString() == null?request.getRequestURL().toString():request.getRequestURL()+"?"+request.getQueryString();String prepayId = response.get(WXPayConstants.PREPAY_ID);data = wxpay.permissionValidate(nonceStr,url,prepayId,wxPayConfigExtend.getKey());return data;}} catch (Exception e) {WXPayUtil.getLogger().error("doUnifiedOrder--下单失败:{}" , e.getMessage());}return null;}
}
wxpay.doWxPayApi(…)封装了对下单接口的调用:
public Map<String, String> doWxPayApi(WxOrderEntity order,String apiType) {Map<String, String> resp = null;try {Map<String,String> map = new HashMap<>();map.put("out_trade_no", order.getOutTradeNo());map.put("nonce_str", order.getNonceStr());map.put("trade_type", order.getTradeType());if ("unifiedorder".equalsIgnoreCase(apiType)) {map.put("spbill_create_ip", order.getSpbillCreateIp());map.put("openid", order.getOpenid());map.put("notify_url", order.getNotifyUrl());map.put("total_fee", String.valueOf(order.getTotalFee()));map.put("body", order.getBody());resp = unifiedOrder(map);} else if ("orderquery".equalsIgnoreCase(apiType)) {resp = orderQuery(map);} else if ("closeorder".equalsIgnoreCase(apiType)) {resp = orderQuery(map);}} catch (Exception e) {WXPayUtil.getLogger().error(order.getOutTradeNo()+" -- 调用接口失败 {}",e.getMessage());}return resp;
}
wxPay.doWxPayApi(…)封装了对签名的二次校验:
public Map<String, Object> permissionValidate(String nonceStr, String url, String prepayId, String key) throws Exception {//jssdk权限验证参数TreeMap<Object, Object> param = new TreeMap<>();Map<String, Object> data = new HashMap<>();param.put("appId", WeChatConstants.APP_ID);String timestamp = String.valueOf(WXPayUtil.getCurrentTimestamp());param.put("timestamp", timestamp);//全小写param.put("nonceStr", nonceStr);//map.put("signature",WeChatUtil.getSignature(timestamp,uuid,RequestUtil.getUrl(request)));param.put("signature", WeChatUtil.getSignature(timestamp, nonceStr, url));data.put("configMap", param);//微信支付权限验证参数Map<String, String> payMap = new HashMap<>();payMap.put("appId", WeChatConstants.APP_ID);payMap.put("timeStamp", timestamp);//驼峰payMap.put("nonceStr", nonceStr);payMap.put("package", "prepay_id=" + prepayId);payMap.put("signType", "MD5");payMap.put("paySign", WXPayUtil.generateSignature(payMap, key));payMap.put("packageStr", "prepay_id=" + prepayId);data.put("payMap", payMap);return data;
}
支付结果通知与回调
配置回调接口的控制器:
@Controller
@RequestMapping("/api/v1/wechat1")
public class NotifyController {WxBackendServiceImpl wxBackendService;/*** 在调用下单接口时,我们会传入 异步接收微信支付结果通知的回调地址,顾名思义这个地址作用就是用来接收支付结果通知,* 当用户在前端支付成功后,微信服务器会自动调用此地址,然后商户再进行处理* @param request* @param response* @return*/@RequestMapping("/wxnotify")public String wxNotify(HttpServletRequest request, HttpServletResponse response) {String respXml = "";try (InputStream in = request.getInputStream();ByteArrayOutputStream baos = new ByteArrayOutputStream()) {byte[] buffer = new byte[1024];int len = 0;while ((len = in.read(buffer)) != -1) {baos.write(buffer, 0, len);}// 获取微信调用我们notify_url的返回信息String notifyData = new String(baos.toByteArray(), "utf-8");// 回调处理respXml = wxBackendService.payCallBack(notifyData);} catch (Exception e) {WXPayUtil.getLogger().error("wxnotify:支付回调发布异常:", e.getMessage());} finally {try (BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream())){// 处理业务完毕bos.write(respXml.getBytes());} catch (IOException e) {WXPayUtil.getLogger().error("wxnotify:支付回调发布异常:out:", e.getMessage());}}return respXml;}
}
回调业务:
public String payCallBack(String notifyData) throws Exception{// String respXml = WXPayConstants.RESP_FAIL_XML;Map<String, String> notifyMap = WXPayUtil.xmlToMap(notifyData);if (WXPayConstants.SUCCESS.equalsIgnoreCase(notifyMap.get(WXPayConstants.RESULT_CODE))) {WXPayUtil.getLogger().info("payCallBack:微信支付----返回成功");if (WXPayUtil.isSignatureValid(notifyMap, WXPayConstants.API_KEY)) {// TODO 数据库操作,付款记录修改 & 记录付款日志WXPayUtil.getLogger().info("payCallBack:微信支付----验证签名成功,更新数据库");/*String outTradeNo = notifyMap.get("out_trade_no");OrderTrading dbOrder = transactionService.findByOutTradeNo(outTradeNo);// 将未支付状态改为已支付if (dbOrder != null && dbOrder.getState() == 1) {// 处理业务 - 修改订单状态OrderTrading order = new OrderTrading();order.setOutTradeNo(outTradeNo);order.setNotifyTime(new Date());order.setState(1);transactionService.updateTransOrderByWxnotify(order);// TODO 数据库更新异常,补偿措施}*/// 通知微信.异步确认成功.必写.不然会一直通知后台.八次之后就认为交易失败了.return WXPayConstants.RESP_SUCCESS_XML;} else {WXPayUtil.getLogger().error("payCallBack:微信支付----判断签名错误");}} else {WXPayUtil.getLogger().error("payCallBack:支付失败,错误信息:" + notifyMap.get(WXPayConstants.ERR_CODE_DES));}return WXPayConstants.RESP_FAIL_XML;
}
静态页面
预下单页面:templates/preOrder.html
确认下单页面:templates/toOrder.html
该页面用于签名校验和参数传递,为便于观察,开启了调试模式
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body>
<h1>立即支付:123</h1><button type="submit" id="payBtn">支付</button><script th:src="@{/static/js/jquery-1.8.3.min.js}" type="text/javascript" charset="utf-8" rel="stylesheet"></script>
<script type="text/javascript" th:src="@{/static/js/jquery.rotate.min.js}" rel="stylesheet"></script>
<!--微信的JSSDK-->
<script th:src="@{http://res.wx.qq.com/open/js/jweixin-1.2.0.js}"></script>
<script>$(function() {<!--通过config接口注入权限验证配置-->alert('[[${configMap}]]');alert('[[${payMap}]]');wx.config({debug: true, // 开启调试模式appId: '[[${configMap.appId}]]', // 公众号的唯一标识timestamp: '[[${configMap.timestamp}]]', // 生成签名的时间戳nonceStr: '[[${configMap.nonceStr}]]', // 生成签名的随机串signature: '[[${configMap.signature}]]',// 签名jsApiList: ['chooseWXPay'] // 填入需要使用的JS接口列表,这里是先声明我们要用到支付的JS接口});<!-- config验证成功后会调用ready中的代码 -->wx.ready(function(){//点击马上付款按钮$("#payBtn").click(function(){ //弹出支付窗口wx.chooseWXPay({timestamp: '[[${payMap.timeStamp}]]', // 支付签名时间戳,nonceStr: '[[${payMap.nonceStr}]]', // 支付签名随机串,不长于 32 位package: '[[${payMap.packageStr}]]', // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=xxxx)signType: '[[${payMap.signType}]]', // 签名方式,默认为'SHA1',使用新版支付需传入'MD5'paySign: '[[${payMap.paySign}]]', // 支付签名success: function (res) {// 支付成功后的回调函数alert("支付成功!");}});})});});
</script>
</body>
</html>
效果演示
项目启动后,点击确认支付就可以看看到debug模式下参数的显示了。最后的支付效果如图:
注:
- 支付回调的端口必须是80,应该是出于安全考虑
- web开发工具只能用于调试,测试支付功能时,需要用手机打开。
- 细心的朋友可能看出来,订单的时间早了一个多月。这个是我之前用公司账号和域名开发的,用的当时的截图。
本文的代码是为了展示统一下单的流程,却不利于移植,目前代码已重构。
源代码请查看:github.com/chetwhy/wpp