整個系統部署在 2 GB RAM 的 EC2 主機,記憶體預算如下:
圖示:2 GB 記憶體分配(應用程式 · Nginx · OS 緩衝)— PostgreSQL 已遷移至 Neon Cloud,釋出 400m
| 項目 | v1 微服務需求 | v2 Modular Monolith 實況 |
|---|---|---|
| JVM 數量 | 5 個 | 1 個 |
| JVM 基本開銷 | 5 × 約 150–200m = 750m–1 GB 純啟動 | 1 × 約 150–200m = 200m 純啟動 |
| 實際可用堆積 (Heap) | 每服務僅剩 ~50m → 隨即 OOM | 單一應用享有 -Xmx480m,舒適運作 |
| Redis(v1 計畫的 cache) | 另需 100–200m | ❌ 不裝。改用 Caffeine in-JVM Cache(0 額外 RAM) |
| 5 個獨立 DB | PostgreSQL 多實例 × 5 ≈ 2 GB | 單一 restaurant_db,模組以表前綴隔離 |
| 結論 | 2 GB 主機跑不起,需 8 GB+ 才舒適 | 2 GB 剛好夠用,仍保留模組邊界 |
關鍵設計:模組邊界仍守住。透過下列規範,未來要把單一模組「搬出去」成獨立微服務時,重構成本極低:
@FeignClient(url = "http://127.0.0.1:8080")),等同微服務通訊介面。@ManyToOne 跨模組指向,避免硬耦合。Caffeine 快取池清單(6 個):
| 快取名稱 | TTL | max size | 用途 |
|---|---|---|---|
menus-today | 3 min | 10 | 今日菜單 — 高頻讀取(午餐尖峰每秒上百次) |
dishes-today | 3 min | 10 | 今日主菜清單 |
drinks-today | 3 min | 10 | 今日飲品清單 |
menu-by-id | 10 min | 200 | 單一菜單細節(瀏覽商品頁) |
dish-by-id | 10 min | 200 | 單一主菜細節 |
drink-by-id | 10 min | 200 | 單一飲品細節 |
create / update / delete / updateBalance / uploadImage)都帶 @Caching(evict = {...}) 自動清除受影響快取;每日午夜 DailyStockResetScheduler 重置庫存時,6 個快取一次清空。
Stripe Top-up 流程(Idempotent — 同一 session 重呼 verify 安全):
1. 前端 POST /wallets/{walletId}/topup/create-session {amount}
2. 後端建 StripeTopupRecord (PENDING)、回傳 sessionUrl
3. 使用者導至 Stripe Checkout
4. 付款後 Stripe 導回 success-url?session_id=xxx
5. 前端 GET /wallets/{walletId}/topup/verify?sessionId=xxx
6. 後端與 Stripe API 對帳、入賬 wallet、標記 SUCCEEDED
7. 回 {status, walletBalance, message}
carts + cart_items 兩張新表finalTotal = baseTotal + depositAmt(等於多收 $50)X-User-Id header,與現行 Spring Security HTTP Basic 不符localStorage,0 個新 DB 表depositAmt 正確語意:totalAmt 的先付部分,非額外加項POST /orders,流程最短v2 的核心策略是:能交給雲端的事,就交給雲端做。本機只跑最關鍵的業務邏輯與資料庫,省下記憶體給 JVM。
| 功能 | 放哪 | 理由 |
|---|---|---|
| 資料庫 | ☁ Neon Serverless | 釋出 400m RAM;Zeabur 以環境變數注入連線字串;本機仍用 Docker postgres |
| 金流付款 | ☁ Stripe Cloud | 合規、安全、可靠性 99.99%,免維運 |
| 菜單圖片儲存 | ☁ Cloudflare R2 | 容器重啟圖片不滅失;免費方案足夠 MVP;image_url 直接存 public URL |
| 使用者認證 | 本地 JWT + BCrypt | 無第三方依賴,控管最大化 |
| 取餐通知 | 本地 WebSocket | 低延遲、無需經外部服務 |
| 排程任務 | 本地 Spring Scheduler | 每日庫存重置,量小無需 cloud job runner |
| 反向代理 | 本地 Nginx 1.29 | 路徑剝離、CORS、靜態檔;輕量 50m RAM |
| 監控 / 日誌(未來) | ☁ CloudWatch / Sentry | 不佔本機 RAM、可長期保存 |
| 元件 | RAM | 備註 |
|---|---|---|
| ☁ Neon Serverless PostgreSQL | 0 MB | 已遷移至 Neon Cloud,不佔 Zeabur 記憶體 |
| restaurant-app (Spring Boot) | 700 MB | JVM -Xmx480m -Xms128m;含 Caffeine cache |
| Nginx 1.29 | ~50 MB | 反向代理 + 靜態檔 |
| OS / Buffer / Cache | ~1.25 GB | Linux 檔案快取、SSH、緩衝空間(較前增加 400m) |
| 合計 | ~2 GB | 餘裕比原本多 400m,穩定性大幅提升 |
雖然這次跑 1 個 JAR,但模組設計仍按微服務的標準,未來只要記憶體升級或業務分流即可逐一搬出:
| 微服務原則 | v2 對應作法 |
|---|---|
| 單一職責 (SRP) | 每個模組獨立 package:com.restaurant.login / wallet / menu / order / pickup |
| 無共享資料庫 | 單一 DB,但無跨模組 JOIN,未來拆分時各模組可帶走自己的表 |
| 服務間通訊 | OpenFeign HTTP 呼叫,未來改 URL 即可指向獨立服務 |
| 故障隔離 | GlobalExceptionHandler 統一處理;單模組異常不影響其他模組 |
| 獨立部署 | 暫時共部署;模組內 controller 已具獨立 API 命名空間 |
當以下任一條件成立時,再啟動「模組搬離」工程:
| 搬出順序 | 候選模組 | 理由 |
|---|---|---|
| 第 1 順位 | wallet | 金流敏感、可獨立銷售予福利社 / 影印站;首先解耦最有商業價值 |
| 第 2 順位 | menu | 讀多寫少,可獨立 scale;圖片可同步搬至 CDN |
| 第 3 順位 | pickup | WebSocket 連線管理特殊,獨立後可改用 AWS API Gateway WebSocket |
| 留在主體 | login / order | 業務核心,最後再考慮拆分 |
同時將 Caffeine cache 替換為 Redis,並改用 SQS / EventBridge 做服務間非同步通訊。屆時架構即升級為「真正的微服務」。
原始設計將上傳圖片存放於容器本地檔案系統(/app/uploads/menu/),
並由 Spring Boot WebConfig 透過 /uploads/** 路徑對外提供服務。
在本機 Docker Compose 環境中,uploads_data named volume 可使圖片在容器重啟後保留。
然而在 Zeabur 雲端平台部署後,每次 git push 觸發重新部署時容器會被替換,
本地檔案系統全數清空。資料庫中的 image_url 欄位仍保留舊路徑(如
/uploads/dish_af962598-....jpg),但實際檔案已不存在,
瀏覽器請求時收到 HTTP 404,前端所有圖片空白。
| 情境 | 圖片是否存活 | 原因 |
|---|---|---|
| 本機 Docker Compose | ✅ 存活 | uploads_data named volume 持久化 |
| Zeabur 重新部署後 | ❌ 消失 | 容器替換,無持久 volume,檔案清零 |
放棄在容器內存圖,改用 Cloudflare R2 Object Storage 作為圖片倉庫。 R2 免費方案(10 GB 儲存 / 月 100 萬次請求)對校園餐廳規模完全足夠。
| 步驟 | 做法 |
|---|---|
| 1. 建立 R2 Bucket | Cloudflare Dashboard → R2 → 建立 bucket(例:canteendb) |
| 2. 開啟公開存取 | Bucket → Settings → Public Access → Allow Access,取得 https://pub-xxx.r2.dev base URL |
| 3. 上傳圖片 | 手動上傳 dish.jpg、drink.jpg 至 R2 bucket |
| 4. 更新資料庫 |
在 PostgreSQL 執行:UPDATE dishes SET image_url = 'https://pub-xxx.r2.dev/dish.jpg';UPDATE drinks SET image_url = 'https://pub-xxx.r2.dev/drink.jpg';
|
| 5. 清除 Caffeine Cache | 重啟 Zeabur 服務,或等待 3 分鐘讓 dishes-today/drinks-today cache TTL 到期 |
此方案的 image_url 欄位直接儲存完整的 HTTPS public URL(如
https://pub-e47315ff76f145eeae9e5a2f8486bea8.r2.dev/dish.jpg),
瀏覽器直接向 Cloudflare CDN 拉取圖片,Spring Boot 完全不參與圖片傳輸。
每次部署後圖片依然存活,無需重新上傳。
v2 新增一個嵌入式 AI 聊天助理,讓顧客無需登入任何第三方平台即可查詢取餐狀況或諮詢問題。
當訊息符合取餐碼格式(如 0289-018),前端直接呼叫 GET /api/pickups/status?code=XXXX,即時顯示狀態,無需呼叫 AI API,節省 token 用量。
| 指令 | 處理方式 | 說明 |
|---|---|---|
| 📋 訂單狀況 | 本地 | 引導輸入取餐碼 |
| 📞 聯絡 | 本地 | 顯示聯絡資訊 |
| ❓ 幫助 | 本地 | 顯示使用說明 |
| 一般問題 | Gemini AI | 呼叫 /api/chat 由 AI 回覆 |
/api/chat),永不直接接觸 Google API。
這份架構說明 v2 並非「降級」,而是「合身」。v1 的願景仍在;v2 是它的第一個可上線版本。