对账模型
以系统里的出金交易为例, 与银行对账不外乎做两件事:①T+1日拉取T日的银行账单,保存银行账单交易流水;②银行账单交易流水与本系统里的通道交易流水比对,记录并处理差异。
{对账模型} | ||||||
我方通道交易流水 | ||||||
↑↓ | ||||||
↑↓ | → → | 对账结果 | → → | 差异账 | ||
↑↓ | ||||||
拉取通道对账单 | → → | 通道对账单 |
【FAQ】
【Q】依据支付时间还是支付完成时间对账?
【A】按支付交易完成时间做对账。
【Q】银行单边在怎么产生的?
【A】case1:银行不返回交易完成时间。这种情况下,在日切临界点,我们0:00前查询通道,0:00后收到通道响应,我们按我们服务器时间更新交易完成时间。这就出现我司与通道侧支付完成时间不一致的情况。case2:再一种银行单边由不靠谱程序产生。我司调用通道侧时使用了事务。结果因调用超时通道侧落单,我司没落单,从而出现通道侧单边。
统一数据词典
T日:T日指交易日。银行系统在T+1日生成T日交易的账单,因此,对账发生在T+1日
账单/对账单:bill
银行账单/银行对账单:bank bill batch
对账:bill check
对账批次:bill check batch
明细:detail
差异:diff
定时任务:job。定时任务命名以-Job结尾,如对账Job命名为BankBillCheckJob
所涉及到的系统现有的词汇包括↓
通道/银行:bank
交易:trans
金额:amount,以“分”为单位存储
交易笔数/交易量:qty
数据表设计
数据表 | 表名 | comment | 主要字段 | |
---|---|---|---|---|
银行账单 | 银行账单批次表 | bank_bill_batch | 银行账单表,每银行每天一条记录 | batchNo-账单批次号(系统生成,PK) bankId-系统里记录的银行通道编号 bankBatchNo-varchar-银行侧对账单批次号/对账文件名(没有则为空) trans_date-int-交易日期/yyyyMMdd — total_amount-总金额 total_qty-总笔数 createTIme-记录创建时间,即账单的首次拉取时间 updateTime-最后更新时间
|
银行账单交易流水 | bank_bill_detail | 银行账单交易明细 | id-PK batchNo-账单批次号 bankId-系统里记录的银行通道编号 transOrderNo-系统请求银行的交易流水号(发生银行单边时,此字段为空) bankOrderNo-银行侧交易单号 transState-银行侧交易状态(程序里转换为系统里的交易状态) transAmount-银行侧交易金额,以分为单位存储 payeeAccount-银行侧收款方账号(银行卡号) transTime-银行侧交易完成时间 createTIme-记录创建时间 | |
银行对账 | 银行对账结果批次表 | bank_bill_check_batch | 与bank_bill_batch一对一 | batchNo-批次号(PK,来自bank_bill_batch) bankId-系统里记录的银行通道编号 checkState-对账处理状态 -(I-初始待对账/P-系统对账中/D-差异待处理/S-对账完成) check_date-对账时间 trans_date-交易日期(针对哪天的交易做的对账)
|
银行对账结果明细表 | bank_bill_check_detail | 银行账单与系统交易对账结果 | id-PK transOrderNo-系统请求银行的交易流水号 bankOrderNo-银行侧交易单号 batchNo-批次号(来自bank_bill_batch) bankId-银行编号 双方记录的交易状态、收款人账号、金额、交易完成时间 checkState-对账处理状态 -(I-初始待对账/D-差异待处理/S-对账完成) diff_field-存在差异的字段,STATE-状态 / AMOUNT-金额 / BANKONLY-银行单边 / SYSONLY系统单边 check_time – 对账时间 diff_process_time – 差异处理时间 diff_process_remark – 差异处理备注
| |
银行对账差异处理记录(Optional) | bank_bill_check_diff_process | 记录差异账的处理 | (暂略) |
银行对账结果明细表->数据样例
id | 系统请求银行 的交易流水号 | 银行侧 交易单号 | 账单批次号 /银行编号 | 系统/银行侧 收款人账号 | 系统/银行侧 交易金额 | 系统/银行侧 交易完成时间 | 系统/银行侧 交易状态 | 对账状态 | 差异字段 | 对账时间 | 差异处理时间 |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | 2023120700901 | 231211110575607 | B20231207PAB /PAB | 6217***1069 /6217***1069 | 10000/10000 | 2023-12-07 11:11:11 / 2023-12-07 11:11:11 | S/F | D | STATE | 2023-12-08 01:06:00 | |
2 | 231211110575608 | B20231207PAB /PAB | – /6217***5638 | -/1 |
/- | -/F | S | BANKONLY | 2023-12-08 01:06:00 | 2023-12-08 09:30:00 | |
3 | 2023120700902 | B20231207PAB /PAB | 6217***1069 /- | 1/- | – /- | F/- | D | SYSONLY | 2023-12-08 01:06:00 | ||
4 | 2023120701094 | 231211110575613 | B20231207PAB /PAB | 9558****0631 /9558****0631 | 500/500 | – /- | F/F | S | – | 2023-12-08 01:06:00 | – |
5 | 2023120701950 | SCLY0906231725 | B20231207ALI /PAB | 6228***7074 /6228***7074 | 12380/12380 | 2023-12-07 02:52:01 / 2023-12-07 02:52:01 | S/S | S | – | 2023-12-08 01:06:00 | – |
如何触发对账?
毋庸置疑,实现方案是使用定时任务(JOB)。每家银行出账单的时间是不一样的,例如有的是凌晨3:00生成,有的是上午8:00生成。因此,可以每隔20分钟触发JOB,调用各银行的API获取账单,直到拉取到,然后再进行对账。
拉取银行账单JOB | 银行对账JOB |
---|---|
↓ | ↓ |
获取需要拉取账单的银行通道列表-bankList 依次遍历 bankList | 查询bank_bill_batch及bank_bill_check_batch,获取待对账的账单批次-batchList 依次遍历 batchList |
↓ | ↓ |
拉取银行账单方法 (bank) { if(已经拉取到) return; 加锁,防重复执行控制 组装银行请求参数,拉取银行账单 解析数据,持久化入库,包括银行账单批次表和账单明细表(事务) } | 银行对账方法(){ 加锁,防重复执行控制 更新batch的checkState=P 对该批次里的交易与系统里的T日交易进行check 对账完成后,标记batch的checkState,存在差异交易则为D,否则为S |
实现要点
业务防重复执行锁
key:日期+银行编号
TTL:拉取银行账单 与 银行对账,保证这两者在T+1日均执行一次即可。 因此可设置TTL=24h
对账及时性保证
银行账单是在T+1日生成T日账单。不同银行的对账单的具体生成时间点有所不同,有的是01:00,有的可能是09:00,甚至有的是中午11:00。因此,定时JOB的开始时间可以从00:30开始,每隔半小时触发。银行账单一旦拉取完成,后续JOB触发时不再重复拉取。
上面表格里的方案是两个JOB,即将拉取银行账单JOB与银行对账JOB分开了。 这是有缺点的。——可能出现对账不及时的情况(银行账单都拉取到了,系统却迟迟没有对账)(见后文【花絮】)。
那么,如何优化呢? 保留一个JOB即可。 拉取银行账单的业务完成后,则异步触发银行对账,保证银行对账及时性。
哪些通道需要对账?
最“朴实”的实现方式,不外乎是读取通道表得到bankList,针对这个集合里的每个通道做对账。
注意上面的朴实是加了双引号的。
比较好的姿势,是根据T日的通道交易流水,筛选出来真正需要对账的bankList。
【花絮】
我组的对账起初也是2个JOB(开发人员错误的以为分开为2个JOB才叫解耦),其中,拉取银行账单JOB是整点每隔1小时执行(cron=0 0 1-12/1 * * ?),银行对账JOB是整半点每隔1小时执行(cron=0 30 1-12/1 * * ?)。后来,产品经理和结算人员反馈银行账单拉取不及时,对账也不及时。怎么办?开发人员就反复调整这2个cron表达式,让其触发时间间隔更接近。 例如,变更银行对账JOB的cron=cron=0 15 1-12/1 * * ?。 但这样依然无法根本解决对账不及时的问题。 因此,更合适的实现方案是,拉取到对账单后就异步触发对账,并且缩短JOB执行间隔为半小时。
【EOF】知识就是力量,但更重要的是…。欢迎关注我的微信公众号「靠谱的程序员」,解密靠谱程序员的日常,让我们一起做靠谱的程序员。