最近公司在做支付模塊,在接入過程中遇到了很多坑,費了不少事,現在分享一下接入方法,也記錄一下,以後可能還用的到。用的是支付寶的即時到帳支付功能和微信的掃碼支付功能,相比起來,個人感覺支付寶的文檔和接入方式都比微信的容易理解和操作,也不用自己寫頁面,接入起來比較方便,畢竟是支付起家的,比微信支付少很多坑,下面就分別介紹着兩種支付的接入方法。
支付寶支付
1、申請簽約
目的是得到開發使用的合作伙伴身份(PID)和MD5祕鑰,申請地址(即時到賬收款):https://b.alipay.com/order/productDetail.htm?productId=2015110218012942
申請方式在開放平臺的文檔上有詳細說明,這裏就不再贅述。
2、接入支付接口
在得到PID和祕鑰後就可以接入接口了,首先在開放平臺中下載官方的demo(java+MD5版本),支付寶的demo做的非常好,下載下來直接配置下jdk就可以運行了。如果遇到Java compiler level does not match錯誤,說明你用的eclipse或myeclipse的jdk編譯版本與demo的JDK編譯版本不一致,修改下jdk編譯版本就可以了。其實就用到了4個類,如下圖
可以選擇把支付功能單獨做一個項目,在其他項目調用接口就可以支付,也可以整合到自己的項目裏,爲了好維護我整合到自己的項目裏了。把這四個類放到自己的項目中,引入相應的jar包
2.1、demo中類的說明
AlipayConfig.java類主要是配置參數信息的類
package com.fahai.pay.alipay;
import com.fahai.utils.ProInfoUtil;
/* *
*類名:AlipayConfig
*功能:基礎配置類
*詳細:設置帳戶有關信息及返回路徑
*版本:3.4
*修改日期:2016-03-08
*說明:
*以下代碼只是爲了方便商戶測試而提供的樣例代碼,商戶可以根據自己網站的需要,按照技術文檔編寫,並非一定要使用該代碼。
*該代碼僅供學習和研究支付寶接口使用,只是提供一個參考。
*/
public class AlipayConfig {
//↓↓↓↓↓↓↓↓↓↓請在這裏配置您的基本信息↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
// 合作身份者ID,簽約賬號,以2088開頭由16位純數字組成的字符串,查看地址:https://b.alipay.com/order/pidAndKey.htm
public static String partner = "你自己的PID";
// 收款支付寶賬號,以2088開頭由16位純數字組成的字符串,一般情況下收款賬號就是簽約賬號
public static String seller_id = partner;
// MD5密鑰,安全檢驗碼,由數字和字母組成的32位字符串,查看地址:https://b.alipay.com/order/pidAndKey.htm
public static String key = "你自己的MD5祕鑰";
// 服務器異步通知頁面路徑 需http://格式的完整路徑,不能加?id=123這類自定義參數,必須外網可以正常訪問
//異步通知頁面,就是接受支付寶支付結果返回信息的Controller,可以處理自己的支付後的邏輯
//測試環境
public static String notify_url = ProInfoUtil.getInstance().getProperty("project_url")+"order/pay/aliPayOrder";
// 頁面跳轉同步通知頁面路徑 需http://格式的完整路徑,不能加?id=123這類自定義參數,必須外網可以正常訪問
//同步跳轉的頁面,就是支付寶支付成功後頁面跳轉的url
public static String return_url = ProInfoUtil.getInstance().getProperty("project_url")+"order/pay/payResponse";
// 簽名方式
public static String sign_type = "MD5";
// 調試用,創建TXT日誌文件夾路徑,見AlipayCore.java類中的logResult(String sWord)打印方法。
public static String log_path = "C:\\";
// 字符編碼格式 目前支持 gbk 或 utf-8
public static String input_charset = "utf-8";
// 支付類型 ,無需修改
public static String payment_type = "1";
// 調用的接口名,無需修改
public static String service = "create_direct_pay_by_user";
//↑↑↑↑↑↑↑↑↑↑請在這裏配置您的基本信息↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
//↓↓↓↓↓↓↓↓↓↓ 請在這裏配置防釣魚信息,如果沒開通防釣魚功能,爲空即可 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
// 防釣魚時間戳 若要使用請調用類文件submit中的query_timestamp函數
public static String anti_phishing_key = "";
// 客戶端的IP地址 非局域網的外網IP地址,如:221.0.0.1
public static String exter_invoke_ip = "";
//↑↑↑↑↑↑↑↑↑↑請在這裏配置防釣魚信息,如果沒開通防釣魚功能,爲空即可 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
}
AlipayCore.java是整理參數的工具類
package com.alipay.util;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.httpclient.methods.multipart.FilePartSource;
import org.apache.commons.httpclient.methods.multipart.PartSource;
import com.alipay.config.AlipayConfig;
/* *
*類名:AlipayFunction
*功能:支付寶接口公用函數類
*詳細:該類是請求、通知返回兩個文件所調用的公用函數核心處理文件,不需要修改
*版本:3.3
*日期:2012-08-14
*說明:
*以下代碼只是爲了方便商戶測試而提供的樣例代碼,商戶可以根據自己網站的需要,按照技術文檔編寫,並非一定要使用該代碼。
*該代碼僅供學習和研究支付寶接口使用,只是提供一個參考。
*/
public class AlipayCore {
/**
* 除去數組中的空值和簽名參數
* @param sArray 簽名參數組
* @return 去掉空值與簽名參數後的新簽名參數組
*/
public static Map<String, String> paraFilter(Map<String, String> sArray) {
Map<String, String> result = new HashMap<String, String>();
if (sArray == null || sArray.size() <= 0) {
return result;
}
for (String key : sArray.keySet()) {
String value = sArray.get(key);
if (value == null || value.equals("") || key.equalsIgnoreCase("sign")
|| key.equalsIgnoreCase("sign_type")) {
continue;
}
result.put(key, value);
}
return result;
}
/**
* 把數組所有元素排序,並按照“參數=參數值”的模式用“&”字符拼接成字符串
* @param params 需要排序並參與字符拼接的參數組
* @return 拼接後字符串
*/
public static String createLinkString(Map<String, String> params) {
List<String> keys = new ArrayList<String>(params.keySet());
Collections.sort(keys);
String prestr = "";
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
String value = params.get(key);
if (i == keys.size() - 1) {//拼接時,不包括最後一個&字符
prestr = prestr + key + "=" + value;
} else {
prestr = prestr + key + "=" + value + "&";
}
}
return prestr;
}
/**
* 寫日誌,方便測試(看網站需求,也可以改成把記錄存入數據庫)
* @param sWord 要寫入日誌裏的文本內容
*/
public static void logResult(String sWord) {
FileWriter writer = null;
try {
writer = new FileWriter(AlipayConfig.log_path + "alipay_log_" + System.currentTimeMillis()+".txt");
writer.write(sWord);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 生成文件摘要
* @param strFilePath 文件路徑
* @param file_digest_type 摘要算法
* @return 文件摘要結果
*/
public static String getAbstract(String strFilePath, String file_digest_type) throws IOException {
PartSource file = new FilePartSource(new File(strFilePath));
if(file_digest_type.equals("MD5")){
return DigestUtils.md5Hex(file.createInputStream());
}
else if(file_digest_type.equals("SHA")) {
return DigestUtils.sha256Hex(file.createInputStream());
}
else {
return "";
}
}
}
AlipayNotify.java是驗證簽名的類
package com.alipay.util;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Map;
import com.alipay.config.AlipayConfig;
import com.alipay.sign.MD5;
/* *
*類名:AlipayNotify
*功能:支付寶通知處理類
*詳細:處理支付寶各接口通知返回
*版本:3.3
*日期:2012-08-17
*說明:
*以下代碼只是爲了方便商戶測試而提供的樣例代碼,商戶可以根據自己網站的需要,按照技術文檔編寫,並非一定要使用該代碼。
*該代碼僅供學習和研究支付寶接口使用,只是提供一個參考
*************************注意*************************
*調試通知返回時,可查看或改寫log日誌的寫入TXT裏的數據,來檢查通知返回是否正常
*/
public class AlipayNotify {
/**
* 支付寶消息驗證地址
*/
private static final String HTTPS_VERIFY_URL = "https://mapi.alipay.com/gateway.do?service=notify_verify&";
/**
* 驗證消息是否是支付寶發出的合法消息
* @param params 通知返回來的參數數組
* @return 驗證結果
*/
public static boolean verify(Map<String, String> params) {
//判斷responsetTxt是否爲true,isSign是否爲true
//responsetTxt的結果不是true,與服務器設置問題、合作身份者ID、notify_id一分鐘失效有關
//isSign不是true,與安全校驗碼、請求時的參數格式(如:帶自定義參數等)、編碼格式有關
String responseTxt = "false";
if(params.get("notify_id") != null) {
String notify_id = params.get("notify_id");
responseTxt = verifyResponse(notify_id);
}
String sign = "";
if(params.get("sign") != null) {sign = params.get("sign");}
boolean isSign = getSignVeryfy(params, sign);
//寫日誌記錄(若要調試,請取消下面兩行註釋)
//String sWord = "responseTxt=" + responseTxt + "\n isSign=" + isSign + "\n 返回回來的參數:" + AlipayCore.createLinkString(params);
//AlipayCore.logResult(sWord);
if (isSign && responseTxt.equals("true")) {
return true;
} else {
return false;
}
}
/**
* 根據反饋回來的信息,生成簽名結果
* @param Params 通知返回來的參數數組
* @param sign 比對的簽名結果
* @return 生成的簽名結果
*/
private static boolean getSignVeryfy(Map<String, String> Params, String sign) {
//過濾空值、sign與sign_type參數
Map<String, String> sParaNew = AlipayCore.paraFilter(Params);
//獲取待簽名字符串
String preSignStr = AlipayCore.createLinkString(sParaNew);
//獲得簽名驗證結果
boolean isSign = false;
if(AlipayConfig.sign_type.equals("MD5") ) {
isSign = MD5.verify(preSignStr, sign, AlipayConfig.key, AlipayConfig.input_charset);
}
return isSign;
}
/**
* 獲取遠程服務器ATN結果,驗證返回URL
* @param notify_id 通知校驗ID
* @return 服務器ATN結果
* 驗證結果集:
* invalid命令參數不對 出現這個錯誤,請檢測返回處理中partner和key是否爲空
* true 返回正確信息
* false 請檢查防火牆或者是服務器阻止端口問題以及驗證時間是否超過一分鐘
*/
private static String verifyResponse(String notify_id) {
//獲取遠程服務器ATN結果,驗證是否是支付寶服務器發來的請求
String partner = AlipayConfig.partner;
String veryfy_url = HTTPS_VERIFY_URL + "partner=" + partner + "¬ify_id=" + notify_id;
return checkUrl(veryfy_url);
}
/**
* 獲取遠程服務器ATN結果
* @param urlvalue 指定URL路徑地址
* @return 服務器ATN結果
* 驗證結果集:
* invalid命令參數不對 出現這個錯誤,請檢測返回處理中partner和key是否爲空
* true 返回正確信息
* false 請檢查防火牆或者是服務器阻止端口問題以及驗證時間是否超過一分鐘
*/
private static String checkUrl(String urlvalue) {
String inputLine = "";
try {
URL url = new URL(urlvalue);
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection
.getInputStream()));
inputLine = in.readLine().toString();
} catch (Exception e) {
e.printStackTrace();
inputLine = "";
}
return inputLine;
}
}
AlipaySubmit.java模擬form表單請求支付寶支付接口的類
package com.alipay.util;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import com.alipay.config.AlipayConfig;
import com.alipay.sign.MD5;
/* *
*類名:AlipaySubmit
*功能:支付寶各接口請求提交類
*詳細:構造支付寶各接口表單HTML文本,獲取遠程HTTP數據
*版本:3.3
*日期:2012-08-13
*說明:
*以下代碼只是爲了方便商戶測試而提供的樣例代碼,商戶可以根據自己網站的需要,按照技術文檔編寫,並非一定要使用該代碼。
*該代碼僅供學習和研究支付寶接口使用,只是提供一個參考。
*/
public class AlipaySubmit {
/**
* 支付寶提供給商戶的服務接入網關URL(新)
*/
private static final String ALIPAY_GATEWAY_NEW = "https://mapi.alipay.com/gateway.do?";
/**
* 生成簽名結果
* @param sPara 要簽名的數組
* @return 簽名結果字符串
*/
public static String buildRequestMysign(Map<String, String> sPara) {
String prestr = AlipayCore.createLinkString(sPara); //把數組所有元素,按照“參數=參數值”的模式用“&”字符拼接成字符串
String mysign = "";
if(AlipayConfig.sign_type.equals("MD5") ) {
mysign = MD5.sign(prestr, AlipayConfig.key, AlipayConfig.input_charset);
}
return mysign;
}
/**
* 生成要請求給支付寶的參數數組
* @param sParaTemp 請求前的參數數組
* @return 要請求的參數數組
*/
private static Map<String, String> buildRequestPara(Map<String, String> sParaTemp) {
//除去數組中的空值和簽名參數
Map<String, String> sPara = AlipayCore.paraFilter(sParaTemp);
//生成簽名結果
String mysign = buildRequestMysign(sPara);
//簽名結果與簽名方式加入請求提交參數組中
sPara.put("sign", mysign);
sPara.put("sign_type", AlipayConfig.sign_type);
return sPara;
}
/**
* 建立請求,以表單HTML形式構造(默認)
* @param sParaTemp 請求參數數組
* @param strMethod 提交方式。兩個值可選:post、get
* @param strButtonName 確認按鈕顯示文字
* @return 提交表單HTML文本
*/
public static String buildRequest(Map<String, String> sParaTemp, String strMethod, String strButtonName) {
//待請求參數數組
Map<String, String> sPara = buildRequestPara(sParaTemp);
List<String> keys = new ArrayList<String>(sPara.keySet());
StringBuffer sbHtml = new StringBuffer();
sbHtml.append("<form id=\"alipaysubmit\" name=\"alipaysubmit\" action=\"" + ALIPAY_GATEWAY_NEW
+ "_input_charset=" + AlipayConfig.input_charset + "\" method=\"" + strMethod
+ "\">");
for (int i = 0; i < keys.size(); i++) {
String name = (String) keys.get(i);
String value = (String) sPara.get(name);
sbHtml.append("<input type=\"hidden\" name=\"" + name + "\" value=\"" + value + "\"/>");
}
//submit按鈕控件請不要含有name屬性
sbHtml.append("<input type=\"submit\" value=\"" + strButtonName + "\" style=\"display:none;\"></form>");
sbHtml.append("<script>document.forms['alipaysubmit'].submit();</script>");
return sbHtml.toString();
}
/**
* 用於防釣魚,調用接口query_timestamp來獲取時間戳的處理函數
* 注意:遠程解析XML出錯,與服務器是否支持SSL等配置有關
* @return 時間戳字符串
* @throws IOException
* @throws DocumentException
* @throws MalformedURLException
*/
public static String query_timestamp() throws MalformedURLException,
DocumentException, IOException {
//構造訪問query_timestamp接口的URL串
String strUrl = ALIPAY_GATEWAY_NEW + "service=query_timestamp&partner=" + AlipayConfig.partner + "&_input_charset" +AlipayConfig.input_charset;
StringBuffer result = new StringBuffer();
SAXReader reader = new SAXReader();
Document doc = reader.read(new URL(strUrl).openStream());
List<Node> nodeList = doc.selectNodes("//alipay/*");
for (Node node : nodeList) {
// 截取部分不需要解析的信息
if (node.getName().equals("is_success") && node.getText().equals("T")) {
// 判斷是否有成功標示
List<Node> nodeList1 = doc.selectNodes("//response/timestamp/*");
for (Node node1 : nodeList1) {
result.append(node1.getText());
}
}
}
return result.toString();
}
}
這幾個類調用支付寶接口的是AlipaySubmit,在網頁選好購買的商品時,在系統中生成訂單,然後進行支付,瀏覽器跳轉到支付寶支付網站,Controller中代碼爲:
/**
* 支付寶支付頁面
*
* @return
* @throws IOException
*/
@RequestMapping(value = "/aliPay")
public void aliPay(HttpServletRequest request, HttpServletResponse response) throws IOException {
LOGGER.info("支付寶支付頁面");
//商戶訂單號,商戶網站訂單系統中唯一訂單號,必填
String orderNo = request.getParameter("orderNo");
//訂單名稱,必填
String subjectName = request.getParameter("subjectName");
//付款金額,必填
String total_fee = request.getParameter("fee");
//商品描述,可空
String body = "法海風控 " + subjectName;
if ("money".equals(body)) {
body = "法海風控 餘額充值";
}
//把請求參數打包成map
Map<String, String> sParaTemp = new HashMap<String, String>();
sParaTemp.put("service", AlipayConfig.service);
sParaTemp.put("partner", AlipayConfig.partner);
sParaTemp.put("seller_id", AlipayConfig.seller_id);
sParaTemp.put("_input_charset", AlipayConfig.input_charset);
sParaTemp.put("payment_type", AlipayConfig.payment_type);
sParaTemp.put("notify_url", AlipayConfig.notify_url);
sParaTemp.put("return_url", AlipayConfig.return_url);
sParaTemp.put("anti_phishing_key", AlipayConfig.anti_phishing_key);
sParaTemp.put("exter_invoke_ip", AlipayConfig.exter_invoke_ip);
sParaTemp.put("out_trade_no", orderNo);
sParaTemp.put("subject", subjectName);
sParaTemp.put("total_fee", total_fee);
sParaTemp.put("body", body);
//其他業務參數根據在線開發文檔,添加參數.文檔地址:https://doc.open.alipay.com/doc2/detail.htm?spm=a219a.7629140.0.0.O9yorI&treeId=62&articleId=103740&docType=1
//如sParaTemp.put("參數名","參數值");
//建立請求
String sHtmlText = AlipaySubmit.buildRequest(sParaTemp,"get","確認");
response.setHeader("Content-Type", "text/html; charset=UTF-8");
response.setCharacterEncoding("UTF-8");
PrintWriter out = response.getWriter();
System.out.println(sHtmlText);
out.println(sHtmlText);
}
請求aliPay會跳轉到支付寶支付頁面:
3、接收支付結果通知
在配置好notify_url之後,支付結果會請求相應的接口,我的是order/pay/aliPayOrder,代碼如下
/**
* 支付寶支付訂單
* @return
* @throws IOException
*/
@ResponseBody
@RequestMapping(value = "pay/aliPayOrder", method = RequestMethod.POST)
public void aliPayOrder(HttpServletRequest request,HttpServletResponse response) throws IOException {
LOGGER.info("支付訂單");
//從request中獲得參數Map,並返回可讀的Map
Map<String, String> params = RequestUtil.getParameterMap(request);
LOGGER.info(params.toString());
//驗證支付寶簽名
boolean aliSign = AlipayNotify.verify(params);
if (aliSign) {//驗證成功
//交易狀態
String tradeStatus = params.get("trade_status");
//訂單編號
String orderNo = params.get("out_trade_no");
//支付單號
String payNo = params.get("trade_no");
//支付賬號
String payAccount = params.get("buyer_email");
//支付金額
String totalFee = params.get("total_fee");
//收款支付寶賬號
String sellerId = params.get("seller_id");
if (Constant.ALIPAY_TRADE_SUCCESS.equals(tradeStatus)) {//支付寶支付狀態爲成功
//驗證支付寶返回信息與請求信息一致
if (ProInfoUtil.getInstance().getProperty("alipay_partner").equals(sellerId)) {
//訂單處理狀態
String orderHandleStatus = "error";
//驗證訂單未做支付處理
Order order = orderService.queryOrderByOrderNo(orderNo);
//訂單已支付
if (Constant.DEALSTATUS_PAY.equals(order.getDealStatus())) {
response.getWriter().print("success");
return;
}
if (null != order && Double.parseDouble(totalFee) == order.getDealPrice() &&
Constant.DEALSTATUS_NOT_PAY.equals(order.getDealStatus())) {//驗證金額是否和訂單一致
//更新訂單爲已支付、更新用戶套餐餘額、添加用戶充值記錄、添加用戶餘額支出記錄
order.setDealStatus(Constant.DEALSTATUS_PAY);
order.setPayNo(payNo);
order.setPayType(Constant.ALIPAY);
order.setPayAccount(payAccount);
try {
//支付成功處理支付業務
boolean result = orderService.payOrder(order);
if (result) {
//成功後向支付寶返回成功標誌
LOGGER.info("支付寶支付成功");
orderHandleStatus = "success";
response.getWriter().print("success");
}
} catch (Exception e) {
e.printStackTrace();
LOGGER.info("支付寶支付失敗");
response.getWriter().print("fail");
}
}
//添加支付信息
Map<String, Object> map = new HashMap<String, Object>();
map.put("params", params.toString());
map.put("payType", Constant.ALIPAY);
map.put("orderNo", orderNo);
map.put("handleStatus", orderHandleStatus);
orderService.addPayInfo(map);
}
}
} else {//驗證失敗
LOGGER.info("支付寶返回驗證失敗");
response.getWriter().print("fail");
}
}
/**
* 從request中獲得參數Map,並返回可讀的Map
*
* @param request
* @return
*/
@SuppressWarnings("unchecked")
public static Map getParameterMap(HttpServletRequest request) {
// 參數Map
Map properties = request.getParameterMap();
// 返回值Map
Map<String, String> returnMap = new HashMap<String, String>();
Iterator entries = properties.entrySet().iterator();
Map.Entry entry;
String name = "";
String value = "";
while (entries.hasNext()) {
entry = (Map.Entry) entries.next();
name = (String) entry.getKey();
Object valueObj = entry.getValue();
if(null == valueObj){
value = "";
}else if(valueObj instanceof String[]){
String[] values = (String[])valueObj;
for(int i=0;i<values.length;i++){
value = values[i] + ",";
}
value = value.substring(0, value.length()-1);
}else{
value = valueObj.toString();
}
returnMap.put(name, value);
}
return returnMap;
}
至此,支付寶支付功能已經做完了,其中有幾個細節需要添加,比如支付時查詢訂單狀態是否已經支付,是否過期等等,可以根據自己的需求去完善。
支付寶的接入還是很順利的,如果熟練的話一兩天就可以完成了,剛開始寫博客,有錯誤或者不明白的地方歡迎大家指出一起交流學習,共同進步。
由於篇幅問題,我在下一章介紹微信支付的接入。
由於個人原因,寫完本文就很少上csdn,沒想到這麼多同學留言要源碼,十分惶恐,十分內疚,恐怕是誤導和耽誤了很多同學,本人亦是討厭留文不留碼的行爲,現在將源碼獻上,不過已經過了4年,支付功能已經改版多次,此源碼僅供參考,更多的還是要看官方demo。
下載地址:https://download.csdn.net/download/qukaiwei/12091787