client = $client ?? new Client([ 'base_uri' => self::BASE_URL, 'connect_timeout' => 8, 'timeout' => 20, 'verify' => true, 'http_errors' => true, ]); } public function complaints(array $config, array $query): array { return $this->request('GET', '/v3/merchant-service/complaints-v2', $config, $query); } public function detail(array $config, string $complaintId): array { return $this->request( 'GET', '/v3/merchant-service/complaints-v2/' . rawurlencode($complaintId), $config ); } public function history(array $config, string $complaintId): array { return $this->request( 'GET', '/v3/merchant-service/complaints-v2/' . rawurlencode($complaintId) . '/negotiation-historys', $config ); } public function reply(array $config, string $complaintId, string $content): array { return $this->request( 'POST', '/v3/merchant-service/complaints-v2/' . rawurlencode($complaintId) . '/response', $config, [], [ 'complainted_mchid' => (string) $config['mchid'], 'response_content' => $content, ] ); } public function complete(array $config, string $complaintId): array { return $this->request( 'POST', '/v3/merchant-service/complaints-v2/' . rawurlencode($complaintId) . '/complete', $config, [], ['complainted_mchid' => (string) $config['mchid']] ); } public function decryptSensitive(string $ciphertext, string $privateKey): string { if ($ciphertext === '') { return ''; } $key = openssl_pkey_get_private($this->normalizePrivateKey($privateKey)); if ($key === false) { throw new ApiException('微信支付商户私钥格式错误,请在微信小程序配置中重新填写'); } $plainText = ''; $success = openssl_private_decrypt( base64_decode($ciphertext, true) ?: '', $plainText, $key, OPENSSL_PKCS1_OAEP_PADDING ); return $success ? $plainText : ''; } private function request( string $method, string $path, array $config, array $query = [], ?array $payload = null ): array { $this->assertConfig($config); $queryString = $query === [] ? '' : http_build_query($query, '', '&', PHP_QUERY_RFC3986); $canonicalUrl = $path . ($queryString !== '' ? '?' . $queryString : ''); $body = $payload === null ? '' : (string) json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $timestamp = time(); $nonce = bin2hex(random_bytes(16)); $message = strtoupper($method) . "\n{$canonicalUrl}\n{$timestamp}\n{$nonce}\n{$body}\n"; $privateKey = openssl_pkey_get_private($this->normalizePrivateKey((string) $config['apiclient_key'])); if ($privateKey === false) { throw new ApiException('微信支付商户私钥格式错误,请在微信小程序配置中重新填写'); } $signature = ''; if (!openssl_sign($message, $signature, $privateKey, OPENSSL_ALGO_SHA256)) { throw new ApiException('微信支付请求签名失败,请检查商户私钥'); } $token = sprintf( 'mchid="%s",nonce_str="%s",signature="%s",timestamp="%d",serial_no="%s"', $config['mchid'], $nonce, base64_encode($signature), $timestamp, $config['certificate_serial_no'] ); $options = [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', 'User-Agent' => 'SaiAdmin-WechatPay/1.0', 'Authorization' => 'WECHATPAY2-SHA256-RSA2048 ' . $token, ], ]; if ($body !== '') { $options['body'] = $body; } try { $response = $this->client->request($method, $canonicalUrl, $options); $content = trim((string) $response->getBody()); if ($content === '') { return []; } $result = json_decode($content, true); if (!is_array($result)) { throw new ApiException('微信支付返回了无法识别的数据'); } return $result; } catch (RequestException $exception) { $responseBody = $exception->getResponse() ? (string) $exception->getResponse()->getBody() : ''; $error = json_decode($responseBody, true); $message = is_array($error) ? (string) ($error['message'] ?? '') : ''; $code = is_array($error) ? (string) ($error['code'] ?? '') : ''; $detail = $message !== '' ? $message : $exception->getMessage(); if ($code !== '') { $detail .= '(' . $code . ')'; } throw new ApiException('微信支付投诉接口请求失败:' . $detail); } } private function assertConfig(array $config): void { foreach (['mchid', 'certificate_serial_no', 'apiclient_key'] as $field) { if (trim((string) ($config[$field] ?? '')) === '') { throw new ApiException('微信小程序支付配置不完整,请补充商户号、证书序列号和商户私钥'); } } } private function normalizePrivateKey(string $privateKey): string { return str_replace(["\\r\\n", "\\n"], "\n", trim($privateKey)); } }