AdminRealtimeRows.php 11 KB

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