Files
x6g2 2924ec30d8
构建并部署 / 检测变更 (push) Successful in 6s
构建并部署 / 打包部署 starapi-admin (push) Skipped
构建并部署 / 打包部署 portal-ui (push) Skipped
构建并部署 / 打包部署 admin-ui (push) Skipped
构建并部署 / 打包部署 starapi-gateway (push) Successful in 1m40s
refactor: 网关配置敏感信息改为环境变量注入
2026-11-10 00:04:23 +08:00
..
2026-11-01 14:14:26 +08:00
2026-11-01 14:14:26 +08:00

starapi-gateway 开放接口网关

开放 API 的独立调用入口:外部调用方只访问网关,网关完成鉴权、限流、幂等、转发、扣费与调用日志采集。 网关是独立 Spring Boot 应用(Jetty 容器),不连接业务库,路由/凭证/授权全部来自 Redis 缓存, 扣费与调用日志通过内网 HTTP 回传给 starapi-admin。

一、模块定位与调用链

外部调用方
    │  /api/v1/**(X-App-Key / X-Timestamp / X-Nonce / X-Signature)
    ▼
┌──────────────────────────────────────────────┐
│ starapi-gateway (8082)                        │
│  1. route   : openapi:route:path 匹配 → openapi:route
│  2. 接口维护中拦截                              │
│  3. 应用凭证 → 应用状态 → 开发者账号状态       │
│  4. IP 白名单                                  │
│  5. 时间戳偏移 → nonce 防重放 → 签名校验       │
│  6. 接口授权(openapi:app:auth:{appId})       │
│  7. 请求ID幂等(回放 or 拒绝)                  │
│  8. QPS 令牌桶 + 应用日调用限额                │
│  9. 计费预检(免费额度/次数包/钱包余额)        │
│ 10. 转发上游(api_info.upstream_url)          │
│ 11. 上游成功(2xx)后扣费 → 回写响应(含余额头)   │
│ 12. 异步写调用日志                            │
└───────┬──────────────────────────┬────────────┘
        │ Redis 只读缓存            │ 内网 HTTP(X-Internal-Token)
        ▼                          ▼
   openapi 发布的缓存          starapi-admin (8080)
                               /internal/billing/precheck
                               /internal/billing/deduct
                               /internal/calllog/record
  • 入口路径:gateway.include-paths(默认仅 /api/v1/**)匹配的请求才进入网关流程,其余请求直接交给后续过滤器与 MVC。
  • 过滤器链:链路按阶段拆成 9 个 OncePerRequestFilter,由类上的 @Order 注解排序(顺序常量见 GatewayConstants.FILTER_ORDER_*,以 10 为步长,计费预检插入在 165 位于限流与转发之间),任一阶段失败即写出统一错误响应并结束链路:
顺序 过滤器 职责
100 OpenApiContextFilter 建立请求上下文(requestId/方法/规范化路径/IP/请求体/开始时间);位于最外层,结束后统一记录调用日志
110 OpenApiRouteFilter 路由缓存匹配、接口维护中拦截
120 OpenApiCredentialFilter 应用凭证、应用状态、开发者账号状态、IP 白名单
130 OpenApiSignFilter 时间戳偏移、nonce 防重放、HMAC-SHA256 签名校验
140 OpenApiAuthFilter 应用对接口的授权校验
150 OpenApiIdempotencyFilter 请求ID幂等:命中缓存回放、并发请求抢占处理锁
160 OpenApiRateLimitFilter QPS 令牌桶、应用日调用限额
165 OpenApiBillingFilter 转发前扣费预检:余额/次数包/免费额度均不可用时直接返回 40201,不调用上游
170 OpenApiForwardFilter 转发上游、2xx 且业务码 200 后扣费、回写响应(含账户余额头)、缓存成功响应;扣费失败时返回 40201/50002 错误响应且不回写上游结果
  • 公共逻辑(路径判定、上下文读取、统一错误响应、异常信息整理)收敛在 AbstractOpenApiFilter,各阶段过滤器只实现自身校验。
  • 网关错误统一返回 JSON:{"code":业务码,"msg":"提示","requestId":"链路ID","timestamp":毫秒时间戳}。
  • 请求ID(X-Request-Id)同时是幂等键、扣费幂等键与调用日志主键;调用方不传或格式非法(超过 64 字符、含字母数字下划线中划线以外的字符)时由网关生成 32 位无横线 UUID。

二、请求头约定

请求头 必填 说明
X-App-Key 是 应用 AppKey,用于读取应用凭证缓存;缺失或未命中返回 40101
X-Timestamp 是 毫秒级时间戳字符串,默认允许与服务器时间相差 ±300 秒,超出返回 40102
X-Nonce 是 请求随机串(建议 16~32 位),同一 AppKey 下只允许使用一次,重复返回 40103
X-Signature 是 签名,64 位小写十六进制 HMAC-SHA256,见第三节;缺失或校验失败返回 40104
X-Request-Id 否 链路追踪ID;缺省由网关生成,用于幂等、扣费与日志关联
Content-Type 按接口 转发时原样传给上游;请求体按原始字节参与签名,GET 等空请求体按空数组处理

网关在签名校验通过、开始转发时还会向上游追加以下请求头,调用方无需也不能伪造:

转发头 说明
X-Request-Id 本次调用的链路追踪ID(调用方未传时为网关生成值)
X-Gateway-App-Id 调用方应用ID
X-Gateway-User-Id 调用方所属开发者用户ID
X-Gateway-Client-Ip 网关解析出的调用方真实IP

鉴权头与 HTTP 逐跳头(X-App-Key、X-Timestamp、X-Nonce、X-Signature、Host、Content-Length、Connection 等)不会透传给上游。

网关附加响应头

除原样回传上游响应头外,网关还会追加以下自有响应头(上游返回的同名头会被丢弃,避免伪造):

响应头 出现时机 说明
X-Gateway-Balance 上游返回 2xx 且扣费结果带账户余额 本次调用扣费后的账户余额(单位:元,保留 4 位小数)。免费额度、次数包、钱包余额三种扣费方式均返回;扣费失败、上游非 2xx 或开发者未开户时不返回
X-Gateway-Replay 命中请求ID幂等回放 固定为 true,表示本次未重复调用上游、未重复扣费;此时 X-Gateway-Balance 为首次调用扣费后的余额快照

扣费失败时不再返回上游结果,也不使用告警头:网关直接返回 40201(账户不可支付)或 50002(计费接口异常)错误响应,调用方重试会重新调用上游。旧版本用于提示扣费失败的 X-Gateway-Warning: charge-failed 响应头已取消。账户余额取自扣费接口返回的 balanceAfter,网关不额外查询钱包;转发前预检本身只读,不改变余额。浏览器跨域调用时需服务端通过 Access-Control-Expose-Headers 放行上述头才能被 JS 读取。

三、签名算法

签名实现见 com.starsee.starapi.gateway.util.OpenApiSignUtils,算法固定、区分大小写,调用方必须完全一致。

3.1 签名串

signContent = appKey + "\n"
            + timestamp + "\n"
            + nonce + "\n"
            + METHOD + "\n"
            + path + "\n"
            + md5Hex(body)

signature = hex( HMAC-SHA256( key = appSecret, data = signContent ) )   // 小写十六进制
段 规则
appKey 请求头 X-App-Key 原值
timestamp 请求头 X-Timestamp 原值(毫秒时间戳字符串,不重新格式化)
nonce 请求头 X-Nonce 原值
METHOD HTTP 方法,统一大写(get → GET)
path 请求路径,不含查询参数;以 / 开头,末尾多余的 / 会被去掉(根路径 / 保留);即 /api/v1/hello?x=1 参与签名的是 /api/v1/hello。为兼容按原始 URI 签名的调用方,网关会先用规范化路径校验,失败后再用保留尾斜杠的原始路径校验一次
body 原始请求体字节,空请求体按空数组计算;空数组的 MD5 为 d41d8cd98f00b204e9800998ecf8427e

补充规则:

  • appSecret 作为 HMAC 密钥,按 UTF-8 编码;签名结果按 小写十六进制 传输,大小写不匹配视为失败。
  • 各段之间只用一个 \n 分隔,末段(MD5)之后没有换行。
  • 网关先校验时间戳、再占用 nonce、最后校验签名;签名失败时 nonce 已被占用,用同一 nonce 重试会得到 40103,重试请更换 nonce。
  • 签名开关 gateway.sign.enabled=false 时整段校验(时间戳/nonce/签名)全部跳过,仅用于临时排查。

3.2 curl + openssl 示例

export APP_KEY='your_app_key'
export APP_SECRET='your_app_secret'
export METHOD='POST'
export API_PATH='/api/v1/example/hello'
export BODY='{"name":"starapi"}'

TS=$(date +%s%3N)                                  # 毫秒时间戳
NONCE=$(openssl rand -hex 16)
BODY_MD5=$(printf '%s' "$BODY" | openssl dgst -md5 -r | cut -d' ' -f1)

SIGN_CONTENT="${APP_KEY}
${TS}
${NONCE}
${METHOD}
${API_PATH}
${BODY_MD5}"

SIGNATURE=$(printf '%s' "$SIGN_CONTENT" | openssl dgst -sha256 -hmac "$APP_SECRET" -r | cut -d' ' -f1)

curl -sS -X "$METHOD" "http://127.0.0.1:8082${API_PATH}" \
  -H 'Content-Type: application/json' \
  -H "X-App-Key: ${APP_KEY}" \
  -H "X-Timestamp: ${TS}" \
  -H "X-Nonce: ${NONCE}" \
  -H "X-Signature: ${SIGNATURE}" \
  -H "X-Request-Id: $(openssl rand -hex 16)" \
  --data-binary "$BODY"

printf '%s' "$SIGN_CONTENT" 会完整保留换行;签名串必须严格是 6 段、5 个 \n。

3.3 Java(Hutool HMac)示例

网关与调用方使用同一套 Hutool API,可直接对照 OpenApiSignUtils#sign:

import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.crypto.digest.HMac;
import cn.hutool.crypto.digest.HmacAlgorithm;

import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.UUID;

String appKey = "your_app_key";
String appSecret = "your_app_secret";
String method = "POST";
String path = "/api/v1/example/hello";
byte[] body = "{\"name\":\"starapi\"}".getBytes(StandardCharsets.UTF_8);

String timestamp = String.valueOf(System.currentTimeMillis());   // 毫秒
String nonce = UUID.randomUUID().toString().replace("-", "");

String signContent = appKey + "\n"
    + timestamp + "\n"
    + nonce + "\n"
    + method.toUpperCase(Locale.ROOT) + "\n"
    + path + "\n"
    + DigestUtil.md5Hex(body);

HMac hmac = new HMac(HmacAlgorithm.HmacSHA256, appSecret.getBytes(StandardCharsets.UTF_8));
String signature = hmac.digestHex(signContent);   // 小写十六进制

四、错误码

业务码与 HTTP 状态码同时返回,来源 GatewayErrorCode。

业务码 HTTP 枚举 含义
40101 401 APP_CREDENTIAL_INVALID 应用凭证无效,请检查AppKey
40102 401 TIMESTAMP_INVALID 请求时间戳无效或已超出允许的时间偏移
40103 401 NONCE_REPLAYED 请求nonce已被使用,请勿重复提交
40104 401 SIGNATURE_INVALID 请求签名校验未通过(缺参数时提示“请求缺少签名参数”)
40201 402 BILLING_NOT_PAYABLE 账户余额或可用次数不足:转发前预检拒绝,或预检后扣费时确认不可支付;msg 为具体原因(余额不足/仅支持套餐包/钱包冻结等)
40301 403 APP_DISABLED 应用已被禁用
40302 403 USER_DISABLED 开发者账号已被停用或冻结
40303 403 IP_NOT_ALLOWED 调用来源IP不在应用白名单内
40304 403 API_NOT_AUTHORIZED 应用未开通该接口
40401 404 ROUTE_NOT_FOUND 开放接口不存在或已下线
40901 409 REQUEST_DUPLICATE 相同的请求ID正在处理或已完成,请勿重复提交
42901 429 RATE_LIMITED 调用频率超过接口QPS限制
42902 429 DAILY_LIMIT_EXCEEDED 调用次数已超过应用日调用限额
50001 500 GATEWAY_ERROR 网关内部错误
50002 500 BILLING_FAILED 上游已调用但本次扣费未完成(内网计费接口异常/超时等),不返回上游结果,可重试
50201 502 UPSTREAM_ERROR 上游服务调用失败
50301 503 API_MAINTAIN 开放接口维护中,请稍后重试
50401 504 UPSTREAM_TIMEOUT 上游服务响应超时

共 18 个业务码,全部来自 GatewayErrorCode。按 HTTP 状态可做通用重试判断,按业务码可精确定位原因。

五、配置说明

配置前缀 gateway,对应 GatewayProperties;application.yml 已给出默认值,application-dev.yml / application-prod.yml 只覆盖 Redis 与 admin 地址。

配置项 默认值 说明
gateway.enabled true 网关过滤器总开关;关闭后 /api/v1/** 不再进入网关流程
gateway.include-paths /api/v1/** 进入网关鉴权与转发的路径匹配规则(Spring PathPattern)
gateway.sign.enabled true 时间戳/nonce/签名校验开关
gateway.sign.timestamp-skew-seconds 300 时间戳允许偏移(秒)
gateway.sign.nonce-ttl-seconds 300 nonce 防重放缓存时长(秒),应不小于时间戳偏移
gateway.rate-limit.enabled true QPS 限流开关
gateway.rate-limit.qps-interval-seconds 1 令牌桶统计窗口(秒),窗口内允许请求数即 QPS 上限
gateway.rate-limit.daily-limit-enabled true 应用日调用限额开关
gateway.idempotent.enabled true 请求ID幂等开关
gateway.idempotent.lock-ttl-seconds 60 “处理中”锁时长(秒)
gateway.idempotent.response-ttl-seconds 600 成功响应回放缓存时长(秒)
gateway.idempotent.max-response-cache-bytes 262144 可回放响应体上限(256KB),超出只去重不回放
gateway.billing.precheck-enabled true 转发前扣费预检开关;关闭后恢复“先转发、后扣费、扣费失败仅告警”的旧行为
gateway.internal.admin-base-url http://127.0.0.1:8080 admin 内网访问地址
gateway.internal.token 空(application.yml 取 ${INTERNAL_API_TOKEN:starapi-internal-token}) 内网令牌,必须与 admin 的 internal.api.token 一致
gateway.internal.connect-timeout-ms 2000 内网接口连接超时(毫秒)
gateway.internal.read-timeout-ms 5000 内网接口读超时(毫秒)
gateway.internal.deduct-path /internal/billing/deduct 扣费接口路径(取 InternalApiConstants)
gateway.internal.precheck-path /internal/billing/precheck 预检接口路径(取 InternalApiConstants)
gateway.internal.call-log-path /internal/calllog/record 调用日志接口路径(取 InternalApiConstants)
gateway.internal.call-log-pool-size 2 调用日志异步线程数(最大线程数不小于 4)
gateway.internal.call-log-queue-capacity 1024 调用日志异步队列容量,队列满由调用线程兜底执行
gateway.forward.connect-timeout-ms 2000 上游连接超时(毫秒);读超时按接口 api_info.timeout_ms,未配置时默认 5000ms

限流阈值不在网关配置里:QPS 取「应用 api_app.qps_limit(默认 50)」与「接口 api_info.qps_limit(默认 100)」的较小值;日限额取应用 api_app.daily_call_limit,未配置(≤0)表示不限。

六、缓存与数据来源

网关只读 Redis 缓存、不连业务库。缓存由 openapi 模块的 ApiRouteCacheServiceImpl 在接口/应用/授权变更时发布,必要时可触发全量刷新。

6.1 业务缓存(openapi 模块发布,网关只读)

缓存键 结构 内容 发布方
openapi:route Hash apiId(字符串) → ApiRouteDTO(上游地址、超时、计费方式、价格、免费次数、QPS、状态等) ApiRouteCacheServiceImpl#publishRoute
openapi:route:path Hash METHOD:path → apiId(字符串);METHOD 为 GET/POST/... 或 ALL 同上
openapi:app:credential Hash appKey → AppCredentialDTO(appId、userId、appSecret、IP白名单、QPS/日限额、应用状态、开发者账号状态) ApiRouteCacheServiceImpl#doRefreshAppCredential
openapi:app:auth:{appId} Hash apiId(字符串) → AppAuthDTO(授权状态、剩余免费次数) ApiRouteCacheServiceImpl#writeAppAuthCache

路由匹配顺序:先按请求方法精确匹配 openapi:route:path,未命中再回退 ALL:path;路径先归一化(去掉查询参数、去掉末尾 /)。 接口状态为「维护中(2)」也会进入路径索引,由网关在命中路由后返回 50301;已下线接口不在索引中,表现为 40401。

状态取值(与生产端一致):应用状态 0 正常、开发者账号状态 0 正常、接口状态 2 维护中、授权状态 0 已开通。

6.2 网关自身运行缓存(网关读写的限流/幂等键)

缓存键 类型 用途 过期
gateway:nonce:{appKey}:{nonce} String(setIfAbsent) nonce 防重放占用 gateway.sign.nonce-ttl-seconds
gateway:qps:{appId}:{apiId}:{qps} Redisson 令牌桶(OVERALL) 应用+接口维度 QPS 限流 3600 秒
gateway:daily:{appId}:{yyyyMMdd} AtomicLong 应用日调用计数 首次自增起 26 小时
gateway:idempotent:lock:{requestId} String 1(setIfAbsent) 相同请求ID的处理中锁 gateway.idempotent.lock-ttl-seconds
gateway:idempotent:response:{requestId} 对象 首次成功响应,用于幂等回放 gateway.idempotent.response-ttl-seconds

七、计费与调用日志

  • 转发前预检:限流之后、转发上游之前调用 admin POST /internal/billing/precheck,同样携带 appId、userId、apiId、chargeType、pricePerCall,由 admin 按「免费试用额度 → 次数包 → 钱包余额」只读判断本次调用是否存在可用的付费来源。判定不可支付时网关直接返回 40201(msg 为具体原因),不调用上游、不产生任何调用成本;免费接口与单价不大于 0 的按次接口不会扣钱包,跳过预检;预检接口不可用时降级继续转发(不再拦截),由扣费阶段兜底:扣费失败返回 50002,不回写上游结果。可通过 gateway.billing.precheck-enabled=false 整体关闭。
  • 扣费时机:上游返回 2xx 且响应体业务码为 200 时,在回写调用方响应之前调用 admin POST /internal/billing/deduct,携带 appId、userId、apiId、chargeType、pricePerCall,并以 requestId 作为幂等键;admin 侧同一 requestId 不会重复扣费。
  • 扣费失败不回写响应:预检通过(或预检被关闭、预检接口降级)后仍可能因并发消耗、admin 内网接口短暂异常导致扣费失败,此时网关丢弃已拿到的上游结果并返回错误响应:admin 明确返回账户不可支付(余额不足、钱包冻结、未开户、仅支持套餐包等)→ 40201 + 业务原因;内网计费接口异常/超时 → 50002。本次响应不缓存、不返回 X-Gateway-Balance,调用日志 isCharge=0 并写入 error_msg;上游非 2xx 或业务码非 200 时不做扣费,按上游结果返回。
  • 返回账户余额:上游 2xx 且扣费结果带余额时,网关在响应头回写 X-Gateway-Balance(单位元、4 位小数),数据直接取自扣费接口返回的 balanceAfter,不额外查询钱包;扣费失败、上游非 2xx 或未开户时不返回该头。
  • 调用日志:请求结束后异步调用 POST /internal/calllog/record(线程池 gatewayCallLogExecutor,@Async),记录 requestId、appId、userId、apiId、请求/响应时间、耗时、HTTP 状态、上游业务码 code、客户端 IP、是否计费、扣费方式/次数/金额、余额、错误信息;缺路由或凭证信息时跳过(admin 侧主键三元组校验要求齐全)。
  • 失败也记日志:所有拒绝分支(鉴权、限流、上游失败等)都会写调用日志,error_msg 按 api_call_log.error_msg 长度(512)截断。
  • 幂等回放响应头:命中首次成功响应回放时返回 X-Gateway-Replay: true,并回写首次调用缓存的 X-Gateway-Balance 余额快照;此时不会再扣费、不会再调用上游。
  • 计数时机:QPS/日限额在转发上游之前占用,因此上游失败(502/504)与预检拒绝(40201)也会计入调用量与 QPS。

八、部署与本地启动

8.1 关键参数

项 值
进程端口 8082(server.port,Docker 中由 SERVER_PORT 覆盖)
应用名 StarAPI-Gateway
上下文路径 /
构建产物 starapi-gateway/target/starapi-gateway.jar
profile 由 Maven profile @profiles.active@ 注入,默认 dev(local 无对应网关配置文件,需显式指定 dev/prod)
内网令牌环境变量 INTERNAL_API_TOKEN,默认 starapi-internal-token

8.2 admin 侧配套要求

网关依赖 admin 的内网接口,admin 的 application.yml 必须满足(当前仓库已配置):

配置 值 说明
security.excludes 包含 /internal/** 内网接口放行 Sa-Token 登录校验,改由内网令牌校验
internal.api.enabled true 内网接口鉴权开关
internal.api.token ${INTERNAL_API_TOKEN:starapi-internal-token} 必须与网关 gateway.internal.token 完全一致,否则扣费/日志全部失败
internal.api.ip-white-list [] 允许来源 IP,空表示不限制
xss.excludeUrls 包含 /internal/** 内网 JSON 调用不做 XSS 清洗

内网接口:POST /internal/billing/precheck、POST /internal/billing/deduct、POST /internal/calllog/record,请求头 X-Internal-Token。

升级顺序:先升级 admin(提供 /internal/billing/precheck),再升级网关。旧版 admin 上预检会 404,网关按降级策略继续转发并输出 WARN,不会阻断调用。

8.3 本地启动

# 1. 打包(网关依赖 starapi-api 与 common 模块,首次需一起构建)
mvn -pl starapi-gateway -am clean package -Pdev

# 2. 启动(默认 profile=dev,需能连到 dev 环境 Redis)
INTERNAL_API_TOKEN=starapi-internal-token \
  java -jar starapi-gateway/target/starapi-gateway.jar --server.port=8082

# 3. 健康检查
curl -s http://127.0.0.1:8082/actuator/health

本地没有 admin 时网关仍可转发上游:转发前预检失败会降级放行,转发成功后扣费失败会返回 50002 错误响应(不回写上游结果),调用日志同样失败(error 日志)。 启动前可用 openapi 管理端触发一次“全量刷新网关缓存”,否则路由/凭证缓存为空,请求会直接返回 40101 / 40401。

8.4 Docker 与 nginx

  • 镜像:starapi/starapi-gateway:6.0.0;Dockerfile 构建上下文为 starapi-gateway/(ADD ./target/starapi-gateway.jar ./app.jar),工作目录 /starapi/gateway,日志写 /starapi/gateway/logs(logback ./logs),EXPOSE 8082。
  • IDEA 已提供 .run/starapi-gateway.run.xml(dockerfile 构建,仅构建镜像)。
  • script/docker/docker-compose.yml 新增 starapi-gateway 服务:network_mode: "host"、TZ=Asia/Shanghai、SERVER_PORT=8082、日志卷 /docker/gateway/logs/:/starapi/gateway/logs/、privileged: true,位于 starapi-server2 之后。
# 构建镜像(在仓库根目录执行)
docker build -t starapi/starapi-gateway:6.0.0 starapi-gateway

# 只启动/更新网关
docker compose -f script/docker/docker-compose.yml up -d starapi-gateway
  • script/docker/nginx/conf/nginx.conf 已增加 upstream gateway { server 127.0.0.1:8082; } 与 location /api/v1/ 反代:
    • proxy_pass http://gateway; 不带 URI,保留 /api/v1 前缀;
    • proxy_read_timeout 300s、proxy_buffering off;
    • location 内不改写、不丢弃 X-App-Key、X-Timestamp、X-Nonce、X-Signature、X-Request-Id,nginx 默认原样透传客户端请求头,签名依赖这些头;
    • 已设置 Host、X-Real-IP、X-Forwarded-For,网关据此解析真实客户端 IP。

九、已知限制与后续

  • 未引入 Spring Cloud Gateway:仓库无 spring-cloud 依赖栈,网关基于 Spring Boot Web + Jetty + RestClient 自行实现转发,保持与既有技术栈一致;上游响应体当前整体读入内存再回写(非流式),大文件下载/SSE 场景需要后续改造。
  • 调用日志暂落 MySQL 单表(api_call_log 不分表、不入 Elasticsearch),日志量上来后再做归档/分表/入 ES。
  • 限流基于 Redis 令牌桶,按「应用 + 接口 + 有效阈值」维度;键中包含有效 QPS,调整配置后自动切换新桶,多实例共享同一份计数。
  • 网关不直连业务库,路由/凭证/授权强依赖 openapi 侧缓存刷新;缓存未刷新或发布失败时只能返回 40101/40401,需在 openapi 管理端重新触发刷新。
  • 幂等只缓存 2xx 成功响应,失败响应不缓存;同一 requestId 并发时只有一个请求执行,其余返回 40901。
  • 扣费失败不回写上游结果:上游已执行但扣费失败(如预检后余额被并发消耗、计费接口异常)时,网关丢弃上游结果并返回 40201/50002;调用方重试会再次调用上游,可能产生重复的上游成本。
  • 幂等回放在验签与授权之后命中:相同 requestId 的重复请求会先完成凭证、IP、签名与接口授权校验,命中首次成功响应后直接回放(返回 X-Gateway-Replay: true),不再占用 QPS、不再扣费、不再调用上游;处理中的并发重复请求返回 40901。
  • QPS/日限额先占后转发,上游失败也计入调用量;如需“仅成功调用计数”,需要把限流挪到转发成功之后并调整令牌桶语义。
  • 网关不承载管理页面与后台接口,运营侧能力全部在 admin(openapi / billing / calllog)。