diff --git a/README.md b/README.md index 94e5e3f..e4df516 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 电子科技大学研究生课表 APP。采用 Flutter,优先 Android,后续支持 iOS。 -当前阶段:配置 Android 开发环境并审核首版设计;尚未生成业务代码或 APP 安装包。 +当前阶段:Android 开发环境已配置,首版设计文件已确认,实现计划待审核;尚未生成业务代码或 APP 安装包。 ## 开发环境 @@ -44,4 +44,4 @@ emulator -avd hepan_everyday ## 首版设计 -见 [首版设计文件](docs/superpowers/specs/2026-10-12-hepan-everyday-design.md)。聊天中的方案已确认,完整设计文件待审核。学校真实课表解析需要在用户本人认证后的页面上验证。 +见 [已确认的首版设计](docs/superpowers/specs/2026-10-12-hepan-everyday-design.md)与[实现计划](docs/superpowers/plans/2026-10-12-hepan-everyday-android.md)。学校真实课表解析需要在用户本人认证后的页面上验证。 diff --git a/docs/superpowers/plans/2026-10-12-hepan-everyday-android.md b/docs/superpowers/plans/2026-10-12-hepan-everyday-android.md new file mode 100644 index 0000000..0644c36 --- /dev/null +++ b/docs/superpowers/plans/2026-10-12-hepan-everyday-android.md @@ -0,0 +1,221 @@ +# 河畔 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 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 courses, Semester semester) -> List`,按开始时间/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 courses, ImportMetadata? importMetadata, Map calendarEventIds=const {}})`;schemaVersion=1,不包含密码/Cookie/认证令牌。 +- `SnapshotStore.load() -> Future`,`SnapshotStore.save(AppSnapshot snapshot) -> Future`;`FileSnapshotStore(Directory directory)`,使用 path_provider Application Support 的目录。 +- `ImportDraft({required List 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 occurrences, AppSettings settings, DateTime now) -> Future`,`requestPermissions() -> Future`、`cancelOwned() -> Future`;reconcile 仅检查权限,不自行弹窗。`ReminderResult` 的 status 为 off/permissionDenied/exact/inexact/error,包含 scheduledCount 与说明。 +- `CalendarService.requestAccess() -> Future`,`listWritableCalendars() -> Future>`,`sync(List occurrences, String calendarId, Map knownEventIds) -> Future>`,`deleteOwned(Map eventIds) -> Future`;`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? courses})`、`leaveDemo()`、`updateSettings(AppSettings value)`、`applyImport(ImportDraft draft,{required bool confirmedEmpty})`、`clearCourses()`、`disableCalendar({required bool deleteEvents})`、`enableRemindersFromUserAction()` 均返回 Future。示例有单独临时数据和“示例”学期,以 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 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`、`logout() -> Future`。`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`、`pendingIds() -> Future>`、`schedule(ReminderRequest value) -> Future`、`cancel(int id) -> Future`。`ReminderRequest(id, occurrenceKey, fireAt, title, body, exact)`;`ReminderPermissions(notifications,exactAlarm)`。`notificationId(String occurrenceKey) -> int` 使用固定算法并显式解决本次集合碰撞,不能依赖 Dart hashCode。 + +- [ ] 写失败测试: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'), notificationId('math/2026-09-09')); + expect(notificationId('math/2026-09-09'), isNonNegative); +}); +``` + +- [ ] 运行 `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。 + expect(await calendar.requestAccess(), isFalse); + expect(controller.snapshot.settings.calendarEnabled, isFalse); +}); +``` + +- [ ] 运行 `flutter test test/services/calendar_service_test.dart` 确认失败;实现 READ_CALENDAR/WRITE_CALENDAR 运行时权限,选日历后才开启设置;写入 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 均分配了测试;副作用失败不改变已保存课表;模块接口名称一致。当前状态:计划已写成并自查,等待用户审核及选择执行方式。 + +建议由主助手在本聊天逐项实现,最后独立审查:七项任务共享相同模型与控制器接口,顺序实现可以减少交接成本。用户也可选择每项交给独立助手实现和审查的方式;选择后按相应技能执行。 diff --git a/docs/superpowers/specs/2026-10-12-hepan-everyday-design.md b/docs/superpowers/specs/2026-10-12-hepan-everyday-design.md index 7c14d16..abda2fa 100644 --- a/docs/superpowers/specs/2026-10-12-hepan-everyday-design.md +++ b/docs/superpowers/specs/2026-10-12-hepan-everyday-design.md @@ -1,6 +1,6 @@ # 河畔 Everyday 首版设计 -日期:2026-10-12。用户已确认聊天中展示的设计,并指定优先配置 Android;此文件待用户审核。采用 Flutter,为后续 iOS 保留共用界面与业务代码。首轮交付 Android 可安装测试包,iOS 打包另行安排。 +日期:2026-10-12。用户已确认聊天中展示的设计,并指定优先配置 Android;用户随后明确确认了此设计文件。采用 Flutter,为后续 iOS 保留共用界面与业务代码。首轮交付 Android 可安装测试包,iOS 打包另行安排。 ## 目标与范围