Realtime.php 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. <?php
  2. namespace App\Http\Controllers\admin;
  3. use App\Constants\HttpStatus;
  4. use App\Http\Controllers\Controller;
  5. use App\Models\Admin;
  6. use App\Services\AdminRealtime\AdminRealtimeAccess;
  7. use App\Services\AdminRealtime\AdminRealtimeFeed;
  8. use App\Services\AdminRealtime\AdminRealtimeRows;
  9. use App\Services\JwtService;
  10. use Exception;
  11. use Illuminate\Http\JsonResponse;
  12. use Illuminate\Validation\ValidationException;
  13. /** 后台订单、用户列表实时推送(Reverb + Laravel Echo),接入说明见 docs/admin-websocket.md */
  14. class Realtime extends Controller
  15. {
  16. /** Echo 连接参数、可订阅频道、当前 seq 与登录过期时间 */
  17. public function config(JwtService $jwtService): JsonResponse
  18. {
  19. try {
  20. if (!config('admin_realtime.enabled')) {
  21. throw new Exception('实时推送未启用');
  22. }
  23. $session = $jwtService->validateToken($this->bearerToken());
  24. $client = config('admin_realtime.client');
  25. $result = [
  26. 'broadcaster' => 'reverb',
  27. 'key' => (string) $client['key'],
  28. 'ws_host' => (string) $client['host'],
  29. 'ws_port' => (int) $client['port'],
  30. 'force_tls' => $client['scheme'] === 'https',
  31. 'auth_endpoint' => '/admin/broadcasting/auth',
  32. 'channels' => AdminRealtimeAccess::allowedChannels((int) request()->user->id),
  33. 'last_seq' => AdminRealtimeFeed::lastSeq(),
  34. 'session_expires_at' => isset($session->exp) ? (int) $session->exp : null,
  35. ];
  36. } catch (Exception $e) {
  37. return $this->error(HttpStatus::CUSTOM_ERROR, $e->getMessage());
  38. }
  39. return $this->success($result);
  40. }
  41. /**
  42. * Echo 私有频道授权(Pusher 协议),直接返回 {auth, channel_data}。
  43. * 不走 admin.jwt:该中间件未登录时仍返回 HTTP 200,Echo 无法区分,这里改用 401 / 403。
  44. */
  45. public function auth(JwtService $jwtService): JsonResponse
  46. {
  47. if (!config('admin_realtime.enabled')) {
  48. return $this->denied(403, '实时推送未启用');
  49. }
  50. $session = $jwtService->validateToken($this->bearerToken());
  51. $admin = $session && isset($session->user_id) ? Admin::query()->find($session->user_id) : null;
  52. if (!$admin) {
  53. return $this->denied(401, __('messages.' . HttpStatus::AUTHORIZATION_HEADER_NOT_FOUND));
  54. }
  55. $channelName = (string) request()->input('channel_name');
  56. $channel = str_starts_with($channelName, 'private-') ? substr($channelName, 8) : '';
  57. if (!isset(AdminRealtimeRows::CHANNELS[$channel]) || !in_array($channel, AdminRealtimeAccess::allowedChannels((int) $admin->id), true)) {
  58. return $this->denied(403, '无权订阅该频道');
  59. }
  60. try {
  61. $auth = AdminRealtimeAccess::authorize((int) $admin->id, isset($session->exp) ? (int) $session->exp : null, $channelName, (string) request()->input('socket_id'));
  62. } catch (\Throwable $e) {
  63. return $this->denied(403, '参数错误');
  64. }
  65. return response()->json($auth);
  66. }
  67. /** 断线重连或首次订阅后补发 seq 之后的事件;返回 resync=true 时前端应重新拉取列表 */
  68. public function replay(): JsonResponse
  69. {
  70. try {
  71. request()->validate([
  72. 'after_seq' => ['required', 'integer', 'min:0'],
  73. 'channels' => ['required', 'string'],
  74. ]);
  75. if (!config('admin_realtime.enabled')) {
  76. throw new Exception('实时推送未启用');
  77. }
  78. $allowed = AdminRealtimeAccess::allowedChannels((int) request()->user->id);
  79. $channels = array_values(array_intersect(array_map('trim', explode(',', (string) request()->input('channels'))), $allowed));
  80. $result = AdminRealtimeFeed::replay((int) request()->input('after_seq'), $channels) + ['channels' => $channels];
  81. } catch (ValidationException $e) {
  82. return $this->error(HttpStatus::CUSTOM_ERROR, $e->validator->errors()->first());
  83. } catch (Exception $e) {
  84. return $this->error(HttpStatus::CUSTOM_ERROR, $e->getMessage());
  85. }
  86. return $this->success($result);
  87. }
  88. private function bearerToken(): string
  89. {
  90. return str_replace('Bearer ', '', (string) request()->header('Authorization'));
  91. }
  92. private function denied(int $status, string $message): JsonResponse
  93. {
  94. return response()->json(['code' => HttpStatus::CUSTOM_ERROR, 'timestamp' => time(), 'msg' => $message, 'data' => []], $status);
  95. }
  96. }