为什么不能复用"应付结算单"
系统里已经有一张"结算单",用于记录我们欠供应商多少(应付)。有人会想:赊销就是反过来,把金额取负号不就行?
不行,因为方向完全不同:
| 维度 | 应付结算单 | 账期应账单 |
|---|---|---|
| 主体 | 供应商(我们欠他) | 下游买方(他欠我们) |
| 生成时机 | 订单完成/发货后 | 按账期周期汇总 |
| 核销方式 | 平台付款 | 买方回款 |
| 对报表影响 | 成本与现金流 | 应收账款与账龄 |
把两者塞进一张表,结果是所有涉及"钱"的报表都要加一个正负号判断,而账龄、逾期这些概念在应付侧没有对应物。
四个概念,四张核心表
赊销的本质是四个问题的分离:
- 还能欠多少 → 信用账户(额度、已用、账期类型、风险等级)
- 这单占了多少 → 额度占用(订单级,可释放)
- 这个周期该收多少 → 账期账单(周期汇总,出账后金额不可变)
- 收了没有 → 回款(待确认 → 核销账单 → 释放额度)
再加两张辅助表:账单明细(订单粒度快照)与额度变更日志(只增不改,供风控审计)。
关键规则一:出账不释放额度
最容易设计错的一条是"什么时候把已用额度还回去"。
直觉做法是"出账后把占用转成账单,占用释放"。但这会造成一个洞:客户 8 月的账单已经出了,额度回到可用状态,他 9 月继续赊——欠着钱还能继续欠。
正确规则是:已用额度 只在两种情况下减少——回款核销与取消/退款释放。出账只是把占用状态从"占用中"改成"已转账单",金额仍然占着。
关键规则二:扣额度必须原子
下单时"先查额度够不够,再更新已用额度"在并发下必然被刷爆。正确做法是一条带条件的原子更新:
```
UPDATE 信用账户
SET credit_used = credit_used + :amt
WHERE id = :id
AND status = 正常
AND credit_limit - credit_used >= :amt
```
影响行数为 0 即表示额度不足,直接拒绝下单。整个过程在一个事务里,同时写入占用记录与变更日志。
关键规则三:幂等键挡住重复出账
两个唯一键承担了幂等:
- 一订单只占一次(
tenant_id + order_biz_no):重复提交下单不会占用两份额度; - 一账户一周期只出一张账单(
tenant_id + account + period_start + period_end):定时任务重跑不会给同一个账期出两张账单。
这两个约束比"代码里判重"可靠得多——判重可能因为并发或异常分支失效,唯一键不会。
逾期用标记,不用状态
账单状态只有五个:待确认、待收款、部分收款、已结清、已作废。
逾期不是状态,而是一个日更标记(is_overdue + overdue_days)。原因是:状态用于表达业务流程走到哪一步,逾期只是时间的函数——一张"部分收款"的账单同样可能逾期。把逾期做成状态,会导致状态机出现"部分收款且逾期"这种无法用单值表达的困境。
每日账实校验
赊销最大的风险是账对不上。所以每天跑一次校验:
- 账户的
已用额度是否等于「未释放的占用 + 未结清的账单」之和; - 账单的
未收金额是否恒等于「应收 − 已收」; - 收入类流水与回款核销是否一一对应。
三条不平就告警,而不是等客户来投诉。
一条边界
这套账期落在业务域(供应链),不进平台资金台账。原因:赊销是租户与买方之间的商业信用,平台不碰这笔钱;一旦写进平台资金账,平台现金流报表会被客户的应收账款污染。
边界清楚,扩展才不打架。