fix: harden school imports and add optional feedback diagnostics
This commit is contained in:
@@ -6,6 +6,7 @@ import 'package:webview_flutter/webview_flutter.dart';
|
||||
import '../../data/json_schedule_import.dart';
|
||||
import '../../domain/exam_schedule.dart';
|
||||
import '../../domain/school_term.dart';
|
||||
import '../../services/diagnostic_log.dart';
|
||||
import 'school_navigation_policy.dart';
|
||||
import 'school_page_adapter.dart';
|
||||
import 'school_exam_adapter.dart';
|
||||
@@ -40,7 +41,10 @@ class SchoolSession extends ChangeNotifier {
|
||||
await web.loadRequest(upgrade);
|
||||
} catch (_) {
|
||||
if (generation == _pageGeneration) {
|
||||
_failure('学校身份认证暂时无法打开,请重试。');
|
||||
_failure(
|
||||
'学校身份认证暂时无法打开,请重试。',
|
||||
code: 'authentication_load_failed',
|
||||
);
|
||||
}
|
||||
}
|
||||
return NavigationDecision.prevent;
|
||||
@@ -55,12 +59,15 @@ class SchoolSession extends ChangeNotifier {
|
||||
'Blocked school navigation origin: ${uri?.scheme}://${uri?.host}',
|
||||
);
|
||||
}
|
||||
if (request.isMainFrame) _failure('学校跳转到了其他地址,请返回学校认证入口重试。');
|
||||
if (request.isMainFrame) {
|
||||
_failure('学校跳转到了其他地址,请返回学校认证入口重试。', code: 'blocked_navigation');
|
||||
}
|
||||
return NavigationDecision.prevent;
|
||||
}
|
||||
return NavigationDecision.navigate;
|
||||
},
|
||||
onPageStarted: (_) {
|
||||
onPageStarted: (url) {
|
||||
DiagnosticLog.instance.navigation('graduate', 'started', url);
|
||||
_poll?.cancel();
|
||||
_manualRead = false;
|
||||
_pageGeneration++;
|
||||
@@ -79,7 +86,11 @@ class SchoolSession extends ChangeNotifier {
|
||||
if (hasOpened &&
|
||||
generation == _pageGeneration &&
|
||||
error.url == current) {
|
||||
_failure('学校页面暂时未能加载,请检查网络后重试。');
|
||||
_failure(
|
||||
'学校页面暂时未能加载,请检查网络后重试。',
|
||||
code: 'web_resource_error',
|
||||
status: error.errorCode,
|
||||
);
|
||||
}
|
||||
},
|
||||
onHttpError: (error) async {
|
||||
@@ -89,7 +100,11 @@ class SchoolSession extends ChangeNotifier {
|
||||
generation == _pageGeneration &&
|
||||
error.request != null &&
|
||||
error.request!.uri == current) {
|
||||
_failure('学校请求未完成,请稍后重试。');
|
||||
_failure(
|
||||
'学校请求未完成,请稍后重试。',
|
||||
code: 'http_error',
|
||||
status: error.response?.statusCode,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -175,6 +190,14 @@ class SchoolSession extends ChangeNotifier {
|
||||
bool forceAuth = false,
|
||||
}) async {
|
||||
final serial = ++_openSerial;
|
||||
DiagnosticLog.instance.add(
|
||||
'login_opened',
|
||||
fields: {
|
||||
'school': 'graduate',
|
||||
'phase': exams ? 'exams' : 'timetable',
|
||||
'term': term?.id,
|
||||
},
|
||||
);
|
||||
final closing = _logoutFuture;
|
||||
if (closing != null) await closing;
|
||||
if (serial != _openSerial) return;
|
||||
@@ -211,7 +234,7 @@ class SchoolSession extends ChangeNotifier {
|
||||
await web.loadRequest(connected ? _target : authUrl);
|
||||
} catch (_) {
|
||||
if (serial == _openSerial && generation == _pageGeneration) {
|
||||
_failure('学校入口无法打开,请检查校园网络后重试。');
|
||||
_failure('学校入口无法打开,请检查校园网络后重试。', code: 'entry_load_failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -244,7 +267,7 @@ class SchoolSession extends ChangeNotifier {
|
||||
await web.loadRequest(_target);
|
||||
} catch (_) {
|
||||
if (serial == _openSerial && generation == _pageGeneration) {
|
||||
_failure('学校学期查询页面暂时无法打开,请重试。');
|
||||
_failure('学校学期查询页面暂时无法打开,请重试。', code: 'query_load_failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,11 +277,20 @@ class SchoolSession extends ChangeNotifier {
|
||||
_timeout?.cancel();
|
||||
_timeout = Timer(
|
||||
const Duration(seconds: 45),
|
||||
() => _failure('查找超时,请检查学校页面后重试。已有数据保持不变。'),
|
||||
() => _failure('查找超时,请检查学校页面后重试。已有数据保持不变。', code: 'read_timeout'),
|
||||
);
|
||||
}
|
||||
|
||||
void _failure(String text) {
|
||||
void _failure(String text, {String code = 'flow_failure', int? status}) {
|
||||
DiagnosticLog.instance.add(
|
||||
'school_read_failed',
|
||||
fields: {
|
||||
'school': 'graduate',
|
||||
'code': code,
|
||||
'status': status,
|
||||
'term': selectedTerm?.id,
|
||||
},
|
||||
);
|
||||
if (!hasOpened || _routeClosed) return;
|
||||
_pageGeneration++;
|
||||
_reader.invalidate();
|
||||
@@ -305,7 +337,7 @@ class SchoolSession extends ChangeNotifier {
|
||||
}
|
||||
_poll?.cancel();
|
||||
if (_pollsLeft-- <= 0) {
|
||||
if (finding) _failure('查找超时,请检查学校页面后重试。已有数据保持不变。');
|
||||
if (finding) _failure('查找超时,请检查学校页面后重试。已有数据保持不变。', code: 'read_timeout');
|
||||
return;
|
||||
}
|
||||
_poll = Timer(const Duration(milliseconds: 750), () {
|
||||
@@ -320,6 +352,9 @@ class SchoolSession extends ChangeNotifier {
|
||||
);
|
||||
|
||||
Future<void> _loaded(String url, {bool resetPolling = true}) async {
|
||||
if (resetPolling) {
|
||||
DiagnosticLog.instance.navigation('graduate', 'finished', url);
|
||||
}
|
||||
if (!hasOpened || _routeClosed) return;
|
||||
final generation = _pageGeneration;
|
||||
final uri = Uri.tryParse(url);
|
||||
@@ -360,6 +395,15 @@ class SchoolSession extends ChangeNotifier {
|
||||
}
|
||||
try {
|
||||
final evidence = await _evidence(uri);
|
||||
DiagnosticLog.instance.add(
|
||||
'school_evidence',
|
||||
fields: {
|
||||
'school': 'graduate',
|
||||
'result': evidence.authenticated,
|
||||
'count': evidence.pages.length,
|
||||
'term': selectedTerm?.id,
|
||||
},
|
||||
);
|
||||
if (!hasOpened ||
|
||||
generation != _pageGeneration ||
|
||||
await web.currentUrl() != url) {
|
||||
@@ -384,14 +428,26 @@ class SchoolSession extends ChangeNotifier {
|
||||
if (requestingExams) {
|
||||
final draft = SchoolExamAdapter.extract(page.html);
|
||||
if (generation != _pageGeneration) return;
|
||||
if (draft.termId != selectedTerm?.id.toString()) continue;
|
||||
if (draft.termId != selectedTerm?.id.toString()) {
|
||||
DiagnosticLog.instance.add(
|
||||
'term_mismatch',
|
||||
fields: {'school': 'graduate'},
|
||||
);
|
||||
continue;
|
||||
}
|
||||
pendingExams = draft;
|
||||
} else {
|
||||
// The probe validates the live document's origin. The strict parser
|
||||
// gets only its recognized fragment with the canonical table path.
|
||||
final draft = await _reader.read(_target, () async => page.html);
|
||||
if (!hasOpened || generation != _pageGeneration) return;
|
||||
if (draft.schoolTerm != selectedTerm) continue;
|
||||
if (draft.schoolTerm != selectedTerm) {
|
||||
DiagnosticLog.instance.add(
|
||||
'term_mismatch',
|
||||
fields: {'school': 'graduate'},
|
||||
);
|
||||
continue;
|
||||
}
|
||||
pendingDraft = draft;
|
||||
}
|
||||
_timeout?.cancel();
|
||||
@@ -399,9 +455,27 @@ class SchoolSession extends ChangeNotifier {
|
||||
connectionState = SchoolConnectionState.connected;
|
||||
finding = true;
|
||||
message = '读取成功,正在保存…';
|
||||
DiagnosticLog.instance.add(
|
||||
'school_parsed',
|
||||
fields: {
|
||||
'school': 'graduate',
|
||||
'phase': requestingExams ? 'exams' : 'timetable',
|
||||
'count':
|
||||
pendingDraft?.courses.length ??
|
||||
pendingExams?.records.length ??
|
||||
0,
|
||||
},
|
||||
);
|
||||
notifyListeners();
|
||||
return;
|
||||
} catch (_) {
|
||||
} catch (error) {
|
||||
DiagnosticLog.instance.add(
|
||||
'school_parse_rejected',
|
||||
fields: {
|
||||
'school': 'graduate',
|
||||
'code': error.runtimeType.toString(),
|
||||
},
|
||||
);
|
||||
if (generation != _pageGeneration) return;
|
||||
// A recognized table can still be incomplete during asynchronous load.
|
||||
}
|
||||
@@ -411,6 +485,14 @@ class SchoolSession extends ChangeNotifier {
|
||||
? null
|
||||
: _flow.onLoaded(uri, authenticated: evidence.authenticated);
|
||||
if (next != null) {
|
||||
DiagnosticLog.instance.add(
|
||||
'school_redirect_requested',
|
||||
fields: {
|
||||
'school': 'graduate',
|
||||
'phase': requestingExams ? 'exams' : 'timetable',
|
||||
'term': selectedTerm?.id,
|
||||
},
|
||||
);
|
||||
finding = true;
|
||||
_loading = true;
|
||||
message = _findingMessage;
|
||||
@@ -420,14 +502,17 @@ class SchoolSession extends ChangeNotifier {
|
||||
await web.loadRequest(next);
|
||||
} catch (_) {
|
||||
if (generation == _pageGeneration) {
|
||||
_failure('课表查询页面暂时无法打开,请重试。');
|
||||
_failure('课表查询页面暂时无法打开,请重试。', code: 'query_load_failed');
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!finding) message = '请在学校网页完成认证;进入研究生系统后会自动读取。';
|
||||
} catch (_) {
|
||||
// JavaScript and frame access can fail briefly while the page is changing.
|
||||
} catch (error) {
|
||||
DiagnosticLog.instance.add(
|
||||
'school_probe_failed',
|
||||
fields: {'school': 'graduate', 'code': error.runtimeType.toString()},
|
||||
);
|
||||
} finally {
|
||||
if (_probeGeneration == generation) _probeGeneration = null;
|
||||
_scheduleProbe(url, generation);
|
||||
|
||||
Reference in New Issue
Block a user