AdminRealtimeRows.php 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. <?php
  2. namespace App\Services\AdminRealtime;
  3. use App\Constants\HttpStatus;
  4. use App\Http\Controllers\admin\Bet;
  5. use App\Http\Controllers\admin\JisuGameOrder;
  6. use App\Http\Controllers\admin\LhcOrder;
  7. use App\Http\Controllers\admin\Order;
  8. use App\Http\Controllers\admin\SportGameOrder;
  9. use App\Http\Controllers\admin\Wallet;
  10. use App\Http\Controllers\admin\Withdraw;
  11. use App\Services\ManualAuditService;
  12. use App\Services\OnlineUserService;
  13. use App\Services\PaymentOrderService;
  14. use App\Services\ThirdGameOrderService;
  15. use App\Services\ThirdPartyDepositService;
  16. use App\Services\UserService;
  17. use Illuminate\Http\JsonResponse;
  18. use Illuminate\Http\Request;
  19. use Illuminate\Support\Facades\DB;
  20. use Illuminate\Support\Facades\Facade;
  21. use RuntimeException;
  22. /**
  23. * 把事件表中的「表 + 主键」解析为对应后台列表接口的单行数据。
  24. *
  25. * 一律复用列表接口本身的查询(控制器内拼查询的直接以 id 参数调用控制器,
  26. * 其余调用列表所用的 service),保证推送的行与列表接口返回的行字段一致。
  27. */
  28. final class AdminRealtimeRows
  29. {
  30. /** 频道 => [HTTP 接口, 菜单按钮权限 uri, 记录标识字段] */
  31. public const CHANNELS = [
  32. 'admin.orders.sport' => ['/admin/order/list', 'admin/order/list', 'id'],
  33. 'admin.orders.lhc' => ['/admin/lhcOrder/list', 'admin/lhcOrder/list', 'id'],
  34. 'admin.orders.sport_game' => ['/admin/sportGameOrder/list', 'admin/sportGameOrder/list', 'id'],
  35. 'admin.orders.jisu_game' => ['/admin/jisuGameOrder/list', 'admin/jisuGameOrder/list', 'id'],
  36. 'admin.orders.third_game' => ['/admin/thirdGame/orders', 'admin/thirdGame/orders', 'id'],
  37. 'admin.orders.bet' => ['/admin/bet', 'admin/bet', 'id'],
  38. 'admin.orders.recharge' => ['/admin/wallet', 'admin/wallet', 'id'],
  39. 'admin.orders.payment' => ['/admin/paymentOrder', 'admin/paymentOrder', 'id'],
  40. 'admin.orders.third_party_deposit' => ['/admin/thirdPartyDeposit', 'admin/thirdPartyDeposit', 'id'],
  41. 'admin.orders.payment_withdraw' => ['/admin/withdraw/rmb', 'admin/withdraw/rmb', 'id'],
  42. 'admin.orders.withdraw' => ['/admin/withdraw', 'admin/withdraw', 'id'],
  43. 'admin.orders.manual_audit' => ['/admin/manualAudit', 'admin/manualAudit', 'id'],
  44. 'admin.users' => ['/admin/user', 'admin/user', 'id'],
  45. 'admin.users.online' => ['/admin/online/users', 'admin/online/users', 'member_id'],
  46. 'admin.stats.platform_count' => ['/admin/online/platformCount', 'admin/online/platformCount', 'scope'],
  47. 'admin.stats.unprocessed' => ['/admin/paymentOrder/unProcessed', 'admin/paymentOrder/unProcessed', 'scope'],
  48. ];
  49. /** 表 => 默认频道(payment_orders 按 type 分流,见 paymentOrders) */
  50. private const RESOURCE_CHANNELS = [
  51. 'sport_order' => 'admin.orders.sport',
  52. 'lhc_order' => 'admin.orders.lhc',
  53. 'sport_game_order' => 'admin.orders.sport_game',
  54. 'jisu_game_order' => 'admin.orders.jisu_game',
  55. 'third_game_orders' => 'admin.orders.third_game',
  56. 'bets' => 'admin.orders.bet',
  57. 'recharges' => 'admin.orders.recharge',
  58. 'withdraws' => 'admin.orders.withdraw',
  59. 'manual_audits' => 'admin.orders.manual_audit',
  60. 'users' => 'admin.users',
  61. AdminRealtime::USER_ONLINE => 'admin.users.online',
  62. ];
  63. /**
  64. * @return list<array{channel:string,key:array,data:?array}> 解析不到(已删除或不在列表口径内)的行不返回,deleted / offline 只带 key
  65. */
  66. public static function resolve(string $resource, string $id, string $action): array
  67. {
  68. if (isset(AdminRealtimeSummaries::DEFINITIONS[$resource])) {
  69. return [self::message(AdminRealtimeSummaries::DEFINITIONS[$resource][0], ['scope' => 'all'], AdminRealtimeSummaries::read($resource))];
  70. }
  71. if ($resource === 'payment_orders') {
  72. return self::paymentOrders($id, $action);
  73. }
  74. $channel = self::RESOURCE_CHANNELS[$resource] ?? null;
  75. if ($channel === null) {
  76. return [];
  77. }
  78. if ($resource === AdminRealtime::USER_ONLINE) {
  79. return self::online($id, $action);
  80. }
  81. if ($action === 'deleted') {
  82. return [self::message($channel, ['id' => self::castId($id)], null)];
  83. }
  84. $rows = match ($resource) {
  85. 'sport_order' => self::viaController(Order::class, 'list', ['id' => $id]),
  86. 'lhc_order' => self::viaController(LhcOrder::class, 'list', ['id' => $id]),
  87. 'sport_game_order' => self::viaController(SportGameOrder::class, 'list', ['id' => $id]),
  88. 'jisu_game_order' => self::viaController(JisuGameOrder::class, 'list', ['id' => $id]),
  89. 'bets' => self::viaController(Bet::class, 'index', ['id' => $id]),
  90. 'recharges' => self::viaController(Wallet::class, 'index', ['id' => $id]),
  91. 'withdraws' => self::viaController(Withdraw::class, 'index', ['id' => $id]),
  92. 'third_game_orders' => app(ThirdGameOrderService::class)->paginate(['id' => $id])['list'] ?? [],
  93. 'manual_audits' => ManualAuditService::list(['id' => $id])['data'] ?? [],
  94. 'users' => UserService::paginate(['id' => $id])['data'] ?? [],
  95. };
  96. $row = self::pick($rows, 'id', $id);
  97. return $row === null ? [] : [self::message($channel, ['id' => self::castId($id)], $row)];
  98. }
  99. /** 代收(type=1)进「代收订单」与「三方充值」两张列表,代付(type=2/4)进「人民币提现」 */
  100. private static function paymentOrders(string $id, string $action): array
  101. {
  102. $key = ['id' => self::castId($id)];
  103. if ($action === 'deleted') {
  104. return array_map(static fn(string $channel): array => self::message($channel, $key, null),
  105. ['admin.orders.payment', 'admin.orders.third_party_deposit', 'admin.orders.payment_withdraw']);
  106. }
  107. $type = (int) DB::table('payment_orders')->where('id', $id)->value('type');
  108. $messages = [];
  109. if ($type === PaymentOrderService::TYPE_PAY) {
  110. $row = self::pick(PaymentOrderService::paginate(['type' => PaymentOrderService::TYPE_PAY, 'id' => $id])['data'] ?? [], 'id', $id);
  111. if ($row !== null) $messages[] = self::message('admin.orders.payment', $key, $row);
  112. $row = self::pick(ThirdPartyDepositService::list(['id' => $id])['data'] ?? [], 'id', $id);
  113. if ($row !== null) $messages[] = self::message('admin.orders.third_party_deposit', $key, $row);
  114. } elseif (in_array($type, [PaymentOrderService::TYPE_PAYOUT, PaymentOrderService::TYPE_SELF_PAYOUT], true)) {
  115. $row = self::pick(self::viaController(Withdraw::class, 'rmb', ['id' => $id, 'type' => $type]), 'id', $id);
  116. if ($row !== null) $messages[] = self::message('admin.orders.payment_withdraw', $key, $row);
  117. }
  118. return $messages;
  119. }
  120. private static function online(string $userId, string $action): array
  121. {
  122. $user = DB::table('users')->where('id', $userId)->first(['member_id', 'user_id']);
  123. if (!$user) {
  124. return [];
  125. }
  126. $key = ['member_id' => (string) ($user->member_id ?: $user->user_id)];
  127. if ($action === 'offline') {
  128. return [self::message('admin.users.online', $key, null)];
  129. }
  130. $row = OnlineUserService::list(['user_ids' => [(int) $userId], 'limit' => 1])['data'][0] ?? null;
  131. return $row === null ? [] : [self::message('admin.users.online', $key, self::normalize($row))];
  132. }
  133. /**
  134. * 以内部子请求调用后台列表控制器(与前端请求列表接口走同一段查询与格式化)。
  135. * 列表接口必须支持 id 筛选,pick() 再按 id 校验一次,防止筛选失效时推错行。
  136. */
  137. private static function viaController(string $class, string $method, array $query): array
  138. {
  139. $app = app();
  140. $original = $app->make('request');
  141. $request = Request::create('/', 'GET', $query + ['page' => 1, 'limit' => 20]);
  142. $request->user = $original->user ?? null;
  143. $app->instance('request', $request);
  144. Facade::clearResolvedInstance('request');
  145. try {
  146. $response = $app->call([$app->make($class), $method]);
  147. } finally {
  148. $app->instance('request', $original);
  149. Facade::clearResolvedInstance('request');
  150. }
  151. $body = $response instanceof JsonResponse ? $response->getData(true) : [];
  152. if ((int) ($body['code'] ?? -1) !== HttpStatus::OK) {
  153. throw new RuntimeException($class . '@' . $method . ' failed: ' . ($body['msg'] ?? 'non-json response'));
  154. }
  155. return $body['data']['data'] ?? [];
  156. }
  157. private static function pick($rows, string $field, string $id): ?array
  158. {
  159. foreach (self::normalize($rows) as $row) {
  160. if (is_array($row) && (string) ($row[$field] ?? '') === $id) {
  161. return $row;
  162. }
  163. }
  164. return null;
  165. }
  166. /** 与 HTTP 响应相同的序列化(模型的 appends / hidden / casts 均生效) */
  167. private static function normalize($value): array
  168. {
  169. return json_decode(json_encode($value), true) ?: [];
  170. }
  171. private static function message(string $channel, array $key, ?array $data): array
  172. {
  173. return ['channel' => $channel, 'key' => $key, 'data' => $data];
  174. }
  175. private static function castId(string $id)
  176. {
  177. return ctype_digit($id) ? (int) $id : $id;
  178. }
  179. }