前言
這篇文章主要講訴系統調用支付寶手機網頁即時交易接口後支付寶傳回的異步通知。
支付寶對商戶的請求資料處理完成後,會将處理的結果資料通過伺服器主動通知的方式通知給商戶網站。這些處理結果資料就是伺服器異步通知參數。
特性
- 必須保證伺服器異步通知頁面(notify_url)上無任何字元,如空格、HTML标簽、開發系統自帶抛出的異常提示資訊或錯誤頁面等。
- 支付寶是用POST方式發送通知資訊,是以該頁面中擷取參數的方式,如: request.Form("out_trade_no")、$_POST['out_trade_no']。
- 支付寶主動發起通知,該方式才會被啟用。
- 隻有在支付寶的交易管理中存在該筆交易,且發生了交易狀态的改變,支付寶才會通過該方式發起伺服器通知(即時到賬中交易狀态為“等待買家付款”的狀态預設是不會發送通知的)。
- 伺服器間的互動,不像頁面跳轉同步通知可以在頁面上顯示出來,這種互動方式是不可見的。
- 第一次交易狀态改變(即時到賬中此時交易狀态是交易完成)時,不僅頁面跳轉同步通知頁面會啟用,而且伺服器異步通知頁面也會收到支付寶發來的處理結果通知。
- 程式執行完後必須列印輸出“success”(不包含引号、前後無空格和其他多餘字元)。如果商戶回報給支付寶的字元不是success這7個字元,支付寶伺服器會不斷重發通知,直到超過24小時22分鐘。一般情況下,25小時以内完成8次通知(通知的間隔頻率一般是:2m,10m,10m,1h,2h,6h,15h)。
- 程式執行完成後,該頁面不能執行頁面跳轉。如果執行頁面跳轉,支付寶會收不到success字元,會被支付寶伺服器判定為該頁面程式運作出現異常,而重發處理結果通知。
- 程式處理過程中出現異常時傳回“fail”,這時支付寶伺服器會選擇重發通知。
- cookies、session等在此頁面會失效,即無法擷取這些資料。
- 該方式的調試與運作必須在伺服器上,即網際網路上能通路。
- 該方式的作用主要防止訂單丢失,即頁面跳轉同步通知沒有處理訂單更新,它則去處理。
- 當商戶收到伺服器異步通知并列印出success時,伺服器異步通知參數notify_id才會失效。也就是說在支付寶發送同一條異步通知時(包含商戶并未成功列印出success導緻支付寶重發數次通知),伺服器異步通知參數notify_id是不變的。
- 異步通知在“交易成功”和“支付成功”狀态都會進行通知發送,需正确處理通知時的交易狀态,處理完成之後都需傳回“success”,避免出現重複通知可能導緻的業務重複處理錯誤。
伺服器異步通知參數說明
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIiclRnblN0LclHdpZXYyd2LcBzNvwVZ2x2bzNXak9CX90TQNNkRrFlQKBTSvwFbslmZvwFMwQzLcVmepNHdu9mZvwFVywUNMZTY18CX052bm9CX90TUNhXRU9keJpXT4FEVkZXUYpVd1kmYr50MZV3YyI2cKJDT29GRjBjUIF2LcRHelR3LcJzLctmch1mclRXY39TNwYzN1czMxEzNxUDM1EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
notify_data通知業務參數清單
樣例
- 商戶使用RSA簽名時收到支付寶的通知請求樣例如下:
-
http://商戶自定義位址/alipay/notify_url.php?service=alipay.wap.trade.create.direct&sign=Rw/y4ROnNicXhaj287Fiw5pvP6viSyg53H3iNiJ61D3YVi7zGniG2680pZv6rakMCeXX++q9XRLw8Rj6I1//qHrwMAHS1hViNW6hQYsh2TqemuL/xjXRCY3vjm1HCoZOUa5zF2jU09yG23MsMIUx2FAWCL/rgbcQcOjLe5FugTc=&sec_id=0001&v=2.0¬ify_data=g3ivqicRwI9rI5jgmSHSU2osBXV1jcxohapSAPjx4f6qiqsoAzstaRWuPuutE0gxQwzMOtwL3npZqWO3Z89J4w4dXIY/fvOLoTNn8FjExAf7OozoptUS6suBhdMyo/YJyS3lVALfCeT3s27pYWihHgQgna6cTfgi67H2MbX40xtexIpUnjgxBkmOLai8DPOUI58y4UrVwoXQgdcwnXsfn2OthhUFiFPfpINgEphUAq1nC/EPymP6ciHdTCWRI6l1BgWuCzdFy0MxJLliPSnuLyZTou7f+Z5Mw24FgOacaISB+1/G+c4XIJVKJwshCDw9Emz+NAWsPvq34FEEQXVAeQRDOphJx8bDqLK75CGZX+6fx88m5ztq4ykuRUcrmoxZLJ+PiABvYFzi5Yx2uBMP/PmknRmj1HUKEhuVWsXR0t6EWpJFXlyQA4uxbShzncWDigndD7wbfNtkNLg5xMSFFIKay+4YzJK68H9deW4xqk4JYTKsv8eom9Eg9MrJZiIrFkFpVYPuaw0y/n61UEFYdzEQZz+garCmMYehEAQCGibYUQXBlf1iwTOZdqJIxdgCpSX21MIa9N9jicmFu8OXWZJkdN+UrSyvIcpzRori+U6522ovMz5Z8EzVTfcUENu+d
- 以上示例中的notify_data參數值為加密内容,商戶需用自己的RSA私鑰先進行解密後再驗簽。
-
- 商戶使用MD5簽名時收到支付寶的通知樣例如下:
-
http://商戶自定義位址/alipay/notify_url.php?service=alipay.wap.trade.create.direct%20&sign=Rw/y4ROnNicXhaj287Fiw5pvP6viSyg53H3iNiJ61D3YVi7zGniG2680pZv6rakMCeXX++q9XRLw8Rj6I1//qHrwMAHS1hViNW6hQYsh2TqemuL/xjXRCY3vjm1HCoZOUa5zF2jU09yG23MsMIUx2FAWCL/rgbcQcOjLe5FugTc=&v=2.0&sec_id=MD5¬ify_data=%3Cnotify%3E%3Cpayment_type%3E1%3C/payment_type%3E%3Csubject%3E%E6%94%B6%E9%93%B6%E5%8F%B0{1283134629741}%3C/subject%3E%3Ctrade_no%3E2014040311001004370000361525%3C/trade_no%3E%3Cbuyer_email%[email protected]%3C/buyer_email%3E%3Cgmt_create%3E2010-08-3010:17:24%3C/gmt_create%3E%3Cnotify_type%3Etrade_status_sync%3C/notify_type%3E%3Cquantity%3E1%3C/quantity%3E%3Cout_trade_no%3E1283134629741%3C/out_trade_no%3E%3Cnotify_time%3E2010-08-3010:18:15%3C/notify_time%3E%3Cseller_id%3E2088101000137799%3C/seller_id%3E%3Ctrade_status%3ETRADE_FINISHED%3C/trade_status%3E%3Cis_total_fee_adjust%3EN%3C/is_total_fee_adjust%3E%3Ctotal_fee%3E1.00%3C/total_fee%3E%3Cgmt_payment%3E2010-08-3010:18:26%3C/gmt_payment%3E%3Cseller_email%[email protected]%3C/seller_email%3E%3Cgmt_close%3E2010-08-3010:18:26%3C/gmt_close%3E%3Cprice%3E1.00%3C/price%3E%3Cbuyer_id%3E2088102001172352%3C/buyer_id%3E%3Cnotify_id%3E509ad84678759176212c247c46bec05303%3C/notify_id%3E%3Cuse_coupon%3EN%3C/use_coupon%3E%3C/notify%3E
- 以上示例中的notify_data參數值為明文内容,無需解密。
-
- 支付寶系統通知待簽名資料構造規則比較特殊,為固定順序。
- 例如商戶收到如下通知資料:
-
http://商戶自定義位址/alipay/notify_url.php?service=alipay.wap.trade.create.direct&sign=Rw/y4ROnNicXhaj287Fiw5pvP6viSyg53H3iNiJ61D3YVi7zGniG2680pZv6rakMCeXX++q9XRLw8Rj6I1//qHrwMAHS1hViNW6hQYsh2TqemuL/xjXRCY3vjm1HCoZOUa5zF2jU09yG23MsMIUx2FAWCL/rgbcQcOjLe5FugTc=&v=1.0&sec_id=0001¬ify_data=<notify><payment_type>1</payment_type></notify>
- 則隻需對以下資料進行驗簽:
-
service=alipay.wap.trade.create.direct&v=1.0&sec_id=0001¬ify_data=<notify>…</notify>
代碼示例
@RequestMapping(value="/notify",method=RequestMethod.POST)
@ResponseBody
public Object notifyUrl(HttpServletRequest request, HttpServletResponse response){
System.out.println("支付提示");
//擷取支付寶POST過來回報資訊
Map<String,String> params = new HashMap<String,String>();
Map requestParams = request.getParameterMap();
for (Iterator iter = requestParams.keySet().iterator(); iter.hasNext();) {
String name = (String) iter.next();
String[] values = (String[]) requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i]
: valueStr + values[i] + ",";
}
//亂碼解決,這段代碼在出現亂碼時使用。如果mysign和sign不相等也可以使用這段代碼轉化
//valueStr = new String(valueStr.getBytes("ISO-8859-1"), "gbk");
params.put(name, valueStr);
}
//驗證
AlipayAuth alipayAuth = new AlipayAuth();
alipayAuth.setKey(orderBusiness.getAliAttributes("key"));
alipayAuth.setPartner(orderBusiness.getAliAttributes("partner"));
boolean flag = false;
try {
flag = AliPayUtils.verifyNotify(params, alipayAuth);
//擷取支付寶的通知傳回參數
//XML解析notify_data資料
Document doc_notify_data = DocumentHelper.parseText(params.get("notify_data"));
//商戶訂單号
String out_trade_no = doc_notify_data.selectSingleNode( "//notify/out_trade_no" ).getText();
//支付寶交易号
String trade_no = doc_notify_data.selectSingleNode( "//notify/trade_no" ).getText();
//交易狀态
String trade_status = doc_notify_data.selectSingleNode( "//notify/trade_status" ).getText();
//交易總金額
String total_fee = doc_notify_data.selectSingleNode( "//notify/total_fee" ).getText();
if(flag){
//付款成功後
//——請根據您的業務邏輯來編寫程式(以下代碼僅作參考)——
if(trade_status.equals("TRADE_FINISHED")){
//判斷該筆訂單是否在商戶網站中已經做過處理
//如果沒有做過處理,根據訂單号(out_trade_no)在商戶網站的訂單系統中查到該筆訂單的詳細,并執行商戶的業務程式
//如果有做過處理,不執行商戶的業務程式
//注意:
//該種交易狀态隻在兩種情況下出現
//1、開通了普通即時到賬,買家付款成功後。
//2、開通了進階即時到賬,從該筆交易成功時間算起,過了簽約時的可退款時限(如:三個月以内可退款、一年以内可退款等)後。
out.println("success"); //請不要修改或删除
} else if (trade_status.equals("TRADE_SUCCESS")){
//判斷該筆訂單是否在商戶網站中已經做過處理
//如果沒有做過處理,根據訂單号(out_trade_no)在商戶網站的訂單系統中查到該筆訂單的詳細,并執行商戶的業務程式
//如果有做過處理,不執行商戶的業務程式
//注意:
//該種交易狀态隻在一種情況下出現——開通了進階即時到賬,買家付款成功後。
out.println("success"); //請不要修改或删除
}
//——請根據您的業務邏輯來編寫程式(以上代碼僅作參考)——
} else {//
return "fail";
}
} catch (Exception e) {
// TODO Auto-generated catch block
log.error(e.getMessage());
e.printStackTrace();
return "fail";
}
}
AlipayAuth
public class AlipayAuth {
// 支付寶網關
private final static String alipayGatewayNew = "http://wappaygw.alipay.com/service/rest.htm?";
private final static String inputCharset = "utf-8";
private String key = "";//從支付寶擷取的密鑰
//接口名稱
private final static String service = "alipay.wap.trade.create.direct";
//請求參數格式
private final static String format = "xml";
//接口版本号
private final static String v = "2.0";
//合作者身份id
public String partner = "2088000000000000";//填寫從支付寶得到的id
//請求号
private String reqId;
//簽名方式
private final static String secId = "MD5";
//簽名
private String sign;
//請求業務參數
private String reqData;
//商品名稱
private String subject;
//商務網站唯一訂單号
private String outTradeNo;
//交易金額
private String totalFee;
//賣家支付寶賬号
private String sellerAccountName;
//支付成功跳轉路徑
private String callBackUrl = "callBack";
//伺服器異步通知頁面路徑 (可空)
private String notifyUrl = "notify";
//商戶系統唯一标示(可空)
private String outUser;
//操作中斷傳回位址(可空)
private String merchantUrl;
//交易自動關閉時間(可空)
private String payExpire;
//代理人id(可空)
private String agentId;
public String getBasePath() {
return basePath;
}
public void setBasePath(String basePath) {
this.basePath = basePath;
}
public String getAlipayGatewayNew() {
return alipayGatewayNew;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getInputCharset() {
return inputCharset;
}
public String getService() {
return service;
}
public String getFormat() {
return format;
}
public String getV() {
return v;
}
public String getPartner() {
return partner;
}
public void setPartner(String partner) {
this.partner = partner;
}
public String getReqId() {
return reqId;
}
public void setReqId(String reqId) {
this.reqId = reqId;
}
public String getSecId() {
return secId;
}
public String getSign() {
return sign;
}
public void setSign(String sign) {
this.sign = sign;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getOutTradeNo() {
return outTradeNo;
}
public void setOutTradeNo(String outTradeNo) {
this.outTradeNo = outTradeNo;
}
public String getTotalFee() {
return totalFee;
}
public void setTotalFee(String totalFee) {
this.totalFee = totalFee;
}
public String getSellerAccountName() {
return sellerAccountName;
}
public void setSellerAccountName(String sellerAccountName) {
this.sellerAccountName = sellerAccountName;
}
public String getCallBackUrl() {
return callBackUrl;
}
public void setCallBackUrl(String callBackUrl) {
this.callBackUrl = callBackUrl;
}
public String getNotifyUrl() {
return notifyUrl;
}
public void setNotifyUrl(String notifyUrl) {
this.notifyUrl = notifyUrl;
}
public String getOutUser() {
return outUser;
}
public void setOutUser(String outUser) {
this.outUser = outUser;
}
public String getMerchantUrl() {
return merchantUrl;
}
public void setMerchantUrl(String merchantUrl) {
this.merchantUrl = merchantUrl;
}
public String getPayExpire() {
return payExpire;
}
public void setPayExpire(String payExpire) {
this.payExpire = payExpire;
}
public String getAgentId() {
return agentId;
}
public void setAgentId(String agentId) {
this.agentId = agentId;
}
public void setReqData(String reqData) {
this.reqData = reqData;
}
public String getReqData() {
reqData = "<direct_trade_create_req>"
+ "<subject>" + subject + "</subject>"//商品名稱
+ "<out_trade_no>" + outTradeNo + "</out_trade_no>"//商戶網站唯一訂單号
+ "<total_fee>" + totalFee + "</total_fee>"//交易金額
+ "<seller_account_name>" + sellerAccountName + "</seller_account_name>"//賣家支付寶賬号
+ "<call_back_url>" + callBackUrl + "</call_back_url>"//支付成功跳轉頁面
+ "<notify_url>" + notifyUrl + "</notify_url>"//異步通知頁面
//+ "<merchantUrl>" + merchantUrl + "</merchantUrl>"//操作終端傳回位址(可空)
+ "</direct_trade_create_req>";
return reqData;
}
}
驗證從支付寶傳遞過來的參數
AliPayUtils.verifyNotify(params, alipayAuth)方法
/**
* 驗證消息是否是支付寶發出的合法消息,驗證伺服器異步通知
* @param params 通知傳回來的參數數組
* @return 驗證結果
*/
public static boolean verifyNotify(Map<String, String> params,AlipayAuth alipayAuth) throws Exception {
//擷取是否是支付寶伺服器發來的請求的驗證結果
String responseTxt = "true";
try {
//XML解析notify_data資料,擷取notify_id
Document document = DocumentHelper.parseText(params.get("notify_data"));
String notify_id = document.selectSingleNode( "//notify/notify_id" ).getText();
responseTxt = verifyResponse(notify_id,alipayAuth);
} catch(Exception e) {
responseTxt = e.toString();
}
//擷取傳回時的簽名驗證結果
String sign = "";
if(params.get("sign") != null) {sign = params.get("sign");}
boolean isSign = getSignVeryfy(params, sign,false,alipayAuth);
//寫日志記錄(若要調試,請取消下面兩行注釋)
//String sWord = "responseTxt=" + responseTxt + "\n isSign=" + isSign + "\n 傳回回來的參數:" + AlipayCore.createLinkString(params);
//AlipayCore.logResult(sWord);
//判斷responsetTxt是否為true,isSign是否為true
//responsetTxt的結果不是true,與伺服器設定問題、合作身份者ID、notify_id一分鐘失效有關
//isSign不是true,與安全校驗碼、請求時的參數格式(如:帶自定義參數等)、編碼格式有關
if (isSign && responseTxt.equals("true")) {
return true;
} else {
return false;
}
}
verifyResponse(notify_id,alipayAuth)代碼塊
/**
* 擷取遠端伺服器ATN結果,驗證傳回URL
* @param notify_id 通知校驗ID
* @return 伺服器ATN結果
* 驗證結果集:
* invalid指令參數不對 出現這個錯誤,請檢測傳回進行中partner和key是否為空
* true 傳回正确資訊
* false 請檢查防火牆或者是伺服器阻止端口問題以及驗證時間是否超過一分鐘
*/
private static String verifyResponse(String notify_id,AlipayAuth alipayAuth) {
//擷取遠端伺服器ATN結果,驗證是否是支付寶伺服器發來的請求
String partner = alipayAuth.getPartner();
String veryfy_url = HTTPS_VERIFY_URL + "partner=" + partner + "¬ify_id=" + notify_id;
return checkUrl(veryfy_url);
}
/**
* 支付寶消息驗證位址
*/
private static final String HTTPS_VERIFY_URL = "https://mapi.alipay.com/gateway.do?service=notify_verify&";
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;
}
getSignVeryfy(params, sign,false,alipayAuth)
/**
* 根據回報回來的資訊,生成簽名結果
* @param Params 通知傳回來的參數數組
* @param sign 比對的簽名結果
* @param isSort 是否排序
* @return 生成的簽名結果
*/
private static boolean getSignVeryfy(Map<String, String> Params, String sign, boolean isSort,AlipayAuth alipayAuth) {
//過濾空值、sign與sign_type參數
Map<String, String> sParaNew = paraFilter(Params);
//擷取待簽名字元串
String preSignStr = "";
if(isSort) {
preSignStr = createLinkString(sParaNew);
} else {
preSignStr = createLinkStringNoSort(sParaNew);
}
//獲得簽名驗證結果
boolean isSign = false;
isSign = verify(preSignStr, sign, alipayAuth.getKey(), alipayAuth.getInputCharset());
return isSign;
}