這裡記錄一個例子,工廠模式的理論就不扯淡了。
遇到的問題:支付方式有很多種,比如微信支付 支付寶支付 銀聯支付 等等。我們在在實現的時候發現他麼的流程上是相似的,以及每個方式都有大量的個性配置,在實例化時需要加載他們,以及為了清晰的講調用方和實現方進行分離,就有來下面的小設計。
以銀聯為例,(通過測試)。
共用接口:
public interface CommonPay {
/**
* <pre>
* 功能描述:
* 支付預處理:生成支付URL或提供給支付平台的數據
*
* @param param 預支付參數
* @return 支付URL或提供給支付平台的數據
* @throws Exception
*/
<T> PrepayDto<T> prePay(PrepayParam param) throws Exception;
}
工廠類:
public class CommonPayFactory implements InitializingBean {
/** 銀聯支付 */
private CommonPay unionPay;
@Override public void afterPropertiesSet() throws Exception {
if (unionPay == null) {
throw new RuntimeException("未配置任何支付實例");
}
}
public CommonPay getUnionPay() {
return unionPay;
}
public void setUnionPay(CommonPay unionPay) {
this.unionPay = unionPay;
}
}
我們利用spring,將bean初始化信息配置好:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="unionPayConfig" class="com.xiaoka.cashier.web.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>
<!-- 通用支付工廠實例 -->
<bean id="commonPayFactory" class="com.xiaoka.cashier.service.pay.CommonPayFactory">
<property name="unionPay">
<bean class="com.xiaoka.cashier.service.pay.UnionPay">
<property name="UnionPayConfig" ref="unionPayConfig" />
</bean>
</property>
</bean>
</beans>
如此在項目啟動時,銀聯實現會根據配置信息實例化,工廠類會自動加載入銀聯實例以備獲取。
以下是銀聯實現:
public class UnionPay implements CommonPay {
/** Logger */
private static final Logger LOGGER = LoggerFactory.getLogger(UnionPay.class);
/** 日期時間格式 */
private static final String DATE_FORMAT_YYYYMMDDHHMMSS = "yyyyMMddHHmmss";
/** 銀聯支付配置 */
private UnionPayConfig unionPayConfig;
@Override public <T> PrepayDto<T> prePay(PrepayParam param) throws Exception {
PrepayDto<String> prepayDto = new PrepayDto<>();
// 構造預下單簽名參數
Map<String, String> params = new TreeMap<>();
/** 銀聯全渠道系統,產品參數,除了encoding自行選擇外其他不需修改 */
// 版本號,全渠道默認值
params.put("version", UnionPayConfig.VERSION);
// 字符集編碼,可以使用UTF-8,GBK兩種方式
params.put("encoding", UnionPayConfig.CHARSET);
// 簽名方法,只支持 01:RSA方式證書加密
params.put("signMethod", "01");
// 交易類型 ,01:消費
params.put("txnType", "01");
// 交易子類型, 01:自助消費
params.put("txnSubType", "01");
// 業務類型,B2C網關支付,手機wap支付
params.put("bizType", "000201");
// 渠道類型,這個字段區分B2C網關支付和手機wap支付;07:PC,平板 08:手機
params.put("channelType", "07");
/** 商戶接入參數 */
// 商戶號碼,請改成自己申請的正式商戶號或者open上注冊得來的777測試商戶號
params.put("merId", unionPayConfig.getMerId());
// 接入類型,0:直連商戶
params.put("accessType", "0");
// 商戶訂單號,8-40位數字字母,不能含“-”或“_”,可以自行定制規則
params.put("orderId", param.getOutTradeNo());
// 訂單發送時間,取系統時間,格式為YYYYMMDDhhmmss,必須取當前時間,否則會報txnTime無效
SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT_YYYYMMDDHHMMSS);
Date orderAddTime = new Date();
String txtTime = sdf.format(orderAddTime);
params.put("txnTime", txtTime);
// 賬號
// 1、 後台類消費交易時上送全卡號或卡號後4位
// 2、 跨行收單且收單機構收集銀行卡信息時上送、
// 3、前台類交易可通過配置後返回,卡號可選上送
params.put("accNo", param.getCardNumber());
// 交易幣種(境內商戶一般是156 人民幣)
params.put("currencyCode", "156");
// 交易金額,單位分,不要帶小數點
params.put("txnAmt", param.getTotalFee());
// 請求方保留域,如需使用請啟用即可;透傳字段(可以實現商戶自定義參數的追蹤)本交易的後台通知,
// 對本交易的交易狀態查詢交易、對賬文件中均會原樣返回,商戶可以按需上傳,長度為1-1024個字節
params.put("reserved", "{cardNumberLock=1}"); // TODO
// 前台通知地址 (需設置為外網能訪問 http https均可),支付成功後的頁面 點擊“返回商戶”按鈕的時候將異步通知報文post到該地址
// 如果想要實現過幾秒中自動跳轉回商戶頁面權限,需聯系銀聯業務申請開通自動返回商戶權限
params.put("frontUrl", unionPayConfig.getPayNotifyFrontUrl());
// 後台通知地址(需設置為【外網】能訪問 http https均可),支付成功後銀聯會自動將異步通知報文post到商戶上送的該地址,失敗的交易銀聯不會發送後台通知
// 注意:1.需設置為外網能訪問,否則收不到通知 2.http https均可
// 3.收單後台通知後需要10秒內返回http200或302狀態碼
// 4.如果銀聯通知服務器發送通知後10秒內未收到返回狀態碼或者應答碼非http200,那麼銀聯會間隔一段時間再次發送。總共發送5次,每次的間隔時間為0,1,2,4分鐘。
// 5.後台通知地址如果上送了帶有?的參數,例如:http://abc/web?a=b&c=d 在後台通知處理程序驗證簽名之前需要編寫邏輯將這些字段去掉再驗簽,否則將會驗簽失敗
params.put("backUrl", unionPayConfig.getPayNotifyUrl());
// 對請求參數拼接後進行消息摘要(SHA-1),然後用商戶私鑰進行簽名
sign(params);
// 生成表單HTML文檔
String html = createAutoFormHtml(unionPayConfig.getPayUrl(), params, UnionPayConfig.CHARSET);
prepayDto.setOrderId(param.getOrderId());
prepayDto.setOrderPaymentMethod(1);//TODO
prepayDto.setOutTradeNo(param.getOutTradeNo());
prepayDto.setResp(html);
return (PrepayDto<T>) prepayDto;
}
private void sign(Map<String, String> params) {
// 證書ID 填寫簽名私鑰證書的Serial Number,該值可通過銀聯提供的SDK獲取
params.put("certId", unionPayConfig.getCertId());
// 對請求參數進行簽名
String reqStr = PayUtil.mapToQueryStr(params, UnionPayConfig.CHARSET, false, true); // 將Map信息轉換成key1=value1&key2=value2的形式
byte[] signDigest = SHA1Util.sha1X16(reqStr, UnionPayConfig.CHARSET); // SHA-1消息摘要
byte[] byteSign = RSA.signBySoft(unionPayConfig.getPrivateKey(), signDigest); // 用商戶私鑰簽名
String sign = com.xiaoka.freework.utils.encrypt.Base64.encode(byteSign); // Base64編碼
// 報文摘要的簽名
params.put("signature", sign);
}
private static String createAutoFormHtml(String reqUrl, Map<String, String> hiddens, String encoding) {
StringBuilder sb = new StringBuilder();
sb.append("<html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=").append(encoding).append("\"/></head><body>");
sb.append("<form id = \"pay_form\" action=\"").append(reqUrl).append("\" method=\"post\">");
if (null != hiddens && 0 != hiddens.size()) {
Set<Map.Entry<String, String>> set = hiddens.entrySet();
for (Map.Entry<String, String> ey : set) {
String key = ey.getKey();
String value = ey.getValue();
sb.append("<input type=\"hidden\" name=\"").append(key).append("\" id=\"")
.append(key).append("\" value=\"").append(value).append("\"/>");
}
}
sb.append("</form>");
sb.append("</body>");
sb.append("<script type=\"text/javascript\">");
sb.append("document.all.pay_form.submit();");
sb.append("</script>");
sb.append("</html>");
return sb.toString();
}
public UnionPayConfig getUnionPayConfig() {
return unionPayConfig;
}
public void setUnionPayConfig(UnionPayConfig unionPayConfig) {
this.unionPayConfig = unionPayConfig;
}
}
調用方只需獲取銀聯實例調用方法即可:
CommonPay unionPay = commonPayFactory.getUnionPay();
PrepayParam prepayParam = new PrepayParam();
prepayParam.setOrderId(orderId);
prepayParam.setOutTradeNo(tradeNo);
prepayParam.setCardNumber(cardNo);
prepayParam.setTotalFee(amount.toString());
PrepayDto prepayDto = null;
try {
prepayDto = unionPay.prePay(prepayParam);
} catch (Exception e) {
logger.error("unionPay.prePay has error", e);
}
耐心點~