手冊首頁 / 租戶與組織 / 租戶管理

租戶管理(Tenant Manage)

功能群:系統管理|三層 RBAC 權限模型 / 租戶部門使用者組織模型先讀 功能群總覽

事實基準:2026-07-05 從 FE / BE code 掃出(tenants 表為 jedi-auth 套件擁有,主專案只有薄 Route 層 + 一個本地 orchestration service);API schema 逐條對照 marshmallow serializer 與 jedi-auth service 組裝碼

1. 功能描述

租戶管理頁是多租戶架構頂層主檔的維運介面:public.tenants 是全系統租戶樹的根節點來源(parent_id / path 樹狀),部門(org_units)、使用者、角色、專案等幾乎所有租戶層資料最終都掛在某個 tenants 列下。本頁提供扁平清單 CRUD(非樹狀元件),透過「隸屬租戶」欄位文字呈現階層關係。

新增租戶時,BE 會連帶自動 seed 一個預設部門「System Administrator」與一個擁有租戶級全部權限的預設角色「System Manager」(見 §6.2、§11),讓新租戶開箱即有可用的管理起點;本期不會自動建立管理員帳號(帳號建立需真實 email,留待平台管理者事後手動新增,見 app/auth/service/tenant_provisioning_service.py:40-41)。

主要使用者:平台 / 系統管理層(super_admin 或已具備租戶管理能力點者)。BE 端點僅檢查 JWT 登入,不在 service 層額外做角色檢查——可見性完全依賴 RBAC 選單(ui_routes.name='tenant-manage',見 _overview §5)。

角色速覽

角色 一句話
平台 / 系統管理層 本頁唯一使用者——新增/編輯/刪除租戶;BE 僅檢查登入,實務上完全靠 RBAC 選單擋非管理員看到入口

1.1 功能總覽(本頁全部功能)

# 功能 說明 位置 詳述
1 租戶清單(分頁 / 排序 / 搜尋) 扁平 DataTable,name 欄位 ILIKE 模糊搜尋 + 名稱/隸屬租戶多欄排序 主畫面 DataTable §6.1
2 新增租戶 名稱(必填)+ 隸屬租戶(選填 Dropdown)+ 描述(選填);連帶 seed 預設部門與管理員角色 Header「新增」→ Dialog UC-TN-01
3 編輯租戶 修改名稱 / 隸屬租戶 / 描述;下拉排除自己(防自我循環) 逐列「編輯」→ Dialog UC-TN-01
4 刪除租戶 二次確認後硬刪除;有子租戶會 ORM 層連帶刪除,有 org_unit/role/user 參照則被 DB FK 擋下 逐列「刪除」 UC-TN-02
5 重置查詢 清空搜尋關鍵字、排序,回到第一頁 Header「重置查詢」 §6.1
6 隸屬租戶下拉(tenantMenu 全域 pinia store menuStore.fetchTenantMenu(),新增/編輯儲存成功後主動刷新 Dialog 內 Dropdown §7

UC(§2)展開 1(新增,含連帶 seed)與 2(刪除,含 FK 限制)這兩條核心操作路徑;編輯屬常規 CRUD,已於總覽表描述完整;搜尋/排序/重置查詢為標準分頁清單行為。

2. Use Case

角色速覽

角色 一句話
平台 / 系統管理層 本頁唯一使用者——新增/編輯/刪除租戶;BE 僅檢查登入,實務上完全靠 RBAC 選單擋非管理員看到入口

流程圖視覺慣例:菱形 = 判斷、橘底 = 例外/擋下、紅框 = 錯誤(標 error code)、綠框 = 成功終點、虛線 = 可選路徑。

UC-TN-01 新增 / 編輯租戶

項目 內容
角色 平台 / 系統管理層
前置條件 已登入;tenant-manage 選單可見
產出 / 後置條件 新增:tenants 新增一列 + 連帶 org_units(System Administrator)+ roles(System Manager,扣停用模組後的 capability 全集);編輯:tenants 該列 name/parent_id/description 更新(path 不重算,見 §12 坑 1)
tenant_uc_create start open 開「新增」Dialog (隸屬租戶下拉=tenantMenu 全部選項) start->open input 填寫名稱(必填)+ 隸屬租戶/描述(選填) open->input valid FE 前端驗證: 名稱非空? input->valid fix 紅字提示,聚焦名稱欄位 valid->fix submit 送出 POST /tenant valid->submit fix->input jwt BE 檢查:JWT 已登入? (無角色守門,見 §3) submit->jwt deny 401 未登入 jwt->deny parentDefault parent_id 未帶(含顯式 null) → 補建立者自己的 tenant_id (子租戶不會誤建成 root) jwt->parentDefault dupCreate BE 檢查:租戶名稱 是否全域重複? parentDefault->dupCreate dupErr AUTH_404005(語意其實是重複, 共用錯誤碼,見 §12 坑 4) dupCreate->dupErr 重複 seed 建租戶+連帶 seed: 預設部門「System Administrator」+ 預設角色「System Manager」(capability 全集) 全程單一 @transaction,任一步失敗整批 rollback dupCreate->seed 不重複 refresh 重新整理清單 + 重抓租戶選單 (menuStore.fetchTenantMenu) seed->refresh
UC-TN-01a 新增流程:parent_id 預設補建立者租戶 + 全域名稱重複檢查 + 連帶 seed 部門與角色(單一 transaction)
tenant_uc_edit start open 開「編輯」Dialog (隸屬租戶下拉排除自己, 避免自我循環) start->open input 修改名稱/隸屬租戶/描述 open->input valid FE 前端驗證: 名稱非空? input->valid fix 紅字提示,聚焦名稱欄位 valid->fix submit 送出 PUT /tenant/{uid} valid->submit fix->input jwt BE 檢查:JWT 已登入? (無角色守門,見 §3) submit->jwt deny 401 未登入 jwt->deny existCheck BE 檢查:uid 對應 租戶是否存在? jwt->existCheck notFound AUTH_404005 租戶不存在 existCheck->notFound 不存在 nameChanged 名稱有變更? existCheck->nameChanged 存在 dupUpdate BE 檢查:新名稱 是否與他租戶重複? nameChanged->dupUpdate updateNoPath 更新 name/parent_id/description (⚠️ path 欄位不重算, 見 §12 坑 1:改隸屬租戶後 RLS 子樹判定可能與畫面顯示不一致) nameChanged->updateNoPath dupErr AUTH_404005(同上共用錯誤碼) dupUpdate->dupErr 重複 dupUpdate->updateNoPath 不重複 refresh 重新整理清單 + 重抓租戶選單 (menuStore.fetchTenantMenu) updateNoPath->refresh
UC-TN-01b 編輯流程:存在性檢查 + 名稱變更才查重,更新不重算 path

UC-TN-02 刪除租戶

項目 內容
角色 平台 / 系統管理層
前置條件 已登入;目標租戶 uid 存在
產出 / 後置條件 成功:tenants 該列(及 ORM cascade 的全部子孫租戶列)被刪除;失敗:仍有 org_units/roles/users(預設租戶) 參照時,DB FK RESTRICT 擋下、回 409
tenant_uc_delete start row 清單逐列點「刪除」 start->row confirm 彈二次確認框 row->confirm del DELETE /tenant/{uid} confirm->del jwt BE 檢查: 僅 JWT?(⚠️ 無角色檢查, 見 §3) del->jwt exist BE 檢查: uid 對應租戶存在? jwt->exist 通過 notfound AUTH_404005 租戶不存在 exist->notfound 不存在 hasChild 是否有子租戶? (ORM children 關聯) exist->hasChild 存在 cascadeChild ORM cascade="all,delete-orphan": 子租戶(含孫層)連帶硬刪除 ⚠️ 與 DB FK ON DELETE SET NULL 語意不同, 只在走 ORM session.delete() 時成立,見 §12 坑 2 hasChild->cascadeChild hasRefs 是否仍有 org_units / roles / users(預設租戶) 指向本租戶? (皆 FK ON DELETE RESTRICT) hasChild->hasRefs cascadeChild->hasRefs fkErr 409 Integrity/ForeignKeyViolation (原始 SQL 錯誤字串, 非 AUTH_ 語意化 error_code,見 §12 坑 3) hasRefs->fkErr 有殘留參照 ok 刪除成功,重新整理清單 + 重抓租戶選單(fetchTenantMenu) hasRefs->ok 無殘留
UC-TN-02 流程:有子租戶走 ORM cascade 連帶刪除、有 org_unit/role/user 參照走 FK RESTRICT 擋下(409 原始 SQL 錯誤,非語意化 error_code)

3. 權限矩陣

操作 FE 判定 BE 強制 BE 檢查位置
選單 / 頁面可見性 RBAC 選單(ui_routes.name='tenant-manage',掛於 group-tenant-org 群組) 無獨立端點層守門,靠 RBAC 選單/capability 過濾 scripts/sql/2026-05-26-menu-group-restructure.sql:87(route 掛載);_overview §5
讀清單 / 選單 無額外前端角色判斷 ⚠️ @jwt_required() api/auth/routes/tenant_route.pyTenantsRoute.post / TenantMenuRoute.get / TenantRoute.get
新增 / 編輯 / 刪除 無額外前端角色判斷(頁面本身即假設只有管理員進得來) ⚠️ @jwt_required(),service 層不做角色檢查 api/auth/routes/tenant_route.pyTenantRoute.post/put/delete)→ jedi_auth.app.service.tenant_service.TenantService

⚠️ 已知設計決策(與本群其餘管理頁一致):本頁與部門管理 / 設備管理等系統管理群頁面同一基調——刻意不在 service 層加角色檢查,完全依賴 RBAC 選單/capability 層擋非管理員使用者看到入口(見 _overview §5)。若使用者已知 API path,任何登入者理論上都能新增/編輯/刪除任一租戶,包含建立 root 層級租戶(parent_id 顯式傳 null 且自己無 tenant_id 時)。

4. 狀態機與前置條件

本頁無輪次 / 階段狀態機,租戶本身也無業務狀態欄位(無 enable/disable,tenants 表沒有狀態欄位)。相關的資料完整性前置條件:

條件 效果 出處
新增時 parent_id 未帶(含顯式 null BE 自動補上建立者自己的 ctx.tenant_id;子租戶不會誤建成 root(root 需 super_admin 且自身 tenant_id 為 None) api/auth/routes/tenant_route.py:89-95
新增租戶名稱 全域唯一(跨租戶不可重名),違反拋 NotFound(HTTP 404)但 error_code 為 AUTH_409006(見 §12 坑 4) jedi_auth/domain/service/tenant_domain_service.py:183-191verify_tenant_name_duplicate
編輯租戶名稱 僅當名稱有變更才觸發重複檢查(未改名稱不查) 同檔 :122-142update_tenant
刪除租戶 目標 uid 須存在,否則 404;存在但仍有 org_units/roles/users(預設租戶) 參照 → DB FK ON DELETE RESTRICT 擋下,回 409 jedi_auth/infra/models/org_unit.py:26role.py:46user.py:48
租戶 path 欄位 僅在 BEFORE INSERT 由 DB trigger trg_tenant_path_insertset_tenant_path_insert_only())計算;UPDATE 不重算 docs/system-design/database/erd/tbls/public.tenants.md(Triggers 段)

5. UI 設計

📸 實際畫面截圖待補(一律亮色模式拍攝):以 playwright 對 STG 擷取,預計補以下兩張到 assets/img/: ① 整頁(搜尋列 + DataTable 清單,含隸屬租戶欄)tenant-overview.png ② 新增 / 編輯租戶 Dialog(含隸屬租戶 Dropdown) tenant-edit-dialog.png

版面骨架(單一 DataTable + 新增/編輯 Dialog,無左側樹、無 Tab、無批次操作):

Header:名稱模糊搜尋(ILIKE)/重置查詢/新增租戶 GET /tenants/menu(隸屬租戶下拉,Dialog 用) 開啟:§6 API 規格 租戶 DataTable:名稱/隸屬租戶(parent_name)/描述;扁平清單、無左樹 POST /tenants(分頁清單,filters.name ILIKE) 逐列「編輯」「刪除」;階層關係僅以文字欄呈現(無樹狀元件) → §6 API 規格(點我開啟) 開啟:UC-TN-01 新增/編輯租戶 新增/編輯 Dialog:名稱*/隸屬租戶(Dropdown,排除自己)/描述 POST /tenant(新增,連帶 seed 部門+角色) PUT /tenant/{uid}(更新,path 不重算) 名稱全域唯一;未帶 parent_id 補建立者租戶 → UC-TN-01(點我開啟) 開啟:UC-TN-02 刪除租戶 刪除租戶:二次確認後硬刪除(無批次操作) DELETE /tenant/{uid} 有子租戶→ORM cascade 連帶刪除 有 org_unit/role/user 參照→FK RESTRICT 擋下(409) → UC-TN-02(點我開啟) 彩色=主要互動/明細區塊,整塊可點擊跳轉本頁 §6 或對應 UC|灰=本頁結構|虛線框=dialog/條件顯示層 API 僅列代表性 2~3 條,完整規格見 §6;本頁無左側樹狀導覽、無批次操作
租戶管理頁版面:Header 搜尋/重置/新增、DataTable(可跳 §6)、新增編輯 Dialog(可跳 UC-TN-01)、刪除(可跳 UC-TN-02)

狀態呈現:清單載入時 DataTable loading 遮罩(tenantLoading);儲存 / 取消按鈕共用 submitted loading 狀態;無 socket,操作後皆重打 getTenants() + menuStore.fetchTenantMenu() 重新整理。無左側樹狀導覽——階層關係僅透過「隸屬租戶」欄文字顯示(parent_name,無則顯示「無」)。

6. API 規格

Envelope:成功 {"status": true, "data": …}(清單另帶 "meta": {...})、失敗經 jedi_common.handler.handler.register_error_handlers 轉為 {"error_code": "…", "msg": "…"}(HTTP 4xx/5xx)。出處:common/util/response_util.py:return_response

6.1 總清單(本頁涉及的全部 endpoint)

分類 Method + Path 說明 完整規格
選單 GET /tenants/menu 租戶下拉選單(Dialog「隸屬租戶」用),僅 {id, uid, name} §6.2
清單 POST /tenants 分頁清單(DataTable 資料來源),filters.name 走 ILIKE 模糊比對 §6.2
CRUD GET /tenant/{uid} 取單筆(本頁未直接呼叫,供他頁 / 除錯用) GAI-SD-02
CRUD POST /tenant 新增租戶(連帶 seed 部門與角色) §6.3
CRUD PUT /tenant/{uid} 更新租戶(名稱 / 隸屬租戶 / 描述) §6.3
CRUD DELETE /tenant/{uid} 刪除租戶 §6.3

6.2 選單 / 清單類

[GET] /tenants/menu

無 query 參數。Response data[](每項一個租戶,僅三欄位):

欄位 型別 說明
uid / name string 租戶識別 / 名稱

出處:api/auth/routes/tenant_route.py:25-38TenantMenuRoute.get,用 TenantResponse(only=["uid", "name"]))。

註:FE menuStore 消費的 tenantMenu 實際上還帶 id / parent_idjedi_auth.app.dto.tenant_menu.TenantMenuDTO),但本頁 Route 層 marshal 時用 only=["uid","name"] 裁切,Dropdown 綁定 optionValue="id" 卻拿不到 id 欄——見 §12 坑 5。

[POST] /tenants

Request(RequestMetaSchema + filters 巢狀):

欄位 型別 必填 說明
pager.page / pager.page_size / pager.with_total int / int / bool 分頁參數
sort[] array {field, direction} 多欄排序
filters.name string ILIKE 模糊比對(_get_pager_list_query 走 like filter,非精確比對)

Response data[](每項一個租戶):

欄位 型別 說明
id / uid int / string 內部 ID(Dropdown optionValue 用)/ 對外識別碼
name string 租戶名稱
parent_id / parent_uid / parent_name int|null / string|null / string|null 隸屬租戶三種型式(DB column_property 算出 parent_name/parent_uid
parent object|null 巢狀父租戶(同 TenantResponse 形狀,遞迴一層)
path string 租戶樹路徑(/1/2/ 格式),本頁清單未顯示但回應內含
description string|null 描述
metadata_json object 租戶 metadata(JSONB,本頁未使用)
created_user / created_at / updated_user / updated_at 審計欄(created_user/updated_user 為 login_name,未 enrich nickname,見 §12 坑 6)

出處:api/auth/serializers/tenant.py:36-54TenantResponse);組裝邏輯 jedi_auth/app/service/tenant_service.py:22-40get_tenants_and_pager)。

6.3 CRUD 寫入類

[POST] /tenant

Request:

欄位 型別 必填 說明
name string 租戶名稱,全域唯一
parent_id int 否(allow_none) 隸屬租戶;省略或顯式 null → BE 補建立者自己的 tenant_id
description string 描述
path / metadata_json string / dict schema 層存在但本頁表單未提供輸入(path 由 DB trigger 算,送了也會被 INSERT 覆蓋)

Response:同 §6.2 GET 單筆形狀(TenantResponse)。出處:api/auth/serializers/tenant.py:23-28TenantRequest)、api/auth/routes/tenant_route.py:72-99TenantRoute.post)、app/auth/service/tenant_provisioning_service.py:56-85add_tenant_with_defaults)。

[PUT] /tenant/{uid}

Request:同 TenantRequestname 技術上仍是 required=True,因此編輯時省略 name 會 400,並非部分更新語意)。

Response:同上。不存在的 uidAUTH_404005。出處:jedi_auth/app/service/tenant_service.py:96-113update_tenant)。

[DELETE] /tenant/{uid}

無 body。Response datatrue。不存在的 uidAUTH_404005;存在但仍被 org_units/roles/users FK 參照 → 409(見 §12 坑 3)。出處:jedi_auth/app/service/tenant_service.py:115-127delete_tenant)。

7. 前端檔案地圖(compliance-manager-fe/)

檔案 角色
src/views/tenant-manage/TenantManage.vue 唯一檢視檔(337 行;DataTable + 新增/編輯 Dialog 全在此,無子元件)
src/config/router/index.js:580-593 route tenant-manage,path /tenant/tenant-manage
src/config/api/api.js:343-345 TENANTS(清單)/ TENANT(CRUD)/ TENANT_MENU(下拉)常數
src/service/BaseService.js 本頁未使用專屬 service 檔,直接用 BaseService 泛型 get/post/put/delete
src/composables/useTableQuery.js 分頁 / 排序 / 全域搜尋 composable(allowFilterFields=['name']
src/stores/queryStore.js 查詢狀態持久化(依 route scope + userId 區隔)
src/stores/menuStore.js:143-146 fetchTenantMenu(),Dialog「隸屬租戶」下拉資料來源
src/config/locales/i18n/{zh-tw,en}/tenant-manage.json 頁面 i18n(19 行,含表單驗證文案)
src/config/locales/i18n/{zh-tw,en}/menu.json 選單顯示名稱「租戶管理」(tenant-manage key)

8. 後端檔案地圖(本頁核心鏈路)

鏈路 Route App Service 底層
租戶選單 api/auth/routes/tenant_route.py::TenantMenuRoute(GET) jedi_auth.app.service.tenant_service.TenantService::get_tenants_menu jedi_auth.domain.service.tenant_domain_service.TenantDomainServicejedi_auth.infra.repository.tenant_repo_impl.TenantRepoImpl::get_tenants_menu(繼承 BaseRepositoryImpl
租戶清單(分頁) TenantsRoute(POST) TenantService::get_tenants_and_pager TenantDomainService::get_tenants_and_pagerTenantRepoImplBaseRepositoryImpl.get_all_by_fields_and_pager,ILIKE like-filter)
租戶單筆 TenantRoute(GET) TenantService::get_tenant_by_uid 同上
新增租戶 TenantRoute(POST) 主專案本地 app/auth/service/tenant_provisioning_service.py::TenantProvisioningService::add_tenant_with_defaults(orchestration:建租戶 + seed 部門 + seed 角色,單一 @transaction 內部依序呼叫 jedi-auth TenantService.add_tenant / OrgUnitService.add_org_unit / CapabilityService.get_capabilities / RoleService.add_role
更新租戶 TenantRoute(PUT) TenantService::update_tenant TenantDomainService::update_tenantTenantRepoImpl.update不含 path 重算
刪除租戶 TenantRoute(DELETE) TenantService::delete_tenant TenantDomainService::delete_tenantTenantRepoImpl.delete_by_uid(ORM session.delete(),觸發 children cascade)

DDD 提醒:新增租戶的 orchestration(TenantProvisioningService)是本專案少數主專案薄殼 + 外部套件重服務的鏈路——route 層仍不碰 DB、@transaction 掛在主專案 service,內部呼叫的 4 個 jedi-auth service 各自 @transaction 皆 reuse 同一 session(reentrant,見該檔 docstring)。若新增本頁類似的「跨套件 orchestration」,比照此檔寫法:tenant_id 一律顯式帶入,不依賴 RLS session 推斷(因為租戶管理本身是 super-admin 操作,RLS 對其可見全部租戶,推斷不可靠)。

9. DB

9.1 資料表總清單(本頁讀寫的全部表)

讀/寫 說明 欄位詳述
public.tenants 租戶主檔(parent_id / path 樹狀),全系統租戶樹根節點 §9.3
public.org_units (新增租戶時 seed 一筆) 新租戶預設部門「System Administrator」 GAI-SD-03
public.roles (新增租戶時 seed 一筆) 新租戶預設角色「System Manager」 GAI-SD-03
public.role_capabilities (新增租戶時批次 seed) 新角色綁定 capability 全集(扣停用模組) GAI-SD-03
public.capabilities 取全域 capability 清單供 seed 角色使用 GAI-SD-03

本頁 UI 只直接操作 tenants;後四張表只在「新增租戶」的連帶 seed 流程被寫入,不在編輯/刪除/清單路徑出現。

9.2 ER 圖(本頁讀寫範圍,按新增 / 刪除兩條路徑分張)

tenant_er tenant public.tenants (id / uid / name / parent_id / path) 本頁唯一直接讀寫的表 tenant->tenant parent_id 自我參考 (DB FK: ON DELETE SET NULL; ORM cascade: delete-orphan,見 §12 坑 2) orgunit public.org_units (tenant_id RESTRICT) 新增租戶連帶 seed 「System Administrator」 tenant->orgunit 新增時 orchestration 連帶寫入 (非 FK,見 §8) role public.roles (tenant_id RESTRICT) 新增租戶連帶 seed 「System Manager」 tenant->role 新增時 orchestration 連帶寫入 rolecap public.role_capabilities 新增租戶連帶批次寫入 (capability 全集扣停用模組) role->rolecap 綁定 capability 全集 cap public.capabilities 讀取全域能力點清單 (供 seed 角色使用) cap->rolecap
租戶管理 ER(新增路徑):tenants 自我參考樹狀(parent_id/path);新增租戶連帶寫入 org_units + roles + role_capabilities(虛線=orchestration 連帶寫入,非同表 FK)
tenant_er_delete_fk tenant public.tenants (id / uid / name / parent_id / path) orgunit public.org_units (tenant_id) orgunit->tenant FK RESTRICT (租戶下有部門則刪除擋下) role public.roles (tenant_id) role->tenant FK RESTRICT users public.users (預設 tenant_id) users->tenant FK RESTRICT(預設租戶) utenant public.user_tenants (tenant_id) utenant->tenant FK CASCADE urole public.user_roles (tenant_id,授權範圍) urole->tenant FK CASCADE(授權範圍)
租戶管理 ER(刪除路徑):org_units / roles / users(預設租戶) 皆 FK RESTRICT 指向 tenants,刪除若有殘留參照即擋下;user_tenants / user_roles 為 FK CASCADE

9.3 核心表欄位(取自 DEV DB dump,含 DB comment)

public.tenants — 租戶主檔

DB comment:「租戶表:定義多租戶架構中的租戶及其層級關係」。

欄位 型別 說明
id integer 內部 PK(nextval('tenants_id_seq')
uid varchar 對外識別碼(gen_random_uuid(),nullable 但實務恆有值)
name text 租戶名稱,NOT NULL;全域唯一(app 層檢查,非 DB unique constraint)
parent_id integer|null 父租戶 ID;FK tenants_parent_id_fkeyREFERENCES tenants(id) ON DELETE SET NULL
path text NOT NULL;/1/2/ 格式;僅 BEFORE INSERT trigger trg_tenant_path_insertset_tenant_path_insert_only())計算,UPDATE 不重算(見 §12 坑 1)
metadata jsonb NOT NULL,預設 '{}'::jsonb;本頁表單未使用
description text|null 描述
created_at / created_user / updated_at / updated_user 審計欄(created_user/updated_user 為 varchar(50) login_name,本頁 API 未 enrich nickname,見 §12 坑 6)

索引idx_tenants_path(btree)、idx_tenants_path_prefixtext_pattern_ops,前綴比對用)、idx_tenants_path_trgmgin_trgm_ops,模糊搜尋用)——皆為支援 RLS path LIKE 'prefix%' 子樹判定與清單模糊搜尋而建。

FK 對照(其他表指向 tenants.id 的刪除行為,決定本頁刪除能否成功)

來源表 FK 刪除行為 對租戶刪除的影響
tenants.parent_id(自我參考) ON DELETE SET NULLDB 層語意 若走原生 SQL 刪除,子租戶只會斷開父層連結,不會被刪
public.org_units.tenant_id ON DELETE RESTRICT 租戶下仍有部門 → 刪除擋下(409)
public.roles.tenant_id ON DELETE RESTRICT 租戶下仍有角色 → 刪除擋下(409)
public.users.tenant_id(預設租戶) ON DELETE RESTRICT 仍有使用者以此為預設租戶 → 刪除擋下(409)
public.user_tenants.tenant_id ON DELETE CASCADE 使用者-租戶關聯列隨刪除自動清除
public.user_roles.tenant_id(授權範圍) ON DELETE CASCADE 該租戶範圍的角色指派列隨刪除自動清除

⚠️ 本頁實際刪除路徑不是走原生 SQL、是走 ORM session.delete()——ORM 層 Tenant.children 關聯設了 cascade="all,delete-orphan",會在 flush 時把子租戶(含孫層)遞迴刪除,與上表 DB FK 語意的 ON DELETE SET NULL 不一致(DB 語意只在繞過 ORM 直接下 SQL 時生效)。詳見 §12 坑 2。

10. 頁面邏輯與資料對應

載入時序onMounted 平行觸發 menuStore.fetchTenantMenu()(Dropdown 選單)與 getTenants()(清單),兩者互不等待;getTenants() 內部 try/finally 自行關閉 tenantLoading。使用者切換(userInfo.value.uid 變更)時自動重新查詢一次。

關鍵欄位對應(API ↔︎ 畫面):

畫面元素 FE state API 欄位
清單「名稱」欄 tenants[].name data[].name
清單「隸屬租戶」欄 tenants[].parent_name(無則顯示「無」) data[].parent_name(DB column_property 算出)
清單「描述」欄 tenants[].description data[].description
全域搜尋框 filterTable.global.value POST /tenants body filters.name(ILIKE)
Dialog「隸屬租戶」下拉 filteredTenant(= tenantMenu 排除自己) GET /tenants/menu 回傳陣列,optionValue="id"
Dialog 儲存 payload currentData.{name, description, parent_id} POST/PUT /tenant body(不含 uid,uid 只用於決定 PUT 路徑與 URL 拼接)

儲存流程onSaveClick 先跑前端必填驗證(vuelidate,僅 name required);驗證通過後依 currentData.uid 是否存在分流 create() / update();成功後皆 getTenants() 重抓清單 + menuStore.fetchTenantMenu() 刷新下拉(新增/改名/刪除都需要同步下拉,因為租戶名稱可能被其他頁面的租戶選單引用)+ toast 提示 + 關閉 Dialog。

錯誤對應:BE error envelope → toast 顯示。本頁常見碼:AUTH_404005(租戶不存在)、AUTH_409006(租戶名稱重複——拋 NotFound/HTTP 404 但碼為 409006,與不存在是兩個獨立常數,見 §12 坑 4)、409(Integrity/ForeignKeyViolation,刪除有殘留參照時,訊息為原始 SQL 錯誤字串而非語意化 error_code,見 §12 坑 3)。

11. 背景行為與外部依賴

類型 內容
通知 無——本頁新增/編輯/刪除均不觸發任何通知
Socket 無 socket / 輪詢;操作後靠前端主動重新 getTenants() / fetchTenantMenu()
連帶 seed(新增租戶時) 建租戶 → 建預設部門「System Administrator」→ 取 capability 全集扣除 TENANT_ADMIN_EXCLUDED_RESOURCE_TYPEScruise-project/dashboard/workflow/resource/report)→ 建預設角色「System Manager」並綁定上述權限集;全程單一 @transaction,任一步失敗整批 rollback,不留半套
jedi-* 套件 jedi-authTenantService/TenantDomainService/TenantRepoImpl/OrgUnitService/CapabilityService/RoleService 皆屬此套件;主專案僅有薄 Route 層與 TenantProvisioningService 這一個本地 orchestration)
系統參數 無 feature flag;TENANT_ADMIN_EXCLUDED_RESOURCE_TYPES 為程式碼常數(非資料庫可調參數),若日後啟用新模組需改此常數

12. 邊界情況與已知坑

  1. 編輯(PUT)改變 parent_idpath 不重算path 欄位只有 trg_tenant_path_insert 這個 BEFORE INSERT trigger 會算,TenantRepoImpl.update()jedi_auth/infra/repository/tenant_repo_impl.py:35-51)沒有任何 path 相關邏輯。移動租戶到新的隸屬租戶後,該租戶(及其全部子孫)的 path 仍是舊值——這會讓 RLS 的子樹判定(tenants_select policy 用 path LIKE 'prefix%')與畫面上顯示的新「隸屬租戶」不一致:使用者可能改了隸屬租戶,但實際的租戶隔離範圍(誰看得到誰)並未跟著變。目前 UI 沒有任何提示告知此限制。
  2. 刪除的 ORM cascade 與 DB FK 語意不一致Tenant.children relationship 設了 cascade="all,delete-orphan"jedi_auth/infra/models/tenant.py:44-46),但 DB 層 tenants_parent_id_fkeyON DELETE SET NULL。本頁刪除走的是 ORM session.delete()BaseRepositoryImpl.delete_by_uid),因此實際行為是子孫租戶被連帶硬刪除,不是 FK 語意上的「子租戶變成無父(root)」。若日後有維運腳本改用原生 SQL 刪除同一張表,行為會與 UI 操作不一致,需特別注意。
  3. 刪除失敗時的錯誤訊息不友善org_units/roles/users.tenant_id 皆為 ON DELETE RESTRICT,刪除仍被參照的租戶會拋 IntegrityError/ForeignKeyViolation,經 jedi_common.handler.sql_exception.sql_error 映射為 HTTP 409,但 msg 是原始 SQL 錯誤字串第一行(如 update or delete on table "tenants" violates foreign key constraint...),error_code 也只是數字 409,不是 AUTH_/GRC_ 風格的語意化碼——前端 toast 會直接顯示這段英文 SQL 訊息給使用者,非本地化、也不告知「請先移除底下的部門/角色/使用者」。
  4. 名稱重複拋 NotFound(HTTP 404),error_code 與租戶不存在不同verify_tenant_name_duplicatejedi_auth/domain/service/tenant_domain_service.py:183-191)名稱重複時 raise NotFound(ErrorCode.AUTH_TENANT_NAME_DUPLICATE)——用 NotFound 例外類(HTTP 404)是 docstring 註記的「共用 404 型別避免洩漏細節」,但 error_code 字串是 AUTH_409006AUTH_TENANT_NAME_DUPLICATE = ("租戶名稱重複", "AUTH_409006")jedi_auth/common/enum/error_code.py:37),與「租戶不存在」的 AUTH_404005AUTH_TENANT_NOT_FOUND,同檔:27)是兩個各自獨立的常數。因此前端憑 error_code 區分兩種情境(404005=不存在、409006=名稱重複),只是 HTTP 狀態同為 404;型別(NotFound/404)與碼語意(409)不一致這點值得留意。
  5. GET /tenants/menu 回應被裁切成兩欄,但 Dropdown 綁定第三欄:Route 層 marshal_with(TenantResponse(only=["uid", "name"])) 只留 uid/name,但 FE Dialog 的 DropdownoptionValue="id"TenantManage.vue:312)。實測需確認 marshmallow 的 only 是否真的濾掉 id(若濾掉,Dropdown 選中值會是 undefined,儲存時 parent_id 送空)——這是本次盤點發現但未實機驗證的存疑項,見開發與驗證段待辦。
  6. 審計欄位未 enrich nicknamecreated_user/updated_user 回傳的是 login_name,本頁未依專案規範(CLAUDE.md「審計欄位回傳規範」)額外提供 created_user_name/updated_user_name(nickname)——但本頁清單也未顯示這兩欄,屬於「欄位存在但未使用」,不影響目前 UI,日後若要在清單顯示建立者需一併補 enrich。
  7. 租戶名稱唯一性是全域,非租戶內唯一:與部門管理(org_units 是 tenant-scoped 唯一)不同,tenants.name 唯一性檢查沒有排除自身租戶或子樹範圍,是整個系統全域唯一——不同租戶不能同名(即使分屬完全不相關的組織)。
  8. 本頁無批次操作:不同於設備管理頁有批次刪除,本頁清單無 checkbox 多選,逐列操作。

13. 開發與驗證

  • 跑起來:BE python main_socketio.py(port 8000,log 在 log/app.log);FE 在 compliance-manager-fe/ 起 Vite dev server。BE 改 service code 後必須重啟(無 hot reload)
  • 測試帳號:dev 環境平台管理層帳號(密碼見 .env / 部署文件)
  • 導航路徑:登入 → 系統設定選單「租戶管理」(tenant-manage,path /tenant/tenant-manage,掛於 group-tenant-org 選單群組)
  • 前置資料:無強制前置資料即可開頁;新增租戶會連帶 seed 部門與角色,建議在測試環境操作、避免污染正式租戶樹
  • 待補驗證(見 §12 坑 5):GET /tenants/menuonly=["uid","name"] 裁切是否真的導致 id 欄缺失、Dropdown optionValue="id" 是否因此失效——需實機開發環境驗證
  • E2Ecompliance-manager-test/ repo(Cucumber + Playwright);本頁相關 feature 檔以 tenant-manage / 租戶管理 搜尋
  • 相關文件:多租戶資料模型決策 docs/analysis/2026-06-29-tenant-scope-data-model.md;RLS 修正紀錄 docs/changelog/2026-06-28-fix-tenant-hierarchy-rls.mddocs/changelog/2026-06-29-feat-tenant-scope-data-model-b2.md;租戶預設值 seed 功能 docs/changelog/2026-06-28-feat-tenant-provisioning-defaults.md;權限 SOP 見 _overview §2;DB / API 全量見 GAI-SD-02/03