CurrencyExchange.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. <?php
  2. namespace App\Utils;
  3. use Cache;
  4. use Exception;
  5. use Http;
  6. use Illuminate\Http\Client\PendingRequest;
  7. use Log;
  8. class CurrencyExchange
  9. {
  10. private static array $apis = ['fixer', 'exchangerateApi', 'wise', 'currencyData', 'exchangeRatesData', 'duckduckgo', 'wsj', 'xRates', 'valutafx', 'baidu', 'unionpay', 'jsdelivrFile', 'it120', 'k780'];
  11. private static ?PendingRequest $basicRequest;
  12. /**
  13. * @param string $target target Currency
  14. * @param float|int $amount exchange amount
  15. * @param string|null $base Base Currency
  16. * @param string|null $source API source
  17. * @return float|null amount in target currency
  18. */
  19. public static function convert(string $target, float|int $amount, ?string $base = null, ?string $source = null): ?float
  20. {
  21. $rate = self::getCurrencyRate($target, $base, $source);
  22. return $rate === null ? null : round($amount * $rate, 2);
  23. }
  24. public static function getCurrencyRate(string $target, ?string $base = null, ?string $source = null): ?float
  25. {
  26. $base = $base ?? (string) sysConfig('standard_currency');
  27. $cacheKey = "Currency_{$base}_{$target}_ExRate";
  28. if (! $source && Cache::has($cacheKey)) {
  29. return Cache::get($cacheKey);
  30. }
  31. self::$basicRequest = Http::timeout(5)->retry(2)->withOptions(['http_errors' => false])->withoutVerifying()->withUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36');
  32. foreach ($source ? [$source] : self::$apis as $api) {
  33. if (! method_exists(self::class, $api)) {
  34. continue;
  35. }
  36. try {
  37. $rate = self::$api($base, $target);
  38. if ($rate !== null) {
  39. Cache::put($cacheKey, $rate, Day);
  40. return $rate;
  41. }
  42. } catch (Exception $e) {
  43. Log::error("[$api] 币种汇率信息获取报错: ".$e->getMessage());
  44. }
  45. }
  46. return null;
  47. }
  48. private static function exchangerateApi(string $base, string $target): ?float
  49. { // Reference: https://www.exchangerate-api.com/docs/php-currency-api
  50. $key = config('services.currency.exchangerate-api_key');
  51. $url = $key ? "https://v6.exchangerate-api.com/v6/$key/pair/$base/$target" : "https://open.er-api.com/v6/latest/$base";
  52. return self::callApi($url, static function ($data) use ($key, $target) {
  53. if ($data['result'] === 'success') {
  54. if ($key && isset($data['conversion_rate'])) {
  55. return $data['conversion_rate'];
  56. }
  57. if (isset($data['rates'][$target])) {
  58. return $data['rates'][$target];
  59. }
  60. }
  61. Log::error('[CurrencyExchange]exchangerateApi exchange failed with following message: '.$data['error-type'] ?? '');
  62. return null;
  63. });
  64. }
  65. private static function callApi(string $url, callable $extractor, array $headers = []): ?float
  66. {
  67. try {
  68. $request = self::$basicRequest;
  69. if (! empty($headers)) {
  70. $request = $request->withHeaders($headers);
  71. }
  72. $response = $request->get($url);
  73. if ($response->ok()) {
  74. $data = $response->json();
  75. return $extractor($data);
  76. }
  77. Log::warning('[CurrencyExchange] API request failed: '.$url.' Response: '.var_export($response, true));
  78. } catch (Exception $e) {
  79. Log::warning("[CurrencyExchange] API $url request exception: ".$e->getMessage());
  80. }
  81. return null;
  82. }
  83. private static function k780(string $base, string $target): ?float
  84. { // Reference: https://www.nowapi.com/api/finance.rate
  85. return self::callApi("https://sapi.k780.com/?app=finance.rate&scur=$base&tcur=$target&appkey=10003&sign=b59bc3ef6191eb9f747dd4e83c99f2a4&format=json", static function ($data) {
  86. if ($data['success'] === '1') {
  87. return $data['result']['rate'] ?? null;
  88. }
  89. Log::emergency('[CurrencyExchange]Nowapi exchange failed with following message: '.$data['msg']);
  90. return null;
  91. });
  92. }
  93. private static function it120(string $base, string $target): ?float
  94. { // Reference: https://www.it120.cc/help/fnun8g.html
  95. $key = config('services.currency.it120_key');
  96. if (! $key) {
  97. return null;
  98. }
  99. return self::callApi("https://api.it120.cc/$key/forex/rate?fromCode=$target&toCode=$base", static function ($data) {
  100. if ($data['code'] === 0) {
  101. return $data['data']['rate'] ?? null;
  102. }
  103. Log::emergency('[CurrencyExchange]it120 exchange failed with following message: '.$data['msg']);
  104. return null;
  105. });
  106. }
  107. private static function fixer(string $base, string $target): ?float
  108. { // Reference: https://apilayer.com/marketplace/fixer-api RATE LIMIT: 100 Requests / Monthly!!!!
  109. $key = config('services.currency.apiLayer_key');
  110. if (! $key) {
  111. return null;
  112. }
  113. return self::callApi("https://api.apilayer.com/fixer/latest?symbols=$target&base=$base", static function ($data) use ($target) {
  114. if ($data['success']) {
  115. return $data['rates'][$target] ?? null;
  116. }
  117. Log::emergency('[CurrencyExchange]Fixer exchange failed with following message: '.$data['error']['type'] ?? '');
  118. return null;
  119. }, ['apikey' => $key]);
  120. }
  121. private static function currencyData(string $base, string $target): ?float
  122. { // Reference: https://apilayer.com/marketplace/currency_data-api RATE LIMIT: 100 Requests / Monthly
  123. $key = config('services.currency.apiLayer_key');
  124. if (! $key) {
  125. return null;
  126. }
  127. return self::callApi("https://api.apilayer.com/currency_data/live?source=$base&currencies=$target", static function ($data) use ($base, $target) {
  128. if ($data['success']) {
  129. return $data['quotes'][$base.$target] ?? null;
  130. }
  131. Log::emergency('[CurrencyExchange]Currency Data exchange failed with following message: '.$data['error']['info'] ?? '');
  132. return null;
  133. }, ['apikey' => $key]);
  134. }
  135. private static function exchangeRatesData(string $base, string $target): ?float
  136. { // Reference: https://apilayer.com/marketplace/exchangerates_data-api RATE LIMIT: 250 Requests / Monthly
  137. $key = config('services.currency.apiLayer_key');
  138. if (! $key) {
  139. return null;
  140. }
  141. return self::callApi("https://api.apilayer.com/exchangerates_data/latest?symbols=$target&base=$base", static function ($data) use ($target) {
  142. if ($data['success']) {
  143. return $data['rates'][$target];
  144. }
  145. Log::emergency('[CurrencyExchange]Exchange Rates Data exchange failed with following message: '.$data['error']['message'] ?? '');
  146. return null;
  147. }, ['apikey' => $key]);
  148. }
  149. private static function jsdelivrFile(string $base, string $target): ?float
  150. { // Reference: https://github.com/fawazahmed0/currency-api
  151. return self::callApi('https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/'.strtolower($base).'.min.json', static function ($data) use ($base, $target) {
  152. return $data[strtolower($base)][strtolower($target)] ?? null;
  153. });
  154. }
  155. private static function duckduckgo(string $base, string $target): ?float
  156. { // Reference: https://duckduckgo.com http://www.xe.com/
  157. return self::callApi("https://duckduckgo.com/js/spice/currency_convert/1/$base/$target", static function ($data) {
  158. return $data['to'][0]['mid'] ?? null;
  159. });
  160. }
  161. private static function wise(string $base, string $target): ?float
  162. { // Reference: https://wise.com/zh-cn/currency-converter/
  163. return self::callApi("https://api.wise.com/v1/rates?source=$base&target=$target", static function ($data) {
  164. return $data[0]['rate'] ?? null;
  165. }, ['Authorization' => 'Basic OGNhN2FlMjUtOTNjNS00MmFlLThhYjQtMzlkZTFlOTQzZDEwOjliN2UzNmZkLWRjYjgtNDEwZS1hYzc3LTQ5NGRmYmEyZGJjZA==']);
  166. }
  167. private static function xRates(string $base, string $target): ?float
  168. { // Reference: https://www.x-rates.com/
  169. try {
  170. $response = self::$basicRequest->get("https://www.x-rates.com/calculator/?from=$base&to=$target&amount=1");
  171. if ($response->ok()) {
  172. preg_match('/<span class="ccOutputRslt">([\d.]+)</', $response->body(), $matches);
  173. return $matches[1] ?? null;
  174. }
  175. } catch (Exception $e) {
  176. Log::warning('[CurrencyExchange] xRates request failed: '.$e->getMessage());
  177. }
  178. return null;
  179. }
  180. private static function valutafx(string $base, string $target): ?float
  181. { // Reference: https://www.valutafx.com/convert/
  182. return self::callApi("https://www.valutafx.com/api/v2/rates/lookup?isoTo=$target&isoFrom=$base&amount=1", static function ($data) {
  183. if ($data['ErrorMessage'] === null) {
  184. return $data['Rate'] ?? null;
  185. }
  186. return null;
  187. });
  188. }
  189. private static function unionpay(string $base, string $target): ?float
  190. { // Reference: https://www.unionpayintl.com/cn/rate/
  191. try {
  192. $response = self::$basicRequest->get('https://www.unionpayintl.com/upload/jfimg/'.date('Ymd').'.json');
  193. if (! $response->ok()) {
  194. $response = self::$basicRequest->get('https://www.unionpayintl.com/upload/jfimg/'.date('Ymd', strtotime('-1 day')).'.json');
  195. }
  196. if ($response->ok()) {
  197. $data = $response->json();
  198. return collect($data['exchangeRateJson'])->where('baseCur', $target)->where('transCur', $base)->pluck('rateData')->first();
  199. }
  200. } catch (Exception $e) {
  201. Log::warning('[CurrencyExchange] Unionpay request failed: '.$e->getMessage());
  202. }
  203. return null;
  204. }
  205. private static function baidu(string $base, string $target): ?float
  206. {
  207. return self::callApi("https://finance.pae.baidu.com/vapi/async/v1?from_money=$base&to_money=$target&srcid=5293", static function ($data) {
  208. if ($data['ResultCode'] === 0) {
  209. return $data['Result'][0]['DisplayData']['resultData']['tplData']['money2_num'] ?? null;
  210. }
  211. return null;
  212. });
  213. }
  214. }