公告檢視(Bulletin View)
功能群:協作(collab)|本群無獨立 _overview;管理側見 公告管理;SPEC 索引見 README
事實基準:2026-07-05 從 FE / BE code 掃出;API schema 逐條對照 marshmallow serializer 與 service 組裝碼
1. 功能描述
本頁涵蓋公告的讀者端三條
route:公告清單(bulletin-list)、公告檢視(bulletin-view)、使用手冊(bulletin-user-guide)。任何登入使用者都能看自己部門
/
全域生效中的公告,點列進入詳情頁閱讀完整內容;使用手冊是側邊選單另一個獨立入口,目前是空殼頁面(見下)。
技術細節:bulletin-list
實際上是把管理頁元件 BulletinManage.vue 以
:view="true" prop
重新渲染(compliance-manager-fe/src/views/bulletins/BulletinList.vue:3-7)——同一份
DataTable 程式碼在管理者與一般使用者眼中呈現不同欄位/按鈕(見 §1.1 /
§5)。bulletin-view 是純讀 detail 頁,全站僅
BulletinManage.vue(含 view 與 manage
兩模式)會導向它。
主要使用者:任何已登入使用者(無角色門檻,見 §3)。頁面本身不提供任何寫入操作。
角色速覽(本群無
_overview.md,角色定義以本頁實際檢查點為準;完整角色矩陣見
system-admin RBAC
若已存在,否則以下即為完整定義):
| 角色 | 一句話 |
|---|---|
| 一般使用者(任何已登入者) | 唯一角色——公告讀取無任何角色 / capability 限制,只受部門與生效時間範圍過濾 |
1.1 功能總覽(本頁全部功能)
| # | 功能 | 說明 | 位置 | 詳述 |
|---|---|---|---|---|
| 1 | 公告清單(授權範圍) | 顯示 enable=1 且在生效期間、且屬使用者部門(或全域)的公告,lazy 分頁 | bulletin-list | UC-BV-01 |
| 2 | 關鍵字搜尋 | 對
title / category / created_user_name / updated_user_name / created_user / updated_user
全域搜尋(前端組 filter) |
bulletin-list Header | §6.1 |
| 3 | 重置查詢 | 清空搜尋 /
排序、回到第一頁(queryStore.resetCurrent()) |
bulletin-list Header | §7 |
| 4 | 分類顯示 | 類別 code 轉譯為中文(system-menu/BULLETIN_CATEGORY
動態選單 + i18n) |
bulletin-list 表格 / bulletin-view 標題列 | §6.1 |
| 5 | 排序 / 分頁 | 標題 / 起訖時間 / 建立修改時間欄位可排序,10/25/50 筆分頁 | bulletin-list 表格 | §7 |
| 6 | 整列可點進入檢視 | view 模式下整列 cursor-pointer,點擊導向 bulletin-view | bulletin-list 表格 | UC-BV-02 |
| 7 | 公告內容檢視 | 標題(含分類前綴)+ HTML 富文本內容(v-dompurify-html
消毒渲染) |
bulletin-view | UC-BV-02 |
| 8 | 返回按鈕 | router.back() 回上一頁(非固定路徑,view/manage
兩入口都適用) |
bulletin-view | §5 |
| 9 | 使用手冊頁 | 側邊選單獨立入口,目前僅顯示標題「使用手冊」,無內容、無 API 呼叫 | bulletin-user-guide | §1 / §12 坑 1 |
UC(§2)展開 1(清單載入與授權範圍)與 6/7(進入檢視)這兩條核心路徑;其餘為常規 CRUD 讀取或純顯示邏輯。使用手冊頁因無任何邏輯,不另立 UC。
2. Use Case
角色速覽(本群無
_overview.md,角色定義以本頁實際檢查點為準;完整角色矩陣見
system-admin RBAC
若已存在,否則以下即為完整定義):
| 角色 | 一句話 |
|---|---|
| 一般使用者(任何已登入者) | 唯一角色——公告讀取無任何角色 / capability 限制,只受部門與生效時間範圍過濾 |
流程圖視覺慣例:菱形 = 判斷、橘底 = 例外/擋下、紅框 = 錯誤(標 error code)、綠框 = 成功終點、虛線 = 可選路徑。
UC-BV-01 讀取公告清單(授權範圍過濾)
| 項目 | 內容 |
|---|---|
| 角色 | 任何已登入使用者 |
| 前置條件 | 已登入(JWT 有效) |
| 產出 / 後置條件 | 無寫入;回傳「enable=1 + 生效期間內 + 使用者部門(或全域)」範圍的公告清單 |
UC-BV-02 進入公告檢視頁
| 項目 | 內容 |
|---|---|
| 角色 | 任何已登入使用者 |
| 前置條件 | route.query.uid 存在(否則導回
/bulletin/bulletin-manage,見 §12 坑 2) |
| 產出 / 後置條件 | 無寫入;GET 單筆公告詳情渲染 |
本 UC 流程已併入 UC-BV-01 圖下半段(
click → detail → exists → show),因兩者共用同一條「清單→詳情」路徑,拆兩張圖會重複超過一半節點。
3. 權限矩陣
本群無 _overview.md 集中定義,角色基調直接列於本頁:
| 操作 | FE 判定 | BE 強制 | BE 檢查位置 |
|---|---|---|---|
| 選單 / 頁面可見性 | 無 capability 判斷式(router/index.js 對三條 route 均無
meta.requiresAdmin 等守門) |
僅 @jwt_required(),無角色 / capability 二次檢查 |
api/bulletin/routes/bulletin_route.py:25,43 |
讀清單(POST /bulletins) |
props.view=true 時固定帶
auth: true(BulletinManage.vue:78-80) |
domain service 依 auth 旗標套用 enable=1 +
生效時間 + 部門範圍(見 §4) |
domain/bulletin/service/bulletin_domain_service.py:16-19 |
讀單筆(GET /bulletin/{uid}) |
無額外判定,直接打 API | ⚠️ 無 enable / 生效時間 / 部門範圍檢查——只要 uid 存在、已登入即可讀取(含已停用、已過期、非本部門的公告) | api/bulletin/routes/bulletin_route.py:45-48 →
bulletin_domain_service.get_bulletin_by_uid(無
filter) |
| 使用手冊頁 | 無 | 無 API 呼叫可言 | — |
⚠️ 已知落差:清單頁有嚴格的授權範圍過濾(enable / 生效期間 / 部門),但點進詳情頁的
GET /bulletin/{uid}完全不重複這層檢查——只要拿到 uid(例如清單載入後從網路請求或分享連結取得),任何登入使用者都能讀到理論上「不該看到」的公告內容(已停用、已過期、或屬其他部門)。因公告內容本身非機密性資料(系統公告皆為告知性質),暫列為已知行為而非資安缺口,但新增類似「先過濾清單、detail 不重複檢查」的模式時應留意,收進 §12 坑 3。
4. 狀態機與前置條件
本頁無跨頁狀態機(公告無審核流程,enable
只是啟用/停用二態旗標,非狀態機)。相關 gate:
| 條件 | 效果 | 出處 |
|---|---|---|
enable = 1 |
清單中僅顯示啟用中的公告(auth=true
時強制套用,不受呼叫端傳值影響) |
domain/bulletin/service/bulletin_domain_service.py:17 |
release_time <= now() |
只顯示已到發布時間的公告 | infra/bulletin/repository/bulletin_repo_impl.py:63-64(欄位名
release_time__gt_now 語意具誤導性,見 §12 坑 4) |
expire_time >= now() |
只顯示尚未過期的公告(expire_time 為 null
視為永不過期,SQL 條件對 null 天生不成立需留意,見 §12 坑 4) |
同檔 :65-66(欄位名
expire_time__le_now) |
org_units__in = [user.org_unit_id] |
使用者有 org_unit_id
時只看自己部門的公告;org_unit_id 為 null
時不篩選部門(視為全域使用者) |
app/bulletin/service/bulletin_service.py:38-40 |
route.query.uid 缺失 |
bulletin-view 頁 onMounted 直接
router.push 導回
/bulletin/bulletin-manage(非管理者也會被導向此頁,見 §12
坑 2) |
BulletinView.vue:20-26 |
5. UI 設計
📸 實際畫面截圖待補(一律亮色模式拍攝):以 playwright 對 STG 擷取,預計補以下三張到
assets/img/: ① 公告清單頁(搜尋列 + DataTable,view 模式欄位配置)bulletin-list-overview.png② 公告檢視頁(標題 + HTML 內容 + 返回按鈕)bulletin-view-detail.png③ 使用手冊頁現況(僅標題的空殼畫面)bulletin-user-guide-empty.png
版面骨架(三頁合併示意):
狀態呈現:清單頁載入中 submit 旗標控制 DataTable
:loading;檢視頁載入中全頁遮罩
ProgressSpinner(BulletinView.vue:63-69,與清單頁的局部
loading 不同做法);無 socket,資料僅在頁面載入 /
操作觸發時查詢,無輪詢或即時更新。
6. API 規格
Envelope:成功
{"status": true, "data": …}(common/util/response_util.py:return_response);失敗依
jedi_common.handler.handler.register_error_handlers
統一處理。
6.1 總清單(本頁呼叫的全部 endpoint)
| 分類 | Method + Path | 說明 | 完整規格 |
|---|---|---|---|
| 載入 | POST /bulletins |
公告清單(分頁 + 排序 + auth=true 授權範圍過濾) |
§6.2 |
| 載入 | GET /bulletin/{uid} |
單筆公告詳情(無 auth 過濾,見 §3 ⚠️) | §6.3 |
| 載入 | GET /system-menu/BULLETIN_CATEGORY |
公告分類下拉選單(清單頁與檢視頁各自獨立呼叫一次) | GAI-SD-02 |
使用手冊頁(bulletin-user-guide)目前無任何 API 呼叫——純靜態模板,見 §1 / §12 坑 1。本頁不含新增 / 修改 / 刪除 endpoint(那些屬管理側,見 公告管理)。
6.2 公告清單類
[POST] /bulletins
Request:
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
| pager | object|null | 否 | {page, page_size} |
| sort | array|null | 否 | 排序條件陣列 |
| filters.auth | bool | 否 | view 模式固定送
true(BulletinManage.vue:78-80);true
時 BE 強制套用 enable/生效時間/部門範圍(§4),前端無法覆蓋 |
| filters.title / category / created_user / updated_user 等 | string | 否 | 全域搜尋欄位對應到的個別篩選(實務上前端走單一關鍵字框,非逐欄輸入) |
出處:BulletinPageQueryRequest /
BulletinQueryRequest(api/bulletin/serializers/bulletin_query.py:5-22)
Response data[](每筆一則公告):
| 欄位 | 型別 | 說明 |
|---|---|---|
| uid | string | 公告識別碼 |
| title | string | 標題 |
| category | string | 分類 code(對照 BULLETIN_CATEGORY 選單轉譯顯示) |
| org_units[] | array | {uid, name}(view
模式表格不顯示此欄,管理頁才顯示) |
| content | string(HTML) | 公告內容(清單不渲染,僅 detail 頁用) |
| release_time / expire_time | date(YYYY-MM-DD) |
發布 / 過期日期 |
| enable | int | 1=啟用 0=停用(view 模式表格不顯示此欄) |
| created_user / created_user_name | string | login_name / nickname |
| created_at | datetime | YYYY-MM-DD HH:MM:SS |
| updated_user / updated_user_name | string | login_name / nickname |
| updated_at | datetime | 同上格式 |
出處:BulletinResponse /
BulletinPageQueryResponse(api/bulletin/serializers/bulletin.py:15-32);組裝邏輯
app/bulletin/service/bulletin_service.py:32-55(get_bulletins_and_pager)
6.3 公告詳情類
[GET] /bulletin/{uid}
無 query 參數,無 request body。Response data:與 §6.2
的單筆欄位形狀相同(同一 BulletinResponse schema)。
出處:api/bulletin/routes/bulletin_route.py:41-48(BulletinRoute.get)→
app/bulletin/service/bulletin_service.py:72-76(get_bulletin,內部呼叫
get_bulletin_by_uid,無任何 filter
條件)
7. 前端檔案地圖(compliance-manager-fe/)
| 檔案 | 角色 |
|---|---|
src/views/bulletins/BulletinList.vue |
清單頁入口(8 行,僅包一層
<BulletinManage :view="true"/>) |
src/views/bulletins/BulletinManage.vue |
實際渲染邏輯(283
行);同時服務管理頁(bulletin-manage)與讀者清單頁(bulletin-list,view=true)兩種模式 |
src/views/bulletins/BulletinView.vue |
檢視頁(89 行):讀 route.query.uid →
GET /bulletin/{uid} → 渲染標題 +
v-dompurify-html 內容 |
src/views/bulletins/UserGuide.vue |
使用手冊頁(16 行):僅一個 <h3> 標題,無 script
邏輯、無 API 呼叫 |
src/config/router/index.js:4-52 |
四條 route 皆掛在 /bulletin
底下:bulletin-manage / bulletin-form /
bulletin-list / bulletin-view /
bulletin-user-guide |
src/config/api/api.js:83-84 |
BULLETINS(清單)/ BULLETIN(單筆
CRUD)常數 |
src/service/BaseService.js |
axios 封裝(get() /
post(),本頁唯一呼叫方式) |
src/composables/useTableQuery.js |
清單頁分頁 / 排序 / 搜尋狀態管理(依使用者與 route scope 持久化) |
src/stores/queryStore.js |
提供 resetCurrent() 重置查詢 |
src/config/locales/i18n/{zh-tw,en}/bulletin.json、menu.json |
頁面 i18n(含分類文案 lang.bulletin_category.*) |
8. 後端檔案地圖(本頁核心鏈路)
| 鏈路 | Route | App Service | 底層 |
|---|---|---|---|
| 公告清單 | api/bulletin/routes/bulletin_route.py::BulletinsRoute.post |
app/bulletin/service/bulletin_service.py::get_bulletins_and_pager(@transaction;依
auth 旗標決定部門篩選邏輯) |
domain/bulletin/service/bulletin_domain_service.py(套用
enable/生效時間白名單)→
infra/bulletin/repository/bulletin_repo_impl.py::BulletinRepoImpl(繼承
BaseRepositoryImpl) |
| 公告詳情 | api/bulletin/routes/bulletin_route.py::BulletinRoute.get |
app/bulletin/service/bulletin_service.py::get_bulletin(@transaction;無
filter,純 uid 查找) |
domain/bulletin/service/bulletin_domain_service.py::get_bulletin_by_uid
→ bulletin_repo.get_by_uid(BaseRepositoryImpl
內建方法) |
| 部門選單資料 | 系統選單模組(GET /system-menu/{code}) |
非本頁專屬,通用 system-menu 服務 | BULLETIN_CATEGORY 屬資料字典項目 |
DDD 提醒:本鏈路完整遵守分層(route 不碰 DB、app
service @transaction、domain service 定義 filter
邏輯、infra 層唯一操作
ORM),是規範遵守良好的範例。唯一落差是 §3
提到的「detail API 缺乏
enable/生效時間/部門範圍二次檢查」——這不是分層違規,是業務邏輯層面的授權範圍遺漏,新增類似
detail API 時應比照清單頁的 filter 邏輯補上,不要只複製本頁
get_bulletin 的寫法。
9. DB
9.1 資料表總清單(本頁讀寫的全部表)
| 表 | 讀/寫 | 說明 | 欄位詳述 |
|---|---|---|---|
| public.bulletins | 讀 | 公告主檔(標題 / 內容 / 分類 / 發布時效 / 啟用狀態) | §9.3 |
| public.bulletin_org_units | 讀 | 公告 × 部門多對多關聯(清單頁部門篩選、管理頁部門欄顯示用) | §9.3 |
本頁完全無寫入——是純讀頁面(新增 / 修改 / 刪除屬管理側
bulletin-manage.md)。無獨立的「已讀」/「瀏覽紀錄」表——db_schema.json 全庫掃描確認只有這兩張bulletin*表,公告閱讀不做任何已讀狀態追蹤(見 §12 坑 5)。
9.2 ER 圖(本頁讀取範圍)
9.3 核心表欄位(取自 DEV DB dump,含 DB comment)
public.bulletins — 公告主檔(本頁只讀)
DB comment:「公告表:儲存系統公告資訊,包含標題、內容、類別及發布時效」
| 欄位 | 型別 | 說明 |
|---|---|---|
| id | integer | 內部序號(PK) |
| uid | varchar(50) | 對外識別碼(API 走 uid,DB comment:「公告唯一識別碼UUID」) |
| title | varchar(255) | 公告標題 |
| content | text | 公告內容(HTML 富文本,前端 v-dompurify-html
消毒後渲染) |
| category | varchar(50) | 公告類別,預設 'others';對照 system-menu
的 BULLETIN_CATEGORY 選單 |
| release_time | timestamp(無 tz) | 發布時間 |
| expire_time | timestamp(無 tz)|null | 過期時間;null 視為永不過期 |
| enable | integer | 是否啟用:1 啟用 0 停用(DB comment 明載) |
| is_delete | integer | 軟刪除旗標:0 未刪除 1 已刪除,預設 0 |
| tenant_id | integer|null | 租戶
FK(bulletins_tenant_fk,ON DELETE RESTRICT) |
| org_unit_id | integer|null | 部門
FK(bulletins_org_fk,ON DELETE RESTRICT;與
bulletin_org_units
關聯表是兩種不同機制,本欄位用途需個案確認,本頁清單 API
邏輯走的是 bulletin_org_units 多對多,見 §12 坑 6) |
| created_user / updated_user | varchar(50)|null | login_name(審計欄;本頁 response 額外 enrich _name
版本) |
| created_at / updated_at | timestamp(無 tz) | 審計欄 |
索引
idx_bulletins_live(enable=1 AND is_delete=0
的部分索引,涵蓋
tenant_id, org_unit_id, release_time)明確為本頁「授權範圍清單查詢」而設。
public.bulletin_org_units — 公告 × 部門關聯(本頁只讀)
複合主鍵(無獨立 uid),純 link table。
| 欄位 | 型別 | 說明 |
|---|---|---|
| bulletin_id | integer | → bulletins.id(ON DELETE
未特別設定,隨父列刪除行為由 FK 定義決定) |
| org_unit_id | integer | →
org_units.id(ON DELETE CASCADE:部門刪除時關聯自動清除) |
10. 頁面邏輯與資料對應
載入時序:
關鍵欄位對應(API ↔︎ 畫面):
| 畫面元素 | FE state | API 欄位 |
|---|---|---|
| 清單搜尋框 | filterTable.global.value(useTableQuery
管理) |
request filters(組合為關鍵字比對) |
| 清單分類欄 | trensCategoryDesc(data.category)(比對
categoryMenu 後
t('lang.bulletin_category.' + value)) |
response category |
| 清單標題欄 | 直接綁定 + tooltip 顯示完整標題 | response title |
| 清單起訖時間欄 | 直接綁定 | response release_time / expire_time |
| 清單建立 / 修改人欄 | 直接綁定 created_user_name /
updated_user_name |
同名欄位 |
| 檢視頁標題列 | currData.category + currData.title
組合顯示 [分類] 標題 |
response category / title |
| 檢視頁內容區 | v-dompurify-html="currData.content" |
response content |
儲存流程:無——本頁三條 route 皆為純讀取展示頁,沒有任何欄位可編輯或儲存。
錯誤對應:BulletinView.vue 與
BulletinList.vue(經
BulletinManage.vue)均未見自訂 catch handler,錯誤走 axios
全域攔截器的統一 toast(BaseService
慣例)。GET /bulletin/{uid} 若 uid 不存在,底層
get_by_uid 回傳行為需視 BaseRepositoryImpl
實作而定(可能回傳 None 導致後續 dump 出錯,而非明確拋 404
— 未在本頁程式碼見到針對此情況的顯式 NotFound 拋出,見 §12
坑 7)。
11. 背景行為與外部依賴
| 類型 | 內容 |
|---|---|
| 通知 | 無——本頁不觸發、也不消費任何通知(公告的建立會不會觸發通知,屬管理側行為,見 公告管理) |
| Socket | 無 socket / 輪詢;每次進頁 / 搜尋皆重新 fetch |
| jedi-* 套件 | jedi-bulletin(BulletinEntity 基礎
DTO/BulletinDTO.from_entity 繼承自
jedi_bulletin.app.dto.bulletin.BulletinDTO)、jedi-auth(OrgUnitDomainService
/ UserDomainService 做部門與使用者查詢、JWT) |
| 系統參數 | 無 feature flag;公告分類為 system-menu 資料字典項目
BULLETIN_CATEGORY,非寫死 enum |
12. 邊界情況與已知坑
- 使用手冊頁(bulletin-user-guide)目前是空殼:僅
<h3>{{t('lang.menu.user-guide')}}</h3>一行(UserGuide.vue:1-16),無任何內容區塊、無 API 呼叫、無資料模型——是靜態占位頁而非資料驅動頁面。若日後要填充內容,需另外設計內容來源(寫死 markdown / 走公告分類專用一則 bulletin / 獨立 CMS 欄位皆可,目前程式碼未給任何線索) bulletin-view缺 uid 時導向bulletin-manage而非bulletin-list:BulletinView.vue:25(router.push('/bulletin/bulletin-manage'))——一般讀者(非管理者)若直接訪問/bulletin/bulletin-view網址但沒帶uidquery,會被導去管理頁而非清單頁;管理頁本身無角色守門(§3),畫面上仍會顯示,但語意上更接近管理入口而非讀者入口- detail API 無二次授權範圍檢查:見 §3
⚠️——清單頁嚴格套用 enable/生效時間/部門範圍,但
GET /bulletin/{uid}完全不重複這層過濾。目前判斷非資安缺口(公告非機密內容),但屬「清單過濾、detail 不過濾」的不對稱模式,之後若擴充成含機密內容的公告類型,需在此補檢查 release_time__gt_now/expire_time__le_now欄位命名與實際語意相反:命名讀起來像「release_time 大於 now」「expire_time 小於等於 now」,但實際 SQL 條件是release_time <= now()(已到發布時間)與expire_time >= now()(尚未過期)——命名沿用BulletinQueryEntity建構參數原樣,維護時勿望文生義(infra/bulletin/repository/bulletin_repo_impl.py:63-66)。另外expire_time為 null 時的邊界行為(SQL 對 null 比較恆為 unknown,等同該公告會被排除在「尚未過期」條件外)在程式碼中沒有特別處理,需個案驗證是否為預期行為- 無已讀 / 瀏覽紀錄追蹤:全庫僅
bulletins+bulletin_org_units兩表(db_schema.json 確認),系統不記錄「誰看過哪則公告」;若需求提到「未讀公告數」等功能,需先新增資料模型,非現有欄位可支援 bulletins.org_unit_id(單一 FK)與bulletin_org_units(多對多關聯表)並存:本頁清單過濾邏輯只用後者(org_units__in走bulletin_org_unitsjoin),前者bulletins.org_unit_id單一外鍵欄位的實際用途在本頁程式碼路徑中未見使用,可能是舊欄位或另有他用,修改部門篩選邏輯前務必先確認兩者關係,不要誤用單一 FK 欄位GET /bulletin/{uid}對不存在 uid 的行為未見明確 404 處理:bulletin_service.get_bulletin直接呼叫get_bulletin_by_uid並無if not bulletin: raise NotFound(...)判斷,行為取決於BaseRepositoryImpl.get_by_uid找不到時是否回傳None——若回傳None,後續BulletinDTO.from_entity(None)極可能拋出AttributeError而非乾淨的 404 錯誤碼,修 bug 或擴充本 API 前應先驗證此路徑的實際行為
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/ 部署文件) - 導航路徑:登入 →
側邊選單「公告」(
bulletin-list)→ 點列進入「檢視公告」(bulletin-view);使用手冊另由選單「使用手冊」(bulletin-user-guide)獨立進入 - 前置資料:DB 需有至少一筆
enable=1且release_time <= now <= expire_time(或expire_time為 null)的公告,且該公告的bulletin_org_units涵蓋測試帳號所屬部門(或帳號org_unit_id為 null) - E2E:
compliance-manager-test/repo(Cucumber + Playwright);本頁相關 feature 檔以公告/bulletin搜尋 - 相關文件:管理側完整 CRUD 行為見 公告管理;DB / API 全量見
GAI-SD-02/03;
docs/api/bulletin/(舊版 API 文件,僅供背景參考,不當清單來源)