复式账怎么设计:借方、贷方和一张能对账的凭证

从科目边界、分录平衡到多币种、幂等与冲正,整理一套可以落到业务系统里的复式账设计方法。

“借方”和“贷方”容易被误解成收入和支出的两个按钮。实际上,它们是分录的两个方向;方向的意义要结合科目类型、账户所有者和具体业务规则来确定。

这篇不试图替代会计准则或企业的科目制度,而是从产品和工程角度整理一个可落地的账本边界:什么事件可以入账、分录怎样生成、怎样保持可重算,以及出现未知结果时如何停下来。

借方和贷方不是正负号

对常见科目,可以先记住“增加方向”这张小表:

科目类型 借方增加 贷方增加 常见例子
资产 是 否 银行存款、已确认的应收款
负债 否 是 商户应付款、客户预存义务
权益 否 是 所有者投入、留存结果
收入 否 是 符合确认条件的服务费收入
费用 是 否 通道费、运营费用

这张表描述的是通常的余额方向,不是“钱从左边流到右边”的流程图。平台收到客户资金时,资产可能在借方增加,同时对客户的负债在贷方增加;平台向商户付款时,资产在贷方减少,商户应付款在借方减少。

因此,业务人员不应只问“这笔钱是收入还是支出”,而应先问:谁控制这笔资金、谁对谁负有义务、这次事件改变了哪几个账户。

先定义科目和账户

科目是账本里的分类,明细账户是某个主体在该分类下的记录单位,余额是分录汇总的结果。科目、账户和余额各有职责。

一个支付系统至少要区分平台自有资金、客户资金、商户应付款、通道清算、手续费收入、退款应付和待核对账户。具体名称要随业务主体和会计政策确定,下面的名称只是系统建模示意。

我会给每个账户保留稳定身份和边界:

  • accountId:不能因为展示名称改变而变化。
  • ownerType / ownerId:说明是平台、客户、商户还是外部清算方。
  • accountType:说明资产、负债、收入、费用等类型。
  • currency:余额的原币种,不能依赖页面当前选择。
  • status:正常、冻结、关闭或待核对。
  • externalRef:外部账户或结算引用,允许为空但不能伪造。

账户本身不应直接接收任意页面请求来修改余额。业务服务产生事件,账本服务根据已版本化的映射生成凭证,再把分录写入不可变的流水。余额可以缓存,但缓存不是事实来源。

业务事件关联凭证,凭证包含至少两条分录,每条分录指向明细账户;余额由已过账分录汇总
数据关系示意,窄屏可左右滑动。 查看原图

表名只是工程示意:

记录 主要字段 关键约束
business_event 来源、业务事件引用、阶段、业务时间、证据摘要 重复接收保留记录;原始证据不可覆盖
journal 凭证号、账簿、事件引用、规则版本、过账时间 同一应入账业务阶段只提交一次
entry 凭证号、行号、账户、借贷方向、金额、币种 行号在凭证内唯一;正数金额精确存储
ledger_account 主体、科目、币种、正常余额方向 主体与账簿边界明确,状态变化留痕
balance_projection 账户、累计借贷、处理到的分录序号 可从分录重建;读者知道投影截止点

在同一受控数据库中,凭证、全部分录、幂等记录,以及用于资金决策的余额或占用变化应按事务一起提交。跨服务通知可用事务发件箱可靠传递,消费方继续幂等;远程通道无法加入本地数据库事务,要通过状态机、查询和对账收敛。

一张凭证怎样保持平衡

凭证是一个业务阶段在账本中的一次确认。一个订单可以产生收款、扣费、结算等多张凭证;每张应有唯一凭证号、事件类型、业务时间、记账时间、来源、关联订单和一组分录。每一条分录至少包含账户、借贷方向、金额、币种和精度。

本例选择每币种独立平衡的业务账本:同一凭证、账簿、主体和币种内,Σ借方金额 = Σ贷方金额;单币种资产负债关系为“资产 = 负债 + 所有者权益”,本期收入减费用形成利润并按政策结转。借方正常账户的余额 = 期初余额 + 本期借方 − 本期贷方;贷方正常账户反过来计算。

简化的入账检查可以写成:

  1. 分录数量至少为两条,且借方和贷方都存在。
  2. 同一凭证按账簿、主体与币种分组,借方合计等于贷方合计;跨主体走各自的往来科目。
  3. 金额大于零,币种有效,账户状态允许记账。
  4. 业务事件阶段的幂等键、凭证号和凭证内分录行号满足各自唯一约束。
  5. 所有检查通过后再一次性提交,失败则整张凭证不生效。

例如收款 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,最多一笔占用成功;写入中断时凭证全成或全不成;关闭期间禁止无授权回写;未知结果不会提前增加可用余额。这里列的是待实现账本的验收目标,本站没有运行资金账本服务。

达到效果后,团队应该可以用同一套记录回答四个问题:这笔钱从哪里来、现在属于谁、为什么改变、怎样证明没有漏记或重复记。可执行的验收包括:随机抽一笔业务能还原事件到凭证;重放同一事件不产生新余额变化;按币种和账户重算结果与展示一致;冲正不删除原记录;未决状态不会被报表误当成已结算。

可以先读为什么复式账比单边账更适合业务系统,再用业务事件到最终入账的流程图检查实现顺序。

有具体场景,欢迎进一步交流。