learningAnalyticsBase = ''; $this->knowledgeServiceBase = rtrim( $knowledgeServiceBase ?: config('services.knowledge_service.url', env('KNOWLEDGE_SERVICE_API_BASE', 'http://localhost:5011')), '/' ); $this->timeout = 20; } /** * 获取学生知识点掌握情况统计 * * @param string $studentId 学生ID * @return array */ public function getStats(string $studentId): array { try { // 使用本地的MasteryCalculator $masteryCalculator = app(MasteryCalculator::class); $overview = $masteryCalculator->getStudentMasteryOverview($studentId); // 转换为兼容格式 $body = [ 'student_id' => $studentId, 'total_knowledge_points' => $overview['total_knowledge_points'], 'average_mastery' => $overview['average_mastery_level'], 'mastered_count' => $overview['mastered_knowledge_points'], 'good_count' => $overview['good_knowledge_points'], 'weak_count' => $overview['weak_knowledge_points'], 'details' => $overview['details'], ]; // 丰富知识点名称(如果有knowledge_points表) $body = $this->enrichWithKnowledgePointNames($body); // 添加知识图谱总数统计 $graphStats = $this->getKnowledgeGraphStats(); $body['graph_total_knowledge_points'] = $graphStats['total'] ?? 0; Log::info('KnowledgeMasteryService::getStats (Local)', ['student_id' => $studentId]); return [ 'success' => true, 'data' => $body, ]; } catch (\Throwable $e) { Log::error('Knowledge mastery stats exception', [ 'student_id' => $studentId, 'error' => $e->getMessage(), ]); return [ 'success' => false, 'error' => '获取知识点掌握情况异常: ' . $e->getMessage(), ]; } } /** * 丰富知识点名称 */ private function enrichWithKnowledgePointNames(array $data): array { if (empty($data['details'])) { return $data; } // 确保 details 是数组格式(处理 stdClass 对象) $details = $data['details']; if (!is_array($details)) { $details = []; } elseif (isset($details[0]) && is_object($details[0])) { // 将 stdClass 对象转换为关联数组 $details = array_map(function ($item) { return (array) $item; }, $details); } // 收集所有kp_code $kpCodes = array_column($details, 'kp_code'); if (empty($kpCodes)) { return $data; } // 批量获取知识点名称 $kpNames = $this->getKnowledgePointNames($kpCodes); // 丰富details数据 foreach ($details as &$detail) { $kpCode = $detail['kp_code'] ?? null; if ($kpCode && isset($kpNames[$kpCode])) { $detail['kp_name'] = $kpNames[$kpCode]; } else { $detail['kp_name'] = $kpCode; // fallback to code } } // 更新原始数据 $data['details'] = $details; return $data; } /** * 批量获取知识点名称 */ private function getKnowledgePointNames(array $kpCodes): array { $result = []; foreach ($kpCodes as $kpCode) { $name = $this->getKnowledgePointName($kpCode); if ($name) { $result[$kpCode] = $name; } } return $result; } /** * 批量获取知识点名称映射(返回关联数组) */ private function getKnowledgePointNamesMap(array $kpCodes): array { $result = []; foreach ($kpCodes as $kpCode) { $name = $this->getKnowledgePointName($kpCode); // 如果获取不到名称,使用kpCode作为默认值 $result[$kpCode] = $name ?: $kpCode; } return $result; } /** * 获取单个知识点名称(带缓存) */ private function getKnowledgePointName(string $kpCode): ?string { $cacheKey = "kp_name_{$kpCode}"; return Cache::remember($cacheKey, 3600, function () use ($kpCode) { try { // 首先尝试从知识图谱服务获取 $response = Http::timeout(5) ->get($this->knowledgeServiceBase . '/knowledge-points/' . $kpCode); if ($response->successful()) { $data = $response->json(); $name = $data['cn_name'] ?? $data['en_name'] ?? null; if ($name) { return $name; } } } catch (\Throwable $e) { Log::debug('Failed to get knowledge point name from service, will use local mapping', [ 'kp_code' => $kpCode, 'error' => $e->getMessage(), ]); } // 如果知识图谱服务不可用,使用本地映射 return $this->getLocalKnowledgePointName($kpCode); }); } /** * 本地知识点名称映射(作为备选方案) */ private function getLocalKnowledgePointName(string $kpCode): ?string { // 常见的数学知识点中文名称映射 $nameMap = [ 'A09' => '因式分解', 'E01' => '一元二次方程', 'E03' => '一元二次方程的解法', 'E04' => '一元二次方程的应用', 'E06' => '判别式', 'F03' => '函数的概念', 'F05' => '函数的图像', 'F06' => '函数的性质', 'G02' => '几何图形', 'G03' => '三角形', 'R06' => '实数', 'S01' => '三角函数', 'S02' => '正弦函数', 'S03' => '余弦函数', 'S05' => '正切函数', 'E01B' => '因式分解法', 'E03B' => '公式法', 'E04B' => '配方法', 'G02B' => '平行四边形', 'G03E' => '等腰三角形', 'G03G' => '等边三角形', 'M01C' => '有理数', 'M04E' => '不等式', 'PY02' => '概率初步', 'ST04' => '统计与概率', 'M04E1' => '一元一次不等式', 'PY02D' => '随机事件', 'SIM02' => '相似三角形', 'ST04D' => '数据的收集', 'APP_E4' => '应用题', 'SIM02A' => '相似比', ]; return $nameMap[$kpCode] ?? null; } /** * 判断知识点是否为另一个知识点的子知识点 */ private function isChildOf(string $childCode, string $parentCode): bool { // 常见的父子关系映射 $parentChildMap = [ 'E01' => ['E01B'], // 一元二次方程 -> 因式分解法 'E03' => ['E03B'], // 一元二次方程的解法 -> 公式法 'E04' => ['E04B'], // 一元二次方程的应用 -> 配方法 'G02' => ['G02B'], // 几何图形 -> 平行四边形 'G03' => ['G03E', 'G03G'], // 三角形 -> 等腰三角形, 等边三角形 'M04E' => ['M04E1'], // 不等式 -> 一元一次不等式 'PY02' => ['PY02D'], // 概率初步 -> 随机事件 'SIM02' => ['SIM02A'], // 相似三角形 -> 相似比 'ST04' => ['ST04D'], // 统计与概率 -> 数据的收集 'G03' => ['G03E', 'G03G'], // 三角形 -> 等腰三角形, 等边三角形 ]; // 直接通过代码前缀判断 if (strpos($childCode, $parentCode) === 0 && $childCode !== $parentCode) { return true; } // 通过映射表判断 if (isset($parentChildMap[$parentCode]) && in_array($childCode, $parentChildMap[$parentCode])) { return true; } return false; } /** * 获取知识图谱统计信息(带缓存) */ public function getKnowledgeGraphStats(): array { return Cache::remember('knowledge_graph_stats', 3600, function () { try { $response = Http::timeout(10) ->get($this->knowledgeServiceBase . '/knowledge-points/'); if ($response->successful()) { $data = $response->json(); $items = $data['data'] ?? $data ?? []; return [ 'total' => count($items), 'updated_at' => now()->toISOString(), ]; } } catch (\Throwable $e) { Log::error('Failed to get knowledge graph stats', [ 'error' => $e->getMessage(), ]); } return ['total' => 0]; }); } /** * 获取学生知识点图谱数据 * * @param string $studentId 学生ID * @param string|null $examId 考试ID(可选,不指定则返回最新快照) * @return array */ public function getGraph(string $studentId, ?string $examId = null): array { try { // 使用本地的MasteryCalculator $masteryCalculator = app(MasteryCalculator::class); $overview = $masteryCalculator->getStudentMasteryOverview($studentId); // 转换为图谱格式 $nodes = []; $edges = []; foreach ($overview['details'] as $detail) { $masteryLevel = floatval($detail->mastery_level ?? 0); $kpCode = $detail->kp_code; // 节点 $nodes[] = [ 'id' => $kpCode, 'label' => $kpCode, 'mastery' => $masteryLevel, 'mastery_level' => $this->getMasteryLevelLabel($masteryLevel), 'color' => $this->getMasteryColor($masteryLevel), 'size' => 20 + ($masteryLevel * 20), ]; // 这里可以添加知识点之间的依赖关系边 // 暂时为空,后续从knowledge_points表获取 } $graphData = [ 'nodes' => $nodes, 'edges' => $edges, 'statistics' => [ 'total_nodes' => count($nodes), 'total_edges' => count($edges), 'average_mastery' => $overview['average_mastery_level'], ], ]; Log::info('KnowledgeMasteryService::getGraph (Local)', ['student_id' => $studentId, 'exam_id' => $examId]); return [ 'success' => true, 'data' => $graphData, ]; } catch (\Throwable $e) { Log::error('Knowledge graph exception', [ 'student_id' => $studentId, 'exam_id' => $examId, 'error' => $e->getMessage(), ]); return [ 'success' => false, 'error' => '获取知识点图谱异常: ' . $e->getMessage(), ]; } } /** * 获取掌握度等级标签 */ private function getMasteryLevelLabel(float $mastery): string { if ($mastery >= 0.85) return 'mastered'; if ($mastery >= 0.70) return 'good'; if ($mastery >= 0.50) return 'fair'; return 'weak'; } /** * 获取掌握度颜色 */ private function getMasteryColor(float $mastery): string { if ($mastery >= 0.85) return '#52c41a'; // 绿色 - 掌握 if ($mastery >= 0.70) return '#1890ff'; // 蓝色 - 良好 if ($mastery >= 0.50) return '#faad14'; // 橙色 - 一般 return '#ff4d4f'; // 红色 - 薄弱 } /** * 获取学生知识点图谱快照列表 * * @param string $studentId 学生ID * @param int $limit 返回数量限制 * @return array */ public function getGraphSnapshots(string $studentId, int $limit = 10): array { try { // 从knowledge_point_mastery_snapshots表获取快照 $snapshots = \DB::table('knowledge_point_mastery_snapshots') ->where('student_id', $studentId) ->orderBy('created_at', 'desc') ->limit($limit) ->get(); $snapshotList = $snapshots->map(function ($snapshot) { // 解析掌握度数据 $masteryData = json_decode($snapshot->mastery_data, true) ?: []; // 提取知识点信息 - 修复字段名 $knowledgePoints = []; $parentKnowledgePoints = []; // 存储父知识点信息 $kpCodes = array_keys($masteryData); // 收集所有kp_code用于批量获取名称 // 批量获取知识点名称 $kpNamesMap = $this->getKnowledgePointNamesMap($kpCodes); // 第一步:分类父知识点和子知识点 foreach ($masteryData as $kpCode => $kpData) { $isParent = (bool) ($kpData['is_parent'] ?? false); $masteryLevel = floatval($kpData['current_mastery'] ?? 0); $change = floatval($kpData['change'] ?? 0); $weight = intval($kpData['weight'] ?? 1); if ($isParent) { // 父知识点 - 稍后重新计算 $parentKnowledgePoints[$kpCode] = [ 'kp_code' => $kpCode, 'kp_name' => $kpNamesMap[$kpCode] ?? $kpCode, 'original_mastery' => $masteryLevel, 'change' => $change, 'weight' => $weight, 'children' => [] // 存储子知识点 ]; } else { // 子知识点 - 直接添加 $knowledgePoints[] = [ 'kp_code' => $kpCode, 'kp_name' => $kpNamesMap[$kpCode] ?? $kpCode, 'mastery_level' => round($masteryLevel, 2), 'change' => round($change, 2), 'is_child' => true, 'parent_code' => null, // 稍后填入父知识点 ]; } } // 第二步:建立父子关系并计算父知识点掌握度 foreach ($masteryData as $kpCode => $kpData) { $isParent = (bool) ($kpData['is_parent'] ?? false); if (!$isParent) { // 查找父知识点(通过代码前缀匹配或其他逻辑) foreach ($parentKnowledgePoints as $parentCode => $parentData) { // 简单的父子关系判断:如果子知识点代码包含父知识点代码前缀 if (strpos($kpCode, $parentCode) === 0 || $this->isChildOf($kpCode, $parentCode)) { // 添加到父知识点的子知识点列表 $parentKnowledgePoints[$parentCode]['children'][] = $kpCode; // 更新子知识点的父知识点信息 foreach ($knowledgePoints as &$child) { if ($child['kp_code'] === $kpCode) { $child['parent_code'] = $parentCode; break; } } break; } } } } // 第三步:计算父知识点的掌握度(加权平均) foreach ($parentKnowledgePoints as $parentCode => &$parentData) { if (empty($parentData['children'])) { // 如果没有子知识点,使用原始值 $parentData['mastery_level'] = round($parentData['original_mastery'], 2); } else { // 计算子知识点的加权平均 $totalWeightedMastery = 0; $totalWeight = 0; foreach ($parentData['children'] as $childCode) { if (isset($masteryData[$childCode])) { $childMastery = floatval($masteryData[$childCode]['current_mastery'] ?? 0); $childWeight = intval($masteryData[$childCode]['weight'] ?? 1); $totalWeightedMastery += $childMastery * $childWeight; $totalWeight += $childWeight; } } if ($totalWeight > 0) { $parentData['mastery_level'] = round($totalWeightedMastery / $totalWeight, 2); // 重新计算变化值 $parentData['change'] = round($parentData['mastery_level'] - $parentData['original_mastery'], 2); } else { $parentData['mastery_level'] = round($parentData['original_mastery'], 2); } } $parentData['is_parent'] = true; unset($parentData['original_mastery']); // 移除临时字段 // 添加到知识点列表 $knowledgePoints[] = $parentData; } unset($parentData); // 避免引用问题 // 第四步:按知识点代码排序 usort($knowledgePoints, function($a, $b) { return strcmp($a['kp_code'], $b['kp_code']); }); return [ 'snapshot_id' => $snapshot->snapshot_id, 'student_id' => $snapshot->student_id, 'paper_id' => $snapshot->paper_id, 'overall_mastery' => floatval($snapshot->overall_mastery), 'weak_knowledge_points_count' => intval($snapshot->weak_knowledge_points_count), 'strong_knowledge_points_count' => intval($snapshot->strong_knowledge_points_count), 'knowledge_points' => $knowledgePoints, 'knowledge_points_count' => count($knowledgePoints), 'created_at' => $snapshot->created_at, 'snapshot_time' => $snapshot->snapshot_time, ]; })->toArray(); Log::info('KnowledgeMasteryService::getGraphSnapshots (Local)', [ 'student_id' => $studentId, 'limit' => $limit, 'snapshot_count' => count($snapshotList) ]); return [ 'success' => true, 'data' => [ 'snapshots' => $snapshotList, 'total' => count($snapshotList), ], ]; } catch (\Throwable $e) { Log::error('Knowledge graph snapshots exception', [ 'student_id' => $studentId, 'limit' => $limit, 'error' => $e->getMessage(), ]); return [ 'success' => false, 'error' => '获取知识点图谱快照列表异常: ' . $e->getMessage(), ]; } } /** * 获取学生知识点掌握摘要(简化版) * * @param string $studentId 学生ID * @return array */ public function getSummary(string $studentId): array { $stats = $this->getStats($studentId); if (!$stats['success']) { return $stats; } $data = $stats['data']; return [ 'success' => true, 'data' => [ 'student_id' => $data['student_id'] ?? $studentId, 'total' => $data['total_knowledge_points'] ?? 0, 'mastered' => $data['mastered_knowledge_points'] ?? 0, 'unmastered' => $data['unmastered_knowledge_points'] ?? 0, 'mastery_rate' => $data['mastery_rate'] ?? 0.0, 'mastery_percentage' => round(($data['mastery_rate'] ?? 0) * 100, 1) . '%', 'graph_total' => $data['graph_total_knowledge_points'] ?? 0, ], ]; } /** * 创建知识点掌握度快照 * * @param string $studentId 学生ID * @param string $snapshotType 快照类型 (exam/report/manual/scheduled) * @param string|null $sourceId 来源ID * @param string|null $sourceName 来源名称 * @param string|null $notes 备注 * @return array */ public function createSnapshot( string $studentId, string $snapshotType = 'report', ?string $sourceId = null, ?string $sourceName = null, ?string $notes = null ): array { try { // 使用LocalAIAnalysisService创建快照 $localAI = app(LocalAIAnalysisService::class); // 获取当前掌握度数据 $masteryCalculator = app(MasteryCalculator::class); $overview = $masteryCalculator->getStudentMasteryOverview($studentId); $snapshots = []; foreach ($overview['details'] as $detail) { $snapshotId = \DB::table('knowledge_point_mastery_snapshots')->insertGetId([ 'student_id' => $studentId, 'kp_code' => $detail->kp_code, 'mastery_level' => $detail->mastery_level, 'confidence_level' => $detail->confidence_level, 'snapshot_type' => $snapshotType, 'source_id' => $sourceId, 'source_name' => $sourceName, 'notes' => $notes, 'created_at' => now(), 'updated_at' => now(), ]); $snapshots[] = [ 'snapshot_id' => $snapshotId, 'kp_code' => $detail->kp_code, 'mastery_level' => $detail->mastery_level, ]; } Log::info('KnowledgeMasteryService::createSnapshot (Local)', [ 'student_id' => $studentId, 'snapshot_type' => $snapshotType, 'snapshot_count' => count($snapshots), ]); return [ 'success' => true, 'data' => [ 'snapshots' => $snapshots, 'total_snapshots' => count($snapshots), ], ]; } catch (\Throwable $e) { Log::error('Create knowledge mastery snapshot exception', [ 'student_id' => $studentId, 'error' => $e->getMessage(), ]); return [ 'success' => false, 'error' => '创建知识点掌握度快照异常: ' . $e->getMessage(), ]; } } }