支付宝成都面试题

题目:编写2个用户之间的转账接口及其内部实现?

要求:完成接口设计、并实现其内部逻辑,以完成A用户转账给B用户的功能。2个用户的账户不在同一个数据库下。注意需要手写代码,尽量不要用伪代码。
提示:接口发布后会暴露给外部应用进行服务调用,请考虑接口规范、安全、幂等、重试、并发、有可能的异常分支事务一致性、用户投诉、资金安全等的处理。

本人技术方案:

如果有更好的方案请在下面留言,一同探讨!
    0.接口规范:入参校验等;
    1.安全:采用加密token保证接口安全(15年发现每日生鲜在调用支付宝付款时可以拦截修改参数,觉得不可思议,后来发现不止每日生鲜,包括很多校园零售App,生鲜App);
    2.幂等:插入转账记录表,使用唯一约束保证幂等;
    3.重试:幂等可以防重;
    4.并发:使用 乐观锁 + 事务;
    5.异常分支:a.安全性校验不通过;
                b.参数校验不通过;
                c.重试不通过;
                d.余额不足;
                f:扣款、付款失败。
    6.事务一致性:在转账记录表中增加状态(status)字段值,1表示扣款成功,2表示付款成功;
                  转账和付款在两个事务,付款操作更新状态为1的记录;如果扣款失败,启动定时任务进行扫描状态为1的记录进行付款;
    7.用户投诉:各阶段异常分支,可以用枚举返回(未实现),用于反馈;
    8.资金安全:先扣款,再付款,保证资金安全;
    9.接口名称:service.TransferAccountsService
    10.接口定义:
        /**
         * 转账
         * @param transferDTO 转账DTO
         * @return success是否成功,message提示信息
         */
        Result transferAccounts(TransferDTO transferDTO);

接口实现如下:


public class TransferAccountsServiceImpl implements TransferAccountsService {

    @Autowired
    private AccountOperationService accountOperationService;

    @Override
    public Result transferAccounts(TransferDTO transferDTO) {
        try{
            //接口安全性校验(identityId为 加密信息)
            Boolean flag = accountOperationService.identityCheck(transferDTO.getIdentityId());
            if (!flag){
                //TODO 发送监控报警
                return new Result(false, "非法请求!");
            }

            //入参校验
            if (StringUtils.isEmpty(transferDTO.getTransactionId()) ||
                    StringUtils.isEmpty(transferDTO.getAmount()) ||
                    StringUtils.isEmpty(transferDTO.getRecAccountId()) ||
                    StringUtils.isEmpty(transferDTO.getPayAccountId())){
                return new Result(false, "参数为空!");
            }

            //减款
            try {
                //幂等性
                Boolean success = accountOperationService.decrease(transferDTO);
                if (success) {
                    return new Result(true, "转账成功!");
                }
            } catch (Exception e) {
               return new Result(false, e.getMessage());
            }

            //付款(根据状态1进行update)
            try{
                accountOperationService.increase(transferDTO);
            } catch (Exception e){
                //如果付款失败:启动一个高频定时任务来扫描状态为1的记录,并进行付款
                return new Result(true, "付款失败!");
            }
        } catch (Exception e) {
            return new Result(false, "转账失败!");
        }
        return new Result(true, "转账成功!");
    }

}

decrease()实现:

 @Override
    @Transactional(rollbackFor = Throwable.class)
    public Boolean decrease(TransferDTO transferDTO) throws Exception{
        //使用transactionId设为唯一约束,用来做幂等性
        try{
            transferMoneyMapper.saveRecord(transferDTO);
        } catch (Exception e){
            return true;//已经发送过
        }

        //查询剩余金额
        AccountDO accountInfo = transferMoneyMapper.getBalance(transferDTO.getPayAccountId());
        if (accountInfo.getAmount() > transferDTO.getAmount()) {
           throw new Exception("余额不足!");
        }

        //扣款(versionId + 1)
        transferMoneyMapper.decrease(transferDTO.getPayAccountId(), transferDTO.getAmount(), accountInfo.getVersionId());

        //扣款成功修改转账记录表状态值为1
        transferMoneyMapper.updateStatus(transferDTO.getTransactionId(), 0, 1);
        return false;
    }

increase()实现:

 @Override
    @Transactional(rollbackFor = Throwable.class)
    public void increase(TransferDTO transferDTO) throws Exception{
        //付款
        transferMoneyMapper.increase(transferDTO.getRecAccountId(), transferDTO.getAmount());

        //更新状态为1的记录
        Integer count = transferMoneyMapper.updateStatus(transferDTO.getTransactionId(), 1, 2);
        if (count != 1){
           throw new Exception("付款失败!");
        }
    }

mapper接口:


public interface TransferMoneyMapper {

    /**
     * 根据AccountId获取账户信息
     * @param payAccountId 账户ID
     * @return 账户实体
     */
    AccountDO getBalance(String payAccountId);

    /**
     * 保存交易信息 其中transactionId设为唯一约束
     * @param transferDTO 实体
     */
    void saveRecord(TransferDTO transferDTO);

    /**
     *   sql: update table set amount = amount - #{amount},version_id =  #{versionId} + 1 where accountId = #{accountId} and version_id = #{versionId}
     * @param payAccountId 账户ID
     * @param amount 金额(分)
     * @param versionId 版本ID
     */
    void decrease(String payAccountId, Long amount, Long versionId);


    /**
     * 根据transactionId更新状态
     *  sql: update table set status = #{newStatus} where transactionId = #{transactionId} and status = #{oldStatus}
     * @param transactionId
     * @param oldStatus 老状态值:1:已收款,2已付款
     * @param newStatus 新状态值:1:已收款,2已付款
     */
    Integer updateStatus(String transactionId, int oldStatus, int newStatus);

    /**
     *  sql: update table set amount = amount + #{amount} where accountId =#{accountId}
     * @param recAccountId  账户ID
     * @param amount 金额(分)
     */
    void increase(String recAccountId, Long amount);
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,589评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,615评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,933评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,976评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,999评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,775评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,474评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,359评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,854评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,007评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,146评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,826评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,484评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,029评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,153评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,420评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,107评论 2 356

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,195评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,672评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,645评论 18 399
  • 悲悯破晓 锦瑟曌星空 萤火依稀清洗满城春雨 落枫沿川泅渡不堪湍急 窗棂情针细绣蝴蝶霓裳 煽动翅膀一路跌宕 ...
    青春的眼泪阅读 271评论 0 3
  • 秦人喜食面食,除了五花八门的面条,馒头是另外一个枝蔓丛生的分支。 馒头,在我的家乡被称作“馍”,这...
    蔡立鹏阅读 1,173评论 0 2