ThirdGameOrderService.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. <?php
  2. namespace App\Services;
  3. use App\Models\ThirdGameOrder as ThirdGameOrderModel;
  4. use App\Services\AdminRealtime\AdminRealtime;
  5. use App\Services\Sharing\EffectiveBet as Money;
  6. use Carbon\Carbon;
  7. use Illuminate\Database\UniqueConstraintViolationException;
  8. use Illuminate\Support\Facades\DB;
  9. class ThirdGameOrderService
  10. {
  11. private const GAME_TYPE_NAMES = [
  12. 1 => '视讯',
  13. 2 => '老虎机',
  14. 3 => '彩票',
  15. 4 => '体育',
  16. 5 => '电竞',
  17. 6 => '捕猎',
  18. 7 => '棋牌',
  19. ];
  20. private const STATUS_NAMES = [
  21. 0 => '未完成',
  22. 1 => '已完成',
  23. 2 => '已取消',
  24. 3 => '已撤单',
  25. ];
  26. private ThirdGameBalanceService $provider;
  27. public function __construct(ThirdGameBalanceService $provider)
  28. {
  29. $this->provider = $provider;
  30. }
  31. public function syncHistory(
  32. string $startTime,
  33. string $endTime,
  34. int $page = 1,
  35. int $limit = 2000
  36. ): array {
  37. return $this->persistProviderResult(
  38. $this->provider->historyOrders($startTime, $endTime, $page, $limit), $page, $limit
  39. );
  40. }
  41. public function syncRealtime(int $page = 1, int $limit = 2000): array
  42. {
  43. return $this->persistProviderResult($this->provider->realtimeOrders($page, $limit), $page, $limit);
  44. }
  45. private function persistProviderResult(array $result, int $page = 1, int $limit = 2000): array
  46. {
  47. if (empty($result['ok'])) {
  48. return $result;
  49. }
  50. $orders = is_array($result['list'] ?? null) ? $result['list'] : [];
  51. $received = count($orders);
  52. $testNamespace = $this->provider->playerPrefix() === 'tu';
  53. $orders = array_values(array_filter($orders, static function (array $order) use ($testNamespace): bool {
  54. $isTestPlayer = str_starts_with(trim((string) ($order['player_id'] ?? '')), 'tu');
  55. return $testNamespace === $isTestPlayer;
  56. }));
  57. $users = $this->usersByPlayerId(array_values(array_unique(array_filter(array_map(
  58. static fn(array $order): string => trim((string) ($order['player_id'] ?? '')),
  59. $orders
  60. )))));
  61. $now = Carbon::now(config('app.timezone', 'Asia/Shanghai'))->format('Y-m-d H:i:s');
  62. $rows = [];
  63. foreach ($orders as $order) {
  64. $platform = trim((string) ($order['platform'] ?? ''));
  65. $gameOrderId = trim((string) ($order['game_order_id'] ?? ''));
  66. if ($gameOrderId === '') {
  67. continue;
  68. }
  69. $playerId = trim((string) ($order['player_id'] ?? ''));
  70. $identity = $this->identity($platform, $playerId, $gameOrderId);
  71. $updated = $this->dateTime($order['last_update_time'] ?? null);
  72. $status = min(255, max(0, (int) ($order['status'] ?? 0)));
  73. if (isset($rows[$identity])
  74. && $this->isStale($updated, $rows[$identity]['last_update_time'], $rows[$identity]['status'])) {
  75. continue;
  76. }
  77. $rows[$identity] = [
  78. 'order_key' => $identity,
  79. 'player_id' => $playerId,
  80. 'platform' => $platform,
  81. 'currency' => trim((string) ($order['currency'] ?? '')),
  82. 'game_type' => min(255, max(0, (int) ($order['game_type'] ?? 0))),
  83. 'game_name' => (string) ($order['game_name'] ?? ''),
  84. 'round_no' => (string) ($order['round'] ?? ''),
  85. 'table_no' => (string) ($order['table'] ?? ''),
  86. 'seat' => (string) ($order['seat'] ?? ''),
  87. 'bet_amount' => $this->decimal($order['bet_amount'] ?? null),
  88. 'valid_amount' => $this->decimal($order['valid_amount'] ?? null),
  89. 'settled_amount' => $this->decimal($order['settled_amount'] ?? null),
  90. 'bet_content' => $this->text($order['bet_content'] ?? ''),
  91. 'status' => $status,
  92. 'game_order_id' => $gameOrderId,
  93. 'bet_time' => $this->dateTime($order['bet_time'] ?? null),
  94. 'last_update_time' => $updated,
  95. 'created_at' => $now,
  96. 'updated_at' => $now,
  97. ];
  98. }
  99. $synced = 0;
  100. $unmatched = 0;
  101. $matchedUserIds = [];
  102. if ($rows !== []) {
  103. $commission = app(ThirdGameCommissionService::class);
  104. $history = $commission->backfillState();
  105. $rates = [];
  106. foreach ($rows as $row) {
  107. if (strtoupper($row['currency']) === $commission::currency()
  108. && $commission::result($row['status'], $row['settled_amount']) === 'loss') {
  109. $rates = $commission->rates()['ratios'];
  110. break;
  111. }
  112. }
  113. ksort($rows);
  114. foreach (array_chunk($rows, 200, true) as $batch) {
  115. $written = $this->writeOrders($batch, $users, $commission, $rates, $history);
  116. $synced += count($written);
  117. foreach ($written as $row) {
  118. if ($row['user_id'] === null) {
  119. ++$unmatched;
  120. } else {
  121. $matchedUserIds[(int) $row['user_id']] = true;
  122. }
  123. }
  124. }
  125. }
  126. $pageNo = (int) ($result['page_no'] ?? $page);
  127. $pageSize = (int) ($result['page_size'] ?? $limit);
  128. $total = (int) ($result['total'] ?? $received);
  129. return [
  130. 'ok' => true,
  131. 'received' => $received,
  132. 'synced' => $synced,
  133. 'matched_users' => count($matchedUserIds),
  134. 'unmatched_orders' => $unmatched,
  135. 'total' => $total,
  136. 'page_no' => $pageNo,
  137. 'page_size' => $pageSize,
  138. 'has_more' => $pageSize > 0 && $pageNo * $pageSize < $total,
  139. ];
  140. }
  141. private function writeOrders(array $batch, array $users, ThirdGameCommissionService $commission, array $rates, array $history): array
  142. {
  143. for ($attempt = 1; ; ++$attempt) {
  144. try {
  145. [$written, $existing] = DB::transaction(function () use ($batch, $users, $commission, $rates, $history) {
  146. $existing = ThirdGameOrderModel::query()->whereIn('order_key', array_keys($batch))
  147. ->orderBy('order_key')->lockForUpdate()->get([
  148. 'id', 'order_key', 'user_id', 'member_id', 'username', 'first_name', 'currency',
  149. 'status', 'settled_amount', 'last_update_time', 'third_game_commission_ratio', 'third_game_commission',
  150. ])->keyBy('order_key')->all();
  151. $new = [];
  152. $updates = [];
  153. foreach ($batch as $key => $row) {
  154. $previous = $existing[$key] ?? null;
  155. if ($previous !== null && $this->isStale($row['last_update_time'],
  156. $previous->getRawOriginal('last_update_time'), (int) $previous->getRawOriginal('status'))) {
  157. continue;
  158. }
  159. $user = $users[$row['player_id']] ?? null;
  160. $row['user_id'] = $user->id ?? $previous->user_id ?? null;
  161. $row['member_id'] = $user->member_id ?? $previous->member_id ?? null;
  162. $row['username'] = (string) ($user->username ?? $previous->username ?? '');
  163. $row['first_name'] = (string) ($user->first_name ?? $previous->first_name ?? '');
  164. $row = $this->applyCommission($row, $previous, $commission, $rates, $history);
  165. if ($previous === null) {
  166. $new[$key] = $row;
  167. } else {
  168. $updates[$key] = $row;
  169. }
  170. }
  171. if ($new !== []) {
  172. // A concurrent insert must be re-read before applying a fee snapshot.
  173. ThirdGameOrderModel::query()->insert(array_values($new));
  174. }
  175. if ($updates !== []) {
  176. ThirdGameOrderModel::query()->upsert(array_values($updates), ['order_key'], [
  177. 'user_id', 'member_id', 'username', 'first_name', 'player_id', 'currency',
  178. 'game_type', 'game_name', 'round_no', 'table_no', 'seat', 'bet_amount',
  179. 'valid_amount', 'settled_amount', 'bet_content', 'status', 'bet_time',
  180. 'last_update_time', 'updated_at', 'third_game_commission_ratio', 'third_game_commission',
  181. ]);
  182. }
  183. return [$new + $updates, $existing];
  184. }, 3);
  185. break;
  186. } catch (UniqueConstraintViolationException $e) {
  187. if ($attempt === 3) {
  188. throw $e;
  189. }
  190. }
  191. }
  192. $changedKeys = array_keys(array_filter($written, fn (array $row, string $key): bool
  193. => $this->snapshotChanged($existing[$key] ?? null, $row), ARRAY_FILTER_USE_BOTH));
  194. $this->recordRealtime($changedKeys, $existing);
  195. return $written;
  196. }
  197. /**
  198. * Whether an incoming version is older than the stored one. Versions are ordered by the provider update time;
  199. * a version without one cannot be ordered, so it may only replace an unfinished version, never a finished
  200. * (settled/cancelled) one. A timed version always replaces an untimed one.
  201. */
  202. private function isStale(?string $candidateTime, ?string $currentTime, int $currentStatus): bool
  203. {
  204. if ($candidateTime === null) return $currentStatus !== 0;
  205. return $currentTime !== null && $candidateTime < $currentTime;
  206. }
  207. private function applyCommission(array $row, ?ThirdGameOrderModel $previous, ThirdGameCommissionService $commission, array $rates, array $history): array
  208. {
  209. $currency = strtoupper($row['currency']);
  210. $ratio = $previous !== null && $currency === strtoupper(trim((string) $previous->currency))
  211. ? $previous->third_game_commission_ratio : null;
  212. $result = $commission::result($row['status'], $row['settled_amount']);
  213. if ($result === 'loss') {
  214. if ($ratio === null && $currency === $commission::currency()) {
  215. if ($commission->isHistorical($previous?->id, $row['last_update_time'], $history)) {
  216. $rates = ($history['currency'] ?? null) === $currency ? $history['snapshot']['ratios'] : [];
  217. }
  218. $ratio = $rates[strtolower($row['platform'])] ?? null;
  219. }
  220. $amount = $ratio === null ? null : $commission::charge($row['settled_amount'], $ratio);
  221. } else {
  222. $amount = $row['status'] === 1 && $result === null ? null : Money::zero();
  223. }
  224. return $row + ['third_game_commission_ratio' => $ratio, 'third_game_commission' => $amount];
  225. }
  226. public function syncForList(array $params): array
  227. {
  228. if ((int) ($params['page'] ?? 1) > 1) {
  229. return ['attempted' => false, 'ok' => true, 'message' => ''];
  230. }
  231. $result = $this->syncRealtime(1, 2000);
  232. if (empty($result['ok'])) {
  233. return [
  234. 'attempted' => true,
  235. 'ok' => false,
  236. 'message' => (string) ($result['msg'] ?? '三方订单同步失败'),
  237. ];
  238. }
  239. unset($result['ok']);
  240. return array_merge([
  241. 'attempted' => true,
  242. 'ok' => true,
  243. 'message' => '',
  244. ], $result);
  245. }
  246. public function paginate(array $params): array
  247. {
  248. $page = max(1, (int) ($params['page'] ?? 1));
  249. $limit = min(200, max(1, (int) ($params['limit'] ?? 20)));
  250. $query = ThirdGameOrderModel::query()
  251. ->leftJoin('users', 'users.id', '=', 'third_game_orders.user_id');
  252. if ($this->provider->playerPrefix() === 'tu') $query->where('third_game_orders.player_id', 'like', 'tu%');
  253. else $query->where('third_game_orders.player_id', 'not like', 'tu%');
  254. if (!empty($params['user_id'])) {
  255. $userId = (int) $params['user_id'];
  256. $query->where(function ($query) use ($userId) {
  257. $query->where('users.id', $userId)
  258. ->orWhere(function ($query) use ($userId) {
  259. $query->whereNull('users.id')
  260. ->where('third_game_orders.user_id', $userId);
  261. });
  262. });
  263. }
  264. if (!empty($params['member_id'])) {
  265. $memberId = (string) $params['member_id'];
  266. $query->where(function ($query) use ($memberId) {
  267. $query->where('users.member_id', $memberId)
  268. ->orWhere(function ($query) use ($memberId) {
  269. $query->whereNull('users.id')
  270. ->where('third_game_orders.member_id', $memberId);
  271. });
  272. });
  273. }
  274. if (!empty($params['username'])) {
  275. $username = '%' . $params['username'] . '%';
  276. $query->where(function ($query) use ($username) {
  277. $query->where('users.username', 'like', $username)
  278. ->orWhere(function ($query) use ($username) {
  279. $query->whereNull('users.id')
  280. ->where('third_game_orders.username', 'like', $username);
  281. });
  282. });
  283. }
  284. if (!empty($params['account'])) {
  285. $query->where('users.account', 'like', '%' . trim((string) $params['account']) . '%');
  286. }
  287. if (!empty($params['first_name'])) {
  288. $firstName = '%' . $params['first_name'] . '%';
  289. $query->where(function ($query) use ($firstName) {
  290. $query->where('users.first_name', 'like', $firstName)
  291. ->orWhere(function ($query) use ($firstName) {
  292. $query->whereNull('users.id')
  293. ->where('third_game_orders.first_name', 'like', $firstName);
  294. });
  295. });
  296. }
  297. // 内部参数:后台实时推送按主键取单行
  298. if (!empty($params['id'])) {
  299. $query->where('third_game_orders.id', (int) $params['id']);
  300. }
  301. if (!empty($params['player_id'])) {
  302. $query->where('third_game_orders.player_id', (string) $params['player_id']);
  303. }
  304. if (!empty($params['platform'])) {
  305. $query->where('third_game_orders.platform', (string) $params['platform']);
  306. }
  307. if (!empty($params['game_order_id'])) {
  308. $query->where('third_game_orders.game_order_id', (string) $params['game_order_id']);
  309. }
  310. if (isset($params['game_type']) && $params['game_type'] !== '') {
  311. $query->where('third_game_orders.game_type', (int) $params['game_type']);
  312. }
  313. if (isset($params['status']) && $params['status'] !== '') {
  314. $query->where('third_game_orders.status', (int) $params['status']);
  315. }
  316. if (!empty($params['start_time']) && !empty($params['end_time'])) {
  317. $query->whereBetween('third_game_orders.last_update_time', [
  318. $params['start_time'],
  319. $params['end_time'],
  320. ]);
  321. }
  322. $total = (clone $query)->count('third_game_orders.id');
  323. $list = $query
  324. ->select('third_game_orders.*')
  325. ->addSelect([
  326. 'users.id as current_user_id',
  327. 'users.member_id as current_member_id',
  328. 'users.username as current_username',
  329. 'users.account as current_account',
  330. 'users.first_name as current_first_name',
  331. ])
  332. ->orderByDesc('third_game_orders.last_update_time')
  333. ->orderByDesc('third_game_orders.id')
  334. ->forPage($page, $limit)
  335. ->get()
  336. ->map(fn(ThirdGameOrderModel $order): array => $this->format($order))
  337. ->all();
  338. return [
  339. 'total' => $total,
  340. 'page' => $page,
  341. 'limit' => $limit,
  342. 'list' => $list,
  343. 'options' => [
  344. 'game_types' => $this->options(self::GAME_TYPE_NAMES),
  345. 'statuses' => $this->options(self::STATUS_NAMES),
  346. ],
  347. ];
  348. }
  349. /**
  350. * @return array<string, object>
  351. */
  352. private function usersByPlayerId(array $playerIds): array
  353. {
  354. $requested = []; $prefix = $this->provider->playerPrefix();
  355. $pattern = '/^' . preg_quote($prefix, '/') . '(\d{1,' . (11 - strlen($prefix)) . '})$/D';
  356. foreach ($playerIds as $playerId) {
  357. $playerId = trim((string) $playerId);
  358. if (!preg_match($pattern, $playerId, $matches)) {
  359. continue;
  360. }
  361. $userId = filter_var(ltrim($matches[1], '0'), FILTER_VALIDATE_INT, [
  362. 'options' => ['min_range' => 1, 'max_range' => PHP_INT_MAX],
  363. ]);
  364. if ($userId !== false && ($playerId === $prefix . $userId || $playerId === $this->provider->playerIdFromUserId($userId))) {
  365. $requested[(int) $userId][$playerId] = true;
  366. }
  367. }
  368. $result = $this->usersByLegacyPlayerId($playerIds);
  369. if ($requested === []) {
  370. return $result;
  371. }
  372. $rows = DB::table('users')
  373. ->select(['id', 'member_id', 'username', 'first_name'])
  374. ->whereIn('id', array_keys($requested))
  375. ->get();
  376. foreach ($rows as $row) {
  377. foreach (array_keys($requested[(int) $row->id] ?? []) as $playerId) {
  378. $result[$playerId] = $row;
  379. }
  380. }
  381. return $result;
  382. }
  383. /**
  384. * 2026-08-24 之前的三方账号为 p + md5(sn_member_id) 前10位,历史订单仍使用该账号。
  385. *
  386. * @return array<string, object>
  387. */
  388. private function usersByLegacyPlayerId(array $playerIds): array
  389. {
  390. $legacy = [];
  391. foreach ($playerIds as $playerId) {
  392. $playerId = trim((string) $playerId);
  393. if (preg_match('/^p[0-9a-f]{10}$/D', $playerId)) $legacy[$playerId] = true;
  394. }
  395. if ($legacy === []) {
  396. return [];
  397. }
  398. $sn = (string) config('third_game.sn');
  399. $expression = "CONCAT('p', SUBSTRING(MD5(CONCAT(?, '_', `member_id`)), 1, 10))";
  400. $placeholders = implode(',', array_fill(0, count($legacy), '?'));
  401. $rows = DB::table('users')
  402. ->select(['id', 'member_id', 'username', 'first_name'])
  403. ->selectRaw($expression . ' AS legacy_player_id', [$sn])
  404. ->whereRaw($expression . " IN ({$placeholders})", array_merge([$sn], array_keys($legacy)))
  405. ->get();
  406. $result = [];
  407. foreach ($rows as $row) {
  408. $result[(string) $row->legacy_player_id] = $row;
  409. }
  410. return $result;
  411. }
  412. /** 补齐同步时未能识别旧账号而缺少会员信息的订单。 */
  413. public function backfillLegacyPlayers(): int
  414. {
  415. $updated = 0;
  416. $playerIds = ThirdGameOrderModel::query()->whereNull('user_id')->where('player_id', 'like', 'p%')
  417. ->distinct()->pluck('player_id')->all();
  418. foreach (array_chunk($playerIds, 200) as $chunk) {
  419. foreach ($this->usersByLegacyPlayerId($chunk) as $playerId => $user) {
  420. $ids = ThirdGameOrderModel::query()->whereNull('user_id')->where('player_id', $playerId)->pluck('id')->all();
  421. $updated += ThirdGameOrderModel::query()->whereNull('user_id')->where('player_id', $playerId)->update([
  422. 'user_id' => $user->id,
  423. 'member_id' => $user->member_id,
  424. 'username' => (string) ($user->username ?? ''),
  425. 'first_name' => (string) ($user->first_name ?? ''),
  426. ]);
  427. AdminRealtime::record('third_game_orders', $ids);
  428. }
  429. }
  430. return $updated;
  431. }
  432. /**
  433. * @return array<string, ThirdGameOrderModel>
  434. */
  435. /** 只推送新订单及状态、结算、抽成、会员归属等有实质变化的订单。 */
  436. private function snapshotChanged($previous, array $row): bool
  437. {
  438. if ($previous === null) {
  439. return true;
  440. }
  441. $settled = $previous->getRawOriginal('settled_amount');
  442. $settledChanged = is_numeric($settled) && is_numeric($row['settled_amount'])
  443. ? bccomp((string) $settled, (string) $row['settled_amount'], 10) !== 0
  444. : $settled !== $row['settled_amount'];
  445. return $settledChanged
  446. || $previous->third_game_commission_ratio !== $row['third_game_commission_ratio']
  447. || $previous->third_game_commission !== $row['third_game_commission']
  448. || $previous->currency !== $row['currency']
  449. || (int) $previous->getRawOriginal('status') !== (int) $row['status']
  450. || (string) $previous->getRawOriginal('last_update_time') !== (string) $row['last_update_time']
  451. || (string) $previous->getRawOriginal('user_id') !== (string) $row['user_id'];
  452. }
  453. private function recordRealtime(array $changedKeys, array $existing): void
  454. {
  455. if ($changedKeys === [] || !config('admin_realtime.enabled')) {
  456. return;
  457. }
  458. try {
  459. $ids = ThirdGameOrderModel::query()->whereIn('order_key', $changedKeys)->pluck('id', 'order_key')->all();
  460. $created = array_values(array_intersect_key($ids, array_diff_key(array_flip($changedKeys), $existing)));
  461. AdminRealtime::record('third_game_orders', $created, 'created');
  462. AdminRealtime::record('third_game_orders', array_values(array_diff($ids, $created)));
  463. } catch (\Throwable $e) {
  464. \Illuminate\Support\Facades\Log::channel('realtime')->error('admin_realtime_third_game_failed', ['error' => $e->getMessage()]);
  465. }
  466. }
  467. private function format(ThirdGameOrderModel $order): array
  468. {
  469. $userId = $order->current_user_id ?? $order->user_id;
  470. $memberId = $order->current_member_id ?? $order->member_id;
  471. $username = $order->current_username ?? $order->username;
  472. $firstName = $order->current_first_name ?? $order->first_name;
  473. $gameType = (int) $order->game_type;
  474. $status = (int) $order->status;
  475. $result = ThirdGameCommissionService::result($status, $order->settled_amount);
  476. $commission = $result === 'loss' ? $order->third_game_commission
  477. : ($status === 1 && $result === null ? null : Money::zero());
  478. return [
  479. 'id' => (int) $order->id,
  480. 'user_id' => $userId === null ? null : (int) $userId,
  481. 'member_id' => $memberId === null ? null : (string) $memberId,
  482. 'username' => (string) ($username ?? ''),
  483. 'account' => (string) ($order->current_account ?? ''),
  484. 'first_name' => (string) ($firstName ?? ''),
  485. 'player_id' => (string) $order->player_id,
  486. 'platform' => (string) $order->platform,
  487. 'currency' => (string) $order->currency,
  488. 'game_type' => $gameType,
  489. 'game_type_text' => self::GAME_TYPE_NAMES[$gameType] ?? '未知',
  490. 'game_name' => (string) $order->game_name,
  491. 'round' => (string) $order->round_no,
  492. 'table' => (string) $order->table_no,
  493. 'seat' => (string) $order->seat,
  494. 'bet_amount' => $this->number($order->bet_amount),
  495. 'valid_amount' => $this->number($order->valid_amount),
  496. 'settled_amount' => $this->number($order->settled_amount),
  497. 'bet_result' => $result,
  498. 'third_game_commission_ratio' => $result === 'loss' ? $order->third_game_commission_ratio : null,
  499. 'third_game_commission' => $commission,
  500. 'bet_content' => (string) ($order->bet_content ?? ''),
  501. 'status' => $status,
  502. 'status_text' => self::STATUS_NAMES[$status] ?? '未知',
  503. 'game_order_id' => (string) $order->game_order_id,
  504. 'bet_time' => $this->formatTime($order->bet_time),
  505. 'last_update_time' => $this->formatTime($order->last_update_time),
  506. ];
  507. }
  508. private function identity(string $platform, string $playerId, string $gameOrderId): string
  509. {
  510. return hash('sha256', $platform . "\0" . $playerId . "\0" . $gameOrderId);
  511. }
  512. private function decimal($value): ?string
  513. {
  514. if (!is_numeric($value)) {
  515. return null;
  516. }
  517. $value = trim((string) $value);
  518. if (preg_match('/^-?\d+(?:\.\d+)?$/D', $value)) {
  519. return bcadd($value, '0', 10);
  520. }
  521. return number_format((float) $value, 10, '.', '');
  522. }
  523. private function text($value): string
  524. {
  525. if (is_array($value) || is_object($value)) {
  526. return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '';
  527. }
  528. return is_scalar($value) ? (string) $value : '';
  529. }
  530. private function dateTime($value): ?string
  531. {
  532. if ($value === null || $value === '') {
  533. return null;
  534. }
  535. try {
  536. return Carbon::parse($value, config('app.timezone', 'Asia/Shanghai'))->format('Y-m-d H:i:s');
  537. } catch (\Throwable $e) {
  538. return null;
  539. }
  540. }
  541. private function formatTime($value): ?string
  542. {
  543. return $value ? Carbon::parse($value)->format('Y-m-d H:i:s') : null;
  544. }
  545. private function number($value)
  546. {
  547. return $value === null || $value === '' ? null : (float) $value;
  548. }
  549. private function options(array $items): array
  550. {
  551. $options = [];
  552. foreach ($items as $value => $label) {
  553. $options[] = ['label' => $label, 'value' => $value];
  554. }
  555. return $options;
  556. }
  557. }