Files
TPSchedule/docs/superpowers/plans/2026-10-12-hepan-everyday-android.md
T

222 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 河畔 Everyday Android 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:** 交付可安装的 Android 首版,实现离线周课表、玻璃悬浮 Dock、我的与设置、学校认证入口、经过确认的课程导入、课前提醒和手机日历同步。
**Architecture:** Flutter UI 与纯 Dart 学期/课程计算分离,由一个 ChangeNotifier 应用控制器管理本地快照、导入与平台服务。认证使用设备 WebView 的学校会话;通知采用 Flutter 插件;日历通过独立 Kotlin Calendar Provider 桥接,保留未来 iOS 适配接口。不设自建账号或服务器。
**Tech Stack:** 已安装 Flutter 3.47.7 / Dart 3.13.5 / Java 17 / Android API 36;依赖固定为 webview_flutter 4.14.1、flutter_local_notifications 22.3.2、timezone 0.11.1、path_provider 2.1.6、file_picker 13.1.0;Flutter SDK 自带 flutter_test、integration_test。提交 pubspec.lock。应用业务位于 `mobile/`,已有工具与文档继续位于仓库根目录。
**Spec:** `docs/superpowers/specs/2026-10-12-hepan-everyday-design.md`(用户已确认)。当前环境结果见 `docs/android-setup-status.md`。
## Global Constraints
- 首版只有课表和我的两个标签页;底部图标不显示文字,保留无障碍标签;玻璃效果采用模糊、渐变、细边框和高光。
- 学校课表使用 Asia/Shanghai;11 节起止时间完全采用设计文件表格;连续多节课程只提醒一次。
- 第 1 周为学期开始日所在的周一至周日;开始日前不生成实例;支持明确周次集合、单双周与不连续周次。
- 学期周数 1–30,初始建议 20,首次需确认真实开始日;提前量关闭或 5/10/15/30/60 分钟,默认 15。
- 示例显著标注,不写入真实课表、不默认排提醒/日历;学校认证成功和导入成功分别显示。
- 密码不存盘、不进入日志、不发送到自建后端;仅向验证过的学校 HTTPS 表单提交;验证码使用学校原页面。
- 未验证的真实课表结构不能用猜测解析器声称接入;错误或空解析不得覆盖旧课表。明确无课程需单独确认。
- 日历默认关闭;只管理本 APP 创建的事件;不开启重复的日历提醒;关闭时选择保留或删除自己的事件。
- Android 先交付,minSdk 24、compile/targetSdk 36;测试 applicationId `com.example.hepan_everyday`,显示名称严格为“河畔Everyday”。
- 本轮计划不生成产品代码;完成计划审阅并选择执行方式后才实施。执行时检查隔离工作区;共享现有 .tools,避免重复下载。
## Review Focus
1. 开始日不是周一,以及手机位于其他时区:课程日期和 08:30 上课时间仍正确(Task 1)。
2. 本地文件损坏或保存失败:保留旧课表和设置,显示恢复/保存失败状态(Task 2)。
3. 超长中文、200% 字体、同日课程重叠及周日:文字可查看,所有天数可达,Dock 不遮挡末节(Task 3)。
4. 非学校域名、认证过期与重复点击同步:不注入密码、不重复提交、不误报成功(Task 4)。
5. 权限被撤销、系统日历手工删除事件、同步途中退出:后续能恢复,避免复制/误删日程(Task 5/6)。
## 文件与边界
|目录/文件|责任|
|---|---|
|mobile/lib/domain/{course,semester,app_settings,app_snapshot,course_occurrence,lesson_times}.dart|不可变值对象、验证和快照序列化|
|mobile/lib/domain/schedule_engine.dart|周次、实例、时区与冲突计算|
|mobile/lib/data/{snapshot_store,file_snapshot_store,demo_schedule,json_schedule_import}.dart|持久化、显著标记的示例、结构化导入|
|mobile/lib/app/{app_controller,hepan_app,app_theme}.dart|状态、服务协调与主题|
|mobile/lib/features/timetable/{timetable_page,week_grid,course_details}.dart|周课表与详情|
|mobile/lib/features/profile/{profile_page,settings_page,import_preview}.dart|我的、设置、确认导入|
|mobile/lib/features/school/{school_auth_page,school_session,school_page_adapter,school_navigation_policy}.dart|学校 WebView、认证和已验证页面适配边界|
|mobile/lib/services/{notification_service,local_notification_service,calendar_service,android_calendar_service}.dart|平台服务接口与适配|
|mobile/lib/widgets/glass_dock.dart|两个纯图标的 Dock|
|mobile/android/app/src/main/kotlin/com/example/hepan_everyday/{MainActivity,CalendarBridge}.kt|Android MethodChannel 和日历权限/事件操作|
|mobile/test/、mobile/integration_test/|业务测试、关键交互与设备验证|
|scripts/android-env.ps1、scripts/build-android.ps1|复用本地工具、构建并导出 APK|
所有命令先激活根目录 `scripts/android-env.ps1`;进入 `mobile/` 后执行 Flutter 命令。隔离工作区通过 `HEPAN_TOOLCHAIN_ROOT` 指向已安装的 `C:/Users/david/Desktop/app/.tools`;环境脚本添加此可选参数,默认行为仍使用本目录 .tools。APK 与日志不进入 Git。
### Task 1: 可运行项目与准确的课程计算
**Files:** 创建 `mobile/` Flutter Android 工程、上述 domain 文件、`mobile/lib/main.dart`、`mobile/lib/app/hepan_app.dart`、`mobile/test/domain/schedule_engine_test.dart`;修改 `scripts/android-env.ps1`、根 `.gitignore`。
**Interfaces:**
- `Course({required String id, required String name, required String teacher, required String room, required int weekday, required int startPeriod, required int endPeriod, required Set<int> weeks, required CourseSource source})`;`CourseSource` 为 school/file/demo。
- `Semester({required DateTime startDate, required int totalWeeks})`;startDate 的年月日代表学校当地日期,totalWeeks 限制 1–30。
- `AppSettings({Semester? semester, bool remindersEnabled=true, int reminderMinutes=15, ColorMode colorMode=ColorMode.multicolor, int singleColorArgb=0xFF348F91, bool calendarEnabled=false, String? calendarId})`;定义 `ColorMode.single/multicolor`。
- `LessonSlot(int period, int startMinute, int endMinute)`;`lessonSlots` 为 11 项常量。
- `CourseOccurrence({required String key, required Course course, required int week, required tz.TZDateTime start, required tz.TZDateTime end})`;key = `course.id/YYYY-MM-DD`,上课时区固定。
- `ScheduleEngine.weekNumber(Semester semester, DateTime schoolDate) -> int?`;学期外返回 null。
- `ScheduleEngine.occurrences(List<Course> courses, Semester semester) -> List<CourseOccurrence>`,按开始时间/ID 排序。
- `ScheduleEngine.overlaps(CourseOccurrence a, CourseOccurrence b) -> bool`。
- [ ] 建立隔离执行目录并复用已安装工具;执行 `flutter create --platforms=android --org=com.example --project-name=hepan_everyday mobile`。仅添加本计划依赖和 SDK 测试依赖,保存锁文件;将 Android APP 名称设置为“河畔Everyday”。
- [ ] 先写失败测试:11 节开始分钟 `[510,560,620,670,870,920,980,1030,1170,1220,1270]`、结束分钟 `[555,605,665,715,915,965,1025,1075,1215,1265,1315]`;2026-09-09 开始的学期中,9 月 7 日不生成实例,9 月 9 日属于第 1 周,9 月 14 日属于第 2 周;周日课程可达;2–3 节实例结束为 11:05。
- [ ] 增加失败测试:周次 `{1,3,5}` 只生成 3 个实例;节次 0/12、星期 8、非法周次拒绝;相邻但不重叠返回 false;把 `2026-09-09T00:30Z` 转为学校日期后,上课仍为学校时间 08:30,开始 UTC 为 00:30。
```dart
test('morning break before period 3 is fifteen minutes', () {
expect(lessonSlots[2].startMinute - lessonSlots[1].endMinute, 15);
expect(lessonSlots.last.endMinute, 1315);
});
```
- [ ] 运行 `flutter test test/domain/schedule_engine_test.dart`,确认因缺少实现失败;实现上述类型与计算,初始化 timezone 数据并固定 Asia/Shanghai。clock 的瞬间先转换为上海时间;日期选择器的年月日仅解释为学校当地日期,不能用手机当前时区重新偏移。
- [ ] 运行同一测试并确认全通过;让基础 APP 能启动,默认无学期、无真实课程,无权限弹窗;`flutter analyze` 无问题。提交本任务。
### Task 2: 持久化、导入和单一应用状态
**Files:** 创建 data 文件、`mobile/lib/domain/app_snapshot.dart`、`mobile/lib/app/app_controller.dart`、`mobile/lib/services/notification_service.dart`、`mobile/lib/services/calendar_service.dart`、`mobile/test/data/{snapshot_store_test,json_schedule_import_test}.dart`、`mobile/test/app/app_controller_test.dart`、`mobile/test/support/fakes.dart`。
**Interfaces:**
- 消费 Task 1 类型。`ImportMetadata({required CourseSource source, required DateTime importedAt, required String parserVersion})`。
- `AppSnapshot({required AppSettings settings, required List<Course> courses, ImportMetadata? importMetadata, Map<String,String> calendarEventIds=const {}})`;schemaVersion=1,不包含密码/Cookie/认证令牌。
- `SnapshotStore.load() -> Future<AppSnapshot?>`,`SnapshotStore.save(AppSnapshot snapshot) -> Future<void>`;`FileSnapshotStore(Directory directory)`,使用 path_provider Application Support 的目录。
- `ImportDraft({required List<Course> courses, required bool explicitlyEmpty, required CourseSource source, required String parserVersion})`;`JsonScheduleImport.parse(String text) -> ImportDraft`,只接受 schemaVersion 1、结构化字段和最大 2 MiB UTF-8 数据。根字段为 `schemaVersion`、`isDemo`、`explicitlyEmpty`、`courses`;课程字段为 `id/name/teacher/room/weekday/startPeriod/endPeriod/weeks`,weeks 是整数数组。文件来源仅可为 file 或 demo,不能冒充 school;isDemo=true 进入示例体验。
- `NotificationService.reconcile(List<CourseOccurrence> occurrences, AppSettings settings, DateTime now) -> Future<ReminderResult>`,`requestPermissions() -> Future<void>`、`cancelOwned() -> Future<void>`;reconcile 仅检查权限,不自行弹窗。`ReminderResult` 的 status 为 off/permissionDenied/exact/inexact/error,包含 scheduledCount 与说明。
- `CalendarService.requestAccess() -> Future<bool>`,`listWritableCalendars() -> Future<List<DeviceCalendar>>`,`sync(List<CourseOccurrence> occurrences, String calendarId, Map<String,String> knownEventIds) -> Future<Map<String,String>>`,`deleteOwned(Map<String,String> eventIds) -> Future<void>`;`DeviceCalendar(id,name)`。
- `AppController(SnapshotStore store, NotificationService notifications, CalendarService calendar, DateTime Function() clock)` extends ChangeNotifier;暴露持久化 `snapshot`、显示用 `visibleCourses/visibleSettings`、`isDemo`、`lastError`、`reminderResult`、`calendarSyncError`。`initialize()`、`showDemo({List<Course>? courses})`、`leaveDemo()`、`updateSettings(AppSettings value)`、`applyImport(ImportDraft draft,{required bool confirmedEmpty})`、`clearCourses()`、`disableCalendar({required bool deleteEvents})`、`enableCalendarFromUserAction(String calendarId)`、`enableRemindersFromUserAction()` 均返回 Future<void>。示例有单独临时数据和“示例”学期,以 clock 所在周为第 1 周,不改变真实快照。clearCourses 始终清理对应通知和自有日历事件;仅关闭日历的保留选择由 disableCalendar 处理。
- [ ] 写失败测试:有效 JSON 转为确定周次集合;部分记录非法使整个草稿失败;空数组且 explicitlyEmpty=false 不得覆盖;重复 ID 拒绝;schemaVersion 未知和超过 2 MiB 拒绝;isDemo=true 导入仅改变显示数据,不改真实快照、不调用平台服务。
- [ ] 写失败测试:临时目录保存、重新打开恢复所有课程/设置;损坏文件报错且不返回“空课表”;模拟保存异常后控制器仍保留旧 snapshot;showDemo 不调用通知或日历;快速并发两次更新按顺序保留最后一份设置;移除课程后只清理本 APP 的副作用。
```dart
test('unknown import version is rejected', () {
expect(() => JsonScheduleImport.parse('{"schemaVersion":2,"courses":[]}'),
throwsFormatException);
});
```
- [ ] 运行 `flutter test test/data test/app` 确认失败;实现 JSON 文件 `.tmp` 写入并 flush、rename 替换,保存前保留最近有效 `.bak`;损坏时提示可恢复备份,不静默丢弃。用串行队列处理持久化和服务协调。
- [ ] 实现控制器:只有确认非 demo 草稿才保存真实快照;保存成功后分别协调服务,一项失败不阻止另一项,记录状态供重试;初始化离线恢复后也执行幂等协调。认证连接状态仅由学校会话确认,不能用存在课程推断已登录。
- [ ] 运行同一测试全通过并提交。演示课程与真实快照分开,demo 学期仅供展示,名称/教师标记“示例”。
### Task 3: 两个页面、玻璃 Dock 与所有设置
**Files:** 创建 app_theme、widgets 和 timetable/profile 文件;修改 hepan_app/main;创建 `mobile/test/widgets/{navigation_test,timetable_test,settings_test,import_preview_test}.dart`。
**Interfaces:** 消费 `AppController`。`HepanApp({required AppController controller})`;`GlassDock({required int selectedIndex, required ValueChanged<int> onSelected})`;`TimetablePage({required AppController controller})`、`ProfilePage({required AppController controller})`、`SettingsPage({required AppController controller})`。`ImportPreview({required ImportDraft draft, required AppController controller})` 返回用户确认结果;所有平台能力经控制器接口调用。
- [ ] 写失败交互测试:点击日历/我的图标切换两页,没有第三个标签或文字 Dock;屏幕 360×800、底部安全区 34 时,9–11 节可滚动到且不被 Dock 遮挡;周日可横向滚动到。
- [ ] 写失败测试:200% 字体下超长名称/教师/教室无渲染溢出,详情可完整阅读;跨 1–4 节课程仅一个条;重叠课程同时可见并标冲突;当前周跳转边界不能越过配置学期。
- [ ] 写失败测试:颜色切换、提前 15→30 分钟、关闭提醒、周数 0/31 拒绝、确认开始日后保存;未配置学期不生成提醒;取消导入保留旧课程;示例标签常驻且不能开启示例提醒/日历。
```dart
testWidgets('dock opens profile', (tester) async {
// 本测试 setup 用 Fake 服务创建并挂载 HepanApp。
await tester.tap(find.bySemanticsLabel('我的'));
await tester.pumpAndSettle();
expect(find.byType(ProfilePage), findsOneWidget);
});
```
- [ ] 运行 `flutter test test/widgets` 确认失败;实现浅色蓝绿主题、上午/下午/晚上分组、同步横向滚动日期头、课程详情;周次按钮、今天列与日期范围明确。Dock 用 BackdropFilter+渐变高光,图标提供 Semantics。
- [ ] 实现我的未登录状态、最近真实导入状态、示例体验、JSON 文件选择/预览/确认;file_picker 13 使用 `pickFile()` 和有 2 MiB 上限的 `readAsByteStream()`,未知长度也必须有累计字节限制,非法 UTF-8 拒绝,取消不报失败。设置默认日历关闭、真实开始日必选;日历开启准备权限/选择入口,Task 6 接入。
- [ ] 同一测试通过、模拟器目视检查两个页面和大字体、截图保存到 `artifacts/screenshots/`;提交。本任务交付本地完整体验,学校入口暂明确“待接入”,不显示虚假同步成功。
### Task 4: 学校认证和真实导入适配边界
**Files:** 创建 school 文件、`mobile/test/school/{navigation_policy_test,school_page_adapter_test}.dart`、`mobile/test/fixtures/school/README.md`;修改 profile_page 与 import_preview。
**Interfaces:** `SchoolNavigationPolicy.canInjectCredentials(Uri uri) -> bool` 仅允许 `https://idas.uestc.edu.cn/authserver/login`;`isTimetable(Uri uri) -> bool` 精确匹配 `https://yjsjy.uestc.edu.cn/pyxx/pygl/xskbcx/index/1`。`SchoolPageAdapter.extract(String html, Uri source) -> ImportDraft`;没有经确认的页面匹配器时抛 `UnsupportedSchoolLayout`。`SchoolSession` 包装 WebViewController,暴露 `connectionState`(signedOut/authenticating/connected/expired/unknown)、`busy`、`open()`、`readTimetable() -> Future<ImportDraft>`、`logout() -> Future<void>`。`SchoolAuthPage({required SchoolSession session, required AppController controller})`。
- [ ] 写失败测试:`idas.uestc.edu.cn.evil.test`、HTTP、userinfo 欺骗 URL、其他路径均不能注入凭据;只加载登录页不构成认证成功;课表标题/合法目标和验证结构满足时才解析;未知结构、验证码页和空 HTML 都不得覆盖旧课表;重复同步在 busy 时仅执行一次。
```dart
test('lookalike auth host cannot receive credentials', () {
expect(SchoolNavigationPolicy.canInjectCredentials(
Uri.parse('https://idas.uestc.edu.cn.evil.test/authserver/login')), isFalse);
});
```
- [ ] 运行 `flutter test test/school` 确认失败;实现 URI 精确校验、导航提示、仅主文档错误状态、认证过期保留离线课表。同一 WebView 管理会话,密码字段 submit/cancel/dispose 后清空。
- [ ] 在公开认证页面核对表单字段、提交脚本和动态验证机制;仅能核实时接通自定义输入向官方表单交接。无法验证时显示学校原页面进行认证,自定义界面明确当前方式,不能假装原生账号登录已经联通。
- [ ] 使用目标 URL 的学校实际跳转流程;没有本人会话/页面样本时,只交付认证入口与 fail-closed 解析边界,UI 明确“课表识别待联调”;fixture README 明确没有真实样本。不得用人工编造表格作为已验证学校 fixture。
- [ ] 若用户本人设备取得去标识化页面样本,加入对应 fixture、精确字段匹配器和周次解析测试;真实字段缺失时整体拒绝;真实明确无课程仍必须二次确认。无样本则把该联调状态记录在交付说明。
- [ ] 同一测试通过;退出登录清除 Cookie、WebView 缓存与 localStorage,选择保留时不清课程,选择清除时调用 clearCourses。提交;文案分别表示认证状态与课表导入状态。
### Task 5: Android 课前提醒
**Files:** 创建 local_notification_service、`mobile/test/services/notification_service_test.dart`;修改 `mobile/android/app/build.gradle.kts`、`mobile/android/app/src/main/AndroidManifest.xml`、`mobile/android/app/src/main/res/drawable/ic_stat_calendar.xml`、settings_page/main。
**Interfaces:** `LocalNotificationService` implements Task 2 NotificationService;可注入 `ReminderGateway`,其 `permissions({required bool request}) -> Future<ReminderPermissions>`、`pendingIds() -> Future<Set<int>>`、`schedule(ReminderRequest value) -> Future<void>`、`cancel(int id) -> Future<void>`。`ReminderRequest(id, occurrenceKey, fireAt, title, body, exact)`;`ReminderPermissions(notifications,exactAlarm)`。`notificationId(String occurrenceKey) -> int` 使用 FNV-1a 32-bit 后取正 31-bit;本次集合碰撞按排序后线性探测解决,不能依赖 Dart hashCode。ID 变化时先取消旧计划再重排,不能覆盖碰撞课程。
- [ ] 写失败测试:08:30 课程默认 08:15 触发;已过提醒时间不排任务;只排实际上课周、每连续课程一次;demo、无学期、提醒关闭不排;重复 reconcile 无重复;删除课程取消旧 ID;通知权限拒绝返回 permissionDenied;精确权限拒绝可用 inexact 并显示说明;撤销权限后不显示已生效。
- [ ] 写失败测试:500 条以上未来实例时仅保留最早 400 条并显示“后续提醒在再次打开时补充”;初始化/设置更改补充未来任务,Android 重启恢复已安排的任务。用 FakeReminderGateway 检验相同实例保留稳定 ID。
```dart
test('notification identity is stable', () {
expect(notificationId('math/2026-09-09'), 1794407394);
});
```
- [ ] 运行 `flutter test test/services/notification_service_test.dart` 确认失败;实现时区通知、点击进入课表详情。设置中启用/授权按钮调用 enableRemindersFromUserAction,再请求通知和精确闹钟能力;启动与后台协调只读权限状态,不自动弹窗。
- [ ] 按插件 22.3.2 官方 README 配置 Java 17、core library desugaring 2.1.4、SDK 36、RECEIVE_BOOT_COMPLETED、SCHEDULE_EXACT_ALARM、通知接收器与小图标;使用所需最小权限,不使用 USE_EXACT_ALARM。
- [ ] 同一测试通过;在模拟器安排短时验证通知并退到后台,核实收到/点击/关闭后取消;重启验证恢复。记录厂商省电限制仍需真机检查。提交。
### Task 6: 手机日历同步与精确清理
**Files:** 创建 android_calendar_service、CalendarBridge.kt、`mobile/test/services/calendar_service_test.dart`、`mobile/integration_test/calendar_bridge_test.dart`;修改 MainActivity.kt、Manifest、app_controller、settings_page。
**Interfaces:** AndroidCalendarService 实现 Task 2 CalendarService;MethodChannel `hepan_everyday/calendar` 的方法为 `requestAccess`、`listCalendars`、`syncEvents`、`deleteOwnedEvents`。CalendarBridge 用 CalendarContract 查询可写日历,按 occurrenceKey 和当前 applicationId 设置并查验 CUSTOM_APP_URI/CUSTOM_APP_PACKAGE;Dart 保存 key→eventId 的映射。
- [ ] 写失败测试:重复同步仅一个事件;教师/教室改变更新原事件;用户手工删除日历事件后下一次恢复一条;事件已被改成非本 APP 的标记时不修改/删除;同步到一半失败后重试通过 app URI 找回已创建事件。
- [ ] 写失败测试:权限拒绝/撤销、只读日历、无可写日历都保持未启用状态;关闭选择保留时不删除;选择清除仅删除拥有标记的事件;删除整张课表也不影响其他 APP 日程;不创建 CalendarContract.Reminders。
```dart
test('denied calendar permission keeps calendar off', () async {
// setup 中 Fake CalendarService.requestAccess 返回 false。
await controller.enableCalendarFromUserAction('selected-calendar');
expect(controller.snapshot.settings.calendarEnabled, isFalse);
expect(controller.calendarSyncError, isNotNull);
});
```
- [ ] 运行 `flutter test test/services/calendar_service_test.dart` 确认失败;实现 READ_CALENDAR/WRITE_CALENDAR 运行时权限。enableCalendarFromUserAction 检查授权和可写日历,成功同步后才开启设置;写入 TZ 实例对应的 UTC 毫秒与 EVENT_TIMEZONE=Asia/Shanghai。
- [ ] 实现 Kotlin 桥接和 Dart 重试逻辑。系统写入与快照无法组成跨系统事务,先凭 app URI 查询去重再写;每次已完成映射及时持久化,重启后可恢复。仅操作用户选定日历,无法校验拥有标记时跳过;disableCalendar 保留时关闭后续自动同步,删除时先清理自有事件。清理失败保留待重试映射,不丢失追踪。
- [ ] 同一测试通过;`flutter test integration_test/calendar_bridge_test.dart -d emulator-5554` 验证创建、更新、删除和权限结果。没有可写日历时设备测试明确跳过并报告;仅使用测试专用日历,不向用户其他日历写测试数据。提交。
### Task 7: 联调、Android 测试包与交付
**Files:** 创建 `mobile/integration_test/app_flow_test.dart`、`scripts/build-android.ps1`、`docs/android-mvp-validation.md`、`docs/school-integration-status.md`、`examples/schedule.schema-v1.json`;修改 README;输出 `dist/hepan-everyday-debug.apk`,截图放 artifacts。
**Interfaces:** 应用启动组装真实 SnapshotStore、通知、日历和学校服务;集成测试注入临时存储与显式 fake 平台服务,学校实际会话不在测试中模拟为真实导入。build-android.ps1 激活已安装环境,在 mobile 执行 debug 构建,成功才复制 APK;失败不得复制旧产物冒充本次结果。
- [ ] 写端到端测试:首次启动→我的设置真实开始日/周数→导入结构化示例文件→确认→七天课表→切换多彩/单色→重开应用恢复;进入示例体验不会修改此前真实快照;错误导入不会擦除课程。
```dart
testWidgets('installed app exposes exactly the two tabs', (tester) async {
// setup 挂载应用并完成初始化,权限不自动弹窗。
expect(find.bySemanticsLabel('课表'), findsOneWidget);
expect(find.bySemanticsLabel('我的'), findsOneWidget);
});
```
- [ ] 运行该测试确认未完成连接时失败;接通初始化、服务状态和所有按钮,不留无响应控件或假的成功提示;返回前台复核权限变化并重试尚未成功的通知/日历协调。
- [ ] 完成 `flutter analyze`、`flutter test`、`flutter test integration_test/app_flow_test.dart -d emulator-5554`;全通过后运行 `flutter build apk --debug`。检查新 APK 的名称、包 ID、版本和时间戳,复制本次产物到 dist。
- [ ] 在 Android 16 模拟器安装本次 APK 并启动;目视核实两页、滚动末节、周日、大字体、离线重启与登录错误状态,保存关键截图。通知后台/重启、日历设备结果独立记录;不能把单元测试通过当成系统验证。
- [ ] 在验证报告区分“已验证”“需真机验证”“学校页面待联调”,写清没有本人会话时尚未完成真实学校自动导入;提供 isDemo=true 的 JSON 样例字段说明和启动/构建方式,真实数据文件使用 isDemo=false,不展示学校密码。
- [ ] 对照设计逐项自查并完成所选执行方式要求的独立审查;修复可重现问题后,只重跑受影响检查。保存源码、报告与 APK 的可点击入口;提交交付文档,不发布到应用商店。
## 核查来源与执行交接
依赖版本及 Android 配置于 2026-10-12 核查:[WebView 官方包](https://pub.dev/packages/webview_flutter)、[通知插件维护者文档](https://pub.dev/packages/flutter_local_notifications)、[时区包](https://pub.dev/packages/timezone)、[路径插件](https://pub.dev/packages/path_provider)、[文件选择器](https://pub.dev/packages/file_picker)、[Android Calendar Provider](https://developer.android.com/identity/providers/calendar-provider)。执行时以锁定版本的实际 API 为准;若依赖解析或构建证明不兼容,记录证据并作最小必要版本调整。
自查:设计的页面、时间、设置、学校接入边界、数据保存、提醒、日历和设备验收均有对应任务;五项 Review Focus 均分配了测试;副作用失败不改变已保存课表;模块接口名称一致。当前状态:计划已写成并自查,等待用户审核及选择执行方式。
建议由主助手在本聊天逐项实现,最后独立审查:七项任务共享相同模型与控制器接口,顺序实现可以减少交接成本。用户也可选择每项交给独立助手实现和审查的方式;选择后按相应技能执行。