參考文檔 JSAPI支付開發文檔javascript
目前微信主流的支付方式有如下6種php
方式 | 說明 |
---|---|
付款碼支付 | 付款碼支付是用戶展現微信錢包內的「刷卡條碼/二維碼」給商戶系統掃描後直接完成支付的模式。主要應用線下面對面收銀的場景。 |
Native支付 | Native支付是商戶系統按微信支付協議生成支付二維碼,用戶再用微信「掃一掃」完成支付的模式。該模式適用於PC網站支付、實體店單品或訂單支付、媒體廣告支付等場景。 |
JSAPI支付 | JSAPI支付是用戶在微信中打開商戶的H5頁面,商戶在H5頁面經過調用微信支付提供的JSAPI接口調起微信支付模塊完成支付。 |
APP支付 | APP支付又稱移動端支付,是商戶經過在移動端應用APP中集成開放SDK調起微信支付模塊完成支付的模式。 |
H5支付 | H5支付主要是在手機、ipad等移動設備中經過瀏覽器來喚起微信支付的支付產品。 |
小程序支付 | 小程序支付是專門被定義使用在小程序中的支付產品。目前在小程序中能且只能使用小程序支付的方式來喚起微信支付。 |
由於前面作過關於公衆號的文章,所以這裏主要介紹JSAPI支付,後面的開發等也圍繞於此。html
JSAPI應用場景有:前端
不一樣於微信公衆號的測試開發,可使用內網穿透,和普通的測試帳號等。微信支付要求開發者,必需要有一個已經過驗證的真實商戶號,且該商戶號開通支付功能,以及該商戶下有真實的公衆號等。java
【微信商戶平臺】 微信商戶平臺是微信支付相關的商戶功能集合,包括參數配置、支付數據查詢與統計、在線退款、代金券或立減優惠運營等功能 平臺入口:pay.weixin.qq.com。jquery
【微信公衆平臺】 微信公衆平臺是微信公衆帳號申請入口和管理後臺。商戶能夠在公衆平臺提交基本資料、業務資料、財務資料申請開通微信支付功能。 平臺入口:mp.weixin.qq.com。git
【微信支付系統】 微信支付系統是指完成微信支付流程中涉及的API接口、後臺業務處理系統、帳務系統、回調通知等系統的總稱。github
【商戶證書】 商戶證書是微信提供的二進制文件,商戶系統發起與微信支付後臺服務器通訊請求的時候,做爲微信支付後臺識別商戶真實身份的憑據。web
【商戶後臺系統】 商戶後臺系統是商戶後臺處理業務系統的總稱,例如:商戶網站、收銀系統、進銷存系統、發貨系統、客服系統等,通常關聯開發者本身的數據庫。算法
【簽名】 商戶後臺和微信支付後臺根據相同的密鑰和算法生成一個結果,用於校驗雙方身份合法性。簽名的算法由微信支付制定並公開,經常使用的簽名方式有:MD五、SHA一、SHA25六、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數據傳輸。
1、採用SpringBoot+Thymeleaf結構,參考微信公衆號快速開發(二)項目搭建與被動回覆
2、引入官方SDK工具包
閱讀文檔後發現,對於xml解析,加密算法等其實都時經常使用的方法,微信爲咱們直接提供了經常使用工具類方法的半成品,注意,這些只能是半成品,使用時須要作適當的更改。
連接:SDK與DEMO下載,選擇JAVA版本下載後解壓便可
1、將公衆號和商戶的信息注入到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,關於微信網頁受權可參考:微信公衆號快速開發(四)微信網頁受權
頁面:
頁面這裏直接設計了一個能夠發起預支付的按鈕的靜態頁面: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);
}
}
}
複製代碼
啓動項目,請求接口:
1、 微信開發者工具的地址欄輸入:{網頁受權域名}//api/v1/wechat1/index
2、確認【贊成】受權,(這裏目的是爲了獲取openid,也可使用base靜默受權的模式,不用顯示的提示受權),跳轉到預支付頁面,如圖:
當用戶確認預支付頁面的訂單時,將請求【/placeOrder】接口,該業務將調用微信的【統一下單】接口:
1、微信統一下單實體類
@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;
}
複製代碼
2、微信支付的業務層
@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模式下參數的顯示了。最後的支付效果如圖:
注:
本文的代碼是爲了展現統一下單的流程,卻不利於移植,目前代碼已重構。
源代碼請查看:github.com/chetwhy/wpp