*/ public $config = []; public function __construct(string $configKey = 'SupefinaSpeiOut') { $payConfigService = new PayConfig(); $this->config = $payConfigService->getConfig($configKey) ?? []; } /** * 生成随机字符串 */ public function generateNonceStr(int $length = 32): string { $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; $str = ''; $max = strlen($chars) - 1; for ($i = 0; $i < $length; $i++) { $str .= $chars[mt_rand(0, $max)]; } return $str; } /** * 对参数进行签名(MD5,大写),完全遵循 Supefina 文档: * 1. 排除 sign、null、空字符串 * 2. 参数名 ASCII 升序 * 3. key1=value1&key2=value2&...&key=商户密钥 * 4. boolean 转成 "true"/"false",与 Java toString() 一致 * * @param array $params * @return array */ public function sign(array $params): array { $key = trim((string)($this->config['key'] ?? '')); if ($key === '') { throw new \RuntimeException('SupefinaSpei key missing in config'); } $signStr = $this->buildSignString($params, $key); $sign = strtoupper(md5($signStr)); Util::WriteLog('SupefinaSpei', 'sign string (key redacted): ' . str_replace($key, '***', $signStr)); $params['sign'] = $sign; return $params; } /** * 按 Supefina 文档构建待签名字符串 */ private function buildSignString(array $params, string $merchantKey): string { $filtered = []; foreach ($params as $k => $v) { if (strtolower($k) === 'sign') { continue; } if ($v === null) { continue; } if ($v === '' || (is_string($v) && trim($v) === '')) { continue; } $filtered[$k] = $this->valueToString($v); } ksort($filtered); $parts = []; foreach ($filtered as $k => $v) { $parts[] = $k . '=' . $v; } $parts[] = 'key=' . $merchantKey; return implode('&', $parts); } /** * 将值转为签名字符串,与 Java toString() 行为一致 */ private function valueToString($v): string { if (is_bool($v)) { return $v ? 'true' : 'false'; } if (is_array($v)) { return json_encode($v, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } return (string)$v; } /** * 验证回调签名。 * * @param array $data */ public function verify(array $data): bool { if (!isset($data['sign'])) { return false; } $key = trim((string)($this->config['key'] ?? '')); if ($key === '') { return false; } $sign = strtoupper(trim((string)$data['sign'])); $params = $data; unset($params['sign']); $signStr = $this->buildSignString($params, $key); $expected = strtoupper(md5($signStr)); return $sign === $expected; } /** * 发送 JSON POST 请求。 * * @param array $payload */ public function postJson(string $url, array $payload, int $timeout = 20): string { $data = json_encode($payload, JSON_UNESCAPED_UNICODE); $headers = [ 'Content-Type: application/json', ]; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $data); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout); curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); $result = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (curl_errno($ch)) { $error = curl_error($ch); Util::WriteLog('SupefinaSpei_error', 'CURL Error: '.$error); curl_close($ch); throw new \RuntimeException('CURL error: '.$error); } if ($httpCode !== 200) { Util::WriteLog('SupefinaSpei_error', 'HTTP Code: '.$httpCode.' | Response: '.$result); } curl_close($ch); return (string)$result; } }