540 lines
16 KiB
Dart
540 lines
16 KiB
Dart
|
|
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<ECLoginStatus> ecLogin() async {
|
|||
|
|
try {
|
|||
|
|
return await RetryHandler.retry(
|
|||
|
|
operation: () async => await _performEcLogin(),
|
|||
|
|
retryIf: RetryHandler.shouldRetryOnError,
|
|||
|
|
maxAttempts: 3,
|
|||
|
|
);
|
|||
|
|
} catch (e) {
|
|||
|
|
return ECLoginStatus(failUnknownError: true);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Future<ECLoginStatus> _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'<TwfID>(.*?)</TwfID>',
|
|||
|
|
).firstMatch(responseText);
|
|||
|
|
if (twfIdMatch == null) {
|
|||
|
|
return ECLoginStatus(failNotFoundTwfid: true);
|
|||
|
|
}
|
|||
|
|
_twfId = twfIdMatch.group(1);
|
|||
|
|
|
|||
|
|
// 3. 提取RSA密钥
|
|||
|
|
final rsaKeyMatch = RegExp(
|
|||
|
|
r'<RSA_ENCRYPT_KEY>(.*?)</RSA_ENCRYPT_KEY>',
|
|||
|
|
).firstMatch(responseText);
|
|||
|
|
if (rsaKeyMatch == null) {
|
|||
|
|
return ECLoginStatus(failNotFoundRsaKey: true);
|
|||
|
|
}
|
|||
|
|
final rsaKey = rsaKeyMatch.group(1)!;
|
|||
|
|
|
|||
|
|
// 4. 提取RSA指数
|
|||
|
|
final rsaExpMatch = RegExp(
|
|||
|
|
r'<RSA_ENCRYPT_EXP>(.*?)</RSA_ENCRYPT_EXP>',
|
|||
|
|
).firstMatch(responseText);
|
|||
|
|
if (rsaExpMatch == null) {
|
|||
|
|
return ECLoginStatus(failNotFoundRsaExp: true);
|
|||
|
|
}
|
|||
|
|
final rsaExp = rsaExpMatch.group(1)!;
|
|||
|
|
|
|||
|
|
// 5. 提取CSRF代码
|
|||
|
|
final csrfMatch = RegExp(
|
|||
|
|
r'<CSRF_RAND_CODE>(.*?)</CSRF_RAND_CODE>',
|
|||
|
|
).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('<Result>1</Result>')) {
|
|||
|
|
_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<RSAPublicKey>(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<UAAPLoginStatus> uaapLogin() async {
|
|||
|
|
try {
|
|||
|
|
return await RetryHandler.retry(
|
|||
|
|
operation: () async => await _performUaapLogin(),
|
|||
|
|
retryIf: RetryHandler.shouldRetryOnError,
|
|||
|
|
maxAttempts: 3,
|
|||
|
|
);
|
|||
|
|
} catch (e) {
|
|||
|
|
return UAAPLoginStatus(failUnknownError: true);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Future<UAAPLoginStatus> _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<ECCheckStatus> 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<ECCheckStatus> checkUaapLoginStatus() async {
|
|||
|
|
return ECCheckStatus(loggedIn: _uaapLogged);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 健康检查
|
|||
|
|
Future<bool> 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<void> close() async {
|
|||
|
|
_client.close();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 获取HTTP客户端
|
|||
|
|
HTTPClient get client {
|
|||
|
|
healthCheckpoint();
|
|||
|
|
return _client;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 获取CSRF Token
|
|||
|
|
Future<String?> 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<List<Course>> 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<String, dynamic> && data['data'] is List) {
|
|||
|
|
final courseList = (data['data'] as List)
|
|||
|
|
.map((item) => _parseCourse(item))
|
|||
|
|
.where((course) => course != null)
|
|||
|
|
.cast<Course>()
|
|||
|
|
.toList();
|
|||
|
|
return courseList;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return [];
|
|||
|
|
} catch (e) {
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 解析课程数据
|
|||
|
|
Course? _parseCourse(dynamic item) {
|
|||
|
|
try {
|
|||
|
|
if (item is! Map<String, dynamic>) return null;
|
|||
|
|
|
|||
|
|
final id = item['id'] as Map<String, dynamic>?;
|
|||
|
|
final questionnaire = item['questionnaire'] as Map<String, dynamic>?;
|
|||
|
|
|
|||
|
|
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<String?> 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<EvaluationResponse> submitEvaluation(
|
|||
|
|
Map<String, String> 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<String, dynamic>) {
|
|||
|
|
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';
|
|||
|
|
}
|