docs: 系统通知机制设计文档(Spec ③)
ci-pangolin / Lint — shellcheck (pull_request) Successful in 15s
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (pull_request) Successful in 28s
ci-pangolin / Cleartext Scan — Android 禁明文 (pull_request) Successful in 21s
ci-pangolin / OpenAPI Sync Check (pull_request) Successful in 36s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (pull_request) Successful in 20s
ci-pangolin / Codegen Drift — token 生成物未漂移 (pull_request) Successful in 3s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (pull_request) Successful in 4s
ci-pangolin / Go — build + test (pull_request) Failing after 11s
ci-pangolin / E2E Smoke — L4 进程级端到端 (pull_request) Failing after 10s
ci-pangolin / Go — integration (mysql/redis testcontainers) (pull_request) Failing after 4m36s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (pull_request) Failing after 21s
ci-pangolin / Flutter — analyze + test (pull_request) Failing after 11m37s
ci-pangolin / Lint — shellcheck (pull_request) Successful in 15s
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (pull_request) Successful in 28s
ci-pangolin / Cleartext Scan — Android 禁明文 (pull_request) Successful in 21s
ci-pangolin / OpenAPI Sync Check (pull_request) Successful in 36s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (pull_request) Successful in 20s
ci-pangolin / Codegen Drift — token 生成物未漂移 (pull_request) Successful in 3s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (pull_request) Successful in 4s
ci-pangolin / Go — build + test (pull_request) Failing after 11s
ci-pangolin / E2E Smoke — L4 进程级端到端 (pull_request) Failing after 10s
ci-pangolin / Go — integration (mysql/redis testcontainers) (pull_request) Failing after 4m36s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (pull_request) Failing after 21s
ci-pangolin / Flutter — analyze + test (pull_request) Failing after 11m37s
App 内统一通知收件箱,六类(重要/新特性/新闻/到账/版本/活动)。 单 notices 表+user_id(NULL=广播);已读服务端 last_read_at 水位; GET /v1/notices(合并+unread_count)+POST /read;生产三路(nodectl/ 事件钩子同事务/发版联动);邮件仅 important+显式 --email。 含数据流 SVG;登记 docs/index.html;与 Spec② 奖励事件接缝正式接通。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,11 @@
|
||||
</div>
|
||||
|
||||
<h2>设计方案 / Specs</h2>
|
||||
<a class="doc" href="notifications-design.html">
|
||||
<div class="t">系统通知机制(Spec ③)<span class="tag html">HTML</span></div>
|
||||
<div class="d">App 内统一通知收件箱(铃铛+列表),六类:重要/新特性/新闻/到账(个人定向)/版本/活动。单 notices 表 + user_id(NULL=广播);已读用服务端 last_read_at 水位多端同步;GET /v1/notices(合并广播+定向,unread_count) + POST /read。生产三路:nodectl notice 子命令(手工) / 事件钩子同事务(购买开通·邀请·首充·TG 到账,零孤儿通知) / 发版脚本联动插 version 公告。邮件仅 important+显式 --email(SMTP 直发+email_sent_at 幂等)。不做:APNs/FCM(二期)/逐条已读/偏好开关/六语内容。原型先行:两端通知视图先统一(桌面pill vs 移动icon)再动代码。</div>
|
||||
<div class="path">docs/notifications-design.html</div>
|
||||
</a>
|
||||
<a class="doc" href="invite-task-rewards-design.html">
|
||||
<div class="t">邀请奖励 + 奖励任务(加入 TG 频道)(Spec ②)<span class="tag html">HTML</span></div>
|
||||
<div class="d">用「送 Pro 会员天数」驱动增长,两条并行途径共用发天数原语(applySubscription source=invite/task)。① 邀请两段式双方都得:注册各 +3、被邀请人首充双方再各 +7(每转化 +10/+10)。② 奖励任务首个「加入 Telegram 频道」+3,经 Bot getChatMember 真校验(token 绑号→/tg/webhook→查成员→发)。防刷:自邀请拦截 + invitee 唯一 + dp_uuid 设备去重 + 注册段月度封顶(首充不封)+ telegram_id 全局唯一领。数据:users +invite_code/first_paid_at、referrals、通用 reward_claims。不做现金/多级/退群回收;通知集成留接缝待 Spec③。</div>
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>系统通知机制 — 设计</title>
|
||||
<style>
|
||||
:root{
|
||||
--bg:#0f1117; --panel:#171a22; --panel2:#1d2129; --fg:#e6e8ee; --fg2:#a8afbd;
|
||||
--accent:#e0884f; --accent2:#5fb0c9; --ok:#5ec27a; --bad:#e06a6a; --warn:#e0b84f;
|
||||
--border:#272c36; --mono:"SF Mono",ui-monospace,Menlo,Consolas,monospace;
|
||||
--sans:-apple-system,"PingFang SC","Helvetica Neue",Arial,sans-serif;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--sans);line-height:1.7;font-size:15px}
|
||||
.wrap{max-width:920px;margin:0 auto;padding:48px 24px 96px}
|
||||
h1{font-size:30px;line-height:1.3;margin:0 0 8px;letter-spacing:-.01em}
|
||||
.sub{color:var(--fg2);font-size:15px;margin:0 0 32px}
|
||||
h2{font-size:21px;margin:44px 0 14px;padding-bottom:8px;border-bottom:1px solid var(--border)}
|
||||
h3{font-size:16px;margin:26px 0 8px;color:var(--accent2)}
|
||||
p{margin:10px 0}
|
||||
code{font-family:var(--mono);font-size:.88em;background:var(--panel2);padding:1px 6px;border-radius:5px;color:#f0d9c4}
|
||||
pre{background:#0a0c11;border:1px solid var(--border);border-radius:10px;padding:14px 16px;overflow-x:auto;font-family:var(--mono);font-size:13px;line-height:1.55;color:#cdd3df}
|
||||
.cm{color:#6b7280}
|
||||
.tag{display:inline-block;font-size:12px;font-weight:600;padding:2px 9px;border-radius:999px;vertical-align:middle}
|
||||
.tag.ok{background:rgba(94,194,122,.16);color:var(--ok)}
|
||||
.tag.info{background:rgba(95,176,201,.16);color:var(--accent2)}
|
||||
.tag.bad{background:rgba(224,106,106,.16);color:var(--bad)}
|
||||
.card{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:18px 20px;margin:16px 0}
|
||||
table{width:100%;border-collapse:collapse;margin:16px 0;font-size:14px}
|
||||
th,td{text-align:left;padding:9px 12px;border-bottom:1px solid var(--border);vertical-align:top}
|
||||
th{color:var(--fg2);font-weight:600;font-size:13px}
|
||||
td code{font-size:.85em}
|
||||
ul,ol{padding-left:22px;margin:10px 0}
|
||||
li{margin:5px 0}
|
||||
.lead{background:linear-gradient(180deg,rgba(224,136,79,.10),transparent);border:1px solid var(--border);border-radius:12px;padding:18px 20px;margin:0 0 8px}
|
||||
.small{color:var(--fg2);font-size:13px}
|
||||
a{color:var(--accent2)}
|
||||
.back{display:inline-block;margin-bottom:24px;font-size:13px}
|
||||
b{color:#fff}
|
||||
.cols{display:grid;grid-template-columns:1fr 1fr;gap:16px}
|
||||
@media(max-width:680px){.cols{grid-template-columns:1fr}}
|
||||
.diagram{background:#0a0c11;border:1px solid var(--border);border-radius:10px;padding:12px;margin:16px 0;overflow-x:auto}
|
||||
.diagram svg{display:block;margin:0 auto;min-width:680px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<a class="back" href="index.html">← 文档索引</a>
|
||||
<h1>系统通知机制</h1>
|
||||
<p class="sub">设计文档 · 2026-07-13 · Spec ③(系统通知)· 状态 <span class="tag info">已评审定稿</span> · 配套实现计划见 <a href="index.html">索引「实现计划」分类</a>(落定后互链)</p>
|
||||
|
||||
<div class="lead">
|
||||
<b>目标</b>:App 内统一通知收件箱(顶栏铃铛 + 通知列表),覆盖六类内容:重要通知 / 新特性 / 新闻 / 到账(购买·邀请·任务奖励,个人定向)/ 版本 / 活动。<br>
|
||||
<b>送达</b>:<b>App 内为主</b>(打开/回前台拉取),<b>邮件仅兜底「重要通知」且需显式指定</b>;系统推送(APNs/FCM)明确二期。<br>
|
||||
<b>决策(已定)</b>:单表 + audience(user_id 空=广播)· 已读用服务端 <code>last_read_at</code> 水位(多端同步)· 发版脚本联动插 version 公告 · 发布走 <code>nodectl notice</code> 子命令。<br>
|
||||
<b>现状底子</b>:<code>GET /v1/notices</code> 空壳已在(恒空列表);openapi 已有双语 Notice schema(需扩 type/link/unread);客户端占位屏/铃铛/五条 l10n 已备;jiu 无可参考实现(其通知为二期占位)。
|
||||
</div>
|
||||
|
||||
<h2>1 · 类型口径(单一来源)</h2>
|
||||
<table>
|
||||
<tr><th>type</th><th>标签(zh/en)</th><th>受众</th><th>典型内容</th><th>生产方式</th></tr>
|
||||
<tr><td><code>important</code></td><td>重要 / Important</td><td>广播(可定向)</td><td>服务条款、安全、合规、线路维护</td><td>nodectl(可 <code>--email</code>)</td></tr>
|
||||
<tr><td><code>feature</code></td><td>功能 / Feature</td><td>广播</td><td>新增大阪线路、协议选择上线</td><td>nodectl</td></tr>
|
||||
<tr><td><code>news</code></td><td>新闻 / News</td><td>广播</td><td>产品动态、博客</td><td>nodectl</td></tr>
|
||||
<tr><td><code>reward</code></td><td>到账 / Credited</td><td><b>个人定向</b></td><td>购买开通 ×31 天、邀请注册 +3、首充 +7、TG 任务 +3</td><td><b>事件钩子自动</b>(同事务)</td></tr>
|
||||
<tr><td><code>version</code></td><td>版本 / Update</td><td>广播</td><td>vX.Y.Z 发布 + 要点;点击接更新流程</td><td><b>发版脚本联动</b></td></tr>
|
||||
<tr><td><code>promo</code></td><td>活动 / Promo</td><td>广播</td><td>¥6 限购上线、邀请双倍周、年付折扣</td><td>nodectl</td></tr>
|
||||
</table>
|
||||
<p class="small">promo 与 feature/news 的边界:让用户「花钱/参与活动」的归 promo;产品功能上新归 feature;动态资讯归 news。类型集与客户端类型 pill、原型登记、l10n 标签三处须同集(L1 闸)。</p>
|
||||
|
||||
<h2>2 · 数据流总览</h2>
|
||||
<div class="diagram">
|
||||
<svg width="840" height="360" viewBox="0 0 840 360" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,PingFang SC,Arial" font-size="12.5">
|
||||
<defs><marker id="ah" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#8b93a3"/></marker></defs>
|
||||
<!-- producers -->
|
||||
<g fill="#e6e8ee" text-anchor="middle">
|
||||
<rect x="20" y="24" width="200" height="46" rx="8" fill="#1d2129" stroke="#3a4150"/>
|
||||
<text x="120" y="43" font-weight="600">nodectl notice add</text>
|
||||
<text x="120" y="60" fill="#a8afbd" font-size="11">important/feature/news/promo</text>
|
||||
<rect x="20" y="92" width="200" height="46" rx="8" fill="#1d2129" stroke="#3a4150"/>
|
||||
<text x="120" y="111" font-weight="600">事件钩子(同事务)</text>
|
||||
<text x="120" y="128" fill="#a8afbd" font-size="11">购买开通 · 邀请/首充/TG 到账</text>
|
||||
<rect x="20" y="160" width="200" height="46" rx="8" fill="#1d2129" stroke="#3a4150"/>
|
||||
<text x="120" y="179" font-weight="600">release-client.sh</text>
|
||||
<text x="120" y="196" fill="#a8afbd" font-size="11">发版联动插 version 公告</text>
|
||||
</g>
|
||||
<!-- notices table -->
|
||||
<rect x="300" y="80" width="180" height="90" rx="10" fill="#22201a" stroke="#5a4a33"/>
|
||||
<text x="390" y="112" fill="#e0a06a" text-anchor="middle" font-weight="700">notices 表</text>
|
||||
<text x="390" y="132" fill="#a8afbd" text-anchor="middle" font-size="11">user_id NULL=广播</text>
|
||||
<text x="390" y="149" fill="#a8afbd" text-anchor="middle" font-size="11">非空=个人定向</text>
|
||||
<g stroke="#8b93a3" marker-end="url(#ah)">
|
||||
<line x1="220" y1="47" x2="296" y2="95"/>
|
||||
<line x1="220" y1="115" x2="296" y2="120"/>
|
||||
<line x1="220" y1="183" x2="296" y2="150"/>
|
||||
</g>
|
||||
<!-- api -->
|
||||
<rect x="560" y="40" width="250" height="56" rx="8" fill="#17291d" stroke="#2e5738"/>
|
||||
<text x="685" y="62" fill="#7fcf95" text-anchor="middle" font-weight="600">GET /v1/notices</text>
|
||||
<text x="685" y="80" fill="#a8afbd" text-anchor="middle" font-size="11">广播+我的 · unread 按水位算 · unread_count</text>
|
||||
<rect x="560" y="112" width="250" height="46" rx="8" fill="#17291d" stroke="#2e5738"/>
|
||||
<text x="685" y="131" fill="#7fcf95" text-anchor="middle" font-weight="600">POST /v1/notices/read</text>
|
||||
<text x="685" y="148" fill="#a8afbd" text-anchor="middle" font-size="11">users.notices_read_at = now(多端同步)</text>
|
||||
<g stroke="#8b93a3" marker-end="url(#ah)">
|
||||
<line x1="480" y1="105" x2="556" y2="70"/>
|
||||
<line x1="480" y1="130" x2="556" y2="133"/>
|
||||
</g>
|
||||
<!-- client -->
|
||||
<rect x="560" y="196" width="250" height="64" rx="8" fill="#1d2129" stroke="#3a4150"/>
|
||||
<text x="685" y="220" fill="#e6e8ee" text-anchor="middle" font-weight="600">客户端 noticesProvider</text>
|
||||
<text x="685" y="238" fill="#a8afbd" text-anchor="middle" font-size="11">启动延迟拉 + 回前台拉;铃铛红点=unread_count>0</text>
|
||||
<text x="685" y="253" fill="#a8afbd" text-anchor="middle" font-size="11">打开列表 → 调 read 清红点</text>
|
||||
<g stroke="#8b93a3" marker-end="url(#ah)"><line x1="685" y1="96" x2="685" y2="192"/></g>
|
||||
<!-- email -->
|
||||
<rect x="300" y="240" width="180" height="56" rx="8" fill="#2a1f1a" stroke="#5a3f2e"/>
|
||||
<text x="390" y="262" fill="#e0a06a" text-anchor="middle" font-weight="600">邮件兜底(SMTP)</text>
|
||||
<text x="390" y="280" fill="#a8afbd" text-anchor="middle" font-size="11">仅 important + 显式 --email</text>
|
||||
<g stroke="#e0884f" stroke-dasharray="4 3" marker-end="url(#ah)"><line x1="390" y1="170" x2="390" y2="236"/></g>
|
||||
<text x="20" y="330" fill="#6b7280" font-size="11">系统推送(APNs/FCM)二期;openapi 描述过的「静态镜像 JSON 副本」意图保留、本期不实现。</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h2>3 · 数据模型(迁移 000026,sqlite/mysql 两套)</h2>
|
||||
<p><b>notices</b> 新表:</p>
|
||||
<table>
|
||||
<tr><th>列</th><th>类型</th><th>说明</th></tr>
|
||||
<tr><td><code>id</code></td><td>PK 自增</td><td></td></tr>
|
||||
<tr><td><code>type</code></td><td>CHECK/ENUM(六值)</td><td><code>important|feature|news|reward|version|promo</code></td></tr>
|
||||
<tr><td><code>user_id</code></td><td>bigint <b>NULL</b></td><td><b>NULL=全员广播;非空=定向该用户</b>(比字符串 audience 可索引);FK users(id)</td></tr>
|
||||
<tr><td><code>title_zh / title_en</code></td><td>text</td><td>双语标题(内容仅 zh/en;ja/ko/ru/es 界面回落 en)</td></tr>
|
||||
<tr><td><code>body_zh / body_en</code></td><td>text NULL</td><td>双语正文(Markdown;reward 类一行文案可空)</td></tr>
|
||||
<tr><td><code>link</code></td><td>text NULL</td><td>可选动作(version 类点击接更新流程等)</td></tr>
|
||||
<tr><td><code>published_at</code></td><td>datetime</td><td>排序与未读水位比较基准(Go 端算好传 ?)</td></tr>
|
||||
<tr><td><code>expires_at</code></td><td>datetime NULL</td><td>到期自动不再下发(promo 常用)</td></tr>
|
||||
<tr><td><code>revoked_at</code></td><td>datetime NULL</td><td>撤回(nodectl notice revoke)</td></tr>
|
||||
<tr><td><code>email_sent_at</code></td><td>datetime NULL</td><td>邮件兜底幂等标记(同公告只发一轮)</td></tr>
|
||||
<tr><td colspan="3">索引:<code>(user_id, published_at)</code>;广播查询走 user_id IS NULL 分支</td></tr>
|
||||
</table>
|
||||
<p><b>users 加一列</b>:<code>notices_read_at DATETIME NULL</code> —— 已读水位:<code>published_at > notices_read_at</code> 即未读。<b>不做逐条已读</b>(YAGNI,水位方案多端同步且零关联表)。</p>
|
||||
|
||||
<h2>4 · API(扩现有空壳 <code>accountAPI.ListNotices</code>)</h2>
|
||||
<table>
|
||||
<tr><th>端点</th><th>行为</th></tr>
|
||||
<tr><td><code>GET /v1/notices</code>(protected)</td><td>返回 <code>{notices:[…], unread_count}</code>:合并「<code>user_id IS NULL</code> + <code>user_id=me</code>」,过滤 <code>revoked_at IS NOT NULL</code> 与 <code>expires_at < now</code>,<code>published_at</code> 倒序 <b>limit 50</b>;每条含 <code>id/type/title_zh/title_en/body_zh/body_en/link/published_at/unread</code>(unread 服务端按水位算)。</td></tr>
|
||||
<tr><td><code>POST /v1/notices/read</code>(protected)</td><td><code>UPDATE users SET notices_read_at=?</code>(now)。客户端打开通知列表时调,红点全端清零。</td></tr>
|
||||
</table>
|
||||
<ul>
|
||||
<li>openapi:<code>Notice</code> schema 增 <code>type/link/unread</code>,响应对象增 <code>unread_count</code>(现有 id/title/body/published_at 必填保持兼容)。</li>
|
||||
<li>不做独立未读计数端点——列表响应自带,客户端启动/回前台拉列表即得红点。</li>
|
||||
</ul>
|
||||
|
||||
<h2>5 · 生产者(三路)</h2>
|
||||
<h3>5.1 nodectl notice 子命令(手工发布,important/feature/news/promo)</h3>
|
||||
<pre>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] <span class="cm"># 含已撤回/过期</span>
|
||||
nodectl notice revoke <id></pre>
|
||||
<ul>
|
||||
<li>接入方式沿 nodectl 既有模式(直连 DB 或管理通道,以现状为准);校验:type 合法、双语标题必填、--email 仅 important 可用。</li>
|
||||
<li>发布/撤回写 <code>audit_log</code>(action <code>notice_publish</code>/<code>notice_revoke</code>)。</li>
|
||||
</ul>
|
||||
<h3>5.2 事件自动通知(reward 类,个人定向,<b>同事务</b>)</h3>
|
||||
<table>
|
||||
<tr><th>事件</th><th>接缝(既有事务内追加一条 INSERT)</th><th>文案模板(双语,服务端渲染入库)</th></tr>
|
||||
<tr><td>购买开通</td><td>pay <code>webhook.settle</code>(Grant→MarkPaid 之后、Commit 前)</td><td>「已开通 Pro ·31 天 / Pro activated · 31 days」</td></tr>
|
||||
<tr><td>邀请注册奖励(双方)</td><td>reward <code>OnRegister</code> 两次 Grant 后</td><td>「邀请奖励 +3 天已到账 / Invite reward +3 days credited」</td></tr>
|
||||
<tr><td>首充奖励(双方)</td><td>reward <code>OnFirstPaidTx</code> 两次 Grant 后</td><td>「好友首购奖励 +7 天已到账 / …+7 days credited」</td></tr>
|
||||
<tr><td>TG 任务</td><td>reward <code>ClaimTelegram</code> Grant 后 Commit 前</td><td>「任务奖励 +3 天已到账 / Task reward +3 days credited」</td></tr>
|
||||
</table>
|
||||
<p class="small"><b>同事务不变量</b>:通知与发放同 COMMIT/同回滚——用户看到「到账」通知 ⟺ 权益真到账,零孤儿通知(与钱路既有纪律一致)。实现为 <code>reward/notices</code> 侧一个 <code>InsertNoticeTx(ctx, tx, userID, type, …)</code> 小函数,各接缝一行调用。</p>
|
||||
<h3>5.3 发版联动(version 类)</h3>
|
||||
<ul>
|
||||
<li><code>scripts/ci/release-client.sh</code> 在推 version.yaml 到 pangolin1 后,追加一步经 ssh 调 <code>nodectl notice add --type version --title-zh "vX.Y.Z 已发布" …</code>(版本号/要点来自 tag 与 release notes)。</li>
|
||||
<li>客户端 version 类行点击 → 走现有 <code>update_provider</code>/更新流程(link 承载)。</li>
|
||||
</ul>
|
||||
|
||||
<h2>6 · 邮件兜底</h2>
|
||||
<ul>
|
||||
<li><b>范围</b>:仅 <code>important</code> 且发布时显式 <code>--email</code>;广播=遍历 active 用户逐发,定向=单发。复用现有 SMTP mailer(auth/mailer.go 同款配置)。</li>
|
||||
<li><b>幂等</b>:<code>notices.email_sent_at</code> 置位后不重发;发送失败记日志继续(best-effort,不阻塞发布)。用户量增大后再演进为队列(明确非本期)。</li>
|
||||
<li>模板一套双语合排(邮件不受 App 单显铁律约束):标题 + 正文 + 「在 App 内查看」。</li>
|
||||
</ul>
|
||||
|
||||
<h2>7 · 客户端</h2>
|
||||
<ul>
|
||||
<li><b>noticesProvider</b>(AsyncNotifier,照 update_provider 惯例):启动延迟数秒首拉 + 回前台重拉;缓存内存;<code>unread_count>0</code> 驱动铃铛红点(替换现在两处硬编码 <code>hasUnread: true</code>——content_top_bar 与 account_page)。</li>
|
||||
<li><b>通知页真实化</b>(notifications_page):删静态示例;行 = 类型 pill + 标题 + 相对时间(复用 <code>relativeTime</code> 单源)+ 未读点;点击行内展开正文(Markdown 降级纯文本渲染即可);<b>进入页面即调 <code>POST /read</code></b> 清红点;空态用现有 <code>notifEmpty</code>;version 类行尾「去更新」动作接现有更新流程。</li>
|
||||
<li><b>l10n</b>:类型标签补 <code>notifTypeReward / notifTypeVersion / notifTypePromo</code>(六语);通知<b>内容</b>按 App 语言取 zh 或 en 字段(非中即英)。</li>
|
||||
<li><b>原型先行(铁律)</b>:动代码前先统一两端原型通知视图——现状桌面用文字 pill、移动用图标,<b>不一致</b>;统一为「类型 pill + 未读点」桌面式(移动同构),并补 reward/version/promo 三类示例行 + 展开态;同一提交回填两份原型。</li>
|
||||
</ul>
|
||||
|
||||
<h2>8 · 边界(YAGNI,本期不做)</h2>
|
||||
<ul>
|
||||
<li><span class="tag bad">不做</span> APNs/FCM 系统推送(二期;大陆 FCM 可达性另议)。</li>
|
||||
<li><span class="tag bad">不做</span> 逐条已读 / 通知偏好按类型开关 / 管理后台页。</li>
|
||||
<li><span class="tag bad">不做</span> 六语内容(内容 zh/en,其余界面语言回落 en)。</li>
|
||||
<li><span class="tag bad">不做</span> openapi 描述里的「多镜像签名静态 JSON 副本」(意图保留,后续抗封锁专项再做)。</li>
|
||||
<li><span class="tag bad">不做</span> 邮件队列/重试体系(现量级直发 + email_sent_at 幂等够用)。</li>
|
||||
</ul>
|
||||
|
||||
<h2>9 · 测试要点</h2>
|
||||
<ul>
|
||||
<li><b>查询合并</b>:广播+定向合并、他人定向不可见;revoked/expired 过滤;limit 50;倒序。</li>
|
||||
<li><b>未读水位</b>:read 前后 unread/unread_count 变化;新公告晚于水位再变未读;多“设备”(两次会话)读同一水位。</li>
|
||||
<li><b>同事务</b>:TG 领奖/首充/注册奖励成功 → reward 通知在;Grant 失败回滚 → 通知不留(孤儿为零);购买开通同款。</li>
|
||||
<li><b>nodectl</b>:类型校验、--email 限 important、revoke 后列表即消失、audit_log 两 action。</li>
|
||||
<li><b>邮件</b>:仅 important+--email 触发;email_sent_at 幂等(重复执行不再发);失败不阻塞发布。</li>
|
||||
<li><b>客户端</b>:红点由 unread_count 驱动(0 不显);进入列表调 read 后红点清;空态;version 行动作;golden(通知页多主题)。</li>
|
||||
<li>双 DB:迁移 up/down round-trip;时间 Go 端传 ?。</li>
|
||||
</ul>
|
||||
|
||||
<h2>10 · 涉及文件(实现锚点)</h2>
|
||||
<pre><span class="cm"># 后端</span>
|
||||
server/migrations/{mysql,sqlite}/000026_notices.{up,down}.sql <span class="cm"># notices 表 + users.notices_read_at</span>
|
||||
server/internal/notices/ <span class="cm"># 新包:store(查询合并/水位/InsertNoticeTx)+ service(邮件兜底)+ handler</span>
|
||||
server/internal/httpapi/account.go <span class="cm"># ListNotices 空壳替换为真实现(或移交 notices 包)</span>
|
||||
server/internal/reward/service.go <span class="cm"># OnRegister/OnFirstPaidTx/ClaimTelegram 接 InsertNoticeTx</span>
|
||||
server/internal/pay/webhook.go <span class="cm"># settle 接购买开通通知</span>
|
||||
server/cmd/nodectl/ <span class="cm"># notice add/list/revoke 子命令</span>
|
||||
scripts/ci/release-client.sh <span class="cm"># 发版联动插 version 公告</span>
|
||||
server/api/openapi.yaml <span class="cm"># Notice schema 扩 type/link/unread + /notices/read</span>
|
||||
<span class="cm"># 客户端</span>
|
||||
design/prototype/screens/{ui-desktop,ui-mobile}.html <span class="cm"># 原型先行:两端通知视图统一 + 三类新示例</span>
|
||||
client/lib/state/notices_provider.dart <span class="cm"># 新:拉取/未读/read</span>
|
||||
client/lib/services/notices_api.dart <span class="cm"># 新:GET /v1/notices + POST /read</span>
|
||||
client/lib/screens/notifications_page.dart <span class="cm"># 占位 → 真实</span>
|
||||
client/lib/widgets/{notification_bell,content_top_bar}.dart + screens/account_page.dart <span class="cm"># 红点接 provider</span>
|
||||
client/lib/l10n/ <span class="cm"># notifTypeReward/Version/Promo 六语</span></pre>
|
||||
|
||||
<p class="small" style="margin-top:32px">下一步:writing-plans 出逐任务实现计划(md 真相源 + HTML 阅读版),Subagent 驱动执行。与 <a href="invite-task-rewards-design.html">邀请奖励设计(Spec ②)</a>的「奖励事件接缝」在 5.2 节正式接通。</p>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user