Blog posts and course content are currently in Chinese. Translation is in progress.
Back to Blog

额度用完想续费?怡途Claw 套餐订阅与支付下单 API 全流程实战

2026年8月2日42 min read
额度用完想续费?怡途Claw 套餐订阅与支付下单 API 全流程实战

怡途Claw 套餐订阅、续费、充值与支付下单 API 实战:POST /api/app/payment/create 走微信/支付宝/苹果内购三渠道,下单返回 qrCode 还是 payUrl 取决于渠道和 openid,type 决定月付年付还是充值,targetInstanceId 续费已有实例,query 轮询订单状态,close 关单,apple-iap/restore 换机恢复订阅。和 301 计费查询不同层。

前面讲了实例怎么启动停止(306)、登录怎么拿 token(307)、资料怎么改和注销(308)。这组 /api/app/* 里还有一类专门管"花钱"的:买套餐、续费实例、充值余额、查订单状态。它和 301 那篇讲的计费不是一回事——301 是"我已经花了多少 token、还剩多少额度",是查询;这篇是"我要怎么下单付钱、这笔单到底付了没有",是交易。一个看账本,一个收银台。

实际接进来你会碰到这些问题:

  • 免费额度用完了,调哪个接口下单买套餐?请求体里 channeltype 怎么填?
  • 微信、支付宝、苹果内购三个渠道,下单后返回的字段一样吗?PC 端扫码和手机端 JSAPI 调起,参数差在哪?
  • 我是续费一个快到期的实例,还是新买一个套餐,传参有什么不同?
  • 下单返回里 qrCodepayUrl 都有,到底给前端哪个?
  • 用户跳去支付了,我怎么知道他到底付没付?是一直轮询 query 还是等回调?
  • 订单创建出来一直没付,过期了怎么办?能不能主动关掉?
  • 苹果内购的用户换手机、重装 App,之前买的年度订阅怎么恢复?

这组接口挂在 app/payment 下,全部要带 Authorization: Bearer <token>(307 讲过怎么拿)。本文从 app-payment.controller.tspayment-order.service.ts 的代码来看每个接口到底做了什么。

先搞清楚三个枚举

下单之前得把三个枚举对上号,不然请求体填不对。

支付渠道 channel,三选一:

  • wechat 微信支付
  • alipay 支付宝
  • apple_iap 苹果内购(App Store)

订单类型 type,三选一:

  • monthly 月付套餐
  • yearly 年付套餐
  • recharge 充值(往余额里充钱,不是买套餐)

订单状态 status,五态:

  • pending 待支付(刚下单)
  • paid 已支付
  • failed 支付失败
  • closed 已关闭(超时或主动关单)
  • refunded 已退款

注意 channeltype 是组合关系,不是二选一。比如"微信买年付套餐"就是 channel=wechat, type=yearly;"支付宝充值 100 块"就是 channel=alipay, type=recharge。苹果内购只走套餐(monthly/yearly),不充直营余额。

创建订单:POST /api/app/payment/create

最核心的接口。请求体:

{
  "channel": "wechat",
  "type": "yearly",
  "planId": "plan_xxx",
  "targetInstanceId": "inst_xxx",
  "returnUrl": "https://your-app.com/payment/success",
  "openid": "oXXXXXXX",
  "runtimeType": "openclaw",
  "llmBaseUrl": "https://api.example.com/v1",
  "llmApiKey": "sk-xxx",
  "llmModelId": "gpt-4o"
}

字段说明(除了 channeltype 必填,其余都可选,但组合起来语义不同):

  • planId 套餐 ID。不传就用数据库里的默认"个人版"套餐。后端代码里写死了兜底逻辑:找不到指定套餐就回退到个人版,再找不到才报错。所以你只想卖默认套餐时可以不传。
  • targetInstanceId 续费目标实例 ID。这是续费和新购的分水岭:传了就是给这个已有实例续期,不传就是新购(支付成功后后端会自动创建一个新实例)。续费时订单会把 planIdtargetInstanceId 绑在一起,回调成功后只延长这个实例的到期时间,不会新建。
  • amount 充值金额,只有 type=recharge 时才需要,单位是元,最小 0.01。套餐购买的金额由 planId 对应的套餐价格决定,不用传。
  • openid 微信 JSAPI 支付才需要——用户在微信浏览器/小程序里付款时要带。这个字段还兼职装苹果内购的收据数据(同一个 openid 字段,苹果内购时塞 receipt),DTO 没强类型区分,按渠道约定传。
  • returnUrl 支付完跳回的前端地址,支付宝 H5 和部分微信 H5 场景用。
  • runtimeType 用户选的运行时类型(openclaw/hermes/claude-code),新购时跟着订单走,支付成功后建实例用。
  • llmBaseUrl/llmApiKey/llmModelId 大模型配置,同样是新购时绑进订单,建实例时直接用,省得用户买完还要再配一遍。

返回:

{
  "success": true,
  "data": {
    "orderId": "ord_a1b2c3",
    "outTradeNo": "YT20260802xxxx",
    "amount": 299.00,
    "amountCent": 29900,
    "status": "pending",
    "expireAt": "2026-08-02T03:30:00.000Z",
    "qrCode": "weixin://wxpay/bizpayurl?pr=xxx"
  }
}

几个要点:

  • amountamountCent 同时给,单位分别是元和分。后端 yuanToCent = Math.round(value * 100)。这是防前端浮点踩坑的常规操作——你直接用 amountCent 算最稳,不用自己 *100 担心精度。
  • status 下单就是 pending,待支付。订单一创建就开始倒计时,expireAt 是过期时间,过了这个点订单自动失效。
  • 最关键的是 data 里除了固定几个字段,还有一坨渠道相关的支付参数直接展开(spread)进来,叫 payData。这一坨按渠道长得完全不一样,下面单独说。

payData 按渠道不同,别拿错字段

这是接支付最容易翻车的地方。create 返回的 data{ orderId, outTradeNo, amount, amountCent, status, expireAt, ...payData },那个 ...payData 是按渠道展开的,不同渠道字段不同:

  • 微信 Native 扫码(PC 端,没传 openid):{ qrCode: "weixin://wxpay/bizpayurl?pr=xxx" }。你把这个 qrCode 字符串生成二维码贴出来,用户用微信扫。
  • 微信 JSAPI(传了 openid,微信内浏览器/小程序):{ jsapiParams: { appId, timeStamp, nonceStr, package, signType, paySign } }。这是一整套前端 wx.requestPayment 调起参数,直接喂给 JS-SDK。
  • 微信 H5(手机普通浏览器):{ payUrl: "https://wx.tenpay.com/..." },一个跳转链接,window.location 过去拉起微信。
  • 支付宝{ payUrl: "https://..." },支付宝的收银台跳转链接,前端跳过去就行。
  • 苹果内购{ transactionId, productId },给客户端确认这笔交易用的。
  • 免费订单(amount 为 0,活动赠送给之类):{ payUrl: ".../payment/success?orderId=xxx&isFree=true" },直接跳成功页,没有真实支付环节。

所以前端处理 create 返回时不能写死取某个字段,要按 channel 分支:微信扫码取 qrCode 画码,微信 JSAPI 取 jsapiParams 调起,支付宝/H5 取 payUrl 跳转。代码里那个 fallback: true 标记是降级提示——比如你本来想要 H5 链接但只拿到了 qrCode,说明这次降级成了扫码,前端得提示用户去扫。

查订单状态:GET /api/app/payment/query

下单之后用户到底付没付,靠这个接口。前端轮询它:

curl "https://www.yitukeji.cn/api/app/payment/query?orderId=ord_a1b2c3" \
  -H "Authorization: Bearer <你的token>"

返回:

{
  "success": true,
  "data": {
    "orderId": "ord_a1b2c3",
    "outTradeNo": "YT20260802xxxx",
    "tradeNo": "420000xxxx20260802xxxx",
    "planId": "plan_xxx",
    "targetInstanceId": "inst_xxx",
    "channel": "wechat",
    "type": "yearly",
    "amount": 299.00,
    "amountCent": 29900,
    "status": "paid",
    "paidAt": "2026-08-02T02:15:33.000Z",
    "expireAt": "2026-08-02T03:30:00.000Z",
    "createdAt": "2026-08-02T02:00:20.000Z"
  }
}

判定的核心是 status

  • pending 还没付,继续轮询。
  • paid 付了,paidAt 是支付完成时间,tradeNo 是第三方(微信/支付宝)那边的交易号。这时后端回调链路已经跑完——订阅生效、实例续期或新建、余额入账都在这步之后自动完成。
  • failed/closed 这单废了,让用户重新下单。
  • refunded 退过款了。

一个安全细节:queryOrder 虽然只传 orderId,但 service 里会校验这个订单是不是当前 token 用户的。你不能拿别人的 orderId 去查别人的单,查不到自己的会直接报错。所以这个接口对前端是安全的,不用担心越权。

轮询节奏别太快,建议 3-5 秒一次,看到 paid 或终态就停。订单 expireAt 一般是下单后 30 分钟左右,过点了自动转 closed,没必要一直轮询。

订单列表:GET /api/app/payment/orders

看当前用户的所有订单:

curl "https://www.yitukeji.cn/api/app/payment/orders?page=1&limit=10&status=paid" \
  -H "Authorization: Bearer <你的token>"

pagelimit 分页(limit 默认 10),status 可选过滤某个状态。返回:

{
  "success": true,
  "data": {
    "orders": [
      {
        "orderId": "ord_a1b2c3",
        "outTradeNo": "YT20260802xxxx",
        "tradeNo": "420000xxxx",
        "channel": "wechat",
        "type": "yearly",
        "amount": 299.00,
        "amountCent": 29900,
        "status": "paid",
        "paidAt": "2026-08-02T02:15:33.000Z",
        "expireAt": "2026-08-02T03:30:00.000Z",
        "createdAt": "2026-08-02T02:00:20.000Z",
        "planName": "个人版年付",
        "planId": "plan_xxx",
        "targetInstanceId": "inst_xxx"
      }
    ],
    "pagination": { "page": 1, "limit": 10, "total": 23 }
  }
}

这里有个后端做了但你不一定注意到的活:planName 不是订单表里存的字段,是后端拿到这页订单里所有的 planId 去重,再批量查套餐表回填的名字。代码里对每个 planId 单独 getPlanById,查不到(套餐下架了)就 catch 掉、planName 给空字符串。所以你可能在历史订单里看到个别 planName 是空的——那是套餐已经被下架了,不是 bug。

targetInstanceId 在列表里也带,能直接看出这单是续费哪个实例的还是新购的(新购时这个字段是空的,支付成功后建实例时才生成 ID,所以新购订单的 targetInstanceId 可能为空)。

订单详情:GET /api/app/payment/orders/:orderId

query 返回的字段几乎一样,区别是路径风格——这个走 RESTful 路径参数,query 走 query string。都是查单笔订单,都是带用户鉴权的。按你前端的路由习惯选一个就行,没必要两个都调。

关闭订单:POST /api/app/payment/close/:orderId

用户下了单没付,想取消,或者前端想清理掉一个 pending 订单:

curl -X POST "https://www.yitukeji.cn/api/app/payment/close/ord_a1b2c3" \
  -H "Authorization: Bearer <你的token>"

返回就一句:

{ "success": true, "message": "订单已关闭" }

关单后订单状态变 closed,用户没法再扫那个码付款。这个操作幂等——关一个已经关了的单不会报错。续费/购买场景下少用,主要是充值或活动订单用户改主意时清掉,避免一堆 pending 挂着。

苹果内购恢复:POST /api/app/payment/apple-iap/restore

苹果内购的特殊场景:用户换了手机、卸载重装 App、或者多设备登录,之前买的年度套餐丢了,要恢复。苹果那边交易记录在苹果服务器上,靠收据找回。

curl -X POST "https://www.yitukeji.cn/api/app/payment/apple-iap/restore" \
  -H "Authorization: Bearer <你的token>" \
  -H "Content-Type: application/json" \
  -d '{ "receipt": "<苹果返回的base64收据>" }'

后端流程:

  1. verifyReceiptForRestore 向苹果验签,valid=false 直接 400 Apple 收据验证失败: <原因>
  2. 验签通过拿到所有交易记录,再逐笔 isActiveAppleRestoreTransaction 过滤出当前还没过期的
  3. 判过期分两种:订阅型(expiresDateMs 存在)直接比当前时间;一次性/非自动续期,按 productId 反查套餐、用 purchaseDateMs + durationMonths 算到期。

返回未过期的交易:

{
  "success": true,
  "data": {
    "environment": "production",
    "transactions": [
      {
        "productId": "com.yitu.yearly",
        "transactionId": "2xxxxxx",
        "purchaseDateMs": "1719800000000",
        "expiresDateMs": "1751400000000",
        "isTrial": false
      }
    ]
  }
}

前端拿到这批交易后,可以再走正常下单/激活流程把订阅绑回当前账号。注意恢复只返回交易记录,不会自动给当前用户开订阅——拿到记录后还要根据业务再走一遍权益发放,这一步是分开的。

这一整套和 301 计费什么关系

容易混,这里说清楚:

  • 301 讲的 /api/app/billing/* 是账本视角:查余额(balance)、查 token 用量明细(token-usage)、查套餐和当前订阅(plans/subscription)。只读,告诉你"现在什么状态、花了多少"。
  • 这篇讲的 /api/app/payment/* 是收银台视角:下单(create)、查这单付没付(query)、订单历史(orders)、关单(close)、苹果恢复(apple-iap/restore)。写操作,真的在产生交易。

完整链路是这样的:用户在套餐页看到 billing/plans 列出来的套餐 -> 选一个调 payment/create 下单 -> 前端拿 payData 拉起微信/支付宝/苹果支付 -> 轮询 payment/querystatus=paid -> 第三方回调到后端,后端自动发放权益(订阅生效、实例续期或新建、充值入账)-> 用户再调 billing/balance 就能看到余额变了。

所以一个"用户买套餐"的动作,前端要调的不只是 payment/create 这一下,而是 billing/plans(看价)+ payment/create(下单)+ payment/query(确认)三段,外加支付渠道 SDK 自己的调起。把这三段接顺,支付链路就通了。

下一篇会落到 app-agents——查当前用户的 agent 概要和性能数据,那组接口端点偏少但和实例运行状态直接相关,接监控面板时用得上。