订阅管理 API
用户订阅接口,包括 Driver V2 稳定只读摘要,以及网页端查询、变更套餐、取消订阅退款、 取消续订和恢复续订。
路由前缀:/billing
源码:apps/backend/src/routes/billing-subscription.ts
认证
JWT 用户会话可以访问全部下列接口。OAuth 按最小权限拆分:
billing:read:只能调用GET /billing/summary,且只签发给独立环境变量白名单中的官方 Driver V2 client。orders:checkout:保留网页/订单链路的查询和四个写接口,不能读取新的 summary。
Authorization: Bearer <JWT>
Authorization: Bearer wno-...billing:read V1 只读取用户首次创建的默认个人团队,不支持团队切换。旧查询接口允许 owner 或 accepted member;写入接口仍要求 owner/admin。
端点
网页 Driver 购买资格
GET /billing/driver-offer?teamId=<当前团队ID>,仅 JWT 用户会话可用;OAuth 不支持此接口。必填 teamId,只允许读取本人默认个人团队的资格,响应设置 Cache-Control: no-store。接口不创建订单、不调用支付渠道。
成功返回 { success: true, data },data 包含 teamId、defaultTeamSlug、status、cloudAccess 和 purchase。cloudAccess 为已付权益摘要,停售时仍可读取有效期。非本人默认团队返回 wrong_team 和本人默认团队 slug,不查询其他团队权益;无默认团队返回 no_team。
status 为 available 时,purchase 包含当前数据库实际 productCode、priceCode、currency、amountMinor、billingPeriod,网页用户主动点击后传入现有订阅支付流程。其余状态 not_on_sale、pilot_unavailable、subscription_exists、purchase_conflict 均返回 purchase: null。缺少团队参数返回 400,非 JWT 返回 403,数据库或有效销售价格配置异常返回固定脱敏错误 driver_subscription_unavailable / 503。缺失或未启用的销售配置返回 not_on_sale。
此接口仅供 /o/:teamSlug/settings/billing 的独立 Driver 卡片使用,不依赖平台 billing.plans。下单及支付发起时仍复核资格,不能把只读查询结果当作付款授权。
1. 获取 Driver V2 只读订阅摘要
GET /billing/summary?teamId=<optional>teamId 仅为未来多团队合同预留。V1 不传时稳定选择 teams.owner_user_id 属于当前用户、按 created_at ASC, id ASC 排序的首个个人团队;传入时必须等于该团队,否则统一返回 404。
响应只包含当前套餐、成交价快照、计费周期、取消语义、权益可用性标志、最近 3 条订阅账单摘要和网页管理入口,不返回 provider data、支付凭据、卡信息或地址。
{
"success": true,
"data": {
"teamId": "uuid",
"plan": { "code": "pro", "name": "Pro" },
"price": { "amountMinor": 999, "currency": "USD", "source": "subscription_snapshot" },
"billingPeriod": "monthly",
"status": "active",
"currentPeriodStart": "2026-08-01T00:00:00.000Z",
"currentPeriodEnd": "2026-09-01T00:00:00.000Z",
"renewal": {
"willRenew": false,
"cancelAtPeriodEnd": true,
"cancelEffectiveAt": "2026-09-01T00:00:00.000Z",
"canResume": true
},
"entitlements": { "status": "unavailable", "items": [] },
"recentBillingRecords": [],
"manageUrl": "https://app.example.com/o/personal-team/settings/billing"
}
}状态枚举固定为 none|active|canceled|past_due|trialing|unknown。历史订阅缺少可靠成交币种时 price=null;不会用当前目录价伪造成交价。manageUrl 不携带 OAuth token。
错误码: 默认个人团队不存在、传入其它团队或任何不可访问团队时均返回 404 BILLING_TEAM_NOT_FOUND;服务端配置或数据库读取失败返回 500 BILLING_SUMMARY_FAILED。
2. 获取原有订阅与积分余额
GET /billing/subscription获取当前团队的订阅详情和积分余额。
查询参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
teamId | string | 否 | 团队 ID;未提供时稳定回退到默认个人团队 |
示例请求:
curl -X GET "https://block2-api.wainao.chat/billing/subscription?teamId=xxx" \
-H "Authorization: Bearer <JWT>"响应 200 OK:
{
"success": true,
"data": {
"subscription": {
"id": "uuid",
"planCode": "pro",
"billingPeriod": "monthly",
"status": "active",
"monthlyCredits": 100000,
"currentPeriodStart": "2026-03-01T00:00:00.000Z",
"currentPeriodEnd": "2026-04-01T00:00:00.000Z",
"cancelAtPeriodEnd": false,
"canceledAt": null,
"scheduledPlanCode": null
},
"balance": {
"totalCredits": 85000,
"grantCredits": 80000,
"purchaseCredits": 5000,
"periodEnd": "2026-04-01T00:00:00.000Z",
"periodGrantedCredits": 100000
}
}
}提示
subscription 或 balance 可能为 null(例如新团队尚未创建订阅记录时)。
错误码:
| HTTP | 错误码 | 说明 |
|---|---|---|
| 400 | NO_TEAM | 用户不在任何团队中 |
| 500 | — | 服务端错误 |
3. 变更套餐
POST /billing/subscription/change-plan申请变更订阅计划。升级时返回支付指引(需先创建支付订单);降级时设置 scheduledPlanCode,在当前计费周期末自动生效。
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
teamId | string | 是 | 团队 ID |
targetPlanCode | string | 是 | 目标计划代码(如 pro、free) |
billingPeriod | string | 是 | 计费周期(如 monthly、yearly) |
示例请求:
curl -X POST "https://block2-api.wainao.chat/billing/subscription/change-plan" \
-H "Authorization: Bearer <JWT>" \
-H "Content-Type: application/json" \
-d '{"teamId": "xxx", "targetPlanCode": "pro", "billingPeriod": "monthly"}'响应 200 OK:
{
"success": true,
"data": {
"action": "downgrade_scheduled",
"scheduledPlanCode": "free",
"effectiveAt": "2026-04-01T00:00:00.000Z"
}
}错误码:
| HTTP | 错误码 | 说明 |
|---|---|---|
| 400 | BAD_REQUEST | 缺少 teamId |
| 400 | — | 缺少 targetPlanCode 或 billingPeriod |
| 400 | NO_TEAM | 用户不在该团队中 |
| 403 | FORBIDDEN | 无权限(非 owner/admin) |
| 500 | — | 服务端错误 |
4. 取消订阅并退款
POST /billing/subscription/cancel这是“立即取消”合同,不是只停自动续费。当前周期开始未满 7 天时,服务端会固定当前 周期 A 和所有已付款未来周期,先关闭尚可支付的旧入口,再先 future、后 current 地逐笔 全额退款。正好满 7 天即不退款。订单周期无法精确证明时不会猜测或发起部分退款。
网页默认个人团队账单页的独立 Driver 卡片提供此操作,已付有效且停售时仍可申请。必须二次确认,不自动触发或重试;团队或账号变化会关闭旧确认。界面仅以 200 且 status=succeeded 显示完成,202 中的 processing、partially_failed、failed 分别展示实际进度或联系支持的提示,不伪装成退款成功。
billing:read 只有只读权限,不能调用本接口;JWT 或 orders:checkout 仍需通过团队 owner/admin 权限检查。
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
teamId | string | 是 | 团队 ID;V1 为默认个人团队 |
reason | string | 否 | 取消原因 |
处理中返回 202 Accepted,全部同步完成返回 200 OK:
{
"success": true,
"data": {
"batchId": "00000000-0000-0000-0000-000000000001",
"status": "processing",
"currentPeriodStart": "2026-08-01T00:00:00.000Z",
"itemCount": 2,
"items": []
}
}常见拒绝包括:当前周期已满 7 天、当前付款周期无法证明、存在无法归属的已收款未来 订单。以上情况均不会调用支付渠道。
5. 取消续订
POST /billing/subscription/cancel-renewal取消自动续费。订阅在当前周期结束后不再续期。
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
teamId | string | 是 | 团队 ID |
示例请求:
curl -X POST "https://block2-api.wainao.chat/billing/subscription/cancel-renewal" \
-H "Authorization: Bearer <JWT>" \
-H "Content-Type: application/json" \
-d '{"teamId": "xxx"}'响应 200 OK:
{
"success": true,
"data": {
"status": "active",
"cancelAtPeriodEnd": true,
"effectiveAt": "2026-04-01T00:00:00.000Z",
"canResume": true,
"idempotent": false
}
}错误码:
| HTTP | 错误码 | 说明 |
|---|---|---|
| 400 | BAD_REQUEST | 缺少 teamId |
| 400 | NO_TEAM | 用户不在该团队中 |
| 403 | FORBIDDEN | 无权限(非 owner/admin) |
| 404 | subscription_not_found | 订阅不存在 |
| 409 | subscription_not_cancelable | 当前订阅状态不能取消续订 |
| 500 | — | 服务端错误 |
6. 恢复续订
POST /billing/subscription/resume-renewal仅在订阅仍为 active、尚未到期且处于 pending cancel 时恢复自动续费。已经到期或终态取消时返回 409 subscription_not_resumable,不会静默重新激活。
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
teamId | string | 是 | 团队 ID |
示例请求:
curl -X POST "https://block2-api.wainao.chat/billing/subscription/resume-renewal" \
-H "Authorization: Bearer <JWT>" \
-H "Content-Type: application/json" \
-d '{"teamId": "xxx"}'响应 200 OK:
{
"success": true,
"data": {
"status": "active",
"cancelAtPeriodEnd": false,
"effectiveAt": null,
"canResume": false,
"idempotent": false
}
}错误码:
| HTTP | 错误码 | 说明 |
|---|---|---|
| 400 | BAD_REQUEST | 缺少 teamId |
| 400 | NO_TEAM | 用户不在该团队中 |
| 403 | FORBIDDEN | 无权限(非 owner/admin) |
| 404 | subscription_not_found | 订阅不存在 |
| 409 | subscription_not_resumable | 已到期或终态订阅不能恢复 |
| 500 | — | 服务端错误 |
消费余额校验
消费扣费在数据库事务中校验正数金额、分项一致性和整笔费用是否足额。余额不足会拒绝扣费,不会把余额扣成负数;拒付不写成功消费流水、不增加 API Key 消耗。重复请求复用原有成功记录,不重复收费。
若提示 balance_inconsistent,表示历史总余额与赠送/购买分项不符,需要根据原流水审计修复,不能通过额外充值或重试掩盖。该错误与正常的 insufficient_credits(可用余额不足)不同。
取消后迟到收款说明
取消完成后,旧付款订单的成功通知仍按原订单退款归属处理。已确认的迟到收款只补退资金, 不重新发放权益,也不影响后来新购的订阅。重复通知与恢复任务复用原批次和在途退款号。 渠道状态无法确认时保留待核对,不能把关单请求成功视为退款或到账成功。
取消中的付款入口待核对
付款入口尚无权威终态时,取消响应保持 processing 并返回 warnings,不得展示成退款完成。payment_entry_unconfirmed 表示渠道意图已记录但入口查询未确认,legacy_payment_intent_unknown 表示历史付款意图未知;原因持久保存于原退款明细 last_error,管理员可按订单检索。一次查无订单不能证明未来没有迟到收款,确认收款后仍按原批次补退。部署时 SUBSCRIPTION_PAYMENT_INTENT_CLOSURE_ENABLED 默认关闭,只有共库节点旧支付进程和请求全部耗尽、并获配置批准后才能启用新单的本地关单证明;历史未知订单不会回填。