256 lines
21 KiB
HTML
256 lines
21 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>邀请奖励 + 奖励任务(加入 TG 频道) — 实现计划</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}
|
||
.kw{color:#c68bd6}.str{color:#b6d47a}.num{color:#e0b06a}.cm{color:#6b7280}.fn{color:#5fb0c9}
|
||
.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.warn{background:rgba(224,184,79,.16);color:var(--warn)}
|
||
.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}
|
||
.card.root{border-left:3px solid var(--accent)}
|
||
.card h3{margin-top: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}}
|
||
.task-num{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:8px;background:var(--panel2);color:var(--accent2);font-weight:700;font-size:13px;margin-right:10px;flex:none}
|
||
.task-head{display:flex;align-items:center;margin-bottom:4px}
|
||
.task-head h3{margin:0;color:var(--fg)}
|
||
.files{font-family:var(--mono);font-size:12.5px;color:var(--fg2);margin:8px 0}
|
||
.files b{color:#f0d9c4;font-weight:400}
|
||
.accept{margin:6px 0 0}
|
||
.accept li{color:var(--fg2)}
|
||
.accept li b{color:var(--fg)}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="wrap">
|
||
<a class="back" href="index.html">← 文档索引</a>
|
||
<h1>邀请奖励 + 奖励任务(加入 TG 频道)</h1>
|
||
<p class="sub">实现计划阅读版 · 2026-07-12 · Spec ② · 14 任务 TDD · 状态 <span class="tag info">待执行</span></p>
|
||
<p class="small">配套设计文档 <a href="invite-task-rewards-design.html">邀请奖励 + 奖励任务 — 设计</a>。执行真相源(含 checkbox,驱动 <code>subagent-driven-development</code>/<code>executing-plans</code>):<code>docs/superpowers/plans/2026-07-12-invite-task-rewards.md</code>。</p>
|
||
|
||
<div class="lead">
|
||
<b>Goal</b>:用「送 Pro 会员天数」驱动增长 —— 邀请两段式(注册双方各 +3、被邀请人首充双方各 +7)+ 奖励任务(加入 TG 频道 +3,Bot 真校验),全部复用现有 subscriptions 发天数链路,不造新轮子。<br><br>
|
||
<b>Architecture</b>:新增 <code>server/internal/reward</code> 包承载发奖 + 防刷;发天数复用 <code>codes.Service</code>(新增 <code>GrantRewardTx</code>)。注册段在 <code>auth.Service.Register</code> 后 best-effort 触发(自有 tx,不拖累注册主流程);首充段挂 <code>pay/webhook.settle</code> 同事务内(幂等、失败即整笔回滚重试);TG 走 <code>POST /tg/webhook</code>(Telegram <code>getChatMember</code> 真校验)。客户端 <code>invite_page</code> 由占位转真实(邀请区 + 任务区),注册页加邀请码输入。<br><br>
|
||
<b>Tech Stack</b>:Go(chi + 裸 SQL + golang-migrate 双 DB)、Redis(TG 绑定 token)、Flutter/Riverpod、Telegram Bot API。
|
||
</div>
|
||
|
||
<h2>Global Constraints</h2>
|
||
<ul>
|
||
<li>发天数一律复用 <code>codes.Service</code>(Pro plan);迁移须把 <code>subscriptions.source</code> CHECK/ENUM 从 <code>('trial','code','pay')</code> 扩到 <code>('trial','code','pay','invite','task')</code>(sqlite 重建表 / mysql <code>MODIFY</code>)。</li>
|
||
<li>奖励天数:注册段各 <b>3</b>、首充段各 <b>7</b>、TG 任务 <b>3</b>;注册段邀请人<b>月度封顶默认 10</b>(env <code>INVITE_REG_MONTHLY_CAP</code>)。</li>
|
||
<li>审计一律走 <code>audit_log</code>(<code>store.WriteAuditLog</code>),<b>无 sub_events 表</b>。</li>
|
||
<li>多 DB:<code>server/migrations/{mysql,sqlite}/</code> 两套一一对应;裸 SQL + <code>internal/db</code> 方言层;时间一律 Go 端 <code>time.Now().UTC()</code> 传 <code>?</code>,<b>禁</b> <code>NOW()</code>/<code>UTC_TIMESTAMP()</code>。</li>
|
||
<li>邀请码 = <b>8 位 base32 大写、去 <code>0/O/1/I</code></b>;<code>users.invite_code</code> UNIQUE、惰性生成。</li>
|
||
<li>绑定<b>仅注册时</b>、first-touch 永久不可改。防刷四道:自邀请拦截 + <code>referrals.invitee_id</code> UNIQUE + 设备去重(<code>devices.uuid</code> 已属别人则注册奖励不发、记 <code>rejected</code>)+ 注册段月度封顶 + <code>reward_claims</code> 双唯一 <code>(user_id,task_key)</code>/<code>(task_key,external_ref)</code>。</li>
|
||
<li>TG:<code>getChatMember</code> 返回 <code>member</code>/<code>administrator</code>/<code>creator</code> 视为已加入;bot 未配(<code>TG_REWARD_BOT_TOKEN</code> 空)则 <code>/tg/webhook</code> 404 且 App 任务卡隐藏。webhook 校验 <code>X-Telegram-Bot-Api-Secret-Token</code>。</li>
|
||
<li>客户端 l10n <b>无 codegen</b>:新 string 须 <code>app_text.dart</code> 加抽象 getter + 6 个 <code>strings_{zh,en,es,ja,ko,ru}.dart</code> 各加实现。</li>
|
||
<li><b>deep-link 自动预填 = 可选后置任务(Task 13 Step 6)</b>;MVP 走注册页手填邀请码。</li>
|
||
<li>TG 新基建(reward bot / 设为频道管理员 / <code>setWebhook</code>)由用户在实现前于 Telegram 侧配好。</li>
|
||
<li>命令:后端 <code>cd server && go test ./...</code>;客户端 <code>cd client && flutter analyze && flutter test</code>。</li>
|
||
</ul>
|
||
|
||
<h3>File Structure(总览)</h3>
|
||
<pre><span class="cm"># 后端(新建)</span>
|
||
server/migrations/{mysql,sqlite}/000024_invite_rewards.{up,down}.sql
|
||
server/internal/reward/{store,service,telegram,handler}.go
|
||
<span class="cm"># 后端(修改)</span>
|
||
server/internal/codes/paygrant.go <span class="cm"># + GrantRewardTx</span>
|
||
server/internal/auth/{service,handler}.go <span class="cm"># Register 加 inviteCode + ReferralHook</span>
|
||
server/internal/pay/webhook.go <span class="cm"># settle 首充钩子 + Rewarder</span>
|
||
server/cmd/server/main.go <span class="cm"># 装配 + 路由 + env</span>
|
||
<span class="cm"># 客户端(新建/修改)</span>
|
||
client/lib/services/invite_api.dart · state/invite_provider.dart
|
||
client/lib/screens/invite_page.dart <span class="cm"># 占位转真实</span>
|
||
client/lib/widgets/auth_screen.dart + services/auth_api.dart <span class="cm"># 邀请码输入</span>
|
||
client/lib/l10n/app_text.dart + strings_*.dart ×6</pre>
|
||
|
||
<h2>后端(Task 1–10)</h2>
|
||
|
||
<div class="card root">
|
||
<div class="task-head"><span class="task-num">1</span><h3>迁移 000024 — 新表 + source 扩容</h3></div>
|
||
<p>建 <code>referrals</code>(邀请关系,一对一绑定)、<code>reward_claims</code>(通用一次性任务领取)两张表;<code>users</code> 加 <code>invite_code</code>/<code>first_paid_at</code>;<code>subscriptions.source</code> CHECK/ENUM 扩容到含 <code>invite</code>/<code>task</code>(SQLite 需重建表,MySQL 直接 <code>MODIFY</code>)。sqlite/mysql 各一对 up/down,四个文件。</p>
|
||
<div class="files"><b>新建</b> server/migrations/{sqlite,mysql}/000024_invite_rewards.{up,down}.sql</div>
|
||
<ul class="accept">
|
||
<li><b>验收</b>:<code>server/internal/store/migrate_sqlite_test.go</code> 现有 up→down→up 全量测试(<code>run_sqlite_test.sh</code>)通过。</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="task-head"><span class="task-num">2</span><h3><code>codes.Service.GrantRewardTx</code> — 发奖天数原语</h3></div>
|
||
<p>在 <code>codes</code> 包加一个薄封装:复用既有 <code>applySubscription</code>(<code>max(到期,now)+days</code> 顺延语义)发 Pro 天数,<code>source∈{invite,task}</code>,并写一条 <code>audit_log</code>。这是后续所有发奖调用的唯一入口。</p>
|
||
<div class="files"><b>改</b> server/internal/codes/paygrant.go · <b>新建</b> reward_grant_sqlite_test.go</div>
|
||
<p><b>接口</b>:<code>GrantRewardTx(ctx, tx, userID, days, source, auditAction, ref) (subID, expiresAt, err)</code></p>
|
||
<ul class="accept">
|
||
<li><b>验收</b>:新用户发奖建订阅、<code>source</code> 落对、<code>audit_log</code> 命中 1 条。</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="task-head"><span class="task-num">3</span><h3><code>reward.Store</code> — 数据访问层</h3></div>
|
||
<p>新建 <code>reward</code> 包的纯数据访问层:邀请码惰性生成/解析、设备复用查询、月度计数、referrals/reward_claims 的 CRUD,唯一冲突统一映射为 <code>ErrClaimExists</code>。</p>
|
||
<div class="files"><b>新建</b> server/internal/reward/{store.go, store_sqlite_test.go}</div>
|
||
<p><b>关键方法</b>:<code>EnsureInviteCode</code> · <code>ResolveInviteCode</code> · <code>DeviceUsedByOther</code> · <code>RegRewardCountThisMonth</code> · <code>InsertReferralTx</code> · <code>MarkFirstPaidTx</code> · <code>ReferralByInvitee</code> · <code>MarkPaidRewardedTx</code> · <code>InsertClaimTx</code> · <code>Summary</code> · <code>TelegramClaimed</code></p>
|
||
<ul class="accept">
|
||
<li><b>验收</b>:邀请码生成幂等(二次 ensure 不变);同用户/同 telegram_id 重复领取皆命中 <code>ErrClaimExists</code>。</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="task-head"><span class="task-num">4</span><h3><code>reward.Service</code> — 邀请码生成、注册段发奖 + 防刷</h3></div>
|
||
<p>建 <code>Service</code>(依赖 <code>Store</code> + <code>Granter</code> 接口,由 <code>*codes.Service</code> 满足)。核心 <code>OnRegister</code>:best-effort(自有事务,失败只 log、不回滚注册)解析邀请码 → 自邀请/设备复用/月度封顶三道防刷判定 → 建 <code>referrals</code> → 未被拒则双方各发注册段天数。</p>
|
||
<div class="files"><b>新建</b> server/internal/reward/{service.go, service_sqlite_test.go}</div>
|
||
<p><b>关键方法</b>:<code>GenInviteCode()</code>(8 位 base32,去 0/O/1/I)· <code>EnsureCode</code> · <code>OnRegister(ctx, inviteeID, inviteCode, deviceUUID)</code></p>
|
||
<ul class="accept">
|
||
<li><b>验收</b>:正常注册双方各得 <code>RegDays</code>;自邀请不建关系;复用他人设备时 status=<code>rejected</code> 且不发奖。</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="task-head"><span class="task-num">5</span><h3>接入注册 — <code>auth.Register</code> 加 inviteCode</h3></div>
|
||
<p><code>auth.Service.Register</code> 签名加 <code>inviteCode</code> 参数;新增 <code>ReferralHook</code> 接口 + <code>SetReferralHook</code> setter,注册成功(<code>recordLogin</code> 之后)best-effort 调用钩子。<code>handler.go</code> 的 <code>registerRequest</code> 加 <code>invite_code</code> 字段并透传。</p>
|
||
<div class="files"><b>改</b> server/internal/auth/{service.go, handler.go} · <b>新建</b> register_invite_test.go</div>
|
||
<ul class="accept">
|
||
<li><b>验收</b>:注册后 hook 收到正确 <code>inviteeID/code/deviceID</code>;全库搜 <code>.Register(</code> 更新所有调用点后包内测试全绿。</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="task-head"><span class="task-num">6</span><h3>首充钩子 — pay webhook 接首充段发奖</h3></div>
|
||
<p><code>reward.Service</code> 加 <code>OnFirstPaidTx</code>(同事务内调用):先 <code>MarkFirstPaidTx</code> 判是否真首充,是则查 <code>ReferralByInvitee</code>,未发过/未拒则双方各发首充段天数并置 <code>paid_rewarded</code>。<code>pay/webhook.go</code> 加 <code>Rewarder</code> 接口 + <code>SetRewarder</code>,在 <code>GrantPaidSubscriptionTx</code> 之后、<code>tx.Commit()</code> 之前调用 —— 失败则整笔回滚,靠 webhook 重试保证最终发放。</p>
|
||
<div class="files"><b>改</b> server/internal/reward/service.go(追加)、server/internal/pay/webhook.go · <b>新建</b> webhook_referral_sqlite_test.go</div>
|
||
<ul class="accept">
|
||
<li><b>验收</b>:首充双方各得 <code>PaidDays</code>、<code>referrals.status→paid_rewarded</code>、<code>users.first_paid_at</code> 落值;二次付费不再重复发(幂等)。</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="task-head"><span class="task-num">7</span><h3><code>GET /v1/invite</code> 端点</h3></div>
|
||
<p>新建 <code>reward.Handler</code>:惰性拿邀请码 + 邀请战绩汇总(已邀请/已转化/累计获赠天数)+ TG 任务态(enabled/joined/channel)一次性打包返回。</p>
|
||
<div class="files"><b>新建</b> server/internal/reward/handler.go(GetInvite 部分)、handler_invite_test.go</div>
|
||
<p><b>响应</b>:<code>{invite_code, invite_link, invited, converted, earned_days, telegram:{enabled, joined, channel}}</code></p>
|
||
<ul class="accept">
|
||
<li><b>验收</b>:返回 200,<code>invite_code</code> 非空,<code>telegram.enabled</code> 与构造参数一致。</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="task-head"><span class="task-num">8</span><h3>TG 绑定 token — <code>GET /v1/tasks/telegram/start</code></h3></div>
|
||
<p>签发一次性 10 分钟 token 绑定当前账户(Redis <code>SETEX</code>,Redis 为 nil 时内存 map 兜底供测试)、消费即失效(<code>GETDEL</code>)。端点返回 bot 深链 <code>t.me/<bot>?start=<token></code>。</p>
|
||
<div class="files"><b>新建</b> server/internal/reward/telegram.go(token 部分)· <b>改</b> handler.go · <b>新建</b> telegram_token_test.go</div>
|
||
<ul class="accept">
|
||
<li><b>验收</b>:token 签发后可消费一次拿回正确 userID,二次消费返回 not-ok。</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="task-head"><span class="task-num">9</span><h3>TG webhook — getChatMember 真校验 + 发奖</h3></div>
|
||
<p><code>ChatMemberChecker</code> 接口(默认实现打 Telegram <code>getChatMember</code> API,测试可注入 fake)。<code>ClaimTelegram</code>:真是成员 → <code>InsertClaimTx</code>(唯一守卫)+ <code>GrantRewardTx(source='task')</code>。<code>Handler.TelegramWebhook</code>:校验 <code>X-Telegram-Bot-Api-Secret-Token</code> → 解析 <code>/start <token></code> → 消费 token → 领取 → <code>sendMessage</code> 回执(成功/已领/非成员话术不同)。</p>
|
||
<div class="files"><b>改</b> server/internal/reward/{telegram.go, handler.go} · <b>新建</b> telegram_webhook_test.go</div>
|
||
<ul class="accept">
|
||
<li><b>验收</b>:成员一次性发 <code>TgDays</code>,二次领取不再发;非成员不发;全包 <code>go test ./internal/reward/...</code> 绿。</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="task-head"><span class="task-num">10</span><h3>装配 main.go — 构造 reward svc + 注入 + 路由 + env</h3></div>
|
||
<p>纯装配任务,无独立单测(逻辑单测已在各包)。读 <code>INVITE_REG_MONTHLY_CAP</code>/<code>TG_REWARD_BOT_TOKEN</code>/<code>TG_REWARD_BOT_USER</code>/<code>TG_REWARD_CHANNEL</code>/<code>TG_WEBHOOK_SECRET</code> 构造 <code>rewardSvc</code>;注入 <code>authSvc.SetReferralHook</code> + <code>payWebhook.SetRewarder</code>;挂路由 <code>protected: GET /invite, GET /tasks/telegram/start</code>、<code>public v1: POST /tg/webhook</code>。</p>
|
||
<div class="files"><b>改</b> server/cmd/server/main.go</div>
|
||
<ul class="accept">
|
||
<li><b>验收</b>:<code>go build ./... && go vet ./cmd/server/</code> 无错;<code>go test ./...</code> 全绿。</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<h2>客户端(Task 11–13)</h2>
|
||
|
||
<div class="card root">
|
||
<div class="task-head"><span class="task-num">11</span><h3>客户端 invite api + provider</h3></div>
|
||
<p>新建 <code>InviteApi</code>(<code>fetch()</code> 拉 <code>GET /v1/invite</code>、<code>telegramStartLink()</code> 拉深链)+ <code>InviteInfo</code> 数据类;Riverpod <code>InviteNotifier</code>(未登录返回 null、不打网络)。<code>auth_api.dart</code> 的 <code>register</code> 加可选 <code>inviteCode</code> 参并入请求体。</p>
|
||
<div class="files"><b>新建</b> client/lib/services/invite_api.dart · state/invite_provider.dart · <b>改</b> services/auth_api.dart</div>
|
||
<ul class="accept">
|
||
<li><b>验收</b>:<code>invite_api_test.dart</code> 用 <code>MockClient</code> 断言 <code>/v1/invite</code> 响应正确解析到 <code>InviteInfo</code> 各字段。</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="task-head"><span class="task-num">12</span><h3>客户端 invite_page 真实化 + l10n</h3></div>
|
||
<p>邀请页占位转真实:邀请区(真实码/链接 + 复制/分享 + 战绩三格)+ 任务区「更多得会员」(TG 任务卡:未完成显「加入频道」+「验证领取」两步,已完成显「已领 +3 天」置灰,bot 未配整卡隐藏)。先加 9 个 l10n getter(<code>app_text.dart</code> + 6 语言实现),再改页面引用。颜色一律走 token,禁硬编码。</p>
|
||
<div class="files"><b>改</b> client/lib/screens/invite_page.dart · l10n/app_text.dart + strings_{zh,en,es,ja,ko,ru}.dart</div>
|
||
<ul class="accept">
|
||
<li><b>验收</b>:<code>flutter test test/widget/invite_page_test.dart</code> 通过(注入 fake InviteInfo,断言邀请码文本 + TG 任务卡文案可见);<code>flutter analyze</code> 对改动文件无 issue。</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="task-head"><span class="task-num">13</span><h3>注册页加邀请码输入 + deep-link 预填(可选后置)</h3></div>
|
||
<p>Step 1–5(必做):<code>auth_screen.dart</code> 注册表单密码步下方加一个可选「邀请码(选填)」输入框(key <code>invite-code-field</code>),提交时传 <code>inviteCode</code> 给 <code>auth_api.register</code>。</p>
|
||
<p>Step 6(<span class="tag warn">可选后置</span>,MVP 不含):<code>app_links</code> 依赖 + 四端原生配置(Android intent-filter / iOS associated domains / macOS URL scheme),解析邀请链接自动预填并锁定注册页邀请码字段。单列为独立后续任务。</p>
|
||
<div class="files"><b>改</b> client/lib/widgets/auth_screen.dart · <b>新建</b> test/widget/auth_invite_field_test.dart</div>
|
||
<ul class="accept">
|
||
<li><b>验收</b>:widget 测试断言注册表单存在邀请码输入框;<code>flutter analyze</code> 对该文件无 issue。</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<h2>文档(Task 14)</h2>
|
||
|
||
<div class="card">
|
||
<div class="task-head"><span class="task-num">14</span><h3>计划 HTML 阅读版 + 索引登记</h3></div>
|
||
<p>即本文档:按项目「设计/计划双产物」规范,把 <code>docs/superpowers/plans/2026-07-12-invite-task-rewards.md</code>(执行真相源,含 checkbox)生成同内容 HTML 阅读版,并登记进 <code>docs/index.html</code>「实现计划 / Plans」分类、与设计文档互链。</p>
|
||
<div class="files"><b>新建</b> docs/invite-task-rewards-plan.html · <b>改</b> docs/index.html</div>
|
||
<ul class="accept">
|
||
<li><b>验收</b>:两文件登记完整、互链可点;<code>.md</code> 保持执行真相源不变(供 subagent-driven-development 继续驱动 Task 1–13)。</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<h2>验收(端到端)</h2>
|
||
<ul>
|
||
<li>后端:<code>cd server && go test ./...</code> 全绿(含 reward 包 + auth/pay 回归)。</li>
|
||
<li>客户端:<code>cd client && flutter analyze && flutter test</code> 全绿。</li>
|
||
<li>真机联调(需 TG 新基建就绪):A 注册拿邀请码 → B 用 A 的码注册 → A/B 各 +3;B 用人民币下一单付成 → A/B 各再 +7;B 在 App 点「加入频道」+「验证领取」→ bot 校验成员 → +3;重复领被拒。</li>
|
||
<li>防刷:自邀请无关系;同设备第二账号注册奖励被拒(status=rejected);同一 telegram_id 换账户领被拒。</li>
|
||
</ul>
|
||
|
||
<h2>不在本轮(YAGNI)</h2>
|
||
<ul>
|
||
<li><span class="tag bad">不做</span> 现金/提现、多级分销、退群回收、注册后补填邀请码。</li>
|
||
<li><span class="tag bad">不做</span> deep-link 自动预填(Task 13 Step 6 单列后续)。</li>
|
||
<li><span class="tag bad">不做</span> 通知集成(奖励事件接缝留给 Spec ③)。</li>
|
||
</ul>
|
||
|
||
<p class="small" style="margin-top:32px">本页为阅读版,不含逐步 TDD 代码细节;完整测试代码/实现片段见执行真相源 <code>docs/superpowers/plans/2026-07-12-invite-task-rewards.md</code>。</p>
|
||
|
||
</div>
|
||
</body>
|
||
</html>
|