一句話:租戶層級設定子系統的 source of truth;新功能若要動設定(例如 團號 的代碼表、訂單派發 的派發模式)應 extend 本檔,而非各自描述。
畫面(UI = wireframe):
../apps/wireframes/app/admin/settings/page.tsx
設定是 租戶層級(tenant-scoped):每個租戶 DB 一列 singleton(tenant_settings.id = 'singleton'),整個租戶共用。
重要:本系統沒有 per-user 個人設定表。 使用者個人層級只有:
user 表,與商業表同庫;無共享 auth_db)member_profiles,見 會員)凡是「整個旅行社的設定」一律進 tenant_settings;凡是「某個人的偏好」目前無對應載體(YAGNI,真需要時再開 user_preferences)。
tenant_settings 單例表(全欄位在 packages/core/src/schema/business.ts;singleton 有 DB CHECK 硬約束 id = 'singleton',插不進第二列)。現行欄位(以 code 為準):
| 欄位語意 | 說明 |
|---|---|
id | 主鍵,恆 'singleton'(default + CHECK) |
company_name / company_phone / tax_id / company_address / logo_url | 公司抬頭(顯示於發票、請款單、Email 頁尾) |
site_tagline / seo_default_description / og_image_url | 站台級 SEO 預設(無頁面覆寫時 fallback) |
email_reply_to | 回覆地址;無 email_from 欄——寄件人 = sender_local_part@sending_domain 運行時組合(emails) |
sending_domain / resend_domain_id / sending_domain_status / sender_local_part | 自有寄件 domain 快照 + Resend 資源 id + 驗證狀態(CHECK enum)+ local-part(空值視為 info) |
assignment_mode(CHECK:manual_review | auto_round_robin | auto_customer_choice | auto_by_destination) | 訂單派發模式 |
default_assignee_user_id(FK user)/ allow_sales_claim | 派發兜底業務 / sales 自派搶單開關 |
group_code_pattern / group_code_seq_style(CHECK:alpha | alpha_no_io | numeric)/ group_code_seq_width | 團號租戶自訂 pattern 與序號樣式 |
ecpay_merchant_id / ecpay_secrets_enc(jsonb EncryptedField) | ECPay per-tenant 憑證(write-only、KMS envelope) |
travelinvoice_merchant_id / travelinvoice_secrets_enc(jsonb EncryptedField) | 代收轉付憑證(同 pattern,travelinvoice-integration) |
ecpay_fee_bps / fongshou_fee_bps / profit_tax_bps | 內外帳淨利試算費率(bps 整數) |
sales_commission_bps / monthly_top_bonus_bps | 排行榜佣金試算費率 |
passport_expiry_warning_days(integer default 180) | 證件效期 threshold(海外模組生效時才用到) |
updated_at | 最後更新 |
模組開通不在這裡:登山 / 海外模組「有沒有開通」是平台方決策,存
control_db.tenant(非tenant_settings),由 provisioning CLI 管。租戶端只能唯讀看自己方案含哪些模組。見 模組化 §3.1。
派發兩欄(
assignment_mode/default_assignee_user_id)已扶正進 drizzle schema(穩定業務欄位一律直接進schema/business.ts,見 系統總綱);setup-business-extras.ts現只 idempotent 建立 singleton 列、不再 ALTER 這兩欄。getTenantSettings仍以顯式欄位清單的 raw SQL 讀取(避免 schema diff 意外遮蔽)。
packages/core/src/settings/admin-repo.ts:
getTenantSettings(db): TenantSettingsRow — 讀 singleton,不存在則 lazy insertupdateTenantSettings(db, input) — 公司 / Email / 站台 SEO 欄位 partial update(只 patch 表單實際帶到的欄位,缺欄不動 → 各 tab 互不洗資料);SEO 三欄(site_tagline / seo_default_description / og_image_url)由 seo tab 的 updateSeoSettingsAction 呼叫updateAssignmentSettings(db, input) — 派發模式子集(單獨 action / form path)第一個參數一律 db: ScopedDb(branded type,見 系統總綱)。
/admin/settings(src/app/(admin)/admin/settings/page.tsx),17 個 tab(順序同 code):
Tab(value) | 可寫? | 後端 |
|---|---|---|
公司資料(company) | ✏️ 可編輯 | updateTenantSettingsAction |
訂單派發(assignment) | ✏️ 可編輯 | updateAssignmentSettingsAction(訂單派發) |
付款設定(payments) | 🔒 唯讀 + ✏️ 可編輯 | provider 顯示唯讀(PAYMENT_PROVIDER env-backed,改需重啟);ECPay 憑證(特店 ID / HashKey / HashIV)為 per-tenant 可寫表單(updateEcpayCredentialsAction / clearEcpayCredentialsAction),三欄一次填齊、write-only 加密存租戶 DB,HashKey/HashIV 永不回讀(僅末 4 碼遮罩供顯示),無租戶憑證時僅 staging(ECPAY_IS_STAGING=true)退回平台 env 過渡,prod 即 fail-closed(見下方註腳) |
收款帳戶(accounts) | ✏️ 可編輯 | 收款帳戶目錄 + 費用類型路由維護(SettingsAccountsPanel:receiving_accounts CRUD 新增 / 改名 / 改性質 / 設幣別 / 停用 + fee_category_routes 費用路由設定,receiving_account.manage)。租戶自管、product-agnostic(不綁產品線);外幣帳戶=外幣池。從 多帳戶對帳 / 子系統 B 移入(對帳頁專注對帳、右上 link 過來)。唯一對 accountant(非 admin)也可見的 tab。 |
費用類別(expense-categories) | ✏️ 可編輯 | 公司營業費用科目目錄 + 月損益分組(SettingsExpenseCategoriesPanel:expense_categories 新增 / 改名 / 停用;operating_expense.read 可見、operating_expense.manage 可寫)。admin + accountant 可見。見 公司營業費用 §2.1 |
損益分區(pnl-regions) | ✏️ 可編輯 | 月損益分區維護(SettingsPnlRegionsPanel)。進階會計模組 tab。見 公司營業費用 |
月固定費用(recurring-expenses) | ✏️ 可編輯 | 月固定費用範本維護(SettingsRecurringTemplatesPanel,每月生成 operating_expenses 列)。進階會計模組 tab,admin + accountant 可見。見 公司營業費用 |
內外帳費率(ledger-fees) | ✏️ 可編輯 | 內外帳「淨利試算」預估費率(updateLedgerFeeRatesAction 寫 ecpay_fee_bps / fongshou_fee_bps / profit_tax_bps,basis points 整數,0–10000)。見 內外帳 §2.3 |
郵件設定(email) | ✏️ 可編輯 | 不顯示任何 provider 憑證(Resend key 平台級 env、無 BYOK 欄位,決策見 emails §1.1);回覆 email_reply_to 與寄件 local-part(sender_local_part,預設 info)可編輯(updateTenantSettingsAction);自有網域寄件另配 SendingDomainActionForm。寄件 domain = 綁定站台 domain 派生,見 emails §2/§4 |
API / Webhook(api) | 🔒 唯讀 | 顯示 ECPay notify / return callback URL 與 App Base URL,供外部設定 |
方案模組(modules) | 🔒 唯讀 | 顯示平台授權的登山 / 海外 / 訂房模組(getTenantModules,來源 control_db.tenant.module_* 經 proxy header 注入);開通由平台 CLI 管、租戶不可自改 —— 只讓租戶管理者知道方案範圍。見 模組化 §3.1 |
PDPA / 同意書(consent) | 🔗 外連 | 非就地編輯;以 <Link href="/admin/consent"> 導向同意書範本管理頁(consent_templates 版本化由該頁維護) |
團號格式(group-code) | ✏️ 可編輯 | 租戶自訂團號 pattern 與代碼表。見 團號系統 |
SEO / 站台(seo) | ✏️ 可編輯 | updateSeoSettingsAction 寫 site_tagline / seo_default_description / og_image_url(站台級 SEO 預設,無頁面覆寫時 fallback;見 SEO/JSON-LD) |
外觀(appearance) | ✏️ 可編輯 | 後台介面色彩模式(ThemeModeControl,next-themes attribute="data-theme";自動跟隨系統 / 固定淺色 / 深色)· 純前端偏好,不寫 tenant_settings |
危險區域(danger) | ⚠️ 操作 | purgeTestData:TRUNCATE CASCADE 訂單 / 付款 / 旅客 / 同意書簽署 / 稽核紀錄(保留 trips / departures / consent_templates);繞過 append-only trigger(TRUNCATE 不 fire row-level trigger) |
店面(storefront) | ✏️ 可編輯 | 前台店面模板選擇(tenant_settings.storefront_template,儲存前可預覽 —— role.manage 鑄短效 signed token、前台 host 兌換後 draftMode + 簽名 cookie(split-host 下 cookie/session 不跨 host,token handoff 是唯一通路),預覽不進公開 cache)+內容槽編輯(storefront_contents 只存覆蓋值,預設住模板定義;圖片欄位走圖片選擇器)。admin-only,write action 掛 role.manage。設計 SSOT 見 店面模板 |
進階會計模組閘(accounting-module §5/§6):收款帳戶(
accounts,含費用路由)/ 費用類別(expense-categories)/ 內外帳費率(ledger-fees)三個 tab 屬module_accounting。accountingActive(modules)為 false 時三個 tab(trigger + content)server 端不渲染、目錄資料不抓,其 write action 亦掛requireAccountingModule()fail-closed。accountant 於 module OFF 無任何可管 tab → 顯示「尚未開通進階會計模組」空狀態;admin 仍看得到其餘非會計 tab。
權限:進站閘 = requirePermission('receiving_account.manage')(admin 走 wildcard、accountant 通過;op / sales 被擋在整頁外)。不再是舊的整頁 role.manage —— F1 把收款帳戶管理從對帳頁移入本頁後,若仍用 role.manage(admin only)會把持有 receiving_account.manage 的 accountant 一併擋在整頁外。進站後再用 isAdmin(hasPermissionForRole(..., 'role', 'manage'))二次守門敏感 tab:admin 看全部 tab;accountant 只看得到「收款帳戶」+「費用類別」等會計 tab(後者再 gate operating_expense.read),其餘 admin-only tab 不渲染、props 不外漏(defaultValue 對 accountant 落在 accounts)。寫入 action 各自 requirePermission(敏感 tab 對應 role.manage,帳戶 / 費用類別對應 receiving_account.manage / operating_expense.manage)。每次寫入記 audit_log(tenant_settings.update / tenant_settings.assignment_mode.change 等)。
付款憑證已 per-tenant(KMS envelope 加密存租戶 DB,
/admin/settings付款 tab 自填 write-only)。郵件憑證決議維持平台級(寄信是平台代履行的服務職責,與「錢進租戶帳戶」的 ECPay 本質不同)—— per-tenant 的是寄件身分(email_from/email_reply_to/ 自有寄件 domain),見 emails。
control_db,provisioning CLI),settings 只唯讀顯示方案範圍 —— 不是租戶可自改的設定tenant_settings.feature_* 欄,透過 CLI 改(見 memory wildtw-phase2-multitenant-strategy)@better-auth/api-key、PAT 式綁使用者、單一 Authorization: Bearer header 以 toko_sk_ 前綴分派、api_key.manage);MVP 建立限 admin(settings 既有進站閘不動;開放 sales / op 自建連同 UI 入口列 tenant-cli §11 開放議題)。wireframe 已有(settings 線稿「開發者」tab),code 未實作。資料不進 tenant_settings(key 存 plugin 的 apikey 表、CLI session 即 better-auth session 表)