支付系统集成 - PayPal 迁移到 Lemon Squeezy
背景
PayPal 沙箱环境对中国用户极度不友好:
- 登录后自动跳转 paypal.cn,沙箱支付流程无法走通
- 沙箱账户不能用卖家账户测试付款
- Client ID/Secret 认证失败(401 invalid_client)
决定切换到 Lemon Squeezy。
为什么选 Lemon Squeezy
- 个人开发者可用,不需要公司主体
- MoR(Merchant of Record)模式,自动处理全球税务
- 支持信用卡 + PayPal
- 自带订阅管理、优惠券、License Key
- 集成简单 — 一个 Checkout URL 搞定支付
完成的工作
1. Lemon Squeezy 账号配置
- 注册并创建 Store(ID: 331780)
- Store 货币从 CNY 改为 USD
- API Key 保存到
config/lemonsqueezy.json - 创建 3 个产品(Test Mode):
- AI Humanizer Basic $6/月(Variant 1469206)— 20,000 词/月
- AI Humanizer Pro $15/月(Variant 1469239)— 100,000 词/月
- AI Humanizer Lifetime $49 一次性(Variant 1469240)— 500,000 词
2. 代码改动
- 新增
lib/lemonsqueezy.ts— LS API 封装(createCheckout) - 新增
app/api/checkout/route.ts— 创建 checkout 并返回跳转 URL - 新增
app/api/webhook/route.ts— 接收 LS 支付通知,更新 D1 用户 credits - 新增
components/ui/checkout-button.tsx— 替代 PayPalButton - 更新
pricing-section.tsx— 已购套餐判断逻辑 + 字数计费 UI - 更新
humanize/route.ts— 按输入字数扣 credits(1 credit = 1 word) - 更新
db.ts— 注册免费额度从 5 次 → 3,000 词 - 更新
dashboard/page.tsx— "Credits" → "Words Remaining",千位分隔符 - 更新
humanize/page.tsx— 注册引导文案改为 3,000 free words - 简化
success/page.tsx— LS 处理支付,webhook 自动加 credits - 支付页面强制
locale: "en"避免自动跳其他国家语言
3. 支付流程
用户点击按钮 → 检查登录(未登录跳 Google 登录,callbackUrl 回 pricing)→
调 /api/checkout → LS API 创建 checkout → 返回 checkout URL →
浏览器跳转 LS 支付页(英文界面)→ 用户付款 →
LS 发 webhook → /api/webhook 更新 D1(plan + credits)→
LS 跳转 /success 页面
4. Webhook 事件处理(完整)
| 事件 | 处理 |
|---|---|
| order_created | 一次性付款成功 → 加 credits + 更新 plan |
| subscription_created | 首次订阅 → 加 credits + 更新 plan |
| subscription_payment_success | 每月续费 → 再加 credits(自动续额度) |
| subscription_payment_failed | 续费失败 → 记录日志,保留现有 credits |
| subscription_cancelled | 取消 → 记录日志,保留到过期 |
| subscription_expired | 过期 → 降级为 free |
| order_refunded | 退款 → 扣除对应 credits |
5. Pricing 页面逻辑
- 未登录 → 按钮正常显示,点击跳 Google 登录
- 当前 Pro → Basic 禁用(Downgrade),Pro 显示 Active,Lifetime 可买升级
- 当前 Lifetime → 全部禁用(Already Lifetime)
- 当前 Basic → Pro 可买升级,Lifetime 可买升级
- 当前套餐绿色高亮 + "Current Plan" 标签
竞品定价调研
调研了 Undetectable.ai、StealthWriter、QuillBot、Jasper 等产品:
- 行业共识:全部用月订阅 + 字数额度制,没有按量付费的
- Undetectable.ai: 19/月(20K词) → $31/月(35K词),年付 5 折
- 原因:用户心智明确、API 成本可控、订阅 LTV 远高于按量付费
成本核算
使用 GLM-4-Plus 模型(主模型):
- 系统提示 ~2,500 tokens + 输入输出按字数计
- 每千词成本:¥0.0015($0.0002)
- 毛利率 99%+,LLM 成本极低
- 原因:GLM-4-Plus 定价极低,如果切 Gemini Flash(免费额度大)成本趋近于零
最终定价方案
采用高性价比方案,额度远超市面竞品:
| 方案 | 价格 | 额度 | 竞品对比 | |---|---|---|---| | Basic | 9.99/10K词 | | Pro | 19/20K词 | | Lifetime | 0 | 3,000 词(注册送) | — |
定价策略
- 低价获客:额度比竞品大 3-5 倍,价格低 30-50%
- 早期冲用户量,积累口碑后可涨价
- 老用户锁定低价(订阅不涨价惯例)
- 不做无限额度(Lifetime 限 50 万词,防滥用)
- 按量扣费:1 credit = 1 word,用多少扣多少
测试结果
- ✅ Test Mode 支付成功(测试卡 4242 4242 4242 4242)
- ✅ Webhook 已配置(ID: 86412),8 个事件全部注册
- ✅ 首次购买后 plan 从 free 更新为 pro,credits +500
- ✅ Pricing 页面已购套餐逻辑正常
- ✅ 支付页面强制英文,不再跳其他国家语言
- ✅ 字数计费模式已部署
待办
- [ ] Webhook 签名验证(目前只检查 header 存在)
- [ ] 切正式环境时生成 Live API Key
- [ ] 清理 PayPal 相关代码和文件(
lib/paypal.ts、app/api/paypal/*、components/ui/paypal-button.tsx) - [ ] Lifetime variant 的 interval 从 year 改为 one-time(LS Dashboard 操作)
- [ ] 正式上线前 publish 三个产品(当前都是 pending)
- [ ] 新产品(934587/934612/934613)Variant ID 未关联,暂用旧 Variant
踩坑记录
PayPal
s.render is not a function原因:
paypal.Buttons是构造函数,不能直接当对象调render()。且未登录时只显示错误文字不跳转。 修复:改为重定向方式,并加了 signIn 跳转。
部署后不自测就通知用户
连续两次部署都有 bug,都是用户测出来的。 教训:部署后必须用 Playwright/curl 冒烟测试再通知。
CF Pages 环境变量 PATCH 是全量替换
之前踩过的坑又差点踩。secret 类型变量读不出来,无法全量 PATCH。 方案:LS API Key 直接硬编码在代码里(test key 无所谓,live key 上线前改)。
LS Store 货币默认 CNY
中国区注册 LS 默认货币是人民币,需要在 Settings → Store 里手动改为 USD。
LS API 不支持创建产品和变体
产品和变体只能在 Dashboard 手动创建,API 只能读取和创建 checkout/webhook。
LS Test Mode 和 Live Mode 隔离
Test Mode 创建的 API Key 看不到 Live Mode 的产品,反之亦然。
LS 价格单位是美分
API 返回的 price 字段是最小货币单位(cents),$6.00 = price: 600。price_string 可能为 None。
LS 修改产品即时生效
在 Dashboard 改价格、描述等不需要点发布,API 立即返回新值。但产品本身需要 publish 才能在 Live Mode 使用。