Audlis 的创作者收益系统基于 Stripe Connect Express 实现。当买家购买使用商业授权(commercial license)设计的商品时,设计创作者获得分成(默认 70%),每月自动结算到创作者的 Stripe Connect 账户。
整体流程
买家下单 (Stripe Checkout)
│
▼ 订单付款成功
Stripe Webhook → 记录版税 (royalty_ledger, state='accrued')
│
▼ 14 天退款窗口期过后 (cron 每小时检查)
版税成熟 (state='accrued' → 'eligible')
│
▼ 每月 1 日 00:00 UTC (cron 自动触发)
月度结算 (runPayoutSweep)
│ ├─ 汇总每个创作者的 eligible 余额
│ ├─ 跳过 < $20 的账户
│ ├─ 跳过未完成 Stripe onboarding 的创作者
│ ├─ stripe.transfers.create → 转账到创作者 Connect 账户
│ └─ 发送邮件通知 (Resend)
│
▼
版税标记已付 (state='eligible' → 'paid')
│
▼ Stripe 自行处理
创作者银行账户到账 (通常 1-3 个工作日)用户指南
1. 设置收款(Stripe Connect Onboarding)
入口:登录 → Account → Earnings 标签页 → "Set up payouts" 按钮
步骤:
点击 "Set up payouts"
- 系统会在 Stripe 创建一个 Express 类型的 Connect 账户
- 你会被重定向到 Stripe 的托管 onboarding 页面(
connect.stripe.com/setup/e/...)
在 Stripe Express 页面完成信息填写
- 这就是 "Sign in to Express" 页面——Stripe 托管的身份验证和银行账户设置
- 你需要填写:邮箱、手机号、身份信息、银行账户
- Stripe(不是 Audlis)负责 KYC 验证——Audlis 看不到你填的敏感信息
完成后自动跳回 Audlis
- Stripe 将你重定向到
/account?onboarding=complete#earnings - EarningsCenter 自动轮询
/api/account/payouts/status获取最新状态 - 状态从
pending变为active(Stripe 验证通过后,通常几分钟到几天)
- Stripe 将你重定向到
如果 30 分钟内没填完
- Stripe 链接过期,你会被重定向到
/account?onboarding=refresh#earnings - EarningsCenter 自动生成新的 onboarding 链接并重新跳转
- Stripe 链接过期,你会被重定向到
2. 收款状态说明
| 状态 | 含义 | 前端显示 |
|---|---|---|
none | 未设置收款 | 显示 "Set up payouts" 按钮 |
pending | 已开始但未完成验证 | 显示 "Verification in progress" + "Continue setup" |
restricted | Stripe 需要更多信息 | 红色卡片 + 待办事项列表 |
active | 完全可用 | 绿色卡片 "Stripe Connect linked" |
3. 收益状态说明
每笔版税记录有 5 个状态:
| 状态 | 含义 | 颜色 |
|---|---|---|
accrued | 订单已付款,但在 14 天退款窗口期内 | 黄色 |
eligible | 退款窗口已过,等待月度结算 | 绿色 |
paid | 已包含在某次月度打款中 | 蓝色 |
refunded | 订单在打款前被退款,版税取消 | 灰色 |
void | 管理员作废(如欺诈订单) | 红色 |
4. Earnings 仪表板
访问 /account#earnings 可看到:
- 当前余额:
accrued + eligible的总和(即将到手的钱)- 拆分显示:Pending(退款窗口期内)+ Eligible(下次结算包含)
- 已付总额:所有
paid状态的版税 - 终身总收入:
accrued + eligible + paid - 已退款:被退款的版税总额
- 联邦传播(如果启用了联邦身份):粉丝数、被点赞数、被转发数
- Top 收益设计:收益最高的 5 个设计
- 最近活动:最近 50 条版税流水记录
开发者指南
技术架构
| 组件 | 文件 | 职责 |
|---|---|---|
| Onboarding | src/lib/stripe-connect.ts | 创建 Connect 账户 + 生成 onboarding 链接 |
| Onboarding API | src/pages/api/account/payouts/onboard.ts | POST 入口 |
| 状态检查 API | src/pages/api/account/payouts/status.ts | GET 当前 onboarding 状态 |
| 版税累计 | src/lib/royalty.ts | accrueRoyalty / getEarningsSummary |
| 月度结算 | src/lib/payouts.ts | runPayoutSweep |
| 邮件通知 | src/lib/payout-mailer.ts | sendPayoutNotification |
| Stripe Webhook | src/pages/api/webhooks/stripe.ts | 订单付款 → 累计版税;account.updated → 同步状态 |
| Cron Worker | cron-worker/index.ts | 每小时成熟版税;每月 1 日结算 |
| 前端 UI | src/components/EarningsCenter.tsx | 完整仪表板 |
| 返回/刷新页 | src/pages/account/payouts/{return,refresh}.astro | Stripe 重定向中转 |
数据库表
creator_payout_accounts — 创作者的 Stripe Connect 账户状态
| 列 | 类型 | 说明 |
|---|---|---|
user_id | TEXT PK | 创作者 user id |
stripe_account_id | TEXT | Stripe acct_xxx |
onboarding_state | TEXT | none / pending / active / restricted |
payout_method | TEXT | stripe_express |
last_synced_at | INTEGER | 上次从 Stripe 同步的时间 |
creator_payouts — 每次月度结算记录
| 列 | 类型 | 说明 |
|---|---|---|
id | TEXT PK | pay_xxx(用作 Stripe 幂等键) |
creator_user_id | TEXT | 创作者 |
period_start / period_end | INTEGER | 结算周期范围 |
gross_cents | INTEGER | 结算总额(分) |
net_cents | INTEGER | 实际转账金额(= gross,手续费当前为 0) |
stripe_transfer_id | TEXT | Stripe tr_xxx |
state | TEXT | pending / sent / failed |
royalty_ledger — 版税流水(每笔一行)
| 列 | 类型 | 说明 |
|---|---|---|
id | TEXT PK | roy_xxx |
creator_user_id | TEXT | 设计创作者(快照) |
design_id | TEXT | 设计 ID |
order_id / order_item_id | TEXT | 订单 + 行项(order_item_id UNIQUE 幂等) |
royalty_rate_bps | INTEGER | 版税率快照(7000 = 70%) |
gross_cents | INTEGER | 订单行小计(分) |
creator_cents | INTEGER | 创作者分成 = floor(gross * bps / 10000) |
platform_cents | INTEGER | 平台分成 = gross - creator |
state | TEXT | accrued / eligible / paid / refunded / void |
eligible_at | INTEGER | 退款窗口到期时间(accrued→eligible) |
paid_at / payout_id | INTEGER / TEXT | 付款时间和关联的 payout 记录 |
配置项(app_config 表)
| Key | 默认值 | 说明 |
|---|---|---|
royalty_creator_bps_default | 7000 | 创作者默认分成比例(7000 bps = 70%) |
royalty_refund_window_days | 14 | 退款窗口天数(版税在此期间保持 accrued) |
royalty_min_payout_cents | 2000 | 最低结算金额(2000 分 = $20) |
这些值可以在 admin SiteSettings 中修改,无需重新部署。
Stripe API 调用方式
Onboarding 路径使用原生 fetch()(不用 Stripe SDK):
stripe-connect.ts → stripeFetchRaw()
POST https://api.stripe.com/v1/accounts (创建 Connect 账户)
POST https://api.stripe.com/v1/account_links (生成 onboarding 链接)原因:Stripe Node SDK 的 HTTP 层在 Cloudflare Workers 中有兼容性问题(502 超时)。原生 fetch 避免了这个问题。
Webhook 和月度结算使用 Stripe SDK(import Stripe from 'stripe'),因为这些路径在 Workers 中正常工作。
幂等性保障
- 版税累计:
royalty_ledger的UNIQUE(order_item_id)约束防止重复累计 - 月度结算:
stripe.transfers.create使用idempotencyKey: payoutId,重试不会重复转账 - 结算后更新:
UPDATE ... WHERE state='eligible'确保已付行不会被重复包含
自购不产生版税
当 reuse.creator_user_id === buyerUserId 时,webhook 跳过版税累计——创作者购买自己的设计不产生分成。
退款处理
- 订单在打款前退款 →
reverseRoyaltiesForOrder将accrued/eligible行翻转为refunded - 订单在打款后退款 → 版税已
paid,不可逆转(平台承担损失) - 这就是 14 天退款窗口的设计目的:确保退款先于结算
管理员手动触发结算
POST /api/admin/payouts/run管理员可手动触发结算(测试或补救部分失败的 cron run)。
Cron 调度
| 任务 | 频率 | 说明 |
|---|---|---|
maturePendingRoyalties | 每小时 | accrued → eligible(退款窗口到期) |
runPayoutSweep | 每月 1 日 00:00 UTC | 汇总 + 转账 + 邮件通知 |
邮件通知
结算成功后通过 Resend 发送邮件:
- 发件人:
Audlis <noreply@shop.audlis.com> - 主题:
Audlis · $X.XX payout sent(英文)/Audlis · 已为你结算 $X.XX(中文) - 内容:结算金额、周期、Audlis Payout ID、Stripe Transfer ID
- 链接到
/account/earnings查看详情 - 语言根据用户的
user_locale_{userId}配置决定
