業務流程圖解
- 用戶下單 → 後台生成訂單號、金額
- 調用支付寶
create_direct_pay_by_user 接口,構建表單/URL
- 用戶跳轉支付寶收銀台完成支付
- 支付寶同步跳轉
return_url(頁面提示)
- 支付寶異步通知
notify_url(核心業務:驗簽、更新訂單狀態、發貨)
核心依賴
<!-- 支付寶官方 SDK(含簽名、HTTP 工具) -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>20230615</version>
</dependency>
<!-- JSON 處理 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
1. 配置類 AlipayConfig
package com.hzdracom.config;
public class AlipayConfig {
// ===== 必須在商家後台獲取 =====
public static final String PARTNER = "2088001466494907"; // PID
public static final String SELLER_EMAIL = "jjj@hzdracom.com"; // 賣家賬號
public static final String PRIVATE_KEY = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQD..."; // RSA2 私鑰
public static final String ALIPAY_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..."; // 支付寶公鑰
// ===== 固定參數 =====
public static final String SIGN_TYPE = "RSA2";
public static final String CHARSET = "UTF-8";
public static final String GATEWAY_URL = "https://openapi.alipay.com/gateway.do";
public static final String FORMAT = "JSON";
// ===== 業務回調地址(需外網可訪問,建議配置域名)=====
public static final String NOTIFY_URL = "https://pay.example.com/alipay/notify";
public static final String RETURN_URL = "https://pay.example.com/alipay/return";
// ===== 可選防釣魚 =====
public static final String ANTI_PHISHING_KEY = "";
public static final String EXTER_INVOKE_IP = "";
}
2. 統一下單 Action AliPayAction
package com.hzdracom.alipay.action;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.hzdracom.config.AlipayConfig;
import com.opensymphony.xwork2.ActionSupport;
import org.apache.struts2.ServletActionContext;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class AliPayAction extends ActionSupport {
private static final Logger log = LogManager.getLogger(AliPayAction.class);
private String totalFee; // 前端傳入金額(元)
private String subject; // 訂單標題
private String body; // 訂單描述
public String pay() {
try {
// 1. 生成唯一訂單號(時間戳+隨機數)
String outTradeNo = System.currentTimeMillis() + "" + (int)(Math.random()*10000);
// 2. 初始化客戶端
AlipayClient client = new DefaultAlipayClient(
AlipayConfig.GATEWAY_URL,
AlipayConfig.PARTNER,
AlipayConfig.PRIVATE_KEY,
AlipayConfig.FORMAT,
AlipayConfig.CHARSET,
AlipayConfig.ALIPAY_PUBLIC_KEY,
AlipayConfig.SIGN_TYPE
);
// 3. 構建請求參數(JSON 格式)
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setReturnUrl(AlipayConfig.RETURN_URL);
request.setNotifyUrl(AlipayConfig.NOTIFY_URL);
request.setBizContent(String.format(
"{\"out_trade_no\":\"%s\",\"product_code\":\"FAST_INSTANT_TRADE_PAY\",\"total_amount\":\"%s\",\"subject\":\"%s\",\"body\":\"%s\"}",
outTradeNo, totalFee, subject, body
));
// 4. 調用 SDK 獲取表單 HTML(自動簽名)
String formHtml = client.pageExecute(request).getBody();
// 5. 直接輸出到瀏覽器,用戶自動跳轉支付寶
ServletActionContext.getResponse().setContentType("text/html;charset=UTF-8");
ServletActionContext.getResponse().getWriter().write(formHtml);
return NONE; // 已手動輸出,不需 Struts 結果視圖
} catch (AlipayApiException | IOException e) {
log.error("支付寶下單失敗", e);
addActionError("下單失敗:" + e.getMessage());
return ERROR;
}
}
// Getter/Setter 省略
}
3. 異步通知處理 NotifyAction(核心業務邏輯)
package com.hzdracom.alipay.action;
import com.alipay.api.AlipayApiException;
import com.alipay.api.internal.util.AlipaySignature;
import com.hzdracom.config.AlipayConfig;
import com.opensymphony.xwork2.ActionSupport;
import org.apache.struts2.ServletActionContext;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
public class NotifyAction extends ActionSupport {
private static final Logger log = LogManager.getLogger(NotifyAction.class);
public String notify() {
HttpServletRequest request = ServletActionContext.getRequest();
try {
// 1. 獲取所有參數
Map params = new HashMap<>();
request.getParameterMap().forEach((k, v) -> params.put(k, v[0]));
// 2. 驗證簽名(防偽造通知)
boolean verified = AlipaySignature.rsaCheckV1(
params, AlipayConfig.ALIPAY_PUBLIC_KEY, AlipayConfig.CHARSET, AlipayConfig.SIGN_TYPE
);
if (!verified) {
log.warn("簽名驗證失敗,疑似偽造通知:{}", params);
return "fail"; // 支付寶收到非 success 會重試
}
// 3. 核心業務字段
String outTradeNo = params.get("out_trade_no"); // 商戶訂單號
String tradeNo = params.get("trade_no"); // 支付寶交易號
String tradeStatus = params.get("trade_status"); // TRADE_SUCCESS / TRADE_FINISHED
String buyerId = params.get("buyer_id"); // 買家用戶號
log.info("收到支付寶異步通知:outTradeNo={}, tradeNo={}, status={}", outTradeNo, tradeNo, tradeStatus);
// 4. 幂等性處理:防止重複通知重複發貨
if ("TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus)) {
boolean processed = OrderService.markPaid(outTradeNo, tradeNo, buyerId);
if (!processed) {
log.warn("訂單已處理,忽略重複通知:{}", outTradeNo);
}
}
// 5. 必須返回純文本 "success",否則支付寶會按失敗重試(最長 24h)
ServletActionContext.getResponse().getWriter().write("success");
return NONE;
} catch (AlipayApiException | IOException e) {
log.error("異步通知處理異常", e);
return "fail";
}
}
}
4. 同步跳轉頁面 ReturnAction
package com.hzdracom.alipay.action;
import com.opensymphony.xwork2.ActionSupport;
import org.apache.struts2.ServletActionContext;
public class ReturnAction extends ActionSupport {
public String returnView() {
// 僅做頁面提示,不可作為訂單狀態最終依據(用戶可能關閉頁面)
ServletActionContext.getRequest().setAttribute("msg", "支付完成,正在為您跳轉...");
return SUCCESS; // 對應 result 頁面 return.jsp
}
}
5. Struts 配置 struts.xml
<package name="alipay" namespace="/alipay" extends="struts-default">
<action name="pay" class="com.hzdracom.alipay.action.AliPayAction" method="pay">
<result name="error">/error.jsp</result>
</action>
<action name="notify" class="com.hzdracom.alipay.action.NotifyAction" method="notify">
<result name="success" type="stream">
<param name="contentType">text/plain</param>
<param name="inputName">inputStream</param>
</result>
<result name="fail" type="stream">
<param name="contentType">text/plain</param>
<param name="inputName">inputStream</param>
</result>
</action>
<action name="return" class="com.hzdracom.alipay.action.ReturnAction" method="returnView">
<result name="success">/alipay/return.jsp</result>
</action>
</package>
6. 關鍵注意事項(避坑指南)
| 項目 | 說明 |
| 回調地址 | notify_url/return_url 必須是 外網可訪問的完整 HTTPS URL,不能用 localhost、內網 IP、帶參數 URL |
| 金額格式 | total_amount 單位為元,保留兩位小數,如 "0.01" |
| 字符集 | 全鏈路統一 UTF-8,JSP 頁面、Tomcat URIEncoding、MySQL 連接串均需一致 |
| 幂等性 | 異步通知可能重複,必須用 out_trade_no + 狀態機判斷是否已處理 |
| 簽名算法 | 強制使用 RSA2(SHA256withRSA),RSA(SHA1withRSA)已停止支持 |
| SDK 版本 | 定期更新 alipay-sdk-java,支付寶會下線舊版接口 |
| 沙箱測試 | 開發階段請使用 支付寶開放平台沙箱,配置沙箱 PID/密鑰/網關 |
7. 測試檢查清單
- 配置文件中 PID、私鑰、公鑰、賣家賬號是否正確?
- notify_url/return_url 是否已在支付寶後台「應用配置」綁定?
- 服務器防火牆/安全組是否放行 443 端口入站?
- 日誌中是否出現「簽名驗證失敗」或「參數解析異常」?
- 沙箱環境完整跑通:下單 → 支付 → 異步通知入庫 → 同步頁面提示