ApcopayClient.php 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. <?php
  2. namespace App\Services\Apcopay;
  3. use App\Services\PayConfig;
  4. use GuzzleHttp\Client;
  5. use GuzzleHttp\Exception\GuzzleException;
  6. use Illuminate\Support\Facades\Log;
  7. class ApcopayClient
  8. {
  9. protected $client;
  10. protected $config;
  11. public function __construct()
  12. {
  13. $payConfigService = new PayConfig();
  14. $this->config =$payConfigService->getConfig('Apcopay');
  15. // 确保base_uri不重复包含/merchanttools
  16. $baseUrl = rtrim($this->config['api_url'], '/');
  17. // 如果baseUrl已经包含merchanttools,则移除它
  18. if (strpos($baseUrl, '/merchanttools') !== false) {
  19. $baseUrl = str_replace('/merchanttools', '', $baseUrl);
  20. }
  21. $this->client = new Client([
  22. 'base_uri' => $baseUrl,
  23. 'timeout' => 30,
  24. 'verify' => false // 开发环境可能需要关闭SSL验证
  25. ]);
  26. // 记录初始化信息
  27. Log::info('Apcopay Client initialized', [
  28. 'base_uri' => $baseUrl,
  29. 'config' => $this->config
  30. ]);
  31. }
  32. /**
  33. * 生成Basic认证头
  34. */
  35. protected function getAuthorizationHeader(): string
  36. {
  37. $credentials = base64_encode($this->config['client_id'] . ':' . $this->config['secret_key']);
  38. return 'Basic ' . $credentials;
  39. }
  40. /**
  41. * 生成签名
  42. */
  43. protected function generateSignature(array $data): string
  44. {
  45. // 记录签名前的数据
  46. Log::info('Apcopay Signature Data Before', [
  47. 'data' => $data
  48. ]);
  49. // 使用品牌密钥 - 直接使用,不做解码
  50. $brandKey = $this->config['brand_key'];
  51. // 1. 扁平化并按字母顺序排序字段
  52. $fields = $this->flattenAndSortFields($data);
  53. // 记录扁平化后的字段
  54. Log::info('Apcopay Flattened Fields', [
  55. 'fields' => $fields
  56. ]);
  57. // 2. 拼接字段
  58. $fieldsString = $this->concatenateFields($fields);
  59. // 记录拼接后的字符串
  60. Log::info('Apcopay Concatenated Fields', [
  61. 'fieldsString' => $fieldsString
  62. ]);
  63. // 3. 转为小写
  64. $lowercaseString = strtolower($fieldsString);
  65. Log::info('Apcopay Lowercase String', [
  66. 'lowercaseString' => $lowercaseString
  67. ]);
  68. // 4. 使用HMAC SHA256计算签名
  69. Log::info('Apcopay Brand Key Length', [
  70. 'length' => strlen($brandKey)
  71. ]);
  72. // 生成签名
  73. $signature = base64_encode(hash_hmac('sha256', $lowercaseString, $brandKey, true));
  74. // 记录生成的签名
  75. Log::info('Apcopay Generated Signature', [
  76. 'signature' => $signature
  77. ]);
  78. return $signature;
  79. }
  80. /**
  81. * 扁平化并排序字段 - 完全按照官方文档实现
  82. */
  83. protected function flattenAndSortFields(array $data, string $prefix = ''): array
  84. {
  85. $fields = [];
  86. foreach ($data as $key => $value) {
  87. $fieldKey = $prefix ? "{$prefix}.{$key}" : $key;
  88. if (is_null($value)) {
  89. $fields[$fieldKey] = '';
  90. } elseif (is_array($value)) {
  91. // 检查是否为索引数组
  92. if (array_keys($value) === range(0, count($value) - 1)) {
  93. // 索引数组处理方式
  94. foreach ($value as $index => $item) {
  95. $arrayKey = $fieldKey . "[" . $index . "]";
  96. if (is_array($item)) {
  97. $fields = array_merge($fields, $this->flattenAndSortFields($item, $arrayKey));
  98. } else {
  99. $fields[$arrayKey] = $this->formatValue($item);
  100. }
  101. }
  102. } else {
  103. // 关联数组处理方式
  104. $fields = array_merge($fields, $this->flattenAndSortFields($value, $fieldKey));
  105. }
  106. } else {
  107. $fields[$fieldKey] = $this->formatValue($value);
  108. }
  109. }
  110. ksort($fields, SORT_STRING | SORT_FLAG_CASE);
  111. return $fields;
  112. }
  113. /**
  114. * 处理布尔值转字符串 - 确保与官方文档一致
  115. */
  116. protected function formatValue($value)
  117. {
  118. if (is_bool($value)) {
  119. return $value ? 'true' : 'false';
  120. } elseif (is_null($value)) {
  121. return '';
  122. }
  123. return (string)$value;
  124. }
  125. /**
  126. * 拼接字段
  127. */
  128. protected function concatenateFields(array $fields): string
  129. {
  130. $pairs = [];
  131. foreach ($fields as $key => $value) {
  132. $pairs[] = $key . '=' . $value;
  133. }
  134. return implode('&', $pairs);
  135. }
  136. /**
  137. * 发送请求
  138. */
  139. protected function request(string $method, string $uri, array $data = [], array $headers = []): array
  140. {
  141. try {
  142. // 记录请求信息
  143. Log::info('Apcopay API Request', [
  144. 'method' => $method,
  145. 'uri' => $uri,
  146. 'data' => $data
  147. ]);
  148. // 生成签名
  149. $signature = $this->generateSignature($data);
  150. // 1. 准备请求头
  151. $headers = array_merge([
  152. 'Authorization' => $this->getAuthorizationHeader(),
  153. 'User-Agent' => 'PHP/ApcopayAPI',
  154. 'Content-Type' => 'application/json',
  155. 'Accept' => 'application/json',
  156. 'Signature' => $signature // 添加签名到请求头
  157. ], $headers);
  158. // 2. 不再将签名添加到请求体中,按照文档仅添加到请求头
  159. // 记录完整请求头和请求体
  160. Log::info('Apcopay API Request Details', [
  161. 'headers' => array_merge(
  162. $headers,
  163. ['Authorization' => 'Basic **REDACTED**'] // 不记录完整的Authorization头
  164. ),
  165. 'data' => $data
  166. ]);
  167. // 3. 发送请求
  168. $options = [
  169. 'headers' => $headers,
  170. 'http_errors' => false // 不自动抛出HTTP错误,我们自己处理
  171. ];
  172. if (!empty($data)) {
  173. $options['json'] = $data;
  174. }
  175. $response = $this->client->request($method, $uri, $options);
  176. // 3. 解析响应
  177. $statusCode = $response->getStatusCode();
  178. $body = $response->getBody()->getContents();
  179. Log::info('Apcopay API Raw Response', [
  180. 'uri' => $uri,
  181. 'status_code' => $statusCode,
  182. 'body' => $body
  183. ]);
  184. $result = json_decode($body, true);
  185. if (json_last_error() !== JSON_ERROR_NONE) {
  186. Log::error('Apcopay API Invalid JSON', [
  187. 'uri' => $uri,
  188. 'body' => $body,
  189. 'json_error' => json_last_error_msg()
  190. ]);
  191. throw new \RuntimeException('Invalid JSON response: ' . $body);
  192. }
  193. // 4. 检查HTTP状态码
  194. if ($statusCode >= 400) {
  195. Log::error('Apcopay API HTTP Error', [
  196. 'uri' => $uri,
  197. 'status_code' => $statusCode,
  198. 'result' => $result
  199. ]);
  200. // 返回错误信息而不是抛出异常,让调用者决定如何处理
  201. return [
  202. 'isSuccess' => false,
  203. 'errorMessage' => $result['errorMessage'] ?? "HTTP Error: {$statusCode}",
  204. 'statusCode' => $statusCode,
  205. 'rawResponse' => $result
  206. ];
  207. }
  208. // 记录响应信息
  209. Log::info('Apcopay API Response', [
  210. 'uri' => $uri,
  211. 'result' => $result
  212. ]);
  213. return $result;
  214. } catch (GuzzleException $e) {
  215. Log::error('Apcopay API Error', [
  216. 'method' => $method,
  217. 'uri' => $uri,
  218. 'data' => $data,
  219. 'error' => $e->getMessage(),
  220. 'trace' => $e->getTraceAsString()
  221. ]);
  222. return [
  223. 'isSuccess' => false,
  224. 'errorMessage' => 'Apcopay API request failed: ' . $e->getMessage(),
  225. 'exception' => get_class($e)
  226. ];
  227. } catch (\Exception $e) {
  228. Log::error('Apcopay General Error', [
  229. 'method' => $method,
  230. 'uri' => $uri,
  231. 'data' => $data,
  232. 'error' => $e->getMessage(),
  233. 'trace' => $e->getTraceAsString()
  234. ]);
  235. return [
  236. 'isSuccess' => false,
  237. 'errorMessage' => 'Error processing Apcopay request: ' . $e->getMessage(),
  238. 'exception' => get_class($e)
  239. ];
  240. }
  241. }
  242. /**
  243. * GET请求
  244. */
  245. protected function get(string $uri, array $query = [], array $headers = []): array
  246. {
  247. return $this->request('GET', $uri . '?' . http_build_query($query), [], $headers);
  248. }
  249. /**
  250. * POST请求
  251. */
  252. protected function post(string $uri, array $data = [], array $headers = []): array
  253. {
  254. return $this->request('POST', $uri, $data, $headers);
  255. }
  256. /**
  257. * PUT请求
  258. */
  259. protected function put(string $uri, array $data = [], array $headers = []): array
  260. {
  261. return $this->request('PUT', $uri, $data, $headers);
  262. }
  263. /**
  264. * DELETE请求
  265. */
  266. protected function delete(string $uri, array $headers = []): array
  267. {
  268. return $this->request('DELETE', $uri, [], $headers);
  269. }
  270. }