| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183 |
- <?php
- namespace plugin\saiadmin\app\service\user;
- use GuzzleHttp\Client;
- use GuzzleHttp\Exception\RequestException;
- use plugin\saiadmin\exception\ApiException;
- /**
- * 微信支付商户投诉 API v3 客户端。
- *
- * 签名串和 Authorization 头严格按照微信支付 API v3 规范生成,配置直接复用
- * sa_wechat_mini_program 中的商户号、证书序列号和商户私钥。
- */
- class WechatPayComplaintService
- {
- private const BASE_URL = 'https://api.mch.weixin.qq.com';
- private Client $client;
- public function __construct(?Client $client = null)
- {
- $this->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));
- }
- }
|