| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653 |
- <?php
- namespace App\Services;
- use Illuminate\Support\Facades\Http;
- use Illuminate\Support\Facades\Log;
- use Illuminate\Support\Facades\Cache;
- /**
- * 知识点掌握情况服务
- *
- * 提供:
- * 1. 获取学生知识点掌握情况统计
- * 2. 获取知识点图谱数据(考试快照)
- * 3. 获取知识点图谱快照列表
- */
- class KnowledgeMasteryService
- {
- protected string $learningAnalyticsBase;
- protected string $knowledgeServiceBase;
- protected int $timeout;
- public function __construct(?string $learningAnalyticsBase = null, ?string $knowledgeServiceBase = null, ?int $timeout = null)
- {
- // 已迁移到本地,使用MasteryCalculator
- $this->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(),
- ];
- }
- }
- }
|