diff --git a/docs/index.html b/docs/index.html index 5955fc3..d658899 100644 --- a/docs/index.html +++ b/docs/index.html @@ -96,6 +96,11 @@

实现计划 / Plans

+ +
邀请奖励 + 奖励任务 实现计划(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。
+
docs/invite-task-rewards-plan.html · 真相源 docs/superpowers/plans/2026-07-12-invite-task-rewards.md
+
前端设计系统治理重构(ds-flow 全端)HTML
阅读版;执行真相源 docs/superpowers/plans/2026-07-07-frontend-ds-refactor.md(含 checkbox)。用 ds-flow 把 Flutter 五端 + 官网 + 用户中心收口到「设计单源·代码镜像·静态闸拦漂移·golden/fidelity 双级像素验收兜底」。非从零 bootstrap(已约 65% 达标):补原型三件套(atoms.css/icons.js/index.html 登记页)+ Web 共享原子层去重(各自实现+同源闸)+ 硬编码色/fidelity 闸 + 启用 pre-commit。6 阶段:CLAUDE.md → 原型单源 → Web token 同源 → Web 原子对齐 → Flutter golden 补齐 → 闸挂满。主题保持 light/dark。
diff --git a/docs/invite-task-rewards-plan.html b/docs/invite-task-rewards-plan.html new file mode 100644 index 0000000..d827f9c --- /dev/null +++ b/docs/invite-task-rewards-plan.html @@ -0,0 +1,255 @@ + + + + + +邀请奖励 + 奖励任务(加入 TG 频道) — 实现计划 + + + +
+← 文档索引 +

邀请奖励 + 奖励任务(加入 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

+ +
+ +