import 'dart:convert'; import 'dart:typed_data'; import 'package:dio/dio.dart'; import 'package:crypto/crypto.dart'; import 'package:pointycastle/export.dart'; import '../models/login_status.dart'; import '../models/course.dart'; import 'http_client.dart'; import '../utils/retry_handler.dart'; /// AUFE教务系统连接类 class AUFEConnection { final String userId; final String ecPassword; final String password; late HTTPClient _client; String? _twfId; String? _token; bool _ecLogged = false; bool _uaapLogged = false; DateTime _lastCheck = DateTime.now(); // 配置常量 static const String serverUrl = 'https://vpn.aufe.edu.cn'; static const String uaapLoginUrl = 'http://uaap-aufe-edu-cn.vpn2.aufe.edu.cn:8118/cas/login?service=http%3A%2F%2Fjwcxk2.aufe.edu.cn%2Fj_spring_cas_security_check'; static const String uaapCheckUrl = 'http://jwcxk2-aufe-edu-cn.vpn2.aufe.edu.cn:8118/'; static const String ecCheckUrl = 'http://txzx-aufe-edu-cn-s.vpn2.aufe.edu.cn:8118/dzzy/list.htm'; AUFEConnection({ required this.userId, required this.ecPassword, required this.password, }); /// 初始化HTTP客户端 void startClient() { _client = HTTPClient(baseUrl: serverUrl, timeout: 30000); } /// EC系统登录(RSA加密) Future ecLogin() async { try { return await RetryHandler.retry( operation: () async => await _performEcLogin(), retryIf: RetryHandler.shouldRetryOnError, maxAttempts: 3, ); } catch (e) { return ECLoginStatus(failUnknownError: true); } } Future _performEcLogin() async { try { // 1. 获取认证参数 final response = await _client.get('/por/login_auth.csp?apiversion=1'); final responseText = response.data.toString(); // 2. 提取TwfID final twfIdMatch = RegExp( r'(.*?)', ).firstMatch(responseText); if (twfIdMatch == null) { return ECLoginStatus(failNotFoundTwfid: true); } _twfId = twfIdMatch.group(1); // 3. 提取RSA密钥 final rsaKeyMatch = RegExp( r'(.*?)', ).firstMatch(responseText); if (rsaKeyMatch == null) { return ECLoginStatus(failNotFoundRsaKey: true); } final rsaKey = rsaKeyMatch.group(1)!; // 4. 提取RSA指数 final rsaExpMatch = RegExp( r'(.*?)', ).firstMatch(responseText); if (rsaExpMatch == null) { return ECLoginStatus(failNotFoundRsaExp: true); } final rsaExp = rsaExpMatch.group(1)!; // 5. 提取CSRF代码 final csrfMatch = RegExp( r'(.*?)', ).firstMatch(responseText); if (csrfMatch == null) { return ECLoginStatus(failNotFoundCsrfCode: true); } final csrfCode = csrfMatch.group(1)!; // 6. RSA加密密码 final passwordToEncrypt = '${ecPassword}_$csrfCode'; final encryptedPassword = _rsaEncrypt(passwordToEncrypt, rsaKey, rsaExp); // 7. 执行登录 final loginResponse = await _client.post( '/por/login_psw.csp?anti_replay=1&encrypt=1&type=cs', data: { 'svpn_rand_code': '', 'mitm': '', 'svpn_req_randcode': csrfCode, 'svpn_name': userId, 'svpn_password': encryptedPassword, }, options: Options( contentType: Headers.formUrlEncodedContentType, headers: {'Cookie': 'TWFID=$_twfId'}, ), ); final loginResponseText = loginResponse.data.toString(); // 8. 检查登录结果 if (loginResponseText.contains('1')) { _client.setCookie('TWFID', _twfId!); _ecLogged = true; return ECLoginStatus(success: true); } else if (loginResponseText.contains('Invalid username or password!')) { return ECLoginStatus(failInvalidCredentials: true); } else if (loginResponseText.contains('[CDATA[maybe attacked]]') || loginResponseText.contains('CAPTCHA required')) { return ECLoginStatus(failMaybeAttacked: true); } else { return ECLoginStatus(failUnknownError: true); } } on DioException catch (e) { return ECLoginStatus(failNetworkError: true); } catch (e) { return ECLoginStatus(failUnknownError: true); } } /// RSA加密 String _rsaEncrypt(String plaintext, String modulusHex, String exponentStr) { // 解析模数和指数 final modulus = BigInt.parse(modulusHex, radix: 16); final exponent = BigInt.parse(exponentStr); // 创建RSA公钥 final publicKey = RSAPublicKey(modulus, exponent); // 创建加密器 final encryptor = PKCS1Encoding(RSAEngine()); encryptor.init(true, PublicKeyParameter(publicKey)); // 加密 final plainBytes = utf8.encode(plaintext); final encrypted = encryptor.process(Uint8List.fromList(plainBytes)); // 转换为十六进制字符串 return encrypted.map((b) => b.toRadixString(16).padLeft(2, '0')).join(''); } /// UAAP系统登录(DES加密) Future uaapLogin() async { try { return await RetryHandler.retry( operation: () async => await _performUaapLogin(), retryIf: RetryHandler.shouldRetryOnError, maxAttempts: 3, ); } catch (e) { return UAAPLoginStatus(failUnknownError: true); } } Future _performUaapLogin() async { try { // 1. 获取登录页面 final response = await _client.get(uaapLoginUrl); final responseText = response.data.toString(); // 2. 提取lt参数 final ltMatch = RegExp( r'name="lt" value="(.*?)"', ).firstMatch(responseText); if (ltMatch == null) { return UAAPLoginStatus(failNotFoundLt: true); } final ltValue = ltMatch.group(1)!; // 3. 提取execution参数 final executionMatch = RegExp( r'name="execution" value="(.*?)"', ).firstMatch(responseText); if (executionMatch == null) { return UAAPLoginStatus(failNotFoundExecution: true); } final executionValue = executionMatch.group(1)!; // 4. DES加密密码 final encryptedPassword = _desEncrypt(password, ltValue); // 5. 提交登录表单 final loginResponse = await _client.post( uaapLoginUrl, data: { 'username': userId, 'password': encryptedPassword, 'lt': ltValue, 'execution': executionValue, '_eventId': 'submit', 'submit': 'LOGIN', }, options: Options( contentType: Headers.formUrlEncodedContentType, followRedirects: false, validateStatus: (status) => status! < 500, ), ); // 6. 检查登录结果并访问重定向URL以建立session if (loginResponse.statusCode == 302) { final location = loginResponse.headers['location']?.first ?? ''; print('🔐 UAAP redirect location: $location'); if (location.contains('ticket=')) { // 访问重定向URL以完成CAS认证并建立session print('🔐 Following redirect to establish session...'); final ticketResponse = await _client.get( location, options: Options( followRedirects: true, validateStatus: (status) => status! < 500, ), ); print('🔐 Ticket response status: ${ticketResponse.statusCode}'); _uaapLogged = true; return UAAPLoginStatus(success: true); } } final loginResponseText = loginResponse.data.toString(); if (loginResponseText.contains('Invalid username or password')) { return UAAPLoginStatus(failInvalidCredentials: true); } return UAAPLoginStatus(failUnknownError: true); } on DioException catch (e) { return UAAPLoginStatus(failNetworkError: true); } catch (e) { return UAAPLoginStatus(failUnknownError: true); } } /// DES加密(使用TripleDES ECB模式) String _desEncrypt(String plaintext, String key) { // 处理密钥 - 取前8字节 var keyBytes = utf8.encode(key); if (keyBytes.length > 8) { keyBytes = keyBytes.sublist(0, 8); } else if (keyBytes.length < 8) { // 不足8字节用0填充 keyBytes = Uint8List(8)..setRange(0, keyBytes.length, keyBytes); } // 创建DES密钥(TripleDES使用相同的8字节密钥重复3次) final desKey = KeyParameter( Uint8List(24) ..setRange(0, 8, keyBytes) ..setRange(8, 16, keyBytes) ..setRange(16, 24, keyBytes), ); // 创建加密器 final cipher = PaddedBlockCipherImpl(PKCS7Padding(), DESedeEngine()); cipher.init(true, PaddedBlockCipherParameters(desKey, null)); // 加密 final plainBytes = utf8.encode(plaintext); final encrypted = cipher.process(Uint8List.fromList(plainBytes)); // Base64编码 return base64.encode(encrypted); } /// 检查EC登录状态 Future checkEcLoginStatus() async { if (!_ecLogged) { return ECCheckStatus(loggedIn: false); } try { final response = await _client.get(ecCheckUrl); if (response.statusCode == 200) { return ECCheckStatus(loggedIn: true); } else { return ECCheckStatus(loggedIn: false); } } on DioException catch (e) { return ECCheckStatus(failNetworkError: true); } catch (e) { return ECCheckStatus(failUnknownError: true); } } /// 检查UAAP登录状态 Future checkUaapLoginStatus() async { return ECCheckStatus(loggedIn: _uaapLogged); } /// 健康检查 Future healthCheck() async { final delta = DateTime.now().difference(_lastCheck); // 5分钟未检查则视为不健康 if (delta.inSeconds > 300) { return false; } // 检查UAAP登录状态 final uaapStatus = await checkUaapLoginStatus(); if (!uaapStatus.isLoggedIn) { return false; } // 检查EC登录状态 final ecStatus = await checkEcLoginStatus(); if (!ecStatus.isLoggedIn) { return false; } return true; } /// 更新健康检查时间戳 void healthCheckpoint() { _lastCheck = DateTime.now(); } /// 关闭连接 Future close() async { _client.close(); } /// 获取HTTP客户端 HTTPClient get client { healthCheckpoint(); return _client; } /// 获取CSRF Token Future getToken() async { try { final response = await _client.get( 'http://jwcxk2-aufe-edu-cn.vpn2.aufe.edu.cn:8118/student/teachingEvaluation/evaluation/index', ); if (response.statusCode != 200) { return null; } final html = response.data.toString(); final tokenMatch = RegExp( r'id="tokenValue"[^>]*value="([^"]*)"', ).firstMatch(html); if (tokenMatch != null) { _token = tokenMatch.group(1); return _token; } return null; } catch (e) { return null; } } /// 获取待评课程列表 Future> fetchCourseList() async { try { final response = await _client.post( 'http://jwcxk2-aufe-edu-cn.vpn2.aufe.edu.cn:8118/student/teachingEvaluation/teachingEvaluation/search?sf_request_type=ajax', data: {'optType': '1', 'pagesize': '50'}, options: Options(contentType: Headers.formUrlEncodedContentType), ); if (response.statusCode != 200) { return []; } final data = response.data; if (data is Map && data['data'] is List) { final courseList = (data['data'] as List) .map((item) => _parseCourse(item)) .where((course) => course != null) .cast() .toList(); return courseList; } return []; } catch (e) { return []; } } /// 解析课程数据 Course? _parseCourse(dynamic item) { try { if (item is! Map) return null; final id = item['id'] as Map?; final questionnaire = item['questionnaire'] as Map?; return Course( id: id?['evaluatedPeople']?.toString() ?? '', name: item['evaluationContent']?.toString() ?? '', teacher: item['evaluatedPeople']?.toString() ?? '', evaluatedPeople: item['evaluatedPeople']?.toString() ?? '', evaluatedPeopleNumber: id?['evaluatedPeople']?.toString() ?? '', coureSequenceNumber: id?['coureSequenceNumber']?.toString() ?? '', evaluationContentNumber: id?['evaluationContentNumber']?.toString() ?? '', questionnaireCode: questionnaire?['questionnaireNumber']?.toString() ?? '', questionnaireName: questionnaire?['questionnaireName']?.toString() ?? '', isEvaluated: item['isEvaluated']?.toString() == '是', ); } catch (e) { return null; } } /// 访问评价页面并返回HTML内容 Future accessEvaluationPage( Course course, { int? totalCourses, }) async { try { print('📝 Accessing evaluation page for: ${course.name}'); if (_token == null) { print('📝 Getting token first...'); await getToken(); print('📝 Token: $_token'); } // count is the total number of courses in the course list final count = totalCourses?.toString() ?? '28'; print('📝 Using count: $count (total courses: $totalCourses)'); print('📝 Posting to evaluation page...'); final response = await _client.post( 'http://jwcxk2-aufe-edu-cn.vpn2.aufe.edu.cn:8118/student/teachingEvaluation/teachingEvaluation/evaluationPage', data: { 'count': count, 'evaluatedPeople': course.evaluatedPeople, 'evaluatedPeopleNumber': course.evaluatedPeopleNumber, 'questionnaireCode': course.questionnaireCode, 'questionnaireName': course.questionnaireName, 'coureSequenceNumber': course.coureSequenceNumber, 'evaluationContentNumber': course.evaluationContentNumber, 'evaluationContentContent': '', 'tokenValue': _token ?? '', }, options: Options(contentType: Headers.formUrlEncodedContentType), ); print('📝 Access evaluation page status: ${response.statusCode}'); if (response.statusCode == 200) { final html = response.data.toString(); print('📝 Got HTML content, length: ${html.length}'); return html; } return null; } catch (e, stackTrace) { print('❌ accessEvaluationPage error: $e'); print('❌ Stack trace: $stackTrace'); return null; } } /// 提交评价表单 Future submitEvaluation( Map formData, ) async { try { print('📤 Submitting evaluation with form data:'); print('📤 Form data entries: ${formData.length}'); formData.forEach((key, value) { print( ' $key = ${value.length > 50 ? value.substring(0, 50) + "..." : value}', ); }); final response = await _client.post( 'http://jwcxk2-aufe-edu-cn.vpn2.aufe.edu.cn:8118/student/teachingEvaluation/teachingEvaluation/assessment?sf_request_type=ajax', data: formData, options: Options(contentType: Headers.formUrlEncodedContentType), ); print('📤 Submit response status: ${response.statusCode}'); print('📤 Submit response data: ${response.data}'); if (response.statusCode != 200) { return EvaluationResponse( result: 'error', msg: '网络请求失败 (${response.statusCode})', ); } final data = response.data; if (data is Map) { return EvaluationResponse( result: data['result']?.toString() ?? 'error', msg: data['msg']?.toString() ?? '未知错误', ); } return EvaluationResponse(result: 'error', msg: '响应格式错误'); } catch (e) { print('❌ Submit evaluation error: $e'); return EvaluationResponse(result: 'error', msg: '请求异常: $e'); } } } /// 评价响应 class EvaluationResponse { final String result; final String msg; EvaluationResponse({required this.result, required this.msg}); bool get isSuccess => result == 'success'; }