手冊首頁 / 溝通協作 / 公告管理

公告管理(Bulletin Management)

功能群:協作(collab)|本群無獨立 _overview,共用概念見各頁;SPEC 索引見 README

事實基準:2026-07-05 從 FE / BE code 掃出(jedi-bulletin 套件為底、主專案 extend org_unit 關聯與 created/updated user 顯示名);API schema 逐條對照 marshmallow serializer 與 service 組裝碼

1. 功能描述

公告管理頁是系統管理者維護「公告」的後台清單 + 表單頁面:可新增、編輯、刪除公告,並指定發布類別、目標部門、生效區間與啟用狀態。是 公告(檢視) 的管理端鏡像——本頁看到「全部/自己建立的」公告,檢視端只看到 auth=true(已發布且未過期)的公告。

主要使用者:具公告管理選單可見性的系統管理者/權限管理者(BE 未做角色白名單,見 §3)。

角色速覽(本頁無正式角色定義,見 §3 權限說明):

角色 一句話
系統管理者 公告管理選單可見的操作者——實際上任何登入者打 API 都能操作,FE 僅用選單可見性把關
一般使用者 公告(檢視) 端只讀已發布公告,不會進到本頁

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

# 功能 說明 位置 詳述
1 公告清單(分頁 / 排序 / 搜尋) DataTable lazy 分頁;title/release_time/expire_time/enable/created_user_name/updated_at 等欄可排序;關鍵字搜尋比對 title/category/created_user_name/updated_user_name/created_user/updated_user BulletinManage.vue 主表格 UC-BM-01
2 重置查詢 清空搜尋字串、排序、回到第一頁(queryStore.resetCurrent() Header「重置查詢」按鈕 §7
3 新增公告 導向空白表單頁 Header「新增公告」按鈕 UC-BM-02
4 編輯公告 導向表單頁並帶入既有資料 表格列「編輯」icon UC-BM-02
5 刪除公告 二次確認 dialog 後呼叫 DELETE,成功後重新查詢 表格列「刪除」icon UC-BM-03
6 瀏覽公告(管理端內) 導向公告檢視頁(bulletin-view),from 帶回本頁 route name 供麵包屑動態指回 表格列「瀏覽」icon §7
7 類別欄位顯示 category code 轉譯:system_menu 選單值找不到時 fallback 顯示「其他」 表格「類別」欄 §6.1
8 目標部門欄位顯示 公告關聯部門 chip 清單,超過 5 個收合成「+N…」並 tooltip 展開全部 表格「公告部門」欄 §9.1
9 表單:標題 / 內容 標題純文字(必填);內容為 CKEditor 富文字(必填),存成 HTML 字串 BulletinForm.vue UC-BM-02
10 表單:類別下拉 選項來自 GET /system-menu/BULLETIN_CATEGORY(必填) BulletinForm.vue §6.1
11 表單:目標部門多選 MultiSelect,選項來自部門 menu store(必填,至少一個) BulletinForm.vue §6.1
12 表單:起訖時間 兩個 Calendar(年-月-日),送出時正規化成 YYYY-MM-DD 00:00:00(皆必填) BulletinForm.vue UC-BM-02
13 表單:是否發布(enable) InputSwitch,true↔︎1 / false↔︎0,非必填(未勾選送 0 BulletinForm.vue UC-BM-02
14 表單驗證 vuelidate required 規則:title / category / org_units / release_time / expire_time / content;失敗時自動 focus 第一個錯誤欄位 BulletinForm.vue §12 坑 1
15 取消編輯 二次確認 dialog 後 router.back(),不送出任何寫入 BulletinForm.vue 取消按鈕 §7
16 全頁遮罩 loading 載入既有資料 / 送出儲存時顯示全螢幕半透明遮罩 + spinner BulletinForm.vue §5

UC(§2)只展開 1(清單載入/篩選)/ 2(新增與編輯共用表單流程)/ 3(刪除)三條核心操作路徑;其餘欄位顯示邏輯已在總覽表描述完整。

2. Use Case

角色速覽(本頁無正式角色定義,見 §3 權限說明):

角色 一句話
系統管理者 公告管理選單可見的操作者——實際上任何登入者打 API 都能操作,FE 僅用選單可見性把關
一般使用者 公告(檢視) 端只讀已發布公告,不會進到本頁

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

UC-BM-01 公告清單載入與篩選

項目 內容
角色 任一已登入使用者(BE 無角色守門,見 §3)
前置條件 已登入(JWT 有效)
產出 / 後置條件 無寫入;回傳分頁公告清單,org_unit_id 有值時只回自己建立的公告(§12 坑 2)
bulletin_mgmt_uc_list start 進入公告管理頁 (onMounted) load_menu 平行載入: GET /system-menu/BULLETIN_CATEGORY start->load_menu build_payload buildPayload():組 pager/sort/filters start->build_payload call_api POST /bulletins build_payload->call_api check_jwt BE 檢查:JWT 是否有效? call_api->check_jwt err_401 401 未授權 check_jwt->err_401 check_org BE 檢查: user.org_unit_id 是否有值? check_jwt->check_org scope_self 加過濾: created_user = 自己 login_name check_org->scope_self 有部門 scope_all 不加過濾: 回傳全部公告 check_org->scope_all org_unit_id=null render 渲染 DataTable (分頁/排序/關鍵字搜尋觸發重查) scope_self->render scope_all->render search_action 使用者輸入搜尋字 / 換頁 / 排序 render->search_action search_action->build_payload 重新觸發
UC-BM-01 流程:進頁載入 → buildPayload 組分頁/排序/搜尋 → BE 依 user.org_unit_id 決定過濾規則 → 渲染表格

UC-BM-02 新增/編輯公告

項目 內容
角色 任一已登入使用者(同上)
前置條件 表單五個必填欄位(title/category/org_units/release_time/expire_time)皆有值
產出 / 後置條件 bulletins 一列新增或更新;bulletin_org_units 關聯表 full-replace(先刪後建,見 §6.4)
bulletin_mgmt_uc_create start 點「新增公告」或 列表列「編輯」icon open_form 開啟表單頁;編輯模式另 GET /bulletin/{uid} 帶入資料 (org_units 物件陣列 → map 成 uid 陣列) start->open_form fill_form 填寫:標題/類別/目標部門/ 起訖時間/發布開關/內容 open_form->fill_form click_save 點「儲存」 fill_form->click_save fe_validate FE 檢查:vuelidate 五個必填欄位皆有值? click_save->fe_validate fe_block 擋下送出, focus 第一個錯誤欄位 fe_validate->fe_block normalize 正規化 release_time/expire_time 為 YYYY-MM-DD 00:00:00 fe_validate->normalize branch uid 是否存在? 新增走 POST /bulletin 編輯走 PUT /bulletin/{uid} normalize->branch be_delete_link 編輯時:先整批刪除 既有 bulletin_org_units 關聯 branch->be_delete_link 編輯 be_upsert BE:更新/新增 bulletins 主表列 branch->be_upsert 新增 be_delete_link->be_upsert be_check_org 逐一查 org_unit_uid 是否存在? be_upsert->be_check_org err_404 404 NotFound BULLETIN_404001 be_check_org->err_404 be_rebuild_link 重建 bulletin_org_units 關聯(逐一 insert) be_check_org->be_rebuild_link 是(或無 org_units 送出) success toast 成功 → router.push 導回清單頁 be_rebuild_link->success
UC-BM-02 流程:開表單(新增空白/編輯帶入資料)→ FE vuelidate 擋 → 送出 → BE upsert bulletins + 重建 org_unit 關聯 → 導回清單

UC-BM-03 刪除公告

項目 內容
角色 任一已登入使用者(同上)
前置條件 二次確認 dialog 按下「確定」
產出 / 後置條件 bulletins 該列實際被刪除(非軟刪除,見 §12 坑 3);bulletin_org_units 隨 FK CASCADE 一併刪除
bulletin_mgmt_uc_delete start 點列表列「刪除」icon confirm_dialog 二次確認 dialog: 確定要刪除[標題]嗎? start->confirm_dialog cancel 取消, 不送出任何請求 confirm_dialog->cancel 取消 call_delete DELETE /bulletin/{uid} confirm_dialog->call_delete 確定 check_jwt BE 檢查:JWT 是否有效? call_delete->check_jwt err_401 401 未授權 check_jwt->err_401 check_exists BE 查詢: bulletins 該 uid 是否存在? check_jwt->check_exists return_false 回傳 false (非 404,靜默失敗) check_exists->return_false 不存在 hard_delete session.delete() 實際刪除 bulletins 該列 (非軟刪除,is_delete 欄位未使用) check_exists->hard_delete 存在 cascade FK CASCADE: bulletin_org_units 關聯列一併刪除 hard_delete->cascade toast_success toast 成功 → 重新查詢清單 cascade->toast_success
UC-BM-03 流程:點刪除 icon → 二次確認 dialog → DELETE 打 uid → BE 呼叫 base repo delete_by_uid(硬刪除)→ 重新查詢清單

3. 權限矩陣

操作 FE 判定 BE 強制 BE 檢查位置
選單 / 頁面可見性 選單可見性(ui_routes 標準機制,非公告專屬 capability) 無——路由僅 @jwt_required() api/bulletin/routes/bulletin_route.py:25,43,53,65,76
清單查詢範圍 無額外判定,BE 回什麼顯示什麼 user.org_unit_id 決定:有部門 → 只回 created_user = 自己org_unit_id 為 null → 不限制,回全部 app/bulletin/service/bulletin_service.py:38-45
新增 / 編輯 / 刪除 表格列按鈕恆顯示(canEdit() 在管理頁恆 true,只有 view 模式才隱藏) 無角色檢查,任何已登入者皆可操作 api/bulletin/routes/bulletin_route.py(僅 @jwt_required(),無 require_manager 等守門)

⚠️ 已知缺口(維護時注意):公告新增/編輯/刪除三個寫入操作,BE 完全沒有角色或 capability 二次驗證,僅要求已登入。目前僅靠 FE 選單可見性擋一般使用者進入本頁;若之後要收緊(例如限定系統管理者角色才能寫入),需在 BulletinRoute 補角色檢查——現況不要當作範本複製到其他寫入 API。詳見 §12 坑 4。

4. 狀態機與前置條件

本頁公告本身沒有狀態機(只有 enable 布林開關 + 時間區間),與稽核輪次等狀態機無關。相關 gate:

條件 效果 出處
enable = 1release_time <= nowexpire_time >= now 公告在 檢視端auth=true 查詢)才會顯示——管理端本頁不受此限,恆顯示全部(依 §3 範圍規則) domain/bulletin/service/bulletin_domain_service.py:16-19infra _gen_filters
表單五個必填欄位未填 送出被 vuelidate 擋下,不會打 API BulletinForm.vue:31-52(rules)
user.org_unit_id 有值 清單查詢自動加 created_user = 自己(見 §3) bulletin_service.py:43-44

5. UI 設計

📸 實際畫面截圖待補(一律亮色模式拍攝):以 playwright 對 STG 擷取,預計補以下三張到 assets/img/: ① 公告管理清單整頁(表格 + 篩選列 + 類別/部門欄)bulletin-mgmt-list.png ② 新增/編輯表單整頁(標題、類別、部門、起訖時間、發布開關、CKEditor 內容)bulletin-mgmt-form.png ③ 刪除二次確認 dialog 特寫 bulletin-mgmt-delete-confirm.png

版面骨架(清單頁:Header + DataTable;表單頁:單欄表單卡片;刪除為二次確認 dialog):

Header 列:關鍵字搜尋 +「重置查詢」+「新增公告」按鈕 queryStore.resetCurrent() ・ 新增公告 → 導向 bulletin-form 開啟:公告清單載入與篩選 §6 API 規格 公告清單 DataTable:標題/類別/目標部門 chip/起訖時間/發布/建立修改人員 POST /bulletins(pager/sort/filters;org_unit_id 有值時僅回自己建立) GET /system-menu/BULLETIN_CATEGORY(類別欄轉譯) → #6-api-規格 ・ #uc-bm-01-公告清單載入與篩選(點我開啟) 列:標題/類別/部門 chip(+N…)/起訖時間/發布/建立人 [瀏覽] [編輯] [刪除] …(分頁列表,DataTable lazy 分頁) 開啟:新增/編輯公告 UC-BM-02 新增/編輯公告表單(單欄卡片,導頁而非 dialog) POST /bulletin(新增) PUT /bulletin/{uid}(更新,full payload) GET /bulletin/{uid}(編輯帶入既有資料) 標題(必填)/類別下拉(必填,system-menu) 目標部門 MultiSelect(必填,至少一個) 起訖時間(必填)/發布 InputSwitch/內容 CKEditor(必填) → #uc-bm-02-新增編輯公告(點我開啟) 開啟:刪除公告 UC-BM-03 刪除公告二次確認 dialog DELETE /bulletin/{uid} 確定要刪除這則公告嗎? (硬刪除,非軟刪除,無復原) 取消 確定刪除 → #uc-bm-03-刪除公告(點我開啟) 彩色=可點跳轉本頁 §6/UC,灰=本頁結構(Header/搜尋列)|虛線=表單/dialog 層 API 僅列代表性 2~3 條,完整規格見 §6
公告管理 wireframe:灰=Header 搜尋/新增列;teal=公告清單 DataTable(可點 → §6 API 規格);coral=新增/編輯表單(可點 → UC-BM-02);pink 虛線=刪除二次確認 dialog(可點 → UC-BM-03)

狀態呈現:清單載入中 DataTable 內建 loading 遮罩(submit state);表單頁載入既有資料 / 送出儲存時顯示全螢幕半透明遮罩 + ProgressSpinner(非局部);無 socket,儲存後靠 router.push 導回清單頁觸發重新載入(非本地增量更新)。

6. API 規格

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

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

分類 Method + Path 說明 完整規格
載入 POST /bulletins 分頁公告清單(本頁唯一列表 API) §6.2
載入 GET /bulletin/{uid} 單筆公告詳情(編輯表單帶入資料) §6.3
寫入 POST /bulletin 新增公告 §6.4
寫入 PUT /bulletin/{uid} 更新公告(full payload,非 partial patch) §6.4
寫入 DELETE /bulletin/{uid} 刪除公告(實際為硬刪除,見 §12 坑 3) §6.4
選單 GET /system-menu/BULLETIN_CATEGORY 公告類別下拉選項(通用 system-menu 機制,非公告專屬 API)
選單 部門 menu store(menuStore.fetchOrgUnitMenu() 表單目標部門 MultiSelect 選項

DELETE /bulletin(無 {uid},body 帶 uid)與 PUT /bulletin(同)路由亦存在於 routes.json,但本頁 FE 一律走 {uid} path variant;無 {uid} 的變體本頁未使用,故不展開。

6.2 公告清單類

[POST] /bulletins

Request(繼承 RequestMetaSchema 分頁/排序 + filters 巢狀物件):

欄位 型別 必填 說明
pager object {page, page_size}
sort array [{field, order}]
filters.title / .category / .created_user_name / .updated_user_name / .created_user / .updated_user string 全域搜尋套用於 allowFilterFields 定義的欄位(BulletinManage.vue:28
filters.auth bool 本頁固定不送(false);檢視頁 固定送 true
filters.enable / .is_delete int 狀態過濾(本頁 UI 未提供對應篩選欄位,僅 API 層支援)

出處:api/bulletin/serializers/bulletin_query.py:5-23BulletinQueryRequest / BulletinPageQueryRequest

Response data(陣列,每項):

欄位 型別 說明
uid string 公告識別碼
title / category / content string 標題 / 類別 code / 內容(HTML 字串,fields.Raw
org_units[] array {uid, name}dump_only,唯讀顯示用)
release_time / expire_time date(YYYY-MM-DD 生效區間(僅日期,無時分秒)
enable int 1 啟用 / 0 停用
created_user / created_user_name string login_name / nickname(審計欄位規範:nickname 由 ExtendedBulletin.created_user_name hybrid_property 從 User.nickname 查得)
updated_user / updated_user_name string 同上
created_at / updated_at datetime(YYYY-MM-DD HH:MM:SS 審計時間

出處:api/bulletin/serializers/bulletin.py:15-32BulletinResponse / BulletinPageQueryResponse);分頁彙整 app/bulletin/service/bulletin_service.py:33-55

6.3 公告詳情類

[GET] /bulletin/{uid}

無 body。Response data:同 §6.2 單筆形狀(BulletinResponse)。出處:api/bulletin/routes/bulletin_route.py:45-48

6.4 公告寫入類

[POST] /bulletin

Request:

欄位 型別 必填 說明
title string 標題
category string 類別 code(對應 BULLETIN_CATEGORY menu)
org_units array[string] 目標部門 UID 陣列(寫入時是 full-replace 語意:後端逐一查部門、關聯進 bulletin_org_units
content raw 內容 HTML 字串(CKEditor 輸出,無格式驗證)
release_time / expire_time date(%Y-%m-%d 否(FE 端 vuelidate 視為必填,schema 層未標 required) 生效區間
enable int 否(default 0) 是否啟用

出處:api/bulletin/serializers/bulletin.py:5-12BulletinRequest);建立邏輯 app/bulletin/service/bulletin_service.py:78-100add_bulletin:先建 bulletins 列,再逐一查 org_unit_uidbulletin_org_units 關聯,查無部門 → NotFound(BULLETIN_404001)

Response:同 §6.2 單筆形狀(含新建立的 uid)。

[PUT] /bulletin/{uid}

Request:同 POST 的 BulletinRequest非 partial patch——FE 送出整包 currData)。

更新邏輯(app/bulletin/service/bulletin_service.py:102-125update_bulletin):

  1. 更新 bulletins 主表欄位(title / content / category / enable / release_time / expire_time / updated_user)
  2. 先整批刪除該公告全部 bulletin_org_units 關聯(bulletin_org_unit_domain_service.delete_by_bulletin_id
  3. 再依 request 的 org_units[] 逐一重建關聯——org_units 省略或空陣列會清空全部部門關聯

Response:同 GET 單筆形狀。

[DELETE] /bulletin/{uid}

無 body。Response data:布林值(BooleanResponse)。出處:api/bulletin/routes/bulletin_route.py:74-79;刪除邏輯 domain/bulletin/service/bulletin_domain_service.py:47-48IBaseRepo.delete_by_uidsession.delete(),見 §12 坑 3)

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

檔案 角色
src/views/bulletins/BulletinManage.vue 公告管理清單頁(同元件也被 bulletin-list route 以 view=true prop 複用成公告檢視清單,見 公告檢視
src/views/bulletins/BulletinForm.vue 新增/編輯表單頁(同一元件依 route.query.uid 是否存在切換新增/編輯模式)
src/config/router/index.js:10-23 兩條 route:bulletin-manage(path /bulletin/bulletin-manage)/ bulletin-form(path /bulletin/bulletin-form,query uid
src/config/api/api.js:83-84 BULLETINS(列表)/ BULLETIN(單筆 CRUD)API 常數
src/service/BaseService.js axios 封裝(post/get/put/delete),本頁唯一呼叫方式
src/composables/useTableQuery.js 分頁/排序/搜尋狀態管理(跨頁共用 composable,依 route scope + user uid 持久化)
src/stores/queryStore.js useTableQuery 底層 pinia store,持久化查詢狀態
src/stores/menuStore.js fetchOrgUnitMenu() — 表單部門 MultiSelect 選項來源
src/components/CkEditorToolBar.vue 表單內容欄位的富文字編輯器工具列
src/service/BulletinService.js 未使用的殘留檔——讀寫本地 demo JSON(/demo/data/bulletin.json),本頁實際走 BaseService + API.BULLETIN*,非此檔(見 §12 坑 5)
src/config/locales/i18n/{zh-tw,en}/bulletin.json 頁面 i18n(namespace lang.bulletin_manage.*
src/config/locales/i18n/{zh-tw,en}/menu.json 選單顯示名稱(bulletin-manage / bulletin-form key)

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

鏈路 Route App Service 底層
公告清單 api/bulletin/routes/bulletin_route.py::BulletinsRoute(POST /bulletins app/bulletin/service/bulletin_service.py::get_bulletins_and_pager domain/bulletin/service/bulletin_domain_service.pyinfra/bulletin/repository/bulletin_repo_impl.py::BulletinRepoImpl(繼承 jedi_bulletin base + 主專案覆寫 _gen_filtersaddupdate 以支援 org_units 關聯與 tenant 欄位)
公告詳情 / CRUD 同檔 BulletinRoute(GET/POST/PUT/DELETE /bulletin[/{uid}] 同 service 的 get_bulletin / add_bulletin / update_bulletin / delete_bulletin 同上 + domain/bulletin/service/bulletin_org_unit_domain_service.pyinfra/bulletin/repository/bulletin_org_unit_repo_impl.pybulletin_org_units 關聯表直接 SQL insert/delete,非走 ORM relationship 寫入)

DDD 提醒:本模組是主專案 extend 外部套件(jedi-bulletin)的標準模式——domain/bulletin/entities/bulletin_entity.py 繼承套件 BaseBulletinEntity 再加 org_units/created_user_name 等主專案欄位;infra/bulletin/models/bulletin.py::ExtendedBulletin 繼承套件 Bulletin model 再加 org_units relationship 與 created_user_name/updated_user_name hybrid_property(審計欄位回傳規範的標準做法:app service 不做、model 層直接算)。新增本模組功能時比照此 extend 模式,不要直接改 jedi-bulletin 套件本身除非確認是套件 domain 該有的行為。

9. DB

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

讀/寫 說明 欄位詳述
public.bulletins 公告主表(jedi-bulletin 套件定義 + 主專案 tenant_id/org_unit_id 欄位) §9.3
public.bulletin_org_units 公告 ↔︎ 部門多對多關聯表(純關聯表,無自身 PK) §9.3
public.org_units 部門主檔(jedi-auth),提供目標部門選項與名稱顯示 見 jedi-auth 相關頁
public.users 使用者主檔,created_user_name/updated_user_name 透過 login_name join 取得 nickname 見使用者管理相關頁

9.2 ER 圖(本頁讀寫範圍)

bulletin_mgmt_er bulletins public.bulletins (寫)公告主表 uid|title|content|category release_time|expire_time enable|is_delete(未使用) created_user|updated_user tenant_id|org_unit_id bulletin_org_units public.bulletin_org_units (寫)目標部門關聯 bulletin_id|org_unit_id 複合 PK,無自身序列 bulletins->bulletin_org_units 1:N ON DELETE CASCADE org_units public.org_units (讀)部門主檔 jedi-auth bulletins->org_units org_unit_id(tenant 歸屬部門) ON DELETE RESTRICT users public.users (讀)使用者主檔 login_name → nickname jedi-auth bulletins->users created_user / updated_user = login_name 字串關聯(非 FK) bulletin_org_units->org_units N:1 ON DELETE CASCADE
公告管理 ER:黃 = 唯讀(org_units/users)、綠 = 本頁寫入(bulletins/bulletin_org_units);虛線 = login_name 字串關聯(非 FK)

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

public.bulletins — 公告主表

DB comment:「公告表:儲存系統公告資訊,包含標題、內容、類別及發布時效」。

欄位 型別 說明
id integer 內部 PK(nextval('bulletins_id_seq')
uid varchar(50) 對外識別碼(API 走 uid)
title varchar(255) 公告標題
content text 公告內容(HTML 字串)
category varchar(50) 公告類別,default 'others'
release_time / expire_time timestamp without time zone 發布時間 / 過期時間(expire_time 可為 null;release_time DB 層 not null,但 FE schema 未強制必填,見 §12 坑 1)
enable integer 是否啟用:1 啟用,0 停用,default 1
is_delete integer 是否已刪除:1 已刪除,0 未刪除,default 0——欄位存在但 API 刪除路徑並未使用(見 §12 坑 3)
created_user / updated_user varchar(50) login_name(審計欄位,nickname 由 model hybrid_property 動態查)
tenant_id integer → tenants.id(ON DELETE RESTRICT
org_unit_id integer → org_units.id(ON DELETE RESTRICT)——注意這是 tenant/建立部門欄位,非 org_units 關聯,實際「目標部門」關聯走 bulletin_org_units 多對多表

索引 idx_bulletins_livetenant_id, org_unit_id, release_time WHERE enable=1 AND is_delete=0)顯示原始套件設計是預期 is_delete 會被使用於「生效中公告」查詢——與 §12 坑 3 現況不一致,屬既有債。

public.bulletin_org_units — 公告目標部門關聯(純關聯表)

欄位 型別 說明
bulletin_id integer → bulletins.id(ON DELETE CASCADE),複合 PK 之一
org_unit_id integer → org_units.id(ON DELETE CASCADE),複合 PK 之一

無自身序列 / 額外欄位;PK = (bulletin_id, org_unit_id)

10. 頁面邏輯與資料對應

載入時序(清單頁)

bulletin_mgmt_load_sequence mounted onMounted p1 getCategoryMenu() GET /system-menu/BULLETIN_CATEGORY mounted->p1 平行 p2 getBulletins() POST /bulletins(分頁公告清單) mounted->p2 平行 render 兩者皆完成後 渲染表格 / 下拉選單 p1->render p2->render user_action 使用者操作: 搜尋 / 換頁 / 排序 render->user_action rebuild buildPayload() 重組 pager/sort/filters user_action->rebuild refetch 重打 POST /bulletins rebuild->refetch refetch->render 更新表格
公告管理清單載入時序:onMounted 平行打類別選單 + 公告清單;搜尋/排序/分頁皆重打同一支 API

關鍵欄位對應(API ↔︎ 畫面,清單頁):

畫面元素 FE state API 欄位
類別欄顯示文字 trensCategoryDesc(data.category)(查 categoryMenu 比對 value,找不到 fallback others category
公告部門 chip data.org_units.slice(0, 5) + 「+N…」 org_units[]
發布欄 data.enable === 1 ? '是' : '否' enable
建立 / 修改人員欄 直接綁定 created_user_name / updated_user_name

關鍵欄位對應(表單頁):

畫面元素 FE state API 欄位
目標部門 MultiSelect currData.org_unitsgetBulletin() 時把 res.org_units[] 物件陣列 map 成 uid 陣列) request/response org_units(GET 回物件陣列,PUT/POST 送 uid 陣列——同名欄位讀寫形狀不同,見 §12 坑 6)
起訖時間 currData.release_time / .expire_time(Calendar 元件,Date 物件) 送出前 moment(...).format('YYYY-MM-DD 00:00:00') 正規化字串
內容 currData.content(CKEditor v-model contentfields.Raw,無格式驗證)

儲存流程:表單一次性整包送出(無分段儲存);uid 存在則 PUT 走 update(),否則 POST 走 create();儲存成功後 toast 提示並 router.push('/bulletin/bulletin-manage') 導回清單(觸發清單頁 onMounted 重新載入,非共用 store 或事件通知)。

錯誤對應:BE error envelope → 走全域 axios interceptor 的 axios-error event bus → toast 顯示;已知錯誤碼:BULLETIN_404001(新增/更新時目標部門 UID 查無資料)。

11. 背景行為與外部依賴

類型 內容
通知 無——公告的發布本身不觸發站內通知,僅在檢視端依 auth=true 查詢條件顯示
Socket 無 socket / 輪詢;清單更新靠操作後重新 getBulletins() 或導頁
jedi-* 套件 jedi-bulletinBulletinEntity/Bulletin model/BulletinRepoImpl 基底類別,主專案 extend 加 org_units 關聯與審計欄位顯示名)、jedi-authOrgUnit/User model、JWT)
系統參數 無 feature flag;公告類別選項來自通用 system_menuBULLETIN_CATEGORY code),非本模組專屬設定表

12. 邊界情況與已知坑

  1. release_time/expire_time FE/BE 必填認定不一致:FE vuelidate 兩者皆標 required,但 BE BulletinRequest schema 未標 required=True(僅 fields.DateTime,可省略);DB 層 release_time NOT NULL、expire_time 允許 NULL。繞過 FE 直接打 API 省略 release_time 會在 DB insert 階段才失敗(500,非乾淨的 400),不是本頁 UI 會遇到的路徑,但要注意 API 本身沒有做完整前置驗證
  2. 清單查詢範圍規則寫在 comment 裡、行為容易誤解user.org_unit_id 有值時只回「自己建立」的公告,org_unit_id 為 null 才回全部——這與一般直覺「有部門的人看得到更多」相反,是刻意設計成「無部門掛職者視為系統管理者,可看全部」(bulletin_service.py:42 原始碼註解)。改動這條過濾邏輯前務必理解此意圖
  3. 刪除是硬刪除,is_delete 欄位形同虛設domain/bulletin/service/bulletin_domain_service.py:47 呼叫的是 BaseRepositoryImpl.delete_by_uidsession.delete() 實體刪除),而非利用 is_delete 欄位做軟刪除。DB 索引 idx_bulletins_live 的設計意圖明顯是預期軟刪除(WHERE enable=1 AND is_delete=0),現況等於該索引條件恆為「未刪除」子集但 is_delete 永遠是 0。刪除公告的 API 呼叫方沒有恢復機制,屬已知債,新 code 不要假設 bulletins 有軟刪資料可查
  4. 寫入操作 BE 無角色守門:新增/編輯/刪除三支 API 僅 @jwt_required(),任何登入者皆可操作,即便不是系統管理者。屬已知債(與 我的稽核 §12 坑 8 類似模式,但性質不同——本頁是寫入操作缺角色檢查,非純讀),新增類似 API 不要比照
  5. BulletinService.js 是死碼src/service/BulletinService.js 讀寫 /demo/data/bulletin.json 本地假資料,本頁兩個 view 實際皆透過 BaseService + API.BULLETINS/API.BULLETIN 呼叫真實後端,此檔案未被任何 view import。清理前端死碼時可考慮移除,但需先確認無其他隱藏引用
  6. org_units 欄位讀寫形狀不同:GET 回傳 org_units: [{uid, name}, …](物件陣列,dump_only nested schema),但 POST/PUT 送出時需為 org_units: ["uid1", "uid2", …](純 uid 字串陣列)。BulletinForm.vue:75getBulletin() 手動做了這個轉換(res.org_units.map(obj => obj.uid))——同名欄位跨讀寫語意不同,日後串接或除錯要留意,不能直接假設兩邊形狀一致
  7. PUT 是 full payload,非 partial patchupdate_bulletin 對主表欄位逐一覆寫(title/content/category/enable/release_time/expire_time),且 org_units 關聯每次都整批刪除重建delete_by_bulletin_id 先清空,再依送出的陣列重建)。若呼叫方省略 org_units 或送空陣列,會清空該公告全部目標部門,與 專案任務編輯頁 §12 坑 6 的 job full-replace 語意同一類陷阱
  8. 無 phase / 時間窗前置檢查enable/release_time/expire_time 三者任意組合都能被管理端寫入 API 接受(例如 expire_time 早於 release_time、或 enable=1 但兩個時間都是過去),BE 沒有做邏輯自洽驗證,僅檢視端查詢時用 auth=true 的時間比對條件把「當下未生效」的公告過濾掉

13. 開發與驗證

  • 跑起來:BE python main_socketio.py(port 8000,log 在 log/app.log);FE 在 compliance-manager-fe/ 起 Vite dev server。BE 改 service code 後必須重啟(無 hot reload)
  • 測試帳號:dev 環境一般登入帳號即可操作(密碼見 .env / 部署文件;BE 無角色守門,見 §3)
  • 導航路徑:登入 → 側邊選單「公告管理」(bulletin-manage)→ 新增/編輯走「編輯公告」(bulletin-form
  • 前置資料:需至少一個部門(org_units)存在才能通過表單必填驗證;BULLETIN_CATEGORY system-menu 需有選項資料才能選類別
  • E2Ecompliance-manager-test/ repo(Cucumber + Playwright);本頁相關 feature 檔以 公告 / bulletin 搜尋
  • 相關文件docs/api/bulletin/Bulletin-SA-SD.docx(既有 API 規格文件,可當背景參考,欄位以本頁 §6 為準);歷史修復 docs/changelog/2026-06-08-fix-bulletin-time-filter-direction.mdauth=true 時間比對方向 bug,已在 jedi-bulletin 套件層修正,見 §6.2 filter 說明);檢視端見 公告檢視