| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171 |
- <?php
- namespace App\Services;
- use App\Util;
- class SupefinaSpei
- {
- /**
- * @var array<string,mixed>
- */
- 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<string,mixed> $params
- * @return array<string,mixed>
- */
- 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<string,mixed> $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<string,mixed> $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;
- }
- }
|