跳到内容

Open Platform

受众:开发者 摘要:开放平台:第三方应用 / AK/SK / 签名验签 / 防重放 / scope / 限流 / 调用日志 / 开放 API。

Open Platform 是开放能力聚合模块,用于把框架内部能力通过统一安全边界开放给第三方开发者、外部系统或合作伙伴。

它属于可选模块,不是框架必须依赖。默认应用为了展示完整能力会引入该模块;如果项目不需要对外开放 API,可以直接移除或注释 spring-open-application 中的 spring-open-module-open-platform 依赖,不应影响 core、starter、Web3、Payment、Notification 等其他模块正常运行。

它不是 Web3 专用模块。Web3 只是第一阶段能力域,后续聚合支付、通知、存储、AI 等能力也会通过同一开放平台边界接入。

模块

text
spring-open-modules/spring-open-module-open-platform

包名:

text
com.springopen.module.openplatform

当前模块已建立数据底座、签名工具、安全过滤器、后台管理 API 和 Web3 v1 开放 API,并已接入 spring-open-application。默认应用会暴露 /api/v1/open/**,但所有开放能力都需要有效应用、密钥、签名、防重放、scope 和限流校验。

路由

开放 API 统一使用版本化路由:

text
/api/v1/open/**

后台管理 API 统一使用:

text
/api/v1/admin/open-platform/**

开发者用户控制台 API 统一使用:

text
/api/v1/open/console/**

开放平台是外部契约,所有对外能力必须带版本号。当前第一版使用 v1,后续如果出现破坏性语义变化,再新增 v2v3;同一版本内只做向后兼容的字段新增或非破坏性增强。

设计边界

  • Open Platform 是聚合出口,不是基础依赖;其他模块不能反向依赖 Open Platform。
  • Open Platform 不直接引入链 SDK。
  • Web3 能力通过 spring-open-starter-web3 的 facade / manager 复用。
  • 支付能力通过 spring-open-starter-payment 复用。
  • 通知、存储、AI 等能力通过各自 starter 或业务 service 复用。
  • 第三方 SDK、私钥、签名材料和底层 provider 实现不能泄露到开放 API 层。

配置

yaml
spring:
  open:
    open-platform:
      enabled: true
      security:
        filter-enabled: true
        timestamp-tolerance: 5m
        nonce-ttl: 10m
        ip-whitelist-enabled: true
        default-rate-limit-per-minute: 60
        allowed-signature-methods:
          - HMAC-SHA256
        secret:
          secret-key-prefix: key_
          plain-secret-prefix: sk-
          encryption-key: ${SPRING_OPEN_OPEN_PLATFORM_SECRET_ENCRYPTION_KEY:${spring.application.secret:}}

说明:

配置默认值说明
enabledtrue是否启用开放平台模块
security.filter-enabledtrue是否启用开放平台访问过滤器;当前业务接口使用 /api/v1/open/**
security.timestamp-tolerance5m请求时间戳允许偏差
security.nonce-ttl10mnonce 防重放有效期
security.ip-whitelist-enabledtrue是否启用应用 IP 白名单检查
security.default-rate-limit-per-minute60应用未单独配置限流时的默认每分钟调用次数
security.default-free-trial-daily-quota10000应用没有可用套餐且未配置应用级日配额时的默认免费试用日额度;设为 0 或负数可关闭
security.default-free-trial-monthly-quota100000应用没有可用套餐且未配置应用级月配额时的默认免费试用月额度;设为 0 或负数可关闭
security.allowed-signature-methodsHMAC-SHA256允许的签名算法
secret.secret-key-prefixkey_内部密钥记录标识前缀;默认贴近主流平台 API Key 对象 ID 习惯,可配置为空字符串
secret.plain-secret-prefixsk-明文 API Key 前缀;默认贴近主流开发者工具习惯,可配置为空字符串
secret.encryption-key${spring.application.secret:}AI 服务商 数据库覆盖 API Key、应用 Webhook 签名密钥等需要可逆读取的敏感配置加密 key;生产建议使用 SPRING_OPEN_OPEN_PLATFORM_SECRET_ENCRYPTION_KEY 单独配置。未显式配置时启动期按 显式专用 → 数据库 spring.open.open-platform.secret.encryption-key → 数据库共享 spring.application.secret(首启自动播种,多实例共库收敛一致)→ 本地派生 顺序收敛;更换生效密钥会使已加密的存量数据不可解密,需重新录入

上述 Open Platform 模块启停、安全过滤、限流 / 免费额度和 Secret 前缀 / 加密 key 配置已由 OpenPlatformServiceProvider 声明到 lifecycle 配置 schema。后台配置中心可以发现这些配置;secret.encryption-key 会标记为敏感配置。

AI 服务商 上游 API Key 的数据库覆盖值和开放平台应用 Webhook 签名密钥会通过 spring-open-core-secretSecretManager 写入 secret:v1: 可逆 Secret envelope;默认使用内部 local 服务商 和 spring.open.open-platform.secret.encryption-key,历史明文与旧 enc:v1: 密文仍可读取,下一次保存会写回新 envelope。通用 Secret 底座同时提供内部 byok 服务商,以及显式注册才启用的 KMS / Vault 服务商 SPI;框架不会默认启动第三方 KMS / Vault 服务商实现。需要 AWS KMS、Aliyun KMS 或 HashiCorp Vault Transit 时,显式引入 spring-open-starter-secret-sdk 并手动注册对应 SecretKmsClient / SecretVaultClient。开放平台应用 AppSecret 不走可逆加密,服务端只保存 secret_hashsecret_hintmasked_secret,创建 / 轮换响应中的明文只返回一次。

当前能力

开放平台基础:

  • 开发者应用
  • 应用套餐
  • AK/SK 凭证
  • 请求签名验签
  • timestamp / nonce 防重放
  • IP 白名单
  • scope 权限范围
  • 调用日志
  • 计量事件
  • 日 / 月配额
  • 基础限流
  • 应用 Webhook 回调端点配置

Web3 v1 开放能力:

  • 链信息列表与链信息查询
  • 原生币余额查询
  • 批量原生币余额查询
  • ERC-20 余额查询
  • ERC-20 授权额度查询
  • ERC-20 Token 信息查询
  • Gas Price、Nonce 和 Gas 预估
  • 交易和交易回执查询
  • 批量交易确认数查询
  • 交易确认数查询
  • 交易 input 解码
  • 事件日志查询
  • Token 价格查询
  • 批量转账计划预览,不接收私钥、不签名、不广播
  • 批量转账真实签名,可选广播
  • 资金归集计划预览,不接收私钥、不签名、不广播
  • 资金归集真实签名,可选广播

Voucher 开放能力:

  • 查询业务消费方和持有人当前可用卡券
  • 业务结算前锁定一张卡券
  • 业务成功后确认核销
  • 业务取消、失败或超时时释放锁定

写交易开放接口默认不广播,只返回签名后的 rawTransaction;只有显式传入 broadcast=true 才广播到链上。生产使用必须经过 scope、签名、幂等、审计、风控和可选人工审批。

安全模型

开放平台第一阶段采用 AppKey + AppSecret + 签名 + 防重放 + scope 的轻量模型,适合第三方服务端调用。

AI 模型网关已由独立 spring-open-module-ai 承载,使用 AI 模块原生 Gateway Key、scope、限流、quota、用量事件和 服务商 健康链路,不再作为 Open Platform scope 的例外。Claude Code、CC Switch、OpenAI SDK 等开发者工具接入 AI 网关时,请使用后台 AI 平台创建的 sk-ai-... Gateway Key;Web3、支付、卡券等开放能力继续使用下面的 AppKey + AppSecret + HMAC 签名模式。

推荐请求头:

http
X-Open-App-Key: app_xxx
X-Open-Timestamp: 1779379200000
X-Open-Nonce: random-string
X-Open-Signature: hex-or-base64-signature
X-Open-Signature-Method: HMAC-SHA256
X-Open-Idempotency-Key: optional-business-idempotency-key

限流响应头:

http
X-Open-RateLimit-Limit: 60
X-Open-RateLimit-Remaining: 59
X-Open-RateLimit-Reset: 1779408060
Retry-After: 12

X-Open-RateLimit-Reset 使用 Unix 秒级时间戳。Retry-After 只在被限流时返回,表示建议多少秒后重试。应用不限流时不会返回这些限流响应头。

签名范围:

text
method
path
query string
timestamp
nonce
body sha256

当前签名计算方式:

text
signingKey = SHA-256(AppSecret)
signature = HMAC-SHA256(canonicalString, signingKey)

说明:

  • 创建密钥接口只返回一次明文 AppSecret
  • 服务端数据库保存的是 SHA-256(AppSecret),当前第一版不会保存明文密钥。
  • 客户端应先计算 SHA-256(AppSecret) 作为签名 key,再对 canonical string 做 HMAC-SHA256。
  • 后续如接入 KMS 或密文密钥列,可以在不改变外部安全模型的前提下升级服务端密钥存储方式。

设计原则:

  • AppSecret 只用于服务端签名,不允许出现在前端页面、小程序或移动端源码中。
  • timestamp 默认只允许短时间窗口内有效。
  • nonce 在有效窗口内只能使用一次,避免重放。
  • scope 用于控制应用可访问的能力,统一使用 open:<capability>:<resource>:<action> 风格,例如 open:web3:balance:readopen:web3:transaction:readopen:payment:order:create
  • 调用日志必须记录 app、scope、path、traceId、耗时、结果 code 和脱敏后的请求来源。
  • 幂等 key 只用于创建类、写入类或资金类接口;只读查询不强制要求。

后续如需面向浏览器、小程序或移动 App 暴露能力,应增加 OAuth2/OIDC、短期 token 或服务端代理模式,不应直接下发长期 AppSecret

快速接入

典型接入流程:

  1. 开发者在用户控制台创建自己的开放平台应用,或由后台创建应用。
  2. 为应用生成密钥,保存本次响应中的 AppSecret
  3. 为应用授权需要的 scope,例如 open:web3:balance:read
  4. 客户端服务端按签名规则生成请求头。
  5. 调用 /api/v1/open/** 开放 API。
  6. 通过调用日志查看结果、耗时、来源 IP、traceId 和失败原因。

开放平台适合“服务端调用服务端”。不要把 AppSecret 放到浏览器、小程序或移动 App 中;如果要给前端使用,应由自己的后端代理签名。

开发者控制台接入清单

开发者第一次接入时,建议按下面清单逐项确认,避免把 AI Gateway Key 和通用开放 API 签名模式混用:

步骤要做什么关键点
1创建开放平台应用用户控制台或后台都可以创建;应用归属用于隔离 Key、用量、账单和配额
2生成应用密钥明文只返回一次;通用开放 API 使用 AppKey + AppSecret 计算签名
3授权 scopeWeb3 按接口授权 open:web3:*;支付、卡券、广告等按对应开放能力授权
4选择调用模式Web3 / 支付 / 卡券等开放 API 走 HMAC 签名请求头;AI 网关使用 AI 平台的 Gateway Key,见 AI 模型网关
5复制 baseURLOpenAI Compatible、Claude / CC Switch、Codex / Responses API 的 baseURL 不同,见 AI 模型网关
6购买套餐并查账用户控制台可查看可售套餐、购买预览、支付激活套餐订单、账务流水和月度账单快照
7排障响应中的 traceId 可到后台调用日志、AI 用量事件、服务商 健康和账务流水中串联排查

两种鉴权模式的区别

能力适合场景请求头说明
AI Gateway KeyClaude Code、CC Switch、OpenAI SDK、Codex-like 客户端、Cursor、Continue 等开发者工具Authorization: Bearer sk-ai-...x-api-key: sk-ai-...由 AI 模块后台创建,只对 /api/v1/ai/gateway/** 生效,scope 使用 ai:gateway:*
Open Platform HMACWeb3、支付、卡券、广告、内容访问、业务服务端调用开放 APIX-Open-App-KeyX-Open-TimestampX-Open-NonceX-Open-Signature-MethodX-Open-Signature适合服务端到服务端,前端页面不应持有长期 AppSecret

如果调用 AI 网关接口却传了 HMAC 头,或者调用 Web3 接口却只传 sk-...,都会按接口 scope 和鉴权规则返回未认证或未授权错误。

聚合支付开放 API 使用同一套 HMAC 签名规则;固定下单样例、body hash、canonical string 和 Webhook 验签夹具见 聚合支付平台

卡券开放 API 使用同一套 HMAC 签名规则,路由前缀为 /api/v1/open/vouchers,scope 为 open:voucher:availableopen:voucher:lockopen:voucher:redeem。接口列表和请求体见 卡券

广告开放 API 使用同一套 HMAC 签名规则,路由前缀为 /api/v1/open/advertising,scope 为 open:advertising:placement:readopen:advertising:event:write。接口列表和请求体见 自营广告

内容访问检查开放 API 使用同一套 HMAC 签名规则,路由为 POST /api/v1/open/entitlement/content-access/check,scope 为 open:entitlement:content-access:read。该接口只复用 Entitlement 的中性内容访问策略返回可读性、权益凭证和会员折扣资格,不创建解锁凭证、不写购买订单、不替代 Blog / Forum / Reading / Video 自己的支付、积分扣减和退款状态机。接口字段见 权益与套餐

应用 Webhook 回调端点

后台应用新增 Webhook 回调端点配置,用于把开放平台应用和 Runtime Webhook 投递底座连接起来。当前字段属于应用级运维配置,不改变开放 API 的 HMAC 鉴权规则:

应用字段Runtime Webhook header说明
webhookEnabledwebhook.enabled是否启用该应用的回调端点
webhookEndpointKeywebhook.endpoint-key命名 endpoint key,优先匹配 Runtime Webhook endpoint resolver
webhookUrlwebhook.endpoint直接回调 URL,用于没有命名 endpoint 的轻量场景
webhookSigningEnabledwebhook.signing-enabled是否对投递请求生成 Webhook 签名
webhookSecretwebhook.secret写入时接收明文,服务端用 SecretManager 可逆保护后保存,详情 / 列表不返回明文

webhookSecret 是 write-only 字段。创建或更新时填写会替换现有密钥;留空表示不修改原密钥,不会清空已保存密钥。后台详情和列表只返回 hasWebhookSecretmaskedWebhookSecret、endpoint key / URL、启用状态和签名状态。Runtime 侧需要投递时,由 Open Platform Service 解密后生成标准 Webhook headers,业务 Controller 和 Admin 页面不会接触明文密钥。

创建套餐

http
POST /api/v1/admin/open-platform/packages
Content-Type: application/json

{
  "code": "basic",
  "name": "基础套餐",
  "status": 1,
  "dailyQuota": 10000,
  "monthlyQuota": 300000,
  "giftQuota": 1000,
  "validityDays": 30,
  "unitPrice": 0,
  "currency": "CNY",
  "priceTiers": [
    {
      "name": "标准阶梯",
      "minUnitCount": 0,
      "maxUnitCount": 1000000,
      "unitPrice": 0.01,
      "currency": "CNY",
      "sortOrder": 1
    }
  ]
}

套餐用于声明默认日 / 月调用额度、赠送额度、有效期和后续计费单价。应用单独配置 dailyQuota / monthlyQuota 时优先使用应用配置;应用未配置时回退到绑定套餐额度;没有可用套餐时回退到 security.default-free-trial-daily-quota / security.default-free-trial-monthly-quota,让新开发者先用免费额度调通网关。阶梯价格用于运营配置和价格目录展示;用户控制台购买、续费和补款订单会绑定当前用户拥有的应用,支付动作会写入开放平台账务 recharge 流水,并把套餐与额度激活到目标应用。

创建应用

http
POST /api/v1/admin/open-platform/apps
Content-Type: application/json

{
  "name": "Sample App",
  "packageId": 1,
  "status": "enabled",
  "ipWhitelist": "127.0.0.1",
  "rateLimitPerMinute": 60,
  "dailyQuota": 10000,
  "monthlyQuota": 300000
}

packageId 为空时表示不绑定套餐。ipWhitelist 为空时表示不配置应用级白名单;如果全局启用白名单,生产环境建议填写固定出口 IP。rateLimitPerMinute0 或负数表示该应用不限流,留空时使用全局默认限流。dailyQuota / monthlyQuota 为应用级配额覆盖值,空值、0 或负数表示不覆盖套餐额度;没有可用套餐时会继续使用默认免费试用额度。

创建密钥

http
POST /api/v1/admin/open-platform/apps/{appId}/secrets
Content-Type: application/json

{}

响应中的明文 secret 只出现一次。后续列表和详情接口只返回 secretKeymaskedSecret、状态、状态原因、过期时间、禁用 / 轮换时间和最近使用时间,不返回明文。

用户控制台应用与密钥

开发者登录后可以通过用户侧控制台管理自己的应用和 Key。用户侧接口会按当前登录用户解析工作空间归属,默认个人开发者空间仍落到当前用户;后台管理接口走 /api/v1/admin/open-platform/** 和后台权限。

方法路由说明
GET/api/v1/open/console/apps分页查询我的开放平台应用
GET/api/v1/open/console/apps/{id}查询我的开放平台应用详情
POST/api/v1/open/console/apps创建我的开放平台应用
PUT/api/v1/open/console/apps/{id}更新我的开放平台应用,只能更新当前工作空间拥有的应用
DELETE/api/v1/open/console/apps/{id}删除我的开放平台应用,只能删除当前工作空间拥有的应用
GET/api/v1/open/console/apps/{appId}/usage查询我的应用套餐、日 / 月配额、已用量和剩余额度
GET/api/v1/open/console/apps/{appId}/billing/ledgers查询我的应用账务流水
GET/api/v1/open/console/apps/{appId}/billing/statements查询我的应用月度账单列表
GET/api/v1/open/console/apps/{appId}/billing/statements/{billingMonth}查询我的应用指定月份账单详情
GET/api/v1/open/console/packages查询可售套餐目录
GET/api/v1/open/console/packages/{id}查询可售套餐详情
POST/api/v1/open/console/packages/{id}/purchase-preview预览套餐购买金额和额度
POST/api/v1/open/console/packages/{id}/purchase-orders创建套餐购买待支付订单
POST/api/v1/open/console/packages/{id}/renew-orders创建套餐续费待支付订单
POST/api/v1/open/console/packages/{id}/top-up-orders创建套餐补款待支付订单
GET/api/v1/open/console/packages/purchase-orders分页查询我的套餐购买订单
GET/api/v1/open/console/packages/purchase-orders/{orderNo}查询我的套餐购买订单详情
POST/api/v1/open/console/packages/purchase-orders/{orderNo}/cancel取消我的待支付套餐订单
POST/api/v1/open/console/packages/purchase-orders/{orderNo}/pay支付并激活我的待支付套餐订单
POST/api/v1/open/console/packages/purchase-orders/{orderNo}/refund对已支付套餐订单发起退款状态动作
POST/api/v1/open/console/packages/purchase-orders/{orderNo}/reverse对已支付或已退款套餐订单发起冲正状态动作
GET/api/v1/open/console/apps/{appId}/secrets查询我的应用密钥列表
POST/api/v1/open/console/apps/{appId}/secrets创建我的应用密钥,明文只返回一次
PUT/api/v1/open/console/apps/{appId}/secrets/{secretId}/disable禁用我的应用密钥
POST/api/v1/open/console/apps/{appId}/secrets/{secretId}/rotate轮换我的应用密钥,明文只返回一次
DELETE/api/v1/open/console/apps/{appId}/secrets/{secretId}删除我的应用密钥

用户控制台返回的应用响应包含 ownerUserIdownerTenantIdownerOrganizationIdownerDepartmentId 等归属字段,供后台调试和权限排查;普通页面不应展示内部归属字段给最终用户。应用更新、删除、用量、账务流水和账单接口都会先校验应用归属,只允许开发者操作或查看当前工作空间拥有的应用。请求体中的 idappKey、归属字段不会改变服务端归属判定;后端始终以当前登录态解析出的 OpenPlatformWorkspaceContext 和数据库应用归属为准。

开放平台应用密钥治理分为两类:应用 AppSecret 继续 hash-only 保存,创建 / 轮换只返回一次明文;Webhook Secret、AI 服务商 上游 Key 等需要运行期读取的敏感值统一走 spring-open-core-secretSecretManager envelope。内置实现支持 local / BYOK,KMS / Vault 通过显式 服务商 SPI 接入,不会默认启用第三方密钥服务。付费开放 API 的 scope、usage、quota 和 billing 以中性 Entitlement / Money contract 为准:Open Platform 负责开放 API 鉴权、scope、限流、计量事件和账务流水,Entitlement 负责 open-platform consumer 的套餐 / 配额与内容访问决策,Money / Cashier / Payment 负责资金事实与支付动作。

套餐目录接口只返回启用可售套餐和阶梯价格,用于购买前展示;购买预览接口只计算本次购买的阶梯单价、应付金额、额度合计和有效期,不创建购买订单、不扣费。购买、续费和补款订单接口会接收 unitCountappId,先校验当前登录用户拥有该应用,再固化套餐、阶梯价、额度和应付金额快照,订单初始状态为 pendingpay 动作只允许待支付且未过期订单,会按订单金额写入应用账务账户 recharge 流水,随后把 packageId、日配额和“月配额 + 赠送额度”激活到该应用。订单分页和详情接口只返回当前登录用户自己的订单,前端可以继续展示待支付、已支付、已取消、已过期等状态。

授权 Scope

http
PUT /api/v1/admin/open-platform/apps/{appId}/scopes
Content-Type: application/json

{
  "scopeCodes": [
    "open:web3:network:read",
    "open:web3:balance:read",
    "open:web3:token:read",
    "open:web3:transaction:read"
  ]
}

PUT /scopes 是整体替换。传空数组会清空当前应用授权。

签名示例

签名使用原始请求信息生成 canonical string:

text
METHOD
PATH
QUERY_STRING
TIMESTAMP
NONCE
BODY_SHA256_HEX

规则:

  • METHOD 使用大写,例如 GETPOST
  • PATH 只包含路径,不包含协议、域名和端口。
  • QUERY_STRING 使用请求中的原始 query string;没有 query 时为空行。
  • TIMESTAMP 使用毫秒时间戳,默认允许 5 分钟偏差。
  • NONCE 在有效期内只能使用一次。
  • BODY_SHA256_HEX 是请求体字节的 SHA-256 hex;GET 空 body 使用空字符串的 SHA-256。
  • signingKey = SHA-256(AppSecret)
  • signature = HMAC-SHA256(canonicalString, signingKey),输出 hex。

Node.js 示例:

js
import crypto from "node:crypto";

function sha256Hex(value) {
  return crypto.createHash("sha256").update(value ?? "").digest("hex");
}

function signOpenRequest({ method, path, query = "", body = "", timestamp, nonce, appSecret }) {
  const bodyHash = sha256Hex(body);
  const canonical = [
    method.toUpperCase(),
    path.startsWith("/") ? path : `/${path}`,
    query,
    String(timestamp),
    nonce,
    bodyHash
  ].join("\n");
  const signingKey = sha256Hex(appSecret);
  return crypto.createHmac("sha256", signingKey).update(canonical).digest("hex");
}

GET 请求示例:

js
const appKey = "app_xxx";
const appSecret = "secret_xxx";
const timestamp = Date.now();
const nonce = crypto.randomUUID();
const path = "/api/v1/open/web3/chains/bsc/balances/native";
const query = "address=0x0000000000000000000000000000000000000000";
const signature = signOpenRequest({
  method: "GET",
  path,
  query,
  timestamp,
  nonce,
  appSecret
});

const response = await fetch(`http://localhost:8000${path}?${query}`, {
  headers: {
    "X-Open-App-Key": appKey,
    "X-Open-Timestamp": String(timestamp),
    "X-Open-Nonce": nonce,
    "X-Open-Signature": signature,
    "X-Open-Signature-Method": "HMAC-SHA256"
  }
});

POST 请求示例:

js
const body = JSON.stringify({
  addresses: [
    "0x0000000000000000000000000000000000000000"
  ]
});
const path = "/api/v1/open/web3/chains/bsc/balances/native/batch";
const timestamp = Date.now();
const nonce = crypto.randomUUID();
const signature = signOpenRequest({
  method: "POST",
  path,
  body,
  timestamp,
  nonce,
  appSecret
});

const response = await fetch(`http://localhost:8000${path}`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Open-App-Key": appKey,
    "X-Open-Timestamp": String(timestamp),
    "X-Open-Nonce": nonce,
    "X-Open-Signature": signature,
    "X-Open-Signature-Method": "HMAC-SHA256"
  },
  body
});

固定 Web3 Token 价格签名夹具:

字段
appKeyapp_web3_example
appSecretsecret_web3_example
methodPOST
path/api/v1/open/web3/chains/bsc/token-prices
query空字符串
timestamp1779379200000
nonceweb3-price-nonce-1
scopeopen:web3:price:read

请求体必须按实际发送字节计算 hash,下面 fixture 使用紧凑 JSON:

json
{"tokenAddress":"0x0000000000000000000000000000000000000000","tokenSymbol":"BNB","tokenDecimals":18,"quoteCurrency":"USD","providerName":"pancake-v2"}

对应签名中间值:

text
bodyHash = b758ac948411195867487ab08bce9a7398f065d27095b507c1916e0a59604802
signingKey = 75e43b35a2d094be01bae26603e22ebd8afb8ef3fbaaa6607ee4a1c20cef7473
signature = 61e3f6ec7dcda8b9931aa7eb6e526e32c55d55ae805e55ad59af08212a486821

canonical string:

text
POST
/api/v1/open/web3/chains/bsc/token-prices

1779379200000
web3-price-nonce-1
b758ac948411195867487ab08bce9a7398f065d27095b507c1916e0a59604802

Java 17 HttpClient 精简示例:

下面代码只演示签名规则和请求头生成;第三方服务端应按自己的 HTTP 客户端、日志、重试和密钥管理规范封装。

java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Duration;
import java.util.HexFormat;
import java.util.UUID;

public final class OpenPlatformClient {

    private static final String BASE_URL = "http://localhost:8000";
    private static final String APP_KEY = "app_xxx";
    private static final String APP_SECRET = "secret_xxx";

    private final HttpClient http = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(5))
            .build();

    public String get(String path, String query) throws Exception {
        long timestamp = System.currentTimeMillis();
        String nonce = UUID.randomUUID().toString();
        String signature = sign("GET", path, query, "", timestamp, nonce);

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(BASE_URL + path + (query == null || query.isBlank() ? "" : "?" + query)))
                .header("X-Open-App-Key", APP_KEY)
                .header("X-Open-Timestamp", String.valueOf(timestamp))
                .header("X-Open-Nonce", nonce)
                .header("X-Open-Signature", signature)
                .header("X-Open-Signature-Method", "HMAC-SHA256")
                .GET()
                .build();

        return http.send(request, HttpResponse.BodyHandlers.ofString()).body();
    }

    public String postJson(String path, String body) throws Exception {
        long timestamp = System.currentTimeMillis();
        String nonce = UUID.randomUUID().toString();
        String payload = body == null ? "" : body;
        String signature = sign("POST", path, "", payload, timestamp, nonce);

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(BASE_URL + path))
                .header("Content-Type", "application/json")
                .header("X-Open-App-Key", APP_KEY)
                .header("X-Open-Timestamp", String.valueOf(timestamp))
                .header("X-Open-Nonce", nonce)
                .header("X-Open-Signature", signature)
                .header("X-Open-Signature-Method", "HMAC-SHA256")
                .POST(HttpRequest.BodyPublishers.ofString(payload))
                .build();

        return http.send(request, HttpResponse.BodyHandlers.ofString()).body();
    }

    private static String sign(String method, String path, String query, String body,
            long timestamp, String nonce) throws Exception {
        String canonical = String.join("\n",
                method.toUpperCase(),
                path.startsWith("/") ? path : "/" + path,
                query == null ? "" : query,
                String.valueOf(timestamp),
                nonce,
                sha256Hex(body == null ? "" : body));
        return hmacSha256Hex(canonical, sha256Hex(APP_SECRET));
    }

    private static String sha256Hex(String value) throws Exception {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        return HexFormat.of().formatHex(digest.digest(value.getBytes(StandardCharsets.UTF_8)));
    }

    private static String hmacSha256Hex(String value, String key) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        return HexFormat.of().formatHex(mac.doFinal(value.getBytes(StandardCharsets.UTF_8)));
    }
}

响应统一使用 Result<T>

json
{
  "code": "200",
  "message": "Success",
  "data": {}
}

开放平台鉴权失败、scope 未授权、限流失败等场景,HTTP 状态仍返回 200,业务失败通过 Result.code 表达。

常见业务码:

Code场景
200成功
400请求参数或签名头缺失
401未认证或应用凭证无效
403应用禁用、IP 不允许、scope 未授权、路由未注册 scope
422DTO validation 失败
429应用限流
500服务端异常

常见失败排查:

Result.codemessage key常见原因处理建议
401messages.open-platform.signature.invalidAppKey 不存在、签名不一致、body hash 不一致、query string 与实际请求不一致重新核对 AppSecret、canonical string、原始 query string、请求体字节和签名输出格式
401messages.open-platform.signature.timestamp-invalidX-Open-Timestamp 不是毫秒时间戳使用 System.currentTimeMillis() 或等价毫秒时间戳
401messages.open-platform.signature.timestamp-expired客户端时间与服务端时间偏差超过 timestamp-tolerance校准服务器时间,避免复用过期请求
401messages.open-platform.signature.method-unsupportedX-Open-Signature-Method 不在允许列表中当前默认使用 HMAC-SHA256
401messages.open-platform.secret.unavailable应用没有可用密钥,或密钥已禁用/过期后台重新生成或启用密钥
401messages.open-platform.nonce.replayed同一应用在 nonce 有效期内重复使用相同 nonce每次请求生成新的随机 nonce
403messages.open-platform.app.disabled应用被禁用后台启用应用后再调用
403messages.open-platform.ip.forbidden请求来源 IP 不在应用白名单中配置固定出口 IP,或调整应用白名单
403messages.open-platform.scope.forbidden应用没有当前路由所需 scope后台为应用授权对应 scope
403messages.open-platform.route.scope-not-found新开放 API 没有注册 route -> scope 映射补充 OpenPlatformRouteScopeRegistryCustomizer 和文档
429messages.exception.too-many-requests当前应用超过每分钟限流根据 Retry-AfterX-Open-RateLimit-Reset 退避重试

调用失败后,后台调用日志会记录 app、scope、path、traceId、结果 code、结果 message 和耗时。排障时优先用响应中的 traceId 或调用时间在后台调用日志中定位。

Web3 调用示例

查询链信息:

http
GET /api/v1/open/web3/chains/bsc/info

批量查原生币余额:

http
POST /api/v1/open/web3/chains/bsc/balances/native/batch
Content-Type: application/json

{
  "addresses": [
    "0x0000000000000000000000000000000000000000",
    "0x1111111111111111111111111111111111111111"
  ]
}

查询 ERC-20 信息:

http
GET /api/v1/open/web3/chains/bsc/tokens/erc20?contractAddress=0x...

查询 ERC-20 allowance:

http
GET /api/v1/open/web3/chains/bsc/tokens/erc20/allowance?contractAddress=0x...&ownerAddress=0x...&spenderAddress=0x...

Gas 预估:

http
POST /api/v1/open/web3/chains/bsc/gas/estimate
Content-Type: application/json

{
  "fromAddress": "0x...",
  "toAddress": "0x...",
  "value": "0",
  "data": "0x"
}

批量交易确认数:

http
POST /api/v1/open/web3/chains/bsc/transactions/confirmations/batch
Content-Type: application/json

{
  "hashes": [
    "0x..."
  ],
  "requiredConfirmations": 12
}

批量转账和资金归集属于资金类能力。计划预览接口不接收私钥、不签名、不广播,适合先做金额、Gas、nonce 和余额检查;真实写交易接口会接收私钥用于本次签名,默认 broadcast=false 只返回 rawTransaction;只有显式 broadcast=true 才广播。生产环境建议把这类接口放在可信服务端、专用应用、独立 scope、IP 白名单和更低限流之下。

当前模块已接入开放平台访问过滤器,当前对外业务接口使用 /api/v1/open/**

  1. 根据路由 scope 注册表解析当前请求需要的 scope。
  2. 读取并缓存 request body,后续 Controller 仍可正常读取请求体。
  3. 构建签名校验入参。
  4. 执行应用、签名、timestamp、nonce、IP 白名单和 scope 校验。
  5. 执行应用级限流。
  6. 放行到业务 Controller。
  7. 无论成功或失败,都会尝试写入调用日志。

过滤器采用 fail-closed 策略:开放平台路由如果没有注册 scope,会直接返回统一 Result 失败响应,而不是默认放行。新增版本化开放能力时必须同步注册 route -> scope 映射。

spring-open-core-runtime 已提供 Open API 中立签名底层工具,Open Platform 主链路直接复用这些工具:

  • OpenApiHeaders
  • OpenApiSignatureMethods
  • OpenApiSignatureRequest
  • OpenApiSignatures
  • OpenPlatformAccessRequestFactory

这些工具负责 header 名称、canonical string、SHA-256 body hash、HMAC-SHA256 签名、常量时间比对,以及从 HttpServletRequest 构建安全校验入参。

OpenPlatformAccessRequestFactory 不直接读取请求流,调用方需要传入已缓存的 body 字节。当前访问过滤器会使用 request wrapper 缓存 body,避免业务 Controller 读取不到请求体。

当前模块还提供纯服务层安全校验:

  • OpenPlatformSecurityService
  • OpenPlatformAccessRequest
  • OpenPlatformAccessGrant

校验顺序:

  1. 查找并确认应用启用。
  2. 校验 timestamp 在允许窗口内。
  3. 校验签名算法。
  4. 使用应用可用密钥校验签名。
  5. 校验 IP 白名单。
  6. 校验应用授权 scope。
  7. 消费 nonce,防止重放。
  8. 更新密钥最后使用时间。

该服务已被开放平台访问过滤器调用,用于开放 API 安全校验;后台管理接口不经过该过滤器。

当前模块已提供调用日志记录服务:

  • OpenPlatformCallLogService.recordCall(...)
  • OpenPlatformCallLogRecord

记录字段包括应用、scope、请求方法、路径、query string、请求 IP、User-Agent、traceId、成功状态、结果 code、结果消息、耗时和发生时间。服务会统一裁剪字段长度以匹配数据库列,避免异常请求把调用日志写入打爆。

该服务已被开放平台访问过滤器调用。过滤器会在成功、鉴权失败、限流失败等场景尝试写入调用日志;日志写入失败不会中断业务响应。

后台调用日志分页支持按以下条件筛选:

参数说明
appKey应用 Key
scopeCodescope 编码
methodHTTP 方法,例如 GETPOST
path请求路径,模糊匹配
requestIp请求来源 IP
traceIdTraceId
resultCode业务结果码,例如 401403429
success是否成功
occurredFrom开始时间,ISO 日期时间格式
occurredTo结束时间,ISO 日期时间格式

后台调用日志统计接口:

text
GET /api/v1/admin/open-platform/call-logs/statistics

统计接口复用调用日志查看权限,支持和分页接口一致的筛选参数,返回总调用量、成功调用量、失败调用量、成功率、平均耗时、最大耗时、最小耗时、首次调用时间、最近调用时间,并按应用 Key 和 scope 聚合 Top 20 调用量。successRate 使用 01 的小数表示,例如 0.9500 表示 95%。该接口是 V0.5 第一阶段计量底座,只做只读统计,不自动扣费,也不修改订单、余额或账单。

后台调用日志导出接口:

text
GET /api/v1/admin/open-platform/call-logs/export

导出接口复用调用日志查看权限和分页筛选条件,返回 text/csv;charset=UTF-8 附件。CSV 只包含排障和审计所需的元数据字段:日志 ID、应用 ID、应用 Key、scope、方法、路径、来源 IP、traceId、成功状态、结果 code、耗时、发生时间、创建时间和更新时间;不导出 query string、User-Agent 和结果消息原文,避免把请求参数或上游错误详情扩散到离线文件。

当前模块已提供基础限流服务:

  • OpenPlatformRateLimiterService
  • spring-open-starter-rate-limit

开放平台使用通用 Rate Limit starter 做固定窗口限流。应用单独配置 rate_limit_per_minute 时优先使用应用配置;未配置时使用 security.default-rate-limit-per-minute;配置为 0 或负数表示不限流。

该服务已被开放平台访问过滤器调用。生产多实例场景建议使用 spring.open.rate-limit.default-store=redis;只有单进程诊断或明确的 lite 兼容模式才使用 memory

启用限流时,响应会带上 X-Open-RateLimit-LimitX-Open-RateLimit-RemainingX-Open-RateLimit-Reset。被限流时额外返回标准 Retry-After,方便调用方做退避重试。

当前模块已提供应用日 / 月配额:

  • open_platform_package.daily_quota
  • open_platform_package.monthly_quota
  • open_platform_app.daily_quota
  • open_platform_app.monthly_quota
  • OpenPlatformAppUsageService

配额以计量事件的可计费用量单元为统计来源,按 UTC 自然日和自然月统计。普通开放 API 默认 1 次成功调用计 1 unit,AI 网关按 token 用量写入 unitCount。应用配置的配额优先于套餐配额;应用配额为空、0 或负数时,回退到绑定套餐配额;应用没有可用套餐时,回退到默认免费试用日 / 月额度;默认免费额度也被设置为 0 或负数时才表示不限制调用。开放平台访问过滤器会在签名、scope、nonce 和每分钟限流通过后检查应用配额;配额用完时返回业务码 429,并写入调用日志用于审计。

后台应用用量接口:

text
GET /api/v1/admin/open-platform/apps/{appId}/usage

该接口复用应用查看权限,返回套餐信息、日配额、月配额、已用量、剩余量和当前统计窗口,便于后台展示套餐和用量进度。

用户控制台应用用量接口:

text
GET /api/v1/open/console/apps/{appId}/usage

该接口先校验当前登录用户是否拥有该应用,再返回套餐信息、日配额、月配额、已用量、剩余量和当前统计窗口,便于开发者控制台展示“我的应用”用量和套餐余量。

用户控制台套餐目录接口:

text
GET /api/v1/open/console/packages
GET /api/v1/open/console/packages/{id}
POST /api/v1/open/console/packages/{id}/purchase-preview
POST /api/v1/open/console/packages/{id}/purchase-orders
POST /api/v1/open/console/packages/{id}/renew-orders
POST /api/v1/open/console/packages/{id}/top-up-orders
GET /api/v1/open/console/packages/purchase-orders
GET /api/v1/open/console/packages/purchase-orders/{orderNo}
POST /api/v1/open/console/packages/purchase-orders/{orderNo}/cancel
POST /api/v1/open/console/packages/purchase-orders/{orderNo}/pay
POST /api/v1/open/console/packages/purchase-orders/{orderNo}/refund
POST /api/v1/open/console/packages/purchase-orders/{orderNo}/reverse

这些接口要求当前用户已登录。目录和详情返回可售套餐基础额度、赠送额度、有效期、默认单价和阶梯价格;购买预览接收 unitCount,返回匹配的阶梯价、本次应付金额、展示金额、日 / 月 / 赠送额度合计、有效期和预览过期时间。

购买预览是只读快照,不绑定应用、不创建购买订单、不扣费。购买、续费和补款订单接口接收 unitCountappId,创建 pending 待支付订单并返回 orderNoappId、套餐快照、阶梯价快照、额度合计、应付金额、展示金额、支付截止时间、状态展示值和可操作标记。订单分页和详情接口按当前登录用户隔离,只支持查询自己的购买订单,可按关键字、订单号、套餐编码和订单状态筛选。取消只允许待支付订单;支付只允许待支付且未过期订单,会写入开放平台账务 recharge 流水并激活应用套餐与额度;退款只允许已支付订单;冲正允许已支付或已退款订单。Payment 收银台支付、支付回调和正式月度账单落库已接入;自动续费继续走后续独立商业化闭环。

用户控制台应用账务流水接口:

text
GET /api/v1/open/console/apps/{appId}/billing/ledgers
GET /api/v1/open/console/apps/{appId}/billing/statements
GET /api/v1/open/console/apps/{appId}/billing/statements/{billingMonth}

该接口先校验当前登录用户是否拥有该应用,再按应用 appKey 查询账务流水,支持方向、场景、币种、资产、关联业务、幂等键和发生时间筛选。前端展示金额、资产和时间时优先使用响应中的 displayAssetdisplayAmountdisplayBalanceBeforedisplayBalanceAfterdisplayOccurredAt,不要自行拼接币种和格式。

/billing/statements 是用户侧月度账单查询入口,支持 billingMonthFrom=yyyy-MMbillingMonthTo=yyyy-MM 和分页参数;未传账期时返回当前应用已有的正式账单快照。/billing/statements/{billingMonth} 返回单月账单详情,billingMonth 使用 yyyy-MM。账单详情优先读取 open_platform_billing_statementopen_platform_billing_statement_line_item,缺失时按当前应用归属与账期懒生成一次;已落库账单不会随用量事件变化重新计算。账单响应包含 statementNo、账期范围、生成时间、状态展示值、费用汇总和 lineItems 明细;snapshotOnly=true 表示账单本身是核算快照,不创建充值订单、不触发扣费。

后台计量事件聚合接口:

text
GET /api/v1/admin/open-platform/usage-events/statistics

该接口复用计量事件查看权限,支持按 appKeyappSecretIdscopeCodeapiVersionmethodpathresultCodebillableproviderNamemodelNametraceIdoccurredFromoccurredTo 过滤,返回总事件数、可计费事件数、免费事件数、总用量单元、可计费用量单元、免费用量单元、首次 / 最近发生时间,并按应用、scope、路径聚合 Top 20 用量,同时按币种汇总历史成本快照。

聚合结果只读,不直接扣费、不生成账单、不修改应用余额;它是后续账单、成本统计和余额扣费的统一数据来源。新增调用会在 usage event 中记录当时套餐的 unitPricecurrencycostAmount 快照,避免套餐改价后污染历史成本统计。

Open Platform 账务账户用于开放能力内部扣费和对账,资产编码仍按 fiat:CNYfiat:USD 等法币资产写入,但账户与流水的 decimals 固定为 6 位内部微计费精度。AI Gateway、套餐阶梯价和 usage event 成本快照按 6 位小数向下截断后落账,便于 token 级费用逐笔对账;真实支付渠道的外部收款金额仍由 Cashier / Payment 按渠道规则处理。

后台账单费用预览接口:

text
GET /api/v1/admin/open-platform/billing/preview
GET /api/v1/admin/open-platform/billing/statements/preview
GET /api/v1/admin/open-platform/billing/statements
GET /api/v1/admin/open-platform/billing/statements/{billingMonth}?appKey=app_xxx
POST /api/v1/admin/open-platform/billing/statements/generate
GET /api/v1/admin/open-platform/billing/accounts
GET /api/v1/admin/open-platform/billing/accounts/{id}
GET /api/v1/admin/open-platform/billing/ledgers
GET /api/v1/admin/open-platform/billing/ledgers/{id}

该接口复用计量事件查看权限,支持按 appKeyscopeCodeapiVersionmethodpathoccurredFromoccurredTo 过滤,返回当前筛选范围内的调用量、可计费用量、免费用量、应用 / scope / 路径聚合,以及按币种汇总的历史成本快照。

/billing/statements/preview 是账期账单草稿预览,支持 billingMonth=yyyy-MM,并可继续按 appKeyscopeCodeapiVersionmethodpath 缩小范围。它会把账期归一为当月 [periodStart, periodEnd),返回 statementNo、账期范围、草稿标识、复用费用预览明细,以及按应用 / scope / 版本 / 方法 / 路径 / 币种聚合的 lineItems 明细行。该入口只读,不落账、不扣余额、不生成支付订单。

正式账单接口读取或生成已落库的月度账单快照:后台 GET /billing/statements 支持按 appKey、账期范围、状态和分页查询,GET /billing/statements/{billingMonth} 读取指定应用账期详情,POST /billing/statements/generateappKey + billingMonth 幂等生成正式账单。首次生成会校验应用存在;已有账单直接返回历史快照,不随后续用量事件重算。

费用预览和账期草稿只做只读核算,不扣余额、不生成支付订单,也不会修改开放平台应用状态。账务账户和账务流水后台接口仍只提供查询;正式月度账单基于 usage event 生成不可变快照,便于开发者查看账期费用和明细。模块内部已具备幂等账务写入服务,用于付费调用阶段统一处理充值、扣费、冻结和解冻。套餐单价大于 0 的应用调用会先检查对应币种账务账户可用余额,余额不足、账户不存在或账户停用时直接拒绝进入 服务商。账务预占底座已提供 reserveBillingBalance(...),用 api-reserve 场景冻结预计费用并保持幂等;失败释放底座已提供 releaseBillingReservation(...),用 api-reserve-release 场景释放预占冻结金额;成功结算底座已提供 settleBillingReservation(...),先复用释放入口,再按实际费用写入扣费流水;AI 网关普通响应和流式响应调用链已接入自动预占、成功实扣和 服务商 异常 / 流式写出中断释放。AI 调用审计复用后台计量事件接口,支持按应用 appKey、应用密钥 appSecretId、模型、服务商 和 traceId 查询调用、token、成本快照与扣费链路。购买、续费、补款订单已有用户侧支付激活动作,退款和冲正仍是状态动作 contract;更完整的限流熔断仍属于后续付费调用增强。

计量与计费预留

开放平台后续会支持付费调用服务,但当前阶段不在过滤器里直接扣费,也不在能力 Controller 里写计费逻辑。

推荐边界:

  • 调用日志是审计和排障底座;计量事件是配额、套餐、账单和成本统计的来源。
  • 计量与业务能力解耦,由开放平台统一根据 appKeyscopemethodpathapiVersion 和调用结果生成 usage event。
  • 价格规则应绑定 apiVersion + scope + route + method + capability,避免接口升级后计费语义混乱。
  • 默认只对通过鉴权、未被限流、进入业务 Controller 并得到业务结果的调用计量;签名失败、IP 白名单失败、scope 不足和限流拒绝默认不计费。
  • 外部成本型能力可以通过价格规则声明“失败也计量”,例如第三方供应商已经产生实际成本的调用。
  • 计量必须具备幂等键,当前使用 call-log:{callLogId},避免过滤器重试或异步消费导致重复计费。
  • 账务账户和流水承载付费调用的运维与审计底座;账期草稿预览只基于 usage event 做只读核算;正式月度账单在 Open Platform 内落库为不可变快照,支付扣款也留在 Open Platform / Cashier 协作边界内,不回灌到 Web3、Payment、SMS、Storage 等具体能力模块。

当前已落地 open_platform_packageopen_platform_usage_eventopen_platform_billing_accountopen_platform_billing_ledgeropen_platform_billing_statementopen_platform_billing_statement_line_item:套餐提供默认日 / 月额度、单价和币种,应用可以绑定套餐并按需覆盖额度;成功且通过鉴权的开放 API 调用会在调用日志写入后生成一条可幂等去重的计量事件,并写入当时套餐的单价、币种和成本金额快照;应用日 / 月配额使用计量事件的可计费用量单元统计,避免失败鉴权、限流拒绝或异常日志影响套餐用量,也避免 AI token 调用只按请求次数消耗额度。套餐单价大于 0 时,请求进入 服务商 前会检查应用账务账户同币种可用余额是否至少覆盖本次套餐单价。用量聚合、账单预览和账期草稿预览基于计量事件提供只读统计;正式月度账单按 appKey + billingMonth 幂等生成落库快照,用户侧详情缺失时按应用归属懒生成一次。账务账户和账务流水已补齐资产字段,并在模块内部支持幂等创建账户、充值、扣费、冻结、解冻、余额快照、预计费用预占、预占失败释放和预占成功结算。AI 网关普通响应和流式响应会在进入 服务商 前冻结预计费用,成功后按实际 token 费用扣减,服务商 异常、流式写出异常、客户端断开或实际用量为 0 时释放冻结金额。账务账户 / 流水响应同时返回 displayAssetdisplayAmountdisplayBalanceAmountdisplayOccurredAt 等展示字段,前端优先展示这些字段,不自行拼接金额和资产。后续如果需要定时快照或大数据量报表,可在 Open Platform 模块内增加 open_platform_usage_aggregate。支付补款、套餐续费、限流熔断和审计查询继续在 Open Platform 付费调用阶段实现,不回写 Web3、Payment、SMS、Storage 等具体能力模块。

当前模块已提供路由 scope 注册表:

  • OpenPlatformRouteScopeRegistry
  • OpenPlatformRouteScopeRegistryCustomizer
  • OpenPlatformRouteScope

开放 API 不应在 Filter 中硬编码 scope。每个能力模块应通过 customizer 注册自己的路由和 scope 映射,例如 Web3 v1 已经内置注册余额、交易、事件、价格、批量转账计划、批量转账、资金归集计划和资金归集路由。

注册表支持 Ant 风格路径匹配和 HTTP method 匹配,会优先选择更具体的路由规则。/api/v1/open/** 等版本化开放请求会先通过注册表解析当前请求需要的 scope,再执行验签、scope 授权、限流和调用日志记录。

后台提供只读路由 scope 元数据接口:

text
GET /api/v1/admin/open-platform/route-scopes

该接口复用 scope 查看权限,返回 methodpatternscopeCode。后台页面可以用它辅助应用授权,开发者也可以用它检查新增开放 API 是否已经注册 route -> scope 映射。

表结构

当前最小表:

text
open_platform_app
open_platform_package
open_platform_app_secret
open_platform_scope
open_platform_app_scope
open_platform_nonce
open_platform_call_log
open_platform_usage_event

说明:

作用
open_platform_app开放平台应用主体,保存名称、状态、套餐、IP 白名单、限流策略、应用级日 / 月配额覆盖值和应用 Webhook endpoint / 签名密钥摘要等
open_platform_package开放平台应用套餐,保存默认日 / 月调用配额、单价、币种和启用状态
open_platform_app_secret应用密钥,支持密钥轮换、禁用和过期时间
open_platform_scope开放能力 scope 字典
open_platform_app_scope应用与 scope 授权关系
open_platform_nonce防重放记录,按过期时间清理
open_platform_call_log外部调用日志,用于审计、排障、计费或限流分析
open_platform_usage_event外部调用计量事件,保存用量单元、单价、币种和成本快照,用于应用配额、套餐、账单和后续成本统计
open_platform_billing_account开放平台账务账户,保存应用、币种、资产编码、余额、冻结金额和累计收支,作为后续扣费模型的账户主体
open_platform_billing_ledger开放平台账务流水,保存资产编码、账务方向、场景、金额、前后余额、幂等键和关联业务对象

表名前缀使用 open_platform_,避免与其他后台表混淆。

API

后台管理 API:

text
GET    /api/v1/admin/open-platform/apps
POST   /api/v1/admin/open-platform/apps
GET    /api/v1/admin/open-platform/apps/{id}
GET    /api/v1/admin/open-platform/apps/{appId}/usage
PUT    /api/v1/admin/open-platform/apps/{id}
DELETE /api/v1/admin/open-platform/apps/{id}
DELETE /api/v1/admin/open-platform/apps

GET    /api/v1/admin/open-platform/packages
POST   /api/v1/admin/open-platform/packages
GET    /api/v1/admin/open-platform/packages/{id}
PUT    /api/v1/admin/open-platform/packages/{id}
DELETE /api/v1/admin/open-platform/packages/{id}
DELETE /api/v1/admin/open-platform/packages

GET    /api/v1/admin/open-platform/scopes
POST   /api/v1/admin/open-platform/scopes
GET    /api/v1/admin/open-platform/scopes/{id}
PUT    /api/v1/admin/open-platform/scopes/{id}
DELETE /api/v1/admin/open-platform/scopes/{id}
DELETE /api/v1/admin/open-platform/scopes
GET    /api/v1/admin/open-platform/route-scopes

GET    /api/v1/admin/open-platform/call-logs
GET    /api/v1/admin/open-platform/call-logs/export
GET    /api/v1/admin/open-platform/call-logs/{id}
DELETE /api/v1/admin/open-platform/call-logs/{id}
DELETE /api/v1/admin/open-platform/call-logs

GET    /api/v1/admin/open-platform/usage-events
GET    /api/v1/admin/open-platform/usage-events/statistics
GET    /api/v1/admin/open-platform/usage-events/{id}
DELETE /api/v1/admin/open-platform/usage-events/{id}
DELETE /api/v1/admin/open-platform/usage-events

GET    /api/v1/admin/open-platform/billing/preview
GET    /api/v1/admin/open-platform/billing/statements/preview
GET    /api/v1/admin/open-platform/billing/statements
GET    /api/v1/admin/open-platform/billing/statements/{billingMonth}
POST   /api/v1/admin/open-platform/billing/statements/generate
GET    /api/v1/admin/open-platform/billing/accounts
GET    /api/v1/admin/open-platform/billing/accounts/{id}
GET    /api/v1/admin/open-platform/billing/ledgers
GET    /api/v1/admin/open-platform/billing/ledgers/{id}
GET    /api/v1/open/console/apps/{appId}/billing/ledgers
GET    /api/v1/open/console/apps/{appId}/billing/statements
GET    /api/v1/open/console/apps/{appId}/billing/statements/{billingMonth}

GET    /api/v1/admin/ai/platform/overview
GET    /api/v1/admin/ai/platform/providers
GET    /api/v1/admin/ai/platform/models
PUT    /api/v1/admin/ai/platform/models
GET    /api/v1/admin/ai/platform/prices
GET    /api/v1/admin/ai/platform/routing

后台 AI 运营页使用 /api/v1/admin/ai/platform/** 代理接口,查询统一走后台权限 ai:platform:view,模型目录、价格、服务商 和路由保存动作走 ai:platform:update。这些接口复用 AI Platform 当前生效配置,但不走开放应用 sk-... Key、HMAC 或 Bearer Key 鉴权链路,避免 Admin 页面被开放平台调用方鉴权挡住。

Open Platform 基线迁移会把 developer 内置角色绑定到开放平台后台只读菜单和权限,包括应用、套餐、Scope、路由 Scope、调用日志、用量事件、账务和 AI 运营只读入口。该模板不授予删除、配置中心维护或跨模块全局维护能力;如后续开放开发者后台写操作,必须先补 workspace / 应用归属和数据范围强校验。

AI 模型价格目录默认采用主流供应商一致的 1M tokens 展示口径,并返回 billingUnitSize=1000000 供后台页面和计费逻辑换算。模型 metadata 使用 input-price-per-1m-tokensoutput-price-per-1m-tokensinput-cost-per-1m-tokensoutput-cost-per-1m-tokens 等 key;后续数据库价格覆盖仍归入 AI 运营配置阶段。

后台应用新增 Webhook 运维字段,复用应用新增 / 修改 / 详情接口返回:webhookEnabledwebhookEndpointKeywebhookUrlwebhookSigningEnabledhasWebhookSecretmaskedWebhookSecretwebhookSecret 只允许在保存请求中写入,不会出现在响应体中;旧客户端不提交任何 Webhook 字段时,服务端会保留原有回调配置。

应用密钥与授权 API:

text
GET    /api/v1/open/console/apps
GET    /api/v1/open/console/apps/{id}
POST   /api/v1/open/console/apps
GET    /api/v1/open/console/apps/{appId}/usage
GET    /api/v1/open/console/apps/{appId}/billing/ledgers
GET    /api/v1/open/console/packages
GET    /api/v1/open/console/packages/{id}
POST   /api/v1/open/console/packages/{id}/purchase-preview
POST   /api/v1/open/console/packages/{id}/purchase-orders
POST   /api/v1/open/console/packages/{id}/renew-orders
POST   /api/v1/open/console/packages/{id}/top-up-orders
GET    /api/v1/open/console/packages/purchase-orders
GET    /api/v1/open/console/packages/purchase-orders/{orderNo}
POST   /api/v1/open/console/packages/purchase-orders/{orderNo}/cancel
POST   /api/v1/open/console/packages/purchase-orders/{orderNo}/pay
POST   /api/v1/open/console/packages/purchase-orders/{orderNo}/refund
POST   /api/v1/open/console/packages/purchase-orders/{orderNo}/reverse
GET    /api/v1/open/console/apps/{appId}/secrets
POST   /api/v1/open/console/apps/{appId}/secrets
PUT    /api/v1/open/console/apps/{appId}/secrets/{secretId}/disable
POST   /api/v1/open/console/apps/{appId}/secrets/{secretId}/rotate
DELETE /api/v1/open/console/apps/{appId}/secrets/{secretId}

GET    /api/v1/admin/open-platform/apps/{appId}/secrets
POST   /api/v1/admin/open-platform/apps/{appId}/secrets
PUT    /api/v1/admin/open-platform/apps/{appId}/secrets/{secretId}/disable
POST   /api/v1/admin/open-platform/apps/{appId}/secrets/{secretId}/rotate
DELETE /api/v1/admin/open-platform/apps/{appId}/secrets/{secretId}

GET    /api/v1/admin/open-platform/apps/{appId}/scopes
PUT    /api/v1/admin/open-platform/apps/{appId}/scopes

密钥规则:

  • POST /secrets 创建新密钥并只在本次响应中返回一次明文 secret;如不需要设置过期时间,可直接提交空请求体。
  • 数据库存储 secret_hashsecret_hintmasked_secret,列表接口只返回 secretKeymaskedSecret、状态、状态原因、过期时间和使用时间,不返回明文。
  • GET /secrets 会同时返回该应用当前启用的 scopeCodes、分钟限流、日配额和月配额,方便开发者控制台直接展示“这把 Key 继承了哪些权限和调用限制”。
  • 轮换密钥使用 POST /rotate,会在同一事务内禁用旧密钥并创建新密钥,新密钥明文同样只在本次响应中返回一次;旧密钥会记录 statusReason=rotateddisabledAtrotatedAt,新密钥会记录 rotatedFromSecretId
  • 禁用密钥使用 PUT /disable,删除密钥使用 DELETE;禁用和删除都会记录 statusReason 与禁用时间,删除额外记录 deletedBy 后再逻辑删除。
  • 真实开放请求验签已由开放平台访问过滤器处理,不在后台管理 Controller 中处理。

Scope 授权规则:

  • PUT /scopes 使用传入的 scopeCodes 整体替换应用授权。
  • 传空数组表示清空当前应用授权,系统会把已有授权置为禁用状态。
  • 传入的 scope 必须存在且启用,否则返回业务错误。

第一阶段内置 Web3 scope:

Scope说明
open:web3:balance:read查询原生币和 Token 余额
open:web3:token:read查询 ERC-20 Token 信息和授权额度
open:web3:transaction:read查询交易、回执、确认数和 input 解码
open:web3:event:read查询链上事件日志
open:web3:price:read查询 Token 价格和池子报价
open:web3:network:read查询链信息、可公开 RPC 节点、Gas Price 等网络信息
open:web3:wallet:read查询钱包地址信息和 nonce
open:web3:wallet-signature:verify校验钱包 personal_sign 和 EIP-712 typed data 签名
open:web3:transfer:write批量转账计划预览和真实签名,按请求选择是否广播
open:web3:fund-collection:write资金归集计划预览和真实签名,按请求选择是否广播

其他已内置开放能力 scope:

Scope说明
open:voucher:available查询业务主体可用卡券
open:voucher:lock业务结算前锁定或释放卡券
open:voucher:redeem确认或冲正卡券核销
open:advertising:placement:read查询广告位当前可展示素材
open:advertising:event:write上报广告曝光、点击和转化事件

开放 API:

text
GET  /api/v1/open/web3/chains
GET  /api/v1/open/web3/chains/{chain}/info
GET  /api/v1/open/web3/chains/{chain}/balances/native
POST /api/v1/open/web3/chains/{chain}/balances/native/batch
GET  /api/v1/open/web3/chains/{chain}/gas-price
GET  /api/v1/open/web3/chains/{chain}/accounts/{address}
GET  /api/v1/open/web3/chains/{chain}/accounts/{address}/nonce
POST /api/v1/open/web3/chains/{chain}/wallet-signatures/personal/verify
POST /api/v1/open/web3/chains/{chain}/wallet-signatures/typed-data/verify
POST /api/v1/open/web3/chains/{chain}/gas/estimate
GET  /api/v1/open/web3/chains/{chain}/tokens/erc20
GET  /api/v1/open/web3/chains/{chain}/tokens/erc20/allowance
GET  /api/v1/open/web3/chains/{chain}/balances/erc20
GET  /api/v1/open/web3/chains/{chain}/transactions/{hash}
GET  /api/v1/open/web3/chains/{chain}/transactions/{hash}/receipt
GET  /api/v1/open/web3/chains/{chain}/transactions/{hash}/confirmation
POST /api/v1/open/web3/chains/{chain}/transactions/confirmations/batch
POST /api/v1/open/web3/chains/{chain}/transaction-inputs/decode
POST /api/v1/open/web3/chains/{chain}/event-logs/search
POST /api/v1/open/web3/chains/{chain}/token-prices
POST /api/v1/open/web3/chains/{chain}/transfer-plans/batch
POST /api/v1/open/web3/chains/{chain}/transfers/batch
POST /api/v1/open/web3/chains/{chain}/fund-collection-plans
POST /api/v1/open/web3/chains/{chain}/fund-collections

开放 API 的返回仍使用统一 Result<T>,HTTP 状态只表达传输层结果。

当前已实现的 Web3 v1 开放 API:

APIScope说明
GET /api/v1/open/web3/chainsopen:web3:network:read查询已启用链摘要和可公开 rpc-urls,不返回 private-rpc-urls
GET /api/v1/open/web3/chains/{chain}/infoopen:web3:network:read查询指定链摘要和可公开 rpc-urls,不返回 private-rpc-urls
GET /api/v1/open/web3/chains/{chain}/balances/native?address=...open:web3:balance:read查询原生币余额,返回原始 wei、格式化余额和地址浏览器链接
POST /api/v1/open/web3/chains/{chain}/balances/native/batchopen:web3:balance:read批量查询原生币余额,地址列表放在请求体
GET /api/v1/open/web3/chains/{chain}/gas-priceopen:web3:network:read查询当前 Gas Price
GET /api/v1/open/web3/chains/{chain}/accounts/{address}open:web3:wallet:read查询钱包地址余额、交易计数、是否合约地址和浏览器链接
GET /api/v1/open/web3/chains/{chain}/accounts/{address}/nonce?block=pendingopen:web3:wallet:read查询地址 nonce,默认 pending
POST /api/v1/open/web3/chains/{chain}/wallet-signatures/personal/verifyopen:web3:wallet-signature:verify校验钱包 personal_sign 签名,只返回验签结果,不登录、不创建账号
POST /api/v1/open/web3/chains/{chain}/wallet-signatures/typed-data/verifyopen:web3:wallet-signature:verify校验 EIP-712 typed data 签名,只返回验签结果,不登录、不创建账号
POST /api/v1/open/web3/chains/{chain}/gas/estimateopen:web3:transaction:readtoAddressdatavalue 预估 Gas
GET /api/v1/open/web3/chains/{chain}/tokens/erc20?contractAddress=...open:web3:token:read查询 ERC-20 Token 名称、符号、精度、总供应量和合约浏览器链接
GET /api/v1/open/web3/chains/{chain}/tokens/erc20/allowance?contractAddress=...&ownerAddress=...&spenderAddress=...open:web3:token:read查询 ERC-20 allowance 授权额度
GET /api/v1/open/web3/chains/{chain}/balances/erc20?contractAddress=...&ownerAddress=...open:web3:balance:read查询 ERC-20 余额,并返回 Token 名称、符号、精度和格式化余额
GET /api/v1/open/web3/chains/{chain}/transactions/{hash}open:web3:transaction:read查询交易详情,并返回常见 input 解码摘要
GET /api/v1/open/web3/chains/{chain}/transactions/{hash}/receiptopen:web3:transaction:read查询交易回执、执行状态、Gas 消耗和事件日志
GET /api/v1/open/web3/chains/{chain}/transactions/{hash}/confirmation?requiredConfirmations=12open:web3:transaction:read查询交易确认数和是否达到要求确认数,未传确认数时默认 12
POST /api/v1/open/web3/chains/{chain}/transactions/confirmations/batchopen:web3:transaction:read批量查询交易确认数和成功失败状态
POST /api/v1/open/web3/chains/{chain}/transaction-inputs/decodeopen:web3:transaction:read解码常见 EVM input,不访问链上节点
POST /api/v1/open/web3/chains/{chain}/event-logs/searchopen:web3:event:read按区块范围、合约地址和 topics 搜索事件日志,默认只查 latest 区块
POST /api/v1/open/web3/chains/{chain}/token-pricesopen:web3:price:read查询 Token 价格,可传入 providerName 选择价格 服务商
POST /api/v1/open/web3/chains/{chain}/transfer-plans/batchopen:web3:transfer:write预览批量转账计划,不接收私钥、不签名、不广播,只计算 nonce、gas、总额、余额是否足够和逐条计划
POST /api/v1/open/web3/chains/{chain}/transfers/batchopen:web3:transfer:write批量转账真实签名;默认 broadcast=false 只返回 rawTransactionbroadcast=true 时广播
POST /api/v1/open/web3/chains/{chain}/fund-collection-plansopen:web3:fund-collection:write预览资金归集计划,不接收私钥、不签名、不广播,只计算余额、nonce、gas、可归集金额和逐钱包计划
POST /api/v1/open/web3/chains/{chain}/fund-collectionsopen:web3:fund-collection:write资金归集真实签名;默认 broadcast=false 只返回 rawTransactionbroadcast=true 时广播

计划预览接口不会接收私钥,也不会生成 rawTransaction,适合在后台或第三方系统里先做金额、Gas、nonce 和余额检查。真实写交易接口会接收私钥用于本次签名。开放平台不会把私钥写入返回结果,当前调用日志也不记录请求体;生产环境仍建议只在可信服务端调用这类接口,前端浏览器不应直接提交私钥。

链信息接口会返回可公开 RPC URL,即 spring.open.web3.chains.<chain>.rpc-urls。私有 RPC URL,即 spring.open.web3.chains.<chain>.private-rpc-urls,永远不会通过开放平台返回。

Web3 配置类和运行时链配置快照中的私有 RPC 字段带有 JSON 忽略注解,防止配置对象被误序列化时泄露私有节点。开放平台仍采用 VO 白名单输出,不直接返回配置对象。

RPC URL 本身不是私钥,但私有或付费节点经常带有以下风险:

  • 付费节点 URL 可能包含项目 key,泄露后会被刷额度。
  • 公司自建节点、网关或内网地址会暴露基础设施拓扑。
  • 生产 RPC 被外部复用后,容易带来限流、稳定性和链上查询审计压力。
  • 某些节点服务商会把 project id 放在路径或 query 中,这类 URL 应按敏感配置处理。

Web3 接入样板

最小接入流程:

  1. 后台创建开放平台应用。
  2. 创建应用密钥,妥善保存 appKeyappSecret
  3. 授权 Web3 scope,例如 open:web3:network:readopen:web3:balance:readopen:web3:token:read
  4. 第三方服务端按签名规则生成请求头。
  5. 调用 /api/v1/open/web3/**

查询链信息:

http
GET /api/v1/open/web3/chains/bsc/info
X-Open-App-Key: app_xxx
X-Open-Timestamp: 1779379200000
X-Open-Nonce: 4c68f8d1-1d8c-4b7d-9fc6-4c77369f28aa
X-Open-Signature: <signature>
X-Open-Signature-Method: HMAC-SHA256

查询原生币余额:

http
GET /api/v1/open/web3/chains/bsc/balances/native?address=0x0000000000000000000000000000000000000000
X-Open-App-Key: app_xxx
X-Open-Timestamp: 1779379200000
X-Open-Nonce: 74c27e7d-a4c0-4a82-8701-138c0dd437ef
X-Open-Signature: <signature>
X-Open-Signature-Method: HMAC-SHA256

查询 Token 信息:

http
GET /api/v1/open/web3/chains/bsc/tokens/erc20?contractAddress=0x55d398326f99059fF775485246999027B3197955
X-Open-App-Key: app_xxx
X-Open-Timestamp: 1779379200000
X-Open-Nonce: 01925f73-e9c8-40da-8ed4-f93d52dc79ec
X-Open-Signature: <signature>
X-Open-Signature-Method: HMAC-SHA256

查询 Token 价格:

http
POST /api/v1/open/web3/chains/bsc/token-prices
Content-Type: application/json
X-Open-App-Key: app_web3_example
X-Open-Timestamp: 1779379200000
X-Open-Nonce: web3-price-nonce-1
X-Open-Signature: 61e3f6ec7dcda8b9931aa7eb6e526e32c55d55ae805e55ad59af08212a486821
X-Open-Signature-Method: HMAC-SHA256
json
{
  "tokenAddress": "0x0000000000000000000000000000000000000000",
  "tokenSymbol": "BNB",
  "tokenDecimals": 18,
  "quoteCurrency": "USD",
  "providerName": "pancake-v2"
}

该示例对应 scope 为 open:web3:price:read,签名中间值见上文固定夹具。鉴权、scope、nonce 和每分钟限流任一环节失败时,HTTP 状态仍为 200,业务 Result.code 分别返回 401403429;被限流时读取 Retry-AfterX-Open-RateLimit-Reset 做退避。只有通过鉴权、未被限流、进入业务 Controller 并成功写入可计费 usage event 的调用才消耗日 / 月配额;套餐存在单价时,请求进入 服务商 前还会做同币种账务余额预检。

批量转账计划预览:

http
POST /api/v1/open/web3/chains/bsc/transfer-plans/batch
Content-Type: application/json
X-Open-App-Key: app_xxx
X-Open-Timestamp: 1779379200000
X-Open-Nonce: 7b3c2b4e-0f80-4f66-8b19-450af51f376f
X-Open-Signature: <signature>
X-Open-Signature-Method: HMAC-SHA256
json
{
  "fromAddress": "0x0000000000000000000000000000000000000000",
  "transfers": [
    {
      "toAddress": "0x0000000000000000000000000000000000000001",
      "value": 1000000000000000000
    }
  ]
}

预览响应会返回 currentBalancerequiredBalancebalanceEnoughtotalGasCost 和逐条 nonce / gasLimit / gasCost,不会返回签名交易。

批量转账只签名不广播:

http
POST /api/v1/open/web3/chains/bsc/transfers/batch
Content-Type: application/json
X-Open-App-Key: app_xxx
X-Open-Timestamp: 1779379200000
X-Open-Nonce: d707ac53-6037-4675-86a9-a0e5c600bba2
X-Open-Signature: <signature>
X-Open-Signature-Method: HMAC-SHA256
X-Open-Idempotency-Key: transfer-batch-20260522-0001
json
{
  "privateKey": "0x...",
  "broadcast": false,
  "transfers": [
    {
      "toAddress": "0x0000000000000000000000000000000000000001",
      "value": 1000000000000000000
    }
  ]
}

broadcast=false 适合先对交易内容做二次审核;确认无误后再由可信后端使用 broadcast=true 发送链上交易。

资金归集计划预览:

http
POST /api/v1/open/web3/chains/bsc/fund-collection-plans
Content-Type: application/json
X-Open-App-Key: app_xxx
X-Open-Timestamp: 1779379200000
X-Open-Nonce: 0e74ef91-6c79-4f35-a912-6bcefb64164d
X-Open-Signature: <signature>
X-Open-Signature-Method: HMAC-SHA256
json
{
  "receiverAddress": "0x0000000000000000000000000000000000000009",
  "reserveAmount": 10000000000000000,
  "wallets": [
    {
      "fromAddress": "0x0000000000000000000000000000000000000001"
    }
  ]
}

归集预览会按钱包余额扣除 gasCostreserveAmount 后计算 amount;余额不足的钱包会返回 skipped=trueskipReason=insufficient_balance

Knowledge Ask 接入样板

Knowledge Ask 通过 Open Platform 暴露知识库问答能力,适合第三方服务端检索项目文档、产品说明或运营知识,也可以在管理员显式启用模型能力后请求受控生成。该能力复用开放平台签名、scope、限流、调用日志和计量事件,不使用 AI Gateway 的快捷 sk-... 鉴权。

调用方需要给应用授权 open:knowledge:ask scope。

http
POST /api/v1/open/knowledge/ask
Content-Type: application/json
X-Open-App-Key: app_knowledge_example
X-Open-Timestamp: 1779379200000
X-Open-Nonce: knowledge-ask-nonce-1
X-Open-Signature: 7ea36d16494b1ca5197b5230ca18aa8311db9f6b9a5ff530e8a5d2cbcbfd14c6
X-Open-Signature-Method: HMAC-SHA256
json
{
  "source": "project",
  "question": "跨链桥如何校验出金钱包余额",
  "topK": 3,
  "returnContext": true
}

默认 generate=false,接口只返回检索摘要和引用,不调用模型。需要生成式回答时可额外传:

json
{
  "generate": true,
  "provider": "simulated",
  "model": "spring-open-simulated",
  "temperature": 0.2,
  "maxTokens": 512
}

provider / model 是可选覆盖值;未传时由 Knowledge 与 AI Model 配置决定。真实模型未启用、模型不可用、护栏拒绝、prompt 过大或并发满时,接口仍返回检索摘要,并在 warnings 中标记降级原因。

该示例使用 appSecret=secret_knowledge_example,请求体紧凑 JSON 的 bodyHash 为:

text
3548fc37d7c3ed843657004612a0b4779c48e4de7ba76fd01d55e39fe5a0d18d

canonical string:

text
POST
/api/v1/open/knowledge/ask

1779379200000
knowledge-ask-nonce-1
3548fc37d7c3ed843657004612a0b4779c48e4de7ba76fd01d55e39fe5a0d18d

成功响应的 data 包含 answeranswerModegeneratedprovidermodelfinishReasonusagecontextscitationswarningshasContextmaxScoreanswerMode=generated 表示生成式回答成功,extractive 表示使用检索摘要,no-context 表示没有可用上下文。returnContext=false 时仍会返回回答摘要和引用,但不会返回上下文全文;需要展示引用来源时保持 returnContext=true

鉴权失败、scope 不足、nonce 重放、限流或配额用尽时仍按开放平台统一错误口径返回业务 Result.code;成功进入业务 Controller 的调用按普通开放 API 计量,默认 1 次成功调用计 1 unit。

钱包签名验签

钱包签名验签开放接口用于第三方应用确认“某个钱包地址确实签署了这段内容”。它不等同于 IAM 登录,不会创建账号,也不会消费业务 nonce。

personal_sign 验签请求:

http
POST /api/v1/open/web3/chains/bsc/wallet-signatures/personal/verify
Content-Type: application/json

{
  "walletAddress": "0x0000000000000000000000000000000000000000",
  "nonce": "login-20260522-0001",
  "message": "Sign in to Example App\nNonce: login-20260522-0001",
  "signature": "0x..."
}

EIP-712 typed data 验签请求:

http
POST /api/v1/open/web3/chains/bsc/wallet-signatures/typed-data/verify
Content-Type: application/json

{
  "walletAddress": "0x0000000000000000000000000000000000000000",
  "typedDataJson": "{\"types\":{\"EIP712Domain\":[]},\"primaryType\":\"Login\",\"domain\":{},\"message\":{}}",
  "signature": "0x..."
}

返回示例:

json
{
  "code": "200",
  "message": "Success",
  "data": {
    "chainName": "bsc",
    "chainType": "evm",
    "signatureType": "personal",
    "walletAddress": "0x0000000000000000000000000000000000000000",
    "normalizedAddress": "0x0000000000000000000000000000000000000000",
    "verified": true
  }
}

推荐客户端接入流程:

  1. 浏览器只负责连接钱包并拿到钱包签名。
  2. 前端把 walletAddressmessage / typedDataJsonsignature 发送到第三方应用自己的后端。
  3. 第三方应用后端使用 Open Platform appKey / appSecret 给请求签名,再调用本框架开放接口。
  4. 第三方应用后端收到 verified=true 后,再自行校验 nonce 是否存在、是否过期、是否已使用、业务域名和金额等字段是否可信。

不要把 Open Platform appSecret 放在浏览器、小程序或 App 客户端里。

浏览器侧钱包签名示例:

js
const [walletAddress] = await window.ethereum.request({ method: "eth_requestAccounts" });
const nonce = crypto.randomUUID();
const message = `Sign in to Example App\nNonce: ${nonce}`;
const signature = await window.ethereum.request({
  method: "personal_sign",
  params: [message, walletAddress]
});

await fetch("/your-server/api/wallet-signatures/verify", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ walletAddress, nonce, message, signature })
});

第三方服务端调用开放接口示例:

js
const body = JSON.stringify({ walletAddress, nonce, message, signature });
const path = "/api/v1/open/web3/chains/bsc/wallet-signatures/personal/verify";
const timestamp = Date.now();
const openNonce = crypto.randomUUID();
const openSignature = signOpenRequest({
  method: "POST",
  path,
  body,
  timestamp,
  nonce: openNonce,
  appSecret
});

const response = await fetch(`http://localhost:8000${path}`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Open-App-Key": appKey,
    "X-Open-Timestamp": String(timestamp),
    "X-Open-Nonce": openNonce,
    "X-Open-Signature": openSignature,
    "X-Open-Signature-Method": "HMAC-SHA256"
  },
  body
});

因此配置分层约定为:

  • rpc-urls:可公开节点,可用于开发者调试、开放平台链信息接口和文档示例。
  • private-rpc-urls:私有节点,运行时优先用于链上查询、支付确认、事件扫描和交易广播,不对外返回。

如果 private-rpc-urls 为空,运行时会回退使用 rpc-urls。生产环境建议把内部结算、支付确认或事件扫描使用的节点放在 private-rpc-urls,开放平台只给开发者返回可公开或专门限流的 rpc-urls

未注册 scope 的版本化开放请求会被过滤器拒绝;后续新增开放能力时,必须先注册路由 scope,再补充调用日志、限流、审计和文档。

实施顺序

  1. 后台应用、密钥、scope、调用日志模型已完成。
  2. 签名验签、防重放、IP 白名单、基础限流和开放平台访问过滤器已完成。
  3. Web3 v1 已接入链信息、余额、Token、授权、钱包、nonce、Gas、交易、事件、价格、钱包签名校验、批量转账计划、批量转账、资金归集计划和资金归集。
  4. 后续开放能力按能力域继续扩展,例如聚合支付、通知、存储、AI 或更多 Web3 写交易;资金类接口必须单独评估 scope、限流、审计、风控和计费。

当前状态

模块骨架、第一阶段数据底座、签名底层工具和后台管理 API 第一段已接入 Maven 聚合构建,当前包含:

  • open_platform_app
  • open_platform_package
  • open_platform_app_secret
  • open_platform_scope
  • open_platform_app_scope
  • open_platform_nonce
  • open_platform_call_log
  • open_platform_usage_event
  • open_platform_billing_account
  • open_platform_billing_ledger
  • open_platform_billing_statement
  • open_platform_billing_statement_line_item

当前模块已接入 application;这些表会通过 Liquibase includeAll 自动进入迁移范围。

当前已实现应用、应用套餐、套餐订单支付激活、应用密钥、应用 scope 授权、scope 字典、路由 scope 元数据、调用日志后台接口、计量事件、应用日 / 月配额、无套餐默认免费试用额度、用量聚合统计、成本快照统计、只读账单预览、正式月度账单落库、用户侧月度账单查询、账务账户和账务流水查询、内部幂等账务写入底座、AI 网关预占 / 实扣 / 失败释放、签名工具、HTTP 请求构建器、路由 scope 注册表、安全服务层、调用日志记录服务、内存限流服务、开放平台访问过滤器,以及 Web3 v1 链信息、余额、ERC-20 Token 信息、授权额度、钱包信息、nonce、Gas、交易、交易回执、确认数、事件日志、价格查询、钱包签名校验、批量转账计划、批量转账、资金归集计划和资金归集开放 API。下一阶段建议围绕更多 Web3 Token/NFT 写交易、聚合支付开放能力、支付补款 / 自动续费、更完整限流熔断和真实外部工具联调继续增强。

Released under the Apache License 2.0.