ZimuPayService.php 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. <?php
  2. namespace App\Services\Payment;
  3. use App\Services\BaseService;
  4. use GuzzleHttp\Client;
  5. use GuzzleHttp\Exception\RequestException;
  6. use Illuminate\Support\Facades\Log;
  7. class ZimuPayService extends BaseService
  8. {
  9. public const CHANNEL_RECHARGE = 'ZIMUpay';
  10. public const CHANNEL_WITHDRAW = 'ZIMUwithdraw';
  11. public const CHANNEL_CASH = 'ZIMUcash';
  12. public const PAY_STATUS_SUCCESS = '3';
  13. public const PAY_STATUS_FAIL = '2';
  14. public const WITHDRAW_STATUS_SUCCESS = '2';
  15. public const WITHDRAW_STATUS_FAIL = '5';
  16. public const CASH_STATUS_SUCCESS = '3';
  17. public const CASH_STATUS_FAIL = '4';
  18. public static function isRechargeChannel(?string $channel): bool
  19. {
  20. return in_array(strtolower((string)$channel), [
  21. strtolower(self::CHANNEL_RECHARGE),
  22. 'zimupay',
  23. '808pay',
  24. '808支付',
  25. ], true);
  26. }
  27. public static function isWithdrawChannel(?string $channel): bool
  28. {
  29. return strtolower((string)$channel) === strtolower(self::CHANNEL_WITHDRAW);
  30. }
  31. public static function isCashChannel(?string $channel): bool
  32. {
  33. return strtolower((string)$channel) === strtolower(self::CHANNEL_CASH);
  34. }
  35. public static function isPayoutChannel(?string $channel): bool
  36. {
  37. return self::isWithdrawChannel($channel) || self::isCashChannel($channel);
  38. }
  39. public static function canUserRecharge($userId): bool
  40. {
  41. $allowedUserIds = array_values(array_filter(array_map(
  42. 'trim',
  43. explode(',', (string)config('app.zimu_pay_recharge_user_ids', ''))
  44. )));
  45. if (empty($allowedUserIds)) {
  46. return true;
  47. }
  48. return in_array((string)$userId, $allowedUserIds, true);
  49. }
  50. public static function amount($amount): string
  51. {
  52. return number_format((float)$amount, 2, '.', '');
  53. }
  54. public static function getAppId(): string
  55. {
  56. return (string)config('app.zimu_pay_app_id');
  57. }
  58. public static function getSecret(): string
  59. {
  60. return (string)config('app.zimu_pay_key');
  61. }
  62. public static function getPayNotifyUrl(): string
  63. {
  64. return rtrim(config('app.url'), '/') . '/api/pay/harvest';
  65. }
  66. public static function getPayoutNotifyUrl(): string
  67. {
  68. return rtrim(config('app.url'), '/') . '/api/pay/notify';
  69. }
  70. public static function signature(array $params, ?string $key = null): string
  71. {
  72. unset($params['sign']);
  73. $params = array_filter($params, static function ($value) {
  74. return $value !== null && $value !== '';
  75. });
  76. ksort($params, SORT_STRING);
  77. $parts = [];
  78. foreach ($params as $name => $value) {
  79. $parts[] = $name . '=' . $value;
  80. }
  81. $parts[] = 'key=' . ($key ?? self::getSecret());
  82. return strtoupper(md5(implode('&', $parts)));
  83. }
  84. public static function pay($amount, string $orderNo, string $member): array
  85. {
  86. $data = self::signedData([
  87. 'appId' => self::getAppId(),
  88. 'mchOrderNo' => $orderNo,
  89. 'amount' => self::amount($amount),
  90. 'member' => $member,
  91. 'notifyUrl' => self::getPayNotifyUrl(),
  92. 'timestamp' => self::timestamp(),
  93. 'nonce' => self::nonce(),
  94. ]);
  95. return self::post((string)config('app.zimu_pay_gateway'), $data);
  96. }
  97. public static function queryPayOrder(string $orderNo): array
  98. {
  99. return self::post((string)config('app.zimu_pay_query_gateway'), self::queryData($orderNo));
  100. }
  101. public static function withdraw($amount, string $orderNo, string $member, string $receiveAccount, string $realName = ''): array
  102. {
  103. $data = self::signedData([
  104. 'appId' => self::getAppId(),
  105. 'member' => $member,
  106. 'realName' => $realName,
  107. 'mchOrderNo' => $orderNo,
  108. 'amount' => self::amount($amount),
  109. 'notifyUrl' => self::getPayoutNotifyUrl(),
  110. 'timestamp' => self::timestamp(),
  111. 'nonce' => self::nonce(),
  112. 'receiveAccount' => $receiveAccount,
  113. ]);
  114. return self::post((string)config('app.zimu_withdraw_gateway'), $data);
  115. }
  116. public static function queryWithdrawOrder(string $orderNo): array
  117. {
  118. return self::post((string)config('app.zimu_withdraw_query_gateway'), self::queryData($orderNo));
  119. }
  120. public static function cash($amount, string $orderNo, string $accountName, string $accountNo, string $bank, string $payMethod = ''): array
  121. {
  122. $data = self::signedData([
  123. 'appId' => self::getAppId(),
  124. 'mchOrderNo' => $orderNo,
  125. 'amount' => self::amount($amount),
  126. 'notifyUrl' => self::getPayoutNotifyUrl(),
  127. 'accountName' => $accountName,
  128. 'accountNo' => $accountNo,
  129. 'bank' => $bank,
  130. 'payMethod' => $payMethod,
  131. 'timestamp' => self::timestamp(),
  132. 'nonce' => self::nonce(),
  133. ]);
  134. return self::post((string)config('app.zimu_cash_gateway'), $data);
  135. }
  136. public static function queryCashOrder(string $orderNo): array
  137. {
  138. return self::post((string)config('app.zimu_cash_query_gateway'), self::queryData($orderNo));
  139. }
  140. public static function balance(): array
  141. {
  142. $data = self::signedData([
  143. 'appId' => self::getAppId(),
  144. 'timestamp' => self::timestamp(),
  145. 'nonce' => self::nonce(),
  146. ]);
  147. return self::post((string)config('app.zimu_balance_gateway'), $data);
  148. }
  149. public static function verifyNotify(array $params): bool
  150. {
  151. if (self::getAppId() === '' || ($params['appId'] ?? '') !== self::getAppId() || empty($params['sign'])) {
  152. return false;
  153. }
  154. return hash_equals(self::signature($params), strtoupper((string)$params['sign']));
  155. }
  156. public static function notifySignatureDiagnostics(array $params): array
  157. {
  158. $fields = $params;
  159. unset($fields['sign']);
  160. return [
  161. 'received_sign' => strtoupper((string)($params['sign'] ?? '')),
  162. 'calculated_sign' => self::signature($params),
  163. 'signing_fields' => array_keys(array_filter($fields, static function ($value) {
  164. return $value !== null && $value !== '';
  165. })),
  166. ];
  167. }
  168. public static function cashPayMethod(string $bankName): string
  169. {
  170. if (str_contains($bankName, '微信')) {
  171. return '2';
  172. }
  173. if (str_contains($bankName, '支付宝')) {
  174. return '3';
  175. }
  176. if (str_contains($bankName, '数字')) {
  177. return '4';
  178. }
  179. return '1';
  180. }
  181. private static function queryData(string $orderNo): array
  182. {
  183. return self::signedData([
  184. 'appId' => self::getAppId(),
  185. 'mchOrderNo' => $orderNo,
  186. 'timestamp' => self::timestamp(),
  187. 'nonce' => self::nonce(),
  188. ]);
  189. }
  190. private static function signedData(array $data): array
  191. {
  192. $data['sign'] = self::signature($data);
  193. return $data;
  194. }
  195. private static function timestamp(): string
  196. {
  197. return (string)round(microtime(true) * 1000);
  198. }
  199. private static function nonce(): string
  200. {
  201. return substr(str_replace('.', '', uniqid('', true)), 0, 15);
  202. }
  203. private static function post(string $url, array $data): array
  204. {
  205. $logData = $data;
  206. unset($logData['sign']);
  207. Log::info('ZIMU支付接口请求', [
  208. 'url' => $url,
  209. 'app_id' => self::getAppId(),
  210. 'data' => $logData,
  211. ]);
  212. try {
  213. $response = (new Client(['timeout' => 10.0]))->post($url, [
  214. 'json' => $data,
  215. 'headers' => [
  216. 'Accept' => 'application/json',
  217. 'Content-Type' => 'application/json',
  218. ],
  219. ]);
  220. $body = $response->getBody()->getContents();
  221. Log::info('ZIMU支付接口响应', [
  222. 'url' => $url,
  223. 'app_id' => self::getAppId(),
  224. 'http_status' => $response->getStatusCode(),
  225. 'body' => $body,
  226. ]);
  227. return json_decode($body, true) ?: [];
  228. } catch (RequestException $e) {
  229. $response = $e->getResponse();
  230. $body = $response ? $response->getBody()->getContents() : '';
  231. Log::error('ZIMU支付接口请求失败', [
  232. 'url' => $url,
  233. 'app_id' => self::getAppId(),
  234. 'http_status' => $response ? $response->getStatusCode() : null,
  235. 'body' => $body,
  236. 'error' => $e->getMessage(),
  237. ]);
  238. throw $e;
  239. } catch (\Throwable $e) {
  240. Log::error('ZIMU支付接口异常', [
  241. 'url' => $url,
  242. 'app_id' => self::getAppId(),
  243. 'error' => $e->getMessage(),
  244. ]);
  245. throw $e;
  246. }
  247. }
  248. public static function getWhere(array $search = []): array
  249. {
  250. return [];
  251. }
  252. }