# Bonsale x 3CX 架構 > {%preview https://miro.com/app/board/uXjVJRw4tds=/ %} ``` +---------------+ HTTP/API +-----------------------------+ API +---------+ | | <----------> | | <-----> | | | 前端儀表板 | | 後端應用 (單一程序) | | CRM | | (React) | | - Express API Server | | System | | | <----------> | - Socket.IO Server | <-----> | | +---------------+ WebSocket | - BullMQ Workers | +---------+ | | +--------------+--------------+ | ▲ | │ API ▼ | +-----------------+ +---------+ | Redis | | | | (佇列 + 狀態) | | 3CX | +-----------------+ | System | +---------+ ``` --- ```mermaid sequenceDiagram participant B as 前端瀏覽器 box VM / Docker Network participant N as Nginx (入口 & 反向代理) participant S as 後端應用 (Node.js API) participant R as Redis (佇列 + 狀態) end participant C as 3CX 系統 %% --- 第一階段:頁面載入與連線 (由 Nginx 處理) --- Note over B: 使用者在瀏覽器輸入 VM 的 IP 或網域 B->>N: 1. 請求儀表板頁面 (GET /) N-->>B: 2. Nginx 直接回應建構好的前端靜態檔案 (index.html, JS, CSS) Note over B, S: 瀏覽器執行 JS,準備與後端通訊 B->>N: 3. 建立 WebSocket 連線請求 (e.g., to wss://your-vm-ip/socket.io/) N->>S: 4. Nginx 將 WebSocket 連線請求「轉發」給後端 Node.js 服務 S-->>N: 連線成功 N-->>B: 連線成功 B->>S: 5. 透過已建立的連線,發送 'subscribe_many' 事件 S->>S: 6. 將連線放入記憶體中的 50 個房間 %% --- 第二階段:使用者操作 --- Note over B: 使用者點擊「啟動專案 A」 B->>N: 7. 發送 POST /api/projects/start 請求 N->>S: 8. Nginx 看到 /api 路徑,將請求「轉發」給後端 Node.js 服務 S->>R: 9. 後端將任務放入 Redis (BullMQ 佇列) S-->>N: 10. 後端回應 HTTP 200 OK N-->>B: 11. Nginx 將後端的回應傳回給瀏覽器 %% --- 第三階段:後端處理與即時更新 --- loop 背景持續運作 S->>R: 12. 從 Redis 取得任務 S->>C: 13. 呼叫 3CX API 發起外撥 C->>S: 14. 3CX 發送 Webhook S->>S: 15. 處理 Webhook 邏輯 S->>S: 16. 向記憶體中的「project-A」房間廣播事件 S-->>B: 17. 後端透過 WebSocket 直接推送訊息給前端 B->>B: 18. 前端更新 UI 狀態,畫面即時變化 end ``` --- ## 可能的專案架構 ``` /your-monorepo-project/ ├── apps/ │ ├── backend/ │ │ ├── src/ │ │ ├── Dockerfile <-- 後端的 Dockerfile │ │ └── package.json │ └── frontend/ │ ├── src/ │ ├── dist/ <-- 前端執行 build 後,靜態檔案會產生在這裡 │ └── package.json │ ├── packages/ │ └── shared-types/ # 存放共享的 TypeScript 類型 │ ├── nginx/ <-- **在根目錄建立一個專門的 Nginx 資料夾** │ ├── nginx.conf <-- Nginx 的主要設定檔 │ └── Dockerfile <-- 專門用來建立 Nginx 映像檔的 Dockerfile │ ├── docker-compose.yml <-- **最外層的服務啟動器** ├── package.json # Monorepo 的 package.json ├── pnpm-workspace.yaml └── turbo.json ``` ``` # docker-compose.yml version: '3.8' services: backend: build: context: . # build context 是整個專案 dockerfile: apps/backend/Dockerfile # ... 其他設定 nginx: build: context: . # build context 也是整個專案 dockerfile: nginx/Dockerfile # **指向 nginx 資料夾中的 Dockerfile** ports: - "80:80" depends_on: - backend redis: # ... 其他設定 ``` ``` # nginx/Dockerfile # Stage 1: 先在一個 Node 環境中建置前端專案 FROM node:18-alpine as builder WORKDIR /app # 複製整個專案的檔案到建置環境中 COPY . . # 執行前端的 build 指令 # 假設您使用 pnpm 和 turbo RUN pnpm install --frozen-lockfile RUN pnpm turbo run build --filter=frontend # Stage 2: 建立最終的 Nginx 映像檔 FROM nginx:alpine # 複製您寫好的 Nginx 設定檔 COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf # **從上一個建置階段,只複製建構好的靜態檔案** COPY --from=builder /app/apps/frontend/dist /usr/share/nginx/html EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] ``` ``` # nginx/nginx.conf server { listen 80; # 所有非 API 和非 WebSocket 的請求,都由 Nginx 處理 location / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; } # 所有 /api 的請求,都轉發給後端服務 location /api { # "backend" 是 docker-compose.yml 中定義的服務名稱 # Docker 內建的 DNS 會將它解析到後端容器的 IP proxy_pass http://backend:3000; } # 所有 /socket.io/ 的請求,也轉發給後端服務 location /socket.io/ { proxy_pass http://backend:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; } } ```