← 文档索引

系统通知机制

实现计划阅读版 · 2026-07-13 · Spec ③ · 11 任务 TDD · 状态 待执行

配套设计文档 系统通知机制设计。执行真相源(含 checkbox,驱动 subagent-driven-development/executing-plans):docs/superpowers/plans/2026-07-13-notifications.md

Goal:App 内统一通知收件箱(铃铛红点 + 六类通知列表),含个人到账通知(同事务零孤儿)、nodectl 发布、发版联动与 important 邮件兜底。

Architecture:单 notices 表(user_id NULL=广播/非空=定向)+ users.notices_read_at 已读水位;新 server/internal/notices 包(store/service/handler)替换现有 GET /v1/notices 空壳;生产三路(nodectl 手工、reward/pay 事务内钩子、发版脚本);客户端 noticesProvider 驱动铃铛与列表,原型先行统一两端通知视图。

Tech Stack:Go(chi+裸 SQL 双方言)、SMTP(复用 auth mailer 配置)、Flutter/Riverpod、shared 设计原型。

Global Constraints

File Structure(总览)

# 后端(新建)
server/migrations/{mysql,sqlite}/000026_notices.{up,down}.sql
server/internal/notices/{store,service,handler}.go
# 后端(修改)
server/internal/reward/service.go         # OnRegister×2/OnFirstPaidTx×2/ClaimTelegram 接 InsertNoticeTx
server/internal/pay/webhook.go            # settle 插「购买开通」通知
server/cmd/nodectl/main.go + notice.go    # notice add/list/revoke 子命令
server/cmd/server/main.go                 # 装配 notices + 注入 reward/pay + 路由替换空壳
server/internal/httpapi/account.go        # 删 ListNotices 空壳
server/api/openapi.yaml                   # Notice schema 扩展 + /notices/read
scripts/ci/release-client.sh              # 发版联动插 version 公告
# 客户端/原型(新建/修改)
design/prototype/screens/{ui-desktop,ui-mobile}.html  # 通知视图统一+新类型示例
client/lib/services/notices_api.dart · state/notices_provider.dart
client/lib/screens/notifications_page.dart # 真实化
client/lib/widgets/content_top_bar.dart + screens/account_page.dart # 红点接 provider
client/lib/l10n/app_text.dart + strings_*.dart ×6

后端(Task 1–7)

1

迁移 000026 — notices 表 + users.notices_read_at

新建 notices 表(六值 CHECK/ENUM、user_id NULL=广播、双语标题/正文、link/published_at/expires_at/revoked_at/email_sent_at、索引 (user_id,published_at))+ usersnotices_read_at。sqlite/mysql 各一对 up/down,四个文件。

新建 server/migrations/{sqlite,mysql}/000026_notices.{up,down}.sql
2

notices.Store — 数据访问层

新建 notices 包的纯数据访问层:广播∪定向合并查询(过滤 revoked/expired,倒序 limit)+ 已读水位判定;事务内插通知(事件钩子用,不自开事务)+ 自管连接的广播插入(nodectl/发版用)。

新建 server/internal/notices/{store.go, store_sqlite_test.go}

关键方法:ListForUser · MarkRead · InsertNoticeTx(事务内定向)· InsertBroadcast · Revoke · ListAdmin · MarkEmailSent · ListActiveUserEmails

3

notices HTTP handler + openapi 扩展 + 替换空壳

新建 Handler.List(GET /v1/notices,未登录 401)+ Handler.MarkRead(POST /v1/notices/read);删 httpapi/account.goListNotices 空壳(若编译顺序需要可延到 Task 7);openapi Notice schema 增 type/link/unread,响应增 unread_count,新增 /notices/read

新建 server/internal/notices/{handler.go, handler_test.go} · server/internal/httpapi/account.go、server/api/openapi.yaml
4

事件钩子 — 四处到账通知(同事务)

reward/pay 各自声明小接口 Noticer(照 Granter/Rewarder 惯例避免 import cycle),四接缝在既有事务内、Grant 成功之后追加 InsertNoticeTx:OnRegister(邀请注册双方)、OnFirstPaidTx(首充双方)、ClaimTelegram(TG 任务)、pay webhook.settle(购买开通)。插入失败视为该事务失败(随之回滚),非吞错。

server/internal/reward/service.go、server/internal/pay/webhook.go · 新建 notices_hook_sqlite_test.go、webhook_notice_sqlite_test.go
5

notices.Service — 发布/撤回 + 邮件兜底

校验(type∈六值、标题非空、Email=true 仅 important 可用)→ Publish:InsertBroadcast → 写 audit(notice_publish)→ Email 时逐发 active 用户(失败仅 log)→ MarkEmailSent 幂等。RevokeByID:Revoke + audit(notice_revoke)。

新建 server/internal/notices/{service.go, service_test.go}
6

nodectl notice 子命令

notice add --type … --title-zh … --title-en … [--body-zh] [--body-en] [--link] [--expires] [--email] / notice list [--all] [--limit] / notice revoke <id>。add 组 PublishInputService.Publish(SMTP mailer 复用 auth 配置或包内极简 net/smtp 实现;无配置且 --email 明确报错拒发)。

新建 server/cmd/nodectl/{notice.go, notice_test.go} · server/cmd/nodectl/main.go
7

main.go 装配 + 发版联动

装配 noticesStore/noticesHandler,注入 rewardSvc.SetNoticer/payWebhook.SetNoticer;路由 protected 组替换空壳:GET /notices、新增 POST /notices/readscripts/ci/release-client.sh 在 version.yaml 推送成功后追加 nodectl notice add --type version(失败不阻断发版)。

server/cmd/server/main.go、server/internal/httpapi/account.go、scripts/ci/release-client.sh

原型与客户端(Task 8–10)

8

原型先行 — 两端通知视图统一 + 新类型示例

统一形态为桌面式「类型 pill + 标题 + 相对时间 + 未读点」:改造 ui-mobile 通知子屏对齐桌面 .notif-row 结构(放弃 icon 区分);两端各补三类新示例行(reward/version/promo),i18n 字典补对应示例文案(zh/en),两端占位内容统一为同一组;点击行展开正文的简单交互示意。

design/prototype/screens/{ui-desktop,ui-mobile}.html
9

客户端 notices api + provider

新建 NoticesApi(fetch()GET /v1/noticesmarkRead()POST /v1/notices/read)+ NoticeItem/NoticesData 数据类(fromJson 安全默认,缺字段不 crash);Riverpod noticesProvider(AsyncNotifierProvider):未登录 null、已登录延迟 2s 首拉(照 update_provider 模式),refresh()/markAllRead() 本地态同步。

新建 client/lib/services/notices_api.dart · state/notices_provider.dart · test/unit/notices_api_test.dart
10

通知页真实化 + 铃铛红点接 provider + l10n

l10n 加 notifTypeReward/Version/Promo(六语实现)。notifications_page 删静态数据,ref.watch(noticesProvider) 三态(loading/空/error);行=类型 pill + 双语标题 + 相对时间 + 未读点,点击展开正文,version 行「去更新」接现有更新流程,进页调 markAllRead()content_top_bar/account_pageNotificationBell(hasUnread:true) 硬编码 → 接 unreadCount>0 真值;回前台刷新接壳层 lifecycle。

client/lib/screens/notifications_page.dart、client/lib/widgets/content_top_bar.dart、client/lib/screens/account_page.dart、client/lib/l10n/app_text.dart + strings_{zh,en,ja,ko,ru,es}.dart · 新建 test/widget/notifications_page_test.dart

文档(Task 11)

11

计划 HTML 阅读版 + 索引登记 + 设计文档互链

即本文档:按项目「设计/计划双产物」规范,把 docs/superpowers/plans/2026-07-13-notifications.md(执行真相源,含 checkbox)生成同内容 HTML 阅读版,登记进 docs/index.html「实现计划 / Plans」分类,并把 notifications-design.html 顶部「配套实现计划」链接改为直链本文档。

新建 docs/notifications-plan.html · docs/index.html、docs/notifications-design.html

验收(端到端)

不在本轮(YAGNI)

本页为阅读版,不含逐步 TDD 代码细节;完整测试代码/实现片段见执行真相源 docs/superpowers/plans/2026-07-13-notifications.md