|
|
@@ -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',
|
|
|
+ ];
|
|
|
}
|
|
|
}
|