开发准备
参考文档 JSAPI支付开发文档
支付方式
目前微信主流的支付方式有以下6种
方式 | 说明 |
---|---|
付款码支付 | 付款码支付是用户展示微信钱包内的“刷卡条码/二维码”给商户系统扫描后直接完成支付的模式。主要应用线下面对面收银的场景。 |
Native支付 | Native支付是商户系统按微信支付协议生成支付二维码,用户再用微信“扫一扫”完成支付的模式。该模式适用于PC网站支付、实体店单品或订单支付、媒体广告支付等场景。 |
JSAPI支付 | JSAPI支付是用户在微信中打开商户的H5页面,商户在H5页面通过调用微信支付提供的JSAPI接口调起微信支付模块完成支付。 |
APP支付 | APP支付又称移动端支付,是商户通过在移动端应用APP中集成开放SDK调起微信支付模块完成支付的模式。 |
H5支付 | H5支付主要是在手机、ipad等移动设备中通过浏览器来唤起微信支付的支付产品。 |
小程序支付 | 小程序支付是专门被定义使用在小程序中的支付产品。目前在小程序中能且只能使用小程序支付的方式来唤起微信支付。 |
因为前面做过关于公众号的文章,因此这里主要介绍JSAPI支付,后面的开发等也围绕于此。
JSAPI应用场景有:
- 用户在微信公众账号内进入商家公众号,打开某个主页面,完成支付
- 用户的好友在朋友圈、聊天窗口等分享商家页面连接,用户点击链接打开商家页面,完成支付
- 将商户页面转换成二维码,用户扫描二维码后在微信浏览器中打开页面后完成支付
核心名词
不同于微信公众号的测试开发,可以使用内网穿透,和普通的测试账号等。微信支付要求开发者,必须要有一个已通过验证的真实商户号,且该商户号开通支付功能,以及该商户下有真实的公众号等。
-
【微信商户平台】 微信商户平台是微信支付相关的商户功能集合,包括参数配置、支付数据查询与统计、在线退款、代金券或立减优惠运营等功能 平台入口:pay.weixin.qq.com。
-
【微信公众平台】 微信公众平台是微信公众账号申请入口和管理后台。商户可以在公众平台提交基本资料、业务资料、财务资料申请开通微信支付功能。 平台入口: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();
}
@Override
public String getAppID() {
return WXPayConstants.APP_ID;
}
@Override
public String getMchID() {
return WXPayConstants.MCH_ID;
}
@Override
public String getKey() {
return WXPayConstants.API_KEY;
}
@Override
public InputStream getCertStream() {
ByteArrayInputStream certBis = new ByteArrayInputStream(this.certData);
return certBis;
}
@Override
public int getHttpConnectTimeoutMs() {
return 2000;
}
@Override
public int getHttpReadTimeoutMs() {
return 10000;
}
@Override
public IWXPayDomain getWXPayDomain() {
return WXPayDomainSimpleImpl.instance();
}
public String getPrimaryDomain() {
return "api.mch.weixin.qq.com";
}
public String getAlternateDomain() {
return "api2.mch.weixin.qq.com";
}
@Override
public int getReportWorkerNum() {
return 1;
}
@Override
public 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 {
// 显式授权,获得code
if (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 {
@Autowired
WXPayConfigExtend 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公众号下的openid
order.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/clo…
今天的文章微信JSAPI支付(一)统一下单分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/23359.html