租戶管理(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) |
UC-TN-02 刪除租戶
| 項目 | 內容 |
|---|---|
| 角色 | 平台 / 系統管理層 |
| 前置條件 | 已登入;目標租戶 uid 存在 |
| 產出 / 後置條件 | 成功:tenants 該列(及 ORM cascade
的全部子孫租戶列)被刪除;失敗:仍有
org_units/roles/users(預設租戶)
參照時,DB FK RESTRICT 擋下、回 409 |
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.py(TenantsRoute.post
/ TenantMenuRoute.get / TenantRoute.get) |
| 新增 / 編輯 / 刪除 | 無額外前端角色判斷(頁面本身即假設只有管理員進得來) | ⚠️ 僅 @jwt_required(),service
層不做角色檢查 |
api/auth/routes/tenant_route.py(TenantRoute.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-191(verify_tenant_name_duplicate) |
| 編輯租戶名稱 | 僅當名稱有變更才觸發重複檢查(未改名稱不查) | 同檔 :122-142(update_tenant) |
| 刪除租戶 | 目標 uid 須存在,否則 404;存在但仍有
org_units/roles/users(預設租戶)
參照 → DB FK ON DELETE RESTRICT 擋下,回 409 |
jedi_auth/infra/models/org_unit.py:26、role.py:46、user.py:48 |
租戶 path 欄位 |
僅在 BEFORE INSERT 由 DB trigger
trg_tenant_path_insert(set_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、無批次操作):
狀態呈現:清單載入時 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-38(TenantMenuRoute.get,用
TenantResponse(only=["uid", "name"]))。
註:FE
menuStore消費的tenantMenu實際上還帶id/parent_id(jedi_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-54(TenantResponse);組裝邏輯
jedi_auth/app/service/tenant_service.py:22-40(get_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-28(TenantRequest)、api/auth/routes/tenant_route.py:72-99(TenantRoute.post)、app/auth/service/tenant_provisioning_service.py:56-85(add_tenant_with_defaults)。
[PUT] /tenant/{uid}
Request:同 TenantRequest(name 技術上仍是
required=True,因此編輯時省略 name 會
400,並非部分更新語意)。
Response:同上。不存在的 uid →
AUTH_404005。出處:jedi_auth/app/service/tenant_service.py:96-113(update_tenant)。
[DELETE] /tenant/{uid}
無 body。Response data:true。不存在的
uid → AUTH_404005;存在但仍被
org_units/roles/users FK 參照 →
409(見 §12 坑
3)。出處:jedi_auth/app/service/tenant_service.py:115-127(delete_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.TenantDomainService
→
jedi_auth.infra.repository.tenant_repo_impl.TenantRepoImpl::get_tenants_menu(繼承
BaseRepositoryImpl) |
| 租戶清單(分頁) | TenantsRoute(POST) |
TenantService::get_tenants_and_pager |
TenantDomainService::get_tenants_and_pager →
TenantRepoImpl(BaseRepositoryImpl.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_tenant →
TenantRepoImpl.update(不含 path
重算) |
| 刪除租戶 | TenantRoute(DELETE) |
TenantService::delete_tenant |
TenantDomainService::delete_tenant →
TenantRepoImpl.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 圖(本頁讀寫範圍,按新增 / 刪除兩條路徑分張)
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_fkey:REFERENCES tenants(id) ON DELETE SET NULL |
| path | text | NOT NULL;/1/2/ 格式;僅 BEFORE INSERT trigger
trg_tenant_path_insert(set_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_prefix(text_pattern_ops,前綴比對用)、idx_tenants_path_trgm(gin_trgm_ops,模糊搜尋用)——皆為支援
RLS path LIKE 'prefix%' 子樹判定與清單模糊搜尋而建。
FK 對照(其他表指向 tenants.id
的刪除行為,決定本頁刪除能否成功):
| 來源表 | FK 刪除行為 | 對租戶刪除的影響 |
|---|---|---|
tenants.parent_id(自我參考) |
ON DELETE SET NULL(DB 層語意) |
若走原生 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_TYPES(cruise-project/dashboard/workflow/resource/report)→
建預設角色「System Manager」並綁定上述權限集;全程單一
@transaction,任一步失敗整批 rollback,不留半套 |
| jedi-* 套件 | jedi-auth(TenantService/TenantDomainService/TenantRepoImpl/OrgUnitService/CapabilityService/RoleService
皆屬此套件;主專案僅有薄 Route 層與
TenantProvisioningService 這一個本地 orchestration) |
| 系統參數 | 無 feature flag;TENANT_ADMIN_EXCLUDED_RESOURCE_TYPES
為程式碼常數(非資料庫可調參數),若日後啟用新模組需改此常數 |
12. 邊界情況與已知坑
- 編輯(PUT)改變
parent_id後path不重算:path欄位只有trg_tenant_path_insert這個 BEFORE INSERT trigger 會算,TenantRepoImpl.update()(jedi_auth/infra/repository/tenant_repo_impl.py:35-51)沒有任何path相關邏輯。移動租戶到新的隸屬租戶後,該租戶(及其全部子孫)的path仍是舊值——這會讓 RLS 的子樹判定(tenants_selectpolicy 用path LIKE 'prefix%')與畫面上顯示的新「隸屬租戶」不一致:使用者可能改了隸屬租戶,但實際的租戶隔離範圍(誰看得到誰)並未跟著變。目前 UI 沒有任何提示告知此限制。 - 刪除的 ORM cascade 與 DB FK
語意不一致:
Tenant.childrenrelationship 設了cascade="all,delete-orphan"(jedi_auth/infra/models/tenant.py:44-46),但 DB 層tenants_parent_id_fkey是ON DELETE SET NULL。本頁刪除走的是 ORMsession.delete()(BaseRepositoryImpl.delete_by_uid),因此實際行為是子孫租戶被連帶硬刪除,不是 FK 語意上的「子租戶變成無父(root)」。若日後有維運腳本改用原生 SQL 刪除同一張表,行為會與 UI 操作不一致,需特別注意。 - 刪除失敗時的錯誤訊息不友善:
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 訊息給使用者,非本地化、也不告知「請先移除底下的部門/角色/使用者」。 - 名稱重複拋
NotFound(HTTP 404),error_code 與租戶不存在不同:verify_tenant_name_duplicate(jedi_auth/domain/service/tenant_domain_service.py:183-191)名稱重複時raise NotFound(ErrorCode.AUTH_TENANT_NAME_DUPLICATE)——用NotFound例外類(HTTP 404)是 docstring 註記的「共用 404 型別避免洩漏細節」,但 error_code 字串是AUTH_409006(AUTH_TENANT_NAME_DUPLICATE = ("租戶名稱重複", "AUTH_409006"),jedi_auth/common/enum/error_code.py:37),與「租戶不存在」的AUTH_404005(AUTH_TENANT_NOT_FOUND,同檔:27)是兩個各自獨立的常數。因此前端可憑 error_code 區分兩種情境(404005=不存在、409006=名稱重複),只是 HTTP 狀態同為 404;型別(NotFound/404)與碼語意(409)不一致這點值得留意。 GET /tenants/menu回應被裁切成兩欄,但 Dropdown 綁定第三欄:Route 層marshal_with(TenantResponse(only=["uid", "name"]))只留uid/name,但 FE Dialog 的Dropdown用optionValue="id"(TenantManage.vue:312)。實測需確認 marshmallow 的only是否真的濾掉id(若濾掉,Dropdown 選中值會是undefined,儲存時parent_id送空)——這是本次盤點發現但未實機驗證的存疑項,見開發與驗證段待辦。- 審計欄位未 enrich
nickname:
created_user/updated_user回傳的是 login_name,本頁未依專案規範(CLAUDE.md「審計欄位回傳規範」)額外提供created_user_name/updated_user_name(nickname)——但本頁清單也未顯示這兩欄,屬於「欄位存在但未使用」,不影響目前 UI,日後若要在清單顯示建立者需一併補 enrich。 - 租戶名稱唯一性是全域,非租戶內唯一:與部門管理(
org_units是 tenant-scoped 唯一)不同,tenants.name唯一性檢查沒有排除自身租戶或子樹範圍,是整個系統全域唯一——不同租戶不能同名(即使分屬完全不相關的組織)。 - 本頁無批次操作:不同於設備管理頁有批次刪除,本頁清單無 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/menu的only=["uid","name"]裁切是否真的導致id欄缺失、DropdownoptionValue="id"是否因此失效——需實機開發環境驗證 - E2E:
compliance-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.md、docs/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