fix: import unpublished exam rooms and add opt-in school HTML feedback

This commit is contained in:
Codex
2026-10-16 16:38:00 +08:00
parent defe7fd50e
commit 437ed14dbe
31 changed files with 969 additions and 39 deletions
@@ -1,3 +1,13 @@
class UndergraduateActivityException extends FormatException {
const UndergraduateActivityException(
this.code,
String message,
this.activityNumber,
) : super(message);
final String code;
final int activityNumber;
}
class UndergraduateActivity {
UndergraduateActivity(
this.teacher,
@@ -30,8 +40,26 @@ 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) {
throw const FormatException('教师、教室或课程序号缺失');
if (suffix == null) {
throw UndergraduateActivityException(
'course_sequence_invalid',
'课程序号格式无法识别,请附带网页反馈',
i + 1,
);
}
if (args[1].trim().isEmpty) {
throw UndergraduateActivityException(
'teacher_missing',
'课程活动未提供教师信息,请附带网页反馈',
i + 1,
);
}
if (args[5].trim().isEmpty) {
throw UndergraduateActivityException(
'room_missing',
'课程活动未提供教室信息,请附带网页反馈',
i + 1,
);
}
final bitmap = args[6];
if (!RegExp(r'^0[01]+$').hasMatch(bitmap) ||
@@ -102,11 +102,26 @@ class UndergraduateExamAdapter {
final explicitEmpty = RegExp(r'暂无考试|无考试安排|没有考试|暂无记录|没有符合条件的记录|没有数据|无数据')
.hasMatch(table.text);
for (final row in table.querySelectorAll('tr').skip(1)) {
final cells = row.children
.map((c) => c.text.replaceAll(RegExp(r'\s+'), ' ').trim())
.toList();
if (explicitEmpty && row.children.length == 1) continue;
final cells = <String>[];
for (final cell in row.children) {
final value = cell.text.replaceAll(RegExp(r'\s+'), ' ').trim();
final span = int.tryParse(cell.attributes['colspan'] ?? '1');
if ((cell.attributes['rowspan'] ?? '1') != '1') {
throw const FormatException('考试记录行合并结构无法识别');
}
if (span == 1) {
cells.add(value);
} else if (span == 2 &&
cells.length == 4 &&
RegExp(r'^[\[【]?考试情况尚未发布[\]】]?$').hasMatch(value)) {
// EAMS merges the room and seat columns until it publishes them.
cells.addAll([value, '']);
} else {
throw const FormatException('考试记录列合并结构无法识别');
}
}
if (cells.length != 9) {
if (explicitEmpty && cells.length == 1) continue;
throw const FormatException('考试记录字段不完整');
}
final rowType = _type(cells[7]);
@@ -8,6 +8,8 @@ import '../../../data/json_schedule_import.dart';
import '../../../domain/exam_schedule.dart';
import '../../../domain/undergraduate_exam_query.dart';
import '../../../services/diagnostic_log.dart';
import '../../../services/school_page_diagnostic.dart';
import 'activity_data.dart';
import 'navigation_policy.dart';
import 'page_probe.dart';
import 'timetable_adapter.dart';
@@ -206,6 +208,7 @@ class UndergraduateSession extends ChangeNotifier {
),
);
Future<void> openTimetable() async {
SchoolPageDiagnostic.instance.clear();
DiagnosticLog.instance.add(
'login_opened',
fields: {'school': 'undergraduate', 'phase': 'timetable'},
@@ -222,6 +225,7 @@ class UndergraduateSession extends ChangeNotifier {
}
Future<void> openExams(UndergraduateExamQuery value) async {
SchoolPageDiagnostic.instance.clear();
DiagnosticLog.instance.add(
'login_opened',
fields: {
@@ -434,6 +438,10 @@ class UndergraduateSession extends ChangeNotifier {
return;
}
if (pages.length != 1) throw const FormatException('排考结果页面不明确');
SchoolPageDiagnostic.instance.remember(
pages.single.html,
pages.single.url,
);
final part = UndergraduateExamAdapter.extract(
pages.single.html,
expectedTerm: query!.term,
@@ -519,6 +527,10 @@ class UndergraduateSession extends ChangeNotifier {
if (pages.length != 1) {
throw const FormatException('请先在学校网页打开完整课表,再点击导入。');
}
SchoolPageDiagnostic.instance.remember(
pages.single.html,
pages.single.url,
);
final draft = UndergraduateTimetableAdapter.extract(
pages.single.html,
pages.single.url,
@@ -531,6 +543,18 @@ class UndergraduateSession extends ChangeNotifier {
_emit();
} catch (error) {
if (_valid(g) && d == _document) {
DiagnosticLog.instance.add(
'school_read_failed',
fields: {
'school': 'undergraduate',
'phase': 'timetable',
'code': error is UndergraduateActivityException
? error.code
: error.runtimeType.toString(),
if (error is UndergraduateActivityException)
'count': error.activityNumber,
},
);
message = '导入未完成:$error';
_emit();
}
@@ -554,6 +578,7 @@ class UndergraduateSession extends ChangeNotifier {
}
Future<void> reauthenticate() async {
SchoolPageDiagnostic.instance.clear();
await _logoutFuture;
if (_disposed) return;
_reset();
@@ -566,6 +591,7 @@ class UndergraduateSession extends ChangeNotifier {
}
void invalidateAuthentication() {
SchoolPageDiagnostic.instance.clear();
_reset();
_active = false;
hasOpened = false;
@@ -601,6 +627,7 @@ class UndergraduateSession extends ChangeNotifier {
@override
void dispose() {
SchoolPageDiagnostic.instance.clear();
_disposed = true;
_active = false;
_generation++;