Skip to content

OpenAPI 开放接口

OpenAPI 模块为平台提供了强大的接口开放能力。当您需要将后台接口暴露给外部系统或第三方开发者使用,但又不希望为每个接入方创建独立用户账号时,这个模块可以完美地解决这一需求。

核心优势

  • 🔐 安全的签名验证机制
  • 🚀 灵活的 QPS 限流控制
  • 📊 完整的请求日志记录
  • 💰 支持余额扣费模式

主要特性

  • 签名验证:基于 HMAC-SHA256 的安全签名机制,确保接口调用的安全性
  • 访问控制:支持按用户设置 QPS 限制,有效控制接口访问频率
  • 日志记录:详细记录每次接口调用,便于监控和问题排查
  • 余额管理:支持预付费模式,可按调用次数或其他维度扣费

数据库结构

OpenAPI 模块使用以下三个核心数据表来管理用户信息、余额和请求日志:

表名描述主要用途
openapi_usersOpenAPI 用户表存储第三方用户的基本信息和认证密钥
openapi_user_balance用户余额表管理用户的可用余额和扣费记录
openapi_request_log请求日志表记录所有 API 调用的详细信息
php
  Schema::create('openapi_users', function (Blueprint $table) {
        $table->id()->comment('ID');
        $table->string('username')->comment('用户名');
        $table->string('mobile', 20)->comment('手机号');
        $table->string('password')->comment('密码');
        $table->string('company')->nullable()->comment('公司名称');
        $table->string('description')->nullable()->comment('描述');
        $table->unsignedInteger('qps')->default(100)->comment('每分钟的 QPS');
        $table->string('app_key')->comment('app key');
        $table->string('app_secret')->comment('密钥');
        $table->creatorId();
        $table->createdAt();
        $table->updatedAt();
        $table->deletedAt();

        $table->engine = 'InnoDB';
        $table->comment('openapi 用户表');
    });

  Schema::create('openapi_user_balance', function (Blueprint $table) {
        $table->id();
        $table->uuid();
        $table->integer('user_id')->comment('用户id');
        $table->unsignedInteger('balance')->default(0)->comment('用户余额');
        $table->createdAt();
        $table->updatedAt();
        $table->deletedAt();
        $table->engine = 'InnoDB';
        $table->comment('用户余额表');
    });

  Schema::create('openapi_request_log', function (Blueprint $table) {
        $table->id();
        $table->uuid('request_id')->comment('请求id');
        $table->string('app_key')->comment('app key');
        $table->json('data')->comment('请求数据');
        $table->createdAt();
        $table->updatedAt();

        $table->engine = 'InnoDB';
        $table->comment('openapi 请求日志');
    });

创建 OpenAPI 用户

通过管理后台创建

在管理后台中找到 OpenAPI 模块,通过可视化界面创建新的第三方用户:

catchadmin 专业版-openapi 模块

关键配置说明

QPS 限制说明

QPS 字段用于限制访问接口的频率,当前版本按每分钟计算。例如设置为 100,表示该用户每分钟最多可调用 100 次接口。

认证密钥

用户创建成功后,系统会自动分配一对认证密钥:

  • AppKey:公开的应用标识符,用于识别调用方
  • AppSecret:私密的签名密钥,用于生成请求签名,请妥善保管

安全提醒

  • AppSecret 仅在创建时显示一次,请及时记录保存
  • 建议定期更换密钥以提高安全性
  • 切勿在客户端代码中硬编码 AppSecret

快速接入

路由配置

使用 OpenAPI 功能非常简单,只需在路由中添加相应的中间件即可。在项目根目录的 routes/api.php 文件中添加以下代码:

php
Route::prefix('v1')->middleware([
    \Modules\Openapi\Middlewares\CheckSignatureMiddleware::class,
    \Modules\Openapi\Middlewares\RateLimiterMiddleware::class  // 可选:添加限流中间件
])->group(function () {
    Route::get('user', function (){
       return \Modules\Openapi\Facade\OpenapiResponse::success([
           'message' => 'Hello OpenAPI!',
           'timestamp' => time()
       ]);
    });

    // 更多 API 路由...
});

验证接入

配置完成后,在浏览器中访问 域名/api/v1/user,如果看到以下响应,说明接入成功:

catchadmin专业版-openapi

中间件说明

  • CheckSignatureMiddleware:必需,用于验证请求签名
  • RateLimiterMiddleware:可选,用于限制请求频率

响应格式

所有 OpenAPI 接口都应使用统一的响应格式:

php
// 成功响应
return \Modules\Openapi\Facade\OpenapiResponse::success($data, $message);

// 错误响应
return \Modules\Openapi\Facade\OpenapiResponse::error($message, $code);

接口调用示例

签名算法说明

OpenAPI 使用 HMAC-SHA256 算法对请求参数进行签名验证,确保接口调用的安全性。签名流程如下:

  1. 收集参数:获取所有请求参数(包括 GET 查询参数或 POST 表单参数)
  2. 添加时间戳:添加 timestamp 参数(Unix 时间戳)
  3. 数组扁平化:对嵌套数组和对象进行扁平化处理
    • 对象:user.name -> user.name
    • 数组:items[0].name -> items[0].name
  4. 参数排序:按参数名进行字典序排序
  5. 构建签名串:将参数按 key=value&key=value 格式拼接
  6. 生成签名:使用 AppSecret 对签名串进行 HMAC-SHA256 加密
  7. 发送请求:在请求头中添加 app-keysignature

重要提醒

参数扁平化是签名验证的关键步骤,必须与服务端逻辑保持完全一致,否则会导致签名验证失败。

扁平化示例

假设有以下复杂参数:

json
{
  "user": {
    "name": "张三",
    "age": 25
  },
  "items": [
    { "id": 1, "name": "商品A" },
    { "id": 2, "name": "商品B" }
  ],
  "timestamp": 1640995200
}

扁平化后的参数:

json
{
  "items[0].id": "1",
  "items[0].name": "商品A",
  "items[1].id": "2",
  "items[1].name": "商品B",
  "timestamp": "1640995200",
  "user.age": "25",
  "user.name": "张三"
}

最终签名字符串:

items[0].id=1&items[0].name=商品A&items[1].id=2&items[1].name=商品B&timestamp=1640995200&user.age=25&user.name=张三

多语言调用示例

以下提供了多种编程语言的调用示例,您可以根据项目需求选择合适的实现方式:

javascript
const crypto = require('crypto')
const axios = require('axios')

class OpenAPIClient {
  constructor(appKey, appSecret, baseURL) {
    this.appKey = appKey
    this.appSecret = appSecret
    this.baseURL = baseURL
  }

  // 扁平化数组
  flattenArray(obj, prefix = '') {
    let result = {}

    for (let key in obj) {
      if (obj.hasOwnProperty(key)) {
        const value = obj[key]
        const newKey = prefix ? `${prefix}.${key}` : key

        if (Array.isArray(value)) {
          // 处理数组
          value.forEach((item, index) => {
            if (typeof item === 'object' && item !== null) {
              Object.assign(result, this.flattenArray(item, `${newKey}[${index}]`))
            } else {
              result[`${newKey}[${index}]`] = item
            }
          })
        } else if (typeof value === 'object' && value !== null) {
          // 处理对象
          Object.assign(result, this.flattenArray(value, newKey))
        } else {
          result[newKey] = value
        }
      }
    }

    return result
  }

  // 创建签名
  createSignature(params) {
    // 扁平化参数
    const flattenedParams = this.flattenArray(params)

    // 按键名排序
    const keys = Object.keys(flattenedParams).sort()

    // 构建签名字符串
    const signStr = keys.map((key) => `${key}=${flattenedParams[key]}`).join('&')

    return crypto.createHmac('sha256', this.appSecret).update(signStr).digest('hex')
  }

  // GET 请求
  async get(url, params = {}) {
    params.timestamp = Math.floor(Date.now() / 1000)
    const signature = this.createSignature(params)

    const response = await axios.get(`${this.baseURL}${url}`, {
      params,
      headers: {
        'app-key': this.appKey,
        signature: signature
      }
    })

    return response.data
  }

  // POST 请求
  async post(url, data = {}) {
    data.timestamp = Math.floor(Date.now() / 1000)
    const signature = this.createSignature(data)

    const response = await axios.post(`${this.baseURL}${url}`, data, {
      headers: {
        'app-key': this.appKey,
        signature: signature,
        'Content-Type': 'application/x-www-form-urlencoded'
      }
    })

    return response.data
  }
}

// 使用示例
const client = new OpenAPIClient('your_app_key', 'your_app_secret', 'https://your-domain.com/api')

// GET 请求示例
client
  .get('/v1/user', { page: 1, limit: 10 })
  .then((result) => console.log(result))
  .catch((error) => console.error(error))

// POST 请求示例
client
  .post('/v1/user', { name: 'John', email: 'john@example.com' })
  .then((result) => console.log(result))
  .catch((error) => console.error(error))

// 复杂参数示例
client
  .post('/v1/user', {
    user: { name: '张三', age: 25 },
    items: [
      { id: 1, name: '商品A' },
      { id: 2, name: '商品B' }
    ]
  })
  .then((result) => console.log(result))
  .catch((error) => console.error(error))
php
<?php

class OpenAPIClient
{
    private $appKey;
    private $appSecret;
    private $baseURL;

    public function __construct($appKey, $appSecret, $baseURL)
    {
        $this->appKey = $appKey;
        $this->appSecret = $appSecret;
        $this->baseURL = rtrim($baseURL, '/');
    }

    /**
     * 扁平化数组(与服务端逻辑保持一致)
     */
    private function flattenArray($array, $prefix = '')
    {
        $result = [];

        foreach ($array as $key => $value) {
            $newKey = $prefix ? $prefix . '.' . $key : $key;

            if (is_array($value)) {
                if ($this->isAssociativeArray($value)) {
                    // 关联数组
                    $result = array_merge($result, $this->flattenArray($value, $newKey));
                } else {
                    // 索引数组
                    foreach ($value as $index => $item) {
                        if (is_array($item)) {
                            $result = array_merge($result, $this->flattenArray($item, $newKey . '[' . $index . ']'));
                        } else {
                            $result[$newKey . '[' . $index . ']'] = $item;
                        }
                    }
                }
            } else {
                $result[$newKey] = $value;
            }
        }

        return $result;
    }

    /**
     * 判断是否为关联数组
     */
    private function isAssociativeArray($array)
    {
        if (empty($array)) {
            return false;
        }
        return array_keys($array) !== range(0, count($array) - 1);
    }

    /**
     * 创建签名(与服务端逻辑完全一致)
     */
    private function createSignature($params)
    {
        // 扁平化参数
        $flattenedParams = $this->flattenArray($params);

        // 按键名排序
        ksort($flattenedParams);

        // 构建签名字符串
        $signStr = '';
        foreach ($flattenedParams as $key => $value) {
            $signStr .= $key . '=' . $value . '&';
        }

        // 去除末尾的 & 符号
        $signStr = rtrim($signStr, '&');

        return hash_hmac('sha256', $signStr, $this->appSecret);
    }

    /**
     * GET 请求
     */
    public function get($url, $params = [])
    {
        $params['timestamp'] = time();
        $signature = $this->createSignature($params);

        $queryString = http_build_query($params);
        $fullUrl = $this->baseURL . $url . '?' . $queryString;

        $headers = [
            'app-key: ' . $this->appKey,
            'signature: ' . $signature
        ];

        return $this->makeRequest($fullUrl, 'GET', $headers);
    }

    /**
     * POST 请求
     */
    public function post($url, $data = [])
    {
        $data['timestamp'] = time();
        $signature = $this->createSignature($data);

        $headers = [
            'app-key: ' . $this->appKey,
            'signature: ' . $signature,
            'Content-Type: application/x-www-form-urlencoded'
        ];

        $fullUrl = $this->baseURL . $url;

        return $this->makeRequest($fullUrl, 'POST', $headers, http_build_query($data));
    }

    /**
     * 执行 HTTP 请求
     */
    private function makeRequest($url, $method, $headers, $data = null)
    {
        $ch = curl_init();

        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CUSTOMREQUEST => $method,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_TIMEOUT => 30,
        ]);

        if ($data && $method === 'POST') {
            curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        }

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode !== 200) {
            throw new Exception("HTTP Error: " . $httpCode);
        }

        return json_decode($response, true);
    }
}

// 使用示例
$client = new OpenAPIClient('your_app_key', 'your_app_secret', 'https://your-domain.com/api');

try {
    // GET 请求示例
    $result = $client->get('/v1/user', ['page' => 1, 'limit' => 10]);
    echo "GET Result: " . json_encode($result) . "\n";

    // POST 请求示例
    $result = $client->post('/v1/user', ['name' => 'John', 'email' => 'john@example.com']);
    echo "POST Result: " . json_encode($result) . "\n";

    // 复杂参数示例
    $complexParams = [
        'user' => [
            'name' => '张三',
            'age' => 25
        ],
        'items' => [
            ['id' => 1, 'name' => '商品A'],
            ['id' => 2, 'name' => '商品B']
        ]
    ];
    $result = $client->post('/v1/user', $complexParams);
    echo "Complex Result: " . json_encode($result) . "\n";

} catch (Exception $e) {
    echo "Error: " . $e->getMessage() . "\n";
}
python
import hashlib
import hmac
import time
import requests
from urllib.parse import urlencode
from typing import Dict, Any, Optional

class OpenAPIClient:
    def __init__(self, app_key: str, app_secret: str, base_url: str):
        self.app_key = app_key
        self.app_secret = app_secret
        self.base_url = base_url.rstrip('/')

    def _flatten_array(self, obj: Dict[str, Any], prefix: str = '') -> Dict[str, Any]:
        """扁平化数组(与服务端逻辑保持一致)"""
        result = {}

        for key, value in obj.items():
            new_key = f"{prefix}.{key}" if prefix else key

            if isinstance(value, list):
                # 处理数组
                for index, item in enumerate(value):
                    if isinstance(item, dict):
                        result.update(self._flatten_array(item, f"{new_key}[{index}]"))
                    else:
                        result[f"{new_key}[{index}]"] = item
            elif isinstance(value, dict):
                # 处理对象
                result.update(self._flatten_array(value, new_key))
            else:
                result[new_key] = value

        return result

    def _create_signature(self, params: Dict[str, Any]) -> str:
        """创建签名(与服务端逻辑完全一致)"""
        # 扁平化参数
        flattened_params = self._flatten_array(params)

        # 按键名排序
        sorted_params = sorted(flattened_params.items())

        # 构建签名字符串
        sign_str = '&'.join([f"{key}={value}" for key, value in sorted_params])

        # 使用 HMAC-SHA256 生成签名
        signature = hmac.new(
            self.app_secret.encode('utf-8'),
            sign_str.encode('utf-8'),
            hashlib.sha256
        ).hexdigest()

        return signature

    def get(self, url: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
        """GET 请求"""
        if params is None:
            params = {}

        # 添加时间戳
        params['timestamp'] = int(time.time())

        # 生成签名
        signature = self._create_signature(params)

        # 构建请求头
        headers = {
            'app-key': self.app_key,
            'signature': signature
        }

        # 发送请求
        full_url = f"{self.base_url}{url}"
        response = requests.get(full_url, params=params, headers=headers, timeout=30)
        response.raise_for_status()

        return response.json()

    def post(self, url: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
        """POST 请求"""
        if data is None:
            data = {}

        # 添加时间戳
        data['timestamp'] = int(time.time())

        # 生成签名
        signature = self._create_signature(data)

        # 构建请求头
        headers = {
            'app-key': self.app_key,
            'signature': signature,
            'Content-Type': 'application/x-www-form-urlencoded'
        }

        # 发送请求
        full_url = f"{self.base_url}{url}"
        response = requests.post(full_url, data=data, headers=headers, timeout=30)
        response.raise_for_status()

        return response.json()

# 使用示例
if __name__ == "__main__":
    client = OpenAPIClient(
        app_key='your_app_key',
        app_secret='your_app_secret',
        base_url='https://your-domain.com/api'
    )

    try:
        # GET 请求示例
        result = client.get('/v1/user', {'page': 1, 'limit': 10})
        print(f"GET Result: {result}")

        # POST 请求示例
        result = client.post('/v1/user', {'name': 'John', 'email': 'john@example.com'})
        print(f"POST Result: {result}")

    except requests.exceptions.RequestException as e:
        print(f"Request Error: {e}")
    except Exception as e:
        print(f"Error: {e}")
bash
#!/bin/bash

# 配置参数
APP_KEY="your_app_key"
APP_SECRET="your_app_secret"
BASE_URL="https://your-domain.com/api"

# 创建签名函数(与服务端逻辑保持一致)
create_signature() {
    local params="$1"
    # 注意:cURL 脚本中的参数需要手动扁平化和排序
    # 对于复杂参数,建议使用其他语言的客户端
    echo -n "$params" | openssl dgst -sha256 -hmac "$APP_SECRET" -hex | sed 's/^.* //'
}

# GET 请求示例
echo "=== GET 请求示例 ==="
TIMESTAMP=$(date +%s)
PARAMS="page=1&limit=10&timestamp=$TIMESTAMP"
SIGNATURE=$(create_signature "$PARAMS")

curl -X GET \
  "$BASE_URL/v1/user?$PARAMS" \
  -H "app-key: $APP_KEY" \
  -H "signature: $SIGNATURE" \
  -H "Content-Type: application/json"

echo -e "\n"

# POST 请求示例
echo "=== POST 请求示例 ==="
TIMESTAMP=$(date +%s)
POST_DATA="name=John&email=john@example.com&timestamp=$TIMESTAMP"
SIGNATURE=$(create_signature "$POST_DATA")

curl -X POST \
  "$BASE_URL/v1/user" \
  -H "app-key: $APP_KEY" \
  -H "signature: $SIGNATURE" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "$POST_DATA"

echo -e "\n"

# 使用 jq 格式化 JSON 响应(需要安装 jq)
echo "=== 带格式化输出的请求 ==="
TIMESTAMP=$(date +%s)
PARAMS="timestamp=$TIMESTAMP"
SIGNATURE=$(create_signature "$PARAMS")

curl -s -X GET \
  "$BASE_URL/v1/user?$PARAMS" \
  -H "app-key: $APP_KEY" \
  -H "signature: $SIGNATURE" \
  | jq '.'

ApiFox 前置脚本

如果您使用 ApiFox 进行接口测试,可以添加以下前置脚本来自动处理签名:

catchadmin专业版-openapi

javascript
// 在 ApiFox 环境变量中配置 app_key 和 app_secret
const appKey = pm.environment.get('app_key');
const appSecret = pm.environment.get('app_secret');

// 扁平化数组函数(与服务端逻辑保持一致)
function flattenArray(obj, prefix = '') {
  let result = {};

  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      const value = obj[key];
      const newKey = prefix ? `${prefix}.${key}` : key;

      if (Array.isArray(value)) {
        // 处理数组
        value.forEach((item, index) => {
          if (typeof item === 'object' && item !== null) {
            Object.assign(result, flattenArray(item, `${newKey}[${index}]`));
          } else {
            result[`${newKey}[${index}]`] = item;
          }
        });
      } else if (typeof value === 'object' && value !== null) {
        // 处理对象
        Object.assign(result, flattenArray(value, newKey));
      } else {
        result[newKey] = value;
      }
    }
  }

  return result;
}

function createSign(params) {
  // 扁平化参数
  const flattenedParams = flattenArray(params);

  // 按键名排序
  const keys = Object.keys(flattenedParams).sort();

  // 构建签名字符串
  const signStr = keys.map((key) => `${key}=${flattenedParams[key]}`).join('&');

  return CryptoJS.HmacSHA256(signStr, appSecret).toString();
}

let params = {};
params['timestamp'] = Math.floor(Date.now() / 1000);

if (pm.request.method == 'GET') {
  // 处理 GET 请求参数
  const queryString = pm.request.url.getQueryString();
  if (queryString) {
    pm.request.url
      .getQueryString()
      .split('&')
      .forEach((item) => {
        const items = item.split('=');
        params[items[0]] = items[1];
      });
  }

  const signature = createSign(params);

  // 设置请求头
  pm.request.headers.add({ key: 'app-key', value: appKey });
  pm.request.headers.add({ key: 'signature', value: signature });

  // 更新查询参数
  let query = '';
  for (let key in params) {
    query += key + '=' + params[key] + '&';
  }
  pm.request.url.query = query.slice(0, -1); // 去除末尾的 &

} else if (pm.request.method == 'POST') {
  // 处理 POST 请求参数
  if (pm.request.body.mode == 'urlencoded') {
    pm.request.body.urlencoded.each((item) => {
      params[item.key] = item.value;
    });

    const signature = createSign(params);

    // 设置请求头
    pm.request.headers.add({ key: 'app-key', value: appKey });
    pm.request.headers.add({ key: 'signature', value: signature });

    // 添加时间戳参数
    pm.request.body.urlencoded.add({
      disabled: false,
      key: 'timestamp',
      value: params.timestamp
    });
  }

  // 支持 form-data 格式
  if (pm.request.body.mode == 'formdata') {
    pm.request.body.formdata.each((item) => {
      params[item.key] = item.value;
    });

    const signature = createSign(params);

    pm.request.headers.add({ key: 'app-key', value: appKey });
    pm.request.headers.add({ key: 'signature', value: signature });

    pm.request.body.formdata.add({
      key: 'timestamp',
      value: params.timestamp
    });
  }
}

环境配置

在 ApiFox 中配置环境变量,设置您的 app_keyapp_secret

catchadmin专业版-openapi

测试验证

配置完成后,通过 ApiFox 发送请求,如果出现以下结果,说明接入成功:

catchadmin专业版-openapi

测试建议

  • 先使用简单的 GET 请求进行测试
  • 确认时间戳生成和签名算法正确
  • 检查请求头是否正确设置
  • 验证参数排序逻辑

中间件配置

OpenAPI 模块提供了两个核心中间件来确保接口的安全性和稳定性:

可用中间件

中间件类名功能描述是否必需
签名验证CheckSignatureMiddleware验证请求签名的有效性✅ 必需
速率限制RateLimiterMiddleware控制用户请求频率(QPS)🔄 可选

使用方式

php
Route::prefix('v1')->middleware([
    \Modules\Openapi\Middlewares\CheckSignatureMiddleware::class,  // 必需
    \Modules\Openapi\Middlewares\RateLimiterMiddleware::class      // 可选
])->group(function () {
    // 您的 API 路由
});

注意事项

  • CheckSignatureMiddleware 是必需的,用于保证接口安全
  • RateLimiterMiddleware 建议在生产环境中使用,防止接口滥用
  • 中间件的顺序很重要:签名验证应该在速率限制之前

异常处理

自定义异常

OpenAPI 模块的所有异常都需要继承 OpenapiException 基类。这样可以确保异常处理的一致性:

php
namespace Modules\Openapi\Exceptions;

use Modules\Openapi\Enums\Code;

// 示例:不合法的 AppKey 异常
class InvalidAppKeyException extends OpenapiException
{
    protected $code = Code::INVALID_APP_KEY;

    public function __construct($message = null)
    {
        parent::__construct($message ?: '无效的 AppKey');
    }
}

异常渲染配置

bootstrap/app.php 文件中配置 OpenAPI 异常的统一渲染处理:

php
$app = Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        // 中间件配置
    })
    ->withExceptions(function (Exceptions $exceptions) {
        // OpenAPI 异常统一处理
        $exceptions->render(function (OpenapiException $exception, Request $request) {
            return OpenapiResponse::error(
                $exception->getMessage(),
                $exception->getCode()
            );
        });

        // 其他异常处理...
    })->create();

异常处理说明

  • 所有 OpenAPI 相关异常都会被自动捕获并格式化返回
  • 异常响应格式与成功响应保持一致
  • 建议为不同的错误场景创建专门的异常类

状态码枚举

枚举定义

OpenAPI 模块使用统一的状态码枚举来标识不同的响应状态。所有枚举都需要实现 Modules\Openapi\Enums\Enum 接口:

php
namespace Modules\Openapi\Enums;

enum Code: int implements Enum
{
    // 基础状态码
    case SUCCESS = 10000;           // 请求成功
    case FAILED = 10001;            // 请求失败

    // 认证相关错误
    case APP_KEY_LOST = 10002;      // AppKey 丢失
    case SIGNATURE_LOST = 10003;    // 签名丢失
    case INVALID_APP_KEY = 10004;   // 无效的 AppKey
    case INVALID_SIGNATURE = 10005; // 无效的签名
    case INVALID_TIMESTAMP = 10006; // 无效的时间戳

    // 业务相关错误
    case BALANCE_NOT_ENOUGH = 10007; // 余额不足
    case RATE_LIMIT = 10008;         // 请求频率超限

    /**
     * 获取状态码对应的中文描述
     */
    public function name(): string
    {
        return match ($this) {
            self::SUCCESS => '请求成功',
            self::FAILED => '请求失败',
            self::APP_KEY_LOST => 'AppKey 丢失',
            self::SIGNATURE_LOST => '签名丢失',
            self::INVALID_APP_KEY => '无效的 AppKey',
            self::INVALID_SIGNATURE => '无效的签名',
            self::INVALID_TIMESTAMP => '无效的时间戳',
            self::BALANCE_NOT_ENOUGH => '余额不足',
            self::RATE_LIMIT => '请求过于频繁,请稍后重试'
        };
    }

    /**
     * 比较枚举值
     * @param mixed $value
     * @return bool
     */
    public function equal(mixed $value): bool
    {
        return $this->value === $value;
    }
}

状态码说明

状态码常量名描述触发场景
10000SUCCESS请求成功正常响应
10001FAILED请求失败通用错误
10002APP_KEY_LOSTAppKey 丢失请求头缺少 app-key
10003SIGNATURE_LOST签名丢失请求头缺少 signature
10004INVALID_APP_KEY无效的 AppKeyAppKey 不存在或已禁用
10005INVALID_SIGNATURE无效的签名签名验证失败
10006INVALID_TIMESTAMP无效的时间戳时间戳过期或格式错误
10007BALANCE_NOT_ENOUGH余额不足用户余额不够扣费
10008RATE_LIMIT请求频率超限超过 QPS 限制

自定义状态码

如需添加新的状态码,建议从 10100 开始,避免与系统预留码冲突。

最佳实践

安全建议

  1. 密钥管理

    • 定期更换 AppSecret,建议每 3-6 个月更换一次
    • 使用环境变量存储密钥,避免硬编码
    • 为不同环境(开发、测试、生产)使用不同的密钥
  2. 签名验证

    • 时间戳有效期建议设置为 5-15 分钟
    • 对敏感接口可以添加额外的业务级验证
    • 记录异常签名尝试,便于安全审计
  3. 频率控制

    • 根据业务场景合理设置 QPS 限制
    • 考虑为不同类型的接口设置不同的限流策略
    • 提供友好的限流提示信息

性能优化

  1. 缓存策略

    • 缓存用户的 AppKeyAppSecret 映射关系
    • 使用 Redis 等缓存工具提高验证效率
    • 合理设置缓存过期时间
  2. 日志管理

    • 定期清理过期的请求日志
    • 考虑使用异步方式记录日志
    • 重要信息可以同步到专门的日志系统

监控告警

建议监控以下指标:

  • API 调用成功率
  • 平均响应时间
  • 异常签名尝试次数
  • QPS 超限频率
  • 用户余额预警

常见问题

签名验证失败

问题:接口返回 "无效的签名" 错误

解决方案

  1. 检查参数排序是否正确(按字典序)
  2. 确认时间戳格式(Unix 时间戳)
  3. 验证 AppSecret 是否正确
  4. 确保参数拼接格式为 key=value&key=value
  5. 重要:检查数组扁平化是否正确实现(嵌套对象和数组必须扁平化)

请求频率超限

问题:接口返回 "请求过于频繁" 错误

解决方案

  1. 检查当前 QPS 设置
  2. 优化调用频率,添加请求间隔
  3. 联系管理员调整 QPS 限制
  4. 考虑使用批量接口减少调用次数

时间戳过期

问题:接口返回 "无效的时间戳" 错误

解决方案

  1. 确保服务器时间同步
  2. 检查时间戳生成逻辑(必须是 Unix 时间戳)
  3. 默认时间窗口为 60 秒,确保请求在有效时间内发送
  4. 使用 NTP 服务同步时间

时间戳验证逻辑

服务端验证:abs(time() - $timestamp) <= 60(默认 60 秒) 可通过配置 api.timestamp_period 调整时间窗口


技术支持

如果在使用过程中遇到问题,请参考本文档或联系技术支持团队。