跳轉到

公告管理(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 組裝碼

變更紀錄

日期 FR 內容
2026-10-04 FR-126(1.24.1) 編輯器貼上與載入的 HTML 不保留行內 style(只把對齊轉成 class);表單內容欄實際為 TipTap 編輯器
2026-09-28 1.21.0(FR-114) 公告表單多一個必選的「發送範圍」(全公司/指定部門):選「全公司」不用挑部門、切回全公司會清掉已選部門;選「指定部門」時部門才必填;既有公告一律是「全公司」,清單「公告部門」欄顯示「全公司」。只有作者本人能改、刪:非作者編輯回 404 BULLETIN_404002、刪除回 data=false。挑到不屬目前租戶的部門回 400 BULLETIN_400002。編輯沒帶 org_units 時部門整組維持原值(§1.1 #11、§3、§6.4、§9.3、§12 坑 7/9)
2026-07-17 site-regression 實測回饋 §3/§12 坑4 更新:新增/編輯/刪除已掛 @require_capability("bulletin.{create,update,delete}")(e8b28a4a,FR-048 軸④),無能力點回 403 GRC_403022;§12 坑8 更新:時間窗驗證已補 _assert_bulletin_time_window(5cc31915),expire_time<=release_time 回 400 BULLETIN_400001

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 目標部門欄位顯示 send_scope='ALL' 顯示單一「全公司」chip;指定部門時顯示部門 chip 清單,超過 5 個收合成「+N…」並 tooltip 展開全部 表格「公告部門」欄 §9.1
9 表單:標題 / 內容 標題純文字(必填);內容為 TipTap 富文字(必填,與專案摘要報表共用 TipTapEditor.vue),存成 HTML 字串。貼上的 HTML 不保留行內 style(僅對齊轉 class),載入既有內容同樣處理 BulletinForm.vue UC-BM-02
10 表單:類別下拉 選項來自 GET /system-menu/BULLETIN_CATEGORY(必填) BulletinForm.vue §6.1
11 表單:發送範圍+目標部門 「發送範圍」SelectButton 必選(全公司 ALL/指定部門 ORG_UNIT,新增預設全公司);選「指定部門」才出現部門 MultiSelect(選項來自部門 menu store,此時至少一個);切回「全公司」時清空已選部門 BulletinForm.vue §6.4
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 / send_scope / release_time / expire_time / content;org_units 只在 send_scope='ORG_UNIT' 時必填(requiredIf);失敗時自動 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)

UC-BM-01 流程:進頁載入 → buildPayload 組分頁/排序/搜尋 → BE 依 user.org_unit_id 決定是否只列自己建立的 → 渲染表格

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

項目 內容
角色 任一已登入使用者(同上)
前置條件 表單必填欄位(title/category/send_scope/release_time/expire_time/content;指定部門時加 org_units)皆有值;編輯時操作者須為該公告作者
產出 / 後置條件 bulletins 一列新增或更新(含 send_scope);有帶 org_units 時 bulletin_org_units 先刪後建,沒帶則維持原值(見 §6.4)

UC-BM-02 流程:開表單(新增空白/編輯帶入資料)→ FE vuelidate 擋 → 送出 → BE 檢查時間窗/部門租戶/作者 → upsert bulletins(含 send_scope),有帶部門才重建關聯 → 導回清單

UC-BM-03 刪除公告

項目 內容
角色 任一已登入使用者(同上)
前置條件 二次確認 dialog 按下「確定」;操作者須為該公告作者(非作者 BE 回 data=false、不刪)
產出 / 後置條件 bulletins 該列實際被刪除(非軟刪除,見 §12 坑 3);bulletin_org_units 由 service 先清再刪主表(FK 無 CASCADE,順序反了會撞外鍵)

UC-BM-03 流程:點刪除 icon → 二次確認 dialog → DELETE 打 uid → BE 檢查存在且為作者(否則回 false 不刪)→ 先清部門關聯再硬刪除 → 重新查詢清單

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 模式才隱藏) @require_capability("bulletin.create"),無此能力點回 403 GRC_403022 api/bulletin/routes/bulletin_route.py:55
編輯 同上 @require_capability("bulletin.update"),無此能力點回 403 GRC_403022;另比對作者(created_user=登入帳號),非作者回 404 BULLETIN_404002(不回 403,避免洩漏公告存在) api/bulletin/routes/bulletin_route.py:68;作者檢查 jedi_bulletin/app/service/bulletin_service.py::_assert_author
刪除 同上 @require_capability("bulletin.delete"),無此能力點回 403 GRC_403022;非作者與查無此公告一律回 data=false(不可分辨) api/bulletin/routes/bulletin_route.py:80;jedi_bulletin/app/service/bulletin_service.py::delete_bulletin
選部門 部門選單 寫入前逐一檢查所選部門屬於目前作用租戶,不屬回 400 BULLETIN_400002(母公司管理員看得到子公司部門,但不能發給它) jedi_bulletin/app/service/bulletin_service.py::_assert_org_units_in_active_tenant → core/plugins/bulletin.py::BulletinOrgUnitDirectory.is_in_active_tenant

能力點只管「能不能改公告」,管不到「能不能改別人的公告」——所以編輯與刪除另外比對作者本人。公告沒有建立者(created_user 為 NULL 的歷史資料)時一律視為非作者。

FR-048 軸④ 更新(2026-07-07,commit e8b28a4a):公告新增/編輯/刪除三個寫入操作已從「僅 @jwt_required()」升級為 @require_capability("bulletin.{create,update,delete}"),無對應能力點回 403 GRC_403022(GRC_CAPABILITY_REQUIRED)。§12 坑 4(舊「BE 完全無角色守門」描述)已撤銷。

4. 狀態機與前置條件

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

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

5. UI 設計

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

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

公告管理 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-23(BulletinQueryRequest / 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-32(BulletinResponse / 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)
send_scope string 否(FE 必送) 發送範圍:ALL(全公司)/ORG_UNIT(指定部門),字串凍結(對應 DB CHECK 約束)。沒帶時由 org_units 回推:有部門 → ORG_UNIT、空陣列 → ALL;兩者都沒帶新增時落 ALL、編輯時維持原值
org_units array[string] 否(ORG_UNIT 時 FE 必填) 目標部門 UID 陣列;有帶時後端逐一檢查部門屬目前租戶(否則 400 BULLETIN_400002)、查無部門 404 BULLETIN_404001,再關聯進 bulletin_org_units。ALL 時 FE 送空陣列
content raw 是 內容 HTML 字串(TipTap 輸出,無格式驗證)
release_time / expire_time date(%Y-%m-%d) 否(FE 端 vuelidate 視為必填,schema 層未標 required) 生效區間
enable int 否(default 0) 是否啟用

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

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

[PUT] /bulletin/{uid}

Request:同 POST 的 BulletinRequest(FE 送出整包 currData)。操作者須為作者,非作者回 404 BULLETIN_404002。

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

  1. 比對作者(_assert_author),非作者當作查無此公告
  2. 更新 bulletins 主表欄位(title / content / category / enable / release_time / expire_time / send_scope / updated_user);send_scope 沒帶也回推不出來時維持原值
  3. request 有帶 org_units 才動部門:先整批刪除該公告全部 bulletin_org_units 關聯,再依陣列重建(空陣列=清空,配合 send_scope='ALL');沒帶 org_units 整組維持原值

Response:同 GET 單筆形狀。

[DELETE] /bulletin/{uid}

無 body。Response data:布林值(BooleanResponse);非作者或查無此公告回 false、不刪。刪除前先清 bulletin_org_units 再刪主表,兩者同一 transaction。出處:api/bulletin/routes/bulletin_route.py:74-79;刪除邏輯 domain/bulletin/service/bulletin_domain_service.py:47-48 → IBaseRepo.delete_by_uid(session.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/views/project-summary-report/TipTapEditor.vue 表單內容欄位的富文字編輯器(與專案摘要報表共用;貼上/載入時處理行內 style)
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.py → infra/bulletin/repository/bulletin_repo_impl.py::BulletinRepoImpl(繼承 jedi_bulletin base + 主專案覆寫 _gen_filters/add/update 以支援 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.py → infra/bulletin/repository/bulletin_org_unit_repo_impl.py(bulletin_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 圖(本頁讀寫範圍)

公告管理 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)
send_scope varchar(20) 發送範圍:ALL=全公司、ORG_UNIT=指定部門(配合 bulletin_org_units),default 'ALL',CHECK 限這兩值(chk_bulletins_send_scope);既有公告升級後一律 ALL
org_unit_id integer → org_units.id(ON DELETE RESTRICT)——注意這是 tenant/建立部門欄位,非 org_units 關聯,實際「目標部門」關聯走 bulletin_org_units 多對多表

索引 idx_bulletins_live(tenant_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——刪公告前必須先清本表,見 §6.4),複合 PK 之一
org_unit_id integer 部門 id,軟參照、無外鍵(org_units 屬 jedi-iam),複合 PK 之一

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

10. 頁面邏輯與資料對應

載入時序(清單頁):

公告管理清單載入時序:onMounted 平行打類別選單 + 公告清單;搜尋/排序/分頁皆重打同一支 API

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

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

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

畫面元素 FE state API 欄位
發送範圍 SelectButton currData.send_scope(新增預設 'ALL';切回 ALL 時 watch 清空 org_units) request/response send_scope
目標部門 MultiSelect(僅 ORG_UNIT 顯示) currData.org_units(getBulletin() 時把 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(TipTap v-model) content(fields.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 查無資料)、BULLETIN_404002(編輯非本人公告或公告不存在)、BULLETIN_400001(下架時間不晚於上架時間)、BULLETIN_400002(部門不屬目前租戶)。

11. 背景行為與外部依賴

類型 內容
通知 無——公告的發布本身不觸發站內通知,僅在檢視端依 auth=true 查詢條件顯示
Socket 無 socket / 輪詢;清單更新靠操作後重新 getBulletins() 或導頁
jedi-* 套件 jedi-bulletin(BulletinEntity/Bulletin model/BulletinRepoImpl 基底類別,主專案 extend 加 org_units 關聯與審計欄位顯示名)、jedi-auth(OrgUnit/User model、JWT)
系統參數 無 feature flag;公告類別選項來自通用 system_menu(BULLETIN_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_uid(session.delete() 實體刪除),而非利用 is_delete 欄位做軟刪除。DB 索引 idx_bulletins_live 的設計意圖明顯是預期軟刪除(WHERE enable=1 AND is_delete=0),現況等於該索引條件恆為「未刪除」子集但 is_delete 永遠是 0。刪除公告的 API 呼叫方沒有恢復機制,屬已知債,新 code 不要假設 bulletins 有軟刪資料可查
  4. ~~寫入操作 BE 無角色守門~~ 已修復(2026-07-07,e8b28a4a,FR-048 軸④):新增/編輯/刪除三支 API 已掛 @require_capability("bulletin.{create,update,delete}"),無對應能力點回 403 GRC_403022,詳見 §3
  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:75 的 getBulletin() 手動做了這個轉換(res.org_units.map(obj => obj.uid))——同名欄位跨讀寫語意不同,日後串接或除錯要留意,不能直接假設兩邊形狀一致
  7. org_units 省略與空陣列語意不同:省略=部門整組維持原值;空陣列=清空部門。FE 選「全公司」時送空陣列、send_scope='ALL';直接打 API 只改標題時不要帶 org_units,否則會把發送對象清掉
  8. ~~無 phase / 時間窗前置檢查~~ 已修復(2026-07-07,5cc31915):新增/編輯已加 _assert_bulletin_time_window()(app/bulletin/service/bulletin_service.py:50-58,add_bulletin/update_bulletin 呼叫處各見 121-126、147-153 行)——release_time/expire_time 皆有值且 expire_time <= release_time 時擋下,回 400 BULLETIN_400001(BULLETIN_INVALID_TIME_WINDOW)。單邊 None 或無法解析仍放行不擋。FE 未做對應前端驗證,此為 BE 端單邊防線
  9. 只有作者能改刪,非作者回的不是 403:編輯回 404 BULLETIN_404002、刪除回 data=false——刻意讓「別人的公告」與「不存在的公告」看起來一樣。沒有作者的歷史公告(created_user 為 NULL)任何人都改不動,要動走維運路徑
  10. 發送範圍與部門要一致:send_scope='ALL' 時部門不作用(清單只顯示「全公司」);ORG_UNIT 時只有橋表列到的部門看得到。讀者端的過濾規則見 公告檢視 §4

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 需有選項資料才能選類別
  • E2E:compliance-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.md(auth=true 時間比對方向 bug,已在 jedi-bulletin 套件層修正,見 §6.2 filter 說明);檢視端見 公告檢視