ThirdGameOrderService.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. <?php
  2. namespace App\Services;
  3. use App\Models\ThirdGameOrder as ThirdGameOrderModel;
  4. use Carbon\Carbon;
  5. use Illuminate\Support\Facades\DB;
  6. class ThirdGameOrderService
  7. {
  8. private const GAME_TYPE_NAMES = [
  9. 1 => '视讯',
  10. 2 => '老虎机',
  11. 3 => '彩票',
  12. 4 => '体育',
  13. 5 => '电竞',
  14. 6 => '捕猎',
  15. 7 => '棋牌',
  16. ];
  17. private const STATUS_NAMES = [
  18. 0 => '未完成',
  19. 1 => '已完成',
  20. 2 => '已取消',
  21. 3 => '已撤单',
  22. ];
  23. private ThirdGameBalanceService $provider;
  24. public function __construct(ThirdGameBalanceService $provider)
  25. {
  26. $this->provider = $provider;
  27. }
  28. public function syncHistory(
  29. string $startTime,
  30. string $endTime,
  31. int $page = 1,
  32. int $limit = 2000
  33. ): array {
  34. return $this->persistProviderResult(
  35. $this->provider->historyOrders($startTime, $endTime, $page, $limit)
  36. );
  37. }
  38. public function syncRealtime(int $page = 1, int $limit = 2000): array
  39. {
  40. return $this->persistProviderResult($this->provider->realtimeOrders($page, $limit));
  41. }
  42. private function persistProviderResult(array $result): array
  43. {
  44. if (empty($result['ok'])) {
  45. return $result;
  46. }
  47. $orders = is_array($result['list'] ?? null) ? $result['list'] : [];
  48. $received = count($orders);
  49. $testNamespace = $this->provider->playerPrefix() === 'tu';
  50. $orders = array_values(array_filter($orders, static function (array $order) use ($testNamespace): bool {
  51. $isTestPlayer = str_starts_with(trim((string) ($order['player_id'] ?? '')), 'tu');
  52. return $testNamespace === $isTestPlayer;
  53. }));
  54. $users = $this->usersByPlayerId(array_values(array_unique(array_filter(array_map(
  55. static fn(array $order): string => trim((string) ($order['player_id'] ?? '')),
  56. $orders
  57. )))));
  58. $existing = $this->existingSnapshots($orders);
  59. $now = Carbon::now(config('app.timezone', 'Asia/Shanghai'))->format('Y-m-d H:i:s');
  60. $rows = [];
  61. $matchedUserIds = [];
  62. foreach ($orders as $order) {
  63. $platform = trim((string) ($order['platform'] ?? ''));
  64. $gameOrderId = trim((string) ($order['game_order_id'] ?? ''));
  65. if ($gameOrderId === '') {
  66. continue;
  67. }
  68. $playerId = trim((string) ($order['player_id'] ?? ''));
  69. $identity = $this->identity($platform, $playerId, $gameOrderId);
  70. $user = $users[$playerId] ?? null;
  71. $previous = $existing[$identity] ?? null;
  72. $userId = $user->id ?? $previous->user_id ?? null;
  73. if ($userId !== null) {
  74. $matchedUserIds[(int) $userId] = true;
  75. }
  76. $rows[$identity] = [
  77. 'order_key' => $identity,
  78. 'user_id' => $userId,
  79. 'member_id' => $user->member_id ?? $previous->member_id ?? null,
  80. 'username' => (string) ($user->username ?? $previous->username ?? ''),
  81. 'first_name' => (string) ($user->first_name ?? $previous->first_name ?? ''),
  82. 'player_id' => $playerId,
  83. 'platform' => $platform,
  84. 'currency' => trim((string) ($order['currency'] ?? '')),
  85. 'game_type' => min(255, max(0, (int) ($order['game_type'] ?? 0))),
  86. 'game_name' => (string) ($order['game_name'] ?? ''),
  87. 'round_no' => (string) ($order['round'] ?? ''),
  88. 'table_no' => (string) ($order['table'] ?? ''),
  89. 'seat' => (string) ($order['seat'] ?? ''),
  90. 'bet_amount' => $this->decimal($order['bet_amount'] ?? null),
  91. 'valid_amount' => $this->decimal($order['valid_amount'] ?? null),
  92. 'settled_amount' => $this->decimal($order['settled_amount'] ?? null),
  93. 'bet_content' => $this->text($order['bet_content'] ?? ''),
  94. 'status' => min(255, max(0, (int) ($order['status'] ?? 0))),
  95. 'game_order_id' => $gameOrderId,
  96. 'bet_time' => $this->dateTime($order['bet_time'] ?? null),
  97. 'last_update_time' => $this->dateTime($order['last_update_time'] ?? null),
  98. 'created_at' => $previous->created_at ?? $now,
  99. 'updated_at' => $now,
  100. ];
  101. }
  102. if ($rows !== []) {
  103. ThirdGameOrderModel::query()->upsert(
  104. array_values($rows),
  105. ['order_key'],
  106. [
  107. 'user_id', 'member_id', 'username', 'first_name', 'player_id', 'currency',
  108. 'game_type', 'game_name', 'round_no', 'table_no', 'seat', 'bet_amount',
  109. 'valid_amount', 'settled_amount', 'bet_content', 'status', 'bet_time',
  110. 'last_update_time', 'updated_at',
  111. ]
  112. );
  113. }
  114. $pageNo = (int) ($result['page_no'] ?? $page);
  115. $pageSize = (int) ($result['page_size'] ?? $limit);
  116. $total = (int) ($result['total'] ?? $received);
  117. return [
  118. 'ok' => true,
  119. 'received' => $received,
  120. 'synced' => count($rows),
  121. 'matched_users' => count($matchedUserIds),
  122. 'unmatched_orders' => count(array_filter($rows, static fn(array $row): bool => $row['user_id'] === null)),
  123. 'total' => $total,
  124. 'page_no' => $pageNo,
  125. 'page_size' => $pageSize,
  126. 'has_more' => $pageSize > 0 && $pageNo * $pageSize < $total,
  127. ];
  128. }
  129. public function syncForList(array $params): array
  130. {
  131. if ((int) ($params['page'] ?? 1) > 1) {
  132. return ['attempted' => false, 'ok' => true, 'message' => ''];
  133. }
  134. $result = $this->syncRealtime(1, 2000);
  135. if (empty($result['ok'])) {
  136. return [
  137. 'attempted' => true,
  138. 'ok' => false,
  139. 'message' => (string) ($result['msg'] ?? '三方订单同步失败'),
  140. ];
  141. }
  142. unset($result['ok']);
  143. return array_merge([
  144. 'attempted' => true,
  145. 'ok' => true,
  146. 'message' => '',
  147. ], $result);
  148. }
  149. public function paginate(array $params): array
  150. {
  151. $page = max(1, (int) ($params['page'] ?? 1));
  152. $limit = min(200, max(1, (int) ($params['limit'] ?? 20)));
  153. $query = ThirdGameOrderModel::query()
  154. ->leftJoin('users', 'users.id', '=', 'third_game_orders.user_id');
  155. if ($this->provider->playerPrefix() === 'tu') $query->where('third_game_orders.player_id', 'like', 'tu%');
  156. else $query->where('third_game_orders.player_id', 'not like', 'tu%');
  157. if (!empty($params['user_id'])) {
  158. $userId = (int) $params['user_id'];
  159. $query->where(function ($query) use ($userId) {
  160. $query->where('users.id', $userId)
  161. ->orWhere(function ($query) use ($userId) {
  162. $query->whereNull('users.id')
  163. ->where('third_game_orders.user_id', $userId);
  164. });
  165. });
  166. }
  167. if (!empty($params['member_id'])) {
  168. $memberId = (string) $params['member_id'];
  169. $query->where(function ($query) use ($memberId) {
  170. $query->where('users.member_id', $memberId)
  171. ->orWhere(function ($query) use ($memberId) {
  172. $query->whereNull('users.id')
  173. ->where('third_game_orders.member_id', $memberId);
  174. });
  175. });
  176. }
  177. if (!empty($params['username'])) {
  178. $username = '%' . $params['username'] . '%';
  179. $query->where(function ($query) use ($username) {
  180. $query->where('users.username', 'like', $username)
  181. ->orWhere(function ($query) use ($username) {
  182. $query->whereNull('users.id')
  183. ->where('third_game_orders.username', 'like', $username);
  184. });
  185. });
  186. }
  187. if (!empty($params['first_name'])) {
  188. $firstName = '%' . $params['first_name'] . '%';
  189. $query->where(function ($query) use ($firstName) {
  190. $query->where('users.first_name', 'like', $firstName)
  191. ->orWhere(function ($query) use ($firstName) {
  192. $query->whereNull('users.id')
  193. ->where('third_game_orders.first_name', 'like', $firstName);
  194. });
  195. });
  196. }
  197. if (!empty($params['player_id'])) {
  198. $query->where('third_game_orders.player_id', (string) $params['player_id']);
  199. }
  200. if (!empty($params['platform'])) {
  201. $query->where('third_game_orders.platform', (string) $params['platform']);
  202. }
  203. if (!empty($params['game_order_id'])) {
  204. $query->where('third_game_orders.game_order_id', (string) $params['game_order_id']);
  205. }
  206. if (isset($params['game_type']) && $params['game_type'] !== '') {
  207. $query->where('third_game_orders.game_type', (int) $params['game_type']);
  208. }
  209. if (isset($params['status']) && $params['status'] !== '') {
  210. $query->where('third_game_orders.status', (int) $params['status']);
  211. }
  212. if (!empty($params['start_time']) && !empty($params['end_time'])) {
  213. $query->whereBetween('third_game_orders.last_update_time', [
  214. $params['start_time'],
  215. $params['end_time'],
  216. ]);
  217. }
  218. $total = (clone $query)->count('third_game_orders.id');
  219. $list = $query
  220. ->select('third_game_orders.*')
  221. ->addSelect([
  222. 'users.id as current_user_id',
  223. 'users.member_id as current_member_id',
  224. 'users.username as current_username',
  225. 'users.first_name as current_first_name',
  226. ])
  227. ->orderByDesc('third_game_orders.last_update_time')
  228. ->orderByDesc('third_game_orders.id')
  229. ->forPage($page, $limit)
  230. ->get()
  231. ->map(fn(ThirdGameOrderModel $order): array => $this->format($order))
  232. ->all();
  233. return [
  234. 'total' => $total,
  235. 'page' => $page,
  236. 'limit' => $limit,
  237. 'list' => $list,
  238. 'options' => [
  239. 'game_types' => $this->options(self::GAME_TYPE_NAMES),
  240. 'statuses' => $this->options(self::STATUS_NAMES),
  241. ],
  242. ];
  243. }
  244. /**
  245. * @return array<string, object>
  246. */
  247. private function usersByPlayerId(array $playerIds): array
  248. {
  249. $requested = []; $prefix = $this->provider->playerPrefix();
  250. $pattern = '/^' . preg_quote($prefix, '/') . '(\d{1,' . (11 - strlen($prefix)) . '})$/D';
  251. foreach ($playerIds as $playerId) {
  252. $playerId = trim((string) $playerId);
  253. if (!preg_match($pattern, $playerId, $matches)) {
  254. continue;
  255. }
  256. $userId = filter_var(ltrim($matches[1], '0'), FILTER_VALIDATE_INT, [
  257. 'options' => ['min_range' => 1, 'max_range' => PHP_INT_MAX],
  258. ]);
  259. if ($userId !== false && ($playerId === $prefix . $userId || $playerId === $this->provider->playerIdFromUserId($userId))) {
  260. $requested[(int) $userId][$playerId] = true;
  261. }
  262. }
  263. if ($requested === []) {
  264. return [];
  265. }
  266. $rows = DB::table('users')
  267. ->select(['id', 'member_id', 'username', 'first_name'])
  268. ->whereIn('id', array_keys($requested))
  269. ->get();
  270. $result = [];
  271. foreach ($rows as $row) {
  272. foreach (array_keys($requested[(int) $row->id] ?? []) as $playerId) {
  273. $result[$playerId] = $row;
  274. }
  275. }
  276. return $result;
  277. }
  278. /**
  279. * @return array<string, ThirdGameOrderModel>
  280. */
  281. private function existingSnapshots(array $orders): array
  282. {
  283. $orderKeys = [];
  284. foreach ($orders as $order) {
  285. $gameOrderId = trim((string) ($order['game_order_id'] ?? ''));
  286. if ($gameOrderId === '') {
  287. continue;
  288. }
  289. $orderKeys[] = $this->identity(
  290. trim((string) ($order['platform'] ?? '')),
  291. trim((string) ($order['player_id'] ?? '')),
  292. $gameOrderId
  293. );
  294. }
  295. $orderKeys = array_values(array_unique($orderKeys));
  296. if ($orderKeys === []) {
  297. return [];
  298. }
  299. $rows = ThirdGameOrderModel::query()
  300. ->whereIn('order_key', $orderKeys)
  301. ->get(['order_key', 'user_id', 'member_id', 'username', 'first_name', 'created_at']);
  302. $result = [];
  303. foreach ($rows as $row) {
  304. $result[(string) $row->order_key] = $row;
  305. }
  306. return $result;
  307. }
  308. private function format(ThirdGameOrderModel $order): array
  309. {
  310. $userId = $order->current_user_id ?? $order->user_id;
  311. $memberId = $order->current_member_id ?? $order->member_id;
  312. $username = $order->current_username ?? $order->username;
  313. $firstName = $order->current_first_name ?? $order->first_name;
  314. $gameType = (int) $order->game_type;
  315. $status = (int) $order->status;
  316. return [
  317. 'id' => (int) $order->id,
  318. 'user_id' => $userId === null ? null : (int) $userId,
  319. 'member_id' => $memberId === null ? null : (string) $memberId,
  320. 'username' => (string) ($username ?? ''),
  321. 'first_name' => (string) ($firstName ?? ''),
  322. 'player_id' => (string) $order->player_id,
  323. 'platform' => (string) $order->platform,
  324. 'currency' => (string) $order->currency,
  325. 'game_type' => $gameType,
  326. 'game_type_text' => self::GAME_TYPE_NAMES[$gameType] ?? '未知',
  327. 'game_name' => (string) $order->game_name,
  328. 'round' => (string) $order->round_no,
  329. 'table' => (string) $order->table_no,
  330. 'seat' => (string) $order->seat,
  331. 'bet_amount' => $this->number($order->bet_amount),
  332. 'valid_amount' => $this->number($order->valid_amount),
  333. 'settled_amount' => $this->number($order->settled_amount),
  334. 'bet_content' => (string) ($order->bet_content ?? ''),
  335. 'status' => $status,
  336. 'status_text' => self::STATUS_NAMES[$status] ?? '未知',
  337. 'game_order_id' => (string) $order->game_order_id,
  338. 'bet_time' => $this->formatTime($order->bet_time),
  339. 'last_update_time' => $this->formatTime($order->last_update_time),
  340. ];
  341. }
  342. private function identity(string $platform, string $playerId, string $gameOrderId): string
  343. {
  344. return hash('sha256', $platform . "\0" . $playerId . "\0" . $gameOrderId);
  345. }
  346. private function decimal($value): ?string
  347. {
  348. if (!is_numeric($value)) {
  349. return null;
  350. }
  351. $value = trim((string) $value);
  352. if (preg_match('/^-?\d+(?:\.\d+)?$/D', $value)) {
  353. return bcadd($value, '0', 10);
  354. }
  355. return number_format((float) $value, 10, '.', '');
  356. }
  357. private function text($value): string
  358. {
  359. if (is_array($value) || is_object($value)) {
  360. return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '';
  361. }
  362. return is_scalar($value) ? (string) $value : '';
  363. }
  364. private function dateTime($value): ?string
  365. {
  366. if ($value === null || $value === '') {
  367. return null;
  368. }
  369. try {
  370. return Carbon::parse($value, config('app.timezone', 'Asia/Shanghai'))->format('Y-m-d H:i:s');
  371. } catch (\Throwable $e) {
  372. return null;
  373. }
  374. }
  375. private function formatTime($value): ?string
  376. {
  377. return $value ? Carbon::parse($value)->format('Y-m-d H:i:s') : null;
  378. }
  379. private function number($value)
  380. {
  381. return $value === null || $value === '' ? null : (float) $value;
  382. }
  383. private function options(array $items): array
  384. {
  385. $options = [];
  386. foreach ($items as $value => $label) {
  387. $options[] = ['label' => $label, 'value' => $value];
  388. }
  389. return $options;
  390. }
  391. }