题目:编写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);
}
网友评论