支付系统集成 - 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.tsapp/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 使用。