转帐接口设计

在一个项目中,通常都会支付相关的业务,而涉及到支付一定会有转帐的操做,转帐这一步想起来算是比较关键的部分,这个接口的设计能力,也大体体现出一我的的水平。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("转帐金额为空");
        }
    }

}

复制代码

最后

欢迎一块儿讨论,上面只是个人一点思路,集思广益才能你们一块儿进步。

相关文章
相关标签/搜索