# FSD M5 · 通知公告

LLMS 索引： [llms.txt](/llms.txt)

---

> [← 总则](/qmtv/fsd-qmtv/00-%E6%80%BB%E5%88%99/) ｜ [附录 →](/qmtv/fsd-qmtv/99-%E9%99%84%E5%BD%95/) ｜ 上游：[PRD §4 M5](/qmtv/product/prd-qmtv-v1/)
> 实现落点：`backend/internal/domain/notification/`（service.go 读侧 89 行+hub.go SSE 基座）+ `cmd/server/notify_handlers.go` + `cmd/server/routes.go:96-102`（挂载）。事实基准=Go 代码；冻结版 `qmtv/server/index.js:474-485` 仅差异对拍参照。
> 域内要点：**通知零 SSE**（hub.go 属本包但通知读面不走事件——写入方五源自持 INSERT）；** announcements 公开面无 amw**；`read` 是 MySQL 保留字全 SQL 反引号（PRD ①）；对账勿混：routes.go:288-290 `/admin/announcements*` 3 条属 **M12** 管理后台组。

### M5 通知公告

4 端点全量（读 3+公开 1；写侧散布五域：auth/admin/recharge/seed/evaluation 自持 INSERT notifications——本域零写通知）。PRD §4 M5 已登记差异 ①~④（prd-qmtv-v1.md:168）引用不重立。

#### F5.01 通知读面与已读（notifications 三件）

> 对应 PRD：§4 M5 · GET /api/notifications、GET /api/notifications/unread-count、POST /api/notifications/read ｜ 状态：实现 ｜ 实现落点：notify_handlers.go:13-52、service.go:44-79 ｜ UI：App.vue 顶栏铃铛（60s 轮询 unread-count）+下拉列表 ｜ 场景挂钩：无专门 e2e（E2E-NAV-01 间接；集成锚 WorkbenchNotifyChain 第十二链覆盖 unread-count 联动+read 双分支）

##### 1 功能定义

站内通知读面：最近 50 条列表、未读计数、已读标记（单条/全标）。

##### 2 触发条件与前置状态

三端点登录态（amw）；零前置（无通知返回空/0）。

##### 3 输入与校验

list 无参数；read body `{id?}` 可选——**有 id 单条/无 id 整用户全标**（service.go:69-77）；id 不属于自己也静默 0 行（无 404 面，quirk 照抄冻结 :480-483）。

##### 4 处理流程

list：`SELECT id,user_id,title,content,\`read\`,created_at … WHERE user_id=? ORDER BY created_at DESC **LIMIT 50**`（:53-55——**无已读过滤、无分页**，quirk；`read` 反引号 PRD ①）。unread-count：`COUNT(*) … AND \`read\`=0`（:63-65）。read：单条 `WHERE id=? AND user_id=?` / 全标 `WHERE user_id=?`（:69-77）。

##### 5 输出与结果状态

list=NotificationRow 数组（`read` int64 0/1、Content `*string` 可空下发 null，service.go:20-27）；unread-count=**ok 裸数字**（handler `OK(c, n)`，非对象信封——quirk）；read 恒 `true`。

##### 6 状态流转

`read` 0→1 单向（无未读回置——通知一旦已读不可逆，照冻结）。

##### 7 边界与异常

| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 超过 50 条 | 旧通知不可见（LIMIT 硬截，无分页参数） | — |
| read id 非本人/不存在 | 200 恒 true 静默 0 行（quirk） | — |
| bind 失败/空 body | 等价全标已读（`_ = c.Bind` 静默，notify_handlers.go:45——与 DIF-29-23 同族形态，M9 DIF-30-2 同注） | — |

##### 8 权限与数据规则

`authed + amw` 全部按 token user_id 隔离（越权静默不暴露存在性）；写入方五源形态兼容（content 可空 TEXT）。

#### F5.02 公告公开面（announcements/active）

> 对应 PRD：§4 M5 · GET /api/announcements/active ○ ｜ 状态：实现 ｜ 实现落点：notify_handlers.go:53-63、service.go（announcements 段）+routes.go:102（**pub 组无 amw**）｜ UI：App.vue 全局公告条 ｜ 场景挂钩：E2E-NAV-01（公开路由巡检覆盖）

##### 1 功能定义

启用中公告列表（管理台 M12 写侧的公开读镜像）。

##### 2 触发条件与前置状态

**公开面无鉴权**（routes.go:102 pub 组——PRD ③；冻结 index.js:485 无 authRequired 同形）。

##### 3 输入与校验

无参数。

##### 4 处理流程

`SELECT id, title, content, active, created_at FROM announcements WHERE active=1 ORDER BY created_at DESC LIMIT 10`（显式 5 列非 SELECT *；**LIMIT 10 硬截**——冻结 index.js:485-487 同；active int64 1/0）。

##### 5 输出与结果状态

AnnouncementRow 数组（id/title/content/active/created_at 蛇形，service.go:30-36）；**最多 10 条**（LIMIT 10 硬截，service.go:87）。

##### 6 状态流转

active 由 M12 admin 面翻转（本域只读）。

##### 7 边界与异常

| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 无启用公告 | 空数组（nil→[]，DIF-29-1 同族形态） | — |
| 启用公告 >10 条 | 仅返回最近 10 条（LIMIT 10 硬截，冻结 index.js:487 同） | — |
| 未登录访问 | 200 正常返回（公开面） | — |

##### 8 权限与数据规则

公开面零鉴权零属主；content 全文下发；**admin 写侧三条路由（:288-290）归 M12**——本册对账不含。

---

## 册尾端点覆盖对账（4/4）

对账 `backend/cmd/server/routes.go:96-102` 亲数 4 条 ↔ 本册 F5.01~F5.02：

| # | 端点（routes.go:line） | 节 |
|---|---|---|
| 1 | GET /notifications（:99） | F5.01 |
| 2 | GET /notifications/unread-count（:100） | F5.01 |
| 3 | POST /notifications/read（:101） | F5.01 |
| 4 | GET /announcements/active（:102，pub 无 amw） | F5.02 |

差异纪律：PRD §4 M5 ①~④ 全引用不重立；本册**零新立 DIF**（bind 静默已随 DIF-30-2 族注记；quirk 均双方一致照抄）。

---

反链：

- [QMTV 功能规格说明书（FSD）v1.0 · 总则](/qmtv/fsd-qmtv/00-%E6%80%BB%E5%88%99/)
- [FSD 附录 · PRD 对照 / 代码域索引 / 差异台账](/qmtv/fsd-qmtv/99-%E9%99%84%E5%BD%95/)
