手冊首頁 / 系統管理 / 選單管理

選單管理(System Menu Manage)

功能群:系統管理|共用權限模型 / 組織模型 / system_configs 慣例先讀 功能群總覽

事實基準:2026-07-05 從 FE / BE code 掃出;BE 邏輯完全來自外部套件 jedi-system-menu(主專案僅 route/serializer/DI wiring),API schema 逐條對照 marshmallow serializer 與套件 service 組裝碼

1. 功能描述

選單管理頁是 public.system_menus 這張下拉 / 分類選單 code table 的維護介面——維護的是像「國家」「產業別」「Agent 類型」「稽核方式」這類業務下拉選項的 group/key/value 種子資料,供全系統其他頁面透過 GET /system/menu/<group> 動態取得下拉選項。這與 RBAC 權限選單樹 public.ui_routes 是完全不同的兩張表、兩套用途——ui_routes 決定登入後左側導覽列有哪些節點可見,system_menus 只是給表單 Dropdown 用的分類代碼表,兩者除了都叫「選單」外沒有關聯(詳見 _overview §4 與 §7 已知坑 1)。

本頁邏輯完全由外部套件 jedi-system-menujedi_system_menu PyPI 套件)提供,主專案只負責 route 掛載、marshmallow serializer 與 DI container wiring(di_containers/system_menu/system_menu_containers.py)——domain entity、repository、ORM model、business rule 皆在套件內,不在主專案 app/system_menu/domain/system_menu/(該目錄實質是空殼,僅有 __init__.py)。

主要使用者:系統管理員(後台設定層使用者,見 _overview §5 角色基調)。頁面本身只做單層清單 CRUD,無巢狀結構、無版本、無審核流程。

角色速覽

角色 一句話
系統管理員 本頁唯一使用者——新增/編輯/刪除選單項;頁面可見性由 RBAC 選單控管(見 §3)

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

# 功能 說明 位置 詳述
1 選單項清單(分頁 / 排序 / 搜尋) 列出全部 system_menus 列,欄位:類別(group)、名稱(key);group 顯示走 i18n 對照(查無翻譯退回原代碼) DataTable §6.1
2 關鍵字搜尋 全域搜尋框,比對 group / key 兩欄(allowFilterFields);group 關鍵字若命中 i18n 翻譯文字會被轉回對應原始代碼陣列再送出 Header 搜尋框 §10
3 重置查詢 清空搜尋 / 排序 / 分頁回第一頁(queryStore.resetCurrent() Header「重置查詢」 §10
4 新增選項 開 Dialog 填類別(Dropdown,選項來自既有 group 清單)+ 名稱;value 由 FE 自動設為與 key 相同 Header「新增選項」→ Dialog UC-SM-01
5 編輯選項 開同一顆 Dialog、帶入既有值;儲存走 PUT,同樣 value = key 逐列「編輯」→ Dialog UC-SM-01
6 刪除單筆 二次確認後 DELETE /system-menu/{id} 逐列「刪除」 UC-SM-01
7 批次刪除 勾選多列後二次確認;FE 迴圈逐筆打單筆 DELETEPromise.allSettled),BE 無批次刪除端點;統計成功/失敗數分別 toast Header「批次刪除(N)」(選取數 > 0 才顯示) UC-SM-02
8 類別下拉來源 Dialog 內「類別」Dropdown 選項=呼叫 GET /system/category-menu 取得的既有 group 清單(不能自由輸入新 group,只能挑既有代碼) Dialog §6.2 / §12 坑 2

UC(§2)展開 1(新增/編輯)、7(批次刪除)兩條核心路徑;查詢 / 排序 / 分頁屬常規清單行為,已於 §6.1 / §10 描述完整。本頁沒有「刪除全部下屬項目後才能刪 group」這類巢狀防呆——system_menus 本身無父子結構,group 只是純字串分類欄。

2. Use Case

角色速覽

角色 一句話
系統管理員 本頁唯一使用者——新增/編輯/刪除選單項;頁面可見性由 RBAC 選單控管(見 §3)

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

UC-SM-01 新增 / 編輯選單項

項目 內容
角色 系統管理員
前置條件 已登入;system-menu-manage 選單可見
產出 / 後置條件 public.system_menus 新增或更新一列;(group, key) 唯一索引 uq_system_menus_group_key 生效——重複組合寫入未被包裝成 ConflictError,直接以 DB 唯一約束衝突形式冒出(見 §12 坑 1)
sysmenu_uc_crud start admin 進入選單管理頁 load onMounted:先載 GET /system/category-menu (現有全部 group 清單) 再 POST /system/menus 載入清單 start->load render DataTable 呈現:類別(group,i18n 對照)/ 名稱(key) load->render decide 使用者操作? render->decide create_click 按「新增選項」開 Dialog decide->create_click 新增 edit_click 按列「編輯」開 Dialog(帶入既有值) decide->edit_click 編輯 delete_click 按列「刪除」 confirm.require 二次確認 decide->delete_click 刪除 batch_click 勾選多列 → 按「批次刪除(N)」 confirm.require 二次確認 decide->batch_click 批次刪除 create_fill 填類別(Dropdown,來自 categories)+ 名稱 value 前端自動 = key(value===key 慣例) create_click->create_fill create_validate FE 判斷:group / key 必填? create_fill->create_validate create_block Vuelidate 擋下,欄位標紅 (不送出) create_validate->create_block create_submit POST /system-menu {group, key, value, sort, enable, public} create_validate->create_submit create_conflict BE:(group,key) 已存在? DB unique index uq_system_menus_group_key create_submit->create_conflict create_500 唯一約束衝突 未包裝成 ConflictError → 500(見 §12 坑1) create_conflict->create_500 是(重複 group+key) create_ok 201 新增成功 重新 getMenus() + toast create_conflict->create_ok edit_submit PUT /system-menu/{id} {group, key, value, sort, enable, public} edit_click->edit_submit edit_notfound BE:id 存在? edit_submit->edit_notfound edit_404 404 SYSTEM_MENU_404001 edit_notfound->edit_404 edit_ok 更新成功 重新 getMenus() + toast edit_notfound->edit_ok delete_submit DELETE /system-menu/{id} delete_click->delete_submit delete_notfound BE:id 存在? delete_submit->delete_notfound delete_404 404 SYSTEM_MENU_404001 delete_notfound->delete_404 delete_ok 刪除成功 重新 getMenus() + toast delete_notfound->delete_ok batch_loop FE 迴圈:對每列各打一支 DELETE /system-menu/{id} (Promise.allSettled,無 BE 批次端點) batch_click->batch_loop batch_result 統計成功/失敗數 全成功→success toast 有失敗→partial toast batch_loop->batch_result
UC-SM-01/02 流程:新增走 Dropdown 選既有 group + 必填名稱→POST;編輯同 Dialog→PUT;刪除單筆/批次刪除(FE 迴圈逐筆 DELETE,無 BE 批次端點)

UC-SM-02 批次刪除

項目 內容
角色 系統管理員
前置條件 已勾選 ≥1 列
產出 / 後置條件 每筆各自呼叫 DELETE /system-menu/{id}Promise.allSettled 統計成功/失敗數,非原子性——部分成功部分失敗是常態結果,不會整批 rollback

流程圖同上(UC-SM-01 圖下半部涵蓋批次刪除路徑)。

3. 權限矩陣

操作 FE 判定 BE 強制 BE 檢查位置
選單 / 頁面可見性 RBAC 選單(ui_routes.name='system-menu-manage',經 2026-05-26-menu-group-restructure.sql 掛在 group-system-admin 底下) GET /user/web-menu 依能力集控可見性(見 _overview §2 jedi-auth UiRouteDomainService
新增 / 編輯 / 刪除 / 批次刪除 無額外前端角色判斷(頁面本身假設只有系統管理員進得來) ⚠️ ——全部 7 支 endpoint 僅 @jwt_required(),service 層(jedi_system_menu.app.service.SystemMenuService)不做任何角色 / 能力檢查 api/system_menu/routes/system_menu_route.py(每個 Route class 逐一)
讀清單 / 分頁查詢 / 取類別 同上 同上,僅 JWT 同上

⚠️ 已知設計決策(與本群其餘管理頁一致,非本頁獨有遺漏):本頁與 檔案 Agent 管理 等系統管理層頁面同一基調——完全依賴 RBAC 選單可見性擋非管理員使用者看到入口,寫入端點本身不再加角色守門。若使用者已知 API path,任何登入者理論上都能新增/編輯/刪除選單項,詳見 §12 坑 3。

4. 狀態機與前置條件

本頁無輪次 / 階段狀態機(不屬於稽核流程),亦無版本 / 審核概念。僅有的資料層防呆:

條件 效果 出處
(group, key) 唯一 新增 / 更新為重複組合時,DB 唯一索引 uq_system_menus_group_key 擋下寫入 scripts/deliverables/out/db_schema.json(indexes);BE 未捕捉此例外轉譯為 4xx(見 §12 坑 1)
Dialog 表單必填 group / key 皆必填(Vuelidate required),未填擋下送出(FE 專屬,無對應 BE schema 必填強制以外的邏輯) SystemMenuManage.vue:36-46
value 恆等於 key FE 送出前一律 payload.value = payload.key——value 欄位在本頁 UI 無獨立輸入框,實務上是 key 的鏡射值 SystemMenuManage.vue:174,189create / update
清單查詢固定 public=1 get_system_manus_and_pager 內部強制 _filter.public = 1——本頁清單只顯示 public=1 的列public=0(私有)的選單項存在於表但清單看不到、也無 UI 能改回 public=0(新增 Dialog 無此欄位,schema default 為 1) jedi_system_menu/app/service/system_menu_service.py:41get_system_manus_and_pager

5. UI 設計

📸 實際畫面截圖待補(一律亮色模式拍攝):以 playwright 對 STG/DEV 擷取,預計補以下兩張到 assets/img/: ① 整頁(搜尋列 + DataTable 清單,含類別/名稱兩欄與逐列編輯/刪除按鈕)sysmenu-overview.png ② 新增 / 編輯選項 Dialog(類別 Dropdown + 名稱輸入框)sysmenu-edit-dialog.png

版面骨架(單一 Card:搜尋列 + DataTable,無左樹、無 Tab;新增/編輯共用一顆 Dialog):

Header:關鍵字搜尋(比對 group/key)/重置查詢/新增選項/批次刪除(N) GET /system/category-menu(類別下拉+搜尋 group 轉換) 開啟:§6 API 規格 選單項 DataTable:checkbox/類別(i18n 對照)/名稱;固定只顯示 public=1 POST /system/menus(分頁清單,固定 filters.public=1) 逐列「編輯」「刪除」;勾選多列出現批次刪除鈕 → §6 API 規格(點我開啟) 開啟:UC-SM-01 新增/編輯選單項 新增/編輯 Dialog:類別 Dropdown(僅既有 group)+名稱 POST /system-menu(新增) PUT /system-menu/{id}(更新) value 自動 = key;(group,key) 唯一 → UC-SM-01(點我開啟) 開啟:UC-SM-02 批次刪除 刪除/批次刪除:二次確認後逐筆 DELETE(非原子性) DELETE /system-menu/{id}(單筆;批次=FE 迴圈逐筆呼叫) Promise.allSettled 統計成功/失敗數分別 toast 勾選數 > 0 才顯示批次刪除鈕 → UC-SM-02(點我開啟) 彩色=主要互動/明細區塊,整塊可點擊跳轉本頁 §6 或對應 UC|灰=本頁結構|虛線框=dialog/批次操作條件層 API 僅列代表性 2~3 條,完整規格見 §6;本頁選單項與 RBAC 選單樹(ui_routes)為兩張不相關的表
選單管理頁版面:Header 搜尋/重置/新增/批次刪除、DataTable(可跳 §6)、新增編輯 Dialog(可跳 UC-SM-01)、批次刪除(可跳 UC-SM-02)

狀態呈現:DataTable loading 遮罩(loading.value);新增/儲存按鈕 :loading="submitted";批次刪除按鈕 :loading="batchDeleting";無 socket、無即時推播,操作後一律重打 getMenus() 全量重載清單。分頁 / 排序 / 搜尋狀態經 useTableQuery + queryStore使用者 uid + route.name 做 per-user scope 持久化(切換使用者或重登會重新套用該使用者上次的查詢狀態)。

6. API 規格

Envelope:成功 {"status": true, "data": …}、失敗 {"status": false, "error_code": "…", "msg": "…"}common/util/response_util.py:return_response)。

6.1 總清單(本頁呼叫的全部 endpoint)

分類 Method + Path 說明 完整規格
載入 GET /system/category-menu 取得所有已啟用且公開(enable=1, public=1)的 group 清單,供 Dialog 類別 Dropdown 與搜尋框關鍵字↔︎group 轉換 §6.2
載入 POST /system/menus 分頁清單查詢(本頁清單資料來源),固定只回 public=1 的列 §6.3
CRUD POST /system-menu 新增選單項 §6.4
CRUD PUT /system-menu/{id} 更新選單項 §6.4
CRUD DELETE /system-menu/{id} 刪除選單項(單筆;批次刪除靠 FE 迴圈呼叫此端點) §6.4
其他(非本頁呼叫,同 blueprint) GET /system/menu/{group} 依 group 取已啟用選單項(enable=1,依 sort 排序)——這是全系統其他頁面消費下拉選項的端點,本頁本身不呼叫 §6.5
其他(同 blueprint,未見任何呼叫方) POST /system-menu/{id}SystemMenuRoute 亦掛 POST on <int:id>)、PUT/DELETE /system-menu(無 id) 路由存在但語意不明確(POST 帶 id 與 PUT 撞名、PUT/DELETE 缺 id 無法定位單筆)——FE 未呼叫,Swagger 亦未見對應文件化用法,見 §12 坑 4

標「其他」列出於 routes.json 完整比對用;本頁 UI 只實際呼叫 §6.1 前 5 條。

6.2 [GET] /system/category-menu

無 query 參數。取得 enable=1 AND public=1 的全部選單項後,依 group 去重(set()),回傳順序不保證(Python set 無序)。

Response datastring[](group 代碼陣列,如 ["STORAGE_TYPE", "AGENT_TYPE", "ssp_party_role", ...])。

出處:jedi_system_menu/domain/service/system_menu_domain_service.py:57-68get_menu_categories)、api/system_menu/routes/system_menu_route.py:51-57SystemGroupCategoryRoute)。

6.3 [POST] /system/menus

Request(RequestMetaSchema + filters):

欄位 型別 必填 說明
pager.page / pager.page_size int 預設 1 / 25
pager.with_total bool 預設 true
sort[] array {field, order: asc/desc}
filters.group string 全域搜尋命中的 group 代碼(陣列或字串,見 §10 轉換邏輯)
filters.key string 全域搜尋關鍵字

Response data[](每項):

欄位 型別 說明
id int 主鍵
group string 選單群組代碼
key string 選單鍵名(顯示名稱)
value string 選單值(本頁固定 = key)
sort int 排序順序
enable int 1 啟用/0 停用
public int 1 公開/0 私有(本查詢固定只回 1)

⚠️ 服務層固定覆寫 filters.public = 1_filter.public = 1system_menu_service.py:41)——即使 FE 未來想查私有選單項,這支 API 也回不了,需另開端點或改此行為才行。

出處:api/system_menu/serializers/system_menu.py:20-21,33-34SystemMenuPageQueryRequest / SystemMenuPageQueryResponse)、jedi_system_menu/app/service/system_menu_service.py:36-51get_system_manus_and_pager)。

6.4 選單項 CRUD

[POST] /system-menu

Request:

欄位 型別 必填 說明
group string 選單群組代碼
key string 選單鍵名
value string 是(schema 標 required,FE 固定送 = key) 選單值
sort int 預設 0(schema default=0;service 層另有 kwargs.get("sort", 1) 的獨立預設 1,兩處預設值不一致,見 §12 坑 5)
enable int 預設 1
public int 預設 1

Response:同 §6.3 單筆形狀。(group, key) 重複 → DB 唯一索引擋下(未經 try/except 轉譯,見 §12 坑 1)。

出處:api/system_menu/serializers/system_menu.py:4-10SystemMenuRequest)、jedi_system_menu/app/service/system_menu_service.py:79-98add_system_menu)。

[PUT] /system-menu/{id}

Request:同上 6 欄位(皆非 partial——service 層 kwargs.get(k, old_value) 逐欄回填舊值,實務上等同全量覆蓋語意)。找不到 id → 404。

Response:同上。出處:jedi_system_menu/app/service/system_menu_service.py:100-121update_system_menu)。

[DELETE] /system-menu/{id}

無 body。找不到 id → 404。Response:boolBooleanResponse)。出處:jedi_system_menu/app/service/system_menu_service.py:123-136delete_system_menu)。

6.5 [GET] /system/menu/{group}(消費端點,非本頁呼叫)

無 query。Response data[]:僅回 enable=1 的列,依 sort 升冪排序((sort or 0));欄位形狀同 §6.3。此端點是全系統其他頁面(如儲存設定的 STORAGE_TYPE、Agent 管理的 AGENT_TYPE)拿下拉選項的實際呼叫點,本頁管理的資料即供應此處。出處:api/system_menu/routes/system_menu_route.py:19-29SystemGroupMenuRoute)。

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

檔案 角色
src/views/system-menu-manage/SystemMenuManage.vue 唯一檢視檔(404 行;無子元件,搜尋列 + DataTable + 新增/編輯 Dialog 全在此)
src/config/router/index.js:191-198 route system-menu-manage,path /system/system-menu-manage
src/config/api/api.js:23-26 SYSTEM_MENU/system/menu,消費端點常數,本頁未用)/SYSTEM_MENU_OBJECT/system-menu,CRUD 用)/SYSTEM_MENUS/system/menus,分頁查詢)/SYSTEM_MENU_CATEGORY/system/category-menu
src/service/BaseService.js 本頁未使用專屬 service 檔,直接用 BaseService 泛型 get/post/put/delete
src/composables/useTableQuery.js 共用分頁 / 排序 / 搜尋狀態管理(per-user + per-route scope)
src/stores/queryStore.js useTableQuery 依賴的 pinia store(filterTable / sort / pager 持久化)
src/config/locales/i18n/{zh-tw,en}/pages.jsonsystem_menu_manage namespace,約行 240-260) 頁面文案:新增/編輯/刪除/批次刪除/驗證訊息
src/config/locales/i18n/{zh-tw,en}/options.jsonmenu_category namespace) group 代碼 → 顯示文字對照表(非全量,查無翻譯的 group 直接顯示原代碼,見 §12 坑 6)
src/config/locales/i18n/{zh-tw,en}/menu.jsonsystem-menu-manage key) 側邊選單顯示名稱與說明文字

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

鏈路 Route App Service 底層
選單 CRUD / 清單 / 類別 api/system_menu/routes/system_menu_route.pySystemMenuRoute / SystemGroupMenusRoute / SystemGroupCategoryRoute / SystemGroupMenuRoute jedi_system_menu.app.service.system_menu_service.SystemMenuService(外部套件,@transaction 逐方法裝飾) jedi_system_menu.domain.service.system_menu_domain_service.SystemMenuDomainServicejedi_system_menu.infra.repository.system_menu_repo_impl.SystemMenuRepoImpl(繼承 BaseRepositoryImpl)→ ORM model jedi_system_menu.infra.models.system_menu.SystemMenu
DI wiring di_containers/system_menu/system_menu_containers.pySystemMenuRepoImplSingleton、domain/app service 為 Factory——repo 是 Singleton 但因遵循 BaseRepositoryImpl 的 lazy @property session,不落入 CLAUDE.md 提醒的「Singleton repo 直接 self.session = get_session()」地雷

DDD 提醒:本模組幾乎全部業務邏輯落在外部套件,主專案端只有 route(HTTP 邊界)+ serializer + DI 宣告,完全遵守「route 不碰 DB」。但套件內部有一處值得注意:SystemMenuService 每個 method 內部又 system_menu_domain_service = SystemMenuDomainService(SystemMenuRepoImpl())重建一份 domain service / repo,蓋掉建構子注入的 self.system_menu_domain_service(僅 get_system_manus_and_pager 例外,直接用注入實例)——這是套件內部風格不一致,非主專案程式碼,不要在主專案新模組比照這個寫法(正確做法是全程用建構子注入的實例,見 CLAUDE.md DI 段落)。

9. DB

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

讀/寫 說明 欄位詳述
public.system_menus 下拉 / 分類選單 code table(group/key/value + sort/enable/public),本頁是唯一的維護介面;全系統多處功能(Agent 類型、儲存類型、稽核方式、SSP 角色等)透過 GET /system/menu/<group> 消費此表資料,但寫入只經由本頁 §9.3

本頁無其他表讀寫。與 ui_routes(RBAC 選單樹)無關聯、無 FK——兩者是同名詞「選單」但完全獨立的表,見 §1 與 _overview §7 坑 1

9.2 ER 圖(本頁讀寫範圍)

system_menus 為單表、無 FK、無子表,不另畫 ER 圖(單表無關聯繪圖無資訊增量);資料模型定位見 _overview §6 核心資料模型圖system_menus 已收錄在該圖中,與 ui_routes 併陳對照)。

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

public.system_menus — 系統菜單表

DB comment:「系統菜單表:儲存系統菜單項目,以群組和鍵值對方式組織」。無 FK,兩個索引:pk_system_menus(PK on id)、uq_system_menus_group_key(UNIQUE on group, key)。

欄位 型別 說明
id integer 主鍵,nextval('system_menus_id_seq')
group varchar(255) NOT NULL 菜單群組(如 STORAGE_TYPE / AGENT_TYPE / ssp_party_role
key varchar(255) NOT NULL 菜單鍵名(顯示名稱),與 group 組成唯一索引
value varchar(255) NOT NULL 菜單值(本頁 UI 恆等於 key,但 schema 層 value 與 key 是獨立欄位,其他寫入路徑如 SQL migration 種子腳本可能讓兩者不同,如語言代碼 vs 顯示名)
sort integer,nullable,DB 無預設值default: null 排序順序;marshmallow schema 的 default=0 僅影響 dump 方向、非 DB 預設,實際新增經 service 層補 1(見 §12 坑 5)
enable integer,nullable,default 1 是否啟用:1 啟用、0 停用
public integer,nullable,default 1 是否公開:1 公開、0 私有;本頁清單查詢固定只回 public=1

10. 頁面邏輯與資料對應

載入時序onMounted 序列(非平行)執行:先 getMenuCategories()GET /system/category-menu)取得 group 代碼清單存入 categories(供 Dropdown 選項與 i18n 反查),完成後才 getMenus()POST /system/menus)載入分頁清單——getMenus 內部依賴 categories 做「關鍵字→group 代碼」轉換,因此刻意不平行化(程式碼註解已明講原因)。

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

畫面元素 FE state API 欄位
類別欄顯示文字 transDesc(data.group) data.group(查 lang.menu_category.<code> i18n key,查無退回原代碼)
名稱欄 直接綁定,超長截斷+tooltip data.key
搜尋框 filterTable.global.value filters.group / filters.key(兩欄同時比對關鍵字)
類別 Dropdown 選項 categories{key: 翻譯文字, value: 原代碼}[] GET /system/category-menu 回傳的 group 代碼陣列
Dialog 名稱輸入框 currentOption.key 送出時 payload.value = payload.key(FE 鏡射,非 API 強制)

搜尋關鍵字 → group 代碼轉換邏輯getMenus() 內,SystemMenuManage.vue:111-127):若使用者在全域搜尋框輸入的字串命中 i18n 翻譯後的文字(而非原始代碼),FE 會反查 categories 找出所有翻譯文字包含該關鍵字的原代碼,組成陣列覆蓋 payload.filters.group;若找不到任何對應代碼,直接移除 filters.group(避免把翻譯後的中文字串送給 BE 比對原始英文代碼、必然查無結果)。

儲存流程:新增 / 編輯共用 onSaveClick() → Vuelidate 驗證 → 依 currentOption.id 是否存在分流 create() / update();兩者皆先 payload.value = payload.key 再送出;成功後關閉 Dialog + 重打 getMenus() 全量重載(無 local state 增量更新)。

錯誤對應:BE error envelope → toast 顯示;已知碼 SYSTEM_MENU_404001SYSTEM_MENU_DATA_NOT_FOUND,編輯/刪除不存在的 id);(group,key) 重複寫入未特別轉譯 error code,直接是 DB 例外冒出的通用 500(見 §12 坑 1)。

11. 背景行為與外部依賴

類型 內容
通知 無——本頁不觸發、也不消費任何通知
Socket 無 socket / 輪詢;操作後皆重打 getMenus() 手動重新整理
jedi-* 套件 jedi-system-menu(本頁全部 domain/repo/ORM 邏輯所在,主專案僅 route + serializer + DI wiring)、jedi-common@transaction / BaseRepositoryImpl / RequestMetaSchema / EnvelopeSchema)、jedi-auth(JWT)
系統參數 無 feature flag;system_menus 本身即是「系統參數表」的一種——其他模組的下拉選項(STORAGE_TYPE/AGENT_TYPE/AUDIT_METHOD/ssp_party_role 等)皆以此表為資料源,種子值由對應 FR 的 SQL migration 腳本插入(如 scripts/sql/2026-06-19-fr039-agent-type-menu.sql

12. 邊界情況與已知坑

  1. (group, key) 唯一約束衝突未轉譯成標準錯誤格式:新增 / 更新為既有 (group, key) 組合時,SystemMenuRepoImpl.add_menu() / update_menu() 均無 try/except 包裝 SQLAlchemy IntegrityError,直接讓例外冒出——最終使用者看到的是未分類的 500,而非語意明確的 409 ConflictError。新增類似 CRUD 頁時勿比照此寫法(應照 CLAUDE.md「Exception 類別對照」用 ConflictError + 對應 error code)
  2. 類別 Dropdown 只能挑既有 group,無法新增全新分類:新增 Dialog 的類別欄位是純 Dropdown(選項來自 GET /system/category-menu),沒有「輸入新分類」的自由文字欄——要新增一個全新 group(如未來的 NEW_FEATURE_TYPE),必須先有至少一筆該 group 的種子資料存在(通常靠 SQL migration 先插入第一筆),UI 本身無法憑空建立新 group。這是設計如此,非 bug
  3. 寫入 API 全數無角色檢查:與本群其餘管理頁一致(見 _overview §5),本頁 7 支 endpoint 僅 @jwt_required(),service 層不做角色 / 能力守門,完全依賴 RBAC 選單可見性擋非管理員使用者看到入口。改動時勿誤以為是可隨意加嚴或放寬的細節,這是既有系統性設計,不是本次盤點新發現的缺口
  4. /system-menu/{id} 有 POST、/system-menu(無 id)有 PUT/DELETE,但無任何呼叫方routes.json 列出 POST /api/1.0/system-menu/<int:id>PUT /api/1.0/system-menuDELETE /api/1.0/system-menu(皆綁同一個 SystemMenuRoute class),但該 class 只定義了 post(無 id 參數)、put(self, id, ...)delete(self, id, ...) 三個方法——Flask-RESTful 對同一 URL 規則若方法簽名與路由參數不匹配會在呼叫時报錯而非啟動時。FE 完全沒有呼叫這些形狀(FE 固定用「POST 無 id 建立」「PUT/DELETE 帶 id 操作單筆」),Swagger 文件也被註解關閉(# docs.register(...)),這些多餘路由組合疑似歷史殘留或路由宣告誤植,新人不要誤以為是可用的替代呼叫方式
  5. sort 預設值兩處不一致:marshmallow schema SystemMenuRequest.sortdefault=0(僅影響 dump 方向,非 load 驗證),但 jedi_system_menu service 層 add_system_menu() 對缺 sort 的 kwargs 補的是 1if "sort" not in kwargs: kwargs["sort"] = 1)——若未來要依賴「不填 sort 就是 0」的假設寫下游邏輯,需注意實際落地值是 1 不是 0
  6. menu_category i18n 對照表非全量options.jsonmenu_category namespace 只收錄少數幾個 group 的中文翻譯(如 ENABLE_STATUS/ANS_TYPE/SYSTEM_TYPE 等),本頁清單常見的 STORAGE_TYPE/AGENT_TYPE/ssp_party_role 等 group 未列在此翻譯表transDesc() 查無對應 key 時會直接顯示原始英文/底線命名代碼——清單上看到未翻譯的原始代碼字串是預期行為,不是漏翻譯 bug(除非該 group 本來就打算給使用者可讀的中文名)
  7. 本頁清單固定看不到 public=0 的私有選單項get_system_manus_and_pager() 內部強制覆寫 _filter.public = 1,且新增 Dialog 無 public 輸入欄位(schema default 為 1)——若資料庫中存在 public=0 的舊資料(理論上可能由早期 SQL migration 種子腳本插入),本頁完全看不到也管不到,需直接查 DB 或改本頁邏輯才能觸及
  8. ui_routes vs system_menus 別混(引用 _overview §7 坑 1):本頁維護的是下拉分類 code table,跟 RBAC 選單樹是兩回事——新人最容易搞混的地方,別在改權限選單邏輯時誤動到這張表,反之亦然

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-system-menu,改動需走 CLAUDE.md「外部套件異動規範」(dev 期走 poetry path dependency)
  • 測試帳號:dev 環境系統管理員角色帳號(密碼見 .env / 部署文件)
  • 導航路徑:登入 → 系統設定選單「選單管理」(system-menu-manage,path /system/system-menu-manage
  • 前置資料:無強制前置資料即可開頁(空清單可正常顯示);要驗證「類別 Dropdown 有選項」需 DB 內已存在至少一筆 system_menus 資料(enable=1, public=1),全新環境若尚未跑過任何 seed migration,Dropdown 會是空的
  • E2Ecompliance-manager-test/ repo(Cucumber + Playwright);本頁相關 feature 檔以 system-menu / 選單管理 搜尋
  • 相關文件docs/交付文件/v1.8.0/src/doc-02-api-spec/system_menu.md(GAI-SD-02 API 規格書對應段);DB / API 全量見 GAI-SD-02/03;jedi-system-menu 套件源碼 ~/Projects/Jedicogy/module/jedi-python-package/jedi-system-menu/(若本機有 clone;本次事實蒐集實際讀取的是 .venv/lib/python3.11/site-packages/jedi_system_menu/ 已安裝版本)