部門管理(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 | 全域搜尋 | 對 name/tenant_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/menu(menuStore.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) |
UC-DM-02 刪除部門
| 項目 | 內容 |
|---|---|
| 角色 | 租戶 admin |
| 前置條件 | 該部門列已存在 |
| 產出 / 後置條件 | org_units 該列刪除;⚠️ ORM children
relationship 宣告 cascade="all,delete-orphan",與 DB FK
parent_id ON DELETE SET NULL
不一致,實際刪除是否波及子部門待現場驗證(見 §12 坑 2) |
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-62;api/auth/serializers/org_unit.py:28 |
| 同租戶內名稱不可重複 | 新增/改名時 BE 拋 AUTH_409005 |
org_unit_domain_service.py:166-177(verify_org_unit_name_duplicate,tenant_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):
狀態呈現:清單載入時 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 變體)皆存在於同一OrgUnitRouteMethodResource;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 | 否 | OrgUnitQueryRequest:name/tenant_name(全域搜尋套用欄位)/tenant_id/parent_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-64(OrgUnitResponse);組裝邏輯
~/Projects/Jedicogy/module/jedi-python-package/jedi-auth/jedi_auth/app/service/org_unit_service.py:22-40(get_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-30(OrgUnitMenuRoute);org_unit_service.py:113-117(get_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-95(add_org_unit/update_org_unit)、~/Projects/Jedicogy/module/jedi-python-package/jedi-auth/jedi_auth/infra/repository/org_unit_repo_impl.py:23-54(add/update,可見兩者皆未賦值
path)。
6.5 [DELETE] /org-unit/{uid} — 刪除
無 body。uid 不存在 → AUTH_404004。Response
data:true(bool,非 §6.2
完整物件)。出處:org_unit_service.py:97-111(delete_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 |
分頁/排序/搜尋狀態管理(scopeKey 用
route.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_pager →
OrgUnitRepoImpl.get_all_by_fields_and_pager(繼承
BaseRepositoryImpl) |
| 部門選單 | OrgUnitMenuRoute(GET) |
OrgUnitService.get_org_units_menu |
OrgUnitDomainService.get_org_units_menu →
OrgUnitRepoImpl.get_org_units_menu(自訂 SQL:join
Tenant 取 tenant_uid) |
| CRUD | OrgUnitRoute(GET/POST/PUT/DELETE) |
OrgUnitService.get_org_unit / add_org_unit
/ update_org_unit / delete_org_unit(皆
@transaction) |
OrgUnitDomainService → OrgUnitRepoImpl.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_property(app_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_fkey →
tenants(id) ON DELETE
CASCADE(刪租戶會連帶刪光其下全部部門) |
| name | text NOT NULL | 組織單位名稱(同租戶內唯一,由 service 層驗證,非 DB UNIQUE 約束) |
| parent_id | integer|null | 父組織單位ID;FK org_units_parent_id_fkey →
org_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_prefix(path text_pattern_ops)、idx_org_units_path_trgm(path gin_trgm_ops,模糊比對用)、idx_org_units_tenant_path(tenant_id, path
複合)——三個索引皆圍繞 path
欄位設計,暗示樹狀路徑查詢原本設計上是重要存取模式,但目前本頁 CRUD
完全不維護該欄位(見 §12 坑 1)。
10. 頁面邏輯與資料對應
載入時序:onMounted 用
Promise.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「隸屬租戶」下拉 | tenantMenu(menuStore state) |
GET /tenants/menu 回傳 |
| Dialog「隸屬部門」下拉 | filteredOrgUnits(onTenantChange
計算:orgUnits.value.filter(o => o.tenant_id === value && o.id !== currentData.id)) |
前端從已載入的清單 orgUnits 自行過濾,非另打
API(見 §12 坑 4) |
| 分頁 / 排序 | pager /
multiSortMeta(useTableQuery composable) |
POST /org-units 回傳的
meta(Object.assign(pager.value, meta)) |
儲存流程:onSaveClick → vuelidate
驗證(僅 name 必填)→ 組
payload(tenant_id/name/description/parent_id/tenant_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-auth(OrgUnitService/OrgUnitDomainService/OrgUnitRepoImpl,本頁核心邏輯全在此套件) |
| 系統參數 | 無 feature flag |
| 跨頁影響 | 本頁新增/刪除部門會影響:專案規劃頁的部門下拉(GET /org-units/menu)、任務指派的部門關聯選項;刪除有子部門的部門或有使用者掛載的部門時的行為需留意(§12
坑 2、7) |
12. 邊界情況與已知坑
path欄位由 DB trigger 計算、API 完全不維護:OrgUnitRequestschema 雖允許傳入path,但OrgUnitRepoImpl.add()/update()皆未對path賦值(只設name/description/tenant_id/parent_id/metadata_json)。path是 NOT NULL 且 DBdefault: 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()(見 ERDdocs/system-design/database/erd/tbls/public.org_units.mdTriggers 段;與tenants表trg_tenant_path_insert同一手法)。故新增部門不會因 path NOT NULL 而失敗,path 由 DB 端補值,非應用層責任。- ORM cascade 與 DB FK
語意不一致:
OrgUnit.childrenrelationship 宣告cascade="all,delete-orphan",但 DB 層 FKorg_units_parent_id_fkey是ON DELETE SET NULL。delete_by_uid()(BaseRepositoryImpl通用實作)用 ORMsession.delete(model_instance)——若該部門有子部門,SQLAlchemy ORM 層可能依 relationship cascade 設定連帶刪除子部門(覆蓋 DB FK 的 SET NULL 語意),也可能因 lazy-load 未觸發而不生效,行為需現場驗證(建立父子部門測試資料,刪除父部門後檢查子部門是否被刪或僅parent_id變 null) - 三個名稱用詞不一致:路由/程式碼技術名
department-manage(英文 department)、DB 表名org_units(CLAUDE.md 稱「部門」)、頁面實際 i18n 顯示與新增/編輯/刪除按鈕文案皆用「機關(構)單位」(非「部門」)。維護時若只搜「部門」關鍵字可能漏掉這頁的 i18n 文案;新人上手容易被三種名稱搞混 - 編輯 Dialog 的「隸屬部門」下拉不吃
/org-units/menu:改用前端已載入的orgUnits全清單(getOrgUnits()抓到的當前分頁資料,非全量)在本地用tenant_id+ 排除自身過濾。這表示:① 若目前分頁未涵蓋某些部門,下拉選單會漏選項;② 排序/搜尋改變分頁內容時,下拉候選也會跟著變動,UX 上可能造成「找不到某部門當父層」的困惑 - 列表 API 未 enrich
created_user_name/updated_user_name:OrgUnitResponse只回created_user/updated_user(login_name),未依 CLAUDE.md 審計欄位規範額外提供 nickname 版本——本頁清單也未顯示這兩個審計欄位,UI 面暫無影響,但若日後要顯示建立者/修改者需求,要留意 nickname enrich 未做 onSaveClickpayload 誤送tenant_uid: currentData.value.uid:FE 把「目前編輯列自身的 uid」標成tenant_uid欄位名送出,語意上是 bug(tenant_uid應該是「隸屬租戶的 uid」,不是「本部門自己的 uid」),但因 BEadd_org_unit/update_org_unit從未讀取tenant_uid(純 no-op 欄位),目前無實際功能影響,純粹是死程式碼/命名誤導- 刪除有使用者掛載的部門:
org_units與user_org_units(使用者↔︎部門)之間的 FK 行為本次未展開查證(不在本頁鏈路直接讀寫範圍),若部門下有使用者以此為主要部門(is_primary),刪除行為需另查user_org_units的 FK 級聯設定,避免刪部門後使用者資料不一致 - 無租戶邊界前端防呆:清單依賴 BE/RLS
回傳範圍即顯示,前端未對「隸屬租戶」欄做任何額外過濾或警示;跨租戶部門是否會混雜顯示取決於當前使用者的
allowed_tenant_paths(見 _overview §3 RLS 說明),本頁未做特別處理 - 同租戶名稱唯一僅 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) - 前置資料:無強制前置資料即可開頁(空清單可直接新增);驗證「隸屬部門」下拉需先有至少一筆同租戶部門資料
- E2E:
compliance-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/