Skip to content

feat(schedule): add graduate timetable support - #184

Open
gfdfdxc wants to merge 3 commits into
HeZeBang:masterfrom
gfdfdxc:feat/graduate-schedule
Open

gfdfdxc wants to merge 3 commits into
HeZeBang:masterfrom
gfdfdxc:feat/graduate-schedule

Conversation

@gfdfdxc

@gfdfdxc gfdfdxc commented Nov 2, 2026 •

Copy link
Copy Markdown

改动

现有课表仅使用本科 EAMS,研究生账号无法读取研究生选课系统的排课。新增 IDS 身份分流和研究生只读适配,将学校响应转换成现有课表数据结构,复用时间网格、周切换、自定义课程和日历导出。

  • 使用 IDS 当前 UID 与学生分类区分本研,支持按账号保存的手动来源;本科继续使用原 EAMS 请求、解析和缓存逻辑。
  • 通过官网受保护入口完成研究生 CAS 认证,读取排课及可选学期信息;CASTGC 仅发送给 IDS,日志只保存结构摘要。
  • 支持多个节次方案、未排课及停开课程;使用逐日 rq 与教学周交叉验证首周日期,不能唯一确定时保留手动配置。
  • 研究生缓存校验账号归属,解绑或换号后丢弃旧响应。研究生考试暂未接入,仅避免研究生学期编号被发送到原本科考试接口,本科考试流程不变。

自动模式每次启动的首轮刷新会新增 IDS 分类查询;查询失败时仅沿用同账号已确认的分类,或由用户手动选择来源,不根据研究生系统可访问性猜测身份。

此次改动无需修改后端代码。

本地验证

验证基于上游 00c5645 加本次 11 文件补丁的独立快照:

  • flutter analyze --no-pub:无问题。
  • flutter test --no-pub:718 项通过,3 项失败;课表、身份、SSO、日期推算、缓存、本科分流及请求合并相关测试均通过。
  • 学校真实账号已完成只读课表与日期验证;提交的测试仅使用合成数据。

已知限制

研究生考试暂未接入。

Copilot AI balanced review requested due to automatic review settings November 2, 2026 20:06
@github-project-automation github-project-automation Bot moved this to Backlog in TechPie Nov 2, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@vvbbnn00

vvbbnn00 commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

在测试过程中发现了以下几个问题(脱敏后的接口响应:loadKbxx.sanitized.json):

1. 仅含文字排课的历史课程被误归入“未排课”

在脱敏数据中,results 为空,但 xkjgList 里的 11 门历史课程中有 9 门通过 PKSJDD / PKSJDDMS 提供了文字排课(2025-2026 第一学期 5/7 门,第二学期 4/4 门)。例如 测试课程01(TEST001):

{
  "KCDM": "TEST001",
  "KCMC": "测试课程01",
  "XNXQMC": "2025-2026学年 第二学期",
  "PKSJDD": "1-12周 星期二[3-4节]测试教室01;1-12周 星期四[3-4节]测试教室01;13-16周 星期二[3-4节]测试教室02;13-16周 星期四[3-4节]测试教室02"
}

目前 rows 只读取 results,课表生成也仅遍历 rows,而 unarrangedFor() 会把不在 results 中的已选课程全部归为未排课。

传入上述 JSON 后调用 tableFor() 和 unarrangedFor(),两个学期的课表网格均为 0 门,所有课程都被归入了“未排课”(第一学期 7 门、第二学期 4 门)。

建议: 当 results 缺失对应记录时,回退解析 xkjgList 中的 PKSJDD / PKSJDDMS 文字排课(需兼容跨周换教室、非连续周次及单次排课),区分“无结构化排课记录”与“确实未排课”。

2. 切回本科来源后重启,仍会加载旧的研究生课表

从研究生切回本科时,回退逻辑只更新了内存中的 _selectedSemesterId,并未持久化:

if (!_semesterInfo!.semesters.values
    .any((terms) => terms.containsValue(_selectedSemesterId))) {
  _selectedSemesterId = _semesterInfo?.defaultSemester;
}

重启并重新加载缓存时,读取的仍是旧的研究生学期 ID,且学期合法性校验仅在 isGraduate 为真时执行。实测切回本科后重建服务并加载缓存,会出现 isGraduate=false 但 selectedSemesterId=graduate:20261 的状态,导致加载出旧的研究生课表。

建议: 切换回退学期时同步持久化 _selectedSemesterId,并在读取缓存时校验学期 ID 是否匹配当前数据源。

3. 同一课程存在分段排课时,调课日期会被忽略

_datesFor() 仅在同班级、同节次恰好只有 1 条记录时才读取 rqpkjgallList:

} else if (_text(row['BJDM']).isNotEmpty &&
    rows
            .where(
              (r) =>
                  _inTerm(r, rawTerm(semesterId)) &&
                  r['BJDM'] == row['BJDM'] &&
                  _text(r['KSJCDM']) == _text(row['KSJCDM']) &&
                  _text(r['JSJCDM']) == _text(row['JSJCDM']),
            )
            .length ==
        1) {

遇到前后半学期换教室等拆分为多条记录的情况时,实际日期会被跳过,退回按 ZCMC 和原星期生成课程。基于现有 拆分记录测试样例 进一步检查生成的课表,发现 2026-09-22(周二)的调课丢失,反而多出了原星期的课程(现有单测只断言了首周日期推断,未覆盖最终课表)。

建议: 放宽唯一记录限制或按周次范围匹配调课日期,并补充拆分记录下的调课、停课及教室分配单测。

4. 刷新途中切换账号,会导致新账号无法拉取课表

当账号 A 的请求尚未结束时切换到账号 B,B 的 600ms 定时刷新触发后,fetchAll() 会因为 _loading 为 true 直接返回,但外层的 _fetchAndStamp() 依然会将账号 B 标记为已成功获取:

Future<void> _fetchAndStamp() async {
  final account = _campusAccount();
  await fetchAll();
  if (account != null && account == _campusAccount() && _error == null) {
    _fetchedForAccount = account;
  }
}

随后 A 的响应因账号变更被丢弃,而 B 因为已被标记为 _fetchedForAccount,后续触发 requestFetch(force: true) 也会被直接跳过,导致 B 课表一直为空且无报错。

建议: 仅在当前账号的请求真正完成且未发生账号切换时更新 _fetchedForAccount;若请求期间发生账号切换,待旧请求结束后为新账号补发一次请求。

5. 学期名称重复拼接学年与“学期”后缀

当 XNXQMC 为 2025-2026学年 第一学期 时,界面实际显示为:

2025-2026 2025-2026学年 第一学期学期
1c2428f658b4215281ea531ed15efcd3

原因是 研究生学期解析只匹配了“秋 / 春 / 暑”,未命中时直接保留了完整的 XNXQMC;而 CourseTable 的通用显示逻辑又在首尾拼接了学年和“学期”。

建议: 解析时兼容“第一学期 / 第二学期”等官方命名(或剥离已有的学年与“学期”字样),避免重复拼接。

6. 切换课表来源后,加载中或请求失败时仍展示旧来源课表

从研究生课表切换到本科课表时,selectScheduleSource() 仅清理了身份信息便直接调用 fetchAll(),未清理或切换当前展示的课表与 _semesterInfo。

由于 isGraduate 依赖 _semesterInfo?.tableId,而 _semesterInfo 只有在 本科请求成功解析后才会被替换(请求失败时 仅记录 _error),导致在请求加载期间以及请求失败后,isGraduate 和界面上的 courseTable 依然停留在旧的研究生课表,与当前选中的本科来源不一致。

建议: 切换来源时立即切换或重置展示状态,优先加载目标来源的本地缓存;若无缓存则展示对应的加载态或错误态,避免跨来源残留旧数据。

@gfdfdxc

gfdfdxc commented Nov 10, 2026

Copy link
Copy Markdown
Author

谢谢提供脱敏样本和复现步骤。我在 PR 提交 ebb521a 上逐项核对,6 个问题都能复现,已全部修复。

@gfdfdxc
gfdfdxc requested a balanced review from Copilot November 12, 2026 18:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Active moderate and critical findings remain around stale graduate cache reuse, incorrect CASTGC handling, missing owner scoping, and account-revision race conditions.

6 open issues

Review effort: Lite

if (raw.trim().isEmpty) continue;
try {
final cookie = Cookie.fromSetCookieValue(raw.trim());
if (cookie.name == 'CASTGC' && uri.host != idsHost) continue;
Comment on lines +120 to +125
_identityForAccount = null;
_graduateIdentity = false;
_identityLabel = null;
_identityWarning = null;
_fetchedForAccount = null;
_clearSchedule();
_courseTable = table;
await _storage.saveGraduateSchedule(account, snapshot);
await _storage.setSelectedSemester(semester);
await _storage.saveCourseTable(semester, table);
Comment on lines +462 to +465
await _storage.saveGraduateSchedule(account, snapshot);
await _storage.setSelectedSemester(semester);
await _storage.saveCourseTable(semester, table);
if (revision != _accountRevision) return true;
Comment on lines +314 to +318
Future<void> saveCourseTable(String semesterId, CourseTable table, {String? owner}) =>
_prefs.setString(
'$_courseTablePrefix$semesterId',
jsonEncode({...table.toJson(), if (owner != null) 'owner': owner}),
);
Comment on lines +368 to +372
if (_schedule.hasGraduateIdentity ||
_schedule.isGraduate ||
semesterId?.startsWith(graduateSemesterPrefix) == true) {
_platformErrors.remove('exam');
return null;

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

3 participants