feat: release undergraduate integration 0.7.0
This commit is contained in:
@@ -26,13 +26,15 @@ List<UndergraduateActivity> parseActivityData(
|
||||
final m = matches[i], args = _strings(m[1]!);
|
||||
if (args.length != 7) throw const FormatException('课程活动字段不完整');
|
||||
final suffix = RegExp(r'\(([A-Za-z0-9_.-]+)\)$').firstMatch(args[3]);
|
||||
if (suffix == null || args[1].trim().isEmpty || args[5].trim().isEmpty)
|
||||
if (suffix == null || args[1].trim().isEmpty || args[5].trim().isEmpty) {
|
||||
throw const FormatException('教师、教室或课程序号缺失');
|
||||
}
|
||||
final bitmap = args[6];
|
||||
if (!RegExp(r'^0[01]+$').hasMatch(bitmap) ||
|
||||
bitmap.length > 64 ||
|
||||
(bitmap.length > 31 && bitmap.substring(31).contains('1')))
|
||||
(bitmap.length > 31 && bitmap.substring(31).contains('1'))) {
|
||||
throw const FormatException('上课周次超出1–30周');
|
||||
}
|
||||
final weeks = {
|
||||
for (var w = 1; w < bitmap.length && w <= 30; w++)
|
||||
if (bitmap[w] == '1') w,
|
||||
@@ -49,15 +51,17 @@ List<UndergraduateActivity> parseActivityData(
|
||||
final appends = RegExp(
|
||||
r'table\d+\.activities\[index\]\s*\[[^;]+?\]\s*=\s*activity\s*;',
|
||||
).allMatches(body).length;
|
||||
if (assignments.isEmpty || assignments.length != appends)
|
||||
if (assignments.isEmpty || assignments.length != appends) {
|
||||
throw const FormatException('课程节次赋值不完整');
|
||||
}
|
||||
for (final assignment in assignments) {
|
||||
final expression = RegExp(r'^\s*(\d+)\s*\*\s*unitCount\s*\+\s*(\d+)\s*$')
|
||||
.firstMatch(assignment[1]!);
|
||||
if (expression == null) throw const FormatException('不支持的节次表达式');
|
||||
final day = int.parse(expression[1]!), period = int.parse(expression[2]!);
|
||||
if (day > 6 || period >= unitCount)
|
||||
if (day > 6 || period >= unitCount) {
|
||||
throw const FormatException('课程星期或节次无效');
|
||||
}
|
||||
indices.add(day * unitCount + period);
|
||||
}
|
||||
result.add(
|
||||
@@ -113,8 +117,9 @@ List<String> _strings(String input) {
|
||||
final count = c == 'u' ? 4 : 2;
|
||||
if (p + count > input.length) throw const FormatException('字符串转义无效');
|
||||
final hex = input.substring(p, p + count);
|
||||
if (!RegExp(r'^[0-9a-fA-F]+$').hasMatch(hex))
|
||||
if (!RegExp(r'^[0-9a-fA-F]+$').hasMatch(hex)) {
|
||||
throw const FormatException('字符串转义无效');
|
||||
}
|
||||
b.writeCharCode(int.parse(hex, radix: 16));
|
||||
p += count;
|
||||
} else if (escapes.containsKey(c)) {
|
||||
|
||||
@@ -98,6 +98,7 @@ class _UndergraduateAuthPageState extends State<UndergraduateAuthPage> {
|
||||
}
|
||||
if (!current()) return;
|
||||
if (saved && s.saved(generation: generation)) {
|
||||
if (!mounted) return;
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
if (route?.isCurrent == true) Navigator.pop(context);
|
||||
messenger.showSnackBar(
|
||||
|
||||
@@ -26,11 +26,13 @@ class UndergraduateExamAdapter {
|
||||
UndergraduateExamType? expectedType,
|
||||
}) {
|
||||
try {
|
||||
if (utf8.encode(content).length > 2 * 1024 * 1024)
|
||||
if (utf8.encode(content).length > 2 * 1024 * 1024) {
|
||||
throw const FormatException('考试页面超过2MB');
|
||||
}
|
||||
final doc = html.parse(content);
|
||||
if (doc.querySelector('input[type="password"]') != null)
|
||||
if (doc.querySelector('input[type="password"]') != null) {
|
||||
throw const FormatException('请先完成学校登录');
|
||||
}
|
||||
final form = doc.querySelector('#stdExamTableForm');
|
||||
final scope = form ?? doc;
|
||||
final termValues = scope
|
||||
@@ -58,22 +60,26 @@ class UndergraduateExamAdapter {
|
||||
final query = Uri.splitQueryString(
|
||||
m[1]!.replaceFirst(RegExp(r'^&'), ''),
|
||||
);
|
||||
if (query['semester.id'] != null)
|
||||
if (query['semester.id'] != null) {
|
||||
termValues.add(query['semester.id']!);
|
||||
if (query['examType.id'] != null)
|
||||
}
|
||||
if (query['examType.id'] != null) {
|
||||
typeValues.add(query['examType.id']!);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (termValues.length != 1 || typeValues.length != 1)
|
||||
if (termValues.length != 1 || typeValues.length != 1) {
|
||||
throw const FormatException('排考结果学期或类型不明确');
|
||||
}
|
||||
final term = SchoolTerm(
|
||||
int.parse(termValues.single),
|
||||
school: SupportedSchool.uestcUndergraduate,
|
||||
);
|
||||
final type = UndergraduateExamType.fromId(int.parse(typeValues.single));
|
||||
if (expectedTerm != null && expectedTerm != term ||
|
||||
expectedType != null && expectedType != type)
|
||||
expectedType != null && expectedType != type) {
|
||||
throw const FormatException('排考结果与所选学期或类型不一致');
|
||||
}
|
||||
Element? table;
|
||||
for (final candidate in doc.querySelectorAll('table')) {
|
||||
final cells = candidate
|
||||
@@ -116,12 +122,14 @@ class UndergraduateExamAdapter {
|
||||
examTypeId: rowType,
|
||||
notes: cells[8],
|
||||
);
|
||||
if (record.code.isEmpty || record.name.isEmpty)
|
||||
if (record.code.isEmpty || record.name.isEmpty) {
|
||||
throw const FormatException('考试课程缺失');
|
||||
}
|
||||
records.putIfAbsent(record.undergraduateKey, () => record);
|
||||
}
|
||||
if (records.isEmpty && !explicitEmpty)
|
||||
if (records.isEmpty && !explicitEmpty) {
|
||||
throw const FormatException('尚未识别到明确的排考结果');
|
||||
}
|
||||
return ExamSchedule.fromJson(
|
||||
ExamSchedule(
|
||||
semesterLabel: term.label,
|
||||
@@ -158,14 +166,16 @@ class UndergraduateExamAdapter {
|
||||
part.termId != '${query.term.id}' ||
|
||||
part.requestedExamTypes.length != 1 ||
|
||||
!expected.contains(part.requestedExamTypes.single) ||
|
||||
!seen.add(part.requestedExamTypes.single))
|
||||
!seen.add(part.requestedExamTypes.single)) {
|
||||
throw const FormatException('考试查询批次不一致');
|
||||
}
|
||||
for (final record in part.records) {
|
||||
records.putIfAbsent(record.undergraduateKey, () => record);
|
||||
}
|
||||
}
|
||||
if (seen.length != expected.length || records.length > 500)
|
||||
if (seen.length != expected.length || records.length > 500) {
|
||||
throw const FormatException('考试未全部读取完成或记录超限');
|
||||
}
|
||||
return ExamSchedule.fromJson(
|
||||
ExamSchedule(
|
||||
semesterLabel: query.term.label,
|
||||
|
||||
@@ -82,32 +82,37 @@ class UndergraduatePageEvidence {
|
||||
!UndergraduateNavigationPolicy.canNavigate(
|
||||
current,
|
||||
isMainFrame: true,
|
||||
))
|
||||
)) {
|
||||
throw const FormatException('页面证据无效');
|
||||
}
|
||||
final pages = value['pages'];
|
||||
if (pages is! List || pages.length > 32)
|
||||
if (pages is! List || pages.length > 32) {
|
||||
throw const FormatException('页面片段过多');
|
||||
}
|
||||
var bytes = 0;
|
||||
final result = <UndergraduateFragment>[];
|
||||
for (final page in pages) {
|
||||
if (page is! Map ||
|
||||
page['url'] is! String ||
|
||||
page['html'] is! String ||
|
||||
!const ['timetable', 'exams'].contains(page['kind']))
|
||||
!const ['timetable', 'exams'].contains(page['kind'])) {
|
||||
throw const FormatException('页面片段无效');
|
||||
}
|
||||
final uri = Uri.parse(page['url'] as String),
|
||||
body = page['html'] as String;
|
||||
bytes += utf8.encode(body).length;
|
||||
if (bytes > 2 * 1024 * 1024 ||
|
||||
!UndergraduateNavigationPolicy.isDataPage(current) ||
|
||||
!UndergraduateNavigationPolicy.isDataPage(uri) ||
|
||||
current.origin != uri.origin)
|
||||
current.origin != uri.origin) {
|
||||
throw const FormatException('页面来源或大小无效');
|
||||
}
|
||||
result.add(UndergraduateFragment(page['kind'] as String, uri, body));
|
||||
}
|
||||
if (!UndergraduateNavigationPolicy.isDataPage(current) &&
|
||||
(value['authenticated'] == true || result.isNotEmpty))
|
||||
(value['authenticated'] == true || result.isNotEmpty)) {
|
||||
throw const FormatException('认证页面不能作为数据来源');
|
||||
}
|
||||
return UndergraduatePageEvidence(
|
||||
value['authenticated'] == true,
|
||||
List.unmodifiable(result),
|
||||
|
||||
@@ -102,8 +102,9 @@ class UndergraduateSession extends ChangeNotifier {
|
||||
return NavigationDecision.prevent;
|
||||
}
|
||||
if (request.isMainFrame &&
|
||||
UndergraduateNavigationPolicy.isLogout(uri))
|
||||
UndergraduateNavigationPolicy.isLogout(uri)) {
|
||||
invalidateAuthentication();
|
||||
}
|
||||
return NavigationDecision.navigate;
|
||||
},
|
||||
onPageStarted: (_) {
|
||||
@@ -123,8 +124,9 @@ class UndergraduateSession extends ChangeNotifier {
|
||||
final g = _generation,
|
||||
d = _document,
|
||||
current = await web.currentUrl();
|
||||
if (_valid(g) && d == _document && error.url == current)
|
||||
if (_valid(g) && d == _document && error.url == current) {
|
||||
_failure('学校页面加载未完成,请检查网络后重试。');
|
||||
}
|
||||
},
|
||||
onHttpError: (error) async {
|
||||
final g = _generation,
|
||||
@@ -132,8 +134,9 @@ class UndergraduateSession extends ChangeNotifier {
|
||||
current = await web.currentUrl();
|
||||
if (_valid(g) &&
|
||||
d == _document &&
|
||||
error.request?.uri.toString() == current)
|
||||
error.request?.uri.toString() == current) {
|
||||
_failure('学校请求未完成,请稍后重试。');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -184,7 +187,7 @@ class UndergraduateSession extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
_active = true;
|
||||
if (hasOpened && !_complete && !_failed) {
|
||||
if (hasOpened && !_loading && !_complete && !_failed) {
|
||||
unawaited(
|
||||
web.currentUrl().then((url) {
|
||||
if (url != null && _active) _loaded(url);
|
||||
@@ -216,8 +219,9 @@ class UndergraduateSession extends ChangeNotifier {
|
||||
void _schedulePoll(int g, int document, String url) {
|
||||
_poll?.cancel();
|
||||
_poll = Timer(const Duration(milliseconds: 450), () {
|
||||
if (_valid(g) && _document == document && !_failed && !_complete)
|
||||
if (_valid(g) && _document == document && !_failed && !_complete) {
|
||||
unawaited(_loaded(url));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -237,8 +241,9 @@ class UndergraduateSession extends ChangeNotifier {
|
||||
if (!_valid(g) || d != _document || current != url) return;
|
||||
final uri = Uri.tryParse(url);
|
||||
if (uri == null ||
|
||||
!UndergraduateNavigationPolicy.canNavigate(uri, isMainFrame: true))
|
||||
!UndergraduateNavigationPolicy.canNavigate(uri, isMainFrame: true)) {
|
||||
return;
|
||||
}
|
||||
final status = await _evidence(uri, false);
|
||||
if (!_valid(g) || d != _document) return;
|
||||
_loading = false;
|
||||
@@ -319,8 +324,9 @@ class UndergraduateSession extends ChangeNotifier {
|
||||
final evidence = await _evidence(current, true);
|
||||
if (!_valid(g) || d != _document) return;
|
||||
final pages = evidence.pages.where((p) => p.kind == 'timetable').toList();
|
||||
if (pages.length != 1)
|
||||
if (pages.length != 1) {
|
||||
throw const FormatException('请先在学校网页打开完整课表,再点击导入。');
|
||||
}
|
||||
final draft = UndergraduateTimetableAdapter.extract(
|
||||
pages.single.html,
|
||||
pages.single.url,
|
||||
@@ -345,8 +351,9 @@ class UndergraduateSession extends ChangeNotifier {
|
||||
}
|
||||
|
||||
bool saved({required int generation}) {
|
||||
if (!_valid(generation) || pendingDraft == null && pendingExams == null)
|
||||
if (!_valid(generation) || pendingDraft == null && pendingExams == null) {
|
||||
return false;
|
||||
}
|
||||
pendingDraft = null;
|
||||
pendingExams = null;
|
||||
message = '已导入';
|
||||
|
||||
@@ -21,14 +21,16 @@ class UndergraduateTimetableAdapter {
|
||||
uri.userInfo.isNotEmpty ||
|
||||
!uri.path.startsWith('/eams/') ||
|
||||
source == CourseSource.demo ||
|
||||
utf8.encode(content).length > 2 * 1024 * 1024)
|
||||
utf8.encode(content).length > 2 * 1024 * 1024) {
|
||||
throw const FormatException('不是本科课表页面');
|
||||
}
|
||||
final doc = html.parse(content),
|
||||
table = html
|
||||
.parse(content)
|
||||
.querySelector('#manualArrangeCourseTable');
|
||||
if (table == null || doc.querySelector('input[type="password"]') != null)
|
||||
if (table == null || doc.querySelector('input[type="password"]') != null) {
|
||||
throw const FormatException('请先在学校网页打开课表');
|
||||
}
|
||||
final termIds = doc
|
||||
.querySelectorAll('input[name="semester.id"]')
|
||||
.map((e) => e.attributes['value'])
|
||||
@@ -51,8 +53,9 @@ class UndergraduateTimetableAdapter {
|
||||
: <UndergraduateActivity>[];
|
||||
final explicitEmpty =
|
||||
activities.isEmpty && RegExp(r'暂无课程|无课程安排|没有课程').hasMatch(table.text);
|
||||
if (activities.isEmpty && !explicitEmpty)
|
||||
if (activities.isEmpty && !explicitEmpty) {
|
||||
throw const FormatException('课表尚未加载完整');
|
||||
}
|
||||
final roster = <String, ({String code, String notes})>{};
|
||||
for (final grid in doc.querySelectorAll('table')) {
|
||||
final rows = grid.querySelectorAll('tr');
|
||||
@@ -64,8 +67,9 @@ class UndergraduateTimetableAdapter {
|
||||
note = headers.indexOf('选课备注');
|
||||
for (final row in rows.skip(1)) {
|
||||
final cells = row.children;
|
||||
if (cells.length != headers.length)
|
||||
if (cells.length != headers.length) {
|
||||
throw const FormatException('课程名单不完整');
|
||||
}
|
||||
roster[cells[seq].text.trim()] = (
|
||||
code: cells[code].text.trim(),
|
||||
notes: note >= 0 ? cells[note].text.trim() : '',
|
||||
@@ -162,8 +166,9 @@ class UndergraduateTimetableAdapter {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (courses.isEmpty && !explicitEmpty)
|
||||
if (courses.isEmpty && !explicitEmpty) {
|
||||
throw const FormatException('没有识别到有效上课安排');
|
||||
}
|
||||
return ImportDraft(
|
||||
courses: List.unmodifiable(courses),
|
||||
explicitlyEmpty: explicitEmpty,
|
||||
|
||||
Reference in New Issue
Block a user