docs: confirm design and add Android implementation plan
This commit is contained in:
@@ -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<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})`、`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` 使用固定算法并显式解决本次集合碰撞,不能依赖 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 均分配了测试;副作用失败不改变已保存课表;模块接口名称一致。当前状态:计划已写成并自查,等待用户审核及选择执行方式。
|
||||
|
||||
建议由主助手在本聊天逐项实现,最后独立审查:七项任务共享相同模型与控制器接口,顺序实现可以减少交接成本。用户也可选择每项交给独立助手实现和审查的方式;选择后按相应技能执行。
|
||||
@@ -1,6 +1,6 @@
|
||||
# 河畔 Everyday 首版设计
|
||||
|
||||
日期:2026-10-12。用户已确认聊天中展示的设计,并指定优先配置 Android;此文件待用户审核。采用 Flutter,为后续 iOS 保留共用界面与业务代码。首轮交付 Android 可安装测试包,iOS 打包另行安排。
|
||||
日期:2026-10-12。用户已确认聊天中展示的设计,并指定优先配置 Android;用户随后明确确认了此设计文件。采用 Flutter,为后续 iOS 保留共用界面与业务代码。首轮交付 Android 可安装测试包,iOS 打包另行安排。
|
||||
|
||||
## 目标与范围
|
||||
|
||||
|
||||
Reference in New Issue
Block a user