BaseService.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743
  1. <?php
  2. namespace App\Services;
  3. use App\Models\ActivityReward;
  4. use Endroid\QrCode\Builder\Builder;
  5. use Endroid\QrCode\Writer\PngWriter;
  6. use GuzzleHttp\Client;
  7. use Telegram\Bot\Api;
  8. use App\Models\Config;
  9. use Telegram\Bot\FileUpload\InputFile;
  10. use Telegram\Bot\HttpClients\GuzzleHttpClient;
  11. use Illuminate\Support\Facades\Log;
  12. use App\Jobs\SendTelegramMessageJob;
  13. use App\Jobs\SendTelegramGroupMessageJob;
  14. abstract class BaseService
  15. {
  16. const YES = 1;
  17. const NOT = 0;
  18. public static string $MODEL = "";
  19. /**
  20. * @description: 模型
  21. * @return string
  22. */
  23. public static function model(): string
  24. {
  25. return static::$MODEL;
  26. }
  27. /**
  28. * @description: 获取查询条件
  29. * @param array $search
  30. * @return array
  31. */
  32. abstract public static function getWhere(array $search = []): array;
  33. /**
  34. * @description: 枚举
  35. * @return {*}
  36. */
  37. public static function enum(): string
  38. {
  39. return '';
  40. }
  41. /**
  42. * @description: 生成充值二维码
  43. * @param {*} $address 充值地址
  44. * @return {*}
  45. */
  46. public static function createRechargeQrCode($address = '')
  47. {
  48. $content = $address;
  49. $qrSize = 300;
  50. $font = 4;
  51. $textHeight = 20;
  52. $padding = 10;
  53. // 生成二维码图像对象
  54. $result = Builder::create()
  55. ->writer(new PngWriter())
  56. ->data($content)
  57. ->size($qrSize)
  58. ->margin(0)
  59. ->build();
  60. $qrImage = imagecreatefromstring($result->getString());
  61. // 创建画布(加上下方文字区和边距)
  62. $canvasWidth = $qrSize + $padding * 2;
  63. $canvasHeight = $qrSize + $textHeight + $padding * 2;
  64. $image = imagecreatetruecolor($canvasWidth, $canvasHeight);
  65. // 背景白色
  66. $white = imagecolorallocate($image, 255, 255, 255);
  67. imagefill($image, 0, 0, $white);
  68. // 黑色字体
  69. $black = imagecolorallocate($image, 0, 0, 0);
  70. // 合并二维码图像
  71. imagecopy($image, $qrImage, $padding, $padding, 0, 0, $qrSize, $qrSize);
  72. // 写文字
  73. $textWidth = imagefontwidth($font) * strlen($content);
  74. $x = ($canvasWidth - $textWidth) / 2;
  75. $y = $qrSize + $padding + 5;
  76. imagestring($image, $font, $x, $y, $content, $black);
  77. // 生成文件名
  78. $filename = $address . '.png';
  79. $relativePath = 'recharge/' . $filename;
  80. $storagePath = storage_path('app/public/' . $relativePath);
  81. // 确保目录存在
  82. @mkdir(dirname($storagePath), 0777, true);
  83. // 保存图片到文件
  84. imagepng($image, $storagePath);
  85. // 清理
  86. imagedestroy($qrImage);
  87. imagedestroy($image);
  88. // 返回 public 存储路径(可用于 URL)
  89. return 'storage/' . $relativePath; // 或返回 Storage::url($relativePath);
  90. }
  91. /**
  92. * 判断指定地址的二维码是否已生成(已存在文件)
  93. *
  94. * @param string $address 充值地址
  95. * @return
  96. */
  97. public static function rechargeQrCodeExists(string $address)
  98. {
  99. $filename = $address . '.png';
  100. $relativePath = 'recharge/' . $filename;
  101. $storagePath = storage_path('app/public/' . $relativePath);
  102. $path = '';
  103. if (file_exists($storagePath)) {
  104. $path = 'storage/' . $relativePath;
  105. }
  106. return $path;
  107. }
  108. /**
  109. * @description: 转成树形数据
  110. * @param {*} $list 初始数据
  111. * @param {*} $pid 父id
  112. * @param {*} $level 层级
  113. * @param {*} $pid_name pid字段名称 默认pid
  114. * @param {*} $id_name 主键id 名称
  115. * @return {*}
  116. */
  117. public static function toTree($list, $pid = 0, $level = 0, $pid_name = 'pid', $id_name = 'id')
  118. {
  119. $arr = [];
  120. $level++;
  121. foreach ($list as $k => $v) {
  122. if ($pid == $v[$pid_name]) {
  123. $v['level'] = $level;
  124. $v['children'] = self::toTree($list, $v[$id_name], $level, $pid_name, $id_name);
  125. $arr[] = $v;
  126. }
  127. }
  128. return $arr;
  129. }
  130. public static function buildTree($list,$pid_name='pid',$id_name='id')
  131. {
  132. // 创建映射表
  133. $map = [];
  134. foreach ($list as $item) {
  135. $map[$item[$id_name]] = $item;
  136. }
  137. // 找到顶级节点
  138. $topLevelNodes = [];
  139. foreach ($map as $key => $value) {
  140. if (!isset($map[$value[$pid_name]])) {
  141. $topLevelNodes[$key] = &$map[$key];
  142. }
  143. }
  144. // 构建树形结构
  145. $tree = [];
  146. foreach ($map as &$item) {
  147. if (isset($item[$pid_name]) && isset($map[$item[$pid_name]])) {
  148. $parent = &$map[$item[$pid_name]];
  149. if (!isset($parent['children'])) {
  150. $parent['children'] = [];
  151. }
  152. $parent['children'][] = &$item;
  153. } else {
  154. $tree[] = &$item;
  155. }
  156. }
  157. return $tree;
  158. }
  159. /**
  160. * @description: 实例化TG
  161. * @return {*}
  162. */
  163. public static function telegram()
  164. {
  165. return app(Api::class);
  166. }
  167. protected static function telegramForAttempt(int $attempt): Api
  168. {
  169. $transport = self::getTelegramTransportForAttempt($attempt);
  170. $clientOptions = [];
  171. if (!empty($transport['proxy_url'])) {
  172. $clientOptions['proxy'] = $transport['proxy_url'];
  173. }
  174. $telegram = new Api(config('services.telegram.token'), false, new GuzzleHttpClient(new Client($clientOptions)));
  175. $telegram->setTimeOut((int)$transport['timeout']);
  176. $telegram->setConnectTimeOut((int)$transport['connect_timeout']);
  177. return $telegram;
  178. }
  179. protected static function getTelegramTransportForAttempt(int $attempt): array
  180. {
  181. if ($attempt <= 1 || !config('services.telegram.proxy.enabled', true)) {
  182. return [
  183. 'name' => 'direct',
  184. 'proxy_url' => '',
  185. 'timeout' => (int)config('services.telegram.first_timeout', 5),
  186. 'connect_timeout' => (int)config('services.telegram.first_connect_timeout', 3),
  187. ];
  188. }
  189. return [
  190. 'name' => 'squid',
  191. 'proxy_url' => self::getTelegramProxyUrl(),
  192. 'timeout' => (int)config('services.telegram.proxy_timeout', 20),
  193. 'connect_timeout' => (int)config('services.telegram.proxy_connect_timeout', 8),
  194. ];
  195. }
  196. protected static function getTelegramProxyUrl(): string
  197. {
  198. $scheme = config('services.telegram.proxy.scheme', 'http');
  199. $host = config('services.telegram.proxy.host', '');
  200. $port = config('services.telegram.proxy.port', '3128');
  201. $username = (string)config('services.telegram.proxy.username', '');
  202. $password = (string)config('services.telegram.proxy.password', '');
  203. if (empty($host)) {
  204. return '';
  205. }
  206. if ($username === '' && $password === '') {
  207. return "{$scheme}://{$host}:{$port}";
  208. }
  209. if ($password === '') {
  210. return "{$scheme}://" . rawurlencode($username) . "@{$host}:{$port}";
  211. }
  212. return "{$scheme}://" . rawurlencode($username) . ':' . rawurlencode($password) . "@{$host}:{$port}";
  213. }
  214. protected static function sendTelegramRequest(callable $callback, string $action, array $context = [])
  215. {
  216. $maxAttempts = max(1, (int)config('services.telegram.retry_attempts', 2));
  217. $delaySeconds = max(1, (int)config('services.telegram.retry_delay_seconds', 3));
  218. for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
  219. $transport = self::getTelegramTransportForAttempt($attempt);
  220. try {
  221. return $callback(self::telegramForAttempt($attempt));
  222. } catch (\Throwable $exception) {
  223. $safeMessage = self::sanitizeTelegramError($exception->getMessage());
  224. $logContext = array_merge($context, [
  225. 'action' => $action,
  226. 'attempt' => $attempt,
  227. 'max_attempts' => $maxAttempts,
  228. 'transport' => $transport['name'],
  229. 'using_proxy' => !empty($transport['proxy_url']),
  230. 'timeout' => $transport['timeout'],
  231. 'connect_timeout' => $transport['connect_timeout'],
  232. 'error' => $safeMessage,
  233. 'exception' => get_class($exception),
  234. ]);
  235. if ($attempt >= $maxAttempts || !self::isRetryableTelegramError($safeMessage)) {
  236. Log::channel('issue')->warning('Telegram请求失败', $logContext);
  237. throw new \RuntimeException($safeMessage);
  238. }
  239. $retryAfter = self::getTelegramRetryAfter($safeMessage, $attempt, $delaySeconds);
  240. Log::channel('issue')->warning('Telegram请求失败,准备重试', $logContext + [
  241. 'retry_after_seconds' => $retryAfter,
  242. ]);
  243. sleep($retryAfter);
  244. }
  245. }
  246. }
  247. protected static function sanitizeTelegramError(string $message): string
  248. {
  249. return preg_replace('/bot\d+:[A-Za-z0-9_-]+/', 'bot[redacted]', $message);
  250. }
  251. protected static function isRetryableTelegramError(string $message): bool
  252. {
  253. return str_contains($message, 'Too Many Requests')
  254. || str_contains($message, 'retry after')
  255. || str_contains($message, 'cURL error 7')
  256. || str_contains($message, 'cURL error 28')
  257. || str_contains($message, 'Failed to connect')
  258. || str_contains($message, 'Connection timed out')
  259. || str_contains($message, 'Operation timed out');
  260. }
  261. protected static function getTelegramRetryAfter(string $message, int $attempt, int $delaySeconds): int
  262. {
  263. $maxDelaySeconds = max(1, (int)config('services.telegram.retry_max_delay_seconds', 10));
  264. if (preg_match('/retry after (\d+)/i', $message, $matches)) {
  265. return min($maxDelaySeconds, (int)$matches[1] + 1);
  266. }
  267. if (self::isTelegramConnectionError($message)) {
  268. return 0;
  269. }
  270. return min($maxDelaySeconds, $delaySeconds * $attempt);
  271. }
  272. protected static function isTelegramConnectionError(string $message): bool
  273. {
  274. return str_contains($message, 'cURL error 7')
  275. || str_contains($message, 'cURL error 28')
  276. || str_contains($message, 'Failed to connect')
  277. || str_contains($message, 'Connection timed out')
  278. || str_contains($message, 'Operation timed out');
  279. }
  280. // /**
  281. // * @description: 群组通知(自动分段发送,支持中文与多字节字符)
  282. // * @param string $text 通知内容
  283. // * @param array $keyboard 操作按钮
  284. // * @param string $image 图片路径(可选)
  285. // * @param bool $isTop 是否置顶第一条消息
  286. // */
  287. // public static function bettingGroupNotice($text, $keyboard = [], $image = '', $isTop = false)
  288. // {
  289. // $bettingGroup = Config::where('field', 'betting_group')->first()->val;
  290. // $telegram = self::telegram();
  291. //
  292. // $maxLen = 1024; // Telegram 限制:最多 1024 个字符
  293. // $textParts = [];
  294. // $textLength = mb_strlen($text, 'UTF-8');
  295. // for ($i = 0; $i < $textLength; $i += $maxLen) {
  296. // $textParts[] = mb_substr($text, $i, $maxLen, 'UTF-8');
  297. // }
  298. //
  299. // $firstMessageId = null;
  300. //
  301. // foreach ($textParts as $index => $partText) {
  302. // $botMsg = [
  303. // 'chat_id' => "@{$bettingGroup}",
  304. // 'text' => $partText,
  305. // ];
  306. //
  307. // if (count($keyboard) > 0 && $index === 0) {
  308. // $botMsg['reply_markup'] = json_encode(['inline_keyboard' => $keyboard]);
  309. // }
  310. //
  311. // if (!empty($image) && $index === 0) {
  312. // // 第一条带图片
  313. // $botMsg['photo'] = InputFile::create($image);
  314. // $botMsg['caption'] = $partText;
  315. // $botMsg['protect_content'] = true;
  316. // $response = $telegram->sendPhoto($botMsg);
  317. // } else {
  318. // $response = $telegram->sendMessage($botMsg);
  319. // }
  320. //
  321. // if ($isTop && $index === 0 && $response && $response->get('message_id')) {
  322. // $firstMessageId = $response->get('message_id');
  323. // }
  324. //
  325. // // 防止限流(可选)
  326. // usleep(300000);
  327. // }
  328. //
  329. // if ($isTop && $firstMessageId) {
  330. // $telegram->pinChatMessage([
  331. // 'chat_id' => "@{$bettingGroup}",
  332. // 'message_id' => $firstMessageId
  333. // ]);
  334. // }
  335. // }
  336. /**
  337. * @description: 群组通知
  338. * @apiParam string $text 通知内容
  339. * @apiParam array $keyboard 操作按钮
  340. * @apiParam string $separator 分隔符
  341. * @apiParam boolean $isTop 是否置顶
  342. */
  343. public static function bettingGroupNotice($text, $keyboard = [], $image = '', $isTop = false, $separator = "\n"): array
  344. {
  345. $bettingGroup = Config::where('field', 'betting_group')->first()->val;
  346. if (empty($separator)) $separator = "\n";
  347. $array = explode($separator, $text);
  348. $res = [];
  349. // 为空只发图片
  350. if (empty($text) && !empty($image)) {
  351. $botMsg = [
  352. 'chat_id' => "@{$bettingGroup}",
  353. ];
  354. $botMsg['photo'] = InputFile::create($image);
  355. $botMsg['caption'] = $text;
  356. $botMsg['protect_content'] = true; // 防止转发
  357. if (count($keyboard) > 0) {
  358. $botMsg['reply_markup'] = json_encode(['inline_keyboard' => $keyboard]);
  359. }
  360. $response = self::sendTelegramRequest(fn(Api $telegram) => $telegram->sendPhoto($botMsg), 'sendPhoto', [
  361. 'chat_id' => $botMsg['chat_id'],
  362. 'has_image' => true,
  363. ]);
  364. } else {
  365. foreach ($array as $key => $line) {
  366. if (empty(str_ireplace(" ", "", str_ireplace("\n", '', $line)))) {
  367. unset($array[$key]);
  368. } else {
  369. $array[$key] .= $separator;
  370. }
  371. }
  372. $texts = [];
  373. $len = !empty($image) ? 1024 : 4096;
  374. foreach ($array as $item) {
  375. if (count($texts) > 1) $len = 4096;
  376. if (count($texts) == 0 || strlen($texts[count($texts) - 1] . $item) > $len) {
  377. $texts[] = $item;
  378. } else {
  379. $texts[count($texts) - 1] .= $item;
  380. }
  381. }
  382. foreach ($texts as $index => $item) {
  383. $botMsg = [
  384. 'chat_id' => "@{$bettingGroup}",
  385. 'text' => $item,
  386. ];
  387. if ($index > 0) {
  388. $res[] = $botMsg;
  389. self::sendTelegramRequest(fn(Api $telegram) => $telegram->sendMessage($botMsg), 'sendMessage', [
  390. 'chat_id' => $botMsg['chat_id'],
  391. 'chunk_index' => $index,
  392. 'has_image' => false,
  393. ]);
  394. } else {
  395. if (count($keyboard) > 0) {
  396. $botMsg['reply_markup'] = json_encode(['inline_keyboard' => $keyboard]);
  397. }
  398. if (!empty($image)) {
  399. unset($botMsg['text']);
  400. $botMsg['photo'] = InputFile::create($image);
  401. $botMsg['caption'] = $item;
  402. $botMsg['protect_content'] = true;
  403. $res[] = $botMsg;
  404. $response = self::sendTelegramRequest(fn(Api $telegram) => $telegram->sendPhoto($botMsg), 'sendPhoto', [
  405. 'chat_id' => $botMsg['chat_id'],
  406. 'chunk_index' => $index,
  407. 'has_image' => true,
  408. ]);
  409. } else {
  410. $res[] = $botMsg;
  411. $response = self::sendTelegramRequest(fn(Api $telegram) => $telegram->sendMessage($botMsg), 'sendMessage', [
  412. 'chat_id' => $botMsg['chat_id'],
  413. 'chunk_index' => $index,
  414. 'has_image' => false,
  415. ]);
  416. }
  417. if ($isTop === true) {
  418. self::sendTelegramRequest(fn(Api $telegram) => $telegram->pinChatMessage([
  419. 'chat_id' => "@{$bettingGroup}",
  420. 'message_id' => $response->get('message_id')
  421. ]), 'pinChatMessage', [
  422. 'chat_id' => "@{$bettingGroup}",
  423. 'message_id' => $response->get('message_id'),
  424. ]);
  425. }
  426. }
  427. }
  428. }
  429. return $res;
  430. }
  431. /**
  432. * @description: 异步群组通知
  433. * @param {string} $text 通知内容
  434. * @param {array} $keyboard 操作按钮
  435. * @param {*string} $image 图片
  436. * @return {*}
  437. */
  438. public static function asyncBettingGroupNotice($text, $keyboard = [], $image = '', $isTop = false): void
  439. {
  440. SendTelegramGroupMessageJob::dispatch($text, $keyboard, $image, $isTop);
  441. }
  442. /**
  443. * @description: 发送消息
  444. * @param {string} $chatId 聊天ID
  445. * @param {string} $text 消息内容
  446. * @param {array} $keyboard 操作按钮
  447. * @param {*string} $image 图片
  448. * @return {*}
  449. */
  450. public static function sendMessage($chatId, $text, $keyboard = [], $image = ''): void
  451. {
  452. $botMsg = [
  453. 'chat_id' => $chatId,
  454. ];
  455. if (count($keyboard) > 0) {
  456. $botMsg['reply_markup'] = json_encode(['inline_keyboard' => $keyboard]);
  457. }
  458. if ($image != '') {
  459. $botMsg['photo'] = InputFile::create($image);
  460. $botMsg['caption'] = $text;
  461. $botMsg['protect_content'] = false; // 防止转发
  462. self::sendTelegramRequest(fn(Api $telegram) => $telegram->sendPhoto($botMsg), 'sendPhoto', [
  463. 'chat_id' => $chatId,
  464. 'has_image' => true,
  465. ]);
  466. } else {
  467. $botMsg['text'] = $text;
  468. self::sendTelegramRequest(fn(Api $telegram) => $telegram->sendMessage($botMsg), 'sendMessage', [
  469. 'chat_id' => $chatId,
  470. 'has_image' => false,
  471. ]);
  472. }
  473. }
  474. /**
  475. * @description: 异步发送消息
  476. * @param {string} $chatId 聊天ID
  477. * @param {string} $text 消息内容
  478. * @param {array} $keyboard 操作按钮
  479. * @param {*string} $image 图片
  480. * @return {*}
  481. */
  482. public static function asyncSendMessage($chatId, $text, $keyboard = [], $image = ''): void
  483. {
  484. SendTelegramMessageJob::dispatch($chatId, $text, $keyboard, $image);
  485. }
  486. /**
  487. * @description: 弹窗提示
  488. * @param {*} $memberId
  489. * @param {*} $address
  490. * @return {*}
  491. */
  492. public static function alertNotice($callbackId, $text): void
  493. {
  494. self::telegram()->answerCallbackQuery([
  495. 'callback_query_id' => $callbackId,
  496. 'text' => $text,
  497. 'show_alert' => true // 显示为弹窗
  498. ]);
  499. }
  500. public static function log($message, $context = [])
  501. {
  502. Log::error($message, $context);
  503. }
  504. /**
  505. * @description: 获取操作按钮
  506. * @return {*}
  507. */
  508. public static function getOperateButton()
  509. {
  510. $replyInfo = KeyboardService::findOne(['button' => '投注菜单']);
  511. if ($replyInfo && $replyInfo->buttons) {
  512. $buttons = json_decode($replyInfo->buttons, true);
  513. foreach ($buttons as $row) {
  514. $inlineButton[] = [];
  515. foreach ($row as $button) {
  516. $btn = ['text' => $button['text']];
  517. if (strpos($button['url'], 'http') === 0) {
  518. $btn['url'] = $button['url'];
  519. } else {
  520. $btn['callback_data'] = $button['url'];
  521. }
  522. $inlineButton[count($inlineButton) - 1][] = $btn;
  523. }
  524. }
  525. $inlineButton = array_values($inlineButton);
  526. return $inlineButton;
  527. }
  528. $inlineButton = [];
  529. return $inlineButton;
  530. }
  531. // 获取字符串最后几个字符
  532. public static function getLastChar($str, $num = 1)
  533. {
  534. $length = mb_strlen($str, 'UTF-8');
  535. $lastChar = mb_substr($str, $length - 1, $num, 'UTF-8');
  536. return $lastChar;
  537. }
  538. public static function generateRandomString($length = 8)
  539. {
  540. $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
  541. $randomString = '';
  542. for ($i = 0; $i < $length; $i++) {
  543. $randomString .= $characters[rand(0, strlen($characters) - 1)];
  544. }
  545. return $randomString;
  546. }
  547. public static function generateRandomNumber($length = 8)
  548. {
  549. $characters = '0123456789';
  550. $randomString = '';
  551. for ($i = 0; $i < $length; $i++) {
  552. $randomString .= rand(1, 9);
  553. }
  554. return $randomString;
  555. }
  556. public static function hideMiddleDigits($number, $hideCount = 4)
  557. {
  558. $length = strlen($number);
  559. if ($length <= $hideCount) {
  560. // 数字太短,全部隐藏
  561. return str_repeat("*", $length);
  562. }
  563. // 计算中间开始隐藏的位置
  564. $startLen = floor(($length - $hideCount) / 2);
  565. $endLen = $length - $hideCount - $startLen;
  566. $start = substr($number, 0, $startLen);
  567. $end = substr($number, -$endLen);
  568. return $start . str_repeat("*", $hideCount) . $end;
  569. }
  570. // 生成订单号
  571. public static function createOrderNo($prefix = 'pc28_', $memberId = null)
  572. {
  573. // 处理会员ID,获取后四位
  574. if ($memberId) {
  575. $memberSuffix = str_pad(substr($memberId, -4), 4, '0', STR_PAD_LEFT);
  576. } else {
  577. $memberSuffix = '0000'; // 默认值
  578. }
  579. // 时间部分
  580. $timePart = date('YmdHis');
  581. // 随机部分增加唯一性
  582. $randomPart = mt_rand(1000, 9999);
  583. return $prefix . $timePart . $randomPart . $memberSuffix;
  584. }
  585. /**
  586. * @description: 生成支付二维码
  587. * @param {*} $address 支付地址
  588. * @return {*}
  589. */
  590. public static function createPaymentQrCode($address = '')
  591. {
  592. // $content = $address;
  593. $content = '';
  594. $qrSize = 300;
  595. $font = 4;
  596. $textHeight = 20;
  597. $padding = 10;
  598. // 生成二维码图像对象
  599. $result = Builder::create()
  600. ->writer(new PngWriter())
  601. ->data($address)
  602. ->size($qrSize)
  603. ->margin(0)
  604. ->build();
  605. $qrImage = imagecreatefromstring($result->getString());
  606. // 创建画布(加上下方文字区和边距)
  607. $canvasWidth = $qrSize + $padding * 2;
  608. $canvasHeight = $qrSize + $textHeight + $padding * 2;
  609. $image = imagecreatetruecolor($canvasWidth, $canvasHeight);
  610. // 背景白色
  611. $white = imagecolorallocate($image, 255, 255, 255);
  612. imagefill($image, 0, 0, $white);
  613. // 黑色字体
  614. $black = imagecolorallocate($image, 0, 0, 0);
  615. // 合并二维码图像
  616. imagecopy($image, $qrImage, $padding, $padding, 0, 0, $qrSize, $qrSize);
  617. // 写文字
  618. $textWidth = imagefontwidth($font) * strlen($content);
  619. $x = ($canvasWidth - $textWidth) / 2;
  620. $y = $qrSize + $padding + 5;
  621. imagestring($image, $font, $x, $y, $content, $black);
  622. $address_name = self::generateRandomString(20) . time();
  623. // 生成文件名
  624. $filename = $address_name . '.png';
  625. $relativePath = 'payment/' . $filename;
  626. $storagePath = storage_path('app/public/' . $relativePath);
  627. // 确保目录存在
  628. @mkdir(dirname($storagePath), 0777, true);
  629. // 保存图片到文件
  630. imagepng($image, $storagePath);
  631. // 清理
  632. imagedestroy($qrImage);
  633. imagedestroy($image);
  634. // 返回 public 存储路径(可用于 URL)
  635. return 'storage/' . $relativePath; // 或返回 Storage::url($relativePath);
  636. }
  637. }