工程 · 2026.05.20 · 4 min · 更新於 2026.10.26
從 FastAPI 腳本到 Spring Boot SaaS:我的 Bitfinex 自動放貸平台架構演進
Bitfinex 自動放貸 side project 從「能跑就好」的 FastAPI 單人腳本,重寫成 Spring Boot + Next.js 多模組平台:冪等、對帳、斷路器與監控怎麼設計。
閱讀設定
自動放貸腳本送出掛單後網路斷了,單到底有沒有進 Bitfinex?直接重試,就會重複放貸。
我把原本用 FastAPI 寫的放貸腳本整個重寫成 Spring Boot + Next.js,核心是先寫 DB 再下單的冪等設計:連線出錯時不猜結果,交給對帳收斂。
外面再包一層斷路器和限流,加上監控,讓系統出錯時能自己站起來。
這個 side project 一開始只是想偷懶。Bitfinex 的 funding(放貸)市場利率每天在跳,手動掛單很煩,於是我寫了個腳本自動掛,能跑就好。
直到它開始管真錢、開始有別人用,「能跑就好」就不夠了。
舊版能動,但很脆
第一版很典型:後端 FastAPI,跑幾個 async task 輪詢市場、掛單;前端 Vue 3 + Vite,畫幾張收益圖;Docker Compose 一把梭。
技術選型沒什麼問題,缺的是「出錯時怎麼辦」的概念。
掛單送出去之後如果網路斷了,我不知道單到底進去沒。交易所偶爾抽風回 5xx,整個輪詢就卡住。要加個欄位,得手動改 schema。
它在順風的時候能跑,但放貸是在管錢,逆風才是常態。
重寫成 Spring Boot + Next.js
與其縫縫補補,我決定重寫。後端選 Spring Boot 3 / Java 21,理由很實際:我本職就在寫 Java / Spring,熟悉度直接轉成可靠度。
我要的是 transaction、排程(@Scheduled)、circuit breaker、metrics 這些「無聊但成熟」的東西,Spring 生態一次給齊。強型別加上 JPA 和 Flyway,schema 變更有版控、有 migration,不再手改資料庫。
前端換成 Next.js 16 / React 19,用靜態匯出(output: export)直接丟 Cloudflare Pages。前端不需要 server,CDN 就夠。
整體是一個 monorepo,分層大致如下:
Next.js (靜態匯出) → Cloudflare Pages
│ JWT (HttpOnly cookie) + XSRF
▼
Cloudflare Tunnel → Spring Boot (Java 21)
├─ 放貸引擎(排程)
├─ 對帳服務(排程)
├─ Bitfinex client(斷路器 + 限流)
├─ 通知(Email / Telegram)
└─ 稽核(hash chain)
│
PostgreSQL 16 Redis 7
(Flyway) (快取/鎖/限流)
後端用 Cloudflare Tunnel 對外,伺服器不用開公開 IP、不用自己弄反向代理憑證,這對個人專案省事很多。
先寫 DB,再下單
整個系統最核心的,是下單的冪等設計:同一筆單不管重送幾次,結果都跟只送一次一樣。
放貸引擎在固定的排程週期跑。每個週期大致是:讀市場、算出要掛的利率與期限、算可用餘額、下單。
聽起來單純,但「下單」這一步是跟外部交易所打交道,隨時可能 timeout,也可能送出去了但回應沒收到。這時候直接重試,就會重複放貸。
我的處理是把順序倒過來,先寫 DB 再下單:
- 先在 DB 寫一筆
PENDING的訂單,帶一個自己產生的 idempotency UUID。 - 再把單送去 Bitfinex。
- 成功,就更新成
ACTIVE並記下交易所的 offer ID。 - API 邏輯錯誤,標成
ERROR。 - 連線層級的錯誤(timeout 等),保持
PENDING,不猜結果。
關鍵在第 5 步:連線出錯時,我不假設成功也不假設失敗,把判斷交給對帳。
另一個排程是對帳服務(reconciliation),週期比引擎長。它專門掃那些卡在 PENDING 的訂單,去問交易所這筆到底有沒有進去。
交易所查得到,就更新成對應狀態;查不到,就推斷當初其實沒送成功,下個引擎週期會自動重新處理。
這套「樂觀下單 + 事後對帳」的組合,讓系統在網路不穩時不會重複下單,也不會把錢卡在不明狀態。
這是從舊版學到最重要的一課:分散式環境下,狀態要能自己修復,不能祈禱每次都成功。
冪等靠的是整個下單流程的順序設計,光加個 UUID 不夠。先想清楚「哪一步出錯會怎樣」再寫。
斷路器、限流與金鑰
外部 API 是最不可控的部分,我用兩層保護。
第一層是斷路器(Resilience4j):連續失敗到一定比例就「跳開」,一段時間內不再打交易所,避免在它掛掉時瘋狂重試把自己也拖垮。
這裡有個取捨:只有連線層級的錯誤才計入斷路器。API 邏輯錯誤(例如參數錯)算我自己的 bug,不該觸發斷路,不然連自己的 bug 也熔斷,問題只會更難查。
第二層是限流。交易所有 rate limit,我用 Redis 當共享計數器做 token bucket(額度按固定速度補充,用完就等),留一點 headroom,確保不會因為打太兇被擋。
定價策略本身是以資金簿深度錨定利率、再動態加上溢價,並在送單前做一次利率合理性檢查,擋掉異常值。具體參數就不公開了,但骨架是這樣。
平台會碰到使用者的 Bitfinex API 金鑰,這東西外洩是災難,所以:
- 登入是 Email + 密碼(BCrypt)+ TOTP 兩階段驗證,簽 JWT。
- 使用者的交易所金鑰用 AES-256 加密後才進 DB,要用時才解。
- 稽核日誌做成 hash chain(每筆紀錄包含前一筆的雜湊),事後可驗證有沒有被竄改。
監控與重寫的代價
舊版出事,我只能 grep log。
現在 log 是結構化 JSON。Prometheus 抓 metrics(放貸週期延遲、下單數、斷路器狀態、限流飽和度),Grafana 看板,Alertmanager 出事發 Telegram。Health check 分 liveness / readiness,Docker 跟著探活。
部署是 GitHub Actions 自架 runner 跑 CI,secrets 全放 1Password 注入,Docker 多階段建置出精簡 image。對一個人維護的專案來說,這套讓我「半夜不用爬起來看」。
重寫的成本很真實。從 FastAPI 換到 Spring,要重新想清楚每個邊界條件,光翻譯程式碼不夠。但換來的是「敢讓它自己跑」的安心感。
這個 side project 教我的,其實跟本職工作一樣:寫一個會跑的程式不難,寫一個出錯時還能自己站起來的系統才難。
冪等、對帳、斷路器、可觀測性在 demo 時看不出差別,但它幫你管錢、半夜默默運行的時候,靠的全是這些。

工程
muse.ai 寄信審批卡按不到:Chrome 擴充功能改讀 React 資料判斷
商店有人回報寄信的審批卡按不到。我把能觸發的卡全跑一遍,發現同一筆審批會以三種樣子出現,按鈕文字也隨動作和語言變。1.3.0 改成讀 muse 標在頁面上的屬性和 React 裡的審批資料,讀不到才退回認文字。
留言
載入留言中…