path(), '/')); if (count($segments) < 3 || $segments[0] !== 'api' || $segments[1] !== 'v2') { return $response; } $method = $request->method(); $newLog = $this->resolveOperation($segments[2], $method, $request); if (! $newLog) { return $response; } $currTime = (int) round(microtime(true) * 1000); // 客户端时区(分钟 → 毫秒) $clientTimezone = (int) ($request->cookie('timezone', 0)) * -60 * 1000; /** * ========================= * 1. 操作日志 * ========================= */ UserOperationLog::create([ 'id' => app('snowflake')->id(), 'user_id' => $user['user_id'], 'op_type_id' => $newLog['op_type_id'], 'op_type' => $newLog['op_type'], 'content' => $newLog['content'], 'timezone' => $clientTimezone, 'create_time' => $currTime, ]); /** * ========================= * 2. 活跃时间 Frame * ========================= */ $lastFrame = UserOperationFrame::where('user_id', $user['user_id']) ->latest('updated_at') ->first(); $isNewFrame = true; if ($lastFrame) { $isNewFrame = ($currTime - (int) $lastFrame->op_end) > self::MAX_INTERVAL; } if ($isNewFrame) { UserOperationFrame::create([ 'id' => app('snowflake')->id(), 'user_id' => $user['user_id'], 'op_start' => $currTime - self::MIN_INTERVAL, 'op_end' => $currTime, 'duration' => self::MIN_INTERVAL, 'hit' => 1, 'timezone' => $clientTimezone, ]); $thisActiveTime = self::MIN_INTERVAL; } else { $thisActiveTime = $currTime - (int) $lastFrame->op_end; $lastFrame->update([ 'op_end' => $currTime, 'duration' => $currTime - (int) $lastFrame->op_start, 'hit' => $lastFrame->hit + 1, ]); } /** * ========================= * 3. Daily 汇总 * ========================= */ $clientTime = $currTime + $clientTimezone; $clientDateMs = strtotime(gmdate('Y-m-d', $clientTime / 1000)) * 1000; $daily = UserOperationDaily::firstOrNew([ 'user_id' => $user['user_id'], 'date_int' => $clientDateMs, ]); if ($daily->exists) { $daily->increment('duration', $thisActiveTime); $daily->increment('hit'); } else { $daily->fill([ 'id' => app('snowflake')->id(), 'duration' => self::MIN_INTERVAL, 'hit' => 1, ])->save(); } return $response; } /** * 根据 API 路径与方法解析操作类型 */ private function resolveOperation(string $resource, string $method, Request $request): ?array { return match ($resource) { 'channel' => match ($method) { 'POST' => [ 'op_type_id' => 11, 'op_type' => 'channel_create', 'content' => $request->input('studio') . '/' . $request->input('name'), ], 'PUT' => [ 'op_type_id' => 10, 'op_type' => 'channel_update', 'content' => $request->input('name'), ], default => null, }, 'article' => match ($method) { 'POST' => [ 'op_type_id' => 21, 'op_type' => 'article_create', 'content' => $request->input('studio') . '/' . $request->input('title'), ], 'PUT' => [ 'op_type_id' => 20, 'op_type' => 'article_update', 'content' => $request->input('title'), ], default => null, }, 'dict' => [ 'op_type_id' => 30, 'op_type' => 'dict_lookup', 'content' => $request->input('word'), ], 'terms' => match ($method) { 'POST' => [ 'op_type_id' => 42, 'op_type' => 'term_create', 'content' => $request->input('word'), ], 'PUT' => [ 'op_type_id' => 40, 'op_type' => 'term_update', 'content' => $request->input('word'), ], default => null, }, 'sentence' => match ($method) { 'POST' => [ 'op_type_id' => 71, 'op_type' => 'sent_create', 'content' => $request->input('channel'), ], 'PUT' => [ 'op_type_id' => 70, 'op_type' => 'sent_update', 'content' => $request->input('channel'), ], default => null, }, 'anthology' => match ($method) { 'POST' => [ 'op_type_id' => 81, 'op_type' => 'collection_create', 'content' => $request->input('title'), ], 'PUT' => [ 'op_type_id' => 80, 'op_type' => 'collection_update', 'content' => $request->input('title'), ], default => null, }, 'wbw' => $method === 'POST' ? [ 'op_type_id' => 60, 'op_type' => 'wbw_update', 'content' => implode('_', [ $request->input('book'), $request->input('para'), $request->input('channel_id'), ]), ] : null, default => null, }; } }