|
|
@@ -0,0 +1,212 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace App\Http\Controllers\Api;
|
|
|
+
|
|
|
+use App\Models\KnowledgePoint;
|
|
|
+use Illuminate\Http\JsonResponse;
|
|
|
+use Illuminate\Http\Request;
|
|
|
+use Illuminate\Support\Facades\Log;
|
|
|
+use Illuminate\Support\Facades\Cache;
|
|
|
+
|
|
|
+class KnowledgePointTreeController
|
|
|
+{
|
|
|
+ /**
|
|
|
+ * 获取知识点树形结构
|
|
|
+ * 从 MySQL 数据库读取数据,构建层级结构
|
|
|
+ *
|
|
|
+ * @param Request $request
|
|
|
+ * @return JsonResponse
|
|
|
+ */
|
|
|
+ public function index(Request $request): JsonResponse
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ // 使用缓存避免重复查询
|
|
|
+ $cacheKey = 'knowledge-point-tree-' . md5($request->getQueryString() ?? '');
|
|
|
+ $treeData = Cache::remember($cacheKey, 3600, function () {
|
|
|
+ return $this->buildTreeFromDatabase();
|
|
|
+ });
|
|
|
+
|
|
|
+ return response()->json([
|
|
|
+ 'success' => true,
|
|
|
+ 'data' => $treeData,
|
|
|
+ ]);
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::error('获取知识点树形结构失败', [
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
+ 'trace' => $e->getTraceAsString(),
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return response()->json([
|
|
|
+ 'success' => false,
|
|
|
+ 'message' => '获取知识点数据失败:' . $e->getMessage(),
|
|
|
+ 'data' => [],
|
|
|
+ ], 500);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从数据库构建树形结构
|
|
|
+ *
|
|
|
+ * @return array
|
|
|
+ */
|
|
|
+ private function buildTreeFromDatabase(): array
|
|
|
+ {
|
|
|
+ // 从数据库获取所有知识点
|
|
|
+ $knowledgePoints = KnowledgePoint::query()
|
|
|
+ ->orderBy('kp_code')
|
|
|
+ ->get()
|
|
|
+ ->toArray();
|
|
|
+
|
|
|
+ if (empty($knowledgePoints)) {
|
|
|
+ Log::warning('数据库中没有找到知识点数据');
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建节点映射(以 kp_code 为键)
|
|
|
+ $nodesMap = [];
|
|
|
+ foreach ($knowledgePoints as $point) {
|
|
|
+ $kpCode = $point['kp_code'];
|
|
|
+ $nodesMap[$kpCode] = [
|
|
|
+ 'id' => $kpCode,
|
|
|
+ 'label' => $point['name'] ?? $kpCode,
|
|
|
+ 'children' => [],
|
|
|
+ // 可选字段:如果数据库中有相关数据则添加
|
|
|
+ 'skills' => $this->extractSkills($point),
|
|
|
+ 'direct_score' => $this->extractDirectScore($point),
|
|
|
+ 'related_score' => $this->extractRelatedScore($point),
|
|
|
+ // 保留原始数据用于调试
|
|
|
+ '_raw' => $point,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建树形结构
|
|
|
+ $rootNodes = [];
|
|
|
+ foreach ($nodesMap as $kpCode => &$node) {
|
|
|
+ $parentKpCode = $this->getParentKpCode($node['_raw']);
|
|
|
+
|
|
|
+ // 如果没有父节点,则为根节点
|
|
|
+ if (empty($parentKpCode) || !isset($nodesMap[$parentKpCode])) {
|
|
|
+ $rootNodes[] = &$node;
|
|
|
+ } else {
|
|
|
+ // 添加到父节点的 children 中
|
|
|
+ $nodesMap[$parentKpCode]['children'][] = &$node;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清理临时数据
|
|
|
+ foreach ($nodesMap as &$node) {
|
|
|
+ unset($node['_raw']);
|
|
|
+ }
|
|
|
+
|
|
|
+ Log::info('成功构建知识点树形结构', [
|
|
|
+ 'total_nodes' => count($knowledgePoints),
|
|
|
+ 'root_nodes' => count($rootNodes),
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return $rootNodes;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 提取技能点信息
|
|
|
+ *
|
|
|
+ * @param array $point
|
|
|
+ * @return array
|
|
|
+ */
|
|
|
+ private function extractSkills(array $point): array
|
|
|
+ {
|
|
|
+ // 如果数据库中有 skills 字段,直接返回
|
|
|
+ if (isset($point['skills']) && is_array($point['skills'])) {
|
|
|
+ return $point['skills'];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 否则返回空数组
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 提取直接得分范围
|
|
|
+ *
|
|
|
+ * @param array $point
|
|
|
+ * @return array
|
|
|
+ */
|
|
|
+ private function extractDirectScore(array $point): array
|
|
|
+ {
|
|
|
+ // 如果数据库中有 direct_score 字段,直接返回
|
|
|
+ if (isset($point['direct_score']) && is_array($point['direct_score'])) {
|
|
|
+ return $point['direct_score'];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从 stats 字段提取(如果存在)
|
|
|
+ if (isset($point['stats']) && is_array($point['stats'])) {
|
|
|
+ if (isset($point['stats']['direct_score'])) {
|
|
|
+ return (array) $point['stats']['direct_score'];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 默认值
|
|
|
+ return [1, 3];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 提取相关得分范围
|
|
|
+ *
|
|
|
+ * @param array $point
|
|
|
+ * @return array
|
|
|
+ */
|
|
|
+ private function extractRelatedScore(array $point): array
|
|
|
+ {
|
|
|
+ // 如果数据库中有 related_score 字段,直接返回
|
|
|
+ if (isset($point['related_score']) && is_array($point['related_score'])) {
|
|
|
+ return $point['related_score'];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从 stats 字段提取(如果存在)
|
|
|
+ if (isset($point['stats']) && is_array($point['stats'])) {
|
|
|
+ if (isset($point['stats']['related_score'])) {
|
|
|
+ return (array) $point['stats']['related_score'];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 默认值
|
|
|
+ return [2, 5];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取父知识点代码
|
|
|
+ *
|
|
|
+ * @param array $point
|
|
|
+ * @return string|null
|
|
|
+ */
|
|
|
+ private function getParentKpCode(array $point): ?string
|
|
|
+ {
|
|
|
+ // 直接从 parent_kp_code 字段获取
|
|
|
+ if (!empty($point['parent_kp_code'])) {
|
|
|
+ return $point['parent_kp_code'];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果没有 parent_kp_code,尝试从 kp_code 推断
|
|
|
+ // 例如:R01 的父节点可能是 S01,M01 的父节点可能是 M00
|
|
|
+ $kpCode = $point['kp_code'] ?? '';
|
|
|
+
|
|
|
+ if (empty($kpCode)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 尝试从编码规则推断父节点
|
|
|
+ // M01 -> M00, S01 -> M01, R01 -> S01 等
|
|
|
+ if (preg_match('/^([A-Z]+)\d+$/', $kpCode, $matches)) {
|
|
|
+ $prefix = $matches[1];
|
|
|
+ $number = (int) substr($kpCode, strlen($prefix));
|
|
|
+
|
|
|
+ if ($number > 0) {
|
|
|
+ // 降级:R01 -> S01 (去掉一位数字)
|
|
|
+ $parentNumber = (int) (substr($kpCode, strlen($prefix), -1));
|
|
|
+ if ($parentNumber > 0) {
|
|
|
+ return $prefix . str_pad($parentNumber, strlen($kpCode) - strlen($prefix), '0', STR_PAD_LEFT);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+}
|