複式賬怎麼設計:借方、貸方和一張能對賬的憑證
從科目邊界、分錄平衡到多幣種、冪等與沖正,整理一套可以落到業務系統裡的複式賬設計方法。
「借方」和「貸方」容易被誤解成收入和支出的兩個按鈕。實際上,它們是分錄的兩個方向;方向的意義要結合科目類型、賬戶所有者和具體業務規則來確定。
這篇不試圖替代會計準則或企業的科目制度,而是從產品和工程角度整理一個可落地的賬本邊界:什麼事件可以入賬、分錄怎樣生成、怎樣保持可重算,以及出現未知結果時如何停下來。
借方和貸方不是正負號
對常見科目,可以先記住「增加方向」這張小表:
| 科目類型 | 借方增加 | 貸方增加 | 常見例子 |
|---|---|---|---|
| 資產 | 是 | 否 | 銀行存款、已確認的應收款 |
| 負債 | 否 | 是 | 商戶應付款、客戶預存義務 |
| 權益 | 否 | 是 | 所有者投入、留存結果 |
| 收入 | 否 | 是 | 符合確認條件的服務費收入 |
| 費用 | 是 | 否 | 通道費、營運費用 |
這張表描述的是通常的餘額方向,不是「錢從左邊流到右邊」的流程圖。平台收到客戶資金時,資產可能在借方增加,同時對客戶的負債在貸方增加;平台向商戶付款時,資產在貸方減少,商戶應付款在借方減少。
因此,業務人員不應只問「這筆錢是收入還是支出」,而應先問:誰控制這筆資金、誰對誰負有義務、這次事件改變了哪幾個賬戶。
先定義科目和賬戶
科目是賬本裡的分類,明細賬戶是某個主體在該分類下的記錄單位,餘額是分錄匯總的結果。科目、賬戶和餘額各有職責。
一個支付系統至少要區分平台自有資金、客戶資金、商戶應付款、通道清算、手續費收入、退款應付和待核對賬戶。具體名稱要隨業務主體和會計政策確定,下面的名稱只是系統建模示意。
我會給每個賬戶保留穩定身份和邊界:
- 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,最多一筆佔用成功;寫入中斷時憑證全成或全不成;關閉期間禁止無授權回寫;未知結果不會提前增加可用餘額。這裡列的是待實現賬本的驗收目標,本站沒有運行資金賬本服務。
達到效果後,團隊應該可以用同一套記錄回答四個問題:這筆錢從哪裡來、現在屬於誰、為什麼改變、怎樣證明沒有漏記或重複記。可執行的驗收包括:隨機抽一筆業務能還原事件到憑證;重放同一事件不產生新餘額變化;按幣種和賬戶重算結果與展示一致;沖正不刪除原記錄;未決狀態不會被報表誤當成已結算。
可以先讀為什麼複式賬比單邊記錄更適合業務系統,再用業務事件到最終入賬的流程圖檢查實現順序。