Z Image Turbo 积分充值与 Stripe 后台配置指南

Z Image Turbo Team 2026年7月3日 约 8 分钟

Z Image Turbo 积分充值与 Stripe 后台配置指南

作者:Z Image Turbo Team | 发布时间:2026年7月3日 | 阅读时间:约 8 分钟

Z Image Turbo 现在使用一次性积分充值,不做月订阅。用户按需购买积分,生成图片时扣除积分。本文说明当前积分包规则、Stripe 后台如何创建产品和价格、Webhook 如何配置,以及上线前需要检查哪些环境变量和数据库迁移。

当前积分包

当前使用一个基础积分包,通过 Stripe Checkout 的数量倍数实现不同金额:

用户看到的价格 Stripe 数量 到账积分
$10 1 500 积分
$20 2 1,000 积分
$50 5 2,500 积分
$100 10 5,000 积分

当前每次图像生成消耗 1 积分。如果生成因为服务错误失败,系统会自动退回本次消耗的积分。

Stripe 后台命名规范

建议只创建一个一次性 Price,不要创建四个不同商品。当前代码使用同一个 Price ID,并通过 quantity=1,2,5,10 来得到不同充值档位。

推荐命名如下:

Stripe 对象 推荐名称
Product name Z Image Turbo Credits
Product description Credits for AI image generation on Z Image Turbo
Price nickname Credit Pack 500 - $10
Price type One-time
Unit amount $10.00 USD
Metadata app z-image-turbo
Metadata kind credit_pack
Metadata base_credits 500

注意:代码需要的是 price_... 开头的 Price ID,不是 prod_... 开头的 Product ID。

Stripe 后台创建 Product 和 Price

在 Stripe Dashboard 中按下面步骤操作:

  1. 进入 Product catalog
  2. 点击 Add product
  3. Product name 填:Z Image Turbo Credits
  4. Description 填:Credits for AI image generation on Z Image Turbo
  5. Pricing model 选择标准一次性价格。
  6. Price type 选择 One-time,不要选择 Recurring。
  7. Unit amount 填 $10.00,Currency 选择 USD
  8. Price nickname 填:Credit Pack 500 - $10
  9. 保存后复制 price_... 开头的 Price ID。
  10. 把这个 Price ID 填到环境变量 STRIPE_PRICE_CREDIT_PACK_500

当前代码会把这个 $10 Price 当成基础单元。用户买 $20 时,系统传给 Stripe 的 quantity 是 2;用户买 $50 时,quantity 是 5。

需要配置的环境变量

生产环境至少需要这些变量:

STRIPE_SECRET_KEY=sk_live_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
STRIPE_PRICE_CREDIT_PACK_500=price_xxx

CREDIT_PACK_BASE_CREDITS=500
CREDIT_PACK_BASE_USD_CENTS=1000
CREDIT_PACK_ALLOWED_QUANTITIES=1,2,5,10

含义如下:

变量 含义
STRIPE_SECRET_KEY Stripe Secret key,生产环境使用 sk_live_...
STRIPE_WEBHOOK_SECRET Webhook signing secret,通常是 whsec_...
STRIPE_PRICE_CREDIT_PACK_500 $10 / 500 积分的 Price ID
CREDIT_PACK_BASE_CREDITS 一个基础包包含多少积分
CREDIT_PACK_BASE_USD_CENTS 一个基础包价格,单位是美分,1000 表示 $10
CREDIT_PACK_ALLOWED_QUANTITIES 允许购买的数量倍数

推荐把 STRIPE_SECRET_KEYSTRIPE_WEBHOOK_SECRET 放到 Cloudflare secret,不要写进公开配置文件:

bunx wrangler secret put STRIPE_SECRET_KEY
bunx wrangler secret put STRIPE_WEBHOOK_SECRET

STRIPE_PRICE_CREDIT_PACK_500 可以放在 wrangler.jsoncvars 中,也可以按你的发布流程用环境变量注入。

Webhook 后台配置

在 Stripe Dashboard 中配置 Webhook:

  1. 进入 Developers
  2. 进入 Webhooks
  3. 点击 Add endpoint
  4. Endpoint URL 填:
https://z-image-turbo.com/api/stripe/webhook
  1. 当前一次性积分充值只选择下面这些事件:
checkout.session.completed
checkout.session.async_payment_succeeded
checkout.session.async_payment_failed
charge.refunded
refund.updated

当前项目没有月订阅逻辑,暂时不要配置 customer.subscription.deletedcustomer.subscription.updatedinvoice.payment_failedinvoice.payment_succeeded。这些事件属于订阅续费场景,当前代码不会消费它们。

  1. 创建后进入该 webhook 详情页。
  2. 复制 Signing secret,它通常是 whsec_...
  3. 把它配置为 STRIPE_WEBHOOK_SECRET

Webhook 是发放积分的关键入口。用户支付成功后,Stripe 会调用这个地址,系统验证签名后才会给用户加积分。

charge.refundedrefund.updated 用于同步在 Stripe 后台或 Z Image Turbo 后台发起的退款状态。积分扣回仍建议通过 Z Image Turbo 后台的退款确认表单执行,避免用户余额被自动扣成负数。

当前需要配置的事件

事件 当前用途 是否配置
checkout.session.completed 用户完成 Checkout 后发放积分 必须
checkout.session.async_payment_succeeded 异步支付最终成功后发放积分 建议保留
checkout.session.async_payment_failed 异步支付失败后标记订单失败 建议保留
charge.refunded Stripe 退款后同步订单退款状态 建议保留
refund.updated Stripe 退款状态变化后同步订单 建议保留

未来做订阅时再配置的事件

如果后续重新加入月订阅或自动续费积分,再增加下面这些事件,并同步新增订阅表、订阅状态同步、续费发积分和失败续费处理逻辑:

checkout.session.completed
customer.subscription.created
customer.subscription.updated
customer.subscription.deleted
invoice.payment_succeeded
invoice.payment_failed

如果一次性积分包和订阅同时存在,当前这组一次性支付事件仍然保留:

checkout.session.async_payment_succeeded
checkout.session.async_payment_failed
charge.refunded
refund.updated

使用 Stripe CLI 创建生产 Webhook

如果不想在 Stripe Dashboard 手动创建,也可以用 Stripe CLI 创建真实环境的 webhook endpoint。下面命令对应当前一次性积分充值事件。这里建议显式传入 sk_live_...,这样最不容易误建到测试环境:

stripe webhook_endpoints create \
  --api-key sk_live_xxx \
  --url="https://z-image-turbo.com/api/stripe/webhook" \
  -d "description=Z Image Turbo production webhook" \
  -d "enabled_events[0]=checkout.session.completed" \
  -d "enabled_events[1]=checkout.session.async_payment_succeeded" \
  -d "enabled_events[2]=checkout.session.async_payment_failed" \
  -d "enabled_events[3]=charge.refunded" \
  -d "enabled_events[4]=refund.updated"

如果本机已经通过 stripe login 登录到正确的 Stripe 账户,也可以使用 live 模式:

stripe webhook_endpoints create --live \
  --url="https://z-image-turbo.com/api/stripe/webhook" \
  -d "description=Z Image Turbo production webhook" \
  -d "enabled_events[0]=checkout.session.completed" \
  -d "enabled_events[1]=checkout.session.async_payment_succeeded" \
  -d "enabled_events[2]=checkout.session.async_payment_failed" \
  -d "enabled_events[3]=charge.refunded" \
  -d "enabled_events[4]=refund.updated"

需要注意:--live 只表示请求 live mode,不代表当前 CLI key 一定有足够权限。stripe login 生成的 CLI key 通常是 rk_live_... 这种 restricted key。如果这个 key 没有 webhook endpoint 写权限,Stripe 会返回类似下面的错误:

{
  "error": {
    "message": "The provided key 'rk_live_...' does not have the required permissions for this endpoint..."
  }
}

遇到这个错误时,不是项目代码问题,而是 Stripe API key 权限不足。最稳的处理方式是在 Stripe Dashboard 手动创建 webhook;如果仍想用 CLI,就改用有权限的 sk_live_...,或者在 Dashboard 中给对应 restricted key 增加 webhook endpoint 创建权限。

创建成功后,Stripe 返回结果里会包含 secret: whsec_...。这个值就是生产环境要使用的 STRIPE_WEBHOOK_SECRET,建议立刻写入 Cloudflare secret:

bunx wrangler secret put STRIPE_WEBHOOK_SECRET

注意不要把 stripe listen 当成生产 webhook 配置。stripe listen 主要用于本地开发转发,例如把 Stripe 测试事件转发到 http://127.0.0.1:5187/api/stripe/webhook。生产环境需要的是上面这种持久的 webhook endpoint。

如果命令中使用的是 sk_test_...,或者没有明确使用 live 模式,就会创建测试环境 webhook,真实付款不会打到这个 endpoint。创建后建议在 Stripe Dashboard 的 Developers → Webhooks 里确认该 endpoint 的模式是 live,URL 是 https://z-image-turbo.com/api/stripe/webhook,事件列表只包含当前需要的一次性积分充值事件。

参考官方文档:

Customer Portal 配置

当前账户页会在用户已有 Stripe Customer 后显示 Manage billing 入口。使用前需要在 Stripe Dashboard 中启用 Customer Portal:

  1. 进入 Settings
  2. 进入 Billing 下的 Customer portal
  3. 开启 invoice/payment history 相关能力。
  4. 保存配置。

当前项目只做一次性积分充值,不需要启用订阅管理能力。如果后续重新加入订阅,再在 Customer Portal 中打开 subscription update/cancel 相关配置。

支付到账逻辑

当前实现流程如下:

  1. 用户在账户页选择积分包。
  2. 前端请求 /api/billing/checkout,传入 quantity。
  3. 后端校验 quantity 是否在 1,2,5,10 之内。
  4. 后端创建 billing_order 订单,状态为 pending
  5. 后端创建 Stripe Checkout Session,使用 mode=payment
  6. 用户跳转到 Stripe 完成支付。
  7. Stripe 发送 webhook 到 /api/stripe/webhook
  8. 后端验证 Stripe 签名。
  9. 后端找到对应的 billing_order
  10. 后端给用户增加积分,并写入 credit_ledger
  11. 订单状态更新为 paid

防止重复到账

Stripe 可能会重试 webhook,所以发积分必须幂等。

当前系统使用 Stripe 支付引用作为积分流水引用:

ref_type = stripe_payment_intent
ref_id = pi_xxx

如果没有 PaymentIntent,则回退为:

ref_type = stripe_checkout_session
ref_id = cs_xxx

系统在发放积分前会检查 credit_ledger 中是否已经存在同一引用。即使 Stripe 重试 webhook,同一笔支付也不会重复到账。

数据库迁移

上线前需要在远程 D1 执行迁移,创建 billing_order 表:

bunx wrangler d1 migrations apply z-image-turbo-prod --remote

如果只在本地测试,可以执行:

bun run db:migrate:local

没有执行迁移时,支付订单无法写入数据库,Checkout 会失败。

本地测试建议

本地测试 Stripe 时建议使用 Stripe 测试模式:

  1. Stripe Dashboard 切到 test mode。
  2. 创建 test mode 下的 Product 和 Price。
  3. 使用 sk_test_... 和 test mode 的 price_...
  4. 使用 Stripe CLI 转发 webhook:
stripe listen --forward-to http://127.0.0.1:5187/api/stripe/webhook
  1. 把 CLI 输出的 whsec_... 填到本地 STRIPE_WEBHOOK_SECRET
  2. 使用 Stripe 测试卡完成支付:
4242 4242 4242 4242
任意未来有效期
任意 CVC

注意:本地如果需要 D1/R2 绑定,应使用 Cloudflare 本地开发方式或确保当前开发环境能访问对应绑定。

上线检查清单

上线前逐项确认:

  • Stripe Product 名称是 Z Image Turbo Credits
  • Stripe Price 是 One-time,不是 Recurring。
  • Price ID 是 price_...,不是 prod_...
  • STRIPE_SECRET_KEY 使用 live secret key。
  • STRIPE_WEBHOOK_SECRET 使用生产 webhook 的 signing secret。
  • STRIPE_PRICE_CREDIT_PACK_500 使用生产 Price ID。
  • 已执行远程 D1 迁移。
  • Webhook endpoint 是 https://z-image-turbo.com/api/stripe/webhook
  • Webhook 事件包含 checkout.session.completedcheckout.session.async_payment_succeededcheckout.session.async_payment_failedcharge.refundedrefund.updated
  • 当前没有启用订阅事件:customer.subscription.*invoice.payment_*
  • Stripe Customer Portal 已启用 invoice/payment history。
  • 账户页能显示积分包。
  • 支付成功后 billing_order 状态变为 paid
  • credit_ledger 出现 credit_pack_purchase 记录。
  • 用户 credit_balance 增加对应积分。

常见错误

填了 Product ID 而不是 Price ID

STRIPE_PRICE_CREDIT_PACK_500 必须是 price_...,不是 prod_...

创建成了订阅价格

当前代码只处理一次性支付。Stripe Price 必须是 One-time。如果创建成 Recurring,Checkout 或 webhook 逻辑会和当前实现不匹配。

test 和 live 混用

sk_test_... 只能配 test mode 的 price_...sk_live_... 只能配 live mode 的 price_...。Webhook secret 也分 test/live,不能混用。

Webhook 没配置签名 secret

没有正确的 STRIPE_WEBHOOK_SECRET 时,系统会拒绝 webhook,请求会返回签名错误,积分不会到账。

Stripe CLI 报 rk_live 权限不足

如果执行 stripe webhook_endpoints create --live ... 时看到:

The provided key 'rk_live_...' does not have the required permissions for this endpoint

说明当前 Stripe CLI 使用的是 live restricted key,但这个 key 没有创建 webhook endpoint 的权限。rk_live_... 是受限密钥,权限由 Stripe Dashboard 控制;--live 只切换到真实环境,不会自动提升权限。

推荐处理顺序:

  1. 优先在 Stripe Dashboard 的 Developers → Webhooks → Add endpoint 手动创建生产 webhook。
  2. 或者用有权限的 sk_live_... 执行 CLI 命令,并用 --api-key sk_live_xxx 显式指定。
  3. 或者在 Dashboard 的 Developers → API keys → Restricted keys 中找到当前 CLI key,给它增加 webhook endpoint 创建权限。

生产 webhook 创建成功后,仍然要复制该 endpoint 的 whsec_...,并把它配置成 STRIPE_WEBHOOK_SECRET

没有执行 D1 迁移

如果没有 billing_order 表,支付订单无法创建。上线前必须执行远程迁移。

退款处理建议

当前系统已经有后台退款入口:

  1. 进入 /admin/billing
  2. 找到状态为 paid 的订单。
  3. 在退款区域输入 REFUND 确认。
  4. 默认勾选“扣回本订单积分”。
  5. 提交后系统调用 Stripe Refund API。
  6. 如果退款成功,billing_order 会标记为 refunded,并写入 stripe_refund_id
  7. 如果勾选扣回积分,系统最多扣回用户当前可用余额,不会把余额扣成负数。

如果直接在 Stripe Dashboard 发起退款,Webhook 会同步订单退款状态,但不会自动扣回积分。遇到这类订单时建议在后台审计日志和用户积分流水中人工复核。

为什么现在不做月订阅

积分包更简单,也更适合当前阶段:

  • 用户只在需要时付费。
  • 没有自动续费争议。
  • 余额和消耗清晰可查。
  • Stripe webhook 逻辑更简单。
  • 后台订单和积分流水更容易对账。

等积分充值稳定后,再考虑自动充值或订阅会更稳妥。