import 'dart:math'; import '../models/course.dart'; import '../models/questionnaire.dart'; import '../models/evaluation_result.dart'; import 'aufe_connection.dart'; import 'questionnaire_parser.dart'; import '../utils/text_generator.dart'; /// Service for handling course evaluation operations /// Manages the evaluation process including form data building, /// option selection, and text generation class EvaluationService { final AUFEConnection _connection; final QuestionnaireParser _parser; final TextGenerator _textGenerator; final Random _random = Random(); EvaluationService({ required AUFEConnection connection, required QuestionnaireParser parser, required TextGenerator textGenerator, }) : _connection = connection, _parser = parser, _textGenerator = textGenerator; /// Prepare evaluation for a course (access page, parse questionnaire, generate answers) /// /// [course] - The course to evaluate /// [totalCourses] - Total number of courses (for count parameter) /// Returns form data map if successful, null otherwise Future?> prepareEvaluation( Course course, { int? totalCourses, }) async { try { // 1. Access evaluation page to get questionnaire HTML final html = await _connection.accessEvaluationPage( course, totalCourses: totalCourses, ); if (html == null) { return null; } // 2. Parse questionnaire final questionnaire = await _parser.parseAsync(html); // 3. Build form data final formData = _buildFormData( questionnaire, course: course, totalCourses: totalCourses, ); return formData; } catch (e, stackTrace) { print('❌ prepareEvaluation error: $e'); print('❌ Stack trace: $stackTrace'); return null; } } /// Submit evaluation with prepared form data /// /// [course] - The course being evaluated /// [formData] - Prepared form data from prepareEvaluation /// Returns [EvaluationResult] with success status and error message if failed Future submitEvaluation( Course course, Map formData, ) async { try { final submitResponse = await _connection.submitEvaluation(formData); if (submitResponse.isSuccess) { return EvaluationResult(course: course, success: true); } else { return EvaluationResult( course: course, success: false, errorMessage: submitResponse.msg, ); } } catch (e, stackTrace) { print('❌ submitEvaluation error: $e'); print('❌ Stack trace: $stackTrace'); return EvaluationResult( course: course, success: false, errorMessage: '提交评价出错: $e', ); } } /// Evaluate a single course (legacy method, combines prepare and submit with countdown) /// /// [course] - The course to evaluate /// [totalCourses] - Total number of courses (for count parameter) /// [onStatusChange] - Optional callback to report status changes during evaluation /// [onCountdown] - Optional callback to report countdown progress (seconds remaining, total seconds) /// Returns [EvaluationResult] with success status and error message if failed Future evaluateCourse( Course course, { int? totalCourses, Function(String status)? onStatusChange, Function(int remaining, int total)? onCountdown, }) async { try { // 1. Prepare evaluation onStatusChange?.call('正在访问评价页面...'); final formData = await prepareEvaluation( course, totalCourses: totalCourses, ); if (formData == null) { return EvaluationResult( course: course, success: false, errorMessage: '无法访问评价页面', ); } onStatusChange?.call('正在解析问卷结构...'); onStatusChange?.call('正在生成答案...'); // 2. Wait 140 seconds before submission (server anti-bot requirement) onStatusChange?.call('等待提交中...'); const totalSeconds = 140; for (int i = totalSeconds; i > 0; i--) { onCountdown?.call(i, totalSeconds); // Also report to status for logging final progress = (totalSeconds - i) / totalSeconds; final barLength = 20; final filledLength = (progress * barLength).round(); final bar = '█' * filledLength + '░' * (barLength - filledLength); final percent = (progress * 100).toInt(); onStatusChange?.call('等待提交 [$bar] $percent% (${i}s)'); await Future.delayed(const Duration(seconds: 1)); } // 3. Submit evaluation onStatusChange?.call('正在提交评价...'); final result = await submitEvaluation(course, formData); if (result.success) { onStatusChange?.call('提交成功'); } else { onStatusChange?.call('提交失败: ${result.errorMessage}'); } return result; } catch (e, stackTrace) { print('❌ evaluateCourse error: $e'); print('❌ Stack trace: $stackTrace'); onStatusChange?.call('评教失败'); return EvaluationResult( course: course, success: false, errorMessage: '评教过程出错: $e', ); } } /// Build form data from questionnaire /// /// Automatically selects best options for radio questions /// and generates appropriate text for text questions Map _buildFormData( Questionnaire questionnaire, { required Course course, int? totalCourses, }) { final formData = {}; // Add required metadata fields (matching the correct request format) // Use Course data as primary source, fallback to questionnaire parsed data formData['optType'] = 'submit'; formData['tokenValue'] = questionnaire.tokenValue; formData['questionnaireCode'] = course.questionnaireCode.isNotEmpty ? course.questionnaireCode : questionnaire.questionnaireCode; formData['evaluationContent'] = course.evaluationContentNumber.isNotEmpty ? course.evaluationContentNumber : questionnaire.evaluationContent; formData['evaluatedPeopleNumber'] = course.evaluatedPeopleNumber.isNotEmpty ? course.evaluatedPeopleNumber : questionnaire.evaluatedPeopleNumber; formData['count'] = totalCourses?.toString() ?? ''; // Process radio questions - select best option for each for (final question in questionnaire.radioQuestions) { final selectedOption = _selectBestOption(question.options); formData[question.key] = selectedOption.value; } // Process text questions - generate appropriate text for (final question in questionnaire.textQuestions) { final generatedText = _generateTextAnswer(question); formData[question.key] = generatedText; } print('📝 Form data keys: ${formData.keys.join(", ")}'); print('📝 Metadata fields:'); print(' optType = ${formData['optType']}'); print(' tokenValue = ${formData['tokenValue']}'); print(' questionnaireCode = ${formData['questionnaireCode']}'); print(' evaluationContent = ${formData['evaluationContent']}'); print(' evaluatedPeopleNumber = ${formData['evaluatedPeopleNumber']}'); print(' count = ${formData['count']}'); print( '📝 Question answers (${questionnaire.radioQuestions.length} radio + ${questionnaire.textQuestions.length} text):', ); formData.forEach((key, value) { if (key.startsWith('0000') || key == 'zgpj') { print(' $key = $value'); } }); return formData; } /// Select the best option from a list of radio options /// /// Strategy: Prefer weight 1.0 options (80% probability) /// Otherwise select second highest weight option (20% probability) /// /// [options] - List of available radio options /// Returns the selected [RadioOption] RadioOption _selectBestOption(List options) { if (options.isEmpty) { throw ArgumentError('Options list cannot be empty'); } // Sort options by weight in descending order final sortedOptions = List.from(options) ..sort((a, b) => b.weight.compareTo(a.weight)); // Find options with weight 1.0 final weight1Options = sortedOptions .where((opt) => opt.weight == 1.0) .toList(); // If there are weight 1.0 options if (weight1Options.isNotEmpty) { // 80% chance to select weight 1.0 option if (_random.nextDouble() < 0.8) { // If multiple weight 1.0 options, randomly select one return weight1Options[_random.nextInt(weight1Options.length)]; } } // 20% chance or no weight 1.0 options: select second highest weight if (sortedOptions.length > 1) { // Find second highest weight final highestWeight = sortedOptions[0].weight; final secondHighestOptions = sortedOptions .where((opt) => opt.weight < highestWeight) .toList(); if (secondHighestOptions.isNotEmpty) { // Get all options with second highest weight final secondHighestWeight = secondHighestOptions[0].weight; final secondHighestGroup = secondHighestOptions .where((opt) => opt.weight == secondHighestWeight) .toList(); return secondHighestGroup[_random.nextInt(secondHighestGroup.length)]; } } // Fallback: return highest weight option return sortedOptions[0]; } /// Generate text answer for a text question /// /// Uses TextGenerator to create appropriate text based on question type /// Validates the generated text meets all requirements /// /// [question] - The text question to answer /// Returns generated text string String _generateTextAnswer(TextQuestion question) { // Generate text based on question type String text = _textGenerator.generate(question.type); // Validate the generated text // If validation fails, try again (up to 3 attempts) int attempts = 0; while (!_textGenerator.validate(text) && attempts < 3) { text = _textGenerator.generate(question.type); attempts++; } return text; } /// Batch evaluate all pending courses /// /// [onProgress] - Callback function to report progress with detailed status /// Returns [BatchEvaluationResult] with statistics and individual results Future batchEvaluate({ required Function(int current, int total, Course course, String status) onProgress, }) async { final startTime = DateTime.now(); final results = []; try { // 1. Fetch all pending courses final courses = await _connection.fetchCourseList(); // Filter out already evaluated courses final pendingCourses = courses.where((c) => !c.isEvaluated).toList(); final total = pendingCourses.length; if (total == 0) { return BatchEvaluationResult( total: 0, success: 0, failed: 0, results: [], duration: DateTime.now().difference(startTime), ); } // 2. Evaluate each course for (int i = 0; i < pendingCourses.length; i++) { final course = pendingCourses[i]; // Evaluate the course with status updates final result = await evaluateCourse( course, totalCourses: total, onStatusChange: (status) { // Report progress with current course and status // Use i (not i+1) to show completed count, current course is still in progress onProgress(i, total, course, status); }, ); results.add(result); // Check if evaluation failed if (!result.success) { // Report failure onProgress(i + 1, total, course, '评教失败,任务中断'); // Stop batch evaluation on first error final successCount = results.where((r) => r.success).length; final failedCount = results.where((r) => !r.success).length; final duration = DateTime.now().difference(startTime); return BatchEvaluationResult( total: i + 1, // Only count evaluated courses success: successCount, failed: failedCount, results: results, duration: duration, ); } // Verify evaluation by checking course list onProgress(i + 1, total, course, '验证评教结果'); final updatedCourses = await _connection.fetchCourseList(); final updatedCourse = updatedCourses.firstWhere( (c) => c.id == course.id, orElse: () => course, ); if (!updatedCourse.isEvaluated) { // Evaluation not confirmed, treat as failure results[results.length - 1] = EvaluationResult( course: course, success: false, errorMessage: '评教未生效,服务器未确认', ); onProgress(i + 1, total, course, '评教未生效,任务中断'); final successCount = results.where((r) => r.success).length; final failedCount = results.where((r) => !r.success).length; final duration = DateTime.now().difference(startTime); return BatchEvaluationResult( total: i + 1, success: successCount, failed: failedCount, results: results, duration: duration, ); } // Report successful completion onProgress(i + 1, total, course, '评教完成'); // Small delay between evaluations to avoid overwhelming the server if (i < pendingCourses.length - 1) { await Future.delayed(const Duration(milliseconds: 500)); } } // 3. Calculate statistics final successCount = results.where((r) => r.success).length; final failedCount = results.where((r) => !r.success).length; final duration = DateTime.now().difference(startTime); return BatchEvaluationResult( total: total, success: successCount, failed: failedCount, results: results, duration: duration, ); } catch (e) { // If batch evaluation fails completely, return partial results final successCount = results.where((r) => r.success).length; final failedCount = results.where((r) => !r.success).length; final duration = DateTime.now().difference(startTime); return BatchEvaluationResult( total: results.length, success: successCount, failed: failedCount, results: results, duration: duration, ); } } }