這幾天公司微信端項目結束,把微信公衆號支付做個整理方便有同樣功能需求的同學。
先放上一份官方文檔
https://pay.weixin.qq.com/wiki/doc/api/jsapi_sl.php?chapter=7_1,咱們既然要做支付肯定要先了解人家的規範,多讀幾遍這個文檔能少走不少彎路。
微信這邊的各種平臺紛繁複雜,像:微信公衆平臺、微信開放平臺、微信商戶平臺等,建議大家先把這些關係搞清楚,以便於後期開發的時候去對應各種key、祕鑰、appId之類的。要是他們之間的概念含混不清保證讓你在對應的時候懵逼。
以下名詞解釋摘自微信官方文檔
https://pay.weixin.qq.com/wiki/doc/api/jsapi_sl.php?chapter=2_2
名詞解釋
1、
微信公衆平臺
微信公衆平臺是微信公衆賬號申請入口和管理後臺。商戶可以在公衆平臺提交基本資料、業務資料、財務資料申請開通微信支付功能。
平臺入口:http://mp.weixin.qq.com。
2、
微信開放平臺
微信開放平臺是商戶APP接入微信支付開放接口的申請入口,通過此平臺可申請微信APP支付。
平臺入口:http://open.weixin.qq.com。
3、
微信商戶平臺
微信商戶平臺是微信支付相關的商戶功能集合,包括參數配置、支付數據查詢與統計、在線退款、代金券或立減優惠運營等功能。
平臺入口:http://pay.weixin.qq.com。
4、
微信企業號
微信企業號是企業號的申請入口和管理後臺,商戶可以在企業號提交基本資料、業務資料、財務資料申請開通微信支付功能。
企業號入口:http://qy.weixin.qq.com。
5、
微信支付系統
微信支付系統是指完成微信支付流程中涉及的API接口、後臺業務處理系統、賬務系統、回調通知等系統的總稱。
6、
商戶收銀系統
商戶收銀系統即商戶的POS收銀系統,是錄入商品信息、生成訂單、客戶支付、打印小票等功能的系統。接入微信支付功能主要涉及到POS軟件系統的開發和測試,所以在下文中提到的商戶收銀系統特指POS收銀軟件系統。
7、
商戶後臺系統
商戶後臺系統是商戶後臺處理業務系統的總稱,例如:商戶網站、收銀系統、進銷存系統、發貨系統、客服系統等。
8、
掃碼設備
一種輸入設備,主要用於商戶系統快速讀取媒介上的圖形編碼信息。按讀取碼的類型不同,可分爲條碼掃碼設備和二維碼掃碼設備。按讀取物理原理可分爲紅外掃碼設備、激光掃碼設備。
9、
商戶證書
商戶證書是微信提供的二進制文件,商戶系統發起與微信支付後臺服務器通信請求的時候,作爲微信支付後臺識別商戶真實身份的憑據。
10、
簽名
商戶後臺和微信支付後臺根據相同的密鑰和算法生成一個結果,用於校驗雙方身份合法性。簽名的算法由微信支付制定並公開,常用的簽名方式有:MD5、SHA1、SHA256、HMAC等。
11、
JSAPI網頁支付
JSAPI網頁支付即前文說的公衆號支付,可在微信公衆號、朋友圈、聊天會話中點擊頁面鏈接,或者用微信“掃一掃”掃描頁面地址二維碼在微信中打開商戶HTML5頁面,在頁面內下單完成支付。
12、
Native原生支付
Native原生支付即前文說的掃碼支付,商戶根據微信支付協議格式生成的二維碼,用戶通過微信“掃一掃”掃描二維碼後即進入付款確認界面,輸入密碼即完成支付。
13、
支付密碼
支付密碼是用戶開通微信支付時單獨設置的密碼,用於確認支付完成交易授權。該密碼與微信登錄密碼不同。
14、
Openid
用戶在公衆號內的身份標識,不同公衆號擁有不同的openid。商戶後臺系統通過登錄授權、支付通知、查詢訂單等API可獲取到用戶的openid。主要用途是判斷同一個用戶,對用戶發送客服消息、模版消息等。企業號用戶需要使用企業號userid轉openid接口將企業成員的userid轉換成openid。
這裏簡單的做了一個圖,無論是微信公衆平臺還是微信開放平臺,涉及到支付的時候都在微信商戶平臺這個後臺中去處理,前兩者分別對應了一套商戶平臺的賬號信息。尤其是這些賬號信息分屬不同部門管理時,在開發的時候給開發者帶來不小的影響。
重要參數
這是在支付中用到的幾個重要配置參數,是申請的時候微信給發的郵件。
後臺組裝參數邏輯
其實微信公衆號支付與微信APP支付在後臺組裝參數上基本沒啥區別。多了一個openid,支付方式爲JSAPI。
這裏的邏輯還是:
1、從前臺獲取到用戶提交的訂單參數,生成第一次簽名。
2、調用微信統一下單接口URL https://api.mch.weixin.qq.com/pay/unifiedorder 生成預支付id:
prepayId。
3、對參數進行二次簽名。
4、將所需的數據傳給前臺,調起微信支付。
5、微信將將支付通知給後臺。
6、後臺執行回調操作,完成整個支付流程。
後臺代碼
後臺代碼是在原有基礎上稍加修改的
/**
* 請求微信支付系統的接口,商戶後臺系統調用統一下單接口在微信支付服務後臺生成預支付交易單
* @throws Exception
*/
@RequestMapping("/doWeiXinRequest")
@ApiOperation(value="微信支付生成訂單號,App端再次需要請求的接口",httpMethod="POST",response=JsonUtil.class,notes="生成微信支付中需要的預支付id參數---pay/doWeiXinRequest")
public void doWeiXinRequest(@ApiParam(required=true,name="outTradeNo",value="提交訂單返回的訂單號")@RequestParam String outTradeNo) throws Exception{
//客戶端需要傳遞的參數待定,先把下單接口的參數構造好
String body = "支付採購單";
String userId = (String) session.getAttribute("sysUserId");//確認是購買的用戶進行付款操作
String openid = (String) session.getAttribute("openid");
System.out.println(openid+"~~~~~~~~~~~~~~~~~~");
try {
if(outTradeNo != null && !"".equals(outTradeNo)
&& userId != null && !"".equals(userId)
&& openid != null && !"".equals(openid)){
OrderCommCai orderCommCai = orderCommCaiService.get(outTradeNo);//根據綜合採購單id來查找綜合採購單
String totalFee = orderCommCai.getOrderZongCommPrices().multiply(new BigDecimal(100)).toString();
totalFee = totalFee.substring(0,totalFee.indexOf("."));
if(orderCommCai != null && !"".equals(orderCommCai)){
if("0".equals(orderCommCai.getOrderType())){//未支付
String currTime = TenpayUtil.getCurrTime();
//八位日期相關的字符串
String strTime = currTime.substring(8, currTime.length());
//四位隨機數的字符串
String strRandom = TenpayUtil.buildRandom(4) + "";
//十位隨機字符串
String nonceStr = strTime + strRandom;
/**
* 商品描述,訂單號,訂單總金額從手機端獲取
*/
String spbillCreateIp = request.getRemoteAddr();
SortedMap<String, String> requestParams = new TreeMap<String, String>();
requestParams.put("appid", Constant.APP_ID);//公衆賬號id
requestParams.put("mch_id", Constant.PARTNER); //商戶號
requestParams.put("nonce_str", nonceStr);//隨機字符串
requestParams.put("body", body);//商品描述
requestParams.put("out_trade_no", outTradeNo);//訂單號
requestParams.put("total_fee", String.valueOf(totalFee));//訂單總金額
requestParams.put("spbill_create_ip", spbillCreateIp);//用戶終端ip
requestParams.put("notify_url", Constant.NOTIFIY_URL);//通知地址
requestParams.put("openid", openid);//用戶標識
requestParams.put("trade_type", "JSAPI");
RequestHandler reqHandler = new RequestHandler(request, response);
reqHandler.init(Constant.APP_ID, Constant.APP_SECRET, Constant.PARTNER_KEY);
// String requestSign = reqHandler.createSign(requestParams);//簽名
String requestSign = WXPayUtil.generateSignature(requestParams, Constant.PARTNER_KEY,SignType.MD5);
System.out.println(requestParams);
//構造請求參數
String xml = "<xml>"
+ "<appid>" + Constant.APP_ID + "</appid>"
+ "<body>" + body + "</body>"
+ "<mch_id>" + Constant.PARTNER + "</mch_id>"
+ "<nonce_str>" + nonceStr + "</nonce_str>"
+ "<notify_url>" + Constant.NOTIFIY_URL + "</notify_url>"
+ "<out_trade_no>" + outTradeNo + "</out_trade_no>"
+ "<sign>" + requestSign + "</sign>"
+ "<spbill_create_ip>" + spbillCreateIp + "</spbill_create_ip>"
+ "<total_fee>" + totalFee + "</total_fee>"
+ "<trade_type>JSAPI</trade_type>"
+ "<openid>" + openid + "</openid>"
+ "</xml>";
System.out.println(xml);
new GetWxOrderNo();
String prepayId = GetWxOrderNo.getPayNo(Constant.PRE_URL, xml);//獲得預支付單信息
boolean flag = WXPayUtil.isSignatureValid(xml, Constant.PARTNER_KEY);
/**
* 統一下單接口返回正常的prepay_id,再按簽名規範重新生成簽名後,將數據傳輸給APP。
* 參與簽名的字段名爲appId,partnerId,prepayId,nonceStr,timeStamp,package。
* 注意:package的值格式爲Sign=WXPay
*/
SortedMap<String, String> getParams = new TreeMap<String, String>();
String timeStamp = Sha1Util.getTimeStamp();
String packages = "prepay_id="+prepayId;
getParams.put("appId", Constant.APP_ID);
getParams.put("timeStamp", timeStamp);
getParams.put("nonceStr", nonceStr);
getParams.put("package", packages);
getParams.put("signType", "MD5");
String getSign = reqHandler.createSign(getParams);
System.out.println("--微信支付 ---"+getSign);
map1.put("msg", "成功");//當統一下訂單接口執行完成之後,返回調起支付接口所需要的所有參數給微信服務號前端
map2.put("appId", Constant.APP_ID);
map2.put("nonceStr", nonceStr);
map2.put("packages", "prepay_id="+prepayId);
map2.put("partnerId", Constant.PARTNER);
map2.put("timeStamp", timeStamp);
map2.put("sign", getSign);
}else{
map1.put("msg", "訂單已支付");
}
}else{
map1.put("msg", "訂單不存在");
}
}else{
map1.put("msg", "參數不全");
}
} catch (BusinessException e) {
e.printStackTrace();
map1.put("msg", "發生後臺異常");
}
map.put("data", map2);
map.put("result", map1);
JsonUtil.writeJson(response, JsonUtil.objectToJson(map));
}
簽名工具類
/**
* 生成簽名. 注意,若含有sign_type字段,必須和signType參數保持一致。
*
* @param data 待簽名數據
* @param key API密鑰
* @param signType 簽名方式
* @return 簽名
*/
public static String generateSignature(final Map<String, String> data, String key, SignType signType) throws Exception {
Set<String> keySet = data.keySet();
String[] keyArray = keySet.toArray(new String[keySet.size()]);
Arrays.sort(keyArray);
StringBuilder sb = new StringBuilder();
for (String k : keyArray) {
if (k.equals(WXPayConstants.FIELD_SIGN)) {
continue;
}
if (data.get(k).trim().length() > 0) // 參數值爲空,則不參與簽名
sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
sb.append("key=").append(key);
if (SignType.MD5.equals(signType)) {
System.out.println("-------MD5簽名sign----------"+MD5(sb.toString()).toUpperCase());
return MD5(sb.toString()).toUpperCase();
}
else if (SignType.HMACSHA256.equals(signType)) {
return HMACSHA256(sb.toString(), key);
}
else {
throw new Exception(String.format("Invalid sign_type: %s", signType));
}
}
獲取預支付id工具類
public static String getPayNo(String url,String xmlParam){
DefaultHttpClient client = new DefaultHttpClient();
client.getParams().setParameter(ClientPNames.ALLOW_CIRCULAR_REDIRECTS, true);
HttpPost httpost= HttpClientConnectionManager.getPostMethod(url);
String prepay_id = "";
try {
httpost.setEntity(new StringEntity(xmlParam, "UTF-8"));
HttpResponse response = httpclient.execute(httpost);
String jsonStr = EntityUtils.toString(response.getEntity(), "UTF-8");
Map<String, Object> dataMap = new HashMap<String, Object>();
System.out.println(jsonStr);
if(jsonStr.indexOf("FAIL")!=-1){
return prepay_id;
}
Map map = doXMLParse(jsonStr);
String return_code = (String) map.get("return_code");
prepay_id = (String) map.get("prepay_id");
} catch (Exception e) {
e.printStackTrace();
}
return prepay_id;
}
第二次簽名用到的工具類
/**
* 創建md5摘要,規則是:按參數名稱a-z排序,遇到空值的參數不參加簽名。
*/
public String createSign(SortedMap<String, String> packageParams) {
StringBuffer sb = new StringBuffer();
Set es = packageParams.entrySet();
Iterator it = es.iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
String k = (String) entry.getKey();
String v = (String) entry.getValue();
if (null != v && !"".equals(v) && !"sign".equals(k)
&& !"key".equals(k)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + this.getKey());
System.out.println("md5 sb:" + sb);
String sign = MD5Util.MD5Encode(sb.toString(), this.charset)
.toUpperCase();
System.out.println("packge簽名:" + sign);
return sign;
}
注意事項(跳坑)
1、下面這張圖改動處是微信公衆號支付的參數,openid是預先獲取到的,交易類型是JSAPI。
2、所有參數成功獲取以後,把參數傳給前臺,這裏的packages跟微信APP支付略有不同,需要注意下。
3、第二次簽名參數大小寫問題需要注意(第二次簽名失敗的話,這裏需要着重考慮。)
在微信APP支付方式中,第二次簽名參數全爲小寫,而在微信公衆號支付這裏依舊是區分大小寫的
前端調起支付
//點擊微信支付
function payByWeChat(outTradeNo){
$.ajax({
data:{outTradeNo:outTradeNo},
dataType:"json",
type:"post",
async: false,
url:"${ctxPath}/payWeiXin/doWeiXinRequest",
success:function(msg){
if(msg.result.msg == "成功"){
var content = msg.data;
var appId = content.appId;
var timeStamp = content.timeStamp;
var nonceStr = content.nonceStr;
var packages = content.packages;
var paySign = content.sign;
pay(appId,timeStamp,nonceStr,packages,paySign);
}
},
error:function(msg){
}
});
}
//h5喚起微信支付
function onBridgeReady(appId,timeStamp,nonceStr,packages,paySign){
WeixinJSBridge.invoke(
'getBrandWCPayRequest', {
"appId":appId, //公衆號名稱,由商戶傳入
"timeStamp":timeStamp, //時間戳,自1970年以來的秒數
"nonceStr":nonceStr, //隨機串
"package":packages,
"signType":"MD5", //微信簽名方式:
"paySign":paySign //微信簽名
},
function(res){
console.log(res.err_code + res.err_desc);
console.log(res.err_msg);
alert(res.err_code + res.err_desc);
alert(res.err_msg);
if(res.err_msg == "get_brand_wcpay_request:ok" ) {
alert("支持成功");// 使用以上方式判斷前端返回,微信團隊鄭重提示:res.err_msg將在用戶支付成功後返回 ok,但並不保證它絕對可靠。
window.location.href = "${ctxPath}/jump/toOrderCommCai";
}else if (res.err_msg === 'get_brand_wcpay_request:cancel') {
alert("取消支付");
}
}
);
}
function pay(appId,timeStamp,nonceStr,packages,paySign){
if (typeof WeixinJSBridge == "undefined"){
if( document.addEventListener ){
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
}else if (document.attachEvent){
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
}
}else{
onBridgeReady(appId,timeStamp,nonceStr,packages,paySign);
}
}
微信公衆號支付的回調
/**
* 獲取微信支付回調的接口
*/
@RequestMapping("/wechatPayCallBack")
@ApiOperation(value="微信支付回調的接口",httpMethod="POST",response=JsonUtil.class,notes="微信支付用於回調的接口---pay/wechatPayCallBack")
public void wechatPayCallBack() throws Exception{
//後臺服務器和微信服務器之間的交互,流操作
InputStream is = request.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = is.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
System.out.println("~~~~~~~~~~~~~~~~微信回調付款成功~~~~~~~~~");
baos.close();
is.close();
String result = new String(baos.toByteArray(),"utf-8");//獲取微信調用notify_url的返回信息
Map<Object, Object> resultMap = XMLUtil.doXMLParse(result);
for(Object keyValue : map.keySet()){
System.out.println(keyValue + "=" + map.get(keyValue));
}
if(resultMap.get("return_code").toString().equalsIgnoreCase("SUCCESS")){//支付成功
//綜合採購單的id
String orderNo = (String) resultMap.get("out_trade_no");
//微信支付訂單號
String weixinOrderNo = (String) resultMap.get("transaction_id");
OrderCommCai orderCommCai = orderCommCaiService.get(orderNo);//根據綜合採購單id來查找綜合採購單
if(orderCommCai != null && !"".equals(orderCommCai)){
if("1".equals(orderCommCai.getOrderType())){// 支付狀態爲:已支付
response.getWriter().write(setXML("SUCCESS", ""));
}else{
/**
* 這裏寫回調的業務邏輯
*/
orderCommCai.setOrderType("1");
BigDecimal payWechatPrice = new BigDecimal(resultMap.get("total_fee").toString()).divide(new BigDecimal(100));
orderCommCai.setPayWechatPrice(payWechatPrice);
orderCommCai.setPayWechatStatus("1");//支付成功
orderCommCai.setPayWechatWaterId(weixinOrderNo);//微信支付訂單號
orderCommCai.setPayType("2");//微信支付
orderCommCai.setUpdateBy(Constant.UPDATE_BY);
orderCommCai.setUpdateDate(DateUtil.currentTimestamp());
//更新綜合採購單中的數據
boolean flag = orderCommCaiService.update(orderCommCai);
//獲取社區店實體
BusCommunityPartner busCommunityPartner = busCommunityPartnerService.get(orderCommCai.getCommId());
if(flag){
orderCommCaiService.saveCommSplitCai(orderCommCai);
//發送站內信息
String noticsinfo = "";
noticsinfo = "社區店(" + busCommunityPartner.getCompanyName() + ")已經提交訂單:(" + orderCommCai.getOrderNumber() + ")!";
BaseSysInfo baseSysInfo = new BaseSysInfo();
baseSysInfo.setAccountId(orderCommCai.getCityParterId());
baseSysInfo.setAccountType("2");
baseSysInfo.setBaseInfoRead("0");
baseSysInfo.setContent(noticsinfo);
baseSysInfoService.save(baseSysInfo);
map1.put("msg", "成功");
response.getWriter().write(setXML("SUCCESS", ""));
}else{
response.getWriter().write(setXML("FAIL", ""));
}
}
}else{
response.getWriter().write(setXML("FAIL", ""));
}
}else{
response.getWriter().write(setXML("FAIL", ""));
}
}
public static String setXML(String return_code, String return_msg) {
return "<xml><return_code><![CDATA[" + return_code + "]]></return_code><return_msg><![CDATA[" + return_msg
+ "]]></return_msg></xml>";
}
重要事情說三遍:
一定要多看文檔!一定要多看文檔!一定要多看文檔!