yemeishu před 3 dny
rodič
revize
1d18e458f0

+ 77 - 0
app/Http/Controllers/Api/IntelligentExamController.php

@@ -79,6 +79,21 @@ class IntelligentExamController extends Controller
             'mistake_question_ids' => 'nullable|array',
             'mistake_question_ids.*' => 'string',
             'callback_url' => 'nullable|url',  // 异步完成后推送通知的URL
+            // 新增:组卷类型
+            'exam_type' => 'nullable|string|in:general,diagnostic,practice,mistake,textbook,knowledge',
+            // 新增:专项练习选项
+            'practice_options' => 'nullable|array',
+            'practice_options.weakness_threshold' => 'nullable|numeric|min:0|max:1',
+            'practice_options.intensity' => 'nullable|string|in:low,medium,high',
+            'practice_options.include_new_questions' => 'nullable|boolean',
+            'practice_options.focus_weaknesses' => 'nullable|boolean',
+            // 新增:错题选项
+            'mistake_options' => 'nullable|array',
+            'mistake_options.weakness_threshold' => 'nullable|numeric|min:0|max:1',
+            'mistake_options.review_mistakes' => 'nullable|boolean',
+            'mistake_options.intensity' => 'nullable|string|in:low,medium,high',
+            'mistake_options.include_new_questions' => 'nullable|boolean',
+            'mistake_options.focus_weaknesses' => 'nullable|boolean',
         ]);
 
         if ($validator->fails()) {
@@ -145,6 +160,9 @@ class IntelligentExamController extends Controller
                     'skills' => $data['skills'] ?? [],
                     'question_type_ratio' => $questionTypeRatio,
                     'difficulty_ratio' => $difficultyRatio,
+                    'exam_type' => $data['exam_type'] ?? 'general', // 传递组卷类型
+                    'practice_options' => $data['practice_options'] ?? null, // 传递专项练习选项
+                    'mistake_options' => $data['mistake_options'] ?? null, // 传递错题选项
                 ]);
 
                 if (empty($result['success'])) {
@@ -443,6 +461,65 @@ class IntelligentExamController extends Controller
             }
         }
 
+        // 新增:处理组卷类型,默认值为 general
+        if (!isset($payload['exam_type'])) {
+            $payload['exam_type'] = 'general';
+        }
+
+        // 新增:处理专项练习选项
+        if (isset($payload['practice_options'])) {
+            if (is_string($payload['practice_options'])) {
+                $decoded = json_decode($payload['practice_options'], true);
+                $payload['practice_options'] = is_array($decoded) ? $decoded : [];
+            } elseif (!is_array($payload['practice_options'])) {
+                $payload['practice_options'] = [];
+            }
+
+            // 设置默认值
+            $payload['practice_options'] = array_merge([
+                'weakness_threshold' => 0.7,
+                'intensity' => 'medium',
+                'include_new_questions' => true,
+                'focus_weaknesses' => true,
+            ], $payload['practice_options']);
+        } else {
+            // 如果没有提供 practice_options,创建默认值
+            $payload['practice_options'] = [
+                'weakness_threshold' => 0.7,
+                'intensity' => 'medium',
+                'include_new_questions' => true,
+                'focus_weaknesses' => true,
+            ];
+        }
+
+        // 新增:处理错题选项
+        if (isset($payload['mistake_options'])) {
+            if (is_string($payload['mistake_options'])) {
+                $decoded = json_decode($payload['mistake_options'], true);
+                $payload['mistake_options'] = is_array($decoded) ? $decoded : [];
+            } elseif (!is_array($payload['mistake_options'])) {
+                $payload['mistake_options'] = [];
+            }
+
+            // 设置默认值
+            $payload['mistake_options'] = array_merge([
+                'weakness_threshold' => 0.7,
+                'review_mistakes' => true,
+                'intensity' => 'medium',
+                'include_new_questions' => true,
+                'focus_weaknesses' => true,
+            ], $payload['mistake_options']);
+        } else {
+            // 如果没有提供 mistake_options,创建默认值
+            $payload['mistake_options'] = [
+                'weakness_threshold' => 0.7,
+                'review_mistakes' => true,
+                'intensity' => 'medium',
+                'include_new_questions' => true,
+                'focus_weaknesses' => true,
+            ];
+        }
+
         return $payload;
     }
 

+ 422 - 274
app/Services/LearningAnalyticsService.php

@@ -5,17 +5,20 @@ namespace App\Services;
 use Illuminate\Support\Facades\Http;
 use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Facades\DB;
+use App\Services\ExamTypeStrategy;
+use App\Services\QuestionExpansionService;
 
 class LearningAnalyticsService
 {
     protected string $baseUrl;
     protected int $timeout = 10;
-    protected ?QuestionBankService $questionBankService;
+    protected ?QuestionExpansionService $questionExpansionService;
 
-    public function __construct(?QuestionBankService $questionBankService = null)
-    {
+    public function __construct(
+        ?QuestionExpansionService $questionExpansionService = null
+    ) {
         $this->baseUrl = config('services.learning_analytics.url', env('LEARNING_ANALYTICS_API_BASE', 'http://localhost:5016'));
-        $this->questionBankService = $questionBankService;
+        $this->questionExpansionService = $questionExpansionService;
     }
 
     /**
@@ -453,49 +456,15 @@ class LearningAnalyticsService
      */
     public function submitOCRAnalysis(array $data): array
     {
-        try {
-            Log::info('Sending OCR results to LearningAnalytics', [
-                'student_id' => $data['student_id'] ?? 'unknown',
-                'exam_id' => $data['exam_id'] ?? 'unknown',
-                'question_count' => count($data['questions'] ?? [])
-            ]);
-
-            $response = Http::timeout(30) // 分析可能需要较长时间
-                ->post($this->baseUrl . '/api/analysis/process-answers', $data);
-
-            Log::info('LearningAnalytics Response: Submit OCR Analysis', [
-                'status' => $response->status(),
-                'body' => $response->json()
-            ]);
-
-            if ($response->successful()) {
-                Log::info('Analysis submitted successfully', [
-                    'analysis_id' => $response->json('analysis_id')
-                ]);
-                return $response->json();
-            }
-
-            Log::error('Submit OCR Analysis Error', [
-                'status' => $response->status(),
-                'response' => $response->body(),
-                'data_preview' => array_merge($data, ['questions' => count($data['questions'])])
-            ]);
-
-            return [
-                'error' => true,
-                'message' => 'Failed to submit analysis: ' . $response->body()
-            ];
-        } catch (\Exception $e) {
-            Log::error('Submit OCR Analysis Exception', [
-                'error' => $e->getMessage(),
-                'trace' => $e->getTraceAsString()
-            ]);
+        Log::warning('submitOCRAnalysis 已停用:分析项目已下线', [
+            'student_id' => $data['student_id'] ?? 'unknown',
+            'exam_id' => $data['exam_id'] ?? 'unknown',
+        ]);
 
-            return [
-                'error' => true,
-                'message' => $e->getMessage()
-            ];
-        }
+        return [
+            'success' => false,
+            'message' => 'analysis_api_disabled',
+        ];
     }
 
     /**
@@ -503,46 +472,14 @@ class LearningAnalyticsService
      */
     public function getAnalysisResult(string $analysisId): array
     {
-        try {
-            $endpoint = "/api/analysis/analysis/{$analysisId}";
-
-            Log::info('LearningAnalytics Request: Get Analysis Result', [
-                'endpoint' => $endpoint,
-                'analysis_id' => $analysisId
-            ]);
-
-            $response = Http::timeout($this->timeout)->get($this->baseUrl . $endpoint);
-
-            Log::info('LearningAnalytics Response: Get Analysis Result', [
-                'status' => $response->status(),
-                'body' => $response->json()
-            ]);
-
-            if ($response->successful()) {
-                return $response->json();
-            }
-
-            Log::error('Get Analysis Result Error', [
-                'analysis_id' => $analysisId,
-                'status' => $response->status(),
-                'response' => $response->body()
-            ]);
-
-            return [
-                'error' => true,
-                'message' => 'Failed to fetch analysis result'
-            ];
-        } catch (\Exception $e) {
-            Log::error('Get Analysis Result Exception', [
-                'analysis_id' => $analysisId,
-                'error' => $e->getMessage()
-            ]);
+        Log::warning('getAnalysisResult 已停用:分析项目已下线', [
+            'analysis_id' => $analysisId,
+        ]);
 
-            return [
-                'error' => true,
-                'message' => $e->getMessage()
-            ];
-        }
+        return [
+            'success' => false,
+            'message' => 'analysis_api_disabled',
+        ];
     }
 
     /**
@@ -1128,61 +1065,24 @@ class LearningAnalyticsService
     public function getStudentWeaknesses(string $studentId, int $limit = 10): array
     {
         try {
-            // 首先从本地MySQL(权威数据源)获取数据
-            Log::info('从MySQL权威数据源获取学生薄弱点', [
+            // 从本地MySQL数据库获取学生薄弱点
+            Log::info('从本地MySQL数据库获取学生薄弱点', [
                 'student_id' => $studentId,
-                'source' => 'mysql'
+                'limit' => $limit
             ]);
 
-            $localData = $this->getStudentWeaknessesFromMySQL($studentId, $limit);
+            $weaknesses = $this->getStudentWeaknessesFromMySQL($studentId, $limit);
 
-            if (!empty($localData)) {
-                Log::info('从MySQL权威数据源获取到薄弱点数据', [
+            if (!empty($weaknesses)) {
+                Log::info('从本地数据库获取到薄弱点数据', [
                     'student_id' => $studentId,
-                    'count' => count($localData)
+                    'count' => count($weaknesses)
                 ]);
-                return $localData;
+                return $weaknesses;
             }
 
-            // 本地无数据时,尝试从LearningAnalytics API获取(作为辅助/缓存)
-            Log::warning('MySQL中无该学生薄弱点数据,尝试从LearningAnalytics API获取', [
-                'student_id' => $studentId,
-                'api_url' => $this->baseUrl . "/api/v1/student/{$studentId}/weak-points"
-            ]);
-
-            $response = Http::timeout($this->timeout)
-                ->get($this->baseUrl . "/api/v1/student/{$studentId}/weak-points");
-
-            if ($response->successful()) {
-                $data = $response->json('data', []);
-                $weakPoints = $data['weak_points'] ?? [];
-
-                if (!empty($weakPoints)) {
-                    Log::info('从LearningAnalytics API辅助获取到薄弱点数据', [
-                        'student_id' => $studentId,
-                        'count' => count($weakPoints)
-                    ]);
-
-                    return array_map(function ($item) use ($studentId) {
-                        return [
-                            'kp_code' => $item['kp'] ?? '',
-                            'kp_name' => $item['kp'] ?? '',
-                            'mastery' => $item['mastery_level'] ?? 0,
-                            'stability' => 0.5, // 默认稳定性
-                            'weakness_level' => 1.0 - ($item['mastery_level'] ?? 0.5),
-                            'practice_count' => $item['practice_count'] ?? 0,
-                            'success_rate' => $item['success_rate'] ?? 0,
-                            'priority' => $item['priority'] ?? '中',
-                            'suggested_questions' => $item['suggested_questions'] ?? 0
-                        ];
-                    }, $weakPoints);
-                }
-            }
-
-            Log::warning('所有数据源均无该学生薄弱点数据', [
-                'student_id' => $studentId,
-                'mysql_count' => count($localData),
-                'api_status' => $response->status() ?? 'timeout'
+            Log::warning('本地数据库中无该学生薄弱点数据', [
+                'student_id' => $studentId
             ]);
 
             return [];
@@ -1228,14 +1128,14 @@ class LearningAnalyticsService
                 ]);
 
                 $weaknesses = DB::table('student_mastery as sm')
-                    ->leftJoin('knowledge_points as kp', 'sm.kp', '=', 'kp.kp')
+                    ->leftJoin('knowledge_points as kp', 'sm.kp', '=', 'kp.kp_code')
                     ->where('sm.student_id', $studentId)
                     ->where('sm.mastery', '<', 0.7) // 掌握度低于70%视为薄弱点
                     ->orderBy('sm.mastery', 'asc')
                     ->limit($limit)
                     ->select([
                         'sm.kp as kp_code',
-                        'kp.cn_name as kp_name',
+                        'kp.name as kp_name',
                         'sm.mastery',
                         'sm.attempts',
                         'sm.correct'
@@ -1318,6 +1218,42 @@ class LearningAnalyticsService
         $startTime = microtime(true);
 
         try {
+            // 新增:应用组卷类型策略
+            $examType = $params['exam_type'] ?? 'general';
+            Log::info('LearningAnalyticsService: 检查组卷策略', [
+                'exam_type' => $examType,
+                'has_question_expansion_service' => !empty($this->questionExpansionService)
+            ]);
+
+            if ($examType !== 'general') {
+                try {
+                    // 确保QuestionExpansionService可用
+                    $questionExpansionService = $this->questionExpansionService;
+                    if (!$questionExpansionService) {
+                        $questionExpansionService = app(QuestionExpansionService::class);
+                        Log::info('LearningAnalyticsService: 从容器获取QuestionExpansionService实例');
+                    }
+
+                    $strategy = new ExamTypeStrategy($questionExpansionService);
+                    $params = $strategy->buildParams($params, $examType);
+
+                    Log::info('LearningAnalyticsService: 已应用组卷策略', [
+                        'exam_type' => $examType,
+                        'enhanced_params_keys' => array_keys($params)
+                    ]);
+                } catch (Exception $e) {
+                    Log::warning('LearningAnalyticsService: 组卷策略应用失败,使用默认策略', [
+                        'exam_type' => $examType,
+                        'error' => $e->getMessage(),
+                        'trace' => $e->getTraceAsString()
+                    ]);
+                }
+            } else {
+                Log::info('LearningAnalyticsService: 跳过组卷策略', [
+                    'reason' => 'general类型不需要策略'
+                ]);
+            }
+
             $studentId = $params['student_id'] ?? null;
             $grade = $params['grade'] ?? null; // 用户选择的年级
             $totalQuestions = $params['total_questions'] ?? 20;
@@ -1341,6 +1277,7 @@ class LearningAnalyticsService
                 'total_questions' => $totalQuestions,
                 'kp_codes' => $kpCodes,
                 'skills' => $skills,
+                'exam_type' => $examType,
             ]);
 
             // 1. 如果指定了学生,获取学生的薄弱点
@@ -1369,33 +1306,67 @@ class LearningAnalyticsService
                 'skills' => $skills,
             ]);
 
-            // 2. 调用题库API获取符合条件的所有题目
-            try {
-                Log::info('开始调用 getQuestionsFromBank', [
-                    'kp_codes_count' => count($kpCodes),
-                    'skills_count' => count($skills)
+            // 2. 优先使用学生错题(如果存在)
+            $mistakeQuestionIds = $params['mistake_question_ids'] ?? [];
+            $priorityQuestions = [];
+
+            if (!empty($mistakeQuestionIds)) {
+                Log::info('LearningAnalyticsService: 优先获取学生错题', [
+                    'mistake_question_ids' => $mistakeQuestionIds,
+                    'count' => count($mistakeQuestionIds)
                 ]);
 
-                $allQuestions = $this->getQuestionsFromBank($kpCodes, $skills, $studentId, $questionTypeRatio, $difficultyRatio, 200);
+                // 获取学生错题的详细信息
+                $priorityQuestions = $this->getQuestionsFromBank([], [], $studentId, $questionTypeRatio, $difficultyRatio, 200, $mistakeQuestionIds);
 
-                Log::info('getQuestionsFromBank 调用完成', [
-                    'questions_count' => count($allQuestions),
-                    'is_array' => is_array($allQuestions),
-                    'first_question_id' => !empty($allQuestions) ? ($allQuestions[0]['id'] ?? 'N/A') : 'N/A',
-                    '耗时' => round((microtime(true) - $startTime) * 1000, 2) . 'ms',
+                Log::info('LearningAnalyticsService: 错题获取完成', [
+                    'priority_questions_count' => count($priorityQuestions),
+                    'expected_count' => count($mistakeQuestionIds)
                 ]);
 
-                Log::info('getQuestionsFromBank 返回', [
-                    'questions_count' => count($allQuestions),
-                    'time_ms' => round((microtime(true) - $startTime) * 1000, 2)
-                ]);
-            } catch (\Exception $e) {
-                Log::error('getQuestionsFromBank 调用失败', [
-                    'error' => $e->getMessage(),
-                    'trace' => $e->getTraceAsString()
-                ]);
+                // 如果获取的错题数量少于预期,记录警告
+                if (count($priorityQuestions) < count($mistakeQuestionIds)) {
+                    Log::warning('LearningAnalyticsService: 错题获取不完整', [
+                        'expected' => count($mistakeQuestionIds),
+                        'actual' => count($priorityQuestions),
+                        'missing_ids' => array_diff($mistakeQuestionIds, array_column($priorityQuestions, 'id'))
+                    ]);
+                }
+            }
 
-                throw $e;
+            // 3. 如果错题数量不足,补充其他题目
+            $allQuestions = $priorityQuestions;
+            if (count($priorityQuestions) < $totalQuestions) {
+                try {
+                    Log::info('开始调用 getQuestionsFromBank 补充题目', [
+                        'kp_codes_count' => count($kpCodes),
+                        'skills_count' => count($skills),
+                        'has_mistake_priority' => !empty($mistakeQuestionIds),
+                        'need_more' => $totalQuestions - count($priorityQuestions)
+                    ]);
+
+                    $additionalQuestions = $this->getQuestionsFromBank($kpCodes, $skills, $studentId, $questionTypeRatio, $difficultyRatio, 200);
+                    $allQuestions = array_merge($priorityQuestions, $additionalQuestions);
+
+                    Log::info('getQuestionsFromBank 调用完成', [
+                        'questions_count' => count($allQuestions),
+                        'is_array' => is_array($allQuestions),
+                        'first_question_id' => !empty($allQuestions) ? ($allQuestions[0]['id'] ?? 'N/A') : 'N/A',
+                        '耗时' => round((microtime(true) - $startTime) * 1000, 2) . 'ms',
+                    ]);
+
+                    Log::info('getQuestionsFromBank 返回', [
+                        'questions_count' => count($allQuestions),
+                        'time_ms' => round((microtime(true) - $startTime) * 1000, 2)
+                    ]);
+                } catch (\Exception $e) {
+                    Log::error('getQuestionsFromBank 调用失败', [
+                        'error' => $e->getMessage(),
+                        'trace' => $e->getTraceAsString()
+                    ]);
+
+                    throw $e;
+                }
             }
 
             if (empty($allQuestions)) {
@@ -1483,113 +1454,324 @@ class LearningAnalyticsService
     }
 
     /**
-     * 从题库获取题目 - 使用智能选题API,直接根据要求筛选
+     * 从本地题库获取题目(错题回顾优先)
+     * 支持优先获取指定题目ID的题目
      */
-    private function getQuestionsFromBank(array $kpCodes, array $skills, ?string $studentId, array $questionTypeRatio = [], array $difficultyRatio = [], int $totalNeeded = 100): array
+    private function getQuestionsFromBank(array $kpCodes, array $skills, ?string $studentId, array $questionTypeRatio = [], array $difficultyRatio = [], int $totalNeeded = 100, array $priorityQuestionIds = []): array
     {
+        $startTime = microtime(true);
+
         try {
-            // 构建筛选条件
-            $filters = [];
+            // 错题回顾:优先获取指定的学生错题
+            if (!empty($priorityQuestionIds)) {
+                Log::info('getQuestionsFromBank: 优先获取学生错题', [
+                    'priority_count' => count($priorityQuestionIds),
+                    'priority_ids' => $priorityQuestionIds
+                ]);
+
+                $priorityQuestions = $this->getLocalQuestionsByIds($priorityQuestionIds);
+
+                if (!empty($priorityQuestions)) {
+                    Log::info('getQuestionsFromBank: 优先错题获取成功', [
+                        'count' => count($priorityQuestions)
+                    ]);
+
+                    return $priorityQuestions;
+                } else {
+                    Log::warning('getQuestionsFromBank: 优先错题获取失败');
+                }
+            }
+
+            // 从本地数据库查询题目
+            Log::info('getQuestionsFromBank: 从本地数据库查询题目', [
+                'kp_codes' => $kpCodes,
+                'skills' => $skills,
+                'total_needed' => $totalNeeded,
+                'question_type_ratio' => $questionTypeRatio,
+                'difficulty_ratio' => $difficultyRatio
+            ]);
 
-            // 知识点筛选
+            $query = \App\Models\Question::query();
+
+            // 按知识点筛选
             if (!empty($kpCodes)) {
-                $filters['kp_codes'] = $kpCodes;
+                $query->whereIn('kp_code', $kpCodes);
+                Log::info('应用知识点筛选', ['kp_codes' => $kpCodes]);
             }
 
-            // 技能筛选
+            // 技能筛选(这里使用 tags 字段模拟技能筛选)
             if (!empty($skills)) {
-                $filters['skills'] = $skills;
+                $query->where(function ($q) use ($skills) {
+                    foreach ($skills as $skill) {
+                        $q->orWhere('tags', 'like', "%{$skill}%");
+                    }
+                });
+                Log::info('应用技能筛选', ['skills' => $skills]);
             }
 
-            // 题型配比
-            if (!empty($questionTypeRatio)) {
-                $filters['question_type_ratio'] = $questionTypeRatio;
-            }
+            // 筛选有解题思路的题目
+            $query->whereNotNull('solution')
+                  ->where('solution', '!=', '')
+                  ->where('solution', '!=', '[]');
 
-            // 难度配比
+            // 按难度范围筛选
             if (!empty($difficultyRatio)) {
-                $filters['difficulty_ratio'] = $difficultyRatio;
+                $difficultyRanges = $this->buildDifficultyRanges($difficultyRatio);
+                $query->where(function ($q) use ($difficultyRanges) {
+                    $first = true;
+                    foreach ($difficultyRanges as $range) {
+                        if ($first) {
+                            $q->whereBetween('difficulty', [$range['min'], $range['max']]);
+                            $first = false;
+                        } else {
+                            $q->orWhereBetween('difficulty', [$range['min'], $range['max']]);
+                        }
+                    }
+                });
+                Log::info('应用难度筛选', ['difficulty_ranges' => $difficultyRanges]);
             }
 
-            // 过滤学生做过的题目
-            if ($studentId) {
-                $filters['exclude_student_questions'] = $studentId;
-            }
+            // 限制数量并随机排序
+            $query->limit($totalNeeded * 2) // 多取一些用于后续筛选
+                  ->inRandomOrder();
 
-            // 从容器动态获取实例
-            if (!$this->questionBankService) {
-                $this->questionBankService = app(QuestionBankService::class);
-            }
+            $questions = $query->get();
 
-            // 调用智能选题API - 直接获取符合要求的题目
-            $questions = $this->questionBankService->selectQuestionsForExam($totalNeeded, $filters);
+            Log::info('getQuestionsFromBank: 查询完成', [
+                'raw_count' => $questions->count(),
+                'time_ms' => round((microtime(true) - $startTime) * 1000, 2)
+            ]);
 
-            if (!empty($questions)) {
-                Log::info('从题库获取到题目,开始过滤解题思路', [
-                    'total_from_bank' => count($questions),
-                    'filters' => $filters
-                ]);
+            // 转换为标准格式
+            $formattedQuestions = $questions->map(function ($q) {
+                return [
+                    'id' => $q->id,
+                    'question_code' => $q->question_code,
+                    'kp_code' => $q->kp_code,
+                    'question_type' => $q->question_type,
+                    'difficulty' => (float) $q->difficulty,
+                    'stem' => $q->stem,
+                    'solution' => $q->solution,
+                    'metadata' => [
+                        'has_solution' => true,
+                        'is_choice' => $q->question_type === 'choice',
+                        'is_fill' => $q->question_type === 'fill',
+                        'is_answer' => $q->question_type === 'answer',
+                        'difficulty_label' => $this->getDifficultyLabel($q->difficulty),
+                        'question_type_label' => $this->getQuestionTypeLabel($q->question_type)
+                    ]
+                ];
+            })->toArray();
 
-                $filterStartTime = microtime(true);
-                $questionsWithSolution = [];
-                $noSolutionCount = 0;
+            // 按题型和难度配比筛选
+            $selectedQuestions = $this->selectQuestionsByRatio(
+                $formattedQuestions,
+                $totalNeeded,
+                $questionTypeRatio,
+                $difficultyRatio
+            );
 
-                foreach ($questions as $index => $q) {
-                    try {
-                        $solution = $q['solution'] ?? '';
-                        // 处理 solution 可能是数组的情况
-                        if (is_array($solution)) {
-                            $solution = json_encode($solution, JSON_UNESCAPED_UNICODE);
-                        }
-                        if (!empty(trim($solution))) {
-                            $questionsWithSolution[] = $q;
-                        } else {
-                            $noSolutionCount++;
-                        }
-                    } catch (\Exception $e) {
-                        Log::error('过滤解题思路时出错', [
-                            'question_index' => $index,
-                            'error' => $e->getMessage()
-                        ]);
-                    }
-                }
+            Log::info('getQuestionsFromBank 完成', [
+                'selected_count' => count($selectedQuestions),
+                'time_ms' => round((microtime(true) - $startTime) * 1000, 2)
+            ]);
 
-                $filterTime = (microtime(true) - $filterStartTime) * 1000;
-                $hasSolutionCount = count($questionsWithSolution);
+            return $selectedQuestions;
 
-                Log::info('从题库智能获取题目', [
-                    'total_from_bank' => count($questions),
-                    'has_solution' => $hasSolutionCount,
-                    'no_solution' => $noSolutionCount,
-                    'filter_time_ms' => round($filterTime, 2),
-                    'filters' => $filters
-                ]);
+        } catch (\Exception $e) {
+            Log::error('getQuestionsFromBank 查询失败', [
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
 
-                if ($hasSolutionCount > 0) {
-                    Log::info('返回有解题思路的题目', [
-                        'count' => $hasSolutionCount
-                    ]);
-                    return array_values($questionsWithSolution);
-                } else {
-                    Log::warning('所有题目都没有解题思路,返回空数组', [
-                        'total_questions' => count($questions)
-                    ]);
-                    return [];
-                }
-            }
+            throw $e;
+        }
+    }
 
-            Log::warning('智能选题返回空结果', [
-                'filters' => $filters
+    /**
+     * 从本地数据库获取指定ID的题目
+     */
+    private function getLocalQuestionsByIds(array $questionIds): array
+    {
+        try {
+            $questions = \App\Models\Question::whereIn('id', $questionIds)
+                ->select(['id', 'question_code', 'kp_code', 'question_type', 'difficulty', 'stem'])
+                ->get();
+
+            // 转换为数组格式
+            $result = $questions->map(function ($q) {
+                return [
+                    'id' => $q->id,
+                    'question_code' => $q->question_code,
+                    'kp_code' => $q->kp_code,
+                    'question_type' => $q->question_type,
+                    'difficulty' => $q->difficulty,
+                    'stem' => $q->stem,
+                    'metadata' => [
+                        'has_solution' => true,
+                        'is_choice' => $q->question_type === 'choice',
+                        'is_fill' => $q->question_type === 'fill',
+                        'is_answer' => $q->question_type === 'answer',
+                        'difficulty_label' => $this->getDifficultyLabel($q->difficulty),
+                        'question_type_label' => $this->getQuestionTypeLabel($q->question_type)
+                    ]
+                ];
+            })->toArray();
+
+            Log::info('getLocalQuestionsByIds 获取成功', [
+                'count' => count($result),
+                'question_ids' => $questionIds
             ]);
 
-            return [];
+            return $result;
+
         } catch (\Exception $e) {
-            Log::error('智能选题异常', [
+            Log::error('getLocalQuestionsByIds 获取失败', [
+                'question_ids' => $questionIds,
                 'error' => $e->getMessage()
             ]);
+
+            return [];
+        }
+    }
+
+    /**
+     * 获取难度标签
+     */
+    private function getDifficultyLabel(float $difficulty): string
+    {
+        return match (true) {
+            $difficulty < 0.4 => '基础',
+            $difficulty < 0.7 => '中等',
+            default => '拔高'
+        };
+    }
+
+    /**
+     * 获取题型标签
+     */
+    private function getQuestionTypeLabel(string $questionType): string
+    {
+        return match ($questionType) {
+            'choice' => '选择题',
+            'fill' => '填空题',
+            'answer' => '解答题',
+            default => '未知题型'
+        };
+    }
+
+    /**
+     * 构建难度范围(根据配比)
+     */
+    private function buildDifficultyRanges(array $difficultyRatio): array
+    {
+        $ranges = [];
+
+        if (isset($difficultyRatio['基础'])) {
+            $ranges[] = ['min' => 0.0, 'max' => 0.4, 'label' => '基础'];
+        }
+        if (isset($difficultyRatio['中等'])) {
+            $ranges[] = ['min' => 0.4, 'max' => 0.7, 'label' => '中等'];
+        }
+        if (isset($difficultyRatio['拔高'])) {
+            $ranges[] = ['min' => 0.7, 'max' => 1.0, 'label' => '拔高'];
+        }
+
+        return $ranges;
+    }
+
+    /**
+     * 根据题型和难度配比选择题目
+     */
+    private function selectQuestionsByRatio(
+        array $questions,
+        int $totalNeeded,
+        array $questionTypeRatio = [],
+        array $difficultyRatio = []
+    ): array {
+        if (empty($questions)) {
+            return [];
         }
 
-        return [];
+        // 如果没有配比要求,直接返回
+        if (empty($questionTypeRatio) && empty($difficultyRatio)) {
+            return array_slice($questions, 0, $totalNeeded);
+        }
+
+        $selected = [];
+        $usedIndices = [];
+
+        // 按题型配比选择
+        if (!empty($questionTypeRatio)) {
+            foreach ($questionTypeRatio as $type => $ratio) {
+                $count = (int) round(($ratio / 100) * $totalNeeded);
+                if ($count <= 0) continue;
+
+                $typeQuestions = [];
+                foreach ($questions as $idx => $q) {
+                    if (in_array($idx, $usedIndices)) continue;
+
+                    $qType = $q['question_type'] ?? '';
+                    $label = $this->getQuestionTypeLabel($qType);
+
+                    if ($label === $type) {
+                        $typeQuestions[] = ['idx' => $idx, 'question' => $q];
+                    }
+                }
+
+                // 随机选择
+                shuffle($typeQuestions);
+                $selectedCount = min($count, count($typeQuestions));
+
+                for ($i = 0; $i < $selectedCount; $i++) {
+                    $selected[] = $typeQuestions[$i]['question'];
+                    $usedIndices[] = $typeQuestions[$i]['idx'];
+                }
+            }
+        }
+
+        // 如果还有空缺,按难度配比补充
+        $remaining = $totalNeeded - count($selected);
+        if ($remaining > 0 && !empty($difficultyRatio)) {
+            $difficultyRanges = $this->buildDifficultyRanges($difficultyRatio);
+
+            foreach ($difficultyRanges as $range) {
+                if ($remaining <= 0) break;
+
+                $diffQuestions = [];
+                foreach ($questions as $idx => $q) {
+                    if (in_array($idx, $usedIndices)) continue;
+
+                    $difficulty = (float) ($q['difficulty'] ?? 0);
+                    if ($difficulty >= $range['min'] && $difficulty < $range['max']) {
+                        $diffQuestions[] = ['idx' => $idx, 'question' => $q];
+                    }
+                }
+
+                // 随机选择
+                shuffle($diffQuestions);
+                $count = min($remaining, count($diffQuestions));
+
+                for ($i = 0; $i < $count; $i++) {
+                    $selected[] = $diffQuestions[$i]['question'];
+                    $usedIndices[] = $diffQuestions[$i]['idx'];
+                    $remaining--;
+                }
+            }
+        }
+
+        // 如果还有空缺,补充剩余题目
+        if (count($selected) < $totalNeeded) {
+            foreach ($questions as $idx => $q) {
+                if (count($selected) >= $totalNeeded) break;
+                if (!in_array($idx, $usedIndices)) {
+                    $selected[] = $q;
+                    $usedIndices[] = $idx;
+                }
+            }
+        }
+
+        return array_slice($selected, 0, $totalNeeded);
     }
 
     /**
@@ -2133,48 +2315,14 @@ class LearningAnalyticsService
      */
     public function analyzeStudentAnswers(array $data): array
     {
-        try {
-            $response = Http::timeout($this->timeout)
-                ->post($this->baseUrl . '/api/v1/analysis/submit-answers', [
-                    'paper_id' => $data['paper_id'],
-                    'student_id' => $data['student_id'],
-                    'answers' => $data['answers'],
-                    'submit_time' => $data['submit_time'] ?? now()->toISOString(),
-                ]);
-
-            if ($response->successful()) {
-                Log::info('Student answers analyzed successfully', [
-                    'student_id' => $data['student_id'],
-                    'paper_id' => $data['paper_id'],
-                    'answer_count' => count($data['answers'])
-                ]);
-
-                return [
-                    'success' => true,
-                    'data' => $response->json()
-                ];
-            }
-
-            Log::error('Analyze Student Answers Error', [
-                'data' => $data,
-                'status' => $response->status(),
-                'response' => $response->body()
-            ]);
-
-            return [
-                'success' => false,
-                'message' => '分析失败:' . $response->body()
-            ];
-        } catch (\Exception $e) {
-            Log::error('Analyze Student Answers Exception', [
-                'error' => $e->getMessage(),
-                'data' => $data
-            ]);
+        Log::warning('analyzeStudentAnswers 已停用:分析项目已下线', [
+            'student_id' => $data['student_id'] ?? null,
+            'paper_id' => $data['paper_id'] ?? null,
+        ]);
 
-            return [
-                'success' => false,
-                'message' => $e->getMessage()
-            ];
-        }
+        return [
+            'success' => false,
+            'message' => 'analysis_api_disabled',
+        ];
     }
 }