codex referral attribution mvp
本地来源:Knowledge/World/项目/文档/referral/codex-referral-attribution-mvp.md
邀请奖励 + 归因追踪(MVP)实施文档
目的:给任何接手的人一份“照着做就能改”的执行手册。 状态:仅写文档,不改代码。 适用仓库:
seedvr2.net
0. 一句话总览(小白版)
我们要做三件事: 1) 先把“用户从哪来”记下来(UTM 归因,P0)。 2) 再做“邀请注册有奖励”(P1,MVP重点)。 3) 最后再接“广告回传”(P2,只先放代码框架)。
0.5 最终确认清单(超简版)
- 邀请 Cookie:30 天
- 首单 10%:仅邀请人
- 10% 取整:
Math.floor()向下取整 - 注册基础奖励:读
INITIAL_CREDITS_AMOUNT(只读) - 奖励上限:单次 1000
- 邀请码:
u_+ 8位哈希,注册自动生成
1. 当前项目现状(对照用)
以下是本项目已经有的“底子”,方便定位修改点:
- 认证:Better Auth,用户创建后会自动发注册赠送积分。
位置:
src/lib/auth.ts(databaseHooks.user.create.after→addRegisterGiftCredits) - 积分系统:统一用
addCredits,支持幂等(同 paymentId + type 不重复发)。 位置:src/credits/credits.ts、src/credits/types.ts - 支付:Stripe + Creem 双通道。
- Stripe:
src/payment/provider/stripe.ts - Creem:
src/payment/provider/creem-utils.ts - 路由结构:App Router + i18n
- Settings 页面:
src/app/(default)/(protected)/settings/* - 多语言 Settings 页面:
src/app/[locale]/(protected)/settings/* - Middleware:处理 i18n、CORS、登录跳转、canonical host。
位置:
src/middleware.ts - 环境变量:有统一校验
位置:
src/lib/env-validation.ts,示例:.env.example
2. 需求规则(最终确认版)
2.1 已确认规则(执行口径)
- 邀请奖励目的:用户增长 + 福利
- 层级:单层(A 邀请 B)
- 注册奖励:双方各得 “注册赠送积分 × 1(额外)”
- 基数读取
INITIAL_CREDITS_AMOUNT(只读配置) - 被邀请人总计 = 2 × 注册赠送积分
- 下单奖励:首单额外 10%(按积分,仅邀请人获得)
- 订阅续费不算
- 触发条件:邮箱验证 + 首单支付后才发奖励
- 邀请码 Cookie 保留:30 天
- 归因 Cookie:
attr_first(1 年)、attr_last(30 分钟) - 邀请码入口:
?invite=CODE - 邀请页面:
/settings/referral(已登录可复制链接) - 可裂变:被邀请人也可以再去邀请别人
- MVP:先不做二维码,不做 visitors 表
- 防刷:要做 IP / 设备限制
- 地理定位:Cloudflare Headers(零成本、零延迟)
2.2 已确认规则(最终版 2025-01-09)
✅ 以下规则已最终确认,不再是"待确认"状态
1) 下单奖励给谁
- ✅ 仅邀请人(被邀请人无首单奖励)
2) 奖励上限
- ✅ 单次上限 1,000 积分
3) 10% 取整规则
- ✅ 用 Math.floor() 向下取整
4) 注册赠送积分的基准
- ✅ 读取 INITIAL_CREDITS_AMOUNT(只读配置)
- 邀请奖励 = 双方各得 +基础奖励(等量积分)
5) 邀请码格式
- ✅ u_ + 8位小写字母数字哈希
- 示例:u_a1b2c3d4
6) 邀请码生成时机
- ✅ 注册账号时自动生成
7) 邀请 Cookie 有效期
- ✅ 30 天
3. 模块优先级(按你要求)
0) UTM 归因(先做) 1) 邀请奖励(MVP 核心) 2) 广告回传(后做,只先搭框架)
4. 业务流程(小白话流程图)
4.1 用户访问(归因)
- 用户打开带
?utm_xxx或?invite=CODE的链接 - Middleware 把 UTM 写进
attr_first / attr_last - 邀请码写进
invite_codeCookie(30 天)
4.2 注册(触发邀请关系)
- 用户注册成功 → 建立
referral关系 - 但奖励先不发,等 “邮箱验证 + 首单支付”
4.3 首单支付(发奖励)
- 支付成功(Stripe/Creem)
- 判断是不是首单
- 发放奖励: - 注册奖励:双方各得 +基础奖励(等量积分) - 首单奖励:仅邀请人 获得订单积分 × 10%(上限 1000)
5. 数据结构设计(MVP 方案 B)
5.1 新建 referral 表(推荐)
你给的方案,我保留并补充建议字段
export const referral = pgTable('referral', {
id: text('id').primaryKey(),
inviterId: text('inviter_id').notNull().references(() => user.id), // 邀请人
inviteeId: text('invitee_id').notNull().references(() => user.id), // 被邀请人
inviterReward: integer('inviter_reward').default(0), // 邀请人已获积分
inviteeReward: integer('invitee_reward').default(0), // 被邀请人已获积分
status: text('status').default('pending'), // pending, verified, rewarded
createdAt: timestamp('created_at').defaultNow().notNull(),
});
建议补充字段(可选)
- inviteCode:方便追溯
- firstPaidAt / firstPaymentId:首单判定
- rewardedAt:是否发过奖励
- meta:JSON 记录 IP/UA/utm
5.2 user 表新增字段
建议新增:
- inviteCode:用户自己的邀请码(唯一)
- invitedBy:邀请人 userId(可选)
- invitedAt:被邀请时间(可选)
6. 邀请码生成策略(已确认)
最终确认:u_ + 8位哈希(小写字母数字),注册时自动生成并写入 user.inviteCode。
7. Cookie & 归因规则(按你指定)
| 名称 | 说明 | 生命周期 |
|---|---|---|
attr_first |
首次触点 UTM | 1 年 |
attr_last |
最近触点 UTM | 30 分钟 |
invite_code |
邀请码 | 30 天 |
注意:Cookie 内只存必要字段,避免超过 4KB。
8. 文件修改清单(必须明确到文件)
8.1 数据库
src/db/schema.ts- 新增
referral表 - 给
user表加referralCode、referredById(或referredByCode)
8.2 Middleware(归因 + 邀请)
src/middleware.ts- 捕获 UTM、Click ID,写
attr_first / attr_last - 捕获
?invite=,写invite_codeCookie - 排除
/api避免递归
8.3 邀请页
只保留一个入口(规则页 + 登录后邀请页合并):
- src/app/[locale]/(app)/settings/referral/page.tsx
- 登录态:展示邀请链接 + 统计
- 未登录:只展示规则说明(不做 /invite/[code])
8.4 设置导航 + 路由枚举
src/routes.ts(加SettingsReferral)src/components/settings/settings-tab-nav.tsx(加新 Tab)
8.5 注册逻辑(绑定邀请)
src/lib/auth.ts- 用户创建后写入 referral 关系
- 奖励不要立刻发
8.6 支付成功后发奖励
目标:只在“邮箱已验证 + 首单支付成功”时发奖励,避免重复。
Stripe(主要入口)
- 文件:src/payment/provider/stripe.ts
- 触发点:
1) onCheckoutSessionCompleted(一次性付费 / 购买积分包)
2) onInvoicePaid(订阅首期支付)
- 关键做法(大白话):
- 先判断 user.emailVerified,没验证就“先记账不发”,等验证再补发
- 判断是不是首单:查 payment 里已付订单数量(只认 paid = true)
- 订阅续费不算:只有第一次付费才发
- 统一走一个“奖励发放函数”,保证幂等
Creem(补齐双通道)
- 文件:src/payment/provider/creem-utils.ts
- 触发点:Creem 的 paid/checkout 完成回调
- 做法同 Stripe:先判首单,再发奖励
推荐新增统一入口(避免逻辑散落)
- 新文件:src/referral/reward.ts
- 里面提供函数:
- grantReferralRewardsForPayment({ userId, paymentId, orderCredits, provider })
- 这个函数负责:查 referral 关系、首单判断、幂等判断、发积分
奖励计算(已确认规则)
- 注册奖励:双方各得 +基础奖励(等量积分)
- 基数读取 INITIAL_CREDITS_AMOUNT(只读配置)
- 过期天数:30 天
- 首单奖励:仅邀请人 获得 Math.floor(订单积分 × 10%)
- 订单积分 = 这次实际发给用户的积分
- 只算首单,订阅续费不算
- 使用 Math.floor() 向下取整
- 单次上限:1,000 积分
幂等(防止重复发)
- 依赖现有 credit_transaction 唯一索引:(paymentId + type)
- 每种奖励都用不同 type
- 例如:REFERRAL_INVITER_REGISTER, REFERRAL_INVITEE_REGISTER, REFERRAL_ORDER_BONUS
9. UTM 归因实现(P0)
目标:先把“来源”记下来,不需要建表。
9.1 Middleware 规则
- 写入 Cookie:
attr_first/attr_last attr_first只写一次,attr_last有新来源就更新- 生命周期:
attr_first = 1 年,attr_last = 30 分钟 - 避免递归:
matcher排除/api
9.2 Cookie 内容(小而干净)
- 只放关键字段,避免 4KB 超限:
utm_source / utm_medium / utm_campaigngclid / fbclidts
示例结构(建议):
{ s, m, c, g, f, t }
9.3 Cloudflare Headers(地理信息)
- 使用
cf-ipcountry / cf-ipcity / cf-connecting-ip - 注意:不要直接塞进 Cookie,最多存 DB 或日志
10. 广告回传(P2,只搭框架)
你要求:先加代码框架,不上线
计划新增:
- src/app/api/conversions/meta/route.ts
- src/app/api/conversions/google/route.ts
配套开关:
- CONVERSION_CONSENT_REQUIRED=false(默认不强制 consent)
说明: - 现在只加“空壳 + 日志”,不实际回传。
11. API 设计(邀请模块)
建议最少三条:
1) GET /api/user/referral-code
- 获取或生成邀请码
2) GET /api/user/referral-stats
- 邀请人数、已获积分
3) POST /api/user/referral-preview(可选)
- 根据 invite code 预览邀请人信息
12. 环境变量 & 配置项
MVP 直接用环境变量;注册基础奖励读取
INITIAL_CREDITS_AMOUNT(只读)。
12.1 邀请奖励相关(建议放 .env.example)
REFERRAL_ENABLED=true
REFERRAL_PURCHASE_RATE=0.10
REFERRAL_MAX_REWARD=1000
REFERRAL_REWARD_EXPIRE_DAYS=30
REFERRAL_CODE_PREFIX=u_
REFERRAL_CODE_LENGTH=8
REFERRAL_COOKIE_NAME=invite_code
REFERRAL_COOKIE_MAX_AGE=2592000
12.2 广告回传必需(先加到 .env.example)
META_PIXEL_ID=
META_ACCESS_TOKEN=
GOOGLE_ADS_CUSTOMER_ID=
GOOGLE_ADS_DEVELOPER_TOKEN=
GOOGLE_ADS_ACCESS_TOKEN=
GOOGLE_CONVERSION_ACTION_ID=
CONVERSION_CONSENT_REQUIRED=false
12.3 记得同步
.env.examplesrc/lib/env-validation.tssource.config.ts(如你们用它做 Cloudflare 绑定管理)
13. 防刷方案(MVP 必做)
目标:防止一人刷一堆小号。
推荐做法:
1) IP 限流
- 用现成的 src/lib/rate-limit.ts
- 参考:docs/program/MVP阶段速率限制方案.md
2) 设备限制
- 同设备 24h 只允许 X 个注册
3) 支付门槛
- 必须邮箱验证 + 首单支付才发奖励
4) 黑名单
- IP/邮箱/支付方式黑名单(可后补)
14. TODO 清单(大模块 + 小细节)
P0:UTM 归因
- middleware 写
attr_first/attr_last - Cookie 结构瘦身
/api排除递归
P1:邀请奖励
- 新建
referral表 user表加referralCode/referredById- 生成邀请码(u_ + 8位哈希,注册时自动生成)
- 邀请页面
/settings/referral - 注册时写 referral 关系
- 首单支付发奖励
- 防刷限流
P2:广告回传(框架)
- 新增 Meta/Google API 端点
- 增加 env + 开关
- 支付 metadata 里准备 gclid/fbclid
15. 逻辑 Bug 避免清单
- 不要在 middleware 写 DB
- 奖励发放必须幂等(paymentId + type)
- 订阅只算首单,不算续费
- Cookie 不要超过 4KB
- 未验证邮箱不能发奖励
16. 测试清单(最小必测)
?utm_source=xxx能写attr_first/attr_last?invite=CODE能写invite_codeCookie- 注册后 referral 关系写入
- 首单支付后发奖励
- 订阅续费不发奖励
- Creem 支付也能触发奖励
17. 已确认清单(2025-01-09 最终版)
✅ 所有待确认项已全部确认,可以开始实施
| 序号 | 配置项 | 最终确认值 |
|---|---|---|
| 1 | 下单奖励给谁 | 仅邀请人 |
| 2 | 奖励上限 | 单次 1,000 积分 |
| 3 | 10% 取整规则 | Math.floor() 向下取整 |
| 4 | 注册赠送基准 | 读取 INITIAL_CREDITS_AMOUNT(只读) |
| 5 | 邀请码格式 | u_ + 8位哈希 |
| 6 | 邀请码生成 | 注册时自动生成 |
| 7 | Cookie 有效期 | 30 天 |
18. 结论(小白版)
先把 UTM 记下来,再做邀请奖励,最后再上广告回传。
所有规则已确认完毕,可以按本文档开始实施代码。
附录:奖励规则速查表
┌─────────────────────────────────────────────────────────────┐
│ 邀请奖励规则速查表 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 📝 注册奖励(邮箱验证后触发) │
│ ├── 邀请人 A:+基础奖励 │
│ └── 被邀请人 B:+基础奖励(额外,总计 2×基础奖励) │
│ │
│ 💰 首单奖励(首次付费后触发) │
│ ├── 邀请人 A:订单积分 × 10%(上限1000) │
│ └── 被邀请人 B:无 │
│ │
│ ⚙️ 计算规则 │
│ ├── 取整:Math.floor() │
│ ├── 上限:单次 1,000 积分 │
│ └── 过期:30 天(跟注册赠送一致) │
│ │
│ 🎫 邀请码 │
│ ├── 格式:u_ + 8位哈希(如 u_a1b2c3d4) │
│ ├── 生成:注册时自动 │
│ └── Cookie:30 天 │
│ │
└─────────────────────────────────────────────────────────────┘
本文档为站内渲染。原始文件本地路径:saas/source/knowledge-world/Knowledge-World-项目-文档-referral-codex-referral-attribution-mv-c5fb00.md(仅本地保留,不入库不部署)