Files
zyt/server/app/common/service/gancao/GancaoOpenApiTransport.php
T
2026-05-11 17:49:38 +08:00

180 lines
6.9 KiB
PHP
Executable File

<?php
declare(strict_types=1);
namespace app\common\service\gancao;
/**
* 甘草开放平台网关传输(AES-128-ECB + HTTP 头),对齐官方 GcOpenApi.php。
*/
final class GancaoOpenApiTransport
{
private string $url;
private string $ak;
private string $sk;
private string $userAgent;
public function __construct(string $url, string $ak, string $sk, string $userAgent = 'zyt-admin/1.0')
{
$this->url = rtrim($url, '/');
$this->ak = $ak;
$this->sk = $sk;
$this->userAgent = $userAgent;
}
/**
* @param array<string,mixed> $payload 已含 package、class 及业务字段
* @return array{state:int,msg:string,body?:array<string,mixed>,response?:string}
*/
public function post(array $payload): array
{
// 与甘草网关规范一致:签名/头里的时间戳须与 body 内业务字段 timestamp(若有)一致,避免跨秒不一致导致签名校验失败
$sigTs = time();
if (isset($payload['timestamp']) && is_numeric($payload['timestamp'])) {
$sigTs = (int) $payload['timestamp'];
}
if ($sigTs <= 0) {
$sigTs = time();
}
$json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($json === false) {
return ['state' => 0, 'msg' => 'json_encode 失败'];
}
$noise = self::randStr(8);
$signature = sha1($json . $sigTs . $noise . $this->sk);
$cipher = self::encrypt($json, $this->sk);
if ($cipher === '') {
return ['state' => 0, 'msg' => 'AES 加密失败'];
}
$headers = [
'Connection: close',
'Content-Type: application/json; charset=utf-8',
'Content-length: ' . strlen($cipher),
'Cache-Control: no-cache',
'AK: ' . $this->ak,
'Signature: ' . $signature,
'UTC-Timestamp: ' . $sigTs,
'NOISE: ' . $noise,
'Expect:',
];
// cURL 常量在部分精简构建里可能未注册,统一用 defined() 做优雅降级,值取自官方枚举
$httpVer11 = defined('CURL_HTTP_VERSION_1_1') ? CURL_HTTP_VERSION_1_1 : 2;
$ipv4Only = defined('CURL_IPRESOLVE_V4') ? CURL_IPRESOLVE_V4 : 1;
$ch = curl_init($this->url);
curl_setopt($ch, CURLOPT_HTTP_VERSION, $httpVer11); // 改用 HTTP/1.1
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_USERAGENT, $this->userAgent);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_ENCODING, 'gzip');
curl_setopt($ch, CURLOPT_IPRESOLVE, $ipv4Only);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); // 增加连接超时
curl_setopt($ch, CURLOPT_TIMEOUT, 120);
curl_setopt($ch, CURLOPT_POSTFIELDS, $cipher);
curl_setopt($ch, CURLOPT_HEADER, true);
if (str_starts_with($this->url, 'https:')) {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); // 改为 0,完全禁用主机验证
// 强制 TLS 1.2;常量值为 6,但部分 PHP/cURL 构建未注册该常量,运行时未定义时退化为默认 TLS
$tls12 = defined('CURL_SSLVERSION_TLSv1_2') ? CURL_SSLVERSION_TLSv1_2 : 6;
curl_setopt($ch, CURLOPT_SSLVERSION, $tls12);
// 可选 cipher list。默认不设(交给 cURL 自带默认,兼容性最好)。
// 需要兼容弱加密服务器时,在 config/gancao_scm.php 或 .env 里设:
// GANCAO_SCM_TLS_CIPHERS="DEFAULT@SECLEVEL=1" // 仅 OpenSSL 构建的 cURL 支持此语法
// GANCAO_SCM_TLS_CIPHERS="DEFAULT:!aNULL:!eNULL" // 通用写法
// cURL 是 LibreSSL/NSS/GnuTLS/BoringSSL 时,@SECLEVEL= 会直接报 CURLE_SSL_CIPHER(59)。
$cipherList = trim((string) \think\facade\Config::get('gancao_scm.tls_ciphers', ''));
if ($cipherList !== '') {
curl_setopt($ch, CURLOPT_SSL_CIPHER_LIST, $cipherList);
}
}
// 添加 TCP keepalive(部分旧 libcurl 可能没注册 CURLOPT_TCP_KEEPALIVE,守一下)
if (defined('CURLOPT_TCP_KEEPALIVE')) {
curl_setopt($ch, CURLOPT_TCP_KEEPALIVE, 1);
curl_setopt($ch, CURLOPT_TCP_KEEPIDLE, 120);
curl_setopt($ch, CURLOPT_TCP_KEEPINTVL, 60);
}
$raw = curl_exec($ch);
$info = curl_getinfo($ch);
$curlErr = curl_errno($ch);
$curlMsg = curl_error($ch);
curl_close($ch);
if ($raw === false || (int) ($info['http_code'] ?? 0) !== 200) {
$http = (int) ($info['http_code'] ?? 0);
$err = is_string($raw) ? $raw : '';
return [
'state' => 0,
'msg' => '通信失败:HTTP ' . $http . ($curlErr !== 0 ? ' curl#' . $curlErr . ' ' . $curlMsg : ''),
'response' => $err,
];
}
$bodyRaw = substr($raw, strpos($raw, "\r\n\r\n") + 4);
$plain = self::decrypt($bodyRaw, $this->sk);
if ($plain === '') {
$maybeJson = json_decode($bodyRaw, true);
if (is_array($maybeJson) && isset($maybeJson['status'])) {
return ['state' => 1, 'msg' => '成功(明文)', 'body' => $maybeJson];
}
return ['state' => -1, 'msg' => '解密失败(请核对网关 SK 是否为 16 位且与 AK 匹配)', 'response' => mb_substr($bodyRaw, 0, 500)];
}
$decoded = json_decode($plain, true);
return ['state' => 1, 'msg' => '成功', 'body' => is_array($decoded) ? $decoded : []];
}
private static function encrypt(string $string, string $key): string
{
$out = openssl_encrypt($string, 'AES-128-ECB', $key, OPENSSL_RAW_DATA);
return $out !== false ? base64_encode($out) : '';
}
private static function decrypt(string $string, string $key): string
{
$bin = base64_decode($string, true);
if ($bin === false) {
return '';
}
$out = openssl_decrypt($bin, 'AES-128-ECB', $key, OPENSSL_RAW_DATA);
return $out !== false ? $out : '';
}
private static function randStr(int $length = 8): string
{
$chars = 'ABCDEFGHIJKLMNPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890';
$chars = str_shuffle($chars);
$end = strlen($chars) - 1;
$buf = [];
while (true) {
$c = $chars[random_int(0, $end)];
if ($c !== '0') {
$buf[] = $c;
break;
}
}
$n = 1;
while ($n < $length) {
$r = $chars[random_int(0, $end)];
if ($r !== $buf[count($buf) - 1]) {
$buf[] = $r;
++$n;
}
}
return implode('', $buf);
}
}