Files
ClassHelper/docs/dev/20261017-implementation.md
T

75 lines
9.9 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.
# 2026-10-17 实施与验收记录
## 阶段
1. 数据与共享契约:关系化时间点、日课表、群组、预定层;权限范围与内置组;周数设置。
2. 服务端:唯一课表解析器与兼容门面、事务编辑;转出过滤、聚合成绩、自动升级、下载状态。
3. Web:时间表/日课表编辑、只读名单、日期归档、设置与进度。
4. 客户端:心跳重置、立即远控(冒烟假执行)、更新进度、本机组件方案与编辑器。
5. 验证与文档:临时数据库验证、静态检查、后台冒烟;明确记录未通过与未执行项目。
## 架构选择
- 不引入 Profile:共享时间表与班级日课表替代单机档案。
- 旧 Schedule 保留迁移来源,通过兼容门面投影新模型;迁移保留起止周、单双周与地点,无法从旧数据恢复原来的群组、临时层或时间点名称。
- 范围保存在 PermissionGroup,多个分组取范围并集;部用逗号分隔的年级字符串,空范围不放行。
- 周次上限取 AppSetting,默认 30、允许 1–60;离线取默认。
- 岛方案保存在本机 config.json;天气无离线数据源,本轮不做。
- 自动升级以年份作事务幂等键,升级前存全库快照;新学年保留周数上限、开学日重置为当年 9 月 1 日,清除旧逐周区间。新班不复制任课关系。
## 实测记录
- `pnpm lint`:0 error、0 warning;全仓类型检查、完整构建、图标门禁与客户端产物凭据检查通过。
- `pnpm verify:e2e`:243/243。根入口改为全新临时 SQLite 库与带前缀账号,禁止外网;不会连到本机真实服务。
- `pnpm verify:v14`:35/35。覆盖四周轮换、四种时间点、外键排序与下一节时间、跨班课程、换课不改原表、预定优先级、集体成绩与实时隐私、转出学生的名单/成绩/叫人/未交入口、四岗位越权、41 周生成、兼容 CRUD、上游导入幂等、自启动假适配器。年度升级另在专用临时库并发运行两次,确认同年幂等、快照存在、其余班只升一级、新班无班主任与任课关系。
- `pnpm verify:web`:43/43。包含新增日课表整表保存、时间表编辑入口、自启动不可用原因、升级设置与转出分区;全程隐藏、无焦点。
- 桌面快速检查 24/24;视觉检查 99/99,并通过退出后的第二次快速启动 24/24。均使用独立 userData,远控与安装器不会执行系统操作。
- 文档站严格构建通过,0 warning、0 error。
- 原库迁移前备份:`.cache/dev-before-v14-20261017092353.db`。先在副本验证,再应用本机迁移。用户 3、班级 5、学生 18、课程 53、旧课表 89、成绩 98、作业 16、通知 23 的行数全部保留;89 条旧课表均有关系化映射,生成 450 张按周日课表,外键检查 0 异常。历史 JSON 时间表的 3 个时间点也全部补齐。
- 视觉截图:`.cache/v14-island-shots/`(固定模式各形态、窗口全图、`custom-editor.png`、`custom-alert.png`);Web 手机截图:`.cache/v14-web-shots/`。它们是本次验收留档,不作为官网素材。
- 未打包、未改版本、未改 releases;未真实修改注册表/systemd,未在业务库或演示库执行年度升级,未真实执行关机/重启/注销/锁屏。自启动安装形态使用可注入的系统适配器检查开关与重建控制器后回读。
## 两处 bug 的定位
### 换服务端后迟迟不上报
旧 `client-agent.ts` 在收到 401/403 后把唯一计时器排到五分钟后,配置保存/登录成功没有事件重排,旧 token 又可能继续保留。用两个本机假服务端复现:第一个明确返回 401,切换第二个后必须清空旧 token,登录新 token 后自动恢复心跳。修复为四个显式配置事件、状态清空与 epoch 隔离;迟到响应不能覆盖新实例状态。设备 key 不重建,命令回执固定发回接收命令时的地址。
### 自启动入口与状态不一致
旧 Web 没有调用现有自启动 API 的可达设置页。Windows 服务端还从 `dirname(process.execPath)` 找 `start.cmd`,安装包里的 Node 在 runtime 子目录,而安装器把脚本放在安装根,导致错误地识别为开发环境。旧状态只检查 Run 项存在,没有核对值。现在从服务端目录定位安装根,并校验安装器的 InstallDir,比较完整 Run 值;写入后重新读取并校验。开发/免安装/容器环境明确返回不可用原因。Web 实测开发环境按钮禁用且原因可见,安装形态开关与回读由假系统适配器验证。
## 迁移与兼容的取舍
- 采用新模型投影的兼容门面,不双写业务表。旧 Schedule 与 TimeLayout.items 只作迁移审计来源,后续编辑不会再读取它们复活被删除的课程。种子初始化另有一次物化过程。
- 旧行按实际教学周展开,因此不同周次范围可以共存;同班同日、同周、完全相同起止时间的旧冲突记录只取最小 id。旧数据没有的群组/临时层/时间点名称无法恢复;冲突原行保留可追溯。
- 老时间配置导入同样写关系化时间点,并要求 timetable:manage;同起点但不同类型的课间和课程保留独立外键。整套 ClassIsland 课表导入仍可由本班班主任使用,只建立班级自有作息,不把 TeacherName 当成任课授权。
- 上游导入直接保留 1–9 周三元组、四种时间点、空课、停用、换课标记、群组、预定和临时选择,不经单双周中间格式。稳定导入 id 使 merge 重复导入不复制整套课表。
- 全校周数默认 30、上限 60,由 shared 取值函数统一归一化;旧 MAX_TERM_WEEK 仅保留为弃用的外部兼容导出,内部不再使用。
- 不引入 Profile、天气、.NET 或插件依赖;部使用 PermissionGroup.scopeValue 的逗号分隔年级,空范围不放行。范围并集不会替代 Course.teacherId 的科目写权限。
## 对两项架构判断的答复
本机方案存 config.json 的决定予以保留:这是设备个体偏好,不增加服务端配置或同步通道。通知/作业/叫人使用同一个 IslandApp.vue 实例及原状态机,custom 模式仅通过 Teleport 把它放到组件槽;不是固定层盖在自定义层上,也没有第二个岛窗口。主要行禁用提醒时回落到允许提醒的第一行;全部禁用时不展示提醒槽。多种内容仍服从原队列/列表规则,不会在多个位置重复投递。
## 十项自检
1. 第三周周三第二节:ClassPlan 的规则与 ClassPlanEntry 的 timeLayoutItemId → `resolveActiveClassPlan` → `loadTermContext/resolveCurrentWeek` → `projectPlan` → `/schedules/active-week` → `refreshIslandSchedule` 的整周 IndexedDB 缓存 → `recomputeClassState` 发送当天课程 → `ComponentNode.vue` 课程表组件。
2. 编辑时间点起止时,所有引用该 TimeLayoutItem 外键的课随之改变;只排序改变 position,不改变外键。旧单节时间编辑会先克隆班级日作息,避免误改共享时间表。
3. 拖到第二行后,保存到 `<userData>/config.json` 的 `islandLayout.schemes[].rows[].nodes`;仍由 config.ts 的内存缓存、120ms 合并写盘和退出 flush 管理,重启经 getConfig/白名单桥读回。
4. 新提醒优先进入 main 且 allowNotifications 的行,否则第一条允许提醒的行;在该行对应内容组件的槽展示,没有对应组件时使用该行默认提醒槽。
5. fixed 不挂载 CustomIsland,主进程 reportCustomSize 也先检查 customLayout;原固定窗口、形变路径和默认外观值保留。
6. 关闭个人查询后 overview 直接返回聚合 DTO:没有学号、姓名、个人 id、排名或极值;个人明细/排名接口被服务端拒绝。实时成绩广播同样只发 classId 与 collective 刷新标记,避免经 Socket.IO 绕过 HTTP 闸门。少于五人隐藏均值,均值取整,小于三条的分段及可反推补集的比率不披露;前端也清空已有个人数据。
7. 8 月 3 日启动时识别最近应执行学年缺记录,写警告并在学期周次页显示预览/补跑;不会静默升级业务库。运行中的服务跨过计划时间才自动执行。年份唯一键与事务保证重入只提交一次,执行前先完成快照。
8. 一处 activeStudentWhere/isActiveStudent 规则,应用于名单/班级详情/成绩/作业状态/叫人/通知已读/导入匹配/班级会话与 AI 叫人选择。历史归档/转出专页显式读取历史状态。漏一个入口会造成历史身份重新被选中或历史个人成绩外露,因此回归有服务端负断言。
9. 级长的班级列表与接口断言先经 access.ts → permission-scope.ts 取 GRADE/DIVISION 范围,越界接口返回 403;前端菜单和班级筛选只用于展示。科目内容仍由任课关系独立裁决。
10. 地址变化 → server-changed 清 token/失败/退避与待排计时器 → token-changed/login 立即排心跳 → 新服务器鉴权成功后按稳定 deviceKey 登记 → lastHeartbeatAt 写入并进入在线列表。epoch 丢弃旧请求的迟到结果。
## 验收中修复的边界
- 兼容投影保留单双周元数据;清理测试导入时删除整张测试日课表,空课表不会被隐式回退。
- Windows libsql 同库连接探测复用当前活跃连接;此前完整回归在跨适配器探测处出现 0xC0000005,修复后完整离线回归通过。临时库目录额外带时间戳,避免进程号复用撞到此前的验收库。
- 新旧课表界面与岛桥接共用的缓存仍保存整周数据,岛组件只接收当天投影,避免当天响应覆盖整周离线缓存。
- 最终复跑曾遇到本机冷启动超时:桌面第一次界面无响应,原样重跑在已通过的检查之后被旧 45 秒总限时中止;临时后端也超过了原 10 秒就绪等待。外层预算分别调整为 90 秒与 60 秒,心跳 30 秒等单项业务断言不变。
调整后最终复跑通过后端 243/243 与桌面 24/24(含二次启动),桌面整套实际耗时 20.6 秒。