前言
最近做的一個項目要求實現支付寶手機網站支付和微信公衆號內支付,由於是第一次接觸,所以開發過程中遇到許多的問題(也有自己讀開發文檔不細心),爲了讓大家不重蹈覆轍,少走彎路,以下將介紹具體開發前準備和開發過程。首先先介紹微信公衆號內支付,如果最近有時間,將會介紹支付寶手機網站支付詳細開發過程。
開發準備
需要準備的4個參數
1)商戶ID
3) 商戶平臺設置的祕鑰key(簽名時需要)
4)appID
5) appsecret
開發配置
設置支付授權目錄 (如何配置請看下文-所遇問題4)
位置:微信支付商戶平臺中
設置網頁授權域名(目的獲取openid)
位置:進入服務號 微信公衆平臺
公衆號設置--功能設置--網頁授權域名設置
上面盜用微信官方的圖,我的是tomcat服務器,所以將下載的 .txt 文件放在了 tomcat安裝路徑--webapps--ROOT目錄下。
流程
公衆號內支付時序圖(再次盜用微信官方圖片)
梳理後流程:
1、微信公衆號內選擇商品下單;
2、JS將用戶的商品數據傳給商戶服務器,請求生成支付訂單;
3、商戶後臺調用統一下單API向微信服務器發送請求,微信服務器生成一個預付單, 並生成一個prepay_id返回給商戶後臺;
4、商戶後臺將返回的prepay_id返回給前端;
5、前端JS內調用getBrandWCPayRequest,發起微信支付請求,進入支付流程
詳情點擊;
6、用戶成功支付點擊完成按鈕後,商戶的前端會收到JavaScript的返回值。商戶可直接跳轉到支付成功的靜態頁面進行展示;
7、與此同時微信會把相關支付結果和用戶信息異步發送給商戶,商戶服務器接收處理,並返回應答詳情點擊;
8、完成。
界面效果
代碼
前端部分(使用AUI)
訂單詳情 html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1,maximum-scale=1, user-scalable=no">
<title>訂單詳情</title>
<script src="http://res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>
<script type="text/javascript" src="../common/js/jquery.min.js"></script>
<script type="text/javascript" src="js/createOrder.js"></script>
<link rel="stylesheet" href="../common/css/aui.css" />
</head>
<body>
<div class="aui-content aui-padded-5">
<ul class="aui-list aui-list-in aui-margin-b-15">
<li class="aui-list-header">
訂單詳情
</li>
<li class="aui-list-item aui-list-item-middle">
<div class="aui-list-item-inner">
<div class="aui-list-item-title">商品名稱</div>
<div class="aui-list-item-text aui-text-right">xxxxxx</div>
</div>
</li>
<li class="aui-list-item aui-list-item-middle">
<div class="aui-list-item-inner">
<div class="aui-list-item-title">編號</div>
<div class="aui-list-item-text aui-text-right">012345678910</div>
</div>
</li>
<li class="aui-list-item aui-list-item-middle">
<div class="aui-list-item-inner">
<div class="aui-list-item-title">詳情</div>
<div class="aui-list-item-text aui-text-right">abcdefghijklmnopqrstuvwxyz</div>
</div>
</li>
<li class="aui-list-item">
<div class="aui-list-item-inner">
<div class="aui-list-item-title">金額</div>
<div class="aui-list-item-text aui-text-right">¥88.00</div>
</div>
</li>
</ul>
<div class="aui-btn aui-btn-primary aui-btn-block" onclick="toPay()">支付</div>
</div>
</body>
</html>
訂單詳情 JSfunction toPay(){
document.location.href="pay.html"
}
支付頁面 html<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1,maximum-scale=1, user-scalable=no">
<title>支付</title>
<script src="http://res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>
<script type="text/javascript" src="../common/js/jquery.min.js" ></script>
<script type="text/javascript" src="js/pay.js" ></script>
<link rel="stylesheet" href="../common/css/aui.css" />
<link rel="stylesheet" href="../common/css/common.css" />
</head>
<body>
<div class="aui-content aui-padded-5">
<ul class="aui-list aui-select-list aui-margin-b-15 m-radius-5">
<li class="aui-list-item">
<div class="aui-list-item-label">
<img src="../common/img/icon_wechat.png" />
</div>
<div class="aui-list-item-inner">
微信支付
<div class="aui-list-item-text">
推薦有微信賬號的用戶使用
</div>
</div>
<div class="aui-list-item-label">
<input class="aui-radio" type="radio" name="payType" value="0" checked>
</div>
</li>
<li class="aui-list-item">
<div class="aui-list-item-label">
<img src="../common/img/icon_zfb.png" />
</div>
<div class="aui-list-item-inner">
支付寶支付
<div class="aui-list-item-text">
推薦有支付寶賬號的用戶使用
</div>
</div>
<div class="aui-list-item-label">
<input class="aui-radio" type="radio" name="payType" value="1">
</div>
</li>
</ul>
<div class="aui-btn aui-btn-primary aui-btn-block" onclick="pay()">確認支付</div>
</div>
</body>
</html>
支付頁面 jsfunction pay() {
var payType = "";
var radio = document.getElementsByName("payType");
for (i = 0; i < radio.length; i++) {
if (radio[i].checked) {
payType = radio[i].value;
}
};
var orderid=""; //訂單編號,爲了方便測試,隨機生成
for(i=0;i<11;i++){
orderid +=Math.floor(Math.random()*10); ;
}
var data = {
orderid : orderid,
body : 'abcdefghijklmnopqrstuvwxyz',
totalFee : '0.01',
payType : payType //0:微信; 1:支付寶
};
console.log(data);
$.ajax({
type : 'POST',
dataType : 'json',
url : '/wxCourse/weixinPay/pay.action',
data : data,
success : function(data) {
if (data.code == 200) {
if (data.result.payType == "alipay") {
document.write(data.result.html);
document.close();
} else if (data.result.payType == "weixin") {
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(data);
}
// onBridgeReady(data);
} else {
alert("系統異常,請稍後再試!");
}
} else {
alert(data.msg);
}
},
fail : function(data) {
console.log("請求失敗!")
}
})
}
function onBridgeReady(data) {
console.log(data)
WeixinJSBridge.invoke(
'getBrandWCPayRequest', {
"appId" : data.result.appId, //公衆號名稱,由商戶傳入
"timeStamp" : data.result.timeStamp, //時間戳,自1970年以來的秒數
"nonceStr" : data.result.nonceStr, //隨機串
"package" : data.result.packageValue,
"signType" : data.result.signType, //微信簽名方式:
"paySign" : data.result.paySign, //微信簽名
},
function(res) {
if (res.err_msg == "get_brand_wcpay_request:ok") {
window.location.href = "http://www.baidu.com"; //可在此設置跳轉到支付成功頁面
} else if (res.err_msg == "get_brand_wcpay_request:cancel") {
alert("您的支付已取消!");
} else {
alert("支付失敗!請稍後再試!");
}
}
);
}
後端部分
調用微信支付接口
/**
* 調用微信支付接口
*
* @param orderid
* 商戶訂單編號
* @param body
* 訂單詳情
* @param totalFee
* 訂單金額
* @param payType
* 支付方式,0微信,1支付寶
* @param request
* @return
*/
@RequestMapping("pay")
@ResponseBody
public ResultVO pay(String orderid, String body, String totalFee, String payType, HttpServletRequest request) {
ResultVO res = new ResultVO();
Map<String, Object> map = new HashMap<String, Object>();
String notify_url = WeixinConfig.NOTIFY_URL; // 設置回調地址
String openid = (String) request.getSession().getAttribute("openid"); // 獲取openid。獲取用戶授權信息後,存在session中,怎樣授權自己百度吧
String spbill_create_ip = WeixinUtil.getRemortIP(request); // 獲取終端ip
//這地方可添加根據訂單號查詢訂單狀態的操作,避免重複支付或返回“total_fee爲空提示”,如果訂單狀態爲未支付,則繼續;
//已支付或取消,則退出
try {
// 調用統一下單接口,獲取prepay_id(微信生成的預支付會話標識)
map = WeiXinPay.getPrepayId(notify_url, spbill_create_ip, body, openid, orderid, totalFee);
String result = (String) map.get("result"); // success
// 表示調用統一下單接口並返回prepay_id成功;error表示失敗
if ("SUCCESS".equals(result)) {
res.setCode(ResponseCode.SUCCESS_CODE);
res.setMsg(ResponseCode.SUCCESS_MSG);
res.setResult(map.get("params"));
} else {
res.setCode(ResponseCode.FAIL_CODE);
res.setMsg(ResponseCode.FAIL_MSG);
}
} catch (Exception e) {
logger.error("[調用微信支付接口]異常:", e);
res.setCode(ResponseCode.EXCEPTION_CODE);
res.setMsg(ResponseCode.EXCEPTION_MSG);
}
return res;
}
/**
* 調用統一下單接口,獲取prepay_id(預支付會話標識)
* @param notify_url 異步接收微信支付結果通知的回調地址
* @param spbill_create_ip 終端IP
* @param orderDetail 商品描述
* @param openid 用戶標識
* @param orderno 商戶訂單號
* @param money 標價金額
* @return
* @throws Exception
*/
public static Map<String, Object> getPrepayId(String notify_url,
String spbill_create_ip,String orderDetail,String openid,String orderno,String money) throws Exception{
SortedMap<Object,Object> parameters = new TreeMap<Object,Object>(); //請求參數集合
SortedMap<Object,Object> params = new TreeMap<Object,Object>(); //支付成功,返回參數集合
Map<String,Object> dataMap = new HashMap<String,Object>();
String result = "error";
//元轉分
Double moneyd = Double.parseDouble(money)*100;
Integer moneyi = moneyd.intValue();
String amount = moneyi.toString();
parameters.put("appid", WeixinConfig.APPID);
parameters.put("mch_id", WeixinConfig.MCH_ID);
parameters.put("nonce_str", WeixinUtil.create_nonce_str());
parameters.put("body", orderDetail);
parameters.put("out_trade_no", orderno);
parameters.put("total_fee", amount);
parameters.put("spbill_create_ip",spbill_create_ip);
parameters.put("notify_url", notify_url);
parameters.put("trade_type", "JSAPI");
parameters.put("openid", openid);
if(openid == null || openid==""){
parameters.put("openid", "oRcMct1Q3AG3OnNazAdYmlH9n-_k");
}
String sign = WeixinUtil.createSign(WeixinConfig.CHARSET, parameters); //生成簽名
parameters.put("sign", sign);
String requestXML = WeixinUtil.getRequestXml(parameters); //生成請求xml
System.out.println("requestXML:"+requestXML);
//調用微信統一接口,返回的是xml格式的數據,已將其處理爲map<String,String>格式
Map<String, String> map = WeixinUtil.doPostStrXML("https://api.mch.weixin.qq.com/pay/unifiedorder",requestXML);
System.out.println(map.toString());
if("SUCCESS".equals(map.get("return_code"))){//執行成功並返回 prepay_id
params.put("appId", WeixinConfig.APPID);
params.put("timeStamp", String.valueOf(new Date().getTime()));
params.put("nonceStr", WeixinUtil.create_nonce_str());
params.put("package", "prepay_id="+map.get("prepay_id"));
params.put("signType", WeixinConfig.SIGN_TYPE);
String paySign = WeixinUtil.createSign(WeixinConfig.CHARSET, params);
params.put("paySign", paySign); //paySign的生成規則和Sign的生成規則一致
params.put("packageValue", "prepay_id="+map.get("prepay_id")); //這裏用packageValue是預防package是關鍵字在js獲取值出錯
params.put("payType", "weixin");
dataMap.put("params", params);
}
result = map.get("return_code");
dataMap.put("result", result);
return dataMap;
}
支付完成回調,接收微信服務器發送的相關支付結果和用戶信息,並應答。
(摘自公衆號支付文檔)特別提醒:商戶系統對於支付結果通知的內容一定要做簽名驗證,並校驗返回的訂單金額
是否與商戶側的訂單金額一致,防止數據泄漏導致出現“假通知”,造成資金損失。
/**
* 後端回調,統一下單API notify_url參數設置的路徑
*/
@RequestMapping("callBack")
@ResponseBody
public String callBack(HttpServletRequest request) {
String result = "";
try {
//獲取返回參數
SortedMap<Object, Object> map = WeixinUtil.getReqParams(request);
if (map.get("return_code").equals("SUCCESS")) {
// 驗證簽名
if (WeixinUtil.checkSign(map)) {
logger.info("[驗證簽名成功]");
// 這個地方可添加更改商戶訂單狀態操作
result = WeixinUtil.setXML("SUCCESS", "");
return result;
} else {
logger.info("[驗證簽名失敗]");
}
}
} catch (Exception e) {
logger.error("[微信支付後端回調異常]", e);
}
result = WeixinUtil.setXML("FAIL", "");
return result;
}
工具類
WeixinUtil工具類
package com.gusy.pay.weixin.utils;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import org.apache.log4j.Logger;
/**
* 微信支付工具類
* @author gusy
* @date 2017年9月11日,下午5:23:07
*/
public class WeixinUtil {
private static Logger logger=Logger.getLogger(WeixinUtil.class);
/**
* 生成隨機串
* @return
*/
public static String create_nonce_str() {
return UUID.randomUUID().toString().replace("-", "");
}
/**
* 生成時間戳
* @return
*/
private static String create_timestamp() {
return Long.toString(System.currentTimeMillis() / 1000);
}
/**
* 獲取ip
* @param request
* @return
*/
public static String getRemortIP(HttpServletRequest request) {
if (request.getHeader("x-forwarded-for") == null) {
return request.getRemoteAddr();
}
return request.getHeader("x-forwarded-for");
}
/**
* 獲取請求xml
* @param parameters
* @return
*/
public static String getRequestXml(SortedMap<Object,Object> parameters){
StringBuffer sb = new StringBuffer();
sb.append("<xml>");
Set es = parameters.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 ("attach".equalsIgnoreCase(k)||"body".equalsIgnoreCase(k)||"sign".equalsIgnoreCase(k)) {
sb.append("<"+k+">"+"<![CDATA["+v+"]]></"+k+">");
}else {
sb.append("<"+k+">"+v+"</"+k+">");
}
}
sb.append("</xml>");
return sb.toString();
}
/**
* 返回數據爲xml格式的post請求
* @param url
* @param outStr
* @return
* @throws Exception
*/
public static Map<String, String> doPostStrXML(String url,String outStr) throws Exception{
HttpClient client = new DefaultHttpClient();
HttpPost httpost = new HttpPost(url);
httpost.setEntity(new StringEntity(outStr,"UTF-8"));
HttpResponse response = client.execute(httpost);
String result = EntityUtils.toString(response.getEntity(),"UTF-8");
Map<String, String> doXMLParse = XmlUtils.doXMLParse(result);
return doXMLParse;
}
/**
* 後端回調,應答微信xml
* @param return_code
* @param return_msg
* @return
*/
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>";
}
/**
* 創建簽名
* @param characterEncoding
* @param parameters
* @return
*/
public static String createSign(String characterEncoding,SortedMap<Object,Object> parameters){
StringBuffer sb = new StringBuffer();
Set es = parameters.entrySet();
Iterator it = es.iterator();
while(it.hasNext()) {
Map.Entry entry = (Map.Entry)it.next();
String k = (String)entry.getKey();
Object v = entry.getValue();
if(null != v && !"".equals(v)
&& !"sign".equals(k) && !"key".equals(k)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + WeixinConfig.API_KEY);
String sign = DigestUtils.md5Hex(sb.toString()).toUpperCase();
return sign;
}
/**
* 簽名驗證
* @return
*/
public static boolean checkSign(SortedMap<Object,Object> params){
String sign = params.get("sign").toString();//簽名
String mSign = createSign(WeixinConfig.CHARSET, params);
if(sign.equals(mSign)){
return true;
}
return false;
}
/**
* 獲取支付回調請求參數
* @param request
* @return
*/
public static SortedMap<Object,Object> getReqParams(HttpServletRequest request) throws Exception {
InputStream inStream = request.getInputStream();
ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = inStream.read(buffer)) != -1) {
outSteam.write(buffer, 0, len);
}
outSteam.close();
inStream.close();
String result = new String(outSteam.toByteArray(),"utf-8");//獲取微信調用我們notify_url的返回信息
Map<String, String> map = XmlUtils.doXMLParse(result);
SortedMap<Object,Object> dd = new TreeMap<Object,Object>();
for(Object keyValue : map.keySet()){
dd.put(keyValue, map.get(keyValue));
logger.info("========>>>>微信付款成功回調:"+keyValue+"="+map.get(keyValue));
}
return dd;
}
}
xmlUtil工具類
package com.gusy.pay.weixin.utils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
/**
* 解析xml文件
*/
public class XmlUtils {
/**
* 解析xml,返回第一級元素鍵值對。如果第一級元素有子節點,則此節點的值是子節點的xml數據。
* @param strxml
* @return
* @throws JDOMException
* @throws IOException
*/
public static Map<String, String> doXMLParse(String strxml) throws JDOMException, IOException {
strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");
if(null == strxml || "".equals(strxml)) {
return null;
}
Map m = new HashMap();
//SortedMap<Object,Object> m = new TreeMap<Object,Object>();
InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8"));
SAXBuilder builder = new SAXBuilder();
Document doc = builder.build(in);
Element root = doc.getRootElement();
List list = root.getChildren();
Iterator it = list.iterator();
while(it.hasNext()) {
Element e = (Element) it.next();
String k = e.getName();
String v = "";
List children = e.getChildren();
if(children.isEmpty()) {
v = e.getTextNormalize();
} else {
v = getChildrenText(children);
}
m.put(k, v);
}
//關閉流
in.close();
return m;
}
/**
* 獲取子結點的xml
* @param children
* @return String
*/
public static String getChildrenText(List children) {
StringBuffer sb = new StringBuffer();
if(!children.isEmpty()) {
Iterator it = children.iterator();
while(it.hasNext()) {
Element e = (Element) it.next();
String name = e.getName();
String value = e.getTextNormalize();
List list = e.getChildren();
sb.append("<" + name + ">");
if(!list.isEmpty()) {
sb.append(getChildrenText(list));
}
sb.append(value);
sb.append("</" + name + ">");
}
}
return sb.toString();
}
}
可能遇到的問題
1、遇到“支付參數簽名失敗”,看生成簽名的參數是否多加或缺少;
2、遇到“下單賬號和支付賬號不一致”,那肯定是你的openID是寫死的
解決方法:調用 微信網頁授權接口,獲取openID
3、微信H5內調起支付,返回“get_brand_wcpay_request:fail”,
原因:大概率是支付回調路徑配置錯誤;
4、如何正確配置 支付授權目錄:
保留支付路徑最後一個“/”(包括)及之前內容。
例:假如我的支付路徑爲 http://test.gusy.com/wxCourse/static/pay.html
則添加的路徑爲 http://test.gusy.com/wxCourse/static/;
這個地方我還遇到了一個坑:
做項目時自己一直使用本人的iphone手機測試,可以完美支付;當換用android手機測時,總是返回“get_brand_wcpay_request:fail”,
其間花費了大量時間和精力找解決方法,包括換用 微信JSSDK中調起支付的方法,全部失敗!!!
最終問題答案有些意想不到: iOS和Android 所配置支付路徑不一樣!!!
小結:
如果你們遇到iOS和Android中有一個可以成功支付,但另一個平臺不能支付, 那你們就要看看是不是支付路徑不一樣了 。
附:
本文只是本人寫的的一個測試demo,沒有數據庫相關操作,真實場景肯定需要商戶後臺生成一個訂單並存入數據庫,支付成功後修改訂單狀態等。需要的自己按需添加吧。
還有用戶授權部分的代碼沒有放上去,比較簡單點擊查看微信開發文檔。