1、手续费中的概念
1.1 手续费的定义
根据百度百科的定义,“手续费:办事过程中所产生的费用”,“手续费”可以这样解释:为代理他人办理有关事项所收取的一种劳务补偿;或对委托人来讲,是属于因他人代为办理有关事项,而支付的相应报酬。如:证券交易手续费、代办机票手续费、代扣代缴费用手续费、国债代办手续费等等。
根据以上的概念可以得知,在支付系统中,手续费是支付平台向在进行资金转移的过程中,支付系统的使用方向支付平台支付的“报酬”,也可以称为服务费。
1.2 手续费账户
支付系统的用户或者商户将支付的手续费最终支付的账户,称为手续费收益账户。
2、手续费的业务模型设计
2.1 收款手续费
收款过程中收取的手续费,资金流的方向是商户账户到手续费账户。收款是从银行卡充值到支付系统中。
2.2 付款手续费
付款是将钱从支付系统的账户中体现到银行卡的过程。手续费的资金流是从商户账户到手续费账户。
2.3 交易买卖方手续费
一笔交易是从商户a到商户b的支付账户中。这笔交易过程可能对商户a买方和商户b买方收取手续费。手续费的资金流是商户a到手续费账户,商户b到手续费账户。
3、以交易为例手续费资金流说明
本例中买方和卖方都需要支付交易的手续费。商户a支付100元给商户b,需要卖方商户a支付2元手续费,则商户a共支付102元,商户b支付3元交易手续费,则商户b共收到97元。
伪代码如下:
/** 创建交易 */public abstract Response createTransaction(Request request);/** 支付交易 */public abstract Response payTransaction(Request request);/** 确认交易 */public abstract Response confirmTransaction(Request request);
注意手续费金额单位可以是元也可以是分,为了保证减少小数计算对计算准确度的干扰,推荐使用单位分来作为单位。数据库中amount的单位是分。存储100分,记为1元。Java中金额计算必须要用Bigdecimal 类型,但是如果保证系统中不出现小数,可以使用long来表示金额。
4、手续费的四种计算方式
4.1 按单笔固定金额收取费用
单笔固定金额收费,无论交易金额的大小,比如设置每一笔交易的收费为5毛,那么不管交易额为10元,还是100元,收费均为5毛。
4.2 按比例收取费用
按比例收取费用代表每一笔交易固定比例手续费,比如每笔交易收取5%的手续费,100元则收取5元。
4.3 按固定周期收取费用
按固定周期收取费用,比如按年收费,按月收费,按日收费等;比如按年交易额为1000元,固定收取一次手续费1000元。
4.4 按组合收取费用
按组合收取手续费,比如设置1000元以下的交易额时,按5毛一笔收费,大于1000元时,按百分比收费。
5、手续费相关的数据库设计
一笔交易的手续费涉及买卖双方的手续费,我们将手续费信息存储在交易表中。交易表中的手续费信息设计的字段如下:
字段名称 | 类型 | 说明 |
id | varchar | 主键 |
fee_buy_amount | bigint | 买方手续费 |
fee_buy_user_id | bigint | 买方手续费账户 |
fee_buy_account_type | smallint | 买方手续费账户类型 |
fee_sell_amount | bigint | 卖方手续费 |
fee_sell_user_id | bigint | 卖方手续费账户 |
fee_sell_account_type | smallint | 卖方手续费账户类型 |
6、手续费记账代码实现
/**创建交易时将手续费计算出来*/@Overridepublic Response createTransaction(Request request) {//买方账户String buyUserId = request.getUserId();//交易类型:到卡、到账户Integer feeType = request.getFeeType();//对公、对私Integer payType = request.getPayType();//交易金额Long amount = request.getAmount();//交易的单号String transactionId = request.getTransactionId();//获取交易手续费信息FeeInfo payFeeInfo = feeService.getPayFeeInfo(buyUserId, feeType, payType, amount, transactionId);if(!payFeeInfo.getErrno().equals(SUCCESS)){return new Response(ERR_UNKNOWN);}//创建交易单Response trans = payTransService.createTrans(payFeeInfo);if(!trans.getErrno().equals(SUCCESS)){return new Response(ERR_UNKNOWN);}//支付交易单Response payTrans = payTransService.payTrans(payFeeInfo);if(!trans.getErrno().equals(SUCCESS)){return new Response(ERR_UNKNOWN);}return new Response(SUCCESS);}
实现注意的地方:
1) 调用第三方服务超时可以重试,通过框架实现重试(基于guava-retrying实现或者spring-retry),也可以通过业务实现重试,将调用接口通过消息队列实现,只有获取明确的结果报文才停止调用。
2)保证单号的唯一:同一单号在上下游的系统里保证唯一,可以加交易单号设置成唯一索引或者一个中间映射表的主键。
3) 保证服务的幂等性:如果下游没有落单,并发请求可能拿到不同的影响结果,不能保证接口的幂等性。比如交易账户被冻结,并发两笔请求,第一次请求结果返回交易成功,第二次请求返回账户被冻结,刚好覆盖第一次的请求结果。可以通过分布式锁保证交易结果并发情况下拿到唯一的结果。
4)涉及到多张表的修改:同一数据库开启本地事务即可,分布式数据库开启分布式事务,比如TCC。