通知頻道設定(Notify Channel Config)
功能群:系統管理|三層 RBAC 模型與
system_configskey-value 慣例先讀 功能群總覽 §2 / §4事實基準:2026-07-05 從 FE
src/views/notify-config/+ BEapi/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 |
3. 權限矩陣
系統管理群整體基調見 _overview §5;本頁 FE/BE 對照如下:
| 操作 | FE 判定 | BE 強制 | BE 檢查位置 |
|---|---|---|---|
| 進入通知設定頁 | ui_routes web-menu
可見性(notify_config.read,ALL) |
無端點層強制(僅 @jwt_required) |
api/system_config/routes/system_config_route.py |
| 儲存通知設定 | notify_config.update
能力控制頁內按鈕顯隱(ANY) |
⚠️ 僅 @jwt_required,無角色 /
能力守門 |
system_config_route.py:53-79(SystemConfigRoute.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.py(HIDDEN_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-124(update_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,各自欄位+儲存/測試按鈕):
📸 實機截圖待補(亮色模式)。待補清單:通知設定頁 Telegram / Discord 兩個 tab(含遮罩狀態)。
狀態呈現:
- secret 類欄位(Telegram Bot Token / Discord Webhook URL)皆用
type="password"的原生<input>(非 PrimeVuePassword元件),已設定過時顯示常數字串********(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_configsgroup 讀寫的通用 CRUD 亦見 _overview §4。
6.2 通知頻道設定(載入 / 儲存)
[GET] /system/config/NOTIFY_CONFIG/<channel>
Response
data(SystemConfigResponse,api/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-104(SystemConfigGroupRoute.get,HIDDEN_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-119(SystemConfigGroupRoute.put)→
jedi-system-config
system_config_service.update_config_by_group_key →
system_config_domain_service.py:106-124
6.3 通知頻道測試
[POST] /notify-config/<channel>/test
Request(NotifyConfigTestRequest,api/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.post(api/notify_config/routes/notify_config_route.py:15-34)→
NotifyConfigTestService.test_channel(app/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.py(SystemConfigGroupRoute) |
無主專案 app service 包裝,route 直接注入 jedi-system-config
SystemConfigService |
jedi-system-config system_config_domain_service.py →
ISystemConfigRepo |
| 通知頻道測試 | 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-config(system_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. 邊界情況與已知坑
- SECRET_MASK 是純前端慣例,非 BE 回傳值:BE 對
HIDDEN_SECRET內的 group 一律在 response 剔除secret欄位,FE 靠「value存在即代表已設定」自行決定要不要顯示********常數字串;changePwd旗標才是 BE 判斷要不要覆蓋密碼的依據,遮罩字串本身送到 BE 也無害(因為changePwd=false時 BE 根本不採用 payload 的 secret)。 - 儲存 API
無守門:通知設定儲存端點(
SystemConfigGroupRoute.put)僅@jwt_required,無角色 / 能力守門——直打 API 任何登入者皆可寫。已知缺口,本 spec 只記錄不修。 - 通知頁載入是序列非併行:
loadConfigs()內await loadDiscordConfig()完才await loadTelegramConfig(),兩者互不影響(各自 try/catch),僅影響載入耗時(多一次網路來回的延遲),非功能性 bug。 - 通知測試 vs SMTP 測試的權限落差:通知頻道測試有
is_adminapp 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帳號才能觸發通知頻道測試成功路徑;一般帳號可驗證 403NOTIFY_403001分支。帳號見 memoryreference_dev_login/.env(憑證不寫入本文件)。 - 導航路徑:登入 →
側選單「系統管理」→「通知設定」(
/system/notify-config)。 - 前置資料:通知頻道設定需先在
system_configs建立NOTIFY_CONFIG群組列才有既存值可測。 - E2E:
compliance-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。