聚合支付-支付渠道接入設(shè)計(jì)及實(shí)現(xiàn)

聚合支付:也稱“融合支付”,是指只從事“支付、結(jié)算、清算”服務(wù)之外的“支付服務(wù)”,依托銀行、非銀機(jī)構(gòu)或清算組織,借助銀行、非銀機(jī)構(gòu)或清算組織的支付通道與清結(jié)算能力,利用自身的技術(shù)與服務(wù)集成能力,將一個(gè)以上的銀行、非銀機(jī)構(gòu)或清算組織的支付服務(wù),整合到一起,為商戶提供包括但不限于“支付通道服務(wù)”、“集合對(duì)賬服務(wù)”、“技術(shù)對(duì)接服務(wù)”、“差錯(cuò)處理服務(wù)”、“金融服務(wù)引導(dǎo)”、“會(huì)員賬戶服務(wù)”、“作業(yè)流程軟件服務(wù)”、“運(yùn)行維護(hù)服務(wù)”、“終端提供與維護(hù)”等服務(wù)內(nèi)容,以此減少商戶接入、維護(hù)支付結(jié)算服務(wù)時(shí)面臨的成本支出,提高商戶支付結(jié)算系統(tǒng)運(yùn)行效率的,并收取增值收益的支付服務(wù)
-----百度百科

本文主要介紹了聚合支付系統(tǒng)中支付渠道接入模塊的設(shè)計(jì)和實(shí)現(xiàn),目錄如下:

目錄

1,知識(shí)準(zhǔn)備

  1. 加密和解密
  2. 摘要加密
  3. Base64
  4. 對(duì)稱加密

2,支付渠道配置設(shè)計(jì)

  1. 支付接口類型
  2. 支付接口
  3. 支付通道
  4. 支付通道賬戶

3,支付渠道服務(wù)開(kāi)發(fā)設(shè)計(jì)

  1. 支付流程說(shuō)明
  2. 支付渠道接入設(shè)計(jì)

4,實(shí)戰(zhàn)(支付寶接口接入)

5,總結(jié)


一,知識(shí)準(zhǔn)備

在討論支付渠道接入設(shè)計(jì)之前,我們先來(lái)了解下支付過(guò)程中用到的安全相關(guān)知識(shí)。

1, 加密和解密

加密技術(shù)源遠(yuǎn)流長(zhǎng),自從古代有了信息的傳遞和存儲(chǔ),就有了加密技術(shù)的運(yùn)用。此后,很長(zhǎng)一段時(shí)間里,加密及解密技術(shù)在軍事、政治、外交、金融等特殊領(lǐng)域里被普遍采用,并經(jīng)過(guò)長(zhǎng)時(shí)間的研究和發(fā)展,形成了比較完備的一門學(xué)科——密碼學(xué)。

密碼學(xué)是研究加密方法、秘密通信的原理,以及解密方法、破譯密碼的方法的一門科學(xué)。

加密和解密的過(guò)程大致如下:

  • 首先,信息的發(fā)送方準(zhǔn)備好要發(fā)送信息的原始形式,叫作明文。

  • 然后對(duì)明文經(jīng)過(guò)一系列變換后形成信息的另一種不能直接體現(xiàn)明文含義的形式,叫作密文。

  • 由明文轉(zhuǎn)換為密文的過(guò)程叫作加密。

  • 在加密時(shí)所采用的一組規(guī)則或方法稱為加密算法。

  • 解密:接收者在收到密文后,再把密文還原成明文,以獲得信息的具體內(nèi)容,這個(gè)過(guò)程叫作解密。

  • 解密算法:解密時(shí)也要運(yùn)用一系列與加密算法相對(duì)應(yīng)的方法或規(guī)則,這種方法或規(guī)則叫作解密算法。

  • 密鑰:在加密、解密過(guò)程中,由通信雙方掌握的參數(shù)信息控制具體的加密和解密過(guò)程,這個(gè)參數(shù)叫作密鑰。

密鑰分為加密密鑰和解密密鑰,分別用于加密過(guò)程和解密過(guò)程。

稱密鑰密碼體制:在加密和解密的過(guò)程中,如果采用的加密密鑰與解密密鑰相同,或者從一個(gè)很容易計(jì)算出另一個(gè),則這種方法叫作對(duì)稱密鑰密碼體制,也叫作單鑰密碼體制。

雙鑰密碼體制:反之,如果加密和解密的密鑰并不相同,或者從一個(gè)很難計(jì)算出另外一個(gè),就叫作不對(duì)稱密鑰密碼系統(tǒng)或者公開(kāi)密鑰密碼體制,也叫作雙鑰密碼體制。

2, 摘要加密

摘要數(shù)據(jù):47bce5c74f589f4867dbd57e9ca9f808

摘要是哈希值,我們通過(guò)散列算法比如MD5算法就可以得到這個(gè)哈希值。摘要只是用于驗(yàn)證數(shù)據(jù)完 整性和唯一性的哈希值,

不管原始數(shù)據(jù)是什么樣的,得到的哈希值都是固定長(zhǎng)度的。

不管原始數(shù)據(jù)是什么樣的,得到的哈希值都是固定長(zhǎng)度的,也就是說(shuō)摘要并不是原始數(shù)據(jù)加密后的 密文,只是一個(gè)驗(yàn)證身份的令牌。所以我們無(wú)法通過(guò)摘要解密得到原始數(shù)據(jù)。

常用的摘要算法有:MD5算法(MD2 、MD4、MD5),SHA算法(SHA1、SHA256、SHA384、 SHA512),HMAC算法 摘要加密算法特性:

1:任何數(shù)據(jù)加密,得到的密文長(zhǎng)度固定。

2:密文是無(wú)法解密的(不可逆)。

  1. MD5

    MD5信息摘要算法(英語(yǔ):MD5 Message-Digest Algorithm),一種被廣泛使用的密碼散列函 數(shù),可以產(chǎn)生出一個(gè)128位(16字節(jié))的散列值(hash value),用于確保信息傳輸完整一致。MD5由 美國(guó)密碼學(xué)家羅納德·李維斯特(Ronald Linn Rivest)設(shè)計(jì),于1992年公開(kāi),用以取代MD4算法。這套 算法的程序在 RFC 1321 標(biāo)準(zhǔn)中被加以規(guī)范。1996年后該算法被證實(shí)存在弱點(diǎn),可以被加以破解,對(duì)于 需要高度安全性的數(shù)據(jù),專家一般建議改用其他算法,如SHA-2。2004年,證實(shí)MD5算法無(wú)法防止碰撞 (collision),因此不適用于安全性認(rèn)證,如SSL公開(kāi)密鑰認(rèn)證或是數(shù)字簽名等用途。

    MD5存在一個(gè)缺陷,只要明文相同,那么生成的MD5碼就相同,于是攻擊者就可以通過(guò)撞庫(kù)的方式 來(lái)破解出明文。加鹽就是向明文中加入指定字符,主要用于混淆用戶、并且增加MD5撞庫(kù)破解難度,這 樣一來(lái)即使撞庫(kù)破解,知道了明文,但明文也是混淆了的,真正需要用到的數(shù)據(jù)也需要從明文中摘取, 摘取范圍、長(zhǎng)度、摘取方式都是個(gè)謎,如此一來(lái)就大大增加了暴力破解的難度,使其幾乎不可能破解。

    我們來(lái)編寫(xiě)一個(gè)MD5案例 ,代碼如下:

    public class MD5 {
        /**
        * MD5方法
        * @param text 明文
        * @return 密文
        * @throws Exception
        */
        public static String md5(String text) throws Exception {
            //加密后的字符串
            String encode= DigestUtils.md5Hex(text);
            return encode;
        }
        /**
        * MD5方法
        * @param text 明文
        * @param key 鹽
        * @return 密文
        * @throws Exception
        */
        public static String md5(String text, String key) throws Exception {
            //加密后的字符串
            String encode= DigestUtils.md5Hex(text + key);
            return encode;
        }
        /**
        * MD5驗(yàn)證方法
        * @param text 明文
        * @param key 密鑰
        * @param md5 密文
        * @return true/false
        * @throws Exception
        */
        public static boolean verify(String text, String key, String md5) throws
        Exception {
            //根據(jù)傳入的密鑰進(jìn)行驗(yàn)證
            String md5Text = md5(text, key);
            return md5Text.equalsIgnoreCase(md5);
        }
    }
    
    
  2. 驗(yàn)簽

    驗(yàn)簽其實(shí)就是簽名驗(yàn)證,MD5加密算法經(jīng)常用于簽名安全驗(yàn)證。關(guān)于驗(yàn)簽,我們用下面這個(gè)流程圖來(lái)說(shuō)明:

驗(yàn)簽.png

1:order-service向pay-service服務(wù)發(fā)送數(shù)據(jù)前,先對(duì)數(shù)據(jù)進(jìn)行處理。

2:先把數(shù)據(jù)封裝到Map中,再對(duì)數(shù)據(jù)進(jìn)行排序。

3:獲取排序后的數(shù)據(jù)的MD5只,并將MD5只封裝到Map中。

4:把帶有MD5只的Map傳給pay-service。

5:pay-service中獲取到數(shù)據(jù),移除Map中的MD5值,再將Map排序。

6:獲取排序后的MD5值,并且對(duì)比傳過(guò)來(lái)的MD5值。

7:兩個(gè)MD5值如果一樣,證明該數(shù)據(jù)安全,沒(méi)有被修改,如果不一樣,證明數(shù)據(jù)被修改了。

3, Base64

Base64是網(wǎng)絡(luò)上最常見(jiàn)的用于傳輸8Bit字節(jié)碼的編碼方式之一,Base64就是一種基于64個(gè)可打印 字符來(lái)表示二進(jìn)制數(shù)據(jù)的方法。

Base64編碼是從二進(jìn)制到字符的過(guò)程,可用于在HTTP環(huán)境下傳遞較長(zhǎng)的標(biāo)識(shí)信息。采用Base64編 碼具有不可讀性,需要解碼后才能閱讀。

Base64由于以上優(yōu)點(diǎn)被廣泛應(yīng)用于計(jì)算機(jī)的各個(gè)領(lǐng)域,然而由于輸出內(nèi)容中包括兩個(gè)以上“符號(hào)類” 字符(+, /, =),不同的應(yīng)用場(chǎng)景又分別研制了Base64的各種“變種”。為統(tǒng)一和規(guī)范化Base64的輸出, Base62x被視為無(wú)符號(hào)化的改進(jìn)版本,但Base62x的性能效率偏低,目前還不建議在項(xiàng)目中使用。

標(biāo)準(zhǔn)的Base64并不適合直接放在URL里傳輸,因?yàn)閁RL編碼器會(huì)把標(biāo)準(zhǔn)Base64中的“/”和“+”字符變 為形如“%XX”的形式,而這些“%”號(hào)在存入數(shù)據(jù)庫(kù)時(shí)還需要再進(jìn)行轉(zhuǎn)換,因?yàn)锳NSI SQL中已將“%”號(hào)用作 通配符。

為解決此問(wèn)題,可采用一種用于URL的改進(jìn)Base64編碼,它在末尾填充'='號(hào),并將標(biāo)準(zhǔn)Base64中 的“+”和“/”分別改成了“-”和“_”,這樣就免去了在URL編解碼和數(shù)據(jù)庫(kù)存儲(chǔ)時(shí)所要作的轉(zhuǎn)換,避免了編碼信 息長(zhǎng)度在此過(guò)程中的增加,并統(tǒng)一了數(shù)據(jù)庫(kù)、表單等處對(duì)象標(biāo)識(shí)符的格式。

Base64Util 代碼如下:

public class Base64Util {
    /***
    * 普通解密操作
    * @param encodedText
    * @return
    */
    public static byte[] decode(String encodedText){
        final Base64.Decoder decoder = Base64.getDecoder();
        return decoder.decode(encodedText);
    }
    /***
    * 普通加密操作
    * @param data
    * @return
    */
    public static String encode(byte[] data){
        final Base64.Encoder encoder = Base64.getEncoder();
        return encoder.encodeToString(data);
    }
    /***
    * 解密操作
    * @param encodedText
    * @return
    */
    public static byte[] decodeURL(String encodedText){
        final Base64.Decoder decoder = Base64.getUrlDecoder();
        return decoder.decode(encodedText);
    }
    /***
    * 加密操作
    * @param data
    * @return
    */
    public static String encodeURL(byte[] data){
        final Base64.Encoder encoder = Base64.getUrlEncoder();
        return encoder.encodeToString(data);
    }
}

4,對(duì)稱加密

前面我們學(xué)習(xí)了MD5,MD5加密后本質(zhì)上是無(wú)法解密,是一個(gè)不可逆的過(guò)程,而網(wǎng)上有很多解密其 實(shí)都是一種窮舉法對(duì)比,根本不存在破解方法。

在業(yè)務(wù)中,很多時(shí)候存在解密的需要,我們可以采用對(duì)稱加密,對(duì)稱加密是指加密和解密都采用相 同的秘鑰。使用對(duì)稱加密,發(fā)送方使用密鑰將明文數(shù)據(jù)加密成密文,然后發(fā)送出去,接收方收到密文 后,使用同一個(gè)密鑰將密文解密成明文讀取,我們可以用一個(gè)很形象的例子來(lái)解釋對(duì)稱加密,例如:只 有一模一樣的鑰匙才能打開(kāi)同一個(gè)鎖,也只有那把鑰匙能鎖住那把鎖。

  1. AES詳解

    典型的對(duì)稱加密算法有DES、3DES、AES,但AES加密算法的安全性要高于DES和3DES,所以AES 已經(jīng)成為了主要的對(duì)稱加密算法。

    AES加密算法就是眾多對(duì)稱加密算法中的一種,它的英文全稱是Advanced Encryption Standard, 翻譯過(guò)來(lái)是高級(jí)加密標(biāo)準(zhǔn),它是用來(lái)替代之前的DES加密算法的。

    要理解AES的加密流程,會(huì)涉及到AES加密的五個(gè)關(guān)鍵詞,分別是:分組密碼體制、Padding、密 鑰、初始向量IV和四種加密模式,下面我們一一介紹。

    • 分組密碼體制:所謂分組密碼體制就是指將明文切成一段一段的來(lái)加密,然后再把一段一段的密文 拼起來(lái)形成最終密文的加密方式。AES采用分組密碼體制,即AES加密會(huì)首先把明文切成一段一段的,而 且每段數(shù)據(jù)的長(zhǎng)度要求必須是128位16個(gè)字節(jié),如果最后一段不夠16個(gè)字節(jié)了,就需要用Padding來(lái)把 這段數(shù)據(jù)填滿16個(gè)字節(jié),然后分別對(duì)每段數(shù)據(jù)進(jìn)行加密,最后再把每段加密數(shù)據(jù)拼起來(lái)形成最終的密 文。
    分組密碼填充.png
  • Padding:Padding就是用來(lái)把不滿16個(gè)字節(jié)的分組數(shù)據(jù)填滿16個(gè)字節(jié)用的,它有三種模式 PKCS5、PKCS7和NOPADDING。PKCS5是指分組數(shù)據(jù)缺少幾個(gè)字節(jié),就在數(shù)據(jù)的末尾填充幾個(gè)字節(jié)的 幾,比如缺少5個(gè)字節(jié),就在末尾填充5個(gè)字節(jié)的5。PKCS7是指分組數(shù)據(jù)缺少幾個(gè)字節(jié),就在數(shù)據(jù)的末 尾填充幾個(gè)字節(jié)的0,比如缺少7個(gè)字節(jié),就在末尾填充7個(gè)字節(jié)的0。NoPadding是指不需要填充,也就 是說(shuō)數(shù)據(jù)的發(fā)送方肯定會(huì)保證最后一段數(shù)據(jù)也正好是16個(gè)字節(jié)。那如果在PKCS5模式下,最后一段數(shù)據(jù) 的內(nèi)容剛好就是16個(gè)16怎么辦?那解密端就不知道這一段數(shù)據(jù)到底是有效數(shù)據(jù)還是填充數(shù)據(jù)了,因此對(duì) 于這種情況,PKCS5模式會(huì)自動(dòng)幫我們?cè)谧詈笠欢螖?shù)據(jù)后再添加16個(gè)字節(jié)的數(shù)據(jù),而且填充數(shù)據(jù)也是16 個(gè)16,這樣解密段就能知道誰(shuí)是有效數(shù)據(jù)誰(shuí)是填充數(shù)據(jù)了。PKCS7最后一段數(shù)據(jù)的內(nèi)容是16個(gè)0,也是 同樣的道理。解密端需要使用和加密端同樣的Padding模式,才能準(zhǔn)確的識(shí)別有效數(shù)據(jù)和填充數(shù)據(jù)。我 們開(kāi)發(fā)通常采用PKCS7 Padding模式。

    PKCS5填充方式:


    PKCS5.png
  • 初始向量IV:初始向量IV的作用是使加密更加安全可靠,我們使用AES加密時(shí)需要主動(dòng)提供初始向 量,而且只需要提供一個(gè)初始向量就夠了,后面每段數(shù)據(jù)的加密向量都是前面一段的密文。初始向量IV 的長(zhǎng)度規(guī)定為128位16個(gè)字節(jié),初始向量的來(lái)源為隨機(jī)生成。至于為什么初始向量能使加密更安全可靠。

  • 密鑰:AES要求密鑰的長(zhǎng)度可以是128位16個(gè)字節(jié)、192位或者256位,位數(shù)越高,加密強(qiáng)度自然越 大,但是加密的效率自然會(huì)低一些,因此要做好衡量。我們開(kāi)發(fā)通常采用128位16個(gè)字節(jié)的密鑰,我們 使用AES加密時(shí)需要主動(dòng)提供密鑰,而且只需要提供一個(gè)密鑰就夠了,每段數(shù)據(jù)加密使用的都是這一個(gè)密鑰,密鑰來(lái)源為隨機(jī)生成。

  • 四種加密模式:AES一共有四種加密模式,分別是ECB(電子密碼本模式)CBC(密碼分組鏈接模式)、CFB、OFB,我們一般使用的是ECB和CBC模式。四種模式中除了ECB相對(duì)不安全之外,其它三 種模式的區(qū)別并沒(méi)有那么大,因此這里只會(huì)對(duì)ECB和CBC模式做一下對(duì)比,看看它們?cè)谧鍪裁础?/p>

ECB加密模式.png

ECB模式是最基本的加密模式,即僅僅使用明文和密鑰來(lái)加密數(shù)據(jù),相同的明文塊會(huì)被加密成相同的密文塊, 這樣明文和密文的結(jié)構(gòu)將是完全一樣的,就會(huì)更容易被破解,相對(duì)來(lái)說(shuō)不是那么安全,因此很少使用。

CBC加密模式.png

CBC模式則比ECB模式多了一個(gè)初始向量IV,加密的時(shí)候,第一個(gè)明文塊會(huì)首先和初始向量IV做異或操作,然后再經(jīng)過(guò)密鑰加密,然后第一個(gè)密文塊又會(huì)作為第二個(gè)明文塊的加密向量來(lái)異或,依次類推下去,這樣相同的明文塊加密出的密文塊就是不同的,明文的結(jié)構(gòu)和密文的結(jié)構(gòu)也將是不同的,因此更加安全。

  1. AES算法下載

java 中的 AES 秘鑰為 256bit 算法執(zhí)行時(shí),會(huì)遇到 Illegal key size or default parameters 錯(cuò),原因是因?yàn)楸镜貨](méi)有對(duì)應(yīng)的算法庫(kù),需要下載對(duì)應(yīng)JDK版本的算法庫(kù)。

JDK8 jar 包下載地址: https://www.oracle.com/java/technologies/javase-jce8-downloads.html

JDK7 jar 包下載地址: https://www.oracle.com/java/technologies/javase-jce7-downloads.html

JDK6 jar 包下載地址: https://www.oracle.com/java/technologies/jce-6-download.html

下載后解壓,可以看到 local_policy.jar 和 US_export_policy.jar 以及 readme.txt 。

如果安裝了JRE,將兩個(gè)jar文件放到 %JRE_HOME%\lib\security 目錄下覆蓋原來(lái)的文件。

如果安裝了JDK,還要將兩個(gè)jar文件也放到 %JDK_HOME%\jre\lib\security 目錄下覆蓋原來(lái)文件。

  1. AES實(shí)戰(zhàn)

    使用AES加密、解密,他們的執(zhí)行過(guò)程都是一樣的,步驟如下:

    1:加載加密解密算法處理對(duì)象(包含算法、秘鑰管理)

    2:根據(jù)不同算法創(chuàng)建秘鑰

    3:設(shè)置加密模式(無(wú)論是加密還是解析,模式一致)

    4:初始化加密配置

    5:執(zhí)行加密/解密

    我們編寫(xiě)一個(gè)類 AESCoder ,既可以實(shí)現(xiàn)加密,也可以實(shí)現(xiàn)解密,代碼如下:

    public abstract class AESCoder extends SecurityCoder {
     public static final String KEY_ALGORITHM = "AES";
    
     /**
      * @param rawKey
      *            密鑰
      * @param clearPwd
      *            明文字符串
      * @return 密文字節(jié)數(shù)組
      */
     public static byte[] encrypt(byte[] rawKey, String clearPwd) {
         try {
             SecretKeySpec secretKeySpec = new SecretKeySpec(rawKey, KEY_ALGORITHM);
             Cipher cipher = Cipher.getInstance(KEY_ALGORITHM);
             cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
             byte[] encypted = cipher.doFinal(clearPwd.getBytes());
             return encypted;
         } catch (Exception e) {
             return null;
         }
     }
    
     /**
      * @param encrypted
      *            密文字節(jié)數(shù)組
      * @param rawKey
      *            密鑰
      * @return 解密后的字符串
      */
     public static String decrypt(byte[] encrypted, byte[] rawKey) {
         try {
             SecretKeySpec secretKeySpec = new SecretKeySpec(rawKey, KEY_ALGORITHM);
             Cipher cipher = Cipher.getInstance(KEY_ALGORITHM);
             cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
             byte[] decrypted = cipher.doFinal(encrypted);
             return new String(decrypted);
         } catch (Exception e) {
             e.printStackTrace();
             return "";
         }
     }
    
     /**
      * @param seed 種子數(shù)據(jù)
      * @return 密鑰數(shù)據(jù)
      */
     public static byte[] getRawKey(byte[] seed) {
         byte[] rawKey = null;
         try {
             KeyGenerator kgen = KeyGenerator.getInstance(KEY_ALGORITHM);
             SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
             secureRandom.setSeed(seed);
             // AES加密數(shù)據(jù)塊分組長(zhǎng)度必須為128比特,密鑰長(zhǎng)度可以是128比特、192比特、256比特中的任意一個(gè)
             kgen.init(128, secureRandom);
             SecretKey secretKey = kgen.generateKey();
             rawKey = secretKey.getEncoded();
         } catch (NoSuchAlgorithmException e) {
         }
         return rawKey;
     }
        /**
         * 將二進(jìn)制轉(zhuǎn)換成16進(jìn)制 
         * <p>說(shuō)明:</p>
         * <li></li>
         * @author DuanYong
         * @param buf
         * @return
         * @since 2017年11月16日上午8:59:33
         */
     public static String parseByte2HexStr(byte buf[]) {
         StringBuffer sb = new StringBuffer();
         for (int i = 0; i < buf.length; i++) {
             String hex = Integer.toHexString(buf[i] & 0xFF);
             if (hex.length() == 1) {
                 hex = '0' + hex;
             }
             sb.append(hex.toUpperCase());
         }
         return sb.toString();
     }
        /**
         * 將16進(jìn)制轉(zhuǎn)換為二進(jìn)制 
         * <p>說(shuō)明:</p>
         * <li></li>
         * @author DuanYong
         * @param hexStr
         * @return
         * @since 2017年11月16日上午8:59:51
         */
     public static byte[] parseHexStr2Byte(String hexStr) {
         if (hexStr.length() < 1){
                return null;
            }
         byte[] result = new byte[hexStr.length() / 2];
         for (int i = 0; i < hexStr.length() / 2; i++) {
             int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16);
             int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2), 16);
             result[i] = (byte) (high * 16 + low);
         }
         return result;
     }
    }
    

二,支付渠道配置設(shè)計(jì)

渠道接入.png

支付渠道的接入主要由支付渠道配置和支付渠道服務(wù)開(kāi)發(fā)組成:

支付渠道配置:主要是完成接入渠道所需相關(guān)參數(shù)的配置。

支付渠道服務(wù)開(kāi)發(fā):主要是根據(jù)系統(tǒng)支付渠道接入規(guī)范,開(kāi)發(fā)對(duì)應(yīng)支付渠道服務(wù)。

支付渠道配置設(shè)計(jì)如下圖所示:


支付渠道配置設(shè)計(jì).png
  1. 支付接口類型

    主要定義支付接口的類型,如:阿里支付,微信支付這類渠道類型。

    主要參數(shù):

    • 接口類型代碼:唯一標(biāo)識(shí)一個(gè)渠道,如:阿里支付:alipay

    • 接口類型名稱:名稱,如:支付寶官方支付

    • 狀態(tài) :渠道開(kāi)啟/關(guān)閉狀態(tài)控制

    • 備注信息 :描述信息

    • 配置定義描述:主要用于定義不同渠道參數(shù)配置項(xiàng),以便在支付接口通道配置時(shí)自動(dòng)生成配置項(xiàng)。

      自定義描述符說(shuō)明如下:

      字段 說(shuō)明
      name 字段名稱,如:pid
      desc 字段名稱描述,如:商戶PID
      type 字段類型,取值:
      text->生成input文本輸入框
      textarea->生成textarea文本輸入域
      verify 字段校驗(yàn)類型,取值:
      required->表示必填

      如支付寶渠道配置參數(shù)描述如下:

      [{
         "name": "pid",
         "desc": "商戶PID",
         "type": "text",
         "verify": "required"
      }, {
         "name": "appId",
         "desc": "應(yīng)用App ID",
         "type": "text",
         "verify": "required"
      }, {
         "name": "alipayAccount",
         "desc": "支付寶賬戶",
         "type": "text",
         "verify": "required"
      }, {
         "name": "privateKey",
         "desc": "應(yīng)用私鑰",
         "type": "textarea",
         "verify": "required"
      }, {
         "name": "alipayPublicKey",
         "desc": "支付寶公鑰",
         "type": "textarea"
      }, {
         "name": "reqUrl",
         "desc": "網(wǎng)關(guān)地址",
         "type": "text",
         "verify": "required"
      }] 
      

      界面設(shè)計(jì)效果:

支付接口類型配置.png

數(shù)據(jù)庫(kù)設(shè)計(jì):t_pay_interface_type

字段 類型 長(zhǎng)度 注釋
IfTypeCode varchar 30 接口類型代碼
IfTypeName varchar 30 接口類型名稱
Status tinyint 1 狀態(tài),0-關(guān)閉,1-開(kāi)啟
Param varchar 4096 接口配置定義描述,json字符串
Remark varchar 128 備注
CreateTime timestamp 0 創(chuàng)建時(shí)間
UpdateTime timestamp 0 更新時(shí)間
  1. 支付接口:

    支付接口與支付接口類型為一對(duì)多關(guān)系,主要配置具體的支付接口,如阿里支付接口類型下,包含H5支付,WAP支付,現(xiàn)金紅包支付等各種支付接口。

    主要參數(shù):

    • 接口類型:選擇接口類型,如:支付寶官方支付
    • 接口代碼:定義接口代碼,唯一標(biāo)識(shí)支付接口,如:alipay_pc
    • 接口名稱:定義接口名稱,描述該接口,如:支付寶PC支付
    • 支付類型:定義接口支付類型,如:網(wǎng)銀支付
    • 應(yīng)用場(chǎng)景:描述該接口使用的場(chǎng)景,如:移動(dòng)APP,移動(dòng)網(wǎng)頁(yè),PC網(wǎng)頁(yè),微信公眾平臺(tái),手機(jī)掃碼等
    • 擴(kuò)展參數(shù):
      當(dāng)支付類型為網(wǎng)銀支付時(shí),可配置支持的銀行列表.格式如:[{'bank':'zhonghang','code':'300008'},{'bank':'nonghang','code':'300009'}]
    • 狀態(tài) :接口開(kāi)啟/關(guān)閉狀態(tài)控制
    • 備注信息:一些其他描述

    界面設(shè)計(jì)效果:

支付接口配置.png

數(shù)據(jù)庫(kù)設(shè)計(jì):t_pay_interface

字段 類型 長(zhǎng)度 注釋
IfCode varchar 30 接口代碼
IfName varchar 30 接口名稱
IfTypeCode varchar 30 接口類型代碼
PayType varchar 2 支付類型
Scene tinyint 6 應(yīng)用場(chǎng)景,1:移動(dòng)APP,2:移動(dòng)網(wǎng)頁(yè),3:PC網(wǎng)頁(yè),4:微信公眾平臺(tái),5:手機(jī)掃碼
Status tinyint 6 接口狀態(tài),0-關(guān)閉,1-開(kāi)啟
Param varchar 4096 配置參數(shù),json字符串
Remark varchar 128 備注
CreateTime timestamp 0 創(chuàng)建時(shí)間
UpdateTime timestamp 0 更新時(shí)間
Extra varchar 1024 擴(kuò)展參數(shù)
  1. 支付接口通道:

    支付接口通道與具體的支付接口綁定,定義風(fēng)控,費(fèi)率,子賬戶相關(guān)參數(shù),如:通道費(fèi)率,單筆最大金額,日限額,開(kāi)啟/結(jié)束時(shí)間等。一個(gè)支付接口可以與多個(gè)通道綁定,以支持不同風(fēng)控策略。

    通道基本信息設(shè)置:

    • 通道名稱:定義通道名稱,如:支付寶PC支付通道
    • 支付接口:下拉選擇具體支付接口,如:支付寶PC支付
    • 支付類型:下拉選擇具體支付類型,如:支付寶掃碼支付
    • 通道狀態(tài) :通道開(kāi)啟/關(guān)閉狀態(tài)控制
    • 備注信息:一些其他描述

    通道風(fēng)控信息設(shè)置:

    • 當(dāng)天交易金額(元):當(dāng)天交易最大金額(日限額)
    • 單筆最大金額(元):?jiǎn)喂P交易最大金額
    • 單筆最小金額(元):?jiǎn)喂P交易最小金額
    • 交易開(kāi)始時(shí)間:交易開(kāi)始時(shí)間
    • 交易結(jié)束時(shí)間:交易結(jié)束時(shí)間
    • 風(fēng)控狀態(tài):風(fēng)控開(kāi)啟/關(guān)閉狀態(tài)控制

    通道費(fèi)率信息設(shè)置:

    • 通道費(fèi)率(%):定義通道單筆交易費(fèi)率

    界面設(shè)計(jì)效果:

    通道基本信息設(shè)置

通道基本設(shè)置.png

通道風(fēng)控信息設(shè)置

通道風(fēng)控設(shè)置.png

通道費(fèi)率信息設(shè)置

通道費(fèi)率設(shè)置.png

數(shù)據(jù)庫(kù)設(shè)計(jì):t_pay_passage

字段 類型 長(zhǎng)度 注釋
id int 11 支付通道ID
PassageName varchar 30 通道名稱
IfCode varchar 30 接口代碼
IfTypeCode varchar 30 接口類型代碼
PayType varchar 2 支付類型
Status tinyint 6 通道狀態(tài),0-關(guān)閉,1-開(kāi)啟
PassageRate decimal 20 通道費(fèi)率百分比
MaxDayAmount bigint 20 當(dāng)天交易金額,單位分
MaxEveryAmount bigint 20 單筆最大金額,單位分
MinEveryAmount bigint 20 單筆最小金額,單位分
TradeStartTime varchar 20 交易開(kāi)始時(shí)間
TradeEndTime varchar 20 交易結(jié)束時(shí)間
RiskStatus tinyint 6 風(fēng)控狀態(tài),0-關(guān)閉,1-開(kāi)啟
Remark varchar 128 備注
CreateTime timestamp 0 創(chuàng)建時(shí)間
UpdateTime timestamp 0 更新時(shí)間
  1. 支付接口通道賬戶:

    支付通道賬戶是支付通道下的一個(gè)子配置項(xiàng),主要配置該通道下包含的賬戶信息以及賬戶風(fēng)控信息,可以配置多個(gè),多個(gè)賬戶根據(jù)配置使用策略(單一/輪詢)來(lái)使用。分為基本信息和參數(shù)信息,基本信息描述賬戶相關(guān)基本信息,如名稱,狀態(tài)等。賬戶參數(shù)信息則是根據(jù)通道綁定的支付接口所屬支付接口類型的配置定義描述來(lái)動(dòng)態(tài)生成配置項(xiàng)。

    賬戶基本信息配置:

    • 賬戶名稱:賬戶名稱
    • 賬戶狀態(tài) :賬戶開(kāi)啟/關(guān)閉狀態(tài)控制
    • 渠道商戶ID:聚合支付商戶ID
    • 輪詢權(quán)重:輪詢時(shí)的權(quán)重
    • 備注:一些說(shuō)明

    賬戶參數(shù)信息配置:

    • 根據(jù)通道綁定的支付接口所屬支付接口類型的配置定義描述來(lái)動(dòng)態(tài)生成。

    賬戶風(fēng)控信息配置:

    • 風(fēng)控模式:指定風(fēng)控模式:繼承通道/自定義
    • 當(dāng)天交易金額(元):當(dāng)天交易最大金額(日限額)
    • 單筆最大金額(元):?jiǎn)喂P交易最大金額
    • 單筆最小金額(元):?jiǎn)喂P交易最小金額
    • 交易開(kāi)始時(shí)間:交易開(kāi)始時(shí)間
    • 交易結(jié)束時(shí)間:交易結(jié)束時(shí)間
    • 風(fēng)控狀態(tài):風(fēng)控開(kāi)啟/關(guān)閉狀態(tài)控制

    界面設(shè)計(jì)效果:以支付寶官方支付接口類型為例。

    配置定義描述為:

    [{
     "name": "pid",
     "desc": "商戶PID",
     "type": "text",
     "verify": "required"
    }, {
     "name": "appId",
     "desc": "應(yīng)用App ID",
     "type": "text",
     "verify": "required"
    }, {
     "name": "alipayAccount",
     "desc": "支付寶賬戶",
     "type": "text",
     "verify": "required"
    }, {
     "name": "privateKey",
     "desc": "應(yīng)用私鑰",
     "type": "textarea",
     "verify": "required"
    }, {
     "name": "alipayPublicKey",
     "desc": "支付寶公鑰",
     "type": "textarea"
    }, {
     "name": "reqUrl",
     "desc": "網(wǎng)關(guān)地址",
     "type": "text",
     "verify": "required"
    }] 
    

    生成的界面為:

支護(hù)寶官方支付賬戶配置.png

賬戶風(fēng)控配置界面:

賬戶風(fēng)控配置.png

前端自動(dòng)生成配置項(xiàng):關(guān)鍵代碼

admin.req({
            type: 'post',
            url: layui.setter.baseUrl + '/config/pay_passage/pay_config_get',
            data: {
                payPassageId: payPassageId
            },
            error: function(err){
                layer.alert(err);
            },
            success: function(res){
                if(res.code == 0){
                    $("#ifTypeNameSpan").html(res.data.ifTypeName);
                    var jsonObj = JSON.parse(res.data.param);
                    // 根據(jù)paramVal填充表單值
                    var htm = '';
                    $.each(jsonObj, function(i, obj){
                        htm += `
                                        <div class="layui-form-item">
                                            <label class="layui-form-label"> ` + obj.desc + ` [` + obj.name + `]` +`</label>
                                            <div class="layui-input-block"> `;
                        if(obj.type == 'text') {
                            htm += ` <input type="text" name="` + obj.name + `" lay-verify="` + obj.verify + `" placeholder="請(qǐng)輸入` + obj.desc + `" autocomplete="off" class="layui-input">`;
                        }else if(obj.type == 'textarea') {
                            htm += ` <textarea required name="` + obj.name + `" lay-verify="` + obj.verify + `" placeholder="請(qǐng)輸入` + obj.desc + `" class="layui-textarea"></textarea>`;
                        }
                        htm += ` </div>
                                        </div>
                                    </form>`;
                    });
                    htm += ``;
                    $('#paramInfo').html(htm);
                }else{
                    layer.alert(res.msg,{title:"請(qǐng)求失敗"})
                }
            }
        })

        form.render();

數(shù)據(jù)庫(kù)設(shè)計(jì):t_pay_passage_account

字段 類型 長(zhǎng)度 注釋
id int 11 賬戶ID
AccountName varchar 30 賬戶名稱
PayPassageId int 11 支付通道ID
IfCode varchar 30 接口代碼
IfTypeCode varchar 30 接口類型代碼
Param varchar 4096 賬戶配置參數(shù),json字符串
Status tinyint 2 賬戶狀態(tài),0-停止,1-開(kāi)啟
PassageMchId varchar 64 通道商戶ID
RiskMode tinyint 2 風(fēng)控模式,1-繼承,2-自定義
PassageRate decimal 20 通道費(fèi)率百分比
MaxDayAmount bigint 20 當(dāng)天交易金額,單位分
MaxEveryAmount bigint 20 單筆最大金額,單位分
MinEveryAmount bigint 20 單筆最小金額,單位分
TradeStartTime varchar 20 交易開(kāi)始時(shí)間
TradeEndTime varchar 20 交易結(jié)束時(shí)間
RiskStatus tinyint 6 風(fēng)控狀態(tài),0-關(guān)閉,1-開(kāi)啟
CashCollStatus tinyint 2 資金歸集開(kāi)關(guān),0-關(guān)閉,1-開(kāi)啟
CashCollMode tinyint 2 資金歸集配置,1-繼承全局配置,2-自定義
Remark varchar 128 備注
CreateTime timestamp 0 創(chuàng)建時(shí)間
UpdateTime timestamp 0 更新時(shí)間

三,支付渠道服務(wù)開(kāi)發(fā)設(shè)計(jì)

  1. 支付流程說(shuō)明:
聚合支付統(tǒng)一下單-異步通知-查詢.png

統(tǒng)一下單:用戶向商戶系統(tǒng)發(fā)起支付請(qǐng)求,商戶系統(tǒng)調(diào)用聚合支付統(tǒng)一下單接口,經(jīng)過(guò)參數(shù)校驗(yàn),創(chuàng)建訂單,調(diào)用第三方支付接口完成下單操作,并且由第三方支付系統(tǒng)返回支付連接/支付表單參數(shù)/二維碼等支付信息,到商戶系統(tǒng),商戶系統(tǒng)根據(jù)返回?cái)?shù)據(jù),在客戶端執(zhí)行相應(yīng)動(dòng)作,如喚起客戶端/打開(kāi)支付頁(yè)面等。用戶根據(jù)支付界面完成支付。

異步通知:用戶支付完成后,第三方支付系統(tǒng)會(huì)根據(jù)下單接口中的回調(diào)地址,回調(diào)聚合支付系統(tǒng),推送支付結(jié)果,聚合支付系統(tǒng)根據(jù)支付結(jié)果更新訂單狀態(tài),并回調(diào)商戶系統(tǒng),通知商戶訂單支付狀態(tài)。

訂單查詢:有些第三方支付系統(tǒng),不支持回調(diào),聚合支付系統(tǒng)則根據(jù)提供的查詢接口,開(kāi)啟定時(shí)任務(wù)查詢。有結(jié)果反饋,則更新訂單支付狀態(tài),并通知商戶系統(tǒng)。商戶系統(tǒng)也可通過(guò)聚合支付系統(tǒng)提供的查詢接口,查詢訂單支付狀態(tài)。

  1. 支付渠道服務(wù)開(kāi)發(fā)設(shè)計(jì)

    思路(簡(jiǎn)單但實(shí)用):定義支付渠道服務(wù)接口(PaymentInterface)及相關(guān)方法,結(jié)合支付渠道服務(wù)接口實(shí)現(xiàn)類編碼規(guī)則({支付接口類型代碼}PaymentService),開(kāi)發(fā)具體支付渠道服務(wù),并交由Spring 容器管理。接口調(diào)用時(shí),則通過(guò)預(yù)先約定的服務(wù)渠道支付接口類型代碼,動(dòng)態(tài)組裝服務(wù)類名稱,并根據(jù)名稱在Spring容器中查找對(duì)應(yīng)的實(shí)現(xiàn)類。

    PayOrderController.png

以阿里支付渠道接口接入為例:

  1. 創(chuàng)建支付渠道接口服務(wù)實(shí)現(xiàn)類名稱約定格式為:{支付接口類型代碼}PaymentService,且必須繼承BasePayment。如:AlipayPaymentService

  2. 重寫(xiě)getChannelName抽象方法,返回具體渠道接口類型代碼,如

        @Override
        public String getChannelName() {
            return PayConstant.CHANNEL_NAME_ALIPAY;
        }
        
       String CHANNEL_NAME_ALIPAY = "alipay";                 // 渠道名稱:支付寶
    
  3. 定義配置類,如:AlipayConfig,這里字段取值來(lái)自接口所屬通道賬戶配置

        private String pid;             // 合作伙伴身份partner
        private String appId;           // 應(yīng)用App ID
        private String privateKey;      // 應(yīng)用私鑰
        private String alipayPublicKey; // 支付寶公鑰
        private String alipayAccount;   // 支付寶賬號(hào)
        private String reqUrl;          // 請(qǐng)求網(wǎng)關(guān)地址
        // RSA2
        public static String SIGNTYPE = "RSA2";
        // 編碼
        public static String CHARSET = "UTF-8";
        // 返回格式
        public static String FORMAT = "json";
        //令牌地址
        public static String toAuth = "https://openauth.alipay.com/oauth2/publicAppAuthorize.htm";
        
        public AlipayConfig(){}
    
        public AlipayConfig(String payParam) {
            Assert.notNull(payParam, "init alipay config error");
            JSONObject object = JSON.parseObject(payParam);
            this.pid = object.getString("pid");
            this.appId = object.getString("appId");
            this.privateKey = object.getString("privateKey");
            this.alipayPublicKey = object.getString("alipayPublicKey");
            this.alipayAccount = object.getString("alipayAccount");
            this.reqUrl = object.getString("reqUrl");
        }
       //初始化配置
       AlipayConfig alipayConfig = new AlipayConfig(getPayParam(payOrder));
    
       /**
         * 獲取三方支付配置信息
         */
        public String getPayParam(PayOrder payOrder) {
            String payParam = "";
            PayPassageAccount payPassageAccount = rpcCommonService.rpcPayPassageAccountService.findById(payOrder.getPassageAccountId());
            if(payPassageAccount != null && payPassageAccount.getStatus() == MchConstant.PUB_YES) {
                payParam = payPassageAccount.getParam();
            }
            if(StringUtils.isBlank(payParam)) {
                throw new ServiceException(RetEnum.RET_MGR_PAY_PASSAGE_ACCOUNT_NOT_EXIST);
            }
            return payParam;
        }
    
    
  4. 定位渠道接口服務(wù):在統(tǒng)一下單接口方法中,根據(jù)訂單包含的渠道ID,按照約定查找服務(wù)實(shí)現(xiàn)類

                String channelId = payOrder.getChannelId();
                String channelName = channelId.substring(0, channelId.indexOf("_"));
                try {
                    paymentInterface = (PaymentInterface) SpringUtil.getBean(channelName.toLowerCase() +  "PaymentService");
                }catch (BeansException e) {
                    _log.error(e, "支付渠道類型[channelId="+channelId+"]實(shí)例化異常");
                   ...
                }
    

四,實(shí)戰(zhàn)(支付寶接口接入)

  1. 支付寶接口文檔

    當(dāng)面付:https://opendocs.alipay.com/apis/api_1/alipay.trade.precreate

  2. 創(chuàng)建支付渠道配置參數(shù)類:AlipayConfig

    @Component
    public class AlipayConfig extends BasePayConfig {
        private String pid;             // 合作伙伴身份partner
        private String appId;           // 應(yīng)用App ID
        private String privateKey;      // 應(yīng)用私鑰
        private String alipayPublicKey; // 支付寶公鑰
        private String alipayAccount;   // 支付寶賬號(hào)
        private String reqUrl;          // 請(qǐng)求網(wǎng)關(guān)地址
        // RSA2
        public static String SIGNTYPE = "RSA2";
        // 編碼
        public static String CHARSET = "UTF-8";
        // 返回格式
        public static String FORMAT = "json";
        //令牌地址
        public static String toAuth = "https://openauth.alipay.com/oauth2/publicAppAuthorize.htm";
        
        public AlipayConfig(){}
    
        public AlipayConfig(String payParam) {
            Assert.notNull(payParam, "init alipay config error");
            JSONObject object = JSON.parseObject(payParam);
            this.pid = object.getString("pid");
            this.appId = object.getString("appId");
            this.privateKey = object.getString("privateKey");
            this.alipayPublicKey = object.getString("alipayPublicKey");
            this.alipayAccount = object.getString("alipayAccount");
            //this.sellerId = object.getString("sellerId");
            //this.callback = object.getString("callback");
            this.reqUrl = object.getString("reqUrl");
            this.certPath = object.getString("certPath");
            this.alipayPublicCertPath = object.getString("alipayPublicCertPath");
            this.rootCertPath = object.getString("rootCertPath");
        }
    
      //geteer/setter
    }
    
  3. 創(chuàng)建支付接口服務(wù)類:AlipayPaymentService

    @Service
    public class AlipayPaymentService extends BasePayment {
    
        private static final MyLog _log = MyLog.getLog(AlipayPaymentService.class);
        public final static String PAY_CHANNEL_ALIPAY_QR_H5 = "alipay_qr_h5";             // 支付寶當(dāng)面付之H5支付
        public final static String PAY_CHANNEL_ALIPAY_QR_PC = "alipay_qr_pc";             // 支付寶當(dāng)面付之PC支付
    
        @Override
        public String getChannelName() {
            return PayConstant.CHANNEL_NAME_ALIPAY;
        }
    
        @Override
        public JSONObject pay(PayOrder payOrder) {
            String channelId = payOrder.getChannelId();
            JSONObject retObj;
            switch (channelId) {
                case PAY_CHANNEL_ALIPAY_QR_H5 :
                    retObj = doAliPayQrH5Req(payOrder,"wap");
                    break;
                case PAY_CHANNEL_ALIPAY_QR_PC :
                    retObj = doAliPayQrPcReq(payOrder,"pc");
                    break;
                default:
                    retObj = buildRetObj(PayConstant.RETURN_VALUE_FAIL, "不支持的支付寶渠道[channelId="+channelId+"]");
                    break;
            }
            return retObj;
        }
        /**
         * 支付寶當(dāng)面付(H5)支付
         * 收銀員通過(guò)收銀臺(tái)或商戶后臺(tái)調(diào)用支付寶接口,可直接打開(kāi)支付寶app付款。
         * @param payOrder
         * @return
         */
        public JSONObject doAliPayQrH5Req(PayOrder payOrder, String type) {
            String logPrefix = "【支付寶當(dāng)面付之H5支付下單】";
            String payOrderId = payOrder.getPayOrderId();
            AlipayConfig alipayConfig = new AlipayConfig(getPayParam(payOrder));
            AlipayClient client = new DefaultAlipayClient(alipayConfig.getReqUrl(), alipayConfig.getAppId(), alipayConfig.getPrivateKey(), AlipayConfig.FORMAT, AlipayConfig.CHARSET, alipayConfig.getAlipayPublicKey(), AlipayConfig.SIGNTYPE);
            AlipayTradePrecreateRequest alipay_request = new AlipayTradePrecreateRequest();
            // 封裝請(qǐng)求支付信息
            AlipayTradePrecreateModel model=new AlipayTradePrecreateModel();
            model.setOutTradeNo(payOrderId);
            model.setSubject(payOrder.getSubject());
            model.setTotalAmount(AmountUtil.convertCent2Dollar(payOrder.getAmount().toString()));
            model.setBody(payOrder.getBody());
            // 獲取objParams參數(shù)
            String objParams = payOrder.getExtra();
            if (StringUtils.isNotEmpty(objParams)) {
                try {
                    JSONObject objParamsJson = JSON.parseObject(objParams);
                    if(StringUtils.isNotBlank(objParamsJson.getString("discountable_amount"))) {
                        //可打折金額
                        model.setDiscountableAmount(objParamsJson.getString("discountable_amount"));
                    }
                    if(StringUtils.isNotBlank(objParamsJson.getString("undiscountable_amount"))) {
                        //不可打折金額
                        model.setUndiscountableAmount(objParamsJson.getString("undiscountable_amount"));
                    }
                } catch (Exception e) {
                    _log.error("{}objParams參數(shù)格式錯(cuò)誤!", logPrefix);
                }
            }
            alipay_request.setBizModel(model);
            // 設(shè)置異步通知地址
            alipay_request.setNotifyUrl(alipayConfig.transformUrl(payConfig.getNotifyUrl(getChannelName())));
            // 設(shè)置同步跳轉(zhuǎn)地址
            alipay_request.setReturnUrl(alipayConfig.transformUrl(payConfig.getReturnUrl(getChannelName())));
            String aliResult;
            String codeUrl = "";
            JSONObject retObj = buildRetObj();
            try {
                aliResult = client.execute(alipay_request).getBody();
                JSONObject aliObj = JSONObject.parseObject(aliResult);
                JSONObject aliResObj = aliObj.getJSONObject("alipay_trade_precreate_response");
                codeUrl = aliResObj.getString("qr_code");
            } catch (AlipayApiException e) {
                _log.error(e, "");
                retObj.put("errDes", "下單失敗[" + e.getErrMsg() + "]");
                retObj.put(PayConstant.RETURN_PARAM_RETCODE, PayConstant.RETURN_VALUE_FAIL);
                return retObj;
            } catch (Exception e) {
                _log.error(e, "");
                retObj.put("errDes", "下單失敗[調(diào)取通道異常]");
                retObj.put(PayConstant.RETURN_PARAM_RETCODE, PayConstant.RETURN_VALUE_FAIL);
                return retObj;
            }
            _log.info("{}生成支付寶二維碼:codeUrl={}", logPrefix, codeUrl);
            rpcCommonService.rpcPayOrderService.updateStatus4Ing(payOrderId, null);
    
            String codeImgUrl = payConfig.getPayUrl() + "/qrcode_img_get?url=" + codeUrl + "&widht=200&height=200";
            StringBuffer payForm = new StringBuffer();
            String toPayUrl = payConfig.getPayUrl() + "/alipay/pay_"+type+".htm";
            payForm.append("<form style=\"display: none\" action=\""+toPayUrl+"\" method=\"post\">");
            payForm.append("<input name=\"mchOrderNo\" value=\""+payOrder.getMchOrderNo()+"\" >");
            payForm.append("<input name=\"payOrderId\" value=\""+payOrder.getPayOrderId()+"\" >");
            payForm.append("<input name=\"amount\" value=\""+payOrder.getAmount()+"\" >");
            payForm.append("<input name=\"codeUrl\" value=\""+codeUrl+"\" >");
            payForm.append("<input name=\"codeImgUrl\" value=\""+codeImgUrl+"\" >");
            payForm.append("<input type=\"submit\" value=\"立即支付\" style=\"display:none\" >");
            payForm.append("</form>");
            payForm.append("<script>document.forms[0].submit();</script>");
    
            retObj.put("payOrderId", payOrderId);
            JSONObject payInfo = new JSONObject();
            payInfo.put("payUrl",payForm);
            payInfo.put("payMethod",PayConstant.PAY_METHOD_FORM_JUMP);
            retObj.put("payParams", payInfo);
    
            _log.info("###### 商戶統(tǒng)一下單處理完成 ######");
            return retObj;
        }
        /**
         * 支付寶當(dāng)面付(PC)支付
         * 收銀員通過(guò)收銀臺(tái)或商戶后臺(tái)調(diào)用支付寶接口,生成二維碼后,展示給用戶,由用戶掃描二維碼完成訂單支付。
         * @param payOrder
         * @return
         */
        public JSONObject doAliPayQrPcReq(PayOrder payOrder, String type) {
            String logPrefix = "【支付寶當(dāng)面付之PC支付下單】";
            String payOrderId = payOrder.getPayOrderId();
            AlipayConfig alipayConfig = new AlipayConfig(getPayParam(payOrder));
            AlipayClient client = new DefaultAlipayClient(alipayConfig.getReqUrl(), alipayConfig.getAppId(), alipayConfig.getPrivateKey(), AlipayConfig.FORMAT, AlipayConfig.CHARSET, alipayConfig.getAlipayPublicKey(), AlipayConfig.SIGNTYPE);
            AlipayTradePrecreateRequest alipay_request = new AlipayTradePrecreateRequest();
            // 封裝請(qǐng)求支付信息
            AlipayTradePrecreateModel model=new AlipayTradePrecreateModel();
            model.setOutTradeNo(payOrderId);
            model.setSubject(payOrder.getSubject());
            model.setTotalAmount(AmountUtil.convertCent2Dollar(payOrder.getAmount().toString()));
            model.setBody(payOrder.getBody());
            // 獲取objParams參數(shù)
            String objParams = payOrder.getExtra();
            if (StringUtils.isNotEmpty(objParams)) {
                try {
                    JSONObject objParamsJson = JSON.parseObject(objParams);
                    if(StringUtils.isNotBlank(objParamsJson.getString("discountable_amount"))) {
                        //可打折金額
                        model.setDiscountableAmount(objParamsJson.getString("discountable_amount"));
                    }
                    if(StringUtils.isNotBlank(objParamsJson.getString("undiscountable_amount"))) {
                        //不可打折金額
                        model.setUndiscountableAmount(objParamsJson.getString("undiscountable_amount"));
                    }
                } catch (Exception e) {
                    _log.error("{}objParams參數(shù)格式錯(cuò)誤!", logPrefix);
                }
            }
            alipay_request.setBizModel(model);
            // 設(shè)置異步通知地址
            alipay_request.setNotifyUrl(alipayConfig.transformUrl(payConfig.getNotifyUrl(getChannelName())));
            // 設(shè)置同步跳轉(zhuǎn)地址
            alipay_request.setReturnUrl(alipayConfig.transformUrl(payConfig.getReturnUrl(getChannelName())));
            String aliResult;
            String codeUrl = "";
            JSONObject retObj = buildRetObj();
            try {
                aliResult = client.execute(alipay_request).getBody();
                JSONObject aliObj = JSONObject.parseObject(aliResult);
                JSONObject aliResObj = aliObj.getJSONObject("alipay_trade_precreate_response");
                codeUrl = aliResObj.getString("qr_code");
            } catch (AlipayApiException e) {
                _log.error(e, "");
                retObj.put("errDes", "下單失敗[" + e.getErrMsg() + "]");
                retObj.put(PayConstant.RETURN_PARAM_RETCODE, PayConstant.RETURN_VALUE_FAIL);
                return retObj;
            } catch (Exception e) {
                _log.error(e, "");
                retObj.put("errDes", "下單失敗[調(diào)取通道異常]");
                retObj.put(PayConstant.RETURN_PARAM_RETCODE, PayConstant.RETURN_VALUE_FAIL);
                return retObj;
            }
            _log.info("{}生成支付寶二維碼:codeUrl={}", logPrefix, codeUrl);
            rpcCommonService.rpcPayOrderService.updateStatus4Ing(payOrderId, null);
    
            String codeImgUrl = payConfig.getPayUrl() + "/qrcode_img_get?url=" + codeUrl + "&widht=200&height=200";
            StringBuffer payForm = new StringBuffer();
            String toPayUrl = payConfig.getPayUrl() + "/alipay/pay_"+type+".htm";
            payForm.append("<form style=\"display: none\" action=\""+toPayUrl+"\" method=\"post\">");
            payForm.append("<input name=\"mchOrderNo\" value=\""+payOrder.getMchOrderNo()+"\" >");
            payForm.append("<input name=\"payOrderId\" value=\""+payOrder.getPayOrderId()+"\" >");
            payForm.append("<input name=\"amount\" value=\""+payOrder.getAmount()+"\" >");
            payForm.append("<input name=\"codeUrl\" value=\""+codeUrl+"\" >");
            payForm.append("<input name=\"codeImgUrl\" value=\""+codeImgUrl+"\" >");
            payForm.append("<input type=\"submit\" value=\"立即支付\" style=\"display:none\" >");
            payForm.append("</form>");
            payForm.append("<script>document.forms[0].submit();</script>");
    
            retObj.put("payOrderId", payOrderId);
            JSONObject payInfo = new JSONObject();
            payInfo.put("payUrl",payForm);
            payInfo.put("payMethod",PayConstant.PAY_METHOD_FORM_JUMP);
            retObj.put("payParams", payInfo);
            _log.info("###### 商戶統(tǒng)一下單處理完成 ######");
            return retObj;
        }
    
    }
    
    
  4. 支付接口基類:BasePayment

    @Component
    public abstract class BasePayment extends BaseService implements PaymentInterface {
    
        @Autowired
        public RpcCommonService rpcCommonService;
    
        @Autowired
        public PayConfig payConfig;
    
        public abstract String getChannelName();
    
        
        protected JSONObject getJsonParam1(HttpServletRequest request) {
            String params = request.getParameter("params");
            if(StringUtils.isNotBlank(params)) {
                return JSON.parseObject(params);
            }
            // 參數(shù)Map
            Map properties = request.getParameterMap();
            // 返回值Map
            JSONObject returnObject = new JSONObject();
            Iterator entries = properties.entrySet().iterator();
            Map.Entry entry;
            String name;
            String value = "";
            while (entries.hasNext()) {
                entry = (Map.Entry) entries.next();
                name = (String) entry.getKey();
                Object valueObj = entry.getValue();
                if(null == valueObj){
                    value = "";
                }else if(valueObj instanceof String[]){
                    String[] values = (String[])valueObj;
                    for(int i=0;i<values.length;i++){
                        value = values[i] + ",";
                    }
                    value = value.substring(0, value.length()-1);
                }else{
                    value = valueObj.toString();
                }
                returnObject.put(name, value);
            }
            return returnObject;
        }
    
        /**
         * 獲取三方支付配置信息
         * 如果是平臺(tái)賬戶,則使用平臺(tái)對(duì)應(yīng)的配置,否則使用商戶自己配置的渠道
         * @param payOrder
         * @return
         */
        public String getPayParam(PayOrder payOrder) {
            String payParam = "";
            PayPassageAccount payPassageAccount = rpcCommonService.rpcPayPassageAccountService.findById(payOrder.getPassageAccountId());
            if(payPassageAccount != null && payPassageAccount.getStatus() == MchConstant.PUB_YES) {
                payParam = payPassageAccount.getParam();
            }
            if(StringUtils.isBlank(payParam)) {
                throw new ServiceException(RetEnum.RET_MGR_PAY_PASSAGE_ACCOUNT_NOT_EXIST);
            }
            return payParam;
        }
    
    }
    
  5. 支付測(cè)試及效果

    模擬下單:

模擬.png

支付掃碼:


掃碼支付.jpg

5,總結(jié)

通過(guò)約定支付渠道接入前端配置規(guī)范以及后端服務(wù)開(kāi)發(fā)規(guī)范,讓后續(xù)支付渠道的接入有章可循,有法可依,不僅規(guī)范了開(kāi)發(fā),也降低了渠道接入開(kāi)發(fā)的難度,提高了開(kāi)發(fā)效率,最終實(shí)現(xiàn)了任意支付渠道的靈活接入。不過(guò)也存在一些不足,比如:

  1. 常用工具方法沒(méi)有統(tǒng)一,如加解密,遠(yuǎn)程方法調(diào)用,唯一序列生成,分布式鎖,事件處理等,后續(xù)可結(jié)合SpringBoot Starter機(jī)制開(kāi)發(fā)公共工具組件。
  2. 所有渠道的接入實(shí)現(xiàn)都在同一個(gè)工程,任何修改或者新增都要整體打包發(fā)布,給系統(tǒng)帶來(lái)了不穩(wěn)定性,后續(xù)可采用插件試開(kāi)發(fā)方式,并實(shí)現(xiàn)動(dòng)態(tài)加載渠道實(shí)現(xiàn)。
  3. 前后端未實(shí)現(xiàn)分離,無(wú)論是修改前端代碼或者后端代碼,每次都要整體打包發(fā)布,后續(xù)將系統(tǒng)前后端分離,獨(dú)立開(kāi)發(fā)和部署。

6,系統(tǒng)部分截圖

運(yùn)營(yíng)平臺(tái)系統(tǒng):

運(yùn)營(yíng)平臺(tái).png

商戶系統(tǒng):

商戶系統(tǒng).png

代理商系統(tǒng):

代理商系統(tǒng).png
一些信息
路漫漫其修遠(yuǎn)兮,吾將上下而求索
碼云:https://gitee.com/javacoo
QQ:164863067
作者/微信:javacoo
郵箱:xihuady@126.com
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容