CurrencyExchange.php 11 KB

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