轉帳接口設計

在一個項目中,通常都會支付相關的業務,而涉及到支付一定會有轉帳的操做,轉帳這一步想起來算是比較關鍵的部分,這個接口的設計能力,也大體體現出一我的的水平。java

昨天碰到了一個題目:spring

嘗試用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("轉帳金額爲空");
        }
    }

}

複製代碼

最後

歡迎一塊兒討論,上面只是個人一點思路,集思廣益才能你們一塊兒進步。

相關文章
相關標籤/搜索