|
|
@@ -2,367 +2,193 @@
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
|
+use Illuminate\Support\Facades\Http;
|
|
|
+use Illuminate\Support\Facades\Log;
|
|
|
+
|
|
|
class KnowledgeGraphService
|
|
|
{
|
|
|
- protected MathRecSysService $mathRecSys;
|
|
|
+ protected string $baseUrl;
|
|
|
|
|
|
- public function __construct(MathRecSysService $mathRecSys)
|
|
|
+ public function __construct()
|
|
|
{
|
|
|
- $this->mathRecSys = $mathRecSys;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取知识图谱数据(格式化后)
|
|
|
- *
|
|
|
- * @param string|null $focus 焦点知识点
|
|
|
- * @return array
|
|
|
- */
|
|
|
- public function getGraphData(?string $focus = null): array
|
|
|
- {
|
|
|
- try {
|
|
|
- $graphData = $this->mathRecSys->getKnowledgeGraph($focus);
|
|
|
-
|
|
|
- return [
|
|
|
- 'success' => true,
|
|
|
- 'nodes' => $this->formatNodes($graphData['nodes'] ?? []),
|
|
|
- 'edges' => $this->formatEdges($graphData['edges'] ?? []),
|
|
|
- 'categories' => $this->extractCategories($graphData['nodes'] ?? []),
|
|
|
- 'metadata' => [
|
|
|
- 'total_nodes' => count($graphData['nodes'] ?? []),
|
|
|
- 'total_edges' => count($graphData['edges'] ?? []),
|
|
|
- 'focus' => $focus
|
|
|
- ]
|
|
|
- ];
|
|
|
-
|
|
|
- } catch (\Exception $e) {
|
|
|
- \Log::error('获取知识图谱数据失败', ['error' => $e->getMessage()]);
|
|
|
- return [
|
|
|
- 'success' => false,
|
|
|
- 'nodes' => [],
|
|
|
- 'edges' => [],
|
|
|
- 'categories' => [],
|
|
|
- 'error' => $e->getMessage()
|
|
|
- ];
|
|
|
- }
|
|
|
+ // 从配置文件读取base_url,而不是写死
|
|
|
+ $this->baseUrl = config('services.knowledge_api.base_url', env('KNOWLEDGE_API_BASE', 'http://localhost:5011'));
|
|
|
+ $this->baseUrl = rtrim($this->baseUrl, '/');
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 获取知识点详情
|
|
|
- *
|
|
|
- * @param string $kpId 知识点ID
|
|
|
- * @return array
|
|
|
+ * 获取知识点列表
|
|
|
*/
|
|
|
- public function getKnowledgePointDetail(string $kpId): array
|
|
|
+ public function listKnowledgePoints(int $page = 1, int $perPage = 100): array
|
|
|
{
|
|
|
try {
|
|
|
- // 从MathRecSys获取
|
|
|
- $detail = $this->mathRecSys->getKnowledgePoint($kpId);
|
|
|
-
|
|
|
- // 从本地获取相关题目(这里需要实现)
|
|
|
- $relatedQuestions = $this->getRelatedQuestions($kpId);
|
|
|
-
|
|
|
- return [
|
|
|
- 'success' => true,
|
|
|
- 'data' => array_merge($detail['data'] ?? [], [
|
|
|
- 'related_questions' => $relatedQuestions
|
|
|
- ])
|
|
|
- ];
|
|
|
+ $response = Http::timeout(10)
|
|
|
+ ->withHeaders(['Accept' => 'application/json'])
|
|
|
+ ->get($this->baseUrl . '/knowledge-points/', [
|
|
|
+ 'page' => $page,
|
|
|
+ 'per_page' => $perPage
|
|
|
+ ]);
|
|
|
+
|
|
|
+ if ($response->successful()) {
|
|
|
+ $data = $response->json();
|
|
|
+ $points = $data['data'] ?? $data ?? [];
|
|
|
+
|
|
|
+ // 格式化知识点数据
|
|
|
+ return array_map(function($kp) {
|
|
|
+ return [
|
|
|
+ 'id' => (string)($kp['id'] ?? $kp['kp_id'] ?? uniqid()),
|
|
|
+ 'code' => $kp['kp_code'] ?? $kp['kp_id'] ?? $kp['code'] ?? 'KP_UNKNOWN',
|
|
|
+ 'name' => $kp['cn_name'] ?? $kp['kp_name'] ?? $kp['name'] ?? $kp['kp_code'] ?? '未知知识点',
|
|
|
+ 'subject' => $kp['category'] ?? '数学',
|
|
|
+ 'phase' => $kp['phase'] ?? '',
|
|
|
+ 'importance' => $kp['importance'] ?? 0,
|
|
|
+ ];
|
|
|
+ }, $points);
|
|
|
+ }
|
|
|
|
|
|
+ Log::warning('知识图谱API调用失败', [
|
|
|
+ 'status' => $response->status(),
|
|
|
+ 'url' => $this->baseUrl . '/knowledge-points',
|
|
|
+ 'response' => $response->json()
|
|
|
+ ]);
|
|
|
} catch (\Exception $e) {
|
|
|
- \Log::error('获取知识点详情失败', [
|
|
|
- 'kp_id' => $kpId,
|
|
|
+ Log::error('获取知识点列表失败', [
|
|
|
'error' => $e->getMessage()
|
|
|
]);
|
|
|
- return [
|
|
|
- 'success' => false,
|
|
|
- 'error' => $e->getMessage()
|
|
|
- ];
|
|
|
}
|
|
|
+
|
|
|
+ // 返回备用数据
|
|
|
+ return $this->getFallbackKnowledgePoints();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 获取知识点依赖关系
|
|
|
- *
|
|
|
- * @param string $kpId 知识点ID
|
|
|
- * @return array
|
|
|
+ * 根据知识点代码获取技能列表
|
|
|
*/
|
|
|
- public function getKnowledgePointDependencies(string $kpId): array
|
|
|
+ public function getSkillsByKnowledgePoint(string $kpCode): array
|
|
|
{
|
|
|
try {
|
|
|
- $graphData = $this->mathRecSys->getKnowledgeGraph($kpId);
|
|
|
-
|
|
|
- $dependencies = [
|
|
|
- 'prerequisites' => [], // 前置知识点
|
|
|
- 'advances' => [], // 后续知识点
|
|
|
- 'contrasts' => [] // 对比知识点
|
|
|
- ];
|
|
|
-
|
|
|
- foreach ($graphData['edges'] ?? [] as $edge) {
|
|
|
- if ($edge['start_uuid'] === $kpId) {
|
|
|
- // 该知识点指向其他知识点
|
|
|
- if ($edge['type'] === 'AdvancesTo') {
|
|
|
- $dependencies['advances'][] = [
|
|
|
- 'target' => $edge['end_uuid'],
|
|
|
- 'description' => $edge['properties']['description'] ?? ''
|
|
|
- ];
|
|
|
- } elseif ($edge['type'] === 'ContrastsWith') {
|
|
|
- $dependencies['contrasts'][] = [
|
|
|
- 'target' => $edge['end_uuid'],
|
|
|
- 'description' => $edge['properties']['description'] ?? ''
|
|
|
- ];
|
|
|
- }
|
|
|
- } elseif ($edge['end_uuid'] === $kpId) {
|
|
|
- // 其他知识点指向该知识点
|
|
|
- if ($edge['type'] === 'Prerequisite') {
|
|
|
- $dependencies['prerequisites'][] = [
|
|
|
- 'source' => $edge['start_uuid'],
|
|
|
- 'description' => $edge['properties']['description'] ?? ''
|
|
|
- ];
|
|
|
- }
|
|
|
- }
|
|
|
+ $response = Http::timeout(10)
|
|
|
+ ->withHeaders(['Accept' => 'application/json'])
|
|
|
+ ->get($this->baseUrl . "/graph/node/{$kpCode}");
|
|
|
+
|
|
|
+ if ($response->successful()) {
|
|
|
+ $data = $response->json();
|
|
|
+ $skills = $data['skills'] ?? [];
|
|
|
+
|
|
|
+ return array_map(function($skill) {
|
|
|
+ return [
|
|
|
+ 'code' => $skill['skill_code'] ?? '',
|
|
|
+ 'name' => $skill['skill_name'] ?? '',
|
|
|
+ 'weight' => $skill['weight'] ?? 0,
|
|
|
+ ];
|
|
|
+ }, $skills);
|
|
|
}
|
|
|
|
|
|
- return [
|
|
|
- 'success' => true,
|
|
|
- 'data' => $dependencies
|
|
|
- ];
|
|
|
-
|
|
|
+ Log::warning('获取技能列表失败', [
|
|
|
+ 'status' => $response->status(),
|
|
|
+ 'kp_code' => $kpCode
|
|
|
+ ]);
|
|
|
} catch (\Exception $e) {
|
|
|
- \Log::error('获取知识点依赖关系失败', [
|
|
|
- 'kp_id' => $kpId,
|
|
|
+ Log::error('获取技能列表异常', [
|
|
|
+ 'kp_code' => $kpCode,
|
|
|
'error' => $e->getMessage()
|
|
|
]);
|
|
|
- return [
|
|
|
- 'success' => false,
|
|
|
- 'error' => $e->getMessage()
|
|
|
- ];
|
|
|
}
|
|
|
+
|
|
|
+ return [];
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 获取学习路径建议
|
|
|
- *
|
|
|
- * @param string $studentId 学生ID
|
|
|
- * @param string $targetKp 目标知识点
|
|
|
- * @return array
|
|
|
+ * 获取所有技能列表
|
|
|
*/
|
|
|
- public function getLearningPath(string $studentId, string $targetKp): array
|
|
|
+ public function listSkills(int $page = 1, int $perPage = 50): array
|
|
|
{
|
|
|
try {
|
|
|
- // 获取学生画像
|
|
|
- $profile = $this->mathRecSys->getStudentProfile($studentId);
|
|
|
-
|
|
|
- // 获取知识图谱
|
|
|
- $graphData = $this->mathRecSys->getKnowledgeGraph($targetKp);
|
|
|
-
|
|
|
- // 分析前置知识点掌握情况
|
|
|
- $prerequisites = [];
|
|
|
- foreach ($graphData['edges'] ?? [] as $edge) {
|
|
|
- if ($edge['end_uuid'] === $targetKp && $edge['type'] === 'Prerequisite') {
|
|
|
- $prereqId = $edge['start_uuid'];
|
|
|
- $mastery = $this->getMasteryFromProfile($profile, $prereqId);
|
|
|
-
|
|
|
- $prerequisites[] = [
|
|
|
- 'kp_id' => $prereqId,
|
|
|
- 'description' => $edge['properties']['description'] ?? '',
|
|
|
- 'current_mastery' => $mastery,
|
|
|
- 'is_ready' => $mastery >= 0.6,
|
|
|
- 'priority' => $this->calculatePriority($mastery)
|
|
|
- ];
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 按优先级排序
|
|
|
- usort($prerequisites, function ($a, $b) {
|
|
|
- return $b['priority'] <=> $a['priority'];
|
|
|
- });
|
|
|
-
|
|
|
- // 生成学习路径
|
|
|
- $learningPath = [];
|
|
|
- foreach ($prerequisites as $prereq) {
|
|
|
- if (!$prereq['is_ready']) {
|
|
|
- $learningPath[] = [
|
|
|
- 'type' => 'review',
|
|
|
- 'kp_id' => $prereq['kp_id'],
|
|
|
- 'reason' => '需要先掌握前置知识点',
|
|
|
- 'priority' => $prereq['priority']
|
|
|
+ $response = Http::timeout(10)
|
|
|
+ ->get($this->baseUrl . '/skills/', [
|
|
|
+ 'page' => $page,
|
|
|
+ 'per_page' => $perPage
|
|
|
+ ]);
|
|
|
+
|
|
|
+ if ($response->successful()) {
|
|
|
+ $data = $response->json();
|
|
|
+ $skills = $data['data'] ?? $data ?? [];
|
|
|
+
|
|
|
+ // 格式化技能数据
|
|
|
+ return array_map(function($skill) {
|
|
|
+ return [
|
|
|
+ 'id' => (string)($skill['id'] ?? $skill['skill_code'] ?? uniqid()),
|
|
|
+ 'code' => $skill['skill_code'] ?? $skill['code'] ?? 'SK_UNKNOWN',
|
|
|
+ 'name' => $skill['skill_name'] ?? $skill['name'] ?? $skill['skill_code'] ?? '未知技能',
|
|
|
+ 'category' => $skill['skill_type'] ?? $skill['category'] ?? '基础技能'
|
|
|
];
|
|
|
- }
|
|
|
+ }, $skills);
|
|
|
}
|
|
|
|
|
|
- // 添加目标知识点
|
|
|
- $learningPath[] = [
|
|
|
- 'type' => 'learn',
|
|
|
- 'kp_id' => $targetKp,
|
|
|
- 'reason' => '学习目标知识点',
|
|
|
- 'priority' => 100
|
|
|
- ];
|
|
|
-
|
|
|
- return [
|
|
|
- 'success' => true,
|
|
|
- 'data' => [
|
|
|
- 'student_id' => $studentId,
|
|
|
- 'target_kp' => $targetKp,
|
|
|
- 'learning_path' => $learningPath,
|
|
|
- 'total_steps' => count($learningPath),
|
|
|
- 'estimated_hours' => count($learningPath) * 2 // 假设每个知识点需要2小时
|
|
|
- ]
|
|
|
- ];
|
|
|
-
|
|
|
+ Log::warning('技能API调用失败', [
|
|
|
+ 'status' => $response->status()
|
|
|
+ ]);
|
|
|
} catch (\Exception $e) {
|
|
|
- \Log::error('获取学习路径失败', [
|
|
|
- 'student_id' => $studentId,
|
|
|
- 'target_kp' => $targetKp,
|
|
|
+ Log::error('获取技能列表失败', [
|
|
|
'error' => $e->getMessage()
|
|
|
]);
|
|
|
- return [
|
|
|
- 'success' => false,
|
|
|
- 'error' => $e->getMessage()
|
|
|
- ];
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- /**
|
|
|
- * 格式化节点数据
|
|
|
- *
|
|
|
- * @param array $nodes 原始节点数据
|
|
|
- * @return array
|
|
|
- */
|
|
|
- private function formatNodes(array $nodes): array
|
|
|
- {
|
|
|
- return array_map(function ($node) {
|
|
|
- $props = $node['properties'] ?? [];
|
|
|
-
|
|
|
- return [
|
|
|
- 'id' => $props['uuid'] ?? $props['kp_id'] ?? uniqid(),
|
|
|
- 'name' => $props['node_name'] ?? $props['kp_name'] ?? '未知知识点',
|
|
|
- 'category' => $props['grade'] ?? $props['grade_label'] ?? '未分类',
|
|
|
- 'description' => $props['description'] ?? '',
|
|
|
- 'value' => $props['mastery'] ?? 0.6,
|
|
|
- 'difficulty' => $props['difficulty'] ?? 0.5,
|
|
|
- 'book' => $props['book'] ?? '',
|
|
|
- 'chapter' => $props['chapter'] ?? '',
|
|
|
- 'keywords' => $props['keywords'] ?? '',
|
|
|
- 'uuid' => $props['uuid'] ?? null,
|
|
|
- 'kp_id' => $props['kp_id'] ?? null
|
|
|
- ];
|
|
|
- }, $nodes);
|
|
|
+ // 返回备用数据
|
|
|
+ return $this->getFallbackSkills();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 格式化边数据
|
|
|
- *
|
|
|
- * @param array $edges 原始边数据
|
|
|
- * @return array
|
|
|
+ * 检查服务健康状态
|
|
|
*/
|
|
|
- private function formatEdges(array $edges): array
|
|
|
+ public function checkHealth(): bool
|
|
|
{
|
|
|
- return array_map(function ($edge) {
|
|
|
- return [
|
|
|
- 'source' => $edge['start_uuid'] ?? $edge['parent_kp'] ?? '',
|
|
|
- 'target' => $edge['end_uuid'] ?? $edge['child_kp'] ?? '',
|
|
|
- 'type' => $edge['type'] ?? $edge['relation_type'] ?? 'Prerequisite',
|
|
|
- 'description' => $edge['properties']['description'] ?? '',
|
|
|
- 'strength' => $edge['properties']['strength'] ?? 0.8
|
|
|
- ];
|
|
|
- }, $edges);
|
|
|
- }
|
|
|
+ try {
|
|
|
+ $response = Http::timeout(5)
|
|
|
+ ->get($this->baseUrl . '/health');
|
|
|
|
|
|
- /**
|
|
|
- * 提取分类
|
|
|
- *
|
|
|
- * @param array $nodes 节点数据
|
|
|
- * @return array
|
|
|
- */
|
|
|
- private function extractCategories(array $nodes): array
|
|
|
- {
|
|
|
- $categories = [];
|
|
|
- foreach ($nodes as $node) {
|
|
|
- $props = $node['properties'] ?? [];
|
|
|
- $category = $props['grade'] ?? $props['grade_label'] ?? '未分类';
|
|
|
- if (!in_array($category, $categories)) {
|
|
|
- $categories[] = $category;
|
|
|
- }
|
|
|
+ return $response->successful();
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::error('知识图谱服务健康检查失败', [
|
|
|
+ 'error' => $e->getMessage()
|
|
|
+ ]);
|
|
|
+ return false;
|
|
|
}
|
|
|
-
|
|
|
- // 为每个分类分配颜色
|
|
|
- return array_map(function ($category, $index) {
|
|
|
- return [
|
|
|
- 'name' => $category,
|
|
|
- 'color' => $this->getCategoryColor($index)
|
|
|
- ];
|
|
|
- }, $categories, array_keys($categories));
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 获取分类颜色
|
|
|
- *
|
|
|
- * @param int $index 分类索引
|
|
|
- * @return string
|
|
|
+ * 获取备用知识点数据
|
|
|
*/
|
|
|
- private function getCategoryColor(int $index): string
|
|
|
+ private function getFallbackKnowledgePoints(): array
|
|
|
{
|
|
|
- $colors = [
|
|
|
- '#3b82f6', // 蓝色
|
|
|
- '#10b981', // 绿色
|
|
|
- '#f59e0b', // 黄色
|
|
|
- '#ef4444', // 红色
|
|
|
- '#8b5cf6', // 紫色
|
|
|
- '#ec4899', // 粉色
|
|
|
- '#06b6d4', // 青色
|
|
|
- '#84cc16', // 青柠
|
|
|
+ return [
|
|
|
+ ['id' => 'kp_1', 'code' => 'KP1001', 'name' => '因式分解基础', 'subject' => '数学', 'phase' => '初中', 'importance' => 5],
|
|
|
+ ['id' => 'kp_2', 'code' => 'KP1002', 'name' => '提取公因式', 'subject' => '数学', 'phase' => '初中', 'importance' => 5],
|
|
|
+ ['id' => 'kp_3', 'code' => 'KP1003', 'name' => '平方差公式', 'subject' => '数学', 'phase' => '初中', 'importance' => 5],
|
|
|
+ ['id' => 'kp_4', 'code' => 'KP1004', 'name' => '完全平方公式', 'subject' => '数学', 'phase' => '初中', 'importance' => 5],
|
|
|
+ ['id' => 'kp_5', 'code' => 'KP1005', 'name' => '分组分解法', 'subject' => '数学', 'phase' => '初中', 'importance' => 4],
|
|
|
+ ['id' => 'kp_6', 'code' => 'KP1006', 'name' => '十字相乘法', 'subject' => '数学', 'phase' => '初中', 'importance' => 5],
|
|
|
+ ['id' => 'kp_7', 'code' => 'KP1007', 'name' => '有理数运算', 'subject' => '数学', 'phase' => '初中', 'importance' => 5],
|
|
|
+ ['id' => 'kp_8', 'code' => 'KP1008', 'name' => '一元二次方程', 'subject' => '数学', 'phase' => '初中', 'importance' => 5],
|
|
|
+ ['id' => 'kp_9', 'code' => 'KP1009', 'name' => '不等式', 'subject' => '数学', 'phase' => '初中', 'importance' => 4],
|
|
|
+ ['id' => 'kp_10', 'code' => 'KP1010', 'name' => '函数基础', 'subject' => '数学', 'phase' => '初中', 'importance' => 5],
|
|
|
];
|
|
|
-
|
|
|
- return $colors[$index % count($colors)];
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 从画像中获取掌握度
|
|
|
- *
|
|
|
- * @param array $profile 学生画像
|
|
|
- * @param string $kpId 知识点ID
|
|
|
- * @return float
|
|
|
- */
|
|
|
- private function getMasteryFromProfile(array $profile, string $kpId): float
|
|
|
- {
|
|
|
- $masteryData = $profile['data']['mastery'] ?? $profile['mastery'] ?? [];
|
|
|
- foreach ($masteryData as $mastery) {
|
|
|
- if (($mastery['kp'] ?? $mastery['kp_id'] ?? '') === $kpId) {
|
|
|
- return floatval($mastery['level'] ?? $mastery['mastery_level'] ?? 0);
|
|
|
- }
|
|
|
- }
|
|
|
- return 0.0;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 计算优先级
|
|
|
- *
|
|
|
- * @param float $mastery 掌握度
|
|
|
- * @return int
|
|
|
+ * 获取备用技能数据
|
|
|
*/
|
|
|
- private function calculatePriority(float $mastery): int
|
|
|
+ private function getFallbackSkills(): array
|
|
|
{
|
|
|
- if ($mastery < 0.3) {
|
|
|
- return 90; // 急需掌握
|
|
|
- } elseif ($mastery < 0.6) {
|
|
|
- return 70; // 需要复习
|
|
|
- } elseif ($mastery < 0.8) {
|
|
|
- return 50; // 适当巩固
|
|
|
- } else {
|
|
|
- return 10; // 已经掌握
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取相关题目(需要根据实际题库实现)
|
|
|
- *
|
|
|
- * @param string $kpId 知识点ID
|
|
|
- * @return array
|
|
|
- */
|
|
|
- private function getRelatedQuestions(string $kpId): array
|
|
|
- {
|
|
|
- // 这里需要实现从本地题库查询相关题目的逻辑
|
|
|
- // 暂时返回空数组
|
|
|
- return [];
|
|
|
+ return [
|
|
|
+ ['id' => 'sk_1', 'code' => 'SK001', 'name' => '计算能力', 'category' => '基础技能'],
|
|
|
+ ['id' => 'sk_2', 'code' => 'SK002', 'name' => '逻辑推理', 'category' => '思维技能'],
|
|
|
+ ['id' => 'sk_3', 'code' => 'SK003', 'name' => '模式识别', 'category' => '认知技能'],
|
|
|
+ ['id' => 'sk_4', 'code' => 'SK004', 'name' => '代数运算', 'category' => '专业技能'],
|
|
|
+ ['id' => 'sk_5', 'code' => 'SK005', 'name' => '解题能力', 'category' => '专业技能'],
|
|
|
+ ['id' => 'sk_6', 'code' => 'SK006', 'name' => '分析能力', 'category' => '思维技能'],
|
|
|
+ ['id' => 'sk_7', 'code' => 'SK007', 'name' => '抽象思维', 'category' => '高级技能'],
|
|
|
+ ['id' => 'sk_8', 'code' => 'SK008', 'name' => '创新思维', 'category' => '高级技能'],
|
|
|
+ ];
|
|
|
}
|
|
|
}
|