设计文档 · 2026-07-13 · Spec ③(系统通知)· 状态 已评审定稿 · 配套实现计划见 notifications-plan.html
last_read_at 水位(多端同步)· 发版脚本联动插 version 公告 · 发布走 nodectl notice 子命令。GET /v1/notices 空壳已在(恒空列表);openapi 已有双语 Notice schema(需扩 type/link/unread);客户端占位屏/铃铛/五条 l10n 已备;jiu 无可参考实现(其通知为二期占位)。
| type | 标签(zh/en) | 受众 | 典型内容 | 生产方式 |
|---|---|---|---|---|
important | 重要 / Important | 广播(可定向) | 服务条款、安全、合规、线路维护 | nodectl(可 --email) |
feature | 功能 / Feature | 广播 | 新增大阪线路、协议选择上线 | nodectl |
news | 新闻 / News | 广播 | 产品动态、博客 | nodectl |
reward | 到账 / Credited | 个人定向 | 购买开通 ×31 天、邀请注册 +3、首充 +7、TG 任务 +3 | 事件钩子自动(同事务) |
version | 版本 / Update | 广播 | vX.Y.Z 发布 + 要点;点击接更新流程 | 发版脚本联动 |
promo | 活动 / Promo | 广播 | ¥6 限购上线、邀请双倍周、年付折扣 | nodectl |
promo 与 feature/news 的边界:让用户「花钱/参与活动」的归 promo;产品功能上新归 feature;动态资讯归 news。类型集与客户端类型 pill、原型登记、l10n 标签三处须同集(L1 闸)。
notices 新表:
| 列 | 类型 | 说明 |
|---|---|---|
id | PK 自增 | |
type | CHECK/ENUM(六值) | important|feature|news|reward|version|promo |
user_id | bigint NULL | NULL=全员广播;非空=定向该用户(比字符串 audience 可索引);FK users(id) |
title_zh / title_en | text | 双语标题(内容仅 zh/en;ja/ko/ru/es 界面回落 en) |
body_zh / body_en | text NULL | 双语正文(Markdown;reward 类一行文案可空) |
link | text NULL | 可选动作(version 类点击接更新流程等) |
published_at | datetime | 排序与未读水位比较基准(Go 端算好传 ?) |
expires_at | datetime NULL | 到期自动不再下发(promo 常用) |
revoked_at | datetime NULL | 撤回(nodectl notice revoke) |
email_sent_at | datetime NULL | 邮件兜底幂等标记(同公告只发一轮) |
索引:(user_id, published_at);广播查询走 user_id IS NULL 分支 | ||
users 加一列:notices_read_at DATETIME NULL —— 已读水位:published_at > notices_read_at 即未读。不做逐条已读(YAGNI,水位方案多端同步且零关联表)。
accountAPI.ListNotices)| 端点 | 行为 |
|---|---|
GET /v1/notices(protected) | 返回 {notices:[…], unread_count}:合并「user_id IS NULL + user_id=me」,过滤 revoked_at IS NOT NULL 与 expires_at < now,published_at 倒序 limit 50;每条含 id/type/title_zh/title_en/body_zh/body_en/link/published_at/unread(unread 服务端按水位算)。 |
POST /v1/notices/read(protected) | UPDATE users SET notices_read_at=?(now)。客户端打开通知列表时调,红点全端清零。 |
Notice schema 增 type/link/unread,响应对象增 unread_count(现有 id/title/body/published_at 必填保持兼容)。nodectl notice add --type important --title-zh 线路维护通知 --title-en "Line Maintenance" \
[--body-zh …] [--body-en …] [--link …] [--expires 2026-08-01] [--email]
nodectl notice list [--all] # 含已撤回/过期
nodectl notice revoke <id>
audit_log(action notice_publish/notice_revoke)。| 事件 | 接缝(既有事务内追加一条 INSERT) | 文案模板(双语,服务端渲染入库) |
|---|---|---|
| 购买开通 | pay webhook.settle(Grant→MarkPaid 之后、Commit 前) | 「已开通 Pro ·31 天 / Pro activated · 31 days」 |
| 邀请注册奖励(双方) | reward OnRegister 两次 Grant 后 | 「邀请奖励 +3 天已到账 / Invite reward +3 days credited」 |
| 首充奖励(双方) | reward OnFirstPaidTx 两次 Grant 后 | 「好友首购奖励 +7 天已到账 / …+7 days credited」 |
| TG 任务 | reward ClaimTelegram Grant 后 Commit 前 | 「任务奖励 +3 天已到账 / Task reward +3 days credited」 |
同事务不变量:通知与发放同 COMMIT/同回滚——用户看到「到账」通知 ⟺ 权益真到账,零孤儿通知(与钱路既有纪律一致)。实现为 reward/notices 侧一个 InsertNoticeTx(ctx, tx, userID, type, …) 小函数,各接缝一行调用。
scripts/ci/release-client.sh 在推 version.yaml 到 pangolin1 后,追加一步经 ssh 调 nodectl notice add --type version --title-zh "vX.Y.Z 已发布" …(版本号/要点来自 tag 与 release notes)。update_provider/更新流程(link 承载)。important 且发布时显式 --email;广播=遍历 active 用户逐发,定向=单发。复用现有 SMTP mailer(auth/mailer.go 同款配置)。notices.email_sent_at 置位后不重发;发送失败记日志继续(best-effort,不阻塞发布)。用户量增大后再演进为队列(明确非本期)。unread_count>0 驱动铃铛红点(替换现在两处硬编码 hasUnread: true——content_top_bar 与 account_page)。relativeTime 单源)+ 未读点;点击行内展开正文(Markdown 降级纯文本渲染即可);进入页面即调 POST /read 清红点;空态用现有 notifEmpty;version 类行尾「去更新」动作接现有更新流程。notifTypeReward / notifTypeVersion / notifTypePromo(六语);通知内容按 App 语言取 zh 或 en 字段(非中即英)。# 后端 server/migrations/{mysql,sqlite}/000026_notices.{up,down}.sql # notices 表 + users.notices_read_at server/internal/notices/ # 新包:store(查询合并/水位/InsertNoticeTx)+ service(邮件兜底)+ handler server/internal/httpapi/account.go # ListNotices 空壳替换为真实现(或移交 notices 包) server/internal/reward/service.go # OnRegister/OnFirstPaidTx/ClaimTelegram 接 InsertNoticeTx server/internal/pay/webhook.go # settle 接购买开通通知 server/cmd/nodectl/ # notice add/list/revoke 子命令 scripts/ci/release-client.sh # 发版联动插 version 公告 server/api/openapi.yaml # Notice schema 扩 type/link/unread + /notices/read # 客户端 design/prototype/screens/{ui-desktop,ui-mobile}.html # 原型先行:两端通知视图统一 + 三类新示例 client/lib/state/notices_provider.dart # 新:拉取/未读/read client/lib/services/notices_api.dart # 新:GET /v1/notices + POST /read client/lib/screens/notifications_page.dart # 占位 → 真实 client/lib/widgets/{notification_bell,content_top_bar}.dart + screens/account_page.dart # 红点接 provider client/lib/l10n/ # notifTypeReward/Version/Promo 六语
下一步:writing-plans 出逐任务实现计划(md 真相源 + HTML 阅读版),Subagent 驱动执行。与 邀请奖励设计(Spec ②)的「奖励事件接缝」在 5.2 节正式接通。