Search by

zc-center / thinkphp-sdk

ZC Center SAPI SDK for ThinkPHP

Maintainers

Package info

github.com/763606865/zc-center-sdk-thinkphp

pkg:composer/zc-center/thinkphp-sdk

Transparency log

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.2 2026-09-03 02:13 UTC

This package is auto-updated.

Last update: 2026-09-03 02:13:25 UTC


README

适用于 ThinkPHP 6.1/8.x 的中台 SAPI 服务端 SDK,提供 HMAC-SHA256 请求签名、AES-256-GCM 加解密、响应验签和现有业务接口封装。

环境要求

  • PHP 8.1+
  • OpenSSL 扩展
  • JSON 扩展
  • ThinkPHP 6.1 或 8.x
  • Guzzle 7.x

app_secret 只能保存在生态应用服务端,禁止写入前端代码、客户端安装包或日志。

安装

SDK 尚未发布到 Packagist。生态 ThinkPHP 项目可用 本地 Path 引用 安装。

在生态项目根目录的 composer.json 中增加 Path Repository,并声明依赖:

{
  "repositories": [
    {
      "type": "path",
      "url": "../zc-center/sdk/thinkphp"
    }
  ],
  "require": {
    "zc-center/thinkphp-sdk": "@dev"
  }
}

url 改为相对生态项目根目录到本仓库 sdk/thinkphp 的实际路径,然后执行:

composer update zc-center/thinkphp-sdk

Composer 会通过 symlink(或拷贝)把包装入 vendor/zc-center/thinkphp-sdk。本包在 composer.jsonextra.think.services 中声明了服务提供者,ThinkPHP 会自动注册,一般无需手动配置。

本机联调适合 Path 引用;CI / 部署机拿不到这份源码时,应改用私有 Git(VCS Repository)或内网 Composer 源,不要只依赖本机 path。

配置

在生态项目 .env 中添加:

[ZC_CENTER]
BASE_URL=https://zc-center.example.com
APP_KEY=中台分配的app_key
APP_SECRET=中台创建应用时仅展示一次的app_secret
ENCRYPTION=true
TIMEOUT=10
CONNECT_TIMEOUT=3
VERIFY_SSL=true
DEBUG=false

SDK 服务提供者会自动注册,并将默认配置与生态项目的 config/zc_center.php 合并。如需覆盖默认配置,可创建:

<?php

return [
    'base_url' => env('ZC_CENTER.BASE_URL', ''),
    'app_key' => env('ZC_CENTER.APP_KEY', ''),
    'app_secret' => env('ZC_CENTER.APP_SECRET', ''),
    'encryption' => (bool) env('ZC_CENTER.ENCRYPTION', true),
    'timeout' => (float) env('ZC_CENTER.TIMEOUT', 10),
    'connect_timeout' => (float) env('ZC_CENTER.CONNECT_TIMEOUT', 3),
    'verify_ssl' => (bool) env('ZC_CENTER.VERIFY_SSL', true),
    'debug' => (bool) env('ZC_CENTER.DEBUG', false),
];

中台联调环境关闭了 SAPI_ENCRYPTION_ENABLED 时,生态项目必须同步设置 ENCRYPTION=false。生产环境双方都必须开启加密。

设置 DEBUG=true 后,每次 SAPI 调用会写入日志(ThinkPHP 下走 Log::info,否则 error_log),内容包括:

  • 请求:method、url、query、request_headers、request_params(明文业务参数)、request_body(实际发送 Body,加密时为密文信封)
  • 响应:http_status、response_headers、response_body(原始 Body)、response_payload(验签解密后的业务 JSON)

生产环境请保持 DEBUG=false,避免日志泄露业务数据与签名头。

使用

依赖注入

use ZcCenter\ThinkPHP\Client;

class CenterController
{
    public function ping(Client $center): array
    {
        return $center->ping()->send('hello')->payload();
    }
}

Facade

use ZcCenter\ThinkPHP\ZcCenter;

$response = ZcCenter::ping()->send('hello');
$data = $response->data();

ZcCenter 继承 ThinkPHP 原生 think\Facade,实际代理容器中单例绑定的 Client

注册中台用户

$response = $center->user()->register(
    mobile: '13800138000',
    countryCode: '+86',
    nickname: '示例用户',
    avatar: 'https://example.com/avatar.png',
);

$uuid = $response->data()['user']['uuid'];

来源应用申请 Ticket

$response = $center->auth()->issueTicket(
    uuid: '550e8400-e29b-41d4-a716-446655440000',
    targetAppCode: 'product-b',
);

$ticket = $response->data()['ticket'];

目标应用兑换 Ticket

$response = $center->auth()->exchangeTicket($ticket);
$user = $response->data()['user'];

// 使用中台永久 UUID 查找或创建当前生态产品的本地用户,再签发本产品 Token。
$uuid = $user['uuid'];

获取题目列表

use ZcCenter\ThinkPHP\Api\Question;

$response = $center->question()->list([
    'page' => 1,
    'page_size' => 20,
    'bank_uuid' => '3aae1b52-8fca-42c6-9bd6-7bd4901ceb66', // 可选
    'type' => Question::TYPE_SINGLE,                 // 可选:1单选 2多选 3判断 4填空 5简答
    'difficulty' => Question::DIFFICULTY_MEDIUM,     // 可选:1易 2中 3难
    'tag_uuids' => [],                               // 可选
    'updated_since' => 0,                            // 可选,增量同步
    'include_answer' => false,                       // true 时返回标准答案与解析
]);

$list = $response->data()['list'];
$total = $response->data()['total'];
$engine = $response->data()['engine']; // elasticsearch | mysql

搜索题目

$response = $center->question()->search([
    'keyword' => '导数',
    'page' => 1,
    'page_size' => 20,
    'bank_uuid' => null, // null 会被 SDK 自动省略
    'include_answer' => false,
]);
$hits = $response->data()['list'];

获取题目详情

$response = $center->question()->detail(
    uuid: 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx',
    includeAnswer: false,
);
$question = $response->data()['question'];

批量拉取题目

$response = $center->question()->batch(
    uuids: ['uuid-1', 'uuid-2'],
    includeAnswer: false,
);
$list = $response->data()['list'];

题库列表 / 详情

$banks = $center->questionBank()->list([
    'page' => 1,
    'page_size' => 20,
    'keyword' => '数学',
])->data()['list'];

$bank = $center->questionBank()
    ->detail('3aae1b52-8fca-42c6-9bd6-7bd4901ceb66')
    ->data()['bank'];
// $bank['question_count'] 为该库已发布题目数

上报题目(按题干去重)

仅可向 当前应用归属 的题库上报。同一题库内规范化题干(去 HTML、压缩空白)相同则返回已有题目,不重复创建。

$response = $center->question()->report([
    'bank_uuid' => $bankUuid, // 或 bank_code
    'type' => Question::TYPE_SINGLE,
    'difficulty' => Question::DIFFICULTY_MEDIUM,
    'stem' => '1+1等于多少?',
    'options' => [
        ['key' => 'A', 'content' => '1'],
        ['key' => 'B', 'content' => '2'],
    ],
    'answer' => 'B',
    'analysis' => '基础运算',
    'score' => 1,
]);

$action = $response->data()['action']; // created | exists
$question = $response->data()['question'];

批量上报题目

$response = $center->question()->reportBatch(
    ['bank_code' => 'math_basic'],
    [
        [
            'external_id' => 'local-1001',
            'type' => Question::TYPE_JUDGE,
            'stem' => '地球是圆的。',
            'answer' => true,
        ],
        [
            'external_id' => 'local-1002',
            'type' => Question::TYPE_SINGLE,
            'stem' => '1+1等于多少?',
            'options' => [
                ['key' => 'A', 'content' => '1'],
                ['key' => 'B', 'content' => '2'],
            ],
            'answer' => 'B',
        ],
    ]
);

$data = $response->data();
// created / exists / failed + results[]

扩展生态产品接口

底层 Client 只负责签名、加密、请求和响应验证,标准接口集中在 Api\Sapi。不同生态产品可以定义自己的接口集合,不需要修改 SDK 核心:

<?php

namespace app\service\center;

use ZcCenter\ThinkPHP\Api\AbstractApi;
use ZcCenter\ThinkPHP\Response;

final class ClassroomApi extends AbstractApi
{
    public function syncCourse(string $courseId, string $title): Response
    {
        return $this->post('/sapi/classroom/course/sync', [
            'course_id' => $courseId,
            'title' => $title,
        ]);
    }
}

通过依赖注入使用:

/** @var ClassroomApi $classroom */
$classroom = $center->api(ClassroomApi::class);
$response = $classroom->syncCourse('course-1001', '示例课程');

通过 Facade 使用:

use ZcCenter\ThinkPHP\ZcCenter;

/** @var ClassroomApi $classroom */
$classroom = ZcCenter::api(ClassroomApi::class);
$response = $classroom->syncCourse('course-1001', '示例课程');

对于只有一次调用、不值得建立接口类的情况,仍可使用底层方法:

$response = $center->post('/sapi/example', ['field' => 'value']);

异常处理

use ZcCenter\ThinkPHP\Exception\ApiException;
use ZcCenter\ThinkPHP\Exception\SignatureException;
use ZcCenter\ThinkPHP\Exception\TransportException;

try {
    $response = $center->auth()->exchangeTicket($ticket);
} catch (ApiException $exception) {
    // 中台业务错误,例如 Ticket 已使用或过期。
    $businessCode = $exception->businessCode();
    $httpStatus = $exception->httpStatus();
    $message = $exception->getMessage();
} catch (SignatureException $exception) {
    // 响应可能被篡改、密钥不一致,或中间代理修改了 Body。
} catch (TransportException $exception) {
    // 网络超时、DNS、TLS 或响应格式错误。
}

认证阶段的错误响应可能没有响应签名,此时 ApiException::signatureVerified() 返回 false,不得将其中的数据作为可信用户身份使用。

安全行为

  • 每次请求自动生成 24 位 Base64URL nonce。
  • 请求签名覆盖方法、路径、排序后的查询参数、时间戳、nonce 和原始 Body。
  • 加密使用 SHA-256(app_secret) 派生 AES-256-GCM 密钥。
  • 加密响应先验签、后解密。
  • 明文联调响应基于原始 Body 验签,验签前不会重新编码 JSON。
  • 默认启用 TLS 证书验证,生产环境禁止设置 VERIFY_SSL=false

自测

在中台项目根目录执行:

php sdk/thinkphp/tests/run.php