Journal
替新創影音團隊做一套記帳系統的始末
和一個新創影音團隊的合作。這篇記錄他們的內部記帳系統是怎麼開始的、中間轉了幾次彎、現在長什麼樣子。
來由:錢的問題比想像中瑣碎
創作團隊的金流有幾個特徵:收入從 Patreon 這類平台進來、支出常常是某個成員先代墊、團隊還有一個公費池。這三件事各自都不難,難的是把它們放在同一本帳上還能回答日常的問題——這筆誰墊的?結清了沒?這一期的某個主題總共花了多少?公費池還剩多少?
在這套系統之前,團隊用 Excel 記帳。試算表撐得起「把數字記下來」,但撐不起上面那些問題:分帳與結清狀態靠人工對照、主題與期別的彙總每次都要重拉、代墊統計散在各處。帳越多,回答一個問題的成本越高。
所以這個專案從第一天就把範圍劃得很窄:可追蹤的收支、分帳、結清狀態、主題與期別統計、成員負擔統計。正式會計分錄、資產負債表、稅務,全部明確排除。它是一本給團隊自己看的帳,不是會計系統。
形態
- 架構:pnpm monorepo,分
apps/web(React + Vite + TypeScript)、apps/api(Fastify)、packages/domain三層 - 資料:PostgreSQL + Prisma
- 部署:API 在 Render,前端走 Cloudflare
這是我第一次嘗試偏向全程 vibe coding 的專案:實作交給 AI Agent 執行,commit 紀錄裡就留著 Co-Authored-By: Claude 的簽名。除非不得已才親自下去 debug 或微調 UI,否則我大多數時間花在釐清需求、制定計畫、驗收結果,以及在關鍵的設計分岔口做決定。
為什麼是 PostgreSQL,不是 MongoDB
前一個專案 GEAIKOU(怪乙口) 我用的是 MongoDB,這次是首次使用 PostgreSQL。不是偏好轉移——起初在規劃時就發現,這次的資料形狀和以往不同,有更多「關聯」要設計。
怪乙口處理的遊戲資料是文件形(一個角色、一包屬性、結構還在變),適合 document store;帳不是。帳的本質是關聯:一筆主帳目掛多個細項、細項連到付款人、帳目屬於某個主題和期別、結清狀態要從細項聚合推導。每一張報表——主題小計、期別彙總、成員代墊統計、公費池餘額——都是 JOIN 加 GROUP BY 的形狀。在 MongoDB 裡做這些,等於自己在應用層重新發明關聯資料庫。
另一個原因是錢對「不一致」的容忍度是零。外鍵約束、交易、schema 由資料庫層強制,錯的資料進不去,比在應用層寫防禦邏輯可靠。搭配 Prisma,關聯和約束直接寫在 schema 裡,型別一路推到前端。
目前的理解是:選資料庫先看資料的形狀和一致性要求,再看熟不熟。這次兩個指標都指向關聯式。
轉向一:公費池不是人
最初的設計裡,公費池是用一個虛擬成員「團」來承載的——付款人選「團」,就代表從公費出。這個做法上線後很快暴露出代價:UI 要特別處理這個假成員、查詢要繞路、成員資料維護也跟著彆扭。
後來的重構把它拆成明確的欄位:payerSourceType: 'team_fund' | 'member',成員 ID 在公費支出時允許為空。理由寫在設計文件裡的一句話:公費池不是人,不應該存在 Member 表裡。資料模型撒的謊,遲早會在每一層介面討回來。
轉向二:收入與支出其實是同一種東西
第二個轉向在帳目結構。早期版本把收入(incomeAmount)和支出(expenseLines[])當成兩軌各自處理,做報表的時候才發現彙總邏輯要寫兩份。重構後統一成 LedgerEntry(total + lines[])的結構——一筆帳就是一筆帳,方向只是屬性。主帳目(LedgerEntry)可以掛多個明細(LedgerLine),每個明細有自己的付款人、金額、結清狀態,主帳目的結清狀態由明細聚合推導。
這兩次轉向都發生在專案早期,成本還付得起。目前抓住的原則是:資料模型的彆扭感是訊號,不要用 UI 去補。
實作的步驟
把需求釐清後,將一些團隊重視的內容排開來,收入、支出、團費、團員代墊結清以及一些分析的儀表板等等,設計了一個初版的資料結構後,將一些基本要在後端實現的功能先建立起來,費用的CRUD、分析內容的邏輯。隨後就將我們有實現的內容和核心需求透過AGENT進行畫面設計,對我來說這類型工具是以方便操作為主,UX上尤為重要,因此整個畫面設計上不追求太過花俏,以能夠好操作為主(自己回頭看,也是這塊迂迴最久)。
手機優先的補課
系統的主要使用場景是團隊成員隨手記一筆、隨手查一筆,手機才是主戰場。六月底有一輪完整的行動裝置重構:先建立 Design Tokens 與響應式規範,再逐頁翻新總覽、交易、表單、結清、分析、設定。過程中定下一條架構規則——手機與桌機差異大的頁面,邏輯抽成共用的 hooks,視圖分流到 desktop/、mobile/、shared/ 三個子目錄,不在同一個元件裡塞滿條件判斷。
心得
vibe coding 真正改變的,是時間花在哪裡。Agent 產出很快,但這也代表錯的需求會一樣快地被做出來——描述偏一次,浪費的是一整輪實作。所以這個專案裡最值錢的時間,其實是動工前把需求和驗收條件講清楚的那段。程式碼可以重寫,講不清楚的需求怎麼重跑都是歪的。
而分岔口的判斷,還是人的事。「公費池不是人」「收支其實是同一種東西」這兩個轉向都不是 Agent 提的,是東西做出來、用起來覺得彆扭,回頭盯著資料模型才發現的。工具能把方案生出來,但「這個方案哪裡在說謊」,目前還是得靠人對領域的理解去聞。
還有一件,是把「不做什麼」寫下來,跟「做什麼」一樣重要。這系統從第一天就排掉正式會計、稅務、對外串接,範圍窄到有點無聊——但也因為窄,它真的被做完、被用起來了。回頭看,這大概是整個專案最省錢的一個決定。