手冊首頁 / 系統設定 / 通知頻道設定

通知頻道設定(Notify Channel Config)

功能群:系統管理|三層 RBAC 模型與 system_configs key-value 慣例先讀 功能群總覽 §2 / §4

事實基準:2026-07-05 從 FE src/views/notify-config/ + BE api/notify_config/api/system_config/ + routes.json / db_schema.json 掃出

變更紀錄

日期 FR 變更
2026-07-06 FR-047 notify-smtp-config.md 拆出:Telegram / Discord 通知頻道獨立成頁(SMTP 設定另見 SMTP 伺服器設定

1. 功能描述

通知頻道設定頁讓管理員設定系統對外主動推播的兩個即時通訊頻道:Telegram(Bot Token + Chat ID)與 Discord(Webhook URL)。設定值走 public.system_configs key-value 慣例(見 _overview §4),group=NOTIFY_CONFIG、key=TELEGRAM / DISCORD。單頁 TabView,兩個 channel 各一個 tab,各自獨立啟用開關 / 憑證欄位 / 儲存 / 測試送一則。權限基調為租戶可管(與已收斂為平台層 admin-only 的 SMTP 設定 不同)。

  • 通知設定頁(/system/notify-config):單頁 TabView,Telegram / Discord 各一個 tab,各自獨立儲存與測試。
  • 憑證欄位(Bot Token / Webhook URL)用 SECRET_MASK 遮罩慣例:已設定過顯示 ********,focus 清空、blur 未輸入則還原。

主要使用者:租戶管理員(維護本租戶通知頻道),實際觸發「測試送一則」則需 user.is_admin=True 身分。

角色速覽

角色 一句話
租戶管理員(notify_config.* 能力持有者) 可管本租戶通知頻道設定
系統管理員(user.is_admin=True 唯一可執行「通知頻道測試」API 的身分判定(非 RBAC 角色概念,是 user 記錄上的旗標)
一般使用者 無此選單(ui_routes 可見性未命中 notify_config.read

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

# 功能 說明 位置 詳述
1 Telegram 啟用開關 切換是否啟用 Telegram 通知頻道 Tab「Telegram」 UC-NC-01
2 Telegram Bot Token 設定 password input;已設定過顯示 SECRET_MASK,focus 清空、blur 未輸入則還原 Tab「Telegram」 §12 坑 1
3 Telegram Chat ID 設定 一般文字欄位,非密碼 Tab「Telegram」 §6.3
4 Telegram 測試送一則 呼叫 test API 立即送一則測試訊息 Tab「Telegram」 UC-NC-02
5 Telegram 儲存 Upsert system_configs(group=NOTIFY_CONFIG, key=TELEGRAM) Tab「Telegram」 UC-NC-01
6 Discord 啟用開關 同 #1,另一 channel Tab「Discord」 UC-NC-01
7 Discord Webhook URL 設定 password input + SECRET_MASK,同 #2 邏輯 Tab「Discord」 §12 坑 1
8 Discord 測試送一則 同 #4,另一 channel Tab「Discord」 UC-NC-02
9 Discord 儲存 Upsert(group=NOTIFY_CONFIG, key=DISCORD) Tab「Discord」 UC-NC-01
10 離開頁面未存變更提醒 hasChange 旗標 + onBeforeRouteLeave confirm dialog 頁全域

展開成 UC:UC-NC-01(通知頻道儲存)、UC-NC-02(通知頻道測試)。其餘為常規表單操作。

2. Use Case

角色速覽

角色 一句話
租戶管理員(notify_config.* 能力持有者) 可管本租戶通知頻道設定
系統管理員(user.is_admin=True 唯一可執行「通知頻道測試」API 的身分判定(非 RBAC 角色概念,是 user 記錄上的旗標)
一般使用者 無此選單(ui_routes 可見性未命中 notify_config.read

視覺慣例:判斷框(黃菱形)= 分支條件;橙框 = 擋下;紅框 = 錯誤終點(標 error code);綠框 = 成功終點;虛線 = 可選路徑(secret 欄位 focus/blur 互動)。

UC-NC-01 通知頻道設定儲存

項目 內容
角色 notify_config.update 能力者(選單可見性判定,見 §3)
前置條件 已登入;本頁對 API 呼叫角色 / 能力守門,僅 @jwt_required
產出 / 後置條件 public.system_configs(group=NOTIFY_CONFIG, key=DISCORD|TELEGRAM)upsert 一列;changePwd=false 時 secret 欄位延用既存值

UC-NC-02 通知頻道測試送一則

項目 內容
角色 user.is_admin=True(app service 層檢查,非路由層)
前置條件 已登入;若 changePwd=false 則 DB 須已有該 channel 既存 secret,否則 400
產出 / 後置條件 對外送出一則 Discord webhook 訊息 / Telegram bot API 訊息;不寫 DB
notifysmtp_save start 進入通知設定 / SMTP 設定頁 load 載入既有設定 GET /system/config/NOTIFY_CONFIG/{DISCORD|TELEGRAM} 或 GET /system/config/{uid}(SMTP) BE 一律剔除 secret(HIDDEN_SECRET group) start->load mask secret 欄位顯示遮罩 ******** (曾設定過才顯示;從未設定則空白) load->mask focus focus 該欄位 → 遮罩清空可輸入新值 mask->focus 可選:修改 secret fill 填寫其餘欄位 (啟用開關 / host,port,from / chat_id 等) mask->fill blur blur 且未輸入 → 還原遮罩 (changePwd 旗標維持 false) focus->blur blur->fill savepay 組 payload:{...欄位, changePwd} changePwd = 使用者是否手動改過 secret fill->savepay savecall PUT /system/config/NOTIFY_CONFIG/{channel} 或 PUT/POST /system/config/{uid}(SMTP,僅 @jwt_required,無角色守門) savepay->savecall savebe BE:changePwd=false → 用 DB 既存 secret 覆蓋 payload.secret changePwd=true → 採用送入新值 savecall->savebe saveok 200 → 重新 GET 設定回填(含新遮罩) Toast「儲存成功」 savebe->saveok
通知頻道儲存流程:載入既有設定→SECRET_MASK 顯示/focus 清空/blur 還原→組 payload(含 changePwd)→PUT 儲存→BE 依 changePwd 決定沿用舊 secret 或採新值
notifysmtp_test choice 使用者按「測試」 通知:測試送一則 | SMTP:測試郵件發送 testpay 組 test payload: 通知={value:{...}, changePwd} (NotifyConfigTestRequest,與 save 的 flat shape 不同) SMTP={to, smtp_config, changePwd}(Dialog 先收收件信箱) choice->testpay testcall 通知:POST /notify-config/{channel}/test SMTP:POST /mail/test testpay->testcall admin BE 檢查(僅通知 channel): user.is_admin == true? testcall->admin forbid 403 NOTIFY_403001 僅系統管理員可執行通知測試 (SMTP /mail/test 無此檢查,僅 @jwt_required) admin->forbid resolve changePwd=false → 讀 DB 既存 secret 補回 value (無既存 secret → 400 NOTIFY_400002) SMTP:依收件 email domain 反查 system_configs.key (找不到 fallback '*';仍無回 false) admin->resolve 是 / SMTP 免此檢查 send 對外送出(Discord webhook / Telegram bot API / SMTP 寄信) 獨立於 DB transaction 之外 resolve->send sendok 200 → Toast「測試訊息已送出/發送成功」 send->sendok sendfail 外部服務失敗 / 例外 通知:400 NOTIFY_400001 SMTP:⚠️ raise BadRequestError(MAIL_SEND_FAILED) 但該碼未在 ErrorCode 定義(AttributeError,見 §12) send->sendfail 外部服務逾時 / 拒絕 / 例外
通知頻道測試流程:組 test payload(shape 與 save 不同)→is_admin 檢查→resolve secret(changePwd=false 補既存值)→對外送出

3. 權限矩陣

系統管理群整體基調見 _overview §5;本頁 FE/BE 對照如下:

操作 FE 判定 BE 強制 BE 檢查位置
進入通知設定頁 ui_routes web-menu 可見性(notify_config.readALL 無端點層強制(僅 @jwt_required api/system_config/routes/system_config_route.py
儲存通知設定 notify_config.update 能力控制頁內按鈕顯隱(ANY ⚠️ @jwt_required,無角色 / 能力守門 system_config_route.py:53-79SystemConfigRoute.put / SystemConfigGroupRoute.put
測試通知頻道 同儲存 app service 層檢查 user.is_admin,非 admin → NOTIFY_403001 app/notify_config/service/notify_config_test_service.py:53-57

⚠️ 儲存 API 無守門,測試 API 有 admin 檢查:通知設定的儲存端點只有 @jwt_required(直打 API 任何登入者皆可寫),但通知頻道測試在 app service 層有 is_admin 檢查(NOTIFY_403001)。收進 §12。此不對稱與 SMTP 設定 的權限落差(SMTP 測試完全無檢查)不同,設計新測試端點時應比照本頁 is_admin 模式。

4. 狀態機與前置條件

通知頻道無業務狀態機(system_configs 是單純 key-value 覆蓋式設定),前置條件集中在 SECRET_MASK 語意測試前 secret 存在性

條件 效果 出處
Discord / Telegram secret 欄位:BE 一律回傳前先剔除 secret FE 收到的 GET response 無 secret 明文,只能靠「有無 value」判斷要不要顯示遮罩 system_config_route.pyHIDDEN_SECRET = ['SMTP','THIRD_PARTY_LOGIN','NOTIFY_CONFIG']
changePwd=false(未手動改過 secret) PUT 時 BE 用 DB 既存 secret 覆蓋 payload,忽略前端送來的遮罩字串 jedi-system-config system_config_service.py:87-98 + system_config_domain_service.py:106-124update_config_by_group_key
changePwd=true 採用 payload 送入的新值 同上
通知頻道測試 changePwd=false 但 DB 無既存 secret 400 NOTIFY_400002 notify_config_test_service.py:41-51
通知頻道測試 channel 非 DISCORD/TELEGRAM 400 NOTIFY_400003 notify_config_test_service.py

5. UI 設計

版面骨架(單頁 TabView:Telegram / Discord 兩個 tab,各自欄位+儲存/測試按鈕):

通知頻道設定 — 單頁 TabView(Telegram / Discord 各自獨立儲存與測試) [ Telegram ] [ Discord ] 切換 tab(各 tab 獨立 hasChange 攔截) 開啟:§6.2 通知頻道設定(載入 / 儲存) Tab · Telegram 啟用開關 InputSwitch Bot Token(password+SECRET_MASK) Chat ID(一般文字) GET /system/config/NOTIFY_CONFIG/{channel} PUT /system/config/NOTIFY_CONFIG/{channel} → §6.2(點我) 開啟:§6.2 通知頻道設定(載入 / 儲存) Tab · Discord 啟用開關 InputSwitch Webhook URL(password+SECRET_MASK) (無 Chat ID 欄位) GET /system/config/NOTIFY_CONFIG/{channel} PUT /system/config/NOTIFY_CONFIG/{channel} → §6.2(點我) 開啟:UC-NC-02 通知頻道測試送一則 各 tab 底部 · 測試送一則(is_admin 才通過) 用當前 tab 值組 test payload(shape 與儲存不同);非 admin → 403 NOTIFY_403001 POST /notify-config/{channel}/test → UC-NC-02(點我) 彩色=可點跳本頁小節錨點(teal=§6.2 載入/儲存/coral=UC-NC-02 測試)|灰=結構描述(Tab chrome) API 僅列代表性,完整規格見 §6
通知頻道設定版面:TabView(Telegram tab:啟用開關/Bot Token/Chat ID/儲存/測試;Discord tab:啟用開關/Webhook URL/儲存/測試),可點跳轉本頁 §6 / UC 錨點

📸 實機截圖待補(亮色模式)。待補清單:通知設定頁 Telegram / Discord 兩個 tab(含遮罩狀態)。

狀態呈現

  • secret 類欄位(Telegram Bot Token / Discord Webhook URL)皆用 type="password" 的原生 <input>(非 PrimeVue Password 元件),已設定過時顯示常數字串 ********SECRET_MASK);使用者 focus 該欄位時清空以便輸入新值,若 blur 時未輸入任何字元則還原遮罩、且不標記「已變更」。
  • 頁面載入中顯示 LoadingState(page size)。
  • 頁面有 hasChange 旗標 + onBeforeRouteLeave 攔截,離開前跳確認 dialog。
  • 無 socket;儲存後皆重新 GET 該筆設定回填(非樂觀更新)。

6. API 規格

Envelope:成功 {"code": 1, "data": …}、失敗 {"code": 0, "msg": "…"}(HTTP 4xx/5xx,見專案 CLAUDE.md Response Format)。

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

分類 Method + Path 說明 完整規格
載入 GET /api/1.0/system/config/NOTIFY_CONFIG/<channel> 讀單一 channel 設定(secret 已剔除) §6.2
儲存 PUT /api/1.0/system/config/NOTIFY_CONFIG/<channel> Upsert channel 設定 §6.2
測試 POST /api/1.0/notify-config/<channel>/test 送一則測試訊息(payload shape 與儲存不同) §6.3

endpoint 存在性已對 routes.json 核實;system_configs group 讀寫的通用 CRUD 亦見 _overview §4

6.2 通知頻道設定(載入 / 儲存)

[GET] /system/config/NOTIFY_CONFIG/<channel>

Response dataSystemConfigResponseapi/system_config/serializers/system_config.py:9-15):

欄位 型別 說明
uid string 設定唯一識別碼
group string 固定 NOTIFY_CONFIG
key string DISCORD / TELEGRAM
value dict Discord:{enabled, secret}(secret 已剔除);Telegram:{enabled, secret, chat_id}(secret 已剔除)
created_at / updated_at datetime %Y-%m-%d %H:%M:%S

出處:api/system_config/routes/system_config_route.py:92-104SystemConfigGroupRoute.getHIDDEN_SECRET 剔除 secret)

[PUT] /system/config/NOTIFY_CONFIG/<channel>

Request(SystemConfigRequest 沒有嚴格限制 value 內部欄位,實際送入內容為 FE 組裝):

欄位 型別 必填 說明
enabled bool 是否啟用該頻道
secret string Bot Token(Telegram)/ Webhook URL(Discord);changePwd=false 時此欄位內容(即使是遮罩字串)會被 BE 用既存值覆蓋,故送遮罩也安全
chat_id string 否(Telegram 專用) Telegram chat ID
changePwd bool 是否為使用者手動改過 secret;決定 BE 採用送入值或延用舊值

Response:同 GET shape。出處:system_config_route.py:106-119SystemConfigGroupRoute.put)→ jedi-system-config system_config_service.update_config_by_group_keysystem_config_domain_service.py:106-124

6.3 通知頻道測試

[POST] /notify-config/<channel>/test

Request(NotifyConfigTestRequestapi/notify_config/serializers/notify_config.py:4-6):

欄位 型別 必填 說明
value dict 與儲存 payload 不同 shape{enabled, secret, chat_id?}(Telegram)/ {enabled, secret}(Discord),不含 changePwd
changePwd bool 否(預設 true) 與 §6.2 同語意;false 時從 DB 讀既存 secret 補入 value.secret

Response(NotifyConfigTestResponse):{channel, sent_at}(ISO 8601 UTC)。

BE 鏈路NotifyConfigTestRoute.postapi/notify_config/routes/notify_config_route.py:15-34)→ NotifyConfigTestService.test_channelapp/notify_config/service/notify_config_test_service.py:53-83):① 檢查 get_user_context().is_admin,否則 403 NOTIFY_403001;② 檢查 channel 屬於 DISCORD/TELEGRAM,否則 400 NOTIFY_400003;③ changePwd=false 時讀 DB 既存 secret(無既存值 → 400 NOTIFY_400002);④ 呼叫 jedi-notification NotificationService 對外送出,失敗 → 400 NOTIFY_400001

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

檔案 角色
src/views/notify-config/NotifyConfigForm.vue 通知設定頁(TabView:Telegram / Discord 各自欄位、SECRET_MASK 邏輯、儲存 / 測試)
src/config/router/index.js:273-287 route:notify-config
src/config/api/api.js:328-336 SYSTEM_CONFIG / NOTIFY_CONFIG_TEST 端點常數
src/config/locales/i18n/<locale>/notify-config.json 通知設定頁 i18n(獨立檔案)
src/service/BaseService.js 共用 axios 封裝(get/post/put/delete

8. 後端檔案地圖

鏈路 Route App Service 底層
通知設定 CRUD api/system_config/routes/system_config_route.pySystemConfigGroupRoute 無主專案 app service 包裝,route 直接注入 jedi-system-config SystemConfigService jedi-system-config system_config_domain_service.pyISystemConfigRepo
通知頻道測試 api/notify_config/routes/notify_config_route.py::NotifyConfigTestRoute app/notify_config/service/notify_config_test_service.py::NotifyConfigTestService(本頁唯一在主專案內做權限判斷的服務) jedi-notification NotificationService(Discord/Telegram HTTP client)

DDD 提醒system_config_route.py 的 Route class 直接注入外部套件 SystemConfigService、無主專案 app service 層,屬「外部套件擁有 domain」情形(比照 role-manage 的 jedi-auth 模式)——代表本頁儲存 API 完全無法插入主專案自訂角色檢查(除非改包一層 app service)。NotifyConfigTestService 是本頁唯一在主專案內做權限判斷的服務,新增類似「測試 / 敏感操作」端點應比照它的 is_admin 檢查模式,不要比照 system_config_route.py 的無守門寫法。

9. DB

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

讀/寫 說明 詳述
public.system_configs 讀 / 通知頻道設定(group=NOTIFY_CONFIG,key=DISCORD/TELEGRAM §9.3
public.roles / public.role_capabilities / public.capabilities / public.route_capabilities / public.ui_routes 決定本頁選單可見性(notify_config.* 能力) _overview §6

本頁無獨立專表:通知只是 system_configs 的一個 group 值,不建專表(_overview §7 坑 7)。

9.2 ER 圖

本頁讀寫的表結構單純(單表 + RBAC 唯讀依賴),沿用 _overview §6 核心資料模型圖,不另畫 ER 圖。

9.3 核心表欄位(取自 db_schema.json,含 DB comment)

public.system_configs(comment:「系統配置表:儲存系統設定參數,以群組和鍵值對方式組織」):

欄位 型別 說明
id integer 主鍵 ID
uid varchar(50) 配置唯一識別碼 UUID
group varchar(255) 配置群組:本頁用 NOTIFY_CONFIG
key varchar(255) 配置鍵名:DISCORD / TELEGRAM
value jsonb 配置值:{enabled, secret, chat_id?}
tenant_id integer(nullable) 設定歸屬租戶
org_unit_id integer(nullable) 設定歸屬部門(本頁未使用)
created_at / updated_at timestamp 建立 / 更新時間

created_user / updated_user 欄位:本表只有 created_at/updated_at 時間戳,無操作者欄位,不適用專案 CLAUDE.md 的審計欄位 enrich 規範。

10. 頁面邏輯與資料對應

通知頁載入時序onMounted → 依序 loadDiscordConfig()loadTelegramConfig()(非併行,兩次獨立 GET,任一 404 皆 catch 後保持預設空值不拋錯)。

關鍵欄位對應

畫面元素 FE state API 欄位
Telegram/Discord 啟用開關 telegram.enabled / discord.enabled value.enabled
Telegram Bot Token / Discord Webhook URL telegram.secret / discord.secret(顯示 SECRET_MASK 常數) value.secret(GET 時已被 BE 剔除,僅 PUT 送出)
Telegram Chat ID telegram.chat_id value.chat_id
是否視為「已改過密碼」 discordPwdChanged / telegramPwdChanged(bool ref) payload 頂層 changePwd

儲存流程:兩個 tab 各自獨立 PUT(無整頁 save);成功後重打對應 loadXxxConfig() 回填(含新遮罩狀態)。

錯誤對應:儲存 / 測試失敗 → toast 顯示 err.message 或固定文案(test_failed);BE error envelope 常見碼見 §6 各段。

11. 背景行為與外部依賴

類型 內容
通知 無站內通知;本頁「測試」本身即是對外發送外部通知(Discord/Telegram),非系統站內通知
Job / 排程
Socket 無;操作後皆手動重打 GET 回填
jedi-* 套件 jedi-system-configsystem_configs CRUD 之 service/domain/repo)、jedi-notification(實際對外送信 / webhook / bot API 的 client,NotificationService + 各 channel 的 ConfigDTO/RequestDTO)、jedi-auth(JWT / is_admin context)
系統參數 無 feature flag;HIDDEN_SECRET = ['SMTP', 'THIRD_PARTY_LOGIN', 'NOTIFY_CONFIG'] 是寫死在 system_config_route.py 的常數,決定哪些 group 的 GET response 剔除 secret

12. 邊界情況與已知坑

  1. SECRET_MASK 是純前端慣例,非 BE 回傳值:BE 對 HIDDEN_SECRET 內的 group 一律在 response 剔除 secret 欄位,FE 靠「value 存在即代表已設定」自行決定要不要顯示 ******** 常數字串;changePwd 旗標才是 BE 判斷要不要覆蓋密碼的依據,遮罩字串本身送到 BE 也無害(因為 changePwd=false 時 BE 根本不採用 payload 的 secret)。
  2. 儲存 API 無守門:通知設定儲存端點(SystemConfigGroupRoute.put)僅 @jwt_required,無角色 / 能力守門——直打 API 任何登入者皆可寫。已知缺口,本 spec 只記錄不修。
  3. 通知頁載入是序列非併行loadConfigs()await loadDiscordConfig() 完才 await loadTelegramConfig(),兩者互不影響(各自 try/catch),僅影響載入耗時(多一次網路來回的延遲),非功能性 bug。
  4. 通知測試 vs SMTP 測試的權限落差:通知頻道測試有 is_admin app service 層檢查(NOTIFY_403001),但 SMTP 設定 的測試寄信完全無此檢查——設計新測試端點應比照本頁而非 SMTP。

13. 開發與驗證

  • 跑起來:BE python main_socketio.py(port 8000,log 在 log/app.log);FE 在 compliance-manager-fe/ 起 Vite dev server。BE 改 service code 後必須重啟(無 hot reload)。
  • 測試帳號:需 is_admin=true 帳號才能觸發通知頻道測試成功路徑;一般帳號可驗證 403 NOTIFY_403001 分支。帳號見 memory reference_dev_login / .env(憑證不寫入本文件)。
  • 導航路徑:登入 → 側選單「系統管理」→「通知設定」(/system/notify-config)。
  • 前置資料:通知頻道設定需先在 system_configs 建立 NOTIFY_CONFIG 群組列才有既存值可測。
  • E2Ecompliance-manager-test/ repo(Cucumber + Playwright);本頁相關 feature 檔以 notify-config / 通知設定 搜尋。
  • 相關文件:FR-020(通知設定原始 design,docs/features/FR-020-2604-notify-config/);SMTP 伺服器設定 → smtp-config.md;DB / API 全量見 GAI-SD-02/03。