知识库首页 知识库-世界 codex-referral-attribution-mvp.md

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.tsdatabaseHooks.user.create.afteraddRegisterGiftCredits
  • 积分系统:统一用 addCredits,支持幂等(同 paymentId + type 不重复发)。 位置:src/credits/credits.tssrc/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 天
  • 归因 Cookieattr_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 用户访问(归因)

  1. 用户打开带 ?utm_xxx?invite=CODE 的链接
  2. Middleware 把 UTM 写进 attr_first / attr_last
  3. 邀请码写进 invite_code Cookie(30 天)

4.2 注册(触发邀请关系)

  1. 用户注册成功 → 建立 referral 关系
  2. 但奖励先不发,等 “邮箱验证 + 首单支付”

4.3 首单支付(发奖励)

  1. 支付成功(Stripe/Creem)
  2. 判断是不是首单
  3. 发放奖励: - 注册奖励:双方各得 +基础奖励(等量积分) - 首单奖励:仅邀请人 获得订单积分 × 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 表加 referralCodereferredById(或 referredByCode

8.2 Middleware(归因 + 邀请)

  • src/middleware.ts
  • 捕获 UTM、Click ID,写 attr_first / attr_last
  • 捕获 ?invite=,写 invite_code Cookie
  • 排除 /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_campaign
  • gclid / fbclid
  • ts

示例结构(建议):

{ 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.example
  • src/lib/env-validation.ts
  • source.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_code Cookie
  • 注册后 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(仅本地保留,不入库不部署)