轉賬介面設計

Real_man發表於2019-04-19

在一個專案中,一般都會支付相關的業務,而涉及到支付必定會有轉賬的操作,轉賬這一步想起來算是比較關鍵的部分,這個介面的設計能力,也大致體現出一個人的水平。

昨天碰到了一個題目:

嘗試用java編寫一個轉賬介面,傳入主要業務引數包括轉出賬號,轉入賬號,轉賬金額,完成轉出和轉入賬號的資金處理,該服務要確保在資金處理時轉出賬戶的餘額不會透支,金額計算準確。

設計

  • 首先一般在系統中的引數不會有這麼少,一般情況下請求引數還會有一些公共的資訊,比如請求來源(請求ip與系統)、請求流水號,請求時間,等資訊。閘道器上一般會攔截一些不合法的請求

  • 如果有返回結果,一般包含處理結果,響應時間,處理後的狀態,原始的請求資訊一般也會返回去

  • 看要求是否有強一致性的需求,如果沒有強一致性的需求,是否要及時返回結果。根據需求做出來,如果不用實時返回結果,可以在後端不斷的重試,知道有最終結果,有強一致性的要求,則需要做一些特殊處理,如果沒有保證最終一致性即可。

  • 冪等性設計,一個唯一的請求流水號只能對應一筆支付,防止重複扣款

  • 可能會涉及到一些其它的遠端服務,做一些操作,這裡就需要根據與其它系統協商來處理,當然這個介面的入參也要與會呼叫這個系統的人說下

  • 題目說的,要對餘額做判斷,內部要判斷使用者的資金是否足夠。可以從資料庫層面上,讓使用者的餘額不能小於0。金額計算準確,一般用BigDecimal。

  • 寫程式碼的時候注意一些規範事項

  • 內部注意一些限制條件,賬戶是否合法

程式碼

我的程式碼裡面沒有做冪等性的處理。可能程式碼還有一些其它的問題,如果有沒考慮到的點,歡迎指出

package me.aihe.demo;

import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.math.BigDecimal;
import java.util.HashMap;

/**
 * 嘗試用java編寫一個轉賬介面,傳入主要業務引數包括轉出賬號,
 * 轉入賬號,轉賬金額,完成轉出和轉入賬號的資金處理,
 * 該服務要確保在資金處理時轉出賬戶的餘額不會透支,金額計算準確。
 */
public class FirstProblem {

    /**
     * 假設這個東西是一個遠端服務名稱
     */
    private String checkAmoutisEnoughRemoteService = "一個可以校驗使用者餘額是否足夠的遠端服務";
    /*
    * 題目分析:
    *    定義介面:
    *    入參: 轉出賬號 轉入賬號 轉賬金額
    *    要求:
    *       完成轉出轉入賬號的資金處理
    *       處理時轉出賬戶的餘額不會透支  fromPerson 要判斷餘額是否足夠
    *       金額準確     使用BigDeceimal
    *
    *    疑問:
    *       是否需要返回值?還是隻是一次處理即可
    *
    *
    *    關鍵點:
    *       關鍵操作記得打日誌
    *       如果存在併發情況記得加分散式鎖
    *       其餘的根據需求,是否做一些額外控制,比如限流,回滾,重試
    *       如果遠端呼叫可能存在等待狀態,可以進行重試,儘可能的同步,
    *       如果可以非同步,後臺加定時任務進行非同步資料查詢並更新
    *
    */

    // 我在這裡直接寫介面,就不定義類的名稱了,如果需要也可以定義一下類的名字
    // 因為不是介面,我先把方法留空

    /**
     * 轉賬介面,嘗試用java編寫一個轉賬介面,傳入主要業務引數包括轉出賬號,
     * 轉入賬號,轉賬金額,完成轉出和轉入賬號的資金處理,
     * 該服務要確保在資金處理時轉出賬戶的餘額不會透支,金額計算準確。
     *
     * @author he.ai  2019-04-18 20:16
     *
     * @param sourceAccount 題目中的轉出賬號,也就是從誰哪裡把錢拿出來
     * @param destAccout    題目中的轉入賬號,也就是錢轉給誰
     * @param amout  定義為字串,是想將字串轉為BigDecimal,傳入BigDeceimal也是可以的,可以再做商量
     *
     * 假設這裡需要返回結果的話,一般會用公用的Result類,封裝遠端呼叫的code,結果,已經資料之類的
     */
    void transfer(
            String sourceAccount,
            String destAccout,
            String amout
    ){
        // 假設這裡我們可以獲取到轉出賬號(sourceAccout)的餘額
        // 來源可以為:遠端呼叫服務,直接從資料庫拿,總之要能獲取到當前賬戶的餘額

        // 1. 首先對引數進行校驗,是否合法
        // 如果有異常的話,需定義異常在什麼位置進行處理
        checkParam(sourceAccount, destAccout, amout);
        // 也可以對賬戶是否存在做一次檢驗

        // 2. 校驗轉出賬號的餘額是否足夠,這一步看我們是否有許可權,
        // 如果我們沒有許可權獲取使用者的餘額資訊,需呼叫有許可權的部門進行判斷
        // 我這裡假設的是我們沒有許可權知道使用者的餘額,需要判斷

        // 呼叫遠端的引數,這個入參需要根據與其他系統進行協商
        HashMap<String, String> map = new HashMap<>();
        map.put("account",sourceAccount);
        map.put("amount",amout);
        Result result = callRemoteService(checkAmoutisEnoughRemoteService, map);
        // 對result進行處理
        // 進行判斷使用者餘額是否足夠,我就不寫判斷邏輯了


        // 3. 進行轉賬操作,程式碼能執行到這裡,代表使用者賬戶是ok的,餘額也是ok的
        // 至於什麼風險控制,使用者是否有安全隱患,看需求,以及其它的系統

        // 這裡看要求是否要有一個全域性事務進行控制,如果對資料的一致性要求很高,那麼可以做全域性的事務控制
        // 如果這裡對資料的一致性要求不高,那麼我們可以先進行出來,再補寫定時任務,或者採用非同步通知的方式

        // 甚至可以加鎖
        doTransfer(sourceAccount,destAccout,amout);

        // 到這一步,假設錢已經轉好了,看要求是否要通知其他的業務系統

        sendNotifytoOthers();

    }

    private void sendNotifytoOthers() {

    }

    /**
     * 假設這裡有個全域性事務,本地事務也行
      */
    @Transactional(rollbackFor = Exception.class)
    public void doTransfer(String sourceAccount, String destAccout, String amout) {
        // 其實既然讓我們做了,我們應該是有許可權獲取餘額的
        // 假設我們這裡獲取到了使用者的餘額,當然呼叫其他的系統,真正做也有可能
        // 但是我們還是要有一份備份資料

        // 使用者的餘額,這一步要根據系統要求,看看從哪裡獲取
        BigDecimal sourceLeftMoney = new BigDecimal("1000");
        BigDecimal destLeftMoney = new BigDecimal("200");
        // 再做一個假設,如果我們有許可權,我就直接更新了,上面的校驗也不用呼叫遠端服務了

        // 這裡一般的ORM框架,可以幫我們進行轉換
        // update userDatabase set rest_money = (sourceLeftMoney - amout) where rest_money = sourceLeftMoney and accout = sourceAccount

        // 記得打日誌。
        updatesourceAccount(sourceAccount,amout);

        // 這裡的剩餘金額是轉入賬戶的剩餘金額
        // update userDatabase set rest_money = (destLeftMoney + amout) where rest_money = destLeftMoney and accout = destAccout
        updatedestAccout(destAccout,amout);
    }

    private void updatedestAccout(String destAccout, String amout) {

    }

    private void updatesourceAccount(String sourceAccount, String amout) {

    }

    /**
     * 工具方法,可以直接呼叫遠端服務
     * @param checkAmoutisEnoughRemoteService
     * @param map
     */
    private Result callRemoteService(String checkAmoutisEnoughRemoteService, HashMap<String, String> map) {
        // 遠端服務的處理邏輯
        return null;
    }

    static class Result{

    }

    /**
     * 其實這裡的多個引數可以封裝為一個物件的,就不用專遞這麼多引數
     * @param sourceAccount
     * @param destAccout
     * @param amout
     */
    private void checkParam(String sourceAccount, String destAccout, String amout) {
        if (StringUtils.isEmpty(sourceAccount)){
            // 這個業務異常,專案內一般都有自己專案的業務異常,這裡為了方便就丟擲了執行時異常
            // 至於異常的補貨,根據專案選擇是在當前進行捕獲,或者丟給全域性異常進行捕獲
            // 這裡為了方便,我就不捕獲異常了

            // 如果是關鍵業務,可以嘗試發出報警

            throw new RuntimeException("業務異常" + "轉出賬號為空");
        }

        // 可以根據不同的引數,丟擲不同的異常
        if (StringUtils.isEmpty(destAccout)){
            throw new RuntimeException("業務異常:" + "轉入賬號為空");
        }

        if (StringUtils.isEmpty(amout)){
            throw new RuntimeException("轉賬金額為空");
        }
    }

}

複製程式碼

最後

歡迎一起討論,上面只是我的一點思路,集思廣益才能大家一起進步。

相關文章