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

部門管理(Department Manage)

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

事實基準:2026-07-05 從 FE / BE code 掃出;API schema 逐條對照 marshmallow serializer 與 jedi-auth 套件 service 組裝碼

1. 功能描述

部門管理頁是租戶內組織架構(org_units)的樹狀主檔 CRUD 管理介面——雖然表結構是樹狀(parent_id + path),但本頁 UI 呈現為單層 DataTable(非樹狀展開元件),父層關係只在「隸屬部門」欄與編輯 Dialog 的下拉中呈現。頁面 i18n 顯示名稱為「機關(構)單位管理」,與路由技術名 department-manage、表名 org_units(CLAUDE.md 慣例稱「部門」)三者用詞不完全一致,屬命名沿革差異(詳見 §12 坑 3)。

本頁功能全數由 jedi-auth 套件承載(OrgUnitService / OrgUnitDomainService / OrgUnitRepoImpl),主專案只有 route 層薄封裝;org_units 隸屬 tenants(租戶),亦可被 users 透過 user_org_units 多對多關聯(見 _overview §3)。

主要使用者:租戶 admin(後台組織主檔維運)。

角色速覽

角色 一句話
租戶 admin 本頁唯一使用者——部門主檔的新增/編輯/刪除;選單可見性由 RBAC 控管(見 _overview §2),BE 端點僅檢查 JWT

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

# 功能 說明 位置 詳述
1 部門清單 分頁 DataTable,欄位:功能/名稱/隸屬租戶/隸屬部門/描述;支援多欄排序(名稱/隸屬租戶/隸屬部門) DataTable §5
2 全域搜尋 nametenant_name 兩欄位做關鍵字過濾(allowFilterFields Header 搜尋框 §6.1
3 重置查詢 清空搜尋/排序並回到第一頁(queryStore.resetCurrent() Header「重置查詢」按鈕 §6.1
4 新增部門 開空白 Dialog:填隸屬租戶/隸屬部門/名稱(必填)/描述 Header「新增」按鈕 → Dialog UC-DM-01
5 編輯部門 帶入既有值開 Dialog(含依所選租戶重新過濾「隸屬部門」下拉並排除自身,避免自我循環) 清單「編輯」按鈕/點擊名稱 UC-DM-01
6 刪除部門 二次確認後刪除該列 清單「刪除」按鈕 UC-DM-02
7 隸屬租戶下拉 來源 GET /tenants/menumenuStore.fetchTenantMenu),選擇後觸發 onTenantChange 重新過濾部門下拉 編輯 Dialog §6.1
8 隸屬部門下拉(父層選擇) 前端在已載入的 orgUnits 全清單中,用 tenant_id 過濾 + 排除自身 id 組成候選(非另打 API,見 §12 坑 4) 編輯 Dialog §10

UC(§2)展開 4/5(新增/編輯合一的 UC-DM-01)與 6(UC-DM-02 刪除)。本頁無匯入匯出、無批次操作、無樹狀展開元件——功能面完整性即上表 8 項,無需另立更多 UC。

2. Use Case

角色速覽

角色 一句話
租戶 admin 本頁唯一使用者——部門主檔的新增/編輯/刪除;選單可見性由 RBAC 控管(見 _overview §2),BE 端點僅檢查 JWT

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

UC-DM-01 新增 / 編輯部門

項目 內容
角色 租戶 admin
前置條件 已登入;department-manage 選單可見
產出 / 後置條件 public.org_units 新增或更新一列;成功後重新整理清單並重抓部門選單(menuStore.fetchOrgUnitMenu
dept_uc_crud start open 開「新增/編輯」Dialog (編輯先帶入所選租戶 onTenantChange, 過濾同租戶部門下拉並排除自身) start->open input 填寫名稱(必填)+ 隸屬租戶/隸屬部門/描述(選填) open->input valid FE 前端驗證: 名稱非空? input->valid fix 紅字提示,聚焦名稱欄位 valid->fix submit 送出 POST /org-unit 或 PUT /org-unit/{uid} valid->submit fix->input jwt BE 檢查:JWT 已登入? (無角色/租戶檢查) submit->jwt deny 401 未登入 jwt->deny dup BE 檢查:同租戶內 名稱是否重複? jwt->dup dupErr AUTH_409005 組織單位名稱重複 dup->dupErr 重複 ok upsert org_units 列 (path 欄位非 API 寫入, 由既有列值延續,見 §12 坑 1) 回讀最新資料回傳 dup->ok 不重複 refresh 重新整理清單 + 重抓部門選單 (menuStore.fetchOrgUnitMenu) ok->refresh
UC-DM-01 流程:FE 驗證名稱必填 → 送出 → BE 僅檢查 JWT → 同租戶名稱重複檢查 → upsert(path 欄位非本 API 寫入)→ 回讀最新值

UC-DM-02 刪除部門

項目 內容
角色 租戶 admin
前置條件 該部門列已存在
產出 / 後置條件 org_units 該列刪除;⚠️ ORM children relationship 宣告 cascade="all,delete-orphan",與 DB FK parent_id ON DELETE SET NULL 不一致,實際刪除是否波及子部門待現場驗證(見 §12 坑 2)
dept_uc_delete start click 清單逐列點「刪除」 start->click confirm 彈二次確認框 click->confirm del DELETE /org-unit/{uid} confirm->del jwt BE 檢查: 僅 JWT?(⚠️ 無角色/租戶檢查, 繞過 FE 任何登入者皆可刪任一部門) del->jwt exist BE 檢查: uid 是否存在? jwt->exist notfound AUTH_404004 組織單位不存在 exist->notfound ormcascade ⚠️ ORM session.delete 觸發 children relationship cascade="all,delete-orphan" (DB FK 卻是 parent_id ON DELETE SET NULL, 兩者不一致,見 §12 坑 2,待現場驗證) exist->ormcascade ok 刪除成功 重新整理清單 ormcascade->ok tenantFk tenant_id FK: ON DELETE CASCADE (刪租戶才會波及,非本操作路徑) tenantFk->ok 參考關係(非本 UC 路徑)
UC-DM-02 流程:二次確認 → DELETE → BE 僅檢查 JWT → uid 存在性檢查 → ORM cascade 疑慮 → 成功

3. 權限矩陣

操作 FE 判定 BE 強制 BE 檢查位置
選單 / 頁面可見性 RBAC 選單(ui_routes.name='department-manage',見 _overview §2 ⚠️ ——全部 6 支 endpoint(menu/list/get/create/update/delete)僅 @jwt_required() api/auth/routes/org_unit_route.py(每個 Route class 逐一,皆無角色/租戶檢查裝飾器)
新增 / 編輯 / 刪除 無額外前端角色判斷(頁面本身即假設只有 admin 進得來,按鈕恆常顯示) 同上,僅 JWT;service 層(OrgUnitService / OrgUnitDomainService)也不做角色檢查 ~/Projects/Jedicogy/module/jedi-python-package/jedi-auth/jedi_auth/app/service/org_unit_service.py
跨租戶存取 無租戶邊界前端過濾(清單直接顯示 BE 回傳的全部列,是否受 RLS 限縮取決於使用者 session 的 allowed_tenant_paths RLS session_scope()app.user_id / app.allowed_tenant_paths 注入(見 _overview §3),非 route/service 層額外程式碼 jedi_common.session.database.db

⚠️ 已知設計基調(非本頁獨有):與本群多數管理頁一致,寫入 API 僅 @jwt_required(),完全依賴 RBAC 選單/capability 層擋非 admin 使用者看到入口(見 _overview §5)。若已知 API path,任何登入者理論上都能新增/編輯/刪除任一(RLS 範圍內可見的)部門。

4. 狀態機與前置條件

本頁無輪次 / 階段狀態機,org_units 本身也無業務狀態欄位(無 status/enable 之類)。僅有的前置條件:

條件 效果 出處
名稱必填(FE + BE 雙重驗證) FE:vuelidate required 擋送出;BE:OrgUnitRequest.name marshmallow required=True OrgUnitManage.vue:56-62api/auth/serializers/org_unit.py:28
同租戶內名稱不可重複 新增/改名時 BE 拋 AUTH_409005 org_unit_domain_service.py:166-177verify_org_unit_name_duplicatetenant_id scoped,跨租戶可同名)
隸屬部門下拉排除自身 編輯時 FE onTenantChange 過濾 orgUnit.id !== currentData.id,避免選自己當父層造成循環(純 FE 防呆,BE 無此檢查 OrgUnitManage.vue:211-213
uid 不存在 編輯/刪除回 AUTH_404004 org_unit_domain_service.py:142-152

5. UI 設計

📸 實際畫面截圖待補(一律亮色模式拍攝):以 playwright 對 STG 擷取,預計補以下兩張到 assets/img/: ① 整頁(DataTable 含名稱/隸屬租戶/隸屬部門/描述欄)department-overview.png ② 新增 / 編輯部門對話框(隸屬租戶/隸屬部門下拉 + 名稱/描述輸入)department-edit-dialog.png

版面骨架(單欄:Header + DataTable + 新增/編輯 Dialog,無左側樹、無 Tab):

Header:搜尋框(name/tenant_name)+ 重置查詢 + 新增部門 單欄版面:無左側樹、無 Tab(i18n 顯示為「機關(構)單位管理」) 開啟:部門管理 §6 API 規格 部門清單 DataTable:功能/名稱/隸屬租戶/隸屬部門/描述(分頁+多欄排序) POST /org-units (分頁清單) GET /org-units/menu ・ GET /tenants/menu (下拉來源) 逐列「編輯」/「刪除」按鈕;點名稱亦開編輯 → #6-api-規格(點我開啟) 開啟:UC-DM-01 新增/編輯部門 新增/編輯部門 Dialog 隸屬租戶/隸屬部門下拉(排除自身)/名稱*/描述 POST /org-unit ・ PUT /org-unit/{uid} 同租戶名稱重複 → AUTH_409005 → UC-DM-01(點我開啟) 開啟:UC-DM-02 刪除部門 刪除部門 二次確認 逐列「刪除」按鈕觸發確認 dialog DELETE /org-unit/{uid} ⚠️ ORM cascade 與 DB FK SET NULL 語意不一致(待驗證) → UC-DM-02(點我開啟) 彩色=可點跳轉本頁 §6/UC 的區塊|灰=本頁結構描述|虛線=Dialog/條件顯示層 API 僅列代表性 2~3 條,完整規格見 §6
部門管理頁版面骨架:Header 搜尋/新增、DataTable(功能/名稱/隸屬租戶/隸屬部門/描述,連 §6 API)、新增/編輯 Dialog(連 UC-DM-01)、刪除確認(連 UC-DM-02)

狀態呈現:清單載入時 DataTable loading 遮罩(orgUnitLoading);儲存/刪除走 Dialog/confirm 標準 loading(submitted);無逐列獨立 loading、無 socket,操作後皆重打 getOrgUnits() 重新整理。名稱過長以 tooltip 顯示全文(text-overflow-ellipsis + v-tooltip)。

6. API 規格

Envelope:成功 {"status": true, "data": …}(本頁未見帶 meta 的分頁 envelope 額外欄位,分頁 meta 直接展開於回傳頂層並由 FE Object.assign(pager.value, meta) 消費)、失敗依 jedi_common.handler.handler.register_error_handlers 標準錯誤格式。

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

分類 Method + Path 說明 完整規格
清單 POST /org-units 分頁 + 排序 + 篩選(name/tenant_name 全域搜尋),本頁清單資料來源 §6.2
選單 GET /org-units/menu 部門下拉選單(id, uid, name,供其他頁面如專案規劃的部門下拉使用;本頁編輯 Dialog 的部門下拉不吃這支,改用前端已載入的 orgUnits 全清單自行過濾,見 §12 坑 4) §6.3
CRUD GET /org-unit/{uid} 取單筆(本頁未直接呼叫,路由存在供他頁/除錯用) GAI-SD-02
CRUD POST /org-unit 新增部門 §6.4
CRUD PUT /org-unit/{uid} 更新部門 §6.4
CRUD DELETE /org-unit/{uid} 刪除部門 §6.5
依賴(他頁下拉) GET /tenants/menu 「隸屬租戶」下拉來源(menuStore.fetchTenantMenu GAI-SD-02

上表 method 對照 routes.json 實際登記:GET/POST/PUT/DELETE /api/1.0/org-unit(單筆路徑,POST 亦可不帶 uid 走新增)與 GET/POST/PUT/DELETE /api/1.0/org-unit/<uid>(帶 uid 變體)皆存在於同一 OrgUnitRoute MethodResource;FE 僅使用 POST /org-unit(新增)與 PUT /org-unit/{uid}(更新)+ DELETE /org-unit/{uid}(刪除)三種寫法,其餘組合(如 POST /org-unit/{uid})路由存在但本頁未使用。

6.2 [POST] /org-units — 部門清單(分頁)

Request(RequestMetaSchema 標準分頁 + 篩選):

欄位 型別 必填 說明
pager object {page, page_size, with_total}
sort array [{field, direction}]
filters object OrgUnitQueryRequestnametenant_name(全域搜尋套用欄位)/tenant_idparent_id 等,見 §12 query entity 非-None 預設陷阱提醒

Response data[](每項一個部門,OrgUnitResponse):

欄位 型別 說明
id / uid int / string 內部 id / 對外識別碼
tenant_id / tenant_uid / tenant_name int / string|null / string|null 隸屬租戶(tenant_name/tenant_uid 為 DB column_property,即時算不落地欄位)
tenant object|null 巢狀 TenantResponse(完整租戶物件,僅在 DB 端 eager load 時有值)
name string 部門名稱
parent_id / parent_uid / parent_name int|null / string|null / string|null 隸屬部門(父層,同為 column_property 即時算)
parent object|null 巢狀 OrgUnitResponse(父部門完整物件)
path string 樹狀路徑(如 /10/12/);本頁 UI 未顯示此欄位
metadata_json dict JSONB 擴充欄位(本頁 UI 未使用)
description string|null 描述
created_user / created_at / updated_user / updated_at 審計欄;注意本頁回應僅回 login_name,未 enrich created_user_name/updated_user_name nickname(見 §12 坑 5,CLAUDE.md 審計欄位規範的已知落差)

出處:api/auth/serializers/org_unit.py:42-64OrgUnitResponse);組裝邏輯 ~/Projects/Jedicogy/module/jedi-python-package/jedi-auth/jedi_auth/app/service/org_unit_service.py:22-40get_org_units_and_pager)。

6.3 [GET] /org-units/menu — 部門選單

無 query 參數。Response data[]{id, uid, name}——注意路由用 OrgUnitResponse(only=["id","uid","name"]) 序列化,即使底層 OrgUnitService.get_org_units_menu() 回傳的 OrgUnitMenuDTO 本身含 tenant_id/tenant_uid,最終 response 仍因 only=[...] 限制被裁掉,只剩三欄。出處:api/auth/routes/org_unit_route.py:19-30OrgUnitMenuRoute);org_unit_service.py:113-117get_org_units_menu)。

6.4 [POST] /org-unit・[PUT] /org-unit/{uid} — 新增/更新

Request(OrgUnitRequest,全欄位選填except name):

欄位 型別 必填 說明
name string 部門名稱(同租戶內須唯一)
tenant_id int 隸屬租戶 id(FE 送 currentData.tenant_id
parent_id int 隸屬部門(父層)id
description string 描述
path string schema 允許傳入,但 OrgUnitRepoImpl.add/update 實際未使用此欄位寫入 DB(見 §12 坑 1)
tenant_uid string schema 允許但 add/update 皆未使用(僅 tenant_id 生效)
metadata_json dict schema 允許但 add/update 皆未使用(repo 硬寫入 _entity.metadata_json,惟 FE 從不送此欄位,故實務上恆為預設 {}

Response:同 §6.2 單筆形狀(新增/更新後皆回讀最新資料)。出處:org_unit_service.py:66-95add_org_unit/update_org_unit)、~/Projects/Jedicogy/module/jedi-python-package/jedi-auth/jedi_auth/infra/repository/org_unit_repo_impl.py:23-54add/update,可見兩者皆未賦值 path)。

6.5 [DELETE] /org-unit/{uid} — 刪除

無 body。uid 不存在 → AUTH_404004。Response datatruebool,非 §6.2 完整物件)。出處:org_unit_service.py:97-111delete_org_unit)、org_unit_domain_service.py:179-191

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

檔案 角色
src/views/org-unit/OrgUnitManage.vue 唯一檢視檔(367 行;無子元件,DataTable + 新增/編輯 Dialog 全在此)
src/config/router/index.js:566-579 route department-manage,path /department/department-manage
src/config/api/api.js:348-350 ORG_UNITS(清單 POST)/ORG_UNIT(單筆 CRUD)/ORG_UNIT_MENU 常數
src/service/BaseService.js 本頁未使用專屬 service 檔,直接用 BaseService 泛型 get/post/put/delete
src/composables/useTableQuery.js 分頁/排序/搜尋狀態管理(scopeKeyroute.name
src/stores/menuStore.js:143-156 fetchTenantMenu / fetchOrgUnitMenu(供本頁 Dropdown 與其他頁面共用)
src/stores/queryStore.js 查詢狀態持久化(resetCurrent() 供重置查詢按鈕使用)
src/config/locales/i18n/{zh-tw,en}/department.json 頁面 i18n,namespace lang.department_manage.*(顯示字樣「機關(構)單位」,非「部門」,見 §12 坑 3)
src/config/locales/i18n/{zh-tw,en}/menu.json 選單顯示名稱「機關(構)單位管理」(department-manage key)

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

鏈路 Route App Service 底層
部門清單(分頁) api/auth/routes/org_unit_route.py::OrgUnitsRoute(POST) jedi-auth OrgUnitService.get_org_units_and_pager@transaction OrgUnitDomainService.get_org_units_and_pagerOrgUnitRepoImpl.get_all_by_fields_and_pager(繼承 BaseRepositoryImpl
部門選單 OrgUnitMenuRoute(GET) OrgUnitService.get_org_units_menu OrgUnitDomainService.get_org_units_menuOrgUnitRepoImpl.get_org_units_menu(自訂 SQL:join Tenanttenant_uid
CRUD OrgUnitRoute(GET/POST/PUT/DELETE) OrgUnitService.get_org_unit / add_org_unit / update_org_unit / delete_org_unit(皆 @transaction OrgUnitDomainServiceOrgUnitRepoImpl.add / update / delete_by_uid(後者為 BaseRepositoryImpl 通用實作,ORM session.delete

DDD 提醒:本模組層級守法規範——route 不碰 DB(皆透過注入的 OrgUnitService)、app service 全數 @transaction、domain service 不依賴 infra、repo 繼承 BaseRepositoryImpl本頁邏輯完全在 jedi-auth 套件內,主專案 api/auth/routes/org_unit_route.py 只做 request/response 邊界封裝——改本頁行為多數情況要動套件(依 CLAUDE.md「外部套件異動規範」,動套件前需提醒 user 做決策)。唯一偏離常態的是「寫入 API 無角色檢查」——與本群其餘管理頁一致的既有設計(見 §3、_overview §5)。

9. DB

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

讀/寫 說明 欄位詳述
public.org_units 部門主檔(租戶內樹狀組織架構),本頁核心表 §9.3
public.tenants 隸屬租戶下拉來源(GET /tenants/menu);org_units.tenant_id 的 FK 目標 GAI-SD-03

本頁不直接讀寫 user_org_units(使用者↔︎部門關聯,屬使用者管理頁範疇)。

9.2 ER 圖(本頁讀寫範圍)

本頁涉及的表結構簡單(org_units 自我參考樹狀 + 隸屬 tenants),沿用 _overview §3 組織模型圖assets/sysadmin-org-model.dot)已完整說明 tenants 樹 → org_units 樹的關係,不另畫專屬 ER 重複維護。本頁新增的細節僅為:tenant_name/tenant_uid/parent_name/parent_uid 皆是 DB column_propertyapp_get_tenant_name 等 SQL function)即時算出、非落地欄位。

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

public.org_units — 部門主檔

DB comment:「組織單位表:定義租戶內的組織架構(部門)及其層級關係」。

欄位 型別 說明
id integer 主鍵ID
tenant_id integer NOT NULL 所屬租戶ID;FK org_units_tenant_id_fkeytenants(id) ON DELETE CASCADE(刪租戶會連帶刪光其下全部部門)
name text NOT NULL 組織單位名稱(同租戶內唯一,由 service 層驗證,非 DB UNIQUE 約束)
parent_id integer|null 父組織單位ID;FK org_units_parent_id_fkeyorg_units(id) ON DELETE SET NULL(刪父部門,子部門的 parent_id 變 null,不會連帶刪除——但見 §12 坑 2,ORM 層 cascade 宣告與此不一致,待驗證)
path text NOT NULL 組織單位路徑,例如:/10/12/本頁 API(新增/更新)完全不寫入此欄位,需另外機制維護(見 §12 坑 1)
metadata jsonb NOT NULL DEFAULT {} 組織單位元數據 JSON(ORM 對應 metadata_json);本頁 UI 未使用
uid varchar NOT NULL DEFAULT gen_random_uuid() 組織單位唯一識別碼 UUID
description text|null 組織單位描述
created_user / created_at / updated_user / updated_at varchar(50)|null / timestamptz / varchar(50)|null / timestamptz 審計欄;created_at/updated_at DB 端 now() 預設

索引:idx_org_units_path_prefixpath text_pattern_ops)、idx_org_units_path_trgmpath gin_trgm_ops,模糊比對用)、idx_org_units_tenant_pathtenant_id, path 複合)——三個索引皆圍繞 path 欄位設計,暗示樹狀路徑查詢原本設計上是重要存取模式,但目前本頁 CRUD 完全不維護該欄位(見 §12 坑 1)。

10. 頁面邏輯與資料對應

載入時序onMountedPromise.all 平行觸發三支:menuStore.fetchTenantMenu()menuStore.fetchOrgUnitMenu()getOrgUnits()(清單本體,帶 orgUnitLoading=true 立即顯示載入中,不等 menu API 完成)。

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

畫面元素 FE state API 欄位
清單「名稱」欄 orgUnits[].name data[].name
清單「隸屬租戶」欄 data.tenant_name(有 data.tenant 才顯示,否則顯示「無」) data[].tenant_name + data[].tenant(存在性判斷用巢狀物件)
清單「隸屬部門」欄 data.parent_name(有 data.parent 才顯示,否則顯示「無」) data[].parent_name + data[].parent
Dialog「隸屬租戶」下拉 tenantMenumenuStore state) GET /tenants/menu 回傳
Dialog「隸屬部門」下拉 filteredOrgUnitsonTenantChange 計算:orgUnits.value.filter(o => o.tenant_id === value && o.id !== currentData.id) 前端從已載入的清單 orgUnits 自行過濾,非另打 API(見 §12 坑 4)
分頁 / 排序 pager / multiSortMetauseTableQuery composable) POST /org-units 回傳的 metaObject.assign(pager.value, meta)

儲存流程onSaveClick → vuelidate 驗證(僅 name 必填)→ 組 payload(tenant_idnamedescriptionparent_idtenant_uid)→ 依 currentData.uid 是否存在分派 update()(PUT)或 create()(POST)→ 成功後 getOrgUnits() 重新整理清單 + menuStore.fetchOrgUnitMenu() 重抓部門選單(讓其他頁面的部門下拉同步)→ onCancelClick() 關閉 Dialog 並清空 currentData注意 payload 帶 tenant_uid: currentData.value.uid——這是把「目前編輯列自身的 uid」誤標成 tenant_uid 送出,但 BE add_org_unit/update_org_unit 皆未使用 tenant_uid 欄位(純 no-op),未造成實際影響(見 §12 坑 6)。

錯誤對應:BE error envelope → toast 顯示;本頁常見碼:AUTH_404004(部門不存在,編輯/刪除已被他人先刪除時觸發)、AUTH_409005(同租戶名稱重複)。

11. 背景行為與外部依賴

類型 內容
通知 無——本頁不觸發、也不消費任何通知
Socket 無 socket / 輪詢;操作後皆重打 getOrgUnits() / menuStore.fetchOrgUnitMenu() 重新整理
jedi-* 套件 jedi-authOrgUnitService/OrgUnitDomainService/OrgUnitRepoImpl,本頁核心邏輯全在此套件)
系統參數 無 feature flag
跨頁影響 本頁新增/刪除部門會影響:專案規劃頁的部門下拉(GET /org-units/menu)、任務指派的部門關聯選項;刪除有子部門的部門或有使用者掛載的部門時的行為需留意(§12 坑 2、7)

12. 邊界情況與已知坑

  1. path 欄位由 DB trigger 計算、API 完全不維護OrgUnitRequest schema 雖允許傳入 path,但 OrgUnitRepoImpl.add()/update() 皆未對 path 賦值(只設 name/description/tenant_id/parent_id/metadata_json)。path 是 NOT NULL 且 DB default: null,其值由 org_units 上的 trigger 自動計算——trg_org_path(BEFORE INSERT OR UPDATE OF parent_id, id)/trg_org_path_ins(AFTER INSERT)/trg_org_path_upd(BEFORE UPDATE OF parent_id)皆執行函式 set_org_path()(見 ERD docs/system-design/database/erd/tbls/public.org_units.md Triggers 段;與 tenantstrg_tenant_path_insert 同一手法)。故新增部門不會因 path NOT NULL 而失敗,path 由 DB 端補值,非應用層責任。
  2. ORM cascade 與 DB FK 語意不一致OrgUnit.children relationship 宣告 cascade="all,delete-orphan",但 DB 層 FK org_units_parent_id_fkeyON DELETE SET NULLdelete_by_uid()BaseRepositoryImpl 通用實作)用 ORM session.delete(model_instance)——若該部門有子部門,SQLAlchemy ORM 層可能依 relationship cascade 設定連帶刪除子部門(覆蓋 DB FK 的 SET NULL 語意),也可能因 lazy-load 未觸發而不生效,行為需現場驗證(建立父子部門測試資料,刪除父部門後檢查子部門是否被刪或僅 parent_id 變 null)
  3. 三個名稱用詞不一致:路由/程式碼技術名 department-manage(英文 department)、DB 表名 org_units(CLAUDE.md 稱「部門」)、頁面實際 i18n 顯示與新增/編輯/刪除按鈕文案皆用「機關(構)單位」(非「部門」)。維護時若只搜「部門」關鍵字可能漏掉這頁的 i18n 文案;新人上手容易被三種名稱搞混
  4. 編輯 Dialog 的「隸屬部門」下拉不吃 /org-units/menu:改用前端已載入的 orgUnits 全清單(getOrgUnits() 抓到的當前分頁資料,非全量)在本地用 tenant_id + 排除自身過濾。這表示:① 若目前分頁未涵蓋某些部門,下拉選單會漏選項;② 排序/搜尋改變分頁內容時,下拉候選也會跟著變動,UX 上可能造成「找不到某部門當父層」的困惑
  5. 列表 API 未 enrich created_user_name/updated_user_nameOrgUnitResponse 只回 created_user/updated_user(login_name),未依 CLAUDE.md 審計欄位規範額外提供 nickname 版本——本頁清單也未顯示這兩個審計欄位,UI 面暫無影響,但若日後要顯示建立者/修改者需求,要留意 nickname enrich 未做
  6. onSaveClick payload 誤送 tenant_uid: currentData.value.uid:FE 把「目前編輯列自身的 uid」標成 tenant_uid 欄位名送出,語意上是 bug(tenant_uid 應該是「隸屬租戶的 uid」,不是「本部門自己的 uid」),但因 BE add_org_unit/update_org_unit 從未讀取 tenant_uid(純 no-op 欄位),目前無實際功能影響,純粹是死程式碼/命名誤導
  7. 刪除有使用者掛載的部門org_unitsuser_org_units(使用者↔︎部門)之間的 FK 行為本次未展開查證(不在本頁鏈路直接讀寫範圍),若部門下有使用者以此為主要部門(is_primary),刪除行為需另查 user_org_units 的 FK 級聯設定,避免刪部門後使用者資料不一致
  8. 無租戶邊界前端防呆:清單依賴 BE/RLS 回傳範圍即顯示,前端未對「隸屬租戶」欄做任何額外過濾或警示;跨租戶部門是否會混雜顯示取決於當前使用者的 allowed_tenant_paths(見 _overview §3 RLS 說明),本頁未做特別處理
  9. 同租戶名稱唯一僅 service 層驗證,非 DB 約束org_units.name 沒有 UNIQUE index/constraint,重複檢查完全靠 verify_org_unit_name_duplicate() 這道 service 層邏輯——若有其他寫入路徑(如批次匯入、其他套件呼叫 repo 層 add())繞過 OrgUnitService,可能造成同租戶重名而不被擋下

13. 開發與驗證

  • 跑起來:BE python main_socketio.py(port 8000,log 在 log/app.log);FE 在 compliance-manager-fe/ 起 Vite dev server。BE 改 service code 後必須重啟(無 hot reload);本頁邏輯在 jedi-auth 套件內,改套件程式碼走 CLAUDE.md「開發期間一律走 poetry path dependency」規範
  • 測試帳號:dev 環境租戶 admin 角色帳號(密碼見 .env / 部署文件)
  • 導航路徑:登入 → 系統設定選單「機關(構)單位管理」(department-manage,path /department/department-manage
  • 前置資料:無強制前置資料即可開頁(空清單可直接新增);驗證「隸屬部門」下拉需先有至少一筆同租戶部門資料
  • E2Ecompliance-manager-test/ repo(Cucumber + Playwright);本頁相關 feature 檔以 department / org-unit / 部門 / 機關 搜尋
  • 相關文件:三層 RBAC 權限模型與租戶/部門組織模型見 _overview §2-3;DB / API 全量見 GAI-SD-02/03;jedi-auth 套件參考 ~/Projects/Jedicogy/module/jedi-python-package/jedi-auth/