docs: plan UESTC undergraduate integration
This commit is contained in:
@@ -0,0 +1,286 @@
|
|||||||
|
# TPSchedule 本科生接入 Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** 为电子科技大学本科生提供网页登录后手动导入课表、按学期及多种考试类型自动导入考试,并保持现有研究生数据和流程兼容。
|
||||||
|
|
||||||
|
**Architecture:** 保留研究生连接器,新建本科解析器、受限页面捕获和独立连接器。学校标识及学期作为共用领域对象;本科考试保存在独立于课表学期的数据中,整批成功后一次写入。沿用 Flutter 页面、原子存储、课程提醒及日历服务。
|
||||||
|
|
||||||
|
**Tech Stack:** 现有 Flutter 3.47.7 / Dart 3.13.5、`html`、`webview_flutter`、Flutter Test、Android API 36;不增加产品依赖。
|
||||||
|
|
||||||
|
**Spec:** `F:/program/app/docs/superpowers/specs/2026-10-14-uestc-undergraduate-design.md`(已确认;执行者必须同时阅读)。
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- 工程根目录 `F:/program/app`;应用 TPSchedule,包名 `com.tpschedule.app`;发布版本计划 `0.7.0+18`,当前 `0.6.2+17`。
|
||||||
|
- 不修改密钥、签名配置及0.5.4登记冻结材料、V1.0软著材料;签名证书SHA256必须保持 `AF5C21B1BA1E48E461D8EA38DF0FE1C3653F64EB7E8E7728BAE6E5D988BA0D0A`。
|
||||||
|
- 学校稳定标识 `uestc_graduate`、`uestc_undergraduate`;保存格式仍为 `schemaVersion: 1`,旧字段缺少学校标识时默认研究生。
|
||||||
|
- 本科课表登录地址 `https://eams.uestc.edu.cn/eams/`;只有点击「导入当前课表」才捕获和解析课表。
|
||||||
|
- 本科考试523对应2026–2027第一学期,相邻学期差20;考试类型1期末、2期中、3补考/缓考;至少选一种,首次默认523及期末。
|
||||||
|
- 本科12节,研究生及示例11节;第12节22:00–22:45,前11节作息不变;七天同屏、相同节数课程条等长、教师和教室完整。
|
||||||
|
- 周次1–30;考试合并记录≤500;单文件及一次捕获全部片段合计≤2MB;同源文档遍历最多4层、16个文档。超限拒绝,不能截断。
|
||||||
|
- 读取单类型45秒、整批120秒,均不计算人工认证时间;任何失败、取消或过期回调不得写入临时结果。重新认证后全部类型重新读取。
|
||||||
|
- 本科考试不清课表、不改课表学期/开始日、不触发课程变更协调;现有回到前台服务检查保持。普通启动不创建学校WebView。
|
||||||
|
- 界面沿用蓝色、现有Dock及换周动画;不新增考试提醒、服务端功能或自制密码界面;不执行附件脚本、考试申请或取消操作。
|
||||||
|
|
||||||
|
## Review Focus
|
||||||
|
|
||||||
|
1. 页面初始化默认学期523、实际结果503:使用实际结果证据,不能把历史考试写到新学期(任务4、5)。
|
||||||
|
2. 验证码等待超过120秒及登录后切换账号:等待不超时,重新认证丢弃旧片段整批重读(任务6)。
|
||||||
|
3. 多类型读取的最后一项失败、返回旧页面或重复点击:保留旧考试及查询选项,过期结果不得保存(任务6、7)。
|
||||||
|
4. 第12节仅出现在其他周、多人教师及教室变化:整学期保持12行,教师/教室完整,停课周不生成提醒(任务3、8)。
|
||||||
|
5. 学校选择保存失败、损坏旧文件及本科考试与研究生课表共存:不错误导航/迁移/清空,保留恢复入口(任务1、2、9)。
|
||||||
|
|
||||||
|
## 执行约定与文件职责
|
||||||
|
|
||||||
|
所有下列路径相对工程根目录;测试命令在 `mobile` 目录运行。执行前读取适用的 `AGENTS.md`(若存在)、检查工作树和基线,不覆盖用户的 `output/` 材料。需要隔离时按 using-git-worktrees 技能创建工作树;尚未获得计划执行确认时只允许文档及只读检查。
|
||||||
|
|
||||||
|
新文件:
|
||||||
|
|
||||||
|
| 文件 | 职责 |
|
||||||
|
| --- | --- |
|
||||||
|
| `mobile/lib/domain/school_identity.dart` | 共用学校枚举、稳定标识和名称 |
|
||||||
|
| `mobile/lib/domain/undergraduate_exam_query.dart` | 本科考试类型、有效查询、标签及URL |
|
||||||
|
| `mobile/lib/features/school/undergraduate/activity_data.dart` | 受限活动字面量/索引解析,不执行JS |
|
||||||
|
| `mobile/lib/features/school/undergraduate/timetable_adapter.dart` | 本科课表HTML到ImportDraft |
|
||||||
|
| `mobile/lib/features/school/undergraduate/exam_adapter.dart` | 本科考试HTML到ExamSchedule及整批合并 |
|
||||||
|
| `mobile/lib/features/school/undergraduate/navigation_policy.dart` | 本科官方导航与数据来源检查 |
|
||||||
|
| `mobile/lib/features/school/undergraduate/page_probe.dart` | 状态/捕获脚本与证据解码 |
|
||||||
|
| `mobile/lib/features/school/undergraduate/session.dart` | 延迟WebView、手动课表、自动考试批次 |
|
||||||
|
| `mobile/lib/features/school/undergraduate/auth_page.dart` | 本科浏览器、进度、导入确认与保存 |
|
||||||
|
| `mobile/lib/features/school/undergraduate/exam_query_picker.dart` | 学期单选、类型多选及取消 |
|
||||||
|
| `mobile/lib/data/exam_file_import.dart` | 本科/研究生考试文件分派 |
|
||||||
|
|
||||||
|
既有领域模型负责兼容序列化;`AppController`负责串行原子提交;`HepanApp`和`main.dart`负责连接器分派/生命周期;课表、考试、我的页面负责数据来源及入口。每个任务先写测试并观察预期失败,再实现、验证、提交;不把通用文件整段重写。
|
||||||
|
|
||||||
|
测试初始化(同一个PowerShell会话):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:HEPAN_TOOLCHAIN_ROOT = 'F:/program/app/.tools'
|
||||||
|
. ./scripts/android-env.ps1
|
||||||
|
Set-Location mobile
|
||||||
|
```
|
||||||
|
|
||||||
|
Flutter命令使用已有工具链;此环境如出现SDK执行权限限制,使用授权后的执行方式重试,不能把环境失败算成业务测试失败。Git提交只暂存该任务文件;沿用已有身份,无全局身份时使用命令局部 `-c user.name=Codex -c user.email=codex@local`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: 学校、学期与兼容数据模型
|
||||||
|
|
||||||
|
**Files:** Create `domain/school_identity.dart`、`domain/undergraduate_exam_query.dart`;Modify `domain/school_term.dart`、`domain/course.dart`、`domain/exam_schedule.dart`、`domain/app_snapshot.dart`、`features/school/school_selection_page.dart`(均位于`mobile/lib`)。Test `mobile/test/app/school_term_test.dart`、新建 `mobile/test/domain/undergraduate_models_test.dart`、既有 `mobile/test/data/snapshot_store_test.dart`。
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- `enum SupportedSchool`移动到领域文件,保留 `uestcGraduate`,新增 `uestcUndergraduate`;字段 `String id, label, description`,`static SupportedSchool fromId(String id)`未知值抛FormatException。学校选择文件重导出枚举,兼容既有调用。
|
||||||
|
- `SchoolTerm(int id, {SupportedSchool school = SupportedSchool.uestcGraduate})`,字段 `int id; SupportedSchool school`;JSON增 `schoolId`,缺失默认研究生;相等/hash同时使用二者。原 `choices`、研究生URI保持;本科调用研究生URI getter必须抛StateError。
|
||||||
|
- `static List<SchoolTerm> undergraduateChoices(DateTime now, {SchoolTerm? saved})`:按设计编号公式,2013第一学期至设备当前年份下一学年的第二学期,降序并包含有效saved。
|
||||||
|
- `enum UndergraduateExamType`含 `finalExam(1,'期末考试')`、`midterm(2,'期中考试')`、`makeup(3,'补考/缓考')`,字段 `int id; String label`。
|
||||||
|
- `UndergraduateExamQuery({required SchoolTerm term, required Set<UndergraduateExamType> types})`:不可变、仅本科学期、至少一种;`List<UndergraduateExamType> get orderedTypes`、`Uri uriFor(UndergraduateExamType type)`、`static UndergraduateExamQuery? fromSchedule(ExamSchedule? schedule)`。
|
||||||
|
- `Course`新增兼容可选 `String courseCode = '', notes = ''`,通用节次允许12。学校对应的11/12节合法性在ImportDraft/快照边界验证;未标本科的第12节数据不得保存。
|
||||||
|
- `ExamRecord`新增 `String date = '', status = '', notes = ''`、`int? examTypeId`;原有字段语义保持,保存完整安排于 `time`。`ExamSchedule`新增 `SupportedSchool school = SupportedSchool.uestcGraduate`、`Set<int> requestedExamTypes = const {}`;本科必须有效学期及非空1–3类型集合,记录类型须属于该集合,按复合字段去重;研究生保留原code唯一规则。
|
||||||
|
- `AppSnapshot`新增 `SupportedSchool? selectedSchool`及copyWith,JSON字段名为 `selectedSchoolId`(稳定字符串或null);`ExamSchedule`的学校JSON字段名为 `schoolId`,类型字段为 `requestedExamTypes`(排序的整数数组)。旧数据按可靠学校来源恢复,不能将本科考试编号推断为研究生课表学期。新字段缺失保持旧默认,有明确未知标识则拒绝。
|
||||||
|
|
||||||
|
- [ ] **Step 1 — 写失败测试。** 验证本科483/503/523/543/563标签、523期末URL精确等于用户地址、空类型/编号504拒绝、学校不同编号相同不相等、`{'id':73}`仍为研究生;本科同课程期中/期末均保留,研究生重复code仍拒绝;旧快照round-trip兼容,未知学校/损坏类型拒绝;本科12合法、未标本科12非法,旧课程新增字段为空。
|
||||||
|
|
||||||
|
测试名 `undergraduate semester 523 uses the final exam URL` 的核心断言:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
final term = SchoolTerm(523, school: SupportedSchool.uestcUndergraduate);
|
||||||
|
final query = UndergraduateExamQuery(term: term, types: {UndergraduateExamType.finalExam});
|
||||||
|
expect(term.label, '2026-2027第一学期');
|
||||||
|
expect(query.uriFor(UndergraduateExamType.finalExam).toString(),
|
||||||
|
'https://eams.uestc.edu.cn/eams/stdExamTable!examTable.action?semester.id=523&examType.id=1');
|
||||||
|
expect(SchoolTerm.fromJson({'id': 73}).school, SupportedSchool.uestcGraduate);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2 — 验证失败。** `flutter.bat test --no-pub test/app/school_term_test.dart test/domain/undergraduate_models_test.dart test/data/snapshot_store_test.dart`,预期缺少新接口/行为失败,不能只因找不到工具链失败。
|
||||||
|
- [ ] **Step 3 — 实现上述接口及序列化边界。** 不更改旧枚举CourseSource及schemaVersion;本科年份用 `(id-3)~/20` 推导,位于模型中的验证不依赖界面。
|
||||||
|
- [ ] **Step 4 — 重跑上述测试。** 预期全部PASS,包括既有研究生测试。
|
||||||
|
- [ ] **Step 5 — 提交。** `feat: model undergraduate schools terms and exam queries`,只暂存本任务文件。
|
||||||
|
|
||||||
|
### Task 2: 原子保存与课表/考试学期分离
|
||||||
|
|
||||||
|
**Files:** Modify `mobile/lib/app/app_controller.dart`、`mobile/lib/data/json_schedule_import.dart`;Test 新建 `mobile/test/app/undergraduate_controller_test.dart`、既有 `mobile/test/app/app_controller_test.dart`、`mobile/test/data/json_schedule_import_test.dart`。复用 `mobile/test/support/fakes.dart`。
|
||||||
|
|
||||||
|
**Interfaces:** Consumes任务1类型。新增 `Future<bool> AppController.selectSchool(SupportedSchool school)`:通过既有串行队列保存,成功true;失败false且lastError可见,不提前更新内存。保留 `applyImport(ImportDraft,{required bool confirmedEmpty})`、`applyExams(ExamSchedule)`签名。
|
||||||
|
|
||||||
|
- [ ] **Step 1 — 写失败测试。** 本科503考试替换考试列表后原研究生73课表、日期、颜色、提醒偏好及calendarEventIds相同;通知/日历fake调用数不增加。首次本科导课清开始日但保留偏好和独立考试;同本科同学期保留开始日;旧研究生跨学期规则仍成立。存储失败保存前后JSON相同、selectSchool返回false;本科JSON缺少/矛盾学校、混学期、非法12拒绝;旧版本1JSON正常。
|
||||||
|
|
||||||
|
测试名 `historical undergraduate exams preserve the course semester`,使用既有controller/fake初始化后:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
final before = controller.snapshot;
|
||||||
|
await controller.applyExams(ExamSchedule(
|
||||||
|
semesterLabel: '2025-2026第二学期', termId: '503', records: [],
|
||||||
|
school: SupportedSchool.uestcUndergraduate, requestedExamTypes: {1}));
|
||||||
|
expect(controller.snapshot.courses, before.courses);
|
||||||
|
expect(controller.snapshot.schoolTerm, before.schoolTerm);
|
||||||
|
expect(controller.snapshot.settings.semester, before.settings.semester);
|
||||||
|
expect(controller.snapshot.examSchedule!.termId, '503');
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2 — 运行。** `flutter.bat test --no-pub test/app/undergraduate_controller_test.dart test/app/app_controller_test.dart test/data/json_schedule_import_test.dart`;预期当前applyExams清课表等断言失败。
|
||||||
|
- [ ] **Step 3 — 实现分支。** 本科applyExams仅更新examSchedule,本科applyImport不清独立考试;新课表来源/学期才清开始日。研究生分支只接收研究生SchoolTerm。JSON显式schoolTerm用于本科来源,不由本科考试推断课表来源。保存成功后才更新内存及协调课程服务;偏好保存不协调服务。
|
||||||
|
- [ ] **Step 4 — 重跑。** 上述测试PASS;恢复备份/损坏存储既有测试不得退化。
|
||||||
|
- [ ] **Step 5 — 提交。** `feat: keep undergraduate exams independent of course terms`。
|
||||||
|
|
||||||
|
### Task 3: 本科课表解析和离线课表导入
|
||||||
|
|
||||||
|
**Files:** Create本科 `activity_data.dart`、`timetable_adapter.dart`及 `mobile/test/fixtures/school/undergraduate-timetable.html`(脱敏);Modify `mobile/lib/data/schedule_file_import.dart`。Test 新建 `mobile/test/school/undergraduate_timetable_adapter_test.dart`、既有 `mobile/test/school/import_boundaries_test.dart`。
|
||||||
|
|
||||||
|
**Interfaces:** `UndergraduateTimetableAdapter.extract(String html, Uri uri, {CourseSource source = CourseSource.school}) -> ImportDraft`。内部 `parseActivityData(String script,{required int unitCount}) -> List<UndergraduateActivity>`;`UndergraduateActivity`保存teacher/name/courseCode/room、`Set<int> indices, weeks`及是否停课,只供适配器使用。
|
||||||
|
|
||||||
|
- [ ] **Step 1 — 写脱敏等价fixture及失败测试。** 保留附件10门课程、19条活动/18上课+1停课、3条涉及12节、周位串及索引结构;assert每门课程名称/教师/房间/周次/星期/节次映射,停课周6不产生对应occurrence。另加转义引号/Unicode字符串、同课程不同教室和周、多教师、连续节次分组、重复去重、含中文括号名称、名单备注保留;非法索引、缺教师/房间、超范围周、登录页/加载空表/学期矛盾报错;明确空课提示才explicitlyEmpty。
|
||||||
|
|
||||||
|
测试名 `undergraduate activity data preserves period twelve` 的核心断言:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
final draft = UndergraduateTimetableAdapter.extract(
|
||||||
|
File('test/fixtures/school/undergraduate-timetable.html').readAsStringSync(),
|
||||||
|
Uri.parse('https://eams.uestc.edu.cn/eams/courseTableForStd.action'));
|
||||||
|
expect(draft.schoolTerm, SchoolTerm(523, school: SupportedSchool.uestcUndergraduate));
|
||||||
|
expect(draft.courses.map((c) => c.courseCode).toSet(), hasLength(10));
|
||||||
|
expect(draft.courses.where((c) => c.endPeriod == 12), hasLength(3));
|
||||||
|
expect(draft.courses.any((c) => c.room == '停课'), isFalse);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2 — 运行。** `flutter.bat test --no-pub test/school/undergraduate_timetable_adapter_test.dart test/school/import_boundaries_test.dart`,新解析器尚不存在,预期FAIL。
|
||||||
|
- [ ] **Step 3 — 实现。** 用HTML DOM寻找manualArrangeCourseTable及课程名单表,解析实际semester.id;受限字符串tokenizer处理TaskActivity参数及 `index = 常量 * unitCount + 常量`、对应activities赋值。按相同教师/房间/周次的连续索引分组,停课按课程序号/星期/节次扣周;稳定ID用完整规范化安排的确定性摘要,不包含学生姓名/学号。名单补课程序号/备注,不能执行任何脚本。页面和文件均检查UTF8字节总量≤2MB。
|
||||||
|
- [ ] **Step 4 — 分派并验证。** ScheduleFileImport按本科课表明确特征分派,其余保持旧graduate/JSON;重跑任务测试PASS。直接读取用户原附件的本地验证只记录数量/映射结果,不将原文件或个人信息提交。
|
||||||
|
- [ ] **Step 5 — 提交。** `feat: parse undergraduate timetable activity data`,含脱敏fixture及测试,不含原附件。
|
||||||
|
|
||||||
|
### Task 4: 本科考试解析及文件分派
|
||||||
|
|
||||||
|
**Files:** Create本科 `exam_adapter.dart`、`mobile/lib/data/exam_file_import.dart`、`mobile/test/fixtures/school/undergraduate-exams.html`(脱敏)、`mobile/test/school/undergraduate_exam_adapter_test.dart`;既有 `mobile/test/school/exam_adapter_test.dart`保持回归。
|
||||||
|
|
||||||
|
**Interfaces:** `UndergraduateExamAdapter.extract(String html,{SchoolTerm? expectedTerm, UndergraduateExamType? expectedType}) -> ExamSchedule`;`merge(UndergraduateExamQuery query,List<ExamSchedule> parts) -> ExamSchedule`;`ExamFileImport.parse(String html) -> ExamSchedule`。extract离线从实际结果字段取学期及类型;merge要求所选类型恰好全部成功、同school/term并去完全重复。
|
||||||
|
|
||||||
|
- [ ] **Step 1 — 写失败测试。** fixture4条考试归503,外层默认523不能污染;日期、完整时间、地点、座位、情况、类型、说明round-trip。期中及期末同课程均保留;全重复仅一次;缺一类型/mismatched term/type/超500报错;九列表头+有效查询证据+明确无考试提示为空成功,只有表头/登录页不能当空;异常字段不猜时间。ExamFileImport明确分派本科/研究生,未知HTML拒绝。
|
||||||
|
|
||||||
|
测试名 `actual result term wins over the outer default` 的核心断言:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
final exams = UndergraduateExamAdapter.extract(
|
||||||
|
File('test/fixtures/school/undergraduate-exams.html').readAsStringSync());
|
||||||
|
expect(exams.termId, '503');
|
||||||
|
expect(exams.records, hasLength(4));
|
||||||
|
expect(exams.requestedExamTypes, {1});
|
||||||
|
expect(exams.records.first.date, '2026-06-22');
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2 — 运行。** `flutter.bat test --no-pub test/school/undergraduate_exam_adapter_test.dart test/school/exam_adapter_test.dart`,预期FAIL。
|
||||||
|
- [ ] **Step 3 — 实现。** 按九列表头定位实际排考表,读取表单隐藏字段和结果params;两者矛盾拒绝,忽略控件初始化脚本。通过任务1复合身份合并、类型集合保存于ExamSchedule;不给空日期生成伪造日期。
|
||||||
|
- [ ] **Step 4 — 重跑。** 上述测试PASS;用户原考试附件本地验证4条且503,无原HTML进仓库。
|
||||||
|
- [ ] **Step 5 — 提交。** `feat: parse and merge undergraduate exam schedules`。
|
||||||
|
|
||||||
|
### Task 5: 本科导航、认证证据与按需捕获
|
||||||
|
|
||||||
|
**Files:** Create本科 `navigation_policy.dart`、`page_probe.dart`;Test `mobile/test/school/undergraduate_page_probe_test.dart`、`mobile/test/school/undergraduate_page_probe_js_test.cjs`。复用既有Node JS probe测试的DOM模拟模式。
|
||||||
|
|
||||||
|
**Interfaces:** `UndergraduateNavigationPolicy.canNavigate(Uri uri,{required bool isMainFrame}) -> bool`、`isDataPage(Uri uri) -> bool`、`isLogout(Uri uri) -> bool`、`secureIdentityRedirect(Uri uri) -> Uri?`。`undergraduateStatusProbeScript`只返回认证状态证据;`undergraduateCaptureScript`返回限定数据片段。`UndergraduatePageEvidence.decode(Object raw,Uri current) -> UndergraduatePageEvidence`字段 `bool authenticated; List<UndergraduateFragment> pages`;fragment字段 `String kind, html; Uri url`,kind仅timetable/exams;异常/超限抛FormatException。
|
||||||
|
|
||||||
|
- [ ] **Step 1 — 写失败测试。** exact HTTPS eams/idas/eportal导航、HTTP idas指定登录地址升级;用户信息/自定义端口/相似恶意域名拒绝。状态脚本不包含课表HTML、密码或验证码。capture同源iframe/AJAX DOM实际value523/503覆盖旧attribute;跨源/登录文档不捕获;总量超2MB、深度>4、文档>16拒绝而非部分导入;decode顶层URL不一致/跨源片段拒绝;未知认证回调给明确错误。
|
||||||
|
|
||||||
|
测试名 `capture accepts only undergraduate data pages` 的核心断言:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
expect(UndergraduateNavigationPolicy.isDataPage(
|
||||||
|
Uri.parse('https://eams.uestc.edu.cn/eams/')), isTrue);
|
||||||
|
expect(UndergraduateNavigationPolicy.isDataPage(
|
||||||
|
Uri.parse('https://idas.uestc.edu.cn/authserver/login')), isFalse);
|
||||||
|
expect(UndergraduateNavigationPolicy.canNavigate(
|
||||||
|
Uri.parse('https://eams.uestc.edu.cn.attacker.test/eams/'), isMainFrame: true), isFalse);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2 — 运行。** `flutter.bat test --no-pub test/school/undergraduate_page_probe_test.dart` 和 `node --test test/school/undergraduate_page_probe_js_test.cjs`,预期缺接口FAIL。
|
||||||
|
- [ ] **Step 3 — 实现。** 同步JS返回JSON字符串,不用WebView不会等待的Promise;认证证据需学校系统页面的已登录菜单/退出入口,不能仅依据host。捕获仅识别表、actual输入字段、名单和必要活动数据,含frame位置;结果参数与实际隐藏值保留。只序列化数据相关节点,禁止整个登录HTML;页内JS只读取文本/DOM,不执行原脚本。
|
||||||
|
- [ ] **Step 4 — 重跑。** 两种测试PASS;既有研究生probe/policy测试保持。
|
||||||
|
- [ ] **Step 5 — 提交。** `feat: capture undergraduate pages with scoped evidence`。
|
||||||
|
|
||||||
|
### Task 6: 手动课表连接及自动考试批次
|
||||||
|
|
||||||
|
**Files:** Create本科 `session.dart`;Test `mobile/test/school/undergraduate_session_test.dart`、`mobile/test/school/undergraduate_exam_batch_test.dart`,复用/按需扩展 `mobile/test/support/fake_school_web.dart`。
|
||||||
|
|
||||||
|
**Interfaces:** `UndergraduateSession extends ChangeNotifier`,延迟 `WebViewController get web`;`Future<void> openTimetable()`、`openExams(UndergraduateExamQuery query)`、`readCurrentTimetable()`、`reauthenticate()`、`logout()`;`void setReadingActive(bool active)`、`invalidateAuthentication()`、`bool saved({required int generation})`。只读状态 `hasOpened, canLogout, busy, awaitingAuthentication, statusLabel, message, pageGeneration`,`ImportDraft? pendingDraft`、`ExamSchedule? pendingExams`及当前query。session不依赖AppController、不自行存储。
|
||||||
|
|
||||||
|
- [ ] **Step 1 — 写失败测试。** 构造session不创建原生WebView;openTimetable只载eams根,登录状态probe不捕获课表,点击才capture并产生pendingDraft;切换页面时旧capture被丢弃。考试选择1+2+3时逐项精确GET,前两项不产生pendingExams,完成才一个合并结果;第三项失败/显式取消/加载错误保留旧状态无pending;单项45秒/整批120秒边界。等待登录模拟超过120秒不失败;过期认证丢弃已读片段,认证后从类型1重新读取。重复点击、旧finished回调、route关闭、网页logout、dispose不能推进旧批次;fake Cookie清理失败保持可重试退出状态。
|
||||||
|
|
||||||
|
测试名 `closing the route invalidates pending batch results`,fake驱动已打开批次后:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
final openedGeneration = session.pageGeneration;
|
||||||
|
session.setReadingActive(false);
|
||||||
|
expect(session.pageGeneration, greaterThan(openedGeneration));
|
||||||
|
expect(session.pendingExams, isNull);
|
||||||
|
expect(session.saved(generation: openedGeneration), isFalse);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2 — 运行。** `flutter.bat test --no-pub test/school/undergraduate_session_test.dart test/school/undergraduate_exam_batch_test.dart`,预期FAIL。计时使用测试虚拟时间,不真实等120秒。
|
||||||
|
- [ ] **Step 3 — 实现。** 为每次打开/查询分配generation,任何await后校验generation和routeActive;start/finish匹配当前URL及同源证据。自动GET采用loadRequest,finished后有限轮询DOM直到可识别结果;计时及轮询在离页/登录等待/取消时清除。认证过期停读,重新认证启动新generation重读全部;全部完成调用任务4merge一次。manual capture只有readCurrentTimetable调用。saved只消费匹配generation结果;旧结果不能消费新pending。
|
||||||
|
- [ ] **Step 4 — 重跑。** 新session测试及 `test/school/school_session_flow_test.dart`、`test/school/lazy_school_session_test.dart`均PASS。
|
||||||
|
- [ ] **Step 5 — 提交。** `feat: connect undergraduate manual timetable and exam batches`。
|
||||||
|
|
||||||
|
### Task 7: 本科浏览器页面、查询选项和导入确认
|
||||||
|
|
||||||
|
**Files:** Create本科 `auth_page.dart`、`exam_query_picker.dart`;Modify `mobile/lib/features/profile/import_preview.dart`;Test `mobile/test/widgets/undergraduate_auth_test.dart`、`mobile/test/widgets/undergraduate_exam_picker_test.dart`。必要fake平台widget只为浏览器容器提供可测替代,不绕过真实session。
|
||||||
|
|
||||||
|
**Interfaces:** `UndergraduateAuthPage({required UndergraduateSession session,required AppController controller})`;`Future<UndergraduateExamQuery?> showUndergraduateExamQueryPicker(BuildContext context,{UndergraduateExamQuery? initial,required DateTime now})`。返回null取消,返回validquery才登录;跨school/term课表替换和空课表使用ImportPreview确认,已导入同本科同学期非空课表可直接保存。
|
||||||
|
|
||||||
|
- [ ] **Step 1 — 写失败测试。** picker一个学期、三种类型多选、禁止空选择、cancel不请求、沿用503及已选类型。手动页显示导入按钮,登录后不自动保存;跨学校/学期取消保留快照;空课表未勾确认不能保存;save失败仍在页面且pending可重试。成功返回课表;考试整批完成保存一次并返回,partial不保存;关页后旧回调不pop其他页面;双击导入只处理一次。
|
||||||
|
|
||||||
|
Widget测试名 `the manual importer waits for the import button`,登录后fake页面加载并pump后:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
expect(find.text('导入当前课表'), findsOneWidget);
|
||||||
|
expect(session.pendingDraft, isNull);
|
||||||
|
expect(controller.snapshot.courses, isEmpty);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2 — 运行。** `flutter.bat test --no-pub test/widgets/undergraduate_auth_test.dart test/widgets/undergraduate_exam_picker_test.dart`,预期FAIL。
|
||||||
|
- [ ] **Step 3 — 实现。** init/dispose启停读取并监听session;UI操作和保存用busy保护+mounted/generation检查。课表复制当前generation的draft,确认后调用applyImport并检查lastError;考试调用applyExams后检查错误,成功才saved/pop。替换确认说明学校/学期及开始日需要重设;本科网页选学期文案不引导研究生编号选择器。读失败显示类型、重试、修改查询选项;认证等待提示原网页操作。
|
||||||
|
- [ ] **Step 4 — 重跑。** Widget测试PASS,无未处理异步错误/overflow。
|
||||||
|
- [ ] **Step 5 — 提交。** `feat: add undergraduate import and exam selection screens`。
|
||||||
|
|
||||||
|
### Task 8: 接入共用页面、12节及服务计算
|
||||||
|
|
||||||
|
**Files:** Modify `mobile/lib/app/hepan_app.dart`、`mobile/lib/main.dart`、`mobile/lib/features/school/school_session.dart`(只增加失效协调方法)、`mobile/lib/features/school/school_selection_page.dart`、`mobile/lib/features/profile/profile_page.dart`、`mobile/lib/features/timetable/timetable_page.dart`、`mobile/lib/features/timetable/week_grid.dart`、`mobile/lib/features/timetable/course_details.dart`、`mobile/lib/features/exams/exams_page.dart`、`mobile/lib/domain/lesson_times.dart`。Test `mobile/test/widgets/school_selection_test.dart`、`mobile/test/widgets/undergraduate_navigation_test.dart`、`mobile/test/widgets/undergraduate_grid_test.dart`、`mobile/test/domain/schedule_engine_test.dart`、`mobile/test/services/notification_service_test.dart`、`mobile/test/services/calendar_service_test.dart`。
|
||||||
|
|
||||||
|
**Interfaces:** `HepanApp`新增可选本科session/builder参数,保留原 `school`、`schoolBuilder`测试接口。`WeekGrid`新增 `int periodCount = 11`,只接受11/12;`lessonSlots`末尾加 `LessonSlot(12,1320,1365)`。研究生 `void invalidateAuthentication()`仅取消读取并清连接状态/待导入结果,不创建WebView。考试页新增可选查询选项回调,文件入口改任务4的ExamFileImport。
|
||||||
|
|
||||||
|
- [ ] **Step 1 — 写失败测试。** 两学校都出现,cancel无副作用;本科选择保存失败不进入浏览器,重启恢复选择。课表刷新/更新学期仍manual,考试refresh复用上次query、查询选项可改;不同学校的term不能进入gradURL。共享退出使两连接失效但未使用WebView不创建,旧数据按用户原退出选项处理。source不同banner准确。
|
||||||
|
- [ ] **Step 2 — 写显示及时间失败测试。** 本科无课当前周也12行,研究生/示例11;周切换后第12节可见,七天固定宽,教师/教室全量且同节数卡片等高;详情显示全称、courseCode、notes。第12节单节occurrence22:00–22:45,9–12连排19:30–22:45,多提醒offset及calendar结束时间均正确、停课周无occurrence。使用360dp及大字体检查溢出。
|
||||||
|
|
||||||
|
Widget测试名 `graduate rows stay at eleven after adding period twelve`,分别pump已导入学校的页面后断言:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
expect(tester.widget<WeekGrid>(find.byType(WeekGrid)).periodCount, 11); // graduate
|
||||||
|
// 再pump本科快照:
|
||||||
|
expect(tester.widget<WeekGrid>(find.byType(WeekGrid)).periodCount, 12);
|
||||||
|
expect(lessonSlots.last.startMinute, 1320);
|
||||||
|
expect(lessonSlots.last.endMinute, 1365);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3 — 运行。** `flutter.bat test --no-pub test/widgets/school_selection_test.dart test/widgets/undergraduate_navigation_test.dart test/widgets/undergraduate_grid_test.dart test/domain/schedule_engine_test.dart test/services/notification_service_test.dart test/services/calendar_service_test.dart`,预期新导航/12节相关FAIL。
|
||||||
|
- [ ] **Step 4 — 实现。** main只构造Dart session,浏览器延迟;Shell先await selectSchool成功,再分派,取消/失败保持状态。本科考试首次显示picker,refresh用ExamSchedule成功选项;历史课程schoolTerm不被考试改写。退出仅操作已有连接并协调失效,失败保留重试入口。课表periodCount来源于实际schoolTerm,demo强制11;所有布局循环使用截取的slot数量而非新增全局长度。考试页显示自身school/term/types/date/status/notes,空结果文案按设计;任务3/4文件导入沿用preview。保留前台服务检查、Dock/手势及动画。
|
||||||
|
- [ ] **Step 5 — 重跑。** 上述测试PASS;追加运行已有 `test/widgets/complete_metadata_test.dart test/widgets/conflict_metadata_test.dart test/widgets/course_length_test.dart test/widgets/week_swipe_test.dart test/widgets/week_transition_test.dart test/widgets/startup_slow_test.dart test/widgets/logout_recovery_test.dart`,确保教师全量、动画、启动和退出恢复不退化。
|
||||||
|
- [ ] **Step 6 — 提交。** `feat: integrate undergraduate school workflows and period twelve`。
|
||||||
|
|
||||||
|
### Task 9: 全量验证、发布与签名/登记备份核对
|
||||||
|
|
||||||
|
**Files:** Modify `mobile/pubspec.yaml`、`mobile/lib/app/app_version.dart`、`README.md`、`mobile/README.md`、`docs/school-integration-status.md`、`docs/android-mvp-validation.md`、`dist/README.md`、`dist/TPSchedule-备案信息.txt`;生成发布/调试APK、源码ZIP、脱敏截图及 `.tools/`验证日志。不改用户 `output/` 和登记备份。
|
||||||
|
|
||||||
|
**Interfaces:** 上述全部任务完成且回归通过;复用 `scripts/build-android.ps1`、`scripts/signing-info.py`及已有Android16模拟器/发布流程,不重建密钥、不新增依赖。
|
||||||
|
|
||||||
|
- [ ] **Step 1 — 升版本并完整验证。** 两个版本位置统一0.7.0+18;在mobile运行 `flutter.bat test --no-pub --enable-impeller --reporter expanded`、`flutter.bat analyze --no-pub`,预期全量PASS、No issues found。运行本科及既有研究生Node probe测试。失败追根定位、只在新增变更或失败修复后重跑必要检查,不能跳过。
|
||||||
|
- [ ] **Step 2 — 构建与身份验证。** 先保留当前0.6.2 APK及摘要再构建;根目录运行 `./scripts/build-android.ps1 -ToolchainRoot 'F:/program/app/.tools' -Mode release` 和同命令 `-Mode debug`;构建返回0且APK存在。使用signing-info脚本比较旧/新签名,证书必须等于Global Constraints值;读取包名及versionCode18。校验 `dist/backups/TPSchedule-0.5.4-registration-20261014/SHA256清单.json`全部18项内容摘要不变。
|
||||||
|
- [ ] **Step 3 — Android16/API36检查。** 在模拟器覆盖安装,核对旧数据/选择学校/取消/本科登录按钮/考试多选入口。脱敏离线fixture导入检查12行、课程详情、四条503考试、历史考试后课表不变、重启持久化、换周动画/Dock及无新增启动黑屏;抓关键页面截图。真实账号/验证码及学校回调未实测的部分在记录中明确,不能用模拟测试宣称真实导入通过。
|
||||||
|
- [ ] **Step 4 — 记录和源码包。** 写真实测试总数、静态检查、APK SHA256及限制;更新学校接入说明、本科手动操作和12节作息。源码ZIP使用明确允许的源文件集合,排除原附件、用户个人HTML、`.tools`、密钥、构建缓存、`output`和登记材料;验证解包目录/版本及文件数。签名公开信息可保留,私钥不得进入包。
|
||||||
|
- [ ] **Step 5 — 最终审查并交付。** 按 requesting-code-review 技能审查全量差异(只读),修复会导致错误导入/丢数据/提醒时间错误的发现后验证;核对git diff --check及仅预期改动,再提交 `feat: release undergraduate integration 0.7.0`。沿用已授权本地master集成方式;如果使用隔离工作树,合并前检查没有覆盖用户工作。最终提供APK/源码链接、操作说明和真实登录仍需账户本人验证的边界,不等待上线部署。
|
||||||
|
|
||||||
|
## 自审与交接
|
||||||
|
|
||||||
|
覆盖映射:设计1–3及7→任务5–8;设计4→任务1;设计5→任务3/8;设计6→任务4/6;设计8→任务1/2/8;设计9→任务3/4/8;设计10→任务9。Review Focus五项均已分配具体测试;本科自动考试不复用研究生自动课表状态机,学校身份不使用枚举index,保存入口全部走串行原子队列。
|
||||||
|
|
||||||
|
执行方式建议:**本会话直接执行**,原因是9项任务共享学校/学期及保存接口,顺序实施能减少接口交接成本;完成后做一次独立全量审查。另一选择是逐任务由子代理实施与独立审查,审查更频繁、上下文成本更高。
|
||||||
|
|
||||||
|
本文件供用户审阅;确认计划并选定执行方式后,才进入实现。
|
||||||
Reference in New Issue
Block a user