pay-spring-boot 開(kāi)箱即用的Java支付模塊,整合支付寶支付、微信支付
使用本模塊,可輕松實(shí)現(xiàn)支付寶支付、微信支付對(duì)接,從而專(zhuān)注于業(yè)務(wù),無(wú)需關(guān)心第三方邏輯。
模塊完全獨(dú)立,無(wú)支付寶、微信SDK依賴(lài)。
基于Spring Boot。
依賴(lài)Redis。
支付寶:電腦網(wǎng)站支付、手機(jī)網(wǎng)站支付、掃碼支付、APP支付。
微信:電腦網(wǎng)站支付(同掃碼支付)、手機(jī)網(wǎng)站支付(微信外H5支付)、掃碼支付、APP支付、JSAPI支付(微信內(nèi)H5支付)。
統(tǒng)一支付方法。
異步回調(diào)封裝。
訂單狀態(tài)查詢(xún)。
退款。
公對(duì)私轉(zhuǎn)賬。
請(qǐng)確保支付寶、微信帳號(hào)已經(jīng)申請(qǐng)了相應(yīng)業(yè)務(wù)、權(quán)限
模塊集成
只需要簡(jiǎn)單的、非侵入式的配置,即可集成到項(xiàng)目中。
添加模塊到Maven項(xiàng)目中
父項(xiàng)目中添加pay-spring-boot模塊依賴(lài)(pom.xml):
1 <modules> 2 ... 3 <module>pay-spring-boot</module> 4 ... 5 </modules>
修改pay-spring-boot的父項(xiàng)目(pom.xml):
1 <parent> 2 <groupId>yourself parent groupId</groupId> 3 <artifactId>yourself parent artifactId</artifactId> 4 <version>yourself parent version</version> 5 </parent>
支付憑證
在application.yml(或application-*.yml,視項(xiàng)目具體情況而定)中添加如下配置:
pay:
wx:
appid: wx1d96c6yxxc0d192a
mchid: 1519719912
key: A6nvI8Xp6A6nvI8Xp6A6nvI8Xp6
notifyURL: https://xxx.com/wxpay/notify
certPath: /data/cert/wx/apiclient_cert.p12
certPassword: 1517923901
ali:
appid: 2019138363328891
privateKey: MIIEuwIBADANBgkqhkiG9w...
notifyURL: https://xxx.com/alipay/notify
關(guān)于配置項(xiàng)的具體含義參考AliPayConfig、WxPayConfig兩個(gè)類(lèi),里邊有詳細(xì)說(shuō)明。
注入Redis連接池
在項(xiàng)目中創(chuàng)建一個(gè)Redis連接池工廠實(shí)現(xiàn)類(lèi),名稱(chēng)無(wú)所謂,必須實(shí)現(xiàn)RedisResourceFactory接口,然后添加@ResourceFactoryComponent注解,例如:
1 @ResourceFactoryComponent 2 public class DefaultRedisResourceFactory implements RedisResourceFactory { 3 @Override 4 public JedisPool getJedisPool() { 5 /* 6 框架不關(guān)心JedisPool是怎么來(lái)的 7 這里的RedisService是我自己實(shí)現(xiàn)的服務(wù) 8 根據(jù)項(xiàng)目實(shí)際情況換成你自己的實(shí)現(xiàn) 9 */ 10 return RedisService.getPool(); 11 } 12 }
如何支付
一般來(lái)說(shuō),項(xiàng)目中會(huì)有不同的支付場(chǎng)景,比如:購(gòu)買(mǎi)商品、充值等支付業(yè)務(wù)。
現(xiàn)在用商品購(gòu)買(mǎi)舉例,通過(guò)這個(gè)例子展示如何使用框架。
創(chuàng)建支付適配器
支付適配器是支付模塊和數(shù)據(jù)訪問(wèn)層的橋梁,適配器將支付結(jié)果抽象成支付成功(doPaySuccess)、支付失敗(doPayFail)、退款成功(doRefundSuccess)、退款失敗(doRefundFail)四種情況,代表了四個(gè)狀態(tài)。
建議不同的場(chǎng)景使用不同的適配器,不要所有業(yè)務(wù)邏輯都放一起。
創(chuàng)建訂單的操作,也建議放在適配器中實(shí)現(xiàn)。
以下是商品適配器例子:
1 @Service 2 public class GoodsTradeService extends AbstractPayAdaptor { 3 4 @Autowired 5 private GoodsTradeManager goodsTradeManager; 6 ? 7 /** 8 * 創(chuàng)建訂單 9 * @param id 商品id 10 * @return 11 */ 12 public Message createOrder(long id){ 13 14 } 15 ? 16 @Override 17 public void doPaySuccess(String outTradeNo) { 18 //支付成功,這里一般要更新數(shù)據(jù)庫(kù)的狀態(tài) 19 } 20 ? 21 @Override 22 public void doPayFail(String outTradeNo) { 23 //支付失敗,這里一般要更新數(shù)據(jù)庫(kù)的狀態(tài) 24 } 25 ? 26 @Override 27 public void doRefundSuccess(String outTradeNo) { 28 //退款成功,這里一般要更新數(shù)據(jù)庫(kù)的狀態(tài) 29 } 30 ? 31 @Override 32 public void doRefundFail(String outTradeNo) { 33 //退款失敗,這里一般要更新數(shù)據(jù)庫(kù)的狀態(tài) 34 } 35 }
看起來(lái)非常簡(jiǎn)單,繼承AbstractPayAdaptor抽象類(lèi),然后通過(guò)@Service注解交給Spring管理,為什么要交給Spring呢?因?yàn)檫@里你需要注入數(shù)據(jù)訪問(wèn)層的實(shí)例(Dao),不然怎么操作數(shù)據(jù)庫(kù),只不過(guò)我這沒(méi)有寫(xiě)而已~
這里有一個(gè)GoodsTradeManager,是接下來(lái)要介紹的支付管理器,先不管它。
仔細(xì)觀察會(huì)發(fā)現(xiàn),示例中的適配器名稱(chēng)叫GoodsTradeService,為什么我不管他叫GoodsTradePayAdaptor呢?從支付框架的角度看,這的確是一個(gè)適配器實(shí)現(xiàn),但從業(yè)務(wù)的角度看,它是商品訂單的服務(wù)中心,不僅要處理訂單狀態(tài),還要承擔(dān)創(chuàng)建訂單的職責(zé),它底層(數(shù)據(jù)庫(kù)層)關(guān)聯(lián)的本來(lái)就是一個(gè)訂單表,把它稱(chēng)作訂單服務(wù),更加容易理解。
因此,所謂的適配器,就是用來(lái)適配支付框架和數(shù)據(jù)訪問(wèn)層的。
創(chuàng)建支付管理器
有了適配器,就有了數(shù)據(jù)訪問(wèn)的能力,再配上一個(gè)管理器作為統(tǒng)一調(diào)度中心,那么支付這事就搞定了,實(shí)現(xiàn)一個(gè)管理器非常容易:
1 @PayManagerComponent 2 public class GoodsTradeManager extends AbstractPayManager { 3 ? 4 @Autowired 5 private GoodsTradeService goodsTradeService; 6 ? 7 @Override 8 public String getTradeType() { 9 return "0"; 10 } 11 ? 12 @Override 13 public AbstractPayAdaptor getPayAdaptor() { 14 return goodsTradeService; 15 } 16 }
首先繼承AbstractPayManager,然后使用@PayManagerComponent注解注冊(cè)管理器,這沒(méi)什么神奇的,只不過(guò)是告訴框架這里有一個(gè)管理器,并且把這個(gè)管理器交給Spring維護(hù)。
getTradeType方法返回長(zhǎng)度為1的字符串,建議取值范圍[0-9a-z],類(lèi)型會(huì)拼接到訂單號(hào)中,所以不建議使用特殊字符。因此,一個(gè)項(xiàng)目中最多可創(chuàng)建36個(gè)不同的管理器。
getPayAdaptor方法返回上一步創(chuàng)建的適配器,管理器中包含了適配器。
因此,所謂的管理器,管理的目標(biāo)就是適配器,同時(shí)擔(dān)負(fù)起統(tǒng)一支付調(diào)度的重任,管理器是支付模塊的窗口。
最佳實(shí)踐是:每對(duì)管理器-適配器對(duì)應(yīng)一種支付業(yè)務(wù)。
發(fā)起支付
接下來(lái)就可以發(fā)起支付了,非常簡(jiǎn)單,先來(lái)看一個(gè)支付寶掃碼支付示例:
1 Trade trade = AliTrade 2 .qrcodePay() 3 .subject("商品標(biāo)題") 4 .body("商品描述") 5 .outTradeNo(goodsTradeManager.newTradeNo("你自己的用戶(hù)唯一標(biāo)識(shí)")) 6 .totalAmount("0.01") 7 .build(); 8 TradeToken<String> token = goodsTradeManager.qrcodePay(trade); 9 String url = token.value();
先通過(guò)AliTrade構(gòu)造器的qrcodePay方法創(chuàng)建一個(gè)掃碼支付訂單,然后調(diào)用管理器的qrcodePay方法生成訂單憑證,不同的支付產(chǎn)品的訂單憑證可能不同,你可以自由選擇泛型,掃碼支付的憑證就是一個(gè)url鏈接,因此我使用的String類(lèi)型,調(diào)用憑證的value方法,即可獲得憑證內(nèi)容,憑證內(nèi)容直接返回給前端(網(wǎng)頁(yè)或APP),前端即可調(diào)起支付。
goodsTradeManager上一步已經(jīng)創(chuàng)建好,直接@Autowired注入即可。
newTradeNo方法非常重要,它可以幫你生成一個(gè)訂單號(hào),也就是商戶(hù)訂單號(hào),在我的設(shè)計(jì)中,為了省去繁瑣的全局唯一訂單號(hào)生成,將訂單號(hào)和用戶(hù)關(guān)聯(lián)起來(lái),規(guī)避了訂單號(hào)唯一性問(wèn)題,用戶(hù)唯一標(biāo)識(shí)根據(jù)你的系統(tǒng)自由選擇,建議長(zhǎng)度在[6-10]之間,并且為固定長(zhǎng)度,不能使用特殊字符,用戶(hù)唯一標(biāo)識(shí)會(huì)直接拼接到訂單號(hào)中,長(zhǎng)度不固定或太長(zhǎng)的話(huà),訂單號(hào)會(huì)非常難看,不規(guī)范,如需更多了解,直接看代碼注釋。
AliTrade構(gòu)造器所有的屬性均與支付寶官方文檔相對(duì)應(yīng),具體含義參考代碼注釋或者支付寶官方文檔。
訂單的類(lèi)型AliTrade.qrcodePay和管理器方法goodsTradeManager.qrcodePay必須配套使用。
再來(lái)看一個(gè)微信H5支付的例子:
1 Trade trade = WxTrade 2 .webMobilePay() 3 .body("商品標(biāo)題") 4 .outTradeNo(goodsTradeManager.newTradeNo("你自己的用戶(hù)唯一標(biāo)識(shí)")) 5 .totalFee("1") 6 .spbillCreateIp("127.0.0.1") 7 .sceneInfo("商品測(cè)試場(chǎng)景") 8 .build(); 9 TradeToken<String> token = goodsTradeManager.webMobilePay(trade); 10 String url = token.value();
只不過(guò)是把AliTrade換成了WxTrade,然后調(diào)用WxTrade.webMobilePay構(gòu)造器,加上配套的goodsTradeManager.webMobilePay即可完成微信H5支付。
微信H5支付的憑證也是一個(gè)url,直接交給前端處理即可。
由此可以看出,我們只需要關(guān)心訂單構(gòu)造器,將訂單構(gòu)造好,直接調(diào)用管理器對(duì)應(yīng)的方法即可,管理器不關(guān)心支付寶還是微信,只需要接收一個(gè)配套的訂單,最后拿到訂單憑證,就算是完工了。
依此類(lèi)推,即可完成其它類(lèi)型的支付業(yè)務(wù)。
異步回調(diào)
涉及錢(qián)的事沒(méi)有小事,別忘了還有支付結(jié)果異步回調(diào)。
前端的支付結(jié)果回調(diào)是同步回調(diào),僅供參考,必須以后端的結(jié)果為準(zhǔn)。
支付寶回調(diào):
1 @PostMapping(value = "/notify") 2 public void notify(HttpServletRequest request, HttpServletResponse response){ 3 /* 4 解析請(qǐng)求參數(shù) 5 */ 6 Map<String, String> params = NoticeManagers.getDefaultManager().receiveAliParams(request); 7 8 /* 9 封裝 10 */ 11 AliPayNoticeInfo info = new AliPayNoticeInfo(); 12 TradeStatus status = NoticeManagers.getDefaultManager().execute(params, info); 13 14 /* 15 持久化回調(diào)數(shù)據(jù) 16 */ 17 //TODO: 強(qiáng)烈建議將AliPayNoticeInfo持久化到數(shù)據(jù)庫(kù)中,以備不時(shí)之需,當(dāng)然你也可以忽略 18 19 /* 20 業(yè)務(wù)分發(fā) 21 */ 22 AbstractPayManager payManager = (AbstractPayManager) PayManagers.find(status.getTradeNo()); 23 payManager.doTradeStatus(status); 24 25 /* 26 響應(yīng) 27 */ 28 NoticeManagers.getDefaultManager().sendAliResponse(response); 29 }
微信回調(diào):
1 @PostMapping(value = "/notify") 2 public void notify(HttpServletRequest request, HttpServletResponse response){ 3 /* 4 解析請(qǐng)求參數(shù) 5 */ 6 Map<String, String> params = NoticeManagers.getDefaultManager().receiveWxParams(request); 7 8 /* 9 封裝 10 */ 11 WxPayNoticeInfo info = new WxPayNoticeInfo(); 12 TradeStatus status = NoticeManagers.getDefaultManager().execute(params, info); 13 14 /* 15 持久化回調(diào)數(shù)據(jù) 16 */ 17 //TODO: 強(qiáng)烈建議將WxPayNoticeInfo持久化到數(shù)據(jù)庫(kù)中,以備不時(shí)之需,當(dāng)然你也可以忽略 18 19 /* 20 業(yè)務(wù)分發(fā) 21 */ 22 AbstractPayManager payManager = (AbstractPayManager) PayManagers.find(status.getTradeNo()); 23 payManager.doTradeStatus(status); 24 25 /* 26 響應(yīng) 27 */ 28 NoticeManagers.getDefaultManager().sendWxResponse(response); 29 }
最基本的Spring MVC Controller代碼不用我教了吧。
定義一個(gè)控制器,接收HTTP請(qǐng)求、響應(yīng)對(duì)象,通過(guò)框架解析出參數(shù)和訂單狀態(tài),然后將訂單狀態(tài)分發(fā)給適配器,實(shí)現(xiàn)訂單狀態(tài)更新,最后給支付寶、微信一個(gè)響應(yīng),告訴他們已經(jīng)接收到請(qǐng)求。
回調(diào)處理非常規(guī)范化,基本不需要做什么改動(dòng)(直接Copy),唯一需要做的,也是非常重要的,就是根據(jù)你自己項(xiàng)目的實(shí)際情況,以恰當(dāng)?shù)姆绞匠志没卣{(diào)數(shù)據(jù)。
這里的@PostMapping請(qǐng)求路徑,就是配置在application.yml中的notifyURL,必須保證公網(wǎng)可以無(wú)障礙訪問(wèn)。
主動(dòng)同步訂單狀態(tài)
用來(lái)彌補(bǔ)特殊原因造成的異步回調(diào)丟失,異步回調(diào)不是100%可靠的。
由于需要請(qǐng)求支付寶、微信服務(wù)器,所以速度較慢。
支付寶訂單:
1 Trade trade = AliTrade.query().outTradeNo("商戶(hù)訂單號(hào)").build(); 2 TradeStatus status = goodsTradeManager.status(trade); 3 status.isPaySuccess(); //是否支付成功,其它狀態(tài)不一一列舉,自行看代碼
微信訂單:
1 Trade trade = WxTrade.basic().outTradeNo("商戶(hù)訂單號(hào)").build(); 2 TradeStatus status = goodsTradeManager.status(trade); 3 status.isPaySuccess(); //是否支付成功,其它狀態(tài)不一一列舉,自行看代碼
如何轉(zhuǎn)賬
公對(duì)私轉(zhuǎn)賬
由公司帳號(hào)向個(gè)人帳號(hào)轉(zhuǎn)賬。
支付寶轉(zhuǎn)賬:
1 /* 2 構(gòu)造轉(zhuǎn)賬訂單 3 */ 4 AliTransferTrade transferTrade = AliTransferTrade 5 .transfer() 6 .outBizNo("商戶(hù)轉(zhuǎn)賬唯一訂單號(hào)") 7 .payeeAccount("收款人支付寶帳號(hào)") 8 .amount("0.01") 9 .build(); 10 11 /* 12 轉(zhuǎn)賬 13 */ 14 try{ 15 AliTransfer.getInstance().transfer(transferTrade); 16 }catch (TransferException e){ 17 // 轉(zhuǎn)賬失敗處理邏輯... 18 }
轉(zhuǎn)賬方法無(wú)返回值,不發(fā)生異常代表轉(zhuǎn)賬成功,發(fā)生異常代表轉(zhuǎn)賬失敗,自行處理。
訂單參數(shù)含義參考支付寶官方文檔或代碼注釋。
微信轉(zhuǎn)賬:
1 /* 2 構(gòu)造轉(zhuǎn)賬訂單 3 */ 4 WxTransferTrade transferTrade = WxTransferTrade 5 .transfer() 6 .partnerTradeNo("商戶(hù)轉(zhuǎn)賬唯一訂單號(hào)") 7 .openid("收款人openid") 8 .amount("1") 9 .spbillCreateIp("127.0.0.1") //這里是調(diào)用接口的服務(wù)器公網(wǎng)IP,自行獲取 10 .build(); 11 /* 12 轉(zhuǎn)賬 13 */ 14 try{ 15 WxTransfer.getInstance().transfer(transferTrade); 16 }catch (TransferException e){ 17 // 轉(zhuǎn)賬失敗處理邏輯... 18 }
轉(zhuǎn)賬方法無(wú)返回值,不發(fā)生異常代表轉(zhuǎn)賬成功,發(fā)生異常代表轉(zhuǎn)賬失敗,自行處理。
訂單參數(shù)含義參考微信官方文檔或代碼注釋。
轉(zhuǎn)賬狀態(tài)查詢(xún)
支付寶:
1 /* 2 構(gòu)造轉(zhuǎn)賬查詢(xún)訂單 3 */ 4 AliTransferTrade transferTrade = AliTransferTrade 5 .query() 6 .outBizNo("商戶(hù)轉(zhuǎn)賬唯一訂單號(hào)") 7 .build(); 8 /* 9 轉(zhuǎn)賬查詢(xún) 10 */ 11 TransferStatus status = AliTransfer.getInstance().status(transferTrade);; 12 status.isSuccess(); //轉(zhuǎn)賬成功,其他狀態(tài)自行查看代碼,不一一列舉
微信:
1 /* 2 構(gòu)造轉(zhuǎn)賬查詢(xún)訂單 3 */ 4 WxTransferTrade transferTrade = WxTransferTrade 5 .custom() 6 .partnerTradeNo("商戶(hù)轉(zhuǎn)賬唯一訂單號(hào)") 7 .build(); 8 /* 9 轉(zhuǎn)賬查詢(xún) 10 */ 11 TransferStatus status = WxTransfer.getInstance().status(transferTrade);; 12 status.isSuccess(); //轉(zhuǎn)賬成功,其他狀態(tài)自行查看代碼,不一一列舉
附加工具
獲取客戶(hù)端IP地址
微信支付大部分場(chǎng)景需要客戶(hù)端IP地址,可以通過(guò)本模塊PayHttpUtil.getRealClientIp方法獲取。
如果獲取不到,請(qǐng)檢查代理軟件是否正確設(shè)置了X-Forwarded-For。
其他
如有疑問(wèn),歡迎積極反饋,直接提Issues別客氣。

浙公網(wǎng)安備 33010602011771號(hào)