fix: harden school imports and add optional feedback diagnostics
This commit is contained in:
@@ -21,6 +21,10 @@ List<UndergraduateActivity> parseActivityData(
|
||||
r'new\s+TaskActivity\s*\((.*?)\)\s*;',
|
||||
dotAll: true,
|
||||
).allMatches(script).toList();
|
||||
if (RegExp(r'\bnew\s+TaskActivity\s*\(').allMatches(script).length !=
|
||||
matches.length) {
|
||||
throw const FormatException('课程活动声明不完整');
|
||||
}
|
||||
final result = <UndergraduateActivity>[];
|
||||
for (var i = 0; i < matches.length; i++) {
|
||||
final m = matches[i], args = _strings(m[1]!);
|
||||
@@ -48,9 +52,17 @@ List<UndergraduateActivity> parseActivityData(
|
||||
final assignments = RegExp(r'\bindex\s*=\s*([^;]+);')
|
||||
.allMatches(body)
|
||||
.toList();
|
||||
final appends = RegExp(
|
||||
final appendPattern = RegExp(
|
||||
r'table\d+\.activities\[index\]\s*\[[^;]+?\]\s*=\s*activity\s*;',
|
||||
).allMatches(body).length;
|
||||
);
|
||||
final appends = appendPattern.allMatches(body).length;
|
||||
final unconsumed = body
|
||||
.replaceAll(appendPattern, '')
|
||||
.replaceAll(RegExp(r'\bindex\s*=\s*[^;]+;'), '');
|
||||
if (RegExp(r'\btable\d+\.activities\s*\[|\bindex\s*=')
|
||||
.hasMatch(unconsumed)) {
|
||||
throw const FormatException('存在无法识别的课程节次赋值');
|
||||
}
|
||||
if (assignments.isEmpty || assignments.length != appends) {
|
||||
throw const FormatException('课程节次赋值不完整');
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:webview_flutter/webview_flutter.dart';
|
||||
import '../../../data/json_schedule_import.dart';
|
||||
import '../../../domain/exam_schedule.dart';
|
||||
import '../../../domain/undergraduate_exam_query.dart';
|
||||
import '../../../services/diagnostic_log.dart';
|
||||
import 'navigation_policy.dart';
|
||||
import 'page_probe.dart';
|
||||
import 'timetable_adapter.dart';
|
||||
@@ -98,7 +99,9 @@ class UndergraduateSession extends ChangeNotifier {
|
||||
uri,
|
||||
isMainFrame: request.isMainFrame,
|
||||
)) {
|
||||
if (request.isMainFrame) _failure('学校跳转未完成,请重新认证。');
|
||||
if (request.isMainFrame) {
|
||||
_failure('学校跳转未完成,请重新认证。', code: 'blocked_navigation');
|
||||
}
|
||||
return NavigationDecision.prevent;
|
||||
}
|
||||
if (request.isMainFrame &&
|
||||
@@ -107,7 +110,13 @@ class UndergraduateSession extends ChangeNotifier {
|
||||
}
|
||||
return NavigationDecision.navigate;
|
||||
},
|
||||
onPageStarted: (_) {
|
||||
onPageStarted: (url) {
|
||||
DiagnosticLog.instance.navigation('undergraduate', 'started', url);
|
||||
// Within a batch, type-to-type loads retain their generation. Once
|
||||
// published, any new document invalidates the deferred save result.
|
||||
if (pendingExams != null) {
|
||||
_reset();
|
||||
}
|
||||
_document++;
|
||||
_poll?.cancel();
|
||||
_probeKey = null;
|
||||
@@ -125,7 +134,11 @@ class UndergraduateSession extends ChangeNotifier {
|
||||
d = _document,
|
||||
current = await web.currentUrl();
|
||||
if (_valid(g) && d == _document && error.url == current) {
|
||||
_failure('学校页面加载未完成,请检查网络后重试。');
|
||||
_failure(
|
||||
'学校页面加载未完成,请检查网络后重试。',
|
||||
code: 'web_resource_error',
|
||||
status: error.errorCode,
|
||||
);
|
||||
}
|
||||
},
|
||||
onHttpError: (error) async {
|
||||
@@ -135,12 +148,20 @@ class UndergraduateSession extends ChangeNotifier {
|
||||
if (_valid(g) &&
|
||||
d == _document &&
|
||||
error.request?.uri.toString() == current) {
|
||||
_failure('学校请求未完成,请稍后重试。');
|
||||
_failure(
|
||||
'学校请求未完成,请稍后重试。',
|
||||
code: 'http_error',
|
||||
status: error.response?.statusCode,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
Future<void> openTimetable() async {
|
||||
DiagnosticLog.instance.add(
|
||||
'login_opened',
|
||||
fields: {'school': 'undergraduate', 'phase': 'timetable'},
|
||||
);
|
||||
await _logoutFuture;
|
||||
if (_disposed) return;
|
||||
_reset();
|
||||
@@ -153,6 +174,14 @@ class UndergraduateSession extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> openExams(UndergraduateExamQuery value) async {
|
||||
DiagnosticLog.instance.add(
|
||||
'login_opened',
|
||||
fields: {
|
||||
'school': 'undergraduate',
|
||||
'phase': 'exams',
|
||||
'term': value.term.id,
|
||||
},
|
||||
);
|
||||
await _logoutFuture;
|
||||
if (_disposed) return;
|
||||
_reset();
|
||||
@@ -196,7 +225,16 @@ class UndergraduateSession extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void _failure(String text) {
|
||||
void _failure(String text, {String code = 'read_failure', int? status}) {
|
||||
DiagnosticLog.instance.add(
|
||||
'school_read_failed',
|
||||
fields: {
|
||||
'school': 'undergraduate',
|
||||
'code': code,
|
||||
'status': status,
|
||||
'term': query?.term.id,
|
||||
},
|
||||
);
|
||||
if (_disposed) return;
|
||||
_reset();
|
||||
_failed = true;
|
||||
@@ -233,6 +271,7 @@ class UndergraduateSession extends ChangeNotifier {
|
||||
uri,
|
||||
);
|
||||
Future<void> _loaded(String url) async {
|
||||
DiagnosticLog.instance.navigation('undergraduate', 'finished', url);
|
||||
final g = _generation, d = _document, key = '$_generation:$_document';
|
||||
if (!_valid(g) || _failed || _complete || _probeKey == key) return;
|
||||
_probeKey = key;
|
||||
@@ -245,6 +284,14 @@ class UndergraduateSession extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
final status = await _evidence(uri, false);
|
||||
DiagnosticLog.instance.add(
|
||||
'school_evidence',
|
||||
fields: {
|
||||
'school': 'undergraduate',
|
||||
'result': status.authenticated,
|
||||
'count': status.pages.length,
|
||||
},
|
||||
);
|
||||
if (!_valid(g) || d != _document) return;
|
||||
_loading = false;
|
||||
if (!status.authenticated) {
|
||||
@@ -283,6 +330,14 @@ class UndergraduateSession extends ChangeNotifier {
|
||||
_itemTimeout = null;
|
||||
if (_index == query!.orderedTypes.length) {
|
||||
pendingExams = UndergraduateExamAdapter.merge(query!, _parts);
|
||||
DiagnosticLog.instance.add(
|
||||
'school_parsed',
|
||||
fields: {
|
||||
'school': 'undergraduate',
|
||||
'phase': 'exams',
|
||||
'count': pendingExams!.records.length,
|
||||
},
|
||||
);
|
||||
_complete = true;
|
||||
_cancelTimers();
|
||||
message = '考试已全部读取,正在保存…';
|
||||
@@ -291,7 +346,9 @@ class UndergraduateSession extends ChangeNotifier {
|
||||
await _requestExam(g);
|
||||
}
|
||||
} catch (error) {
|
||||
if (_valid(g) && d == _document) _failure('$error');
|
||||
if (_valid(g) && d == _document) {
|
||||
_failure('$error', code: error.runtimeType.toString());
|
||||
}
|
||||
} finally {
|
||||
if (_probeKey == key) _probeKey = null;
|
||||
}
|
||||
@@ -300,16 +357,24 @@ class UndergraduateSession extends ChangeNotifier {
|
||||
Future<void> _requestExam(int g) async {
|
||||
if (!_valid(g) || query == null || _complete) return;
|
||||
final type = query!.orderedTypes[_index];
|
||||
DiagnosticLog.instance.add(
|
||||
'school_read_requested',
|
||||
fields: {
|
||||
'school': 'undergraduate',
|
||||
'term': query!.term.id,
|
||||
'type': type.id,
|
||||
},
|
||||
);
|
||||
_expected = query!.uriFor(type);
|
||||
_loading = true;
|
||||
message = '正在读取${type.label}(${_index + 1}/${query!.types.length})';
|
||||
_emit();
|
||||
_itemTimeout?.cancel();
|
||||
_itemTimeout = Timer(const Duration(seconds: 45), () {
|
||||
if (_valid(g)) _failure('${type.label}读取超时,请重试。');
|
||||
if (_valid(g)) _failure('${type.label}读取超时,请重试。', code: 'item_timeout');
|
||||
});
|
||||
_batchTimeout ??= Timer(const Duration(seconds: 120), () {
|
||||
if (_valid(g)) _failure('考试读取超时,请重试。');
|
||||
if (_valid(g)) _failure('考试读取超时,请重试。', code: 'batch_timeout');
|
||||
});
|
||||
await _navigate(_expected!);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user