复式账怎么设计:借方、贷方和一张能对账的凭证
从科目边界、分录平衡到多币种、幂等与冲正,整理一套可以落到业务系统里的复式账设计方法。
“借方”和“贷方”容易被误解成收入和支出的两个按钮。实际上,它们是分录的两个方向;方向的意义要结合科目类型、账户所有者和具体业务规则来确定。
这篇不试图替代会计准则或企业的科目制度,而是从产品和工程角度整理一个可落地的账本边界:什么事件可以入账、分录怎样生成、怎样保持可重算,以及出现未知结果时如何停下来。
借方和贷方不是正负号
对常见科目,可以先记住“增加方向”这张小表:
| 科目类型 | 借方增加 | 贷方增加 | 常见例子 |
|---|---|---|---|
| 资产 | 是 | 否 | 银行存款、已确认的应收款 |
| 负债 | 否 | 是 | 商户应付款、客户预存义务 |
| 权益 | 否 | 是 | 所有者投入、留存结果 |
| 收入 | 否 | 是 | 符合确认条件的服务费收入 |
| 费用 | 是 | 否 | 通道费、运营费用 |
这张表描述的是通常的余额方向,不是“钱从左边流到右边”的流程图。平台收到客户资金时,资产可能在借方增加,同时对客户的负债在贷方增加;平台向商户付款时,资产在贷方减少,商户应付款在借方减少。
因此,业务人员不应只问“这笔钱是收入还是支出”,而应先问:谁控制这笔资金、谁对谁负有义务、这次事件改变了哪几个账户。
先定义科目和账户
科目是账本里的分类,明细账户是某个主体在该分类下的记录单位,余额是分录汇总的结果。科目、账户和余额各有职责。
一个支付系统至少要区分平台自有资金、客户资金、商户应付款、通道清算、手续费收入、退款应付和待核对账户。具体名称要随业务主体和会计政策确定,下面的名称只是系统建模示意。
我会给每个账户保留稳定身份和边界:
- accountId:不能因为展示名称改变而变化。
- ownerType / ownerId:说明是平台、客户、商户还是外部清算方。
- accountType:说明资产、负债、收入、费用等类型。
- currency:余额的原币种,不能依赖页面当前选择。
- status:正常、冻结、关闭或待核对。
- externalRef:外部账户或结算引用,允许为空但不能伪造。
账户本身不应直接接收任意页面请求来修改余额。业务服务产生事件,账本服务根据已版本化的映射生成凭证,再把分录写入不可变的流水。余额可以缓存,但缓存不是事实来源。
表名只是工程示意:
| 记录 | 主要字段 | 关键约束 |
|---|---|---|
| business_event | 来源、业务事件引用、阶段、业务时间、证据摘要 | 重复接收保留记录;原始证据不可覆盖 |
| journal | 凭证号、账簿、事件引用、规则版本、过账时间 | 同一应入账业务阶段只提交一次 |
| entry | 凭证号、行号、账户、借贷方向、金额、币种 | 行号在凭证内唯一;正数金额精确存储 |
| ledger_account | 主体、科目、币种、正常余额方向 | 主体与账簿边界明确,状态变化留痕 |
| balance_projection | 账户、累计借贷、处理到的分录序号 | 可从分录重建;读者知道投影截止点 |
在同一受控数据库中,凭证、全部分录、幂等记录,以及用于资金决策的余额或占用变化应按事务一起提交。跨服务通知可用事务发件箱可靠传递,消费方继续幂等;远程通道无法加入本地数据库事务,要通过状态机、查询和对账收敛。
一张凭证怎样保持平衡
凭证是一个业务阶段在账本中的一次确认。一个订单可以产生收款、扣费、结算等多张凭证;每张应有唯一凭证号、事件类型、业务时间、记账时间、来源、关联订单和一组分录。每一条分录至少包含账户、借贷方向、金额、币种和精度。
本例选择每币种独立平衡的业务账本:同一凭证、账簿、主体和币种内,Σ借方金额 = Σ贷方金额;单币种资产负债关系为“资产 = 负债 + 所有者权益”,本期收入减费用形成利润并按政策结转。借方正常账户的余额 = 期初余额 + 本期借方 − 本期贷方;贷方正常账户反过来计算。
简化的入账检查可以写成:
- 分录数量至少为两条,且借方和贷方都存在。
- 同一凭证按账簿、主体与币种分组,借方合计等于贷方合计;跨主体走各自的往来科目。
- 金额大于零,币种有效,账户状态允许记账。
- 业务事件阶段的幂等键、凭证号和凭证内分录行号满足各自唯一约束。
- 所有检查通过后再一次性提交,失败则整张凭证不生效。
例如收款 1,000 CNY 和收取 30 CNY 手续费,可以有不同的凭证拆法。关键不是把所有业务硬塞成两行,而是让每张凭证的业务含义清晰、金额在规定范围内平衡,并能通过关联引用还原完整过程。
借贷平衡检查应在提交入口强制执行,不能只交给页面或每天跑一次的报表。不要为了补齐差额随意使用“其他收入”或“调整科目”。差额进入待核对流程,有凭据和权限后再生成调整凭证;单纯在两边填相同金额不能证明业务正确。
多币种、精度和汇率
多币种会让“借贷相等”多一层范围问题。USD 100 和 EUR 92 不能直接相加,也不能只因为按某个汇率估值相同就说原币账平衡。
设计时至少要分开保存:
| 层次 | 需要保留什么 |
|---|---|
| 交易层 | 原币金额、币种、订单和外部引用 |
| 账本层 | 每个币种的分录、账户和余额 |
| 估值层 | 本位币、汇率来源、时间、精度和舍入 |
| 费用层 | 费用币种、承担方和对应的原始事件 |
换汇可以有美元侧和欧元侧的关联分录,也可以通过桥接或清算科目表达在途状态。两种方式都要说明哪一侧已经确认、哪一侧仍未知,以及汇率和费用如何落位。
例如只说明业务账户内的兑换重分类,假设负债账户里客户有 100 USD,报价为换得 92 EUR,暂不计费用:美元凭证借记“客户 USD 应付款”100 USD、贷记“USD 换汇桥接”100 USD;欧元凭证借记“EUR 换汇桥接”92 EUR、贷记“客户 EUR 应付款”92 EUR。每币种各自平衡,两张凭证关联同一兑换号。桥接科目代表平台承担的币种头寸或在途关系,需要与真实头寸、外部交割和费用继续核对,不能当作凭空产生的资产。
法定会计或本位币报表另按适用政策确认汇兑损益与舍入差额,不能直接用这个业务子账例子替代财务总账。
金额不要用二进制浮点累加。采用精确十进制或按币种定义的最小单位,并把每次舍入记录在可复现的规则里。展示用的估值不应覆盖原币交易金额。
幂等、期间和冲正
账本服务通常会收到重试、重复回调和乱序事件。幂等键应覆盖账簿、来源、稳定业务事件引用和入账阶段;部分请款或部分退款还需要独立的子事件引用。消息投递编号可能每次重试都变,不能拿它代替业务引用;规则版本作为历史依据保存,不能换个版本就把同一事件重新入账。
数据库唯一约束与受控状态迁移共同抵抗并发;“先查没有、再插入”本身不够。同键同载荷返回已提交结果,同键不同金额或币种拒绝并记录冲突。借贷同时重复一整张凭证仍然平衡,只有这些约束和对账才能补上这个漏洞。
一条已经过账的凭证不应被原地编辑。发现错误时,先保留原凭证,再按原业务规则生成冲正或调整凭证,并关联原凭证号。部分退款、部分请款和手续费修正也应保留累计关系和上限。
记账时间和业务发生时间可能不同。期间关闭后不能静默改写历史;跨期修正应进入当前允许的期间,并留下原因、授权人和关联凭证。查询余额时,明确是实时未结账余额、已过账余额还是某个截止时间的余额。
超时或外部结果未知时保留待核对事件。已有充分证据的银行到账但归属不明,可以按已批准规则借记银行、贷记暂收或待核对科目;连资金是否发生都不确定时,只保存事件和占用状态,不能凭空做一张最终资金凭证。已确认的早期分录保留,之后用新证据推进到下一阶段。
前提与验收效果
做好复式账前,至少要有这些前提:
- 明确资金主体、账户所有权、币种和本位币范围。
- 规定哪些业务状态可以生成凭证,授权、请款、清算、到账和退款分别如何落账。
- 有可版本化的科目映射、费用规则、汇率和舍入规则。
- 有稳定事件标识、幂等约束、权限和期间关闭机制。
- 有外部对账资料、异常处理人和可查询的审计记录。
从旧系统迁移还要有经核对的期初余额:按主体、账户、币种确定统一截止时间,盘点在途、占用和未决交易,生成有来源的平衡期初凭证。不能把所有差额塞入“期初调整”来通过试算。先影子核对,再明确切换时刻与唯一写入方,避免新旧账重复记账。
验收应覆盖:三张示例凭证重算得银行 30 CNY、应付款 0;同一事件重放 100 次只入账一次;可用 100 的资金池并发申请两笔 60,最多一笔占用成功;写入中断时凭证全成或全不成;关闭期间禁止无授权回写;未知结果不会提前增加可用余额。这里列的是待实现账本的验收目标,本站没有运行资金账本服务。
达到效果后,团队应该可以用同一套记录回答四个问题:这笔钱从哪里来、现在属于谁、为什么改变、怎样证明没有漏记或重复记。可执行的验收包括:随机抽一笔业务能还原事件到凭证;重放同一事件不产生新余额变化;按币种和账户重算结果与展示一致;冲正不删除原记录;未决状态不会被报表误当成已结算。
可以先读为什么复式账比单边账更适合业务系统,再用业务事件到最终入账的流程图检查实现顺序。