Skip to content

架構 (Architecture)

瑞成 PMS 是一個 單一 Next.js 16 全端 app。沒有獨立的 API server、沒有 FastAPI/Vite 拆分: Server Components 與 Server Actions 透過 Drizzle ORM 直接讀寫 PostgreSQL,Auth.js v5(JWT session)負責認證。

本頁面向開發者,整理系統邊界、請求/變更生命週期、Auth + RBAC、領域模型、燈號預警引擎與資料庫 schema。 細節對齊程式碼;引用的檔案路徑相對於 repo 根目錄。

系統圖 (System Diagram)

┌──────────────────────────────────────────────────────────────┐
│                     NEXT.JS APP (next-app/)                    │
│                                                                │
│  React 19 · App Router · TypeScript · Tailwind v4 · shadcn/ui  │
│  Server Components(預設)+ Client Components("use client")   │
│                                                                │
│  ┌────────────────────┐    ┌──────────────────────────────┐   │
│  │ Server Actions     │    │ Route Handlers (app/api/**)   │   │
│  │ actions/*.ts       │    │ health · openapi · auth/*     │   │
│  │ (真正的 API,RPC) │    │ (唯一真實 HTTP 介面)         │   │
│  └─────────┬──────────┘    └───────────────┬──────────────┘   │
│            │                               │                   │
│            └──────────────┬────────────────┘                  │
│                           │ Drizzle ORM (postgres-js)          │
├───────────────────────────┼────────────────────────────────-──┤
│                           ▼                                    │
│                       PostgreSQL                              │
│   users · accounts · cases · clients · contacts · calendars   │
│   case_categories · node_templates · audit_log · notifications│
└───────────────────────────────────────────────────────────────┘

Edge: proxy.ts → auth() 檢查 → redirect /login 或放行

設計原則: Next.js 是唯一的全端邊界。沒有 billing、沒有金流 webhook、沒有 FastAPI/Vite——這些在 產品從 SaaS 模板 strip 為內部工具時(migration 0008_strip_saas_surfaces)已移除。唯一的 I/O edge 是 Drizzle;完全沒有外部 API


請求生命週期 (Request Lifecycle)

瀏覽器 Request
  → proxy.ts (edge middleware:auth guard、未登入 redirect /login)
  → Next.js router 比對 segment
  → layout.tsx (Server Component:session、導覽資料)
  → page.tsx   (Server Component:以 Drizzle 取頁面資料,依 live role 收斂可見性)
  → Client Components 在瀏覽器 hydrate

變更生命週期 (Mutation Lifecycle)

Client 表單 / 按鈕觸發 Server Action ("use server")
  → auth() 取 session
  → getLiveRole() 從 DB re-read 目前角色 → 守衛 (requireAuth / guard(flag) / requireAdmin)
  → Zod 驗證輸入 (lib/validations/*)
  → Drizzle 寫入
  → rcLog 寫入稽核日誌 (audit_log)
  → revalidatePath() → Next.js 重新渲染受影響 segment
  → 回傳 ActionResult ({ ok: true } / { error } / { fieldErrors })

CRUD 採 modal 不轉頁:建立/編輯開 shadcn Dialog,action 回成功(不 redirect),modal 關閉後列表以 revalidatePath + router.refresh() 刷新。詳見 API 參考Server Actions


Auth + RBAC

工牌登入 (Badge Login)

登入是 工牌編號(R#####)+ 密碼不是 email——沒有 OAuth、沒有 2FA(刻意排除)。 Auth.js v5 的 Credentials provider 以 bcrypt 比對(lib/password.ts)。

JWT session

Credentials provider 必須用 JWT session(不是 DrizzleAdapter 預設的 DB session)。登入時角色被 snapshot 進 JWT,設為 httpOnly cookie。

認證流程

1. 使用者訪問 /dashboard/*
2. proxy.ts (edge) → auth() → 無 session → redirect /login
3. 提交工牌登入表單
4. Auth.js handler (app/api/auth/[...nextauth]) 驗證 credentials(bcrypt compare)
5. 鑄造 JWT session(role snapshot 進 token);設 httpOnly cookie
6. Edge middleware 放行
7. Server Component 呼叫 auth() → 取 session → 以該使用者身分查 DB
8. RBAC 守衛 (lib/permissions.ts) 從 DB re-read live role

三層角色 + 10 旗標權限矩陣

角色定義於 lib/schema/auth.tstype Role = "admin" | "operator" | "staff")。權限矩陣 RC_PERM_MATRIXlib/rc-permissions.ts——10 個旗標的唯一真實來源(can(role, flag)):

旗標adminoperatorstaff
seeAll(可見全部案件)✗(僅被指派)
createCase(建立案件)
editPrereq(前置資料增改/標記)✓(僅被指派案件)
overridePct(覆寫他人進度 %)
editNodes(節點樣板/標記完成)
addSubcase(追加案增改刪/狀態)
manageUsers(帳號管理)
viewAudit(稽核日誌查閱/匯出)
manageTemplates(分類/樣板/自訂日曆)
deleteData(作廢/刪除類)

核心責任界線: 行政(operator)可「代操作」一切文書類動作,但 不能決定進度數字—— overridePct / editNodes 為 admin(技師)專屬。進度 % 是技師的專業判斷與責任。

getLiveRole() — 從 DB re-read 角色

伺服器端守衛(lib/permissions.tsrequireAuth / requireOperatorOrAdmin() / requireAdmin()不信任 JWT 裡的角色,而是用 getLiveRole(userId) 從 DB re-read 目前角色——所以降權立即生效, 不必等 session 過期。UI 隱藏不是安全控制;案件層級可見性(staff 僅見被指派案件)由 lib/rc-visibility.tscanSeeCase 在查詢/動作層收斂。


領域模型 (Domain Model)

六大分類 + 三進度模式

案件分 六大分類 A1–B3(公共工程 / 私人案 / 其他)外加自訂分類(Sn 碼),每類綁定三種進度模式之一:

模式進度怎麼算
節點式stepΣ 已完成節點的 weight;以 completeNode 推進
自填式self技師手填 % + 必填備註(updateProgress
彈性混合式hybrid節點為主,可用 addNode 動態增節點

母子追加案 (B2)

B2 分類支援母子追加案:母案下掛追加案(子案),由 addSubcase / editSubcase / subStatusChange / deleteSubcase 管理。追加案不計母案進度 %

前置資料 (Prerequisites)

案件可掛前置資料項,每項可標 critical、附 NAS 連結(link-only,不上傳檔案)。由 addPrereq / editPrereq / markPrereq(標記已到)/ deletePrereq 管理。

燈號預警 schedule engine

系統核心:把案件的 業主工作日 時程消耗轉成「燈號」,再推導每日通知與 dashboard。 純函式、零 db import、可單元測試(lib/rc-schedule-utils.tslib/rc-alerts.tslib/rc-holidays.tslib/rc-notify-utils.tslib/rc-dashboard-utils.ts)。

業主工作日曆(gov / biz / 24-7;國定假日 + 春節排除:rc-holidays)+ 合約起訖
        │  scheduleConsumptionPct(case, calendar)

   時程消耗 %  ──  alertLevelOf(timePct, status)  ──▶  燈號

        ├─▶ actions/notifications-scan.ts → rc-notify-utils → notifications  (每日 02:00)
        └─▶ dashboard pages + 6 charts    → rc-dashboard-utils

alertLevelOf(timePct, status)lib/rc-schedule-utils.ts)的判定:

條件燈號
status 已作廢void(短路)
status 已結案closed(短路)
status 尚未開始idle(短路)
timePct ≥ 90🔴 red
timePct ≥ 75🟠 amber(橙)
timePct ≥ 50🟡 yellow(黃)
timePct < 50🟢 ok(綠)

每日 02:00 in-app 掃描runNotificationScanCore,去重 upsert;runNotificationScan 為 Admin 手動觸發、 requireAdmin() 守衛),只發系統內通知鈴鐺,不寄 Email / LINE。完整 import-graph 詳見 docs/architecture/scheduling-alert-engine-map.md

稽核日誌 rcLog (immutable)

所有寫入動作都經 rcLog 寫入 audit_log(actor / action / target / 時間,索引於 actorIdcreatedAt)。日誌不可篡改、保存 10 年,僅 Admin 可查閱/匯出 CSV(exportAuditLog / viewAudit 旗標)。


Drizzle Schema 概覽

Schema 在 lib/schema/,匯入點 lib/schema/index.ts;Drizzle client lib/db.ts(lazy-init、postgres-js)。

檔案關鍵 tables
auth.tsusers(含 role enum)· accounts · sessions · verification_tokens · email_verification_tokens · password_reset_tokens
engineering.tscases · case_assignees · case_nodes · case_prereqs · case_subcases · progress_updates · clients · contacts · case_categories · node_templates · node_template_items · work_calendars
system.tsaudit_log(稽核)· notifications(通知鈴鐺)
items.ts模板遺留的 items(示範用,非瑞成核心領域)

Migrations: 生成於 next-app/drizzle/migrations/,目前 → 00120008 strip 掉 SaaS billing surfaces,0009 引入工程領域)。以 pnpm db:migrate 套用,永不手改生成的 SQL;schema 變更請改 Drizzle table 定義後重新生成。


相關文件

  • API 參考 — 兩種介面(HTTP Route Handlers + Server Actions RPC)
  • Server Actions Reference — 全 46 個 action 依領域分組 + 權限
  • Auth Endpoints — 工牌登入端點與三層角色
  • docs/architecture/scheduling-alert-engine-map.md — 燈號引擎 import graph(repo 內)

Released under the MIT License.