GW 通用服务网关

common-server-gateway

通用服务网关

为 closet 等上层应用提供稳定的短信、天气等能力接口,把供应商切换、密钥、签名、错误归一和调用日志留在网关层。

common-server-gateway 架构图
上层应用调用稳定 capability,网关负责 provider adapter 和日志。
当前测试 Base URL https://common-server-gateway-backend-230591-5-1404418380.sh.run.tcloudbase.com
本地 Docker Base URL http://localhost:5110
调用方式 服务端保存 App Token,通过 Authorization: Bearer 调用 /api/v1

Upper App Guide

上层应用对接方式

上层应用只需要接入这里列出的稳定网关 API。第三方供应商的 base URL、密钥、签名、模板、错误码和切换逻辑都留在网关内部。

1

准备环境变量

在上层服务端配置网关地址和 App Token,不要放到前端、移动端、浏览器存储或公开仓库。

API_GATEWAY_BASE_URL=https://common-server-gateway-backend-230591-5-1404418380.sh.run.tcloudbase.com
API_GATEWAY_APP_TOKEN=<app_token>
2

统一请求头

每个业务请求都必须带 App Token。建议同时传入上层业务请求 ID,便于跨系统排查。

Authorization: Bearer <app_token>
X-Request-ID: closet_req_20260524_001
3

按 capability 调用

业务只调用语义化路径,不关心当前路由到 mock、真实天气供应商,还是未来的短信供应商。

POST /api/v1/sms/verification-code/send
POST /api/v1/weather/query

Base URL

环境 Base URL 说明
CloudBase 测试 https://common-server-gateway-backend-230591-5-1404418380.sh.run.tcloudbase.com 当前已发布的测试网关后端。
本地 Docker http://localhost:5110 本地调试时由 Docker Compose 暴露。
生产 尚未配置 生产环境启用时需要独立环境、独立数据库和独立密钥。

所有上层应用可用路径

能力 方法 路径 入参位置 用途
capability.list GET /api/v1/capabilities 查看当前 App Token 已授权且启用的 capability。
capability.docs GET /api/v1/capabilities/{capability_key}/docs Path 读取指定 capability 的代码内契约、示例、curl 和 Markdown 文档。
sms.verification_code.send POST /api/v1/sms/verification-code/send JSON body 发送由上层应用生成、保存和校验的短信验证码。
weather.current.get POST /api/v1/weather/query JSON body 按经纬度获取当前天气或天气预报。

认证与授权规则

  • 每次请求都带 Authorization: Bearer <app_token>
  • App Token 只放在上层服务端密钥配置中,不下发到浏览器、小程序、App 或静态页面。
  • 后台按 capability 授权应用;未授权会返回 capability_forbidden
  • App Client 禁用、Token 轮换或能力授权变更都会立即影响业务调用。
  • 响应头始终返回 X-Request-ID,业务侧应记录它用于排查。
curl "${API_GATEWAY_BASE_URL}/api/v1/capabilities" \
  -H "Authorization: Bearer ${API_GATEWAY_APP_TOKEN}" \
  -H "X-Request-ID: upper_app_req_001"

动态读取能力文档

如果上层服务希望在发布前校验接口契约,可以读取 capability 文档端点。这个端点返回 endpoint、request location、input schema、output schema、request example、response example、curl example 和 Markdown。

curl "${API_GATEWAY_BASE_URL}/api/v1/capabilities/weather.current.get/docs" \
  -H "Authorization: Bearer ${API_GATEWAY_APP_TOKEN}"

Capabilities

稳定能力接口

当前稳定开放的 capability 契约由后端代码固定,Provider 切换不会改变上层应用看到的入参和出参结构。

SMS

sms.verification_code.send

POST /api/v1/sms/verification-code/send

发送由上层应用生成和校验的验证码。网关不保存原始验证码,日志只保留手机号脱敏值和 code length。

入参位置
JSON body
Content-Type
application/json
认证
Authorization: Bearer <app_token>

入参 JSON Schema

{
  "type": "object",
  "required": ["phone", "code", "scene"],
  "additionalProperties": false,
  "properties": {
    "phone": {
      "type": "string",
      "minLength": 4,
      "maxLength": 32
    },
    "code": {
      "type": "string",
      "minLength": 4,
      "maxLength": 12
    },
    "scene": {
      "type": "string",
      "minLength": 2,
      "maxLength": 80
    },
    "template_params": {
      "type": "object",
      "additionalProperties": {
        "type": "string"
      }
    }
  }
}

出参 JSON Schema

{
  "type": "object",
  "required": [
    "request_id",
    "success",
    "capability",
    "provider",
    "provider_request_id",
    "sent_at"
  ],
  "properties": {
    "request_id": {"type": "string"},
    "success": {"type": "boolean"},
    "capability": {
      "type": "string",
      "const": "sms.verification_code.send"
    },
    "provider": {"type": "string"},
    "provider_request_id": {"type": "string"},
    "sent_at": {
      "type": "string",
      "format": "date-time"
    }
  }
}

示例出参 JSON

{
  "request_id": "req_xxx",
  "success": true,
  "capability": "sms.verification_code.send",
  "provider": "mock-sms",
  "provider_request_id": "mock_sms_req_xxx",
  "sent_at": "2026-05-22T12:00:00Z"
}
Weather

weather.current.get

POST /api/v1/weather/query

按经纬度查询当前天气或天气预报,并返回统一的 query、location、weather、observed_at 或 forecasts 字段。

入参位置
JSON body
参数规则
传 latitude、longitude、type 和 count;不接受 city/adcode
认证
Authorization: Bearer <app_token>

入参 JSON Schema

{
  "type": "object",
  "required": ["latitude", "longitude", "type", "count"],
  "additionalProperties": false,
  "properties": {
    "latitude": {
      "type": "number",
      "minimum": -90,
      "maximum": 90
    },
    "longitude": {
      "type": "number",
      "minimum": -180,
      "maximum": 180
    },
    "type": {
      "type": "string",
      "enum": ["current", "hourly", "daily"]
    },
    "count": {
      "type": "integer",
      "minimum": 1,
      "maximum": 168
    }
  }
}

current 会忽略 count 并固定返回当前天气;hourlycount 表示未来小时数;dailycount 表示未来天数。高德不提供小时级天气,hourly 会按 24 小时向上换算成逐日预报天数,例如 24 小时返回 1 天,48 小时返回 2 天。

出参 JSON Schema

{
  "type": "object",
  "required": [
    "request_id",
    "capability",
    "provider",
    "query",
    "location"
  ],
  "properties": {
    "request_id": {"type": "string"},
    "capability": {
      "type": "string",
      "const": "weather.current.get"
    },
    "provider": {"type": "string"},
    "query": {
      "type": "object",
      "required": ["latitude", "longitude", "type", "count"],
      "properties": {
        "latitude": {"type": "number"},
        "longitude": {"type": "number"},
        "type": {"type": "string"},
        "count": {"type": "integer"}
      }
    },
    "location": {
      "type": "object",
      "properties": {
        "country": {"type": ["string", "null"]},
        "province": {"type": ["string", "null"]},
        "city": {"type": ["string", "null"]},
        "adcode": {"type": ["string", "null"]},
        "latitude": {"type": "number"},
        "longitude": {"type": "number"}
      },
      "additionalProperties": true
    },
    "weather": {
      "type": "object",
      "properties": {
        "text": {"type": ["string", "null"]},
        "temperature_c": {"type": ["number", "null"]},
        "humidity": {"type": ["number", "null"]},
        "wind_direction": {"type": ["string", "null"]},
        "wind_speed": {"type": ["string", "null"]},
        "wind_power": {"type": ["string", "null"]}
      },
      "additionalProperties": true
    },
    "observed_at": {
      "type": "string",
      "format": "date-time"
    },
    "forecasts": {"type": "array"}
  }
}

示例出参 JSON

{
  "request_id": "req_xxx",
  "capability": "weather.current.get",
  "provider": "mock-weather",
  "query": {
    "latitude": 31.23,
    "longitude": 121.47,
    "type": "current",
    "count": 1
  },
  "location": {
    "country": "中国",
    "province": "上海市",
    "city": "上海市",
    "adcode": "310000",
    "latitude": 31.23,
    "longitude": 121.47
  },
  "weather": {
    "text": "晴",
    "temperature_c": 24.0,
    "humidity": 58,
    "wind_direction": "东风",
    "wind_power": "2级"
  },
  "observed_at": "2026-05-22T12:00:00Z"
}

Examples

直接可用的调用示例

把示例里的 ${API_GATEWAY_BASE_URL}${API_GATEWAY_APP_TOKEN} 换成上层服务端环境变量即可。不要在浏览器控制台或前端代码中暴露真实 token。

发送短信验证码

验证码由上层应用生成、保存、过期和校验;网关只负责发送和记录脱敏日志。

curl -X POST "${API_GATEWAY_BASE_URL}/api/v1/sms/verification-code/send" \
  -H "Authorization: Bearer ${API_GATEWAY_APP_TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Request-ID: sms_req_001" \
  -d '{
    "phone": "13800138000",
    "code": "123456",
    "scene": "closet_login",
    "template_params": {
      "product": "closet"
    }
  }'

查询当前天气

只传经纬度,不传城市或 adcode。切换 type 可查当前天气、未来小时数或未来天数。

curl -X POST "${API_GATEWAY_BASE_URL}/api/v1/weather/query" \
  -H "Authorization: Bearer ${API_GATEWAY_APP_TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Request-ID: weather_req_001" \
  -d '{
    "latitude": 31.23,
    "longitude": 121.47,
    "type": "hourly",
    "count": 48
  }'

Errors

错误响应和排查方式

业务侧不要只依赖 HTTP 状态码,应读取 error.code 做稳定分支,并把 request_id 写入上层日志。

统一错误格式

所有网关业务错误都会返回统一 JSON。请求头中传入的 X-Request-ID 会原样进入响应;未传时由网关生成。

{
  "error": {
    "code": "provider_route_not_found",
    "message": "No enabled provider route exists.",
    "request_id": "req_xxx"
  }
}
HTTP error.code 含义 上层处理建议
401 unauthorized 缺少 App Token,或 token 无效、已轮换。 检查服务端密钥配置,避免使用旧 token。
403 app_client_disabled 当前应用方已被后台禁用。 联系网关管理员确认应用状态。
403 capability_forbidden App Token 未授权调用该 capability。 在管理后台给应用方勾选对应能力。
404 capability_not_available 能力不存在、未启用或不在代码定义中。 检查路径和 capability key,确认网关能力已发布。
422 invalid_request 参数缺失或不符合约束。 按页面中的入参 schema 修正请求。
502 provider_failed 供应商调用失败,网关已做错误归一。 记录 request_id,必要时由网关侧排查供应商日志。
503 provider_route_not_found 没有启用的 provider route。 联系网关管理员检查 provider 和能力路由。

Boundary

业务边界和上线清单

上层应用负责

  • 保存 API_GATEWAY_BASE_URLAPI_GATEWAY_APP_TOKEN
  • 生成验证码、保存验证码、控制过期时间、重试次数和风控策略。
  • 把业务请求 ID 作为 X-Request-ID 传给网关。
  • error.code 做业务提示、重试或降级。

网关负责

  • 保存 provider 密钥、签名方式和供应商差异。
  • 能力授权、provider route 和 mock provider 配置。
  • 脱敏记录调用日志,不保存短信原始验证码。
  • 把供应商输出归一成稳定 capability 出参。

接入前检查

  • 确认后台已给应用方授权所需 capability。
  • 调用 /api/v1/capabilities 确认 token 能看到目标能力。
  • 调用 /api/v1/capabilities/{capability_key}/docs 校验契约。
  • 压测或灰度前确认 provider 路由和错误降级逻辑。

本地 Docker

运行 MySQL + backend,本地自动迁移并初始化代码内能力和 mock provider 路由。

docker compose -f docker-compose.local.yml up --build

CloudBase Run

后端使用 backend/Dockerfile,端口 8080,健康检查 /health,生产数据库使用独立 schema。

DATABASE_URL=mysql+pymysql://...

静态网站

官网说明和管理后台都放在 CloudBase 静态托管独立目录,避免和已有网站混淆。

/common-server-gateway/index.html