UserOperation.php 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. <?php
  2. namespace App\Http\Middleware;
  3. use Closure;
  4. use Illuminate\Http\Request;
  5. use Symfony\Component\HttpFoundation\Response;
  6. use Illuminate\Support\Facades\Log;
  7. use App\Models\UserOperationLog;
  8. use App\Models\UserOperationFrame;
  9. use App\Models\UserOperationDaily;
  10. use App\Http\Api\AuthApi;
  11. class UserOperation
  12. {
  13. private const MAX_INTERVAL = 600_000; // 10 min (ms)
  14. private const MIN_INTERVAL = 60_000; // 1 min (ms)
  15. /**
  16. * Handle an incoming request.
  17. *
  18. * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
  19. */
  20. public function handle(Request $request, Closure $next): Response
  21. {
  22. $response = $next($request);
  23. $user = AuthApi::current($request);
  24. if (! $user) {
  25. return $response;
  26. }
  27. $segments = explode('/', trim($request->path(), '/'));
  28. if (count($segments) < 3 || $segments[0] !== 'api' || $segments[1] !== 'v2') {
  29. return $response;
  30. }
  31. $method = $request->method();
  32. $newLog = $this->resolveOperation($segments[2], $method, $request);
  33. if (! $newLog) {
  34. return $response;
  35. }
  36. $currTime = (int) round(microtime(true) * 1000);
  37. // 客户端时区(分钟 → 毫秒)
  38. $clientTimezone = (int) ($request->cookie('timezone', 0)) * -60 * 1000;
  39. /**
  40. * =========================
  41. * 1. 操作日志
  42. * =========================
  43. */
  44. UserOperationLog::create([
  45. 'id' => app('snowflake')->id(),
  46. 'user_id' => $user['user_id'],
  47. 'op_type_id' => $newLog['op_type_id'],
  48. 'op_type' => $newLog['op_type'],
  49. 'content' => $newLog['content'],
  50. 'timezone' => $clientTimezone,
  51. 'create_time' => $currTime,
  52. ]);
  53. /**
  54. * =========================
  55. * 2. 活跃时间 Frame
  56. * =========================
  57. */
  58. $lastFrame = UserOperationFrame::where('user_id', $user['user_id'])
  59. ->latest('updated_at')
  60. ->first();
  61. $isNewFrame = true;
  62. if ($lastFrame) {
  63. $isNewFrame = ($currTime - (int) $lastFrame->op_end) > self::MAX_INTERVAL;
  64. }
  65. if ($isNewFrame) {
  66. UserOperationFrame::create([
  67. 'id' => app('snowflake')->id(),
  68. 'user_id' => $user['user_id'],
  69. 'op_start' => $currTime - self::MIN_INTERVAL,
  70. 'op_end' => $currTime,
  71. 'duration' => self::MIN_INTERVAL,
  72. 'hit' => 1,
  73. 'timezone' => $clientTimezone,
  74. ]);
  75. $thisActiveTime = self::MIN_INTERVAL;
  76. } else {
  77. $thisActiveTime = $currTime - (int) $lastFrame->op_end;
  78. $lastFrame->update([
  79. 'op_end' => $currTime,
  80. 'duration' => $currTime - (int) $lastFrame->op_start,
  81. 'hit' => $lastFrame->hit + 1,
  82. ]);
  83. }
  84. /**
  85. * =========================
  86. * 3. Daily 汇总
  87. * =========================
  88. */
  89. $clientTime = $currTime + $clientTimezone;
  90. $clientDateMs = strtotime(gmdate('Y-m-d', $clientTime / 1000)) * 1000;
  91. $daily = UserOperationDaily::firstOrNew([
  92. 'user_id' => $user['user_id'],
  93. 'date_int' => $clientDateMs,
  94. ]);
  95. if ($daily->exists) {
  96. $daily->increment('duration', $thisActiveTime);
  97. $daily->increment('hit');
  98. } else {
  99. $daily->fill([
  100. 'id' => app('snowflake')->id(),
  101. 'duration' => self::MIN_INTERVAL,
  102. 'hit' => 1,
  103. ])->save();
  104. }
  105. return $response;
  106. }
  107. /**
  108. * 根据 API 路径与方法解析操作类型
  109. */
  110. private function resolveOperation(string $resource, string $method, Request $request): ?array
  111. {
  112. return match ($resource) {
  113. 'channel' => match ($method) {
  114. 'POST' => [
  115. 'op_type_id' => 11,
  116. 'op_type' => 'channel_create',
  117. 'content' => $request->input('studio') . '/' . $request->input('name'),
  118. ],
  119. 'PUT' => [
  120. 'op_type_id' => 10,
  121. 'op_type' => 'channel_update',
  122. 'content' => $request->input('name'),
  123. ],
  124. default => null,
  125. },
  126. 'article' => match ($method) {
  127. 'POST' => [
  128. 'op_type_id' => 21,
  129. 'op_type' => 'article_create',
  130. 'content' => $request->input('studio') . '/' . $request->input('title'),
  131. ],
  132. 'PUT' => [
  133. 'op_type_id' => 20,
  134. 'op_type' => 'article_update',
  135. 'content' => $request->input('title'),
  136. ],
  137. default => null,
  138. },
  139. 'dict' => [
  140. 'op_type_id' => 30,
  141. 'op_type' => 'dict_lookup',
  142. 'content' => $request->input('word'),
  143. ],
  144. 'terms' => match ($method) {
  145. 'POST' => [
  146. 'op_type_id' => 42,
  147. 'op_type' => 'term_create',
  148. 'content' => $request->input('word'),
  149. ],
  150. 'PUT' => [
  151. 'op_type_id' => 40,
  152. 'op_type' => 'term_update',
  153. 'content' => $request->input('word'),
  154. ],
  155. default => null,
  156. },
  157. 'sentence' => match ($method) {
  158. 'POST' => [
  159. 'op_type_id' => 71,
  160. 'op_type' => 'sent_create',
  161. 'content' => $request->input('channel'),
  162. ],
  163. 'PUT' => [
  164. 'op_type_id' => 70,
  165. 'op_type' => 'sent_update',
  166. 'content' => $request->input('channel'),
  167. ],
  168. default => null,
  169. },
  170. 'anthology' => match ($method) {
  171. 'POST' => [
  172. 'op_type_id' => 81,
  173. 'op_type' => 'collection_create',
  174. 'content' => $request->input('title'),
  175. ],
  176. 'PUT' => [
  177. 'op_type_id' => 80,
  178. 'op_type' => 'collection_update',
  179. 'content' => $request->input('title'),
  180. ],
  181. default => null,
  182. },
  183. 'wbw' => $method === 'POST'
  184. ? [
  185. 'op_type_id' => 60,
  186. 'op_type' => 'wbw_update',
  187. 'content' => implode('_', [
  188. $request->input('book'),
  189. $request->input('para'),
  190. $request->input('channel_id'),
  191. ]),
  192. ]
  193. : null,
  194. default => null,
  195. };
  196. }
  197. }