learningAnalyticsBase = rtrim( $learningAnalyticsBase ?: config('services.learning_analytics.url', env('LEARNING_ANALYTICS_API_BASE', 'http://localhost:5016')), '/' ); $this->questionBankBase = rtrim( $questionBankBase ?: config('services.question_bank.base_url', env('QUESTION_BANK_API_BASE', 'http://localhost:5015')), '/' ); $this->timeout = $timeout ?? (int) config('services.learning_analytics.timeout', 20); } /** * 获取错题列表(支持多维筛选) */ public function listMistakes(array $params = []): array { $query = array_filter([ 'student_id' => $params['student_id'] ?? null, 'kp_ids' => $this->implodeIfArray($params['kp_ids'] ?? null), 'skill_ids' => $this->implodeIfArray($params['skill_ids'] ?? null), 'error_types' => $this->implodeIfArray($params['error_types'] ?? null), 'time_range' => $params['time_range'] ?? null, 'start_date' => $params['start_date'] ?? null, 'end_date' => $params['end_date'] ?? null, 'page' => $params['page'] ?? 1, 'per_page' => $params['per_page'] ?? 20, ], fn ($value) => filled($value)); try { $response = Http::timeout($this->timeout) ->get($this->learningAnalyticsBase . '/api/mistake-book', $query); if ($response->successful()) { info("MistakeBookService::listMistakes", [$response->json()]); $body = $response->json(); return is_array($body) ? $body : ['data' => $body]; } Log::warning('MistakeBook list failed', [ 'status' => $response->status(), 'body' => $response->body(), 'query' => $query, ]); } catch (\Throwable $e) { Log::error('MistakeBook list exception', [ 'error' => $e->getMessage(), 'query' => $query, ]); } return [ 'data' => [], 'meta' => ['total' => 0, 'page' => 1, 'per_page' => $query['per_page'] ?? 20], ]; } /** * 获取单条错题详情(可选带学生ID保证隔离) */ public function getMistakeDetail(string $mistakeId, ?string $studentId = null): array { $query = array_filter([ 'student_id' => $studentId, ], fn ($value) => filled($value)); try { $response = Http::timeout($this->timeout) ->get($this->learningAnalyticsBase . '/api/mistake-book/' . $mistakeId, $query); if ($response->successful()) { $body = $response->json(); return is_array($body) ? $body : []; } Log::warning('Mistake detail request failed', [ 'mistake_id' => $mistakeId, 'status' => $response->status(), 'body' => $response->body(), ]); // 兼容:LearningAnalytics 当前未提供单条详情接口时,从列表中过滤 if ($studentId) { $fallback = $this->listMistakes([ 'student_id' => $studentId, 'per_page' => 100, // 兼容后端限制(<=100) ]); $matched = collect($fallback['data'] ?? [])->firstWhere('id', (int) $mistakeId); if ($matched) { return $matched; } } } catch (\Throwable $e) { Log::error('Mistake detail request exception', [ 'mistake_id' => $mistakeId, 'student_id' => $studentId, 'error' => $e->getMessage(), ]); } return []; } /** * 获取错题统计概要 */ public function summarize(string $studentId): array { try { $response = Http::timeout($this->timeout) ->get($this->learningAnalyticsBase . '/api/mistake-book/summary', [ 'student_id' => $studentId, ]); if ($response->successful()) { $body = $response->json(); return is_array($body) ? $body : []; } Log::warning('MistakeBook summary failed', [ 'status' => $response->status(), 'body' => $response->body(), 'student_id' => $studentId, ]); } catch (\Throwable $e) { Log::error('MistakeBook summary exception', [ 'student_id' => $studentId, 'error' => $e->getMessage(), ]); } // fallback 返回一个空结构,避免前端崩溃 return [ 'total' => 0, 'this_week' => 0, 'pending_review' => 0, 'mastery_rate' => null, ]; } /** * 获取错误模式与推荐路径 */ public function getMistakePatterns(string $studentId): array { try { $response = Http::timeout($this->timeout) ->get($this->learningAnalyticsBase . '/api/analytics/mistake-pattern', [ 'student_id' => $studentId, ]); if ($response->successful()) { $body = $response->json(); return is_array($body) ? $body : []; } Log::warning('Mistake pattern request failed', [ 'status' => $response->status(), 'body' => $response->body(), 'student_id' => $studentId, ]); } catch (\Throwable $e) { Log::error('Mistake pattern request exception', [ 'student_id' => $studentId, 'error' => $e->getMessage(), ]); } return []; } /** * 收藏/取消收藏错题 */ public function toggleFavorite(string $mistakeId, bool $favorite = true): bool { try { $response = Http::timeout($this->timeout) ->post($this->learningAnalyticsBase . '/api/mistake-book/' . $mistakeId . '/favorite', [ 'favorite' => $favorite, ]); return $response->successful(); } catch (\Throwable $e) { Log::error('Toggle favorite failed', [ 'mistake_id' => $mistakeId, 'favorite' => $favorite, 'error' => $e->getMessage(), ]); } return false; } /** * 标记已复习 */ public function markReviewed(string $mistakeId): bool { try { $response = Http::timeout($this->timeout) ->post($this->learningAnalyticsBase . '/api/mistake-book/' . $mistakeId . '/review'); return $response->successful(); } catch (\Throwable $e) { Log::error('Mark reviewed failed', [ 'mistake_id' => $mistakeId, 'error' => $e->getMessage(), ]); } return false; } /** * 添加到重练清单 */ public function addToRetryList(string $mistakeId): bool { try { $response = Http::timeout($this->timeout) ->post($this->learningAnalyticsBase . '/api/mistake-book/' . $mistakeId . '/retry-list'); return $response->successful(); } catch (\Throwable $e) { Log::error('Add to retry list failed', [ 'mistake_id' => $mistakeId, 'error' => $e->getMessage(), ]); } return false; } /** * 基于错题推荐练习题 */ public function recommendPractice(string $studentId, array $kpIds = [], array $skillIds = []): array { $payload = array_filter([ 'student_id' => $studentId, 'kp_ids' => array_values(array_unique($kpIds)), 'skill_ids' => array_values(array_unique($skillIds)), ], fn ($value) => $value !== null); try { $response = Http::timeout($this->timeout) ->post($this->questionBankBase . '/api/questions/recommend', $payload); if ($response->successful()) { $body = $response->json(); return is_array($body) ? $body : ['data' => $body]; } Log::warning('Recommend practice failed', [ 'status' => $response->status(), 'body' => $response->body(), 'payload' => $payload, ]); } catch (\Throwable $e) { Log::error('Recommend practice exception', [ 'payload' => $payload, 'error' => $e->getMessage(), ]); } return ['data' => []]; } /** * 为学生仪表板提供快照数据 */ public function getPanelSnapshot(string $studentId, int $limit = 5): array { $list = $this->listMistakes([ 'student_id' => $studentId, 'per_page' => $limit, ]); $patterns = $this->getMistakePatterns($studentId); $summary = $this->summarize($studentId); return [ 'recent' => $list['data'] ?? [], 'weak_skills' => $patterns['top_skills'] ?? [], 'weak_kps' => $patterns['top_kps'] ?? [], 'error_types' => $patterns['error_types'] ?? [], 'recommend_path' => $patterns['recommend_path'] ?? [], 'stats' => [ 'total' => $summary['total'] ?? Arr::get($list, 'meta.total', 0), 'this_week' => $summary['this_week'] ?? null, 'pending_review' => $summary['pending_review'] ?? null, 'mastery_rate' => $summary['mastery_rate'] ?? null, ], ]; } private function implodeIfArray($value): ?string { if (is_array($value)) { return implode(',', array_filter($value)); } return $value; } /** * 获取题目的全体正确率(LearningAnalytics 聚合) */ public function getQuestionAccuracy(string $questionId): ?float { try { $response = Http::timeout($this->timeout) ->get($this->learningAnalyticsBase . '/api/analytics/question/' . $questionId . '/accuracy'); if ($response->successful()) { $body = $response->json(); return isset($body['accuracy']) ? floatval($body['accuracy']) : null; } } catch (\Throwable $e) { Log::warning('Get question accuracy failed', [ 'question_id' => $questionId, 'error' => $e->getMessage(), ]); } return null; } }