diff --git a/docs/index.html b/docs/index.html index ca74e0c..53acf9f 100644 --- a/docs/index.html +++ b/docs/index.html @@ -101,6 +101,11 @@

实现计划 / Plans

+ +
系统通知机制 实现计划(Spec ③)HTML
+
阅读版;执行真相源 docs/superpowers/plans/2026-07-13-notifications.md(11 任务 TDD,含 checkbox)。后端(1-7):迁移 000026(notices 表+users.notices_read_at) → notices.Store(广播∪定向合并/已读水位/事务内插) → GET /v1/notices+POST /read(替换空壳)+openapi 扩展 → 四事件钩子同事务(邀请注册/首充/TG任务/购买开通,零孤儿) → notices.Service(发布/撤回/important 邮件兜底幂等) → nodectl notice add/list/revoke → main.go 装配+发版脚本联动插 version 公告。原型与客户端(8-10):原型先行统一两端通知视图(桌面式pill+未读点)+reward/version/promo 示例 → notices api/provider(未读计数/延迟首拉) → 通知页真实化+铃铛红点接 provider+三新类型六语标签。Subagent 驱动执行。设计见 notifications-design.html。
+
docs/notifications-plan.html · 真相源 docs/superpowers/plans/2026-07-13-notifications.md
+
邀请奖励 + 奖励任务 实现计划(Spec ②)HTML
阅读版;执行真相源 docs/superpowers/plans/2026-07-12-invite-task-rewards.md(14 任务 TDD,含 checkbox)。后端(1-10):迁移 000024(referrals/reward_claims + source 扩容) → codes.GrantRewardTx 发奖原语 → reward.Store/Service(邀请码+防刷) → 接注册 ReferralHook → 接首充 pay webhook Rewarder → GET /v1/invite → TG 绑定 token → TG webhook getChatMember 真校验 → main.go 装配。客户端(11-13):invite api/provider → invite_page 真实化+六语 l10n → 注册页邀请码输入(deep-link 预填单列可选后置)。设计见 invite-task-rewards-design.html。
diff --git a/docs/notifications-design.html b/docs/notifications-design.html index e6e2e1d..a0d29c7 100644 --- a/docs/notifications-design.html +++ b/docs/notifications-design.html @@ -48,7 +48,7 @@
← 文档索引

系统通知机制

-

设计文档 · 2026-07-13 · Spec ③(系统通知)· 状态 已评审定稿 · 配套实现计划见 索引「实现计划」分类(落定后互链)

+

设计文档 · 2026-07-13 · Spec ③(系统通知)· 状态 已评审定稿 · 配套实现计划见 notifications-plan.html

目标:App 内统一通知收件箱(顶栏铃铛 + 通知列表),覆盖六类内容:重要通知 / 新特性 / 新闻 / 到账(购买·邀请·任务奖励,个人定向)/ 版本 / 活动。
diff --git a/docs/notifications-plan.html b/docs/notifications-plan.html new file mode 100644 index 0000000..40147c6 --- /dev/null +++ b/docs/notifications-plan.html @@ -0,0 +1,226 @@ + + + + + +系统通知机制 — 实现计划 + + + +
+← 文档索引 +

系统通知机制

+

实现计划阅读版 · 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

+
    +
  • 类型集六值恒定:important|feature|news|reward|version|promo(DB CHECK/ENUM、openapi、客户端 pill、原型登记四处同集)。
  • +
  • 内容仅 zh/en 双语字段;客户端 ja/ko/ru/es 界面取 en 内容。
  • +
  • 已读=水位:published_at > users.notices_read_at 即未读;不做逐条已读
  • +
  • 事件到账通知必须与发放同事务(传入 tx,不自开;失败随事务回滚)。
  • +
  • 邮件 important 且显式 --email;notices.email_sent_at 幂等;发送失败只记日志不阻塞发布。
  • +
  • 多 DB:迁移 server/migrations/{mysql,sqlite}/ 两套一一对应,下一编号 000026;裸 SQL;时间 Go 端 time.Now().UTC()?,禁 NOW()/UTC_TIMESTAMP()。
  • +
  • GET /v1/notices 响应契约(客户端按此解析):{"notices":[{id,type,title_zh,title_en,body_zh,body_en,link,published_at,unread}],"unread_count":N};列表=广播∪我的定向,过滤 revoked_at IS NOT NULL/expires_at<now,published_at 倒序 limit 50
  • +
  • 客户端改 UI 前原型先行(两端通知视图统一为「类型 pill+未读点」桌面式,补 reward/version/promo 示例),原型与代码同批次落地。
  • +
  • 颜色走 token(context.pangolin/var(--token)),文案经 AppText,红线词禁用。
  • +
  • 命令:cd server && go test ./...;cd client && flutter analyze && flutter test;bash server/run_sqlite_test.sh
  • +
+ +

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
+
    +
  • 验收:run_sqlite_test.sh + 迁移 up→down→up 全量测试通过(断言若因新迁移顶偏,按 000024/000025 既有修法更新计数)。
  • +
+
+ +
+
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

+
    +
  • 验收:广播+我的定向合并可见、他人定向/撤回/过期均排除;水位判定正确(空水位全未读);InsertNoticeTx 随外部事务回滚则零孤儿。
  • +
+
+ +
+
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
+
    +
  • 验收:list 返回契约字段齐全(type/unread/unread_count);read 后 unread_count 清零;未登录 401;openapi 校验通过;go build ./... 绿。
  • +
+
+ +
+
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
+
    +
  • 验收:四事件各自触发一条 type='reward' 定向通知;Grant 失败回滚时通知同回滚(零孤儿);双语文案模板落对天数。
  • +
+
+ +
+
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}
+
    +
  • 验收:非法 type/缺标题/email 配非 important 均报错;成功发布落库+audit;email=true 全员发送、email_sent_at 置位、二次 Publish 是新公告不误判幂等;Revoke 后列表不可见+audit 行。
  • +
+
+ +
+
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
+
    +
  • 验收:参数解析层单测覆盖 type 非法/缺标题/email 非 important/expires 格式各红;go build ./... && go test ./cmd/nodectl/ 绿。
  • +
+
+ +
+
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
+
    +
  • 验收:go build ./... && go vet ./cmd/server/ && go test ./... -count=1 全绿;shellcheck 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
+
    +
  • 验收:node design/prototype/serve.mjs 起两文件均 200;新增行无裸 hex,全走 token。
  • +
+
+ +
+
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
+
    +
  • 验收:notices_api_test.dart 用 MockClient 断言 fetch 解析(2 条/unread_count/类型/双语字段)+ markRead 打对路径;flutter analyze 新文件 0 issue。
  • +
+
+ +
+
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
+
    +
  • 验收:widget 测试断言类型 pill 文案在/未读点在/进页触发 markAllRead;flutter analyze 0 新增;flutter test 全绿(通知页 golden 若有则重录)。
  • +
+
+ +

文档(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
+
    +
  • 验收:三文件登记完整、互链可点;.md 保持执行真相源不变(供 subagent-driven-development 继续驱动 Task 1–10)。
  • +
+
+ +

验收(端到端)

+
    +
  • 后端:cd server && go test ./... 全绿;nodectl notice add --type news …GET /v1/notices(带 JWT)可见、红点计数对;revoke 后消失。
  • +
  • 事件:TG 领奖/邀请注册/首充/购买各触发一次 → 对应用户列表出现 reward 通知,与权益同事务。
  • +
  • 邮件:important + --email → active 用户收信一轮,email_sent_at 置位,重复 add 是新公告不误判。
  • +
  • 客户端:铃铛红点=unread_count 驱动;进通知页红点清零(另一设备登录同账号也清);version 行可跳更新;六语标签;空态正常。
  • +
  • 部署:迁移 000026 + server + nodectl 上 pangolin1;发一条真实 news 公告真机验证。
  • +
+ +

不在本轮(YAGNI)

+
    +
  • 不做 APNs/FCM 推送、逐条已读、通知偏好开关、管理后台页、六语内容、静态镜像 JSON 副本、邮件队列。
  • +
+ +

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

+ +
+ +