支付業務常面臨微信、支付寶、銀聯、數字人民幣等多渠道接入,各渠道流程相似但配置參數、簽名算法、報文格式差異巨大。本文以「工廠模式 + 策略模式」結合 Spring 依賴注入,構建可擴展、配置驅動的支付統一網關,附帶完整代碼結構與關鍵實現細節。
CommonPay 定義預下單標準契約,屏蔽渠道差異。CommonPayFactory 持有各渠道實例,Spring 初始化時注入。CommonPayFactory,獲取渠道實例調用統一接口。public interface CommonPay {
/**
* 支付預處理:生成支付 URL 或提供給支付平台的數據
* @param param 預支付參數 (訂單號、金額、卡號等)
* @return 預支付結果 DTO (含表單 HTML、二維碼鏈接等)
*/
<T> PrepayDto<T> prePay(PrepayParam param) throws Exception;
}
public class CommonPayFactory implements InitializingBean {
private CommonPay unionPay; // 銀聯
// private CommonPay wechatPay; // 微信
// private CommonPay alipay; // 支付寶
@Override
public void afterPropertiesSet() {
if (unionPay == null) {
throw new IllegalStateException("未配置任何支付實例");
}
}
public CommonPay getUnionPay() { return unionPay; }
public void setUnionPay(CommonPay unionPay) { this.unionPay = unionPay; }
// 其他渠道 getter/setter...
}
<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 1. 銀聯配置 Bean -->
<bean id="unionPayConfig" class="com.xxx.union.UnionPayConfig">
<property name="payUrl" value="${pay.unionpay.payUrl}"/>
<property name="queryUrl" value="${pay.unionpay.queryUrl}"/>
<property name="merId" value="${pay.unionpay.merId}"/>
<property name="signPfxFile" value="${pay.unionpay.signPfxFile}"/>
<property name="signPfxPwd" value="${pay.unionpay.signPfxPwd}"/>
<property name="verifySignCertFile" value="${pay.unionpay.verifySignCertFile}"/>
<property name="payNotifyFrontUrl" value="${pay.unionpay.payNotifyFrontUrl}"/>
<property name="payNotifyUrl" value="${pay.unionpay.payNotifyUrl}"/>
</bean>
<!-- 2. 工廠 Bean,注入渠道實例 -->
<bean id="commonPayFactory" class="com.xxx.service.pay.CommonPayFactory">
<property name="unionPay">
<bean class="com.xxx.service.pay.UnionPay">
<property name="unionPayConfig" ref="unionPayConfig"/>
</bean>
</property>
<!-- 新增渠道只需在此添加 property -->
</bean>
</beans>
public class UnionPay implements CommonPay {
private UnionPayConfig unionPayConfig;
@Override
public <T> PrepayDto<T> prePay(PrepayParam param) throws Exception {
PrepayDto<String> dto = new PrepayDto<>();
Map<String, String> params = new TreeMap<>(); // TreeMap 保證鍵有序,利於簽名
// 1. 填充銀聯固定業務參數 (版本、編碼、簽名方法、交易類型、渠道類型...)
params.put("version", UnionPayConfig.VERSION);
params.put("encoding", UnionPayConfig.CHARSET);
params.put("signMethod", "01"); // RSA
params.put("txnType", "01"); // 消費
params.put("txnSubType", "01"); // 自助消費
params.put("bizType", "000201"); // B2C 網關支付
params.put("channelType", "07"); // PC/平板
// 2. 填充商戶動態參數
params.put("merId", unionPayConfig.getMerId());
params.put("accessType", "0");
params.put("orderId", param.getOutTradeNo()); // 商戶訂單號
params.put("txnTime", new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()));
params.put("accNo", param.getCardNumber()); // 卡號 (可選)
params.put("currencyCode", "156"); // 人民幣
params.put("txnAmt", param.getTotalFee()); // 金額 (分)
params.put("reserved", "{cardNumberLock=1}"); // 透傳字段
// 3. 通知地址
params.put("frontUrl", unionPayConfig.getPayNotifyFrontUrl()); // 前台通知
params.put("backUrl", unionPayConfig.getPayNotifyUrl()); // 後台通知
// 4. 簽名 (SHA-1 摘要 + RSA 私鑰簽名 + Base64)
sign(params);
// 5. 生成自動提交表單 HTML
String html = createAutoFormHtml(unionPayConfig.getPayUrl(), params, UnionPayConfig.CHARSET);
dto.setResp(html);
dto.setOutTradeNo(param.getOutTradeNo());
return (PrepayDto<T>) dto;
}
private void sign(Map<String, String> params) {
params.put("certId", unionPayConfig.getCertId());
String reqStr = PayUtil.mapToQueryStr(params, UnionPayConfig.CHARSET, false, true);
byte[] signDigest = SHA1Util.sha1X16(reqStr, UnionPayConfig.CHARSET);
byte[] byteSign = RSA.signBySoft(unionPayConfig.getPrivateKey(), signDigest);
params.put("signature", Base64.encode(byteSign));
}
private String createAutoFormHtml(String url, Map<String, String> hiddens, String charset) {
StringBuilder sb = new StringBuilder();
sb.append("<html><head><meta charset=\"").append(charset).append("\"/></head><body>");
sb.append("<form id=\"pay_form\" action=\"").append(url).append("\" method=\"post\">");
hiddens.forEach((k, v) -> sb.append("<input type=\"hidden\" name=\"").append(k)
.append("\" value=\"").append(v).append("\"/>"));
sb.append("</form><script>document.getElementById('pay_form').submit();</script></body></html>");
return sb.toString();
}
}
@Service
public class OrderService {
@Autowired CommonPayFactory payFactory;
public String createPayForm(Long orderId, String tradeNo, String cardNo, BigDecimal amount) {
CommonPay unionPay = payFactory.getUnionPay(); // 獲取渠道實例
PrepayParam param = new PrepayParam();
param.setOrderId(orderId);
param.setOutTradeNo(tradeNo);
param.setCardNumber(cardNo);
param.setTotalFee(amount.multiply(new BigDecimal(100)).longValue() + ""); // 元轉分
try {
PrepayDto dto = unionPay.prePay(param);
return dto.getResp(); // 直接返回 HTML,前端渲染跳轉
} catch (Exception e) {
log.error("銀聯預下單失敗", e);
throw new BizException("支付通道暫時不可用");
}
}
}
CommonPay 接口:WechatPay。WechatPayConfig 並加入 Spring 配置。CommonPayFactory 添加 wechatPay 屬性及 getter/setter,XML 中注入新 Bean。調用方代碼零修改,僅需 payFactory.getWechatPay() 即可切換渠道。
「工廠模式管理實例 + 策略模式封裝算法 + Spring IOC 裝配配置」構建了清晰、可擴展、可測試的支付網關架構。核心優勢: