增长:推荐、折扣码、生命周期邮件
Referral 双边奖励、Stripe Promotion Codes 深链折扣、D0/D3/D7 生命周期邮件。
三个各自独立、可单独关停的增长切片:referral(推荐有礼)、promo(折扣码)、
drip(生命周期邮件)。共同的设计取向:幂等优先——每一次发奖、每一封邮件都经过
唯一约束或 ref 去重,重放、重试、多触发点并发都不会重复发放。
Referral:两档双边 credits 奖励
每位用户在 /app/referrals 有一个专属推荐码,分享链接形如 /r/<code>。
归因(首触,30 天):/r/$code 落地时种下 ref cookie 并 302 到注册页(无效码
也照常跳转,不给探测面)。注册成功后,better-auth 的 user.create.after databaseHook
读 cookie 落库归因——首触优先(referred_user_id 唯一约束,后来的码插不进去),
自我推荐直接拒绝。
两档奖励,双边发放(金额在 referral.config.ts):
| 阶段 | 触发 | 每侧奖励 |
|---|---|---|
verified | 被推荐人完成邮箱验证 | 250 credits(即时正反馈) |
paid | 被推荐人首次付费(个人 Pro 或组织的座位订阅/充值) | 1,000 credits(防刷主奖) |
paid 蕴含 verified——直接付费的用户两档一起结。奖励通过
credits 的幂等流水入账,ref 形如
referral:{id}:{stage}:{side},所以结算函数 settleReferral 可以被任意触发点重复
调用:邮箱验证钩子、付费 webhook、加入组织钩子、每日 cron,每笔仍只发一次。
防刷上限:每个推荐人最多 50 个推荐计酬(按到达 verified 的数量计)。超限后停发的 只是推荐人侧,被推荐人自己的奖励照发——新用户不该为别人刷量买单。
credits 挂在组织上,而结算时对方可能还没有组织(例如刚注册未验证邮箱)。这种情况
settleReferral 直接跳过该侧,留给下一个触发点或每日 cron 的 settleRecent
补结(扫最近 90 天已达标的推荐,幂等重结,见 定时任务)。
事件驱动尽力而为,cron 兜底最终一致。
折扣码:Stripe Promotion Codes
码本身在 Stripe Dashboard 创建(coupon + promotion code),模板不重造那套 UI—— Stripe 是核销次数、有效期、启停状态的唯一事实源。模板补的是两件事:
- 所有 checkout 都收码:订阅、买断、充值的 Checkout Session 一律开
allow_promotion_codes,顾客在 Stripe 收银台手动输码。 - 深链自动应用:
/pricing?code=XXX会种 7 天的fs_promocookie,之后任何 checkout 创建时把 cookie 里的码解析成 promotion-code id、经discounts参数预先 应用——Stripe 收银台无法预填输码框,这是让分享链接"点开即生效"的唯一办法。 两个参数互斥:解析成功走discounts,任何一环失败(无 cookie、格式不对、码已停用、 Stripe 抖动)都回退到手动输码入口,过期的活动链接永远不能挡住付款。
admin 后台有一个只读核销视图(码、折扣、已用次数、上限、过期时间)。只读是有意的: 写操作留在 Stripe Dashboard,避免两边状态互相踩。
Drip:D0/D3/D7 生命周期邮件
旅程在 drip.config.ts 里声明式定义,每晚由 cron 执行一批:
| 天数 | campaign | 发送条件 |
|---|---|---|
| D0 | welcome | 无条件(欢迎邮件) |
| D3 | activate | 从未协作过——不属于任何 ≥2 人的组织 |
| D7 | upgrade | 无任何活跃付费——没有座位订阅所在组织,也没有个人 Pro/买断 |
加一封新邮件 = 在配置数组加一项 + 按 邮件 的约定补模板与双语文案。
Claim-before-send:发送前先插入 (userId, campaign) 唯一行当锁,插入成功才发信。
进程在发送中途崩溃,最坏丢一封邮件,绝不双发——营销邮件宁漏勿重。发送失败只记
日志不回滚 claim,同一封不会反复重试。
合规:每封 drip 邮件带 HMAC-SHA256 签名的一键退订链接(/unsubscribe?uid=…&token=…),
无需登录即可退订(CAN-SPAM 要求非鉴权退出)。退订置 marketingOptOut,此后所有
campaign 跳过该用户;交易类邮件(验证、收据等)不受影响,也不带退订链接。
其余护栏:
- 时效窗 14 天:campaign 只在
afterDays ≤ 用户注册天数 ≤ afterDays + 14内可发。 错过即作废——日后改条件或切换模式,不会突然给一年前的老用户群发 onboarding 邮件。 - 单次上限 100 封:保护 Resend 速率限制,积压的下一晚继续。
- 没配
RESEND_API_KEY就整体跳过:不能白插 claim 行把邮件静默烧掉。 - solo 模式(见 单用户模式):D3 直接跳过(没有 邀请就没有"激活协作"可言);D7 只按个人套餐判定,文案与链接换用 solo 变体。
本地验证
- Referral:登录拿到自己的码 → 无痕窗口走
/r/<code>注册 → 在pnpm dev控制台 点验证链接 → 两侧/app/credits各出现一笔 250 credits 的referral:*流水。 - 折扣码:Stripe 测试模式建一个 promotion code → 访问
/pricing?code=XXX→ 发起任意 checkout,收银台已带折扣。 - Drip:不用等 cron——选择/claim 路径在 workers 池测试里用假 sender 全覆盖,
照
drip.workers.test.ts的写法调runDrip即可。
三个切片都遵循优雅降级:没配 Stripe 就没有折扣码,没配 Resend 就没有 drip, referral 页始终可用(奖励结算等到 credits 可用时补上)。