UserOperation.php 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  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::forceCreate([
  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::forceCreate([
  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->forceFill([
  79. 'op_end' => $currTime,
  80. 'duration' => $currTime - (int) $lastFrame->op_start,
  81. 'hit' => $lastFrame->hit + 1,
  82. ])->save();
  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. $id = app('snowflake')->id();
  100. if (app()->isLocal()) {
  101. Log::debug('snowflake ' . $id);
  102. }
  103. $daily->forceFill([
  104. 'id' => $id,
  105. 'duration' => self::MIN_INTERVAL,
  106. 'hit' => 1,
  107. ])->save();
  108. }
  109. return $response;
  110. }
  111. /**
  112. * 根据 API 路径与方法解析操作类型
  113. */
  114. private function resolveOperation(string $resource, string $method, Request $request): ?array
  115. {
  116. return match ($resource) {
  117. 'channel' => match ($method) {
  118. 'POST' => [
  119. 'op_type_id' => 11,
  120. 'op_type' => 'channel_create',
  121. 'content' => $request->input('studio') . '/' . $request->input('name'),
  122. ],
  123. 'PUT' => [
  124. 'op_type_id' => 10,
  125. 'op_type' => 'channel_update',
  126. 'content' => $request->input('name'),
  127. ],
  128. default => null,
  129. },
  130. 'article' => match ($method) {
  131. 'POST' => [
  132. 'op_type_id' => 21,
  133. 'op_type' => 'article_create',
  134. 'content' => $request->input('studio') . '/' . $request->input('title'),
  135. ],
  136. 'PUT' => [
  137. 'op_type_id' => 20,
  138. 'op_type' => 'article_update',
  139. 'content' => $request->input('title'),
  140. ],
  141. default => null,
  142. },
  143. 'dict' => [
  144. 'op_type_id' => 30,
  145. 'op_type' => 'dict_lookup',
  146. 'content' => $request->input('word'),
  147. ],
  148. 'terms' => match ($method) {
  149. 'POST' => [
  150. 'op_type_id' => 42,
  151. 'op_type' => 'term_create',
  152. 'content' => $request->input('word'),
  153. ],
  154. 'PUT' => [
  155. 'op_type_id' => 40,
  156. 'op_type' => 'term_update',
  157. 'content' => $request->input('word'),
  158. ],
  159. default => null,
  160. },
  161. 'sentence' => match ($method) {
  162. 'POST' => [
  163. 'op_type_id' => 71,
  164. 'op_type' => 'sent_create',
  165. 'content' => $request->input('channel'),
  166. ],
  167. 'PUT' => [
  168. 'op_type_id' => 70,
  169. 'op_type' => 'sent_update',
  170. 'content' => $request->input('channel'),
  171. ],
  172. default => null,
  173. },
  174. 'anthology' => match ($method) {
  175. 'POST' => [
  176. 'op_type_id' => 81,
  177. 'op_type' => 'collection_create',
  178. 'content' => $request->input('title'),
  179. ],
  180. 'PUT' => [
  181. 'op_type_id' => 80,
  182. 'op_type' => 'collection_update',
  183. 'content' => $request->input('title'),
  184. ],
  185. default => null,
  186. },
  187. 'wbw' => $method === 'POST'
  188. ? [
  189. 'op_type_id' => 60,
  190. 'op_type' => 'wbw_update',
  191. 'content' => implode('_', [
  192. $request->input('book'),
  193. $request->input('para'),
  194. $request->input('channel_id'),
  195. ]),
  196. ]
  197. : null,
  198. default => null,
  199. };
  200. }
  201. }