概述
詳細
一、准備工作
先開通微信公眾號,再開通微信公眾號里面的微信支付功能,這些是前提條件,多說一句,申請開通微信公眾號需要等待審核,然后在開通微信支付功能,還得等待審核,前前后后耗時得好幾天。

關於准備工作,再看看微信官方關於“微信支付”的介紹,官方地址 https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_1。這個是文檔的准備,大概可以理解到微信支付有哪些模式,然后大概是怎樣一個東東。
然后重點看看如下幾個,實際上需要准備的東西,紅色花掉的部分(PayConfigUtil類里面),需要根據自己的實際情況填寫:

其中APP_ID和APP_SECRET可以在公眾平台找着,MCH_ID和API_KEY則在商戶平台找到,特別是API_KEY要在商戶平台設置好,對於“微信掃碼支付模式二”(支付與回調)實際只會用到APP_ID、MCH_ID和API_KEY,其他的都不用。
二、程序實現
這里使用spring mvc做一個購買商品,微信掃碼支付的演示。先項目代碼截圖,

以下摘取重點環節的代碼說明下:
1、首先是接入微信接口,獲取微信支付二維碼。
package com.demodashi;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import javax.inject.Named;
import com.demodashi.pay.util.HttpUtil;
import com.demodashi.pay.util.PayToolUtil;
import com.demodashi.pay.util.PayConfigUtil;
import com.demodashi.pay.util.XMLUtil4jdom;
@Named("userService")
public class UserServiceImpl implements UserService {
@Override
public String weixinPay(String userId, String productId) throws Exception {
String out_trade_no = "" + System.currentTimeMillis(); //訂單號 (調整為自己的生產邏輯)
// 賬號信息
String appid = PayConfigUtil.APP_ID; // appid
//String appsecret = PayConfigUtil.APP_SECRET; // appsecret
String mch_id = PayConfigUtil.MCH_ID; // 商業號
String key = PayConfigUtil.API_KEY; // key
String currTime = PayToolUtil.getCurrTime();
String strTime = currTime.substring(8, currTime.length());
String strRandom = PayToolUtil.buildRandom(4) + "";
String nonce_str = strTime + strRandom;
// 獲取發起電腦 ip
String spbill_create_ip = PayConfigUtil.CREATE_IP;
// 回調接口
String notify_url = PayConfigUtil.NOTIFY_URL;
String trade_type = "NATIVE";
SortedMap<Object,Object> packageParams = new TreeMap<Object,Object>();
packageParams.put("appid", appid);
packageParams.put("mch_id", mch_id);
packageParams.put("nonce_str", nonce_str);
packageParams.put("body", "可樂"); //(調整為自己的名稱)
packageParams.put("out_trade_no", out_trade_no);
packageParams.put("total_fee", "10"); //價格的單位為分
packageParams.put("spbill_create_ip", spbill_create_ip);
packageParams.put("notify_url", notify_url);
packageParams.put("trade_type", trade_type);
String sign = PayToolUtil.createSign("UTF-8", packageParams,key);
packageParams.put("sign", sign);
String requestXML = PayToolUtil.getRequestXml(packageParams);
System.out.println(requestXML);
String resXml = HttpUtil.postData(PayConfigUtil.UFDODER_URL, requestXML);
Map map = XMLUtil4jdom.doXMLParse(resXml);
String urlCode = (String) map.get("code_url");
return urlCode;
}
}
以上代碼會按照微信支付的協議,生成類似這樣格式的URL:weixin://wxpay/bizpayurl?pr=pIxXXXX
2、根據以上方法所產生的URL生成二維碼,這里采用我采用的是google的core.jar包來生成二維碼
@ResponseBody
@RequestMapping("/qrcode.do")
public void qrcode(HttpServletRequest request, HttpServletResponse response,
ModelMap modelMap) {
try {
String productId = request.getParameter("productId");
String userId = "user01";
String text = userApplication.weixinPay(userId, productId);
//根據url來生成生成二維碼
int width = 300;
int height = 300;
//二維碼的圖片格式
String format = "gif";
Hashtable hints = new Hashtable();
//內容所使用編碼
hints.put(EncodeHintType.CHARACTER_SET, "utf-8");
BitMatrix bitMatrix;
try {
bitMatrix = new MultiFormatWriter().encode(text, BarcodeFormat.QR_CODE, width, height, hints);
QRUtil.writeToStream(bitMatrix, format, response.getOutputStream());
} catch (WriterException e) {
e.printStackTrace();
}
} catch (Exception e) {
}
}
上面代碼中涉及到幾個工具類:PayConfigUtil、PayCommonUtil、HttpUtil和XMLUtil,其中PayConfigUtil放的就是上面提到一些配置及路徑,PayCommonUtil涉及到了獲取當前事件、產生隨機字符串、獲取參數簽名和拼接xml幾個方法,代碼如下:
package com.demodashi.pay.util;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
public class PayToolUtil {
/**
* 是否簽名正確,規則是:按參數名稱a-z排序,遇到空值的參數不參加簽名。
* @return boolean
*/
public static boolean isTenpaySign(String characterEncoding, SortedMap<Object, Object> packageParams, String API_KEY) {
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(!"sign".equals(k) && null != v && !"".equals(v)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + API_KEY);
//算出摘要
String mysign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toLowerCase();
String tenpaySign = ((String)packageParams.get("sign")).toLowerCase();
//System.out.println(tenpaySign + " " + mysign);
return tenpaySign.equals(mysign);
}
/**
* @author
* @date 2016-4-22
* @Description:sign簽名
* @param characterEncoding
* 編碼格式
* @param parameters
* 請求參數
* @return
*/
public static String createSign(String characterEncoding, SortedMap<Object, Object> packageParams, String API_KEY) {
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=" + API_KEY);
String sign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toUpperCase();
return sign;
}
/**
* @author
* @date 2016-4-22
* @Description:將請求參數轉換為xml格式的string
* @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();
}
/**
* 取出一個指定長度大小的隨機正整數.
*
* @param length
* int 設定所取出隨機數的長度。length小於11
* @return int 返回生成的隨機數。
*/
public static int buildRandom(int length) {
int num = 1;
double random = Math.random();
if (random < 0.1) {
random = random + 0.1;
}
for (int i = 0; i < length; i++) {
num = num * 10;
}
return (int) ((random * num));
}
/**
* 獲取當前時間 yyyyMMddHHmmss
*
* @return String
*/
public static String getCurrTime() {
Date now = new Date();
SimpleDateFormat outFormat = new SimpleDateFormat("yyyyMMddHHmmss");
String s = outFormat.format(now);
return s;
}
}
HttpUtil類如下:
package com.demodashi.pay.util;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.net.URLConnection;
/**
* http工具類,負責發起post請求並獲取的返回
*/
public class HttpUtil {
private final static int CONNECT_TIMEOUT = 5000; // in milliseconds
private final static String DEFAULT_ENCODING = "UTF-8";
public static String postData(String urlStr, String data){
return postData(urlStr, data, null);
}
public static String postData(String urlStr, String data, String contentType){
BufferedReader reader = null;
try {
URL url = new URL(urlStr);
URLConnection conn = url.openConnection();
conn.setDoOutput(true);
conn.setConnectTimeout(CONNECT_TIMEOUT);
conn.setReadTimeout(CONNECT_TIMEOUT);
if(contentType != null)
conn.setRequestProperty("content-type", contentType);
OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream(), DEFAULT_ENCODING);
if(data == null)
data = "";
writer.write(data);
writer.flush();
writer.close();
reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), DEFAULT_ENCODING));
StringBuilder sb = new StringBuilder();
String line = null;
while ((line = reader.readLine()) != null) {
sb.append(line);
sb.append("\r\n");
}
return sb.toString();
} catch (IOException e) {
//logger.error("Error connecting to " + urlStr + ": " + e.getMessage());
} finally {
try {
if (reader != null)
reader.close();
} catch (IOException e) {
}
}
return null;
}
}
XMLUtil4jdom類如下:
package com.demodashi.pay.util;
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 org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
public class XMLUtil4jdom {
/**
* 解析xml,返回第一級元素鍵值對。如果第一級元素有子節點,則此節點的值是子節點的xml數據。
* @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<String, String> m = new HashMap<String, String>();
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 = XMLUtil4jdom.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(XMLUtil4jdom.getChildrenText(list));
}
sb.append(value);
sb.append("</" + name + ">");
}
}
return sb.toString();
}
}
2、支付回調
支付完成后,微信會把相關支付結果和用戶信息發送到我們上面指定的那個回調地址,我們需要接收處理,並返回應答。對后台通知交互時,如果微信收到商戶的應答不是成功或超時,微信認為通知失敗,微信會通過一定的策略定期重新發起通知,盡可能提高通知的成功率,但微信不保證通知最終能成功。 (通知頻率為15/15/30/180/1800/1800/1800/1800/3600,單位:秒)
關於支付回調接口,我們首先要對於支付結果通知的內容進行簽名驗證,然后根據支付結果進行相應的處理流程即可。
支付回調需要在微信公眾號的微信支付里面設置回調地址:

/**
* 微信平台發起的回調方法,
* 調用我們這個系統的這個方法接口,將掃描支付的處理結果告知我們系統
* @throws JDOMException
* @throws Exception
*/
public void weixinNotify(HttpServletRequest request, HttpServletResponse response) throws JDOMException, Exception{
//讀取參數
InputStream inputStream ;
StringBuffer sb = new StringBuffer();
inputStream = request.getInputStream();
String s ;
BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
while ((s = in.readLine()) != null){
sb.append(s);
}
in.close();
inputStream.close();
//解析xml成map
Map<String, String> m = new HashMap<String, String>();
m = XMLUtil4jdom.doXMLParse(sb.toString());
//過濾空 設置 TreeMap
SortedMap<Object,Object> packageParams = new TreeMap<Object,Object>();
Iterator it = m.keySet().iterator();
while (it.hasNext()) {
String parameter = (String) it.next();
String parameterValue = m.get(parameter);
String v = "";
if(null != parameterValue) {
v = parameterValue.trim();
}
packageParams.put(parameter, v);
}
// 賬號信息
String key = PayConfigUtil.API_KEY; //key
//判斷簽名是否正確
if(PayToolUtil.isTenpaySign("UTF-8", packageParams,key)) {
//------------------------------
//處理業務開始
//------------------------------
String resXml = "";
if("SUCCESS".equals((String)packageParams.get("result_code"))){
// 這里是支付成功
//////////執行自己的業務邏輯////////////////
String mch_id = (String)packageParams.get("mch_id");
String openid = (String)packageParams.get("openid");
String is_subscribe = (String)packageParams.get("is_subscribe");
String out_trade_no = (String)packageParams.get("out_trade_no");
String total_fee = (String)packageParams.get("total_fee");
//////////執行自己的業務邏輯////////////////
//暫時使用最簡單的業務邏輯來處理:只是將業務處理結果保存到session中
//(根據自己的實際業務邏輯來調整,很多時候,我們會操作業務表,將返回成功的狀態保留下來)
request.getSession().setAttribute("_PAY_RESULT", "OK");
System.out.println("支付成功");
//通知微信.異步確認成功.必寫.不然會一直通知后台.八次之后就認為交易失敗了.
resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"
+ "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";
} else {
resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
+ "<return_msg><![CDATA[報文為空]]></return_msg>" + "</xml> ";
}
//------------------------------
//處理業務完畢
//------------------------------
BufferedOutputStream out = new BufferedOutputStream(
response.getOutputStream());
out.write(resXml.getBytes());
out.flush();
out.close();
} else{
System.out.println("通知簽名驗證失敗");
}
}
3、支付后網頁自動跳轉
web頁面彈出二維碼后,就開啟輪詢,詢問系統后台支付有微信平台的成功支付返回了,如果有,則跳轉到支付成功的頁面。
<%@ page language="java" pageEncoding="UTF-8"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script type="text/javascript" charset="utf-8" src="/resource/js/jquery-2.min.js"></script>
<script type="text/javascript" charset="utf-8" src="/resource/js/layer/layer.js"></script>
<title>微信掃碼支付例子</title>
</head>
<body>
<form id="pay_form" method="post" >
<h1>可樂特價:0.1元/罐 <input id="pay_submit" name="but" type="button" value="微信支付"/></h1>
</form>
</body>
<script>
$(function(){
$("#pay_submit").click(function(){
buy('001');//傳入可樂的ID號
});
});
/**
* 購買
*/
function buy(productId){
//打開付費二維碼 -- 微信二維碼
layer.open({
area: ['300px', '300px'],
type: 2,
closeBtn: false,
title: false,
shift: 2,
shadeClose: true,
content:'../user/qrcode.do?productId=' + productId
});
//重復執行某個方法
var t1 = window.setInterval("getPayState('" + productId + "')",1500);
}
function getPayState(productId){
var url = '../user/hadPay.do?productId=' + productId;
//輪詢是否已經付費
$.ajax({
type:'post',
url:url,
data:{productId:productId},
cache:false,
async:true,
success:function(json){
if(json.result == 0){
location.href = '/result.jsp';
}
},
error:function(){
layer.msg("執行錯誤!", 8);
}
});
}
</script>
</html>
三、運行效果
項目導入eclipse后,發表到tomcat中運行,或者通過jetty運行,跑起來后,訪問:

點擊微信支付:

這個時候在手機上用微信掃碼:

支付成功后:

然后web網頁會跳轉到購買成功的頁面,這里需要注意,微信支付回調接口,最好部署在公網的服務器上,這樣能被回調,我本地使用改hosts的方法來讓支付回調,不成功。

四、注意點
本例子為了演示,所以一些業務邏輯特別簡單,例如:訂單號的生產,這里只是簡單的用當前時間long數字來表示:
String out_trade_no = "" + System.currentTimeMillis(); //訂單號 (調整為自己的生產邏輯)
實際開發的時候需要考慮並且情況下的訂單號的唯一性。
還有,回調接口,考慮很簡單:
//////////執行自己的業務邏輯////////////////
//暫時使用最簡單的業務邏輯來處理:只是將業務處理結果保存到session中
//(根據自己的實際業務邏輯來調整,很多時候,我們會操作業務表,將返回成功的狀態保留下來)
request.getSession().setAttribute("_PAY_RESULT", "OK");
System.out.println("支付成功");
實際開發,要把支付成功DB保存下來,以及回調信息log下來等等
注:本文著作權歸作者,由demo大師發表,拒絕轉載,轉載需要作者授權
