订单
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
<?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:',
|
||||
];
|
||||
|
||||
$ch = curl_init($this->url);
|
||||
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); // 改用 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, CURL_IPRESOLVE_V4);
|
||||
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,完全禁用主机验证
|
||||
// 添加 SSL 相关选项
|
||||
curl_setopt($ch, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); // 强制使用 TLS 1.2
|
||||
curl_setopt($ch, CURLOPT_SSL_CIPHER_LIST, 'DEFAULT@SECLEVEL=1'); // 降低安全级别
|
||||
}
|
||||
// 添加 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user