← 文档索引

邀请奖励 + 奖励任务(加入 TG 频道)

实现计划阅读版 · 2026-07-12 · Spec ② · 14 任务 TDD · 状态 待执行

配套设计文档 邀请奖励 + 奖励任务 — 设计。执行真相源(含 checkbox,驱动 subagent-driven-development/executing-plans):docs/superpowers/plans/2026-07-12-invite-task-rewards.md

Goal:用「送 Pro 会员天数」驱动增长 —— 邀请两段式(注册双方各 +3、被邀请人首充双方各 +7)+ 奖励任务(加入 TG 频道 +3,Bot 真校验),全部复用现有 subscriptions 发天数链路,不造新轮子。

Architecture:新增 server/internal/reward 包承载发奖 + 防刷;发天数复用 codes.Service(新增 GrantRewardTx)。注册段在 auth.Service.Register 后 best-effort 触发(自有 tx,不拖累注册主流程);首充段挂 pay/webhook.settle 同事务内(幂等、失败即整笔回滚重试);TG 走 POST /tg/webhook(Telegram getChatMember 真校验)。客户端 invite_page 由占位转真实(邀请区 + 任务区),注册页加邀请码输入。

Tech Stack:Go(chi + 裸 SQL + golang-migrate 双 DB)、Redis(TG 绑定 token)、Flutter/Riverpod、Telegram Bot API。

Global Constraints

File Structure(总览)

# 后端(新建)
server/migrations/{mysql,sqlite}/000024_invite_rewards.{up,down}.sql
server/internal/reward/{store,service,telegram,handler}.go
# 后端(修改)
server/internal/codes/paygrant.go        # + GrantRewardTx
server/internal/auth/{service,handler}.go # Register 加 inviteCode + ReferralHook
server/internal/pay/webhook.go            # settle 首充钩子 + Rewarder
server/cmd/server/main.go                 # 装配 + 路由 + env
# 客户端(新建/修改)
client/lib/services/invite_api.dart · state/invite_provider.dart
client/lib/screens/invite_page.dart       # 占位转真实
client/lib/widgets/auth_screen.dart + services/auth_api.dart # 邀请码输入
client/lib/l10n/app_text.dart + strings_*.dart ×6

后端(Task 1–10)

1

迁移 000024 — 新表 + source 扩容

referrals(邀请关系,一对一绑定)、reward_claims(通用一次性任务领取)两张表;usersinvite_code/first_paid_at;subscriptions.source CHECK/ENUM 扩容到含 invite/task(SQLite 需重建表,MySQL 直接 MODIFY)。sqlite/mysql 各一对 up/down,四个文件。

新建 server/migrations/{sqlite,mysql}/000024_invite_rewards.{up,down}.sql
2

codes.Service.GrantRewardTx — 发奖天数原语

codes 包加一个薄封装:复用既有 applySubscription(max(到期,now)+days 顺延语义)发 Pro 天数,source∈{invite,task},并写一条 audit_log。这是后续所有发奖调用的唯一入口。

server/internal/codes/paygrant.go · 新建 reward_grant_sqlite_test.go

接口:GrantRewardTx(ctx, tx, userID, days, source, auditAction, ref) (subID, expiresAt, err)

3

reward.Store — 数据访问层

新建 reward 包的纯数据访问层:邀请码惰性生成/解析、设备复用查询、月度计数、referrals/reward_claims 的 CRUD,唯一冲突统一映射为 ErrClaimExists

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

关键方法:EnsureInviteCode · ResolveInviteCode · DeviceUsedByOther · RegRewardCountThisMonth · InsertReferralTx · MarkFirstPaidTx · ReferralByInvitee · MarkPaidRewardedTx · InsertClaimTx · Summary · TelegramClaimed

4

reward.Service — 邀请码生成、注册段发奖 + 防刷

Service(依赖 Store + Granter 接口,由 *codes.Service 满足)。核心 OnRegister:best-effort(自有事务,失败只 log、不回滚注册)解析邀请码 → 自邀请/设备复用/月度封顶三道防刷判定 → 建 referrals → 未被拒则双方各发注册段天数。

新建 server/internal/reward/{service.go, service_sqlite_test.go}

关键方法:GenInviteCode()(8 位 base32,去 0/O/1/I)· EnsureCode · OnRegister(ctx, inviteeID, inviteCode, deviceUUID)

5

接入注册 — auth.Register 加 inviteCode

auth.Service.Register 签名加 inviteCode 参数;新增 ReferralHook 接口 + SetReferralHook setter,注册成功(recordLogin 之后)best-effort 调用钩子。handler.goregisterRequestinvite_code 字段并透传。

server/internal/auth/{service.go, handler.go} · 新建 register_invite_test.go
6

首充钩子 — pay webhook 接首充段发奖

reward.ServiceOnFirstPaidTx(同事务内调用):先 MarkFirstPaidTx 判是否真首充,是则查 ReferralByInvitee,未发过/未拒则双方各发首充段天数并置 paid_rewardedpay/webhook.goRewarder 接口 + SetRewarder,在 GrantPaidSubscriptionTx 之后、tx.Commit() 之前调用 —— 失败则整笔回滚,靠 webhook 重试保证最终发放。

server/internal/reward/service.go(追加)、server/internal/pay/webhook.go · 新建 webhook_referral_sqlite_test.go
7

GET /v1/invite 端点

新建 reward.Handler:惰性拿邀请码 + 邀请战绩汇总(已邀请/已转化/累计获赠天数)+ TG 任务态(enabled/joined/channel)一次性打包返回。

新建 server/internal/reward/handler.go(GetInvite 部分)、handler_invite_test.go

响应:{invite_code, invite_link, invited, converted, earned_days, telegram:{enabled, joined, channel}}

8

TG 绑定 token — GET /v1/tasks/telegram/start

签发一次性 10 分钟 token 绑定当前账户(Redis SETEX,Redis 为 nil 时内存 map 兜底供测试)、消费即失效(GETDEL)。端点返回 bot 深链 t.me/<bot>?start=<token>

新建 server/internal/reward/telegram.go(token 部分)· handler.go · 新建 telegram_token_test.go
9

TG webhook — getChatMember 真校验 + 发奖

ChatMemberChecker 接口(默认实现打 Telegram getChatMember API,测试可注入 fake)。ClaimTelegram:真是成员 → InsertClaimTx(唯一守卫)+ GrantRewardTx(source='task')Handler.TelegramWebhook:校验 X-Telegram-Bot-Api-Secret-Token → 解析 /start <token> → 消费 token → 领取 → sendMessage 回执(成功/已领/非成员话术不同)。

server/internal/reward/{telegram.go, handler.go} · 新建 telegram_webhook_test.go
10

装配 main.go — 构造 reward svc + 注入 + 路由 + env

纯装配任务,无独立单测(逻辑单测已在各包)。读 INVITE_REG_MONTHLY_CAP/TG_REWARD_BOT_TOKEN/TG_REWARD_BOT_USER/TG_REWARD_CHANNEL/TG_WEBHOOK_SECRET 构造 rewardSvc;注入 authSvc.SetReferralHook + payWebhook.SetRewarder;挂路由 protected: GET /invite, GET /tasks/telegram/startpublic v1: POST /tg/webhook

server/cmd/server/main.go

客户端(Task 11–13)

11

客户端 invite api + provider

新建 InviteApi(fetch()GET /v1/invitetelegramStartLink() 拉深链)+ InviteInfo 数据类;Riverpod InviteNotifier(未登录返回 null、不打网络)。auth_api.dartregister 加可选 inviteCode 参并入请求体。

新建 client/lib/services/invite_api.dart · state/invite_provider.dart · services/auth_api.dart
12

客户端 invite_page 真实化 + l10n

邀请页占位转真实:邀请区(真实码/链接 + 复制/分享 + 战绩三格)+ 任务区「更多得会员」(TG 任务卡:未完成显「加入频道」+「验证领取」两步,已完成显「已领 +3 天」置灰,bot 未配整卡隐藏)。先加 9 个 l10n getter(app_text.dart + 6 语言实现),再改页面引用。颜色一律走 token,禁硬编码。

client/lib/screens/invite_page.dart · l10n/app_text.dart + strings_{zh,en,es,ja,ko,ru}.dart
13

注册页加邀请码输入 + deep-link 预填(可选后置)

Step 1–5(必做):auth_screen.dart 注册表单密码步下方加一个可选「邀请码(选填)」输入框(key invite-code-field),提交时传 inviteCodeauth_api.register

Step 6(可选后置,MVP 不含):app_links 依赖 + 四端原生配置(Android intent-filter / iOS associated domains / macOS URL scheme),解析邀请链接自动预填并锁定注册页邀请码字段。单列为独立后续任务。

client/lib/widgets/auth_screen.dart · 新建 test/widget/auth_invite_field_test.dart

文档(Task 14)

14

计划 HTML 阅读版 + 索引登记

即本文档:按项目「设计/计划双产物」规范,把 docs/superpowers/plans/2026-07-12-invite-task-rewards.md(执行真相源,含 checkbox)生成同内容 HTML 阅读版,并登记进 docs/index.html「实现计划 / Plans」分类、与设计文档互链。

新建 docs/invite-task-rewards-plan.html · docs/index.html

验收(端到端)

不在本轮(YAGNI)

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