本人用的開發(fā)框架是:struts2(用了struts2的0配置,對(duì)于struts的0配置不熟悉的可以看看這個(gè)博客了解下 http://www.cnblogs.com/fpjason/archive/2009/08/01/1536671.html)
本人做的是微信V3版本的微信支付,也是目前最新的微信支付接口。官方文檔下載地址
https://mp.weixin.qq.com/paymch/readtemplate?t=mp/business/course3_tmpl&lang=zh_CN
微信支付成功后 你的郵件會(huì)有以下信息:
1、 信息包括:商戶ID(mch_id)、申請(qǐng)編號(hào)、登錄賬號(hào)、登錄密碼、商戶API密碼(key)
2.、證書包括:商戶API證書、證書密鑰、CA證書
開發(fā)前,我們先登錄自己的服務(wù)號(hào),點(diǎn)擊微信支付----->開發(fā)配置。
如果這里的授權(quán)路徑和下面參數(shù)的notify_url不對(duì) 調(diào)用付款接口時(shí)會(huì)彈出access_denied。
比如,我的授權(quán)目錄是http://183.33.212.175/wxweb/config/,那么我對(duì)應(yīng)的notify_url的回調(diào)方法必須是String notify_url = "http://183.33.212.175:8016/wxweb/config/pay!paySuccess.action";這樣的
下圖是微信支付JSAPI支付方法的流程圖。
登陸Oauth2授權(quán)的問題,我會(huì)另外寫篇博客。在此略過。需要注意一點(diǎn)的是,微信支付是微信5.0以上版本才能支持,所以,可能有些用戶的微信版本是低版本無法進(jìn)行微信支付而用戶又不知道為什么就是不能支付,我們需要進(jìn)行一個(gè)版本判斷,如果用戶的微信版本低于5.0就告知用戶一些提示信息。那么,如何判斷用戶的微信版本是多少呢?我們可以通過這個(gè)方法可以獲取一些信息然后進(jìn)行判斷:request.getHeader("user-agent")獲取信息。
以 iPhone 版本為例,可以通過 user agent 可獲取如下微信版本示例信息:
"Mozilla/5.0(iphone;CPU iphone OS 5_1_1 like Mac OS X) AppleWebKit/534.46(KHTML,like Geocko) Mobile/9B206 MicroMessenger/5.0"
其中 5.0 為用戶安裝的微信版本號(hào),商戶可以判定版本號(hào)是否高于或者等于 5.0。具體判斷代碼請(qǐng)留意后面代碼。
流程圖中,我們看到需要先調(diào)用一次統(tǒng)一接口,然后微信會(huì)返回一個(gè)prepayId給我們。那么我們先來調(diào)用統(tǒng)一接口。調(diào)用接口的URL地址:https://api.mch.weixin.qq.com/pay/unifiedorder,調(diào)用統(tǒng)一接口需要以下參數(shù)。
看到這些參數(shù),不難理解需要傳入什么。那么參數(shù)中的sign需要以下規(guī)則進(jìn)行生成。
下面是我生成sign的方法。注意,參數(shù)是必填項(xiàng)的必須加進(jìn)去。sign的生成規(guī)則是,除了上除列表中sign字段為所有的參數(shù)都可以參與sign的生成且參與生成sign的參數(shù)值不為空.
/**
* @author 李欣樺
* @date 2014-12-5下午2:29:34
* @Description:sign簽名
* @param characterEncoding 編碼格式
* @param parameters 請(qǐng)求參數(shù)
* @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=" + ConfigUtil.API_KEY);
String sign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toUpperCase();
return sign;
}
MD5Util工具類中相關(guān)的方法
public class MD5Util {
private static String byteArrayToHexString(byte b[]) {
StringBuffer resultSb = new StringBuffer();
for (int i = 0; i < b.length; i++)
resultSb.append(byteToHexString(b[i]));
return resultSb.toString();
}
private static String byteToHexString(byte b) {
int n = b;
if (n < 0)
n += 256;
int d1 = n / 16;
int d2 = n % 16;
return hexDigits[d1] + hexDigits[d2];
}
public static String MD5Encode(String origin, String charsetname) {
String resultString = null;
try {
resultString = new String(origin);
MessageDigest md = MessageDigest.getInstance("MD5");
if (charsetname == null || "".equals(charsetname))
resultString = byteArrayToHexString(md.digest(resultString
.getBytes()));
else
resultString = byteArrayToHexString(md.digest(resultString
.getBytes(charsetname)));
} catch (Exception exception) {
}
return resultString;
}
private static final String hexDigits[] = { "0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };
}
各位需要記住,我們發(fā)送給微信服務(wù)器的參數(shù)是xml格式的string,微信返回來的信息也是xml格式的string。
我們提交的正確數(shù)據(jù)應(yīng)該是這樣的。
<xml>
<appid>wx2421b1c4370ec43b</appid>
<attach><![CDATA[att1]]></attach>
<body><![CDATA[JSAPI 支付測(cè)試]]></body>
<device_info>1000</device_info>
<mch_id>10000100</mch_id>
<nonce_str>b927722419c52622651a871d1d9ed8b2</nonce_str>
<notify_url>http://wxpay.weixin.qq.com/pub_v2/pay/notify.php<;/notify_url>
<out_trade_no>1405713376</out_trade_no>
<spbill_create_ip>127.0.0.1</spbill_create_ip>
<total_fee>1</total_fee>
<trade_type>JSAPI</trade_type>
<sign><![CDATA[3CA89B5870F944736C657979192E1CF4]]></sign>
</xml>
那么。我們來生成這樣的post data
SortedMap<Object,Object> parameters = new TreeMap<Object,Object>();
parameters.put("appid", ConfigUtil.APPID);
parameters.put("mch_id", ConfigUtil.MCH_ID);
parameters.put("nonce_str", PayCommonUtil.CreateNoncestr());
parameters.put("body", "LEO測(cè)試");
parameters.put("out_trade_no", "201412051510");
parameters.put("total_fee", "1");
parameters.put("spbill_create_ip",IpAddressUtil.getIpAddr(request));
parameters.put("notify_url", ConfigUtil.NOTIFY_URL);
parameters.put("trade_type", "JSAPI");
parameters.put("openid", "o7W6yt9DUOBpjEYogs4by1hD_OQE");
String sign = PayCommonUtil.createSign("UTF-8", parameters);
parameters.put("sign", sign);
String requestXML = PayCommonUtil.getRequestXml(parameters);
PayCommonUtil.getRequestXml(parameters)方法如下:
/**
* @author 李欣樺
* @date 2014-12-5下午2:32:05
* @Description:將請(qǐng)求參數(shù)轉(zhuǎn)換為xml格式的string
* @param parameters 請(qǐng)求參數(shù)
* @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();
}
最后我們要將這些參數(shù)以POST方式調(diào)用微信統(tǒng)一支付接口:代碼如下
String result =CommonUtil.httpsRequest(ConfigUtil.UNIFIED_ORDER_URL, "POST", requestXML);
CommonUtil.httpsRequest(ConfigUtil.UNIFIED_ORDER_URL, "POST", requestXML);方法如下:
/**
* 發(fā)送https請(qǐng)求
* @param requestUrl 請(qǐng)求地址
* @param requestMethod 請(qǐng)求方式(GET、POST)
* @param outputStr 提交的數(shù)據(jù)
* @return 返回微信服務(wù)器響應(yīng)的信息
*/
public static String httpsRequest(String requestUrl, String requestMethod, String outputStr) {
try {
// 創(chuàng)建SSLContext對(duì)象,并使用我們指定的信任管理器初始化
TrustManager[] tm = { new MyX509TrustManager() };
SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
sslContext.init(null, tm, new java.security.SecureRandom());
// 從上述SSLContext對(duì)象中得到SSLSocketFactory對(duì)象
SSLSocketFactory ssf = sslContext.getSocketFactory();
URL url = new URL(requestUrl);
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(ssf);
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
// 設(shè)置請(qǐng)求方式(GET/POST)
conn.setRequestMethod(requestMethod);
conn.setRequestProperty("content-type", "application/x-www-form-urlencoded");
// 當(dāng)outputStr不為null時(shí)向輸出流寫數(shù)據(jù)
if (null != outputStr) {
OutputStream outputStream = conn.getOutputStream();
// 注意編碼格式
outputStream.write(outputStr.getBytes("UTF-8"));
outputStream.close();
}
// 從輸入流讀取返回內(nèi)容
InputStream inputStream = conn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
StringBuffer buffer = new StringBuffer();
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
// 釋放資源
bufferedReader.close();
inputStreamReader.close();
inputStream.close();
inputStream = null;
conn.disconnect();
return buffer.toString();
} catch (ConnectException ce) {
log.error("連接超時(shí):{}", ce);
} catch (Exception e) {
log.error("https請(qǐng)求異常:{}", e);
}
return null;
}
接著,我們來解析微信返回給我們的信息。返回信息是xml格式的String
XMLUtil.工具類如下:
public class XMLUtil {
/**
* 解析xml,返回第一級(jí)元素鍵值對(duì)。如果第一級(jí)元素有子節(jié)點(diǎn),則此節(jié)點(diǎn)的值是子節(jié)點(diǎn)的xml數(shù)據(jù)。
* @param strxml
* @return
* @throws JDOMException
* @throws IOException
*/
public static Map doXMLParse(String strxml) throws JDOMException, IOException {
strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");
if(null == strxml || "".equals(strxml)) {
return null;
}
Map m = new HashMap();
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 = XMLUtil.getChildrenText(children);
}
m.put(k, v);
}
//關(guān)閉流
in.close();
return m;
}
/**
* 獲取子結(jié)點(diǎn)的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(XMLUtil.getChildrenText(list));
}
sb.append(value);
sb.append("</" + name + ">");
}
}
return sb.toString();
}
}
Map<String, String> map = XMLUtil.doXMLParse(result);//解析微信返回的信息,以Map形式存儲(chǔ)便于取值
現(xiàn)在我們可以拿到想要拿的值,那么可以調(diào)用微信H5網(wǎng)頁(yè)端調(diào)用支付接口。在微信瀏覽器里面打開 H5 網(wǎng)頁(yè)中執(zhí)行 JS 調(diào)起支付。接口輸入輸出數(shù)據(jù)格式為 JSON。
注意:WeixinJSBridge 內(nèi)置對(duì)象在其他瀏覽器中無效;列表中參數(shù)名區(qū)分大小。
現(xiàn)在我們將返回的值傳入到支付jsp頁(yè)面,在支付jsp頁(yè)面調(diào)用支付接口。
SortedMap<Object,Object> params = new TreeMap<Object,Object>();
params.put("appId", "wx0953bae287adfeee");
params.put("timeStamp", Long.toString(new Date().getTime()));
params.put("nonceStr", PayCommonUtil.CreateNoncestr());
params.put("package", "prepay_id="+map.get("prepay_id"));
params.put("signType", ConfigUtil.SIGN_TYPE);
String paySign = PayCommonUtil.createSign("UTF-8", params);
params.put("packageValue", "prepay_id="+map.get("prepay_id")); //這里用packageValue是預(yù)防package是關(guān)鍵字在js獲取值出錯(cuò)
params.put("paySign", paySign); //paySign的生成規(guī)則和Sign的生成規(guī)則一致
params.put("sendUrl", ConfigUtil.SUCCESS_URL); //付款成功后跳轉(zhuǎn)的頁(yè)面
String userAgent = request.getHeader("user-agent");
char agent = userAgent.charAt(userAgent.indexOf("MicroMessenger")+15);
params.put("agent", new String(new char[]{agent}));//微信版本號(hào),用于前面提到的判斷用戶手機(jī)微信的版本是否是5.0以上版本。
String json = JSONObject.fromObject(params).toString();
下面是一個(gè)簡(jiǎn)潔的支付頁(yè)面。代碼如下:
- <body>
- <form action="" method="post" >
- <input type="button" value="確認(rèn)支付" name="ajaxLoadId" id="test"/>
- </form>
- <script type="text/javascript">
- var basePath = "<%=basePath%>";
- $("#test").one("click",function(){
- $.ajax({
- url:basePath+"config/pay!execute.action" //<span style="font-family:微軟雅黑;">ajax調(diào)用微信統(tǒng)一接口獲取prepayId</span>
- }).done(function(data){
- var obj = eval('(' + data + ')');
- if(parseInt(obj.agent)<5){
- alert("您的微信版本低于5.0無法使用微信支付");
- return;
- }
- WeixinJSBridge.invoke('getBrandWCPayRequest',{
- "appId" : obj.appId, //公眾號(hào)名稱,由商戶傳入
- "timeStamp":obj.timeStamp, //時(shí)間戳,自 1970 年以來的秒數(shù)
- "nonceStr" : obj.nonceStr, //隨機(jī)串
- "package" : obj.packageValue, //<span style="font-family:微軟雅黑;">商品包信息</span>
- "signType" : obj.signType, //微信簽名方式:
- "paySign" : obj.paySign //微信簽名
- },function(res){
- alert(res.err_msg);
- if(res.err_msg == "get_brand_wcpay_request:ok" ) {
- window.location.href=obj.sendUrl;
- }else{
- alert("fail");
- window.location.href="http://183.45.18.197:8016/wxweb/config/oauth!execute.action";
- //<span style="font-family:微軟雅黑;">當(dāng)失敗后,繼續(xù)跳轉(zhuǎn)該支付頁(yè)面讓用戶可以繼續(xù)付款,貼別注意不能直接調(diào)轉(zhuǎn)jsp,</span><span style="font-size:10.5pt">不然會(huì)報(bào)</span><span style="font-size:12.0pt"> system:access_denied。</span>
- }
- });
-
- });
- });
- </script>
- </body>
支付成功后,微信就會(huì)調(diào)用你填寫的notify_url的方法,本人微信支付的開發(fā)配置中說明了我的notify_url為http://183.33.212.175:8016/wxweb/config/pay!paySuccess.action
對(duì)后臺(tái)通知交互時(shí),如果微信收到商戶的應(yīng)答不是成功或超時(shí),微信認(rèn)為通知失敗,微信會(huì)通過一定的策略(如
30 分鐘共 8 次)定期重新發(fā)起通知,盡可能提高通知的成功率,
但微信不保證通知最終能成功。由于存在重新収送后臺(tái)通知的情況,因此同樣的通知可能會(huì)多次収送給商戶系統(tǒng)。 商戶系統(tǒng)必須能夠正確處理重復(fù)的通知。
推薦的做法是,當(dāng)收到通知進(jìn)行處理時(shí),首先檢查對(duì)應(yīng)業(yè)務(wù)數(shù)據(jù)的狀態(tài),判斷該通知是否已經(jīng)處理過,如果沒有處理過再進(jìn)行處理,如果處理過直接返回結(jié)果成功。在對(duì)業(yè)務(wù)數(shù)據(jù)
進(jìn)行狀態(tài)檢查和處理之前,要采用數(shù)據(jù)鎖進(jìn)行幵収控制,以避免凼數(shù)重入造成的數(shù)據(jù)混亂。
判斷完成后,我們需要通知微信,我們收到信息了,不然微信就會(huì)通過一定的策略定期重新發(fā)起通知。
怎么通知微信呢。方法如下。
public
void paySuccess() throws Exception{
HttpServletRequest request = ServletActionContext.getRequest();
HttpServletResponse response = ServletActionContext.getResponse();
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);
}
System.out.println("~~~~~~~~~~~~~~~~付款成功~~~~~~~~~");
outSteam.close();
inStream.close();
String result = new String(outSteam.toByteArray(),"utf-8");//獲取微信調(diào)用我們notify_url的返回信息
Map<Object, Object> map = XMLUtil.doXMLParse(result);
for(Object keyValue : map.keySet()){
System.out.println(keyValue+"="+map.get(keyValue));
}
if (map.get("result_code").toString().equalsIgnoreCase("SUCCESS")) {
//TODO 對(duì)數(shù)據(jù)庫(kù)的操作
response.getWriter().write(PayCommonUtil.setXML("SUCCESS", "")); //告訴微信服務(wù)器,我收到信息了,不要在調(diào)用回調(diào)action了
System.out.println("-------------"+PayCommonUtil.setXML("SUCCESS", ""));
}
}
PayCommonUtil.setXML("SUCCESS",
""))方法如下:
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>";
}
上面代碼中工具類打包下載地址:http://download.csdn.net/detail/u011160656/8354883
官方文檔有少許常見錯(cuò)誤解決方法,觀眾們可自行下載查閱,仍有不懂的可留言。
第一次寫這么長(zhǎng)的博客,不足之處請(qǐng)留言告之便于本人成長(zhǎng),再次感謝你的閱讀。
|