← 回到目錄

工程 ·2026年5月20日 ·4 min read

從 FastAPI 腳本到 Spring Boot SaaS:我的 Bitfinex 自動放貸平台架構演進

一個自動放貸 side project 怎麼從「能跑就好」的單人腳本,重寫成有冪等、對帳、斷路器與完整監控的多模組平台。

這個 side project 一開始只是想偷懶。

Bitfinex 的 funding(放貸)市場利率每天在跳,手動掛單很煩,於是我寫了個腳本自動掛。能跑就好——直到它開始管真錢、開始有別人用,「能跑就好」就不夠了。這篇講它怎麼從一坨 Python 腳本,重寫成現在這套架構。

舊版:FastAPI + Vue,能動但脆

第一版很典型:

  • 後端 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 再下單

  1. 先在 DB 寫一筆 PENDING 的訂單,帶一個自己產生的 idempotency UUID
  2. 再把單送去 Bitfinex
  3. 成功 → 更新成 ACTIVE 並記下交易所的 offer ID
  4. API 邏輯錯誤 → 標 ERROR
  5. 連線層級的錯誤(timeout 等)→ 保持 PENDING,不猜結果

關鍵在第 5 點:連線出錯時我不假設成功也不假設失敗,把判斷交給對帳。

對帳:把「不確定」收斂掉

另一個排程是對帳服務(reconciliation),週期比引擎長。它專門掃那些卡在 PENDING 的訂單,去問交易所「這筆到底有沒有進去」:

  • 交易所查得到 → 更新成對應狀態
  • 交易所查不到 → 推斷當初其實沒送成功,下個引擎週期會自動重新處理

這套「樂觀下單 + 事後對帳」的組合,讓系統在網路不穩時不會重複下單,也不會把錢卡在不明狀態。這是從舊版學到最重要的一課:分散式環境下,狀態要能自我修復,而不是祈禱每次都成功。

風控:把交易所的故障隔離掉

外部 API 是最不可控的部分,我用兩層保護:

  • 斷路器(Resilience4j):連續失敗到一定比例就「跳開」,一段時間內不再打交易所,避免在它掛掉時瘋狂重試把自己也拖垮。這裡有個取捨——只有「連線層級」的錯誤才計入斷路器;「API 邏輯錯誤」(例如參數錯)算我自己的 bug,不該觸發斷路。
  • 限流(Redis token bucket):交易所有 rate limit,我用 Redis 當共享計數器做 token bucket,留一點 headroom,確保不會因為打太兇被擋。

定價策略本身是以資金簿深度錨定利率、再動態加上溢價,並在送單前做一次利率合理性檢查,擋掉異常值。具體參數就不公開了,但骨架是這樣。

安全:在管別人的錢,就得認真

平台會碰到使用者的 Bitfinex API 金鑰,這東西外洩是災難,所以:

  • 登入是 Email + 密碼(BCrypt)+ TOTP 兩階段驗證,簽 JWT
  • 使用者的交易所金鑰用 AES-256 加密後才進 DB,要用時才解
  • 稽核日誌做成 hash chain(每筆紀錄包含前一筆的雜湊),事後可驗證有沒有被竄改

觀測性與維運

舊版出事我只能 grep log。現在:

  • 結構化 JSON log
  • Prometheus 抓 metrics(放貸週期延遲、下單數、斷路器狀態、限流飽和度…),Grafana 看板,Alertmanager 出事發 Telegram
  • Health check 分 liveness / readiness,Docker 跟著探活

部署是 GitHub Actions 自架 runner 跑 CI,secrets 全放 1Password 注入,Docker 多階段建置出精簡 image。對一個人維護的專案來說,這套讓我「半夜不用爬起來看」。

踩過的坑

  • 冪等不是加個 UUID 就好,是整個下單流程的順序設計。先想清楚「哪一步出錯會怎樣」再寫。
  • 斷路器要分清楚錯誤類型,不然把自己的 bug 也熔斷,問題只會更難查。
  • 重寫的成本很真實。從 FastAPI 換到 Spring 不是翻譯程式碼,是重新想清楚每個邊界條件。但換來的是「敢讓它自己跑」的安心感。

小結

這個 side project 教我的,其實跟本職工作一樣:寫一個會跑的程式不難,寫一個出錯時還能自己站起來的系統才難。 冪等、對帳、斷路器、可觀測性——這些在 demo 時看不出差別,但在它幫你管錢、半夜默默運行的時候,就是全部。

留言

載入留言中…