# 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 把它放到组件槽;不是固定层盖在自定义层上,也没有第二个岛窗口。主要行禁用提醒时回落到允许提醒的第一行;全部禁用时不展示提醒槽。多种内容仍服从原队列/列表规则,不会在多个位置重复投递。 ## 自定义灵动岛界面重写 按后续反馈,将组件栏明确收进同一个岛体:普通组件和提醒共用外层背景、圆角与阴影,嵌入提醒只保留原渲染器的内容、动画与操作,去掉原卡片的第二层背景形状。每行的普通组件与提醒上下排布,岛宽取两者中较宽的一项,避免横向累加造成留白。没有消息的通知、作业、叫人组件不占位。固定模式继续使用原有完整卡片。 编辑界面重写为左侧组件库、中央岛内预览、右侧当前对象设置。组件库按日常信息、消息提醒、组合容器分组,采用统一 SVG 线性图标;组件设置分为内容、外观、高级三个标签,独立外观关闭时禁用覆盖项。行名称、间距、主要行和通知开关放在行设置中,整体配色、缩放和多行选项放在整体外观中;方案复制与导入导出收在顶部菜单。 主设置页以两个选项选择经典或自定义灵动岛,自定义模式直接进入「编辑岛内组件」,隐藏不适用的固定宽高等选项。退出全屏编辑时,先在隐藏期间等待常态组件重新测量,避免保留行工具栏的空白高度;自然宽度测量也不再被上一轮获准窗口宽度撑住。分离多行时按实际外层留白计算尺寸。 新增回归检查设置标签隔离、共用岛体且无第二层形状、无消息不占位,以及退出编辑后的自然内容尺寸;关闭提醒通过原渲染器的「知道了」按钮验证。最终截图留档在 `.cache/island-rewrite-final-shots/`。 本轮视觉回归通过 102/102,随后补充的岛宽修正与展开可见性检查通过后台 28/28,并通过二次启动。后台最终几何检查先等待原渲染器的形变兜底和尺寸防抖完成,避免在隐藏窗口的中间动画帧判断最终尺寸。客户端构建、类型检查、lint、改动文件格式检查及文档严格构建均通过。 ## 可选常驻岛 本机 `islandLayout.residentEnabled` 默认关闭,仅在 custom 模式生效。开启后,现有方案 rows 作为收起内容,新增 expandedRows 保存没有通知时点击展开的内容;两套内容在同一编辑器中切换编辑,随方案复制、导入导出和本机配置一起保存。旧配置自动补齐展开布局,不改变原来的常驻选择。 按本次产品要求,常驻模式中普通、紧急和叫人消息都先提示右侧铃铛及去重后的数量,点击常驻内容、铃铛或按 Enter/空格才展开通知列表;上课时允许主动查看。没有消息时展开自定义内容,收起、处理完成及通知撤回后保留常驻岛。经典模式与关闭常驻的自定义模式继续使用原提醒规则。 铃铛固定占位,避免收消息时内容横移。普通组件默认继承行的垂直居中对齐;原先无条件 align-self:stretch 会使不同字号文字从顶部排版,现只在父容器有宽度且明确配置时应用覆盖对齐。时钟采用等宽数字,分隔符保持固定线高,展开页标题与收起按钮在同一基准线上。设置页重新获得焦点时读回本机配置,避免编辑器内改常驻选项后主设置页显示旧值。 新增隔离检查覆盖旧配置兼容、空闲常驻、铃铛与文本对齐、无消息展开/收起、普通与紧急消息点击展示、同 id 去重、课堂中查看、键盘展开、处理完成后的窗口几何、空闲展开时新消息提示、两套内容独立编辑和关闭常驻恢复经典模式。截图留档于 `.cache/resident-island-shots/`。 ### 再次核对 ClassIsland 尺寸与避让设置 2026-10-17 重新从 GitHub master 读取 [经典主题](https://github.com/ClassIsland/ClassIsland/blob/master/ClassIsland/XamlThemes/ClassicTheme/Styles.axaml)、[Fluent 主题](https://github.com/ClassIsland/ClassIsland/blob/master/ClassIsland/XamlThemes/FluentTheme/Styles.axaml)、[默认设置](https://github.com/ClassIsland/ClassIsland/blob/master/ClassIsland/Models/Settings.cs)、[窗口设置页](https://github.com/ClassIsland/ClassIsland/blob/master/ClassIsland/Views/SettingPages/WindowSettingsPage.axaml)、[外观设置页](https://github.com/ClassIsland/ClassIsland/blob/master/ClassIsland/Views/SettingPages/AppearanceSettingsPage.axaml)及 [行样式](https://github.com/ClassIsland/ClassIsland/blob/master/ClassIsland/Controls/MainWindowLine.axaml)。主题行高为 40、内容左右留白 12;四档字体为 14/16/18/20,时钟用强调字号 18;默认圆角 8、背景不透明度 0.5、缩放 1、行距 5。共享 CLASSISLAND_ISLAND_METRICS 作为唯一尺寸与默认值表,常驻行移除上下 12px 留白。缩放、圆角与背景透明度范围也与上游一致。 原生窗口复用现有光标轮询,在常驻背景移入时降低到 5% 不透明度并开启点击穿透,移出恢复;支持反转及关闭。保留独立的右侧交互入口:无消息时是小展开箭头,有消息时显示铃铛与数量。它不触发避让,避免和此前点击查看通知的要求冲突;编辑、展开和触笔/触摸后 1 秒内暂停避让。这是为本产品保留交互入口的明确适配,上游原始主界面默认整体穿透。未引入 RawInput/.NET 依赖,轮询方式对应上游默认 UseRawInput=false。窗口隐藏或淡化不清空消息队列。 补充后台检查实测常驻行 40px、原生 setOpacity 的 5% 比例、点击穿透状态、移出恢复、反转/关闭淡化、右侧入口与触摸宽限期。源码缓存和截图分别留在 `.cache/classisland-current/`、`.cache/resident-island-ci-shots/`。 最终构建视觉验收 118/118,随后后台重启检查 43/43、单实例锁和配置句柄释放通过;全仓类型、lint、构建与产物凭据门禁、格式及文档严格检查通过。最终截图为 `.cache/resident-island-ci-final-shots/`,验收日志为 `.cache/resident-island-ci-visual-final.log`。本次不修改业务数据库、不打包或推送发布。 ## 常驻岛界面与动画重写 按用户反馈重新读取上游 `ClassicTheme/Styles.axaml`、`Controls/MainWindowLine.cs` 与 `Controls/MainWindowLine.axaml`。 上游背景宽度过渡为 550ms、`cubic-bezier(0,1,.5,1.1)`,内容使用 200ms 的 SineEaseIn 淡入;行高 40、左右留白 12。 本产品需要可交互的通知列表和独立空闲展开页,沿用这些度量、背景曲线与内容淡入,展开页高度也使用同一背景曲线。 旧问题由三个因素叠加:内嵌提醒仍播放 WinIsland 胶囊弹簧,外层 ResizeObserver 把动画中间帧当成自然尺寸,渲染端与主进程各延迟 120ms 调整窗口;外壳 `overflow:auto` 在两端不同步时出现滚动条。 新 `ResidentIsland.vue` 独立渲染常驻内容、空闲页和通知列表,保留既有的消息去重、排序、读回执及按钮动作。 经典渲染器保留挂载和心跳,但在常驻模式停止弹簧循环、兜底定时器和命中上报,不再内嵌到常驻岛。 常驻窗口使用受工作区限制的固定画布,开合不调用逐帧 setBounds,也不再隐藏 320ms 后重新显示。 两页内容按各自未变形的 offsetWidth/offsetHeight 测量,保持自己的排版和缩放;只让一个岛壳过渡尺寸。 内容自然尺寸、配置缩放、字体加载和窗口 resize 决定目标尺寸。长文字先限制宽度并省略,时间、分隔线和文字成组对齐;多行布局整体适配可用宽高。 消息正文限制可见行数,按高度限制列表数量,剩余内容由应用内入口承接;底部动作不被正文挤出,运行界面没有滚动容器。 原生窗口的 show/hide 通过专用白名单桥同步到渲染器;不可见时直接应用目标几何,避免后台 CSS 动画冻结在中间帧。 命中矩形仅在动画期间每 60ms 限流上报,结束时补报;静止时没有动画采样循环。 开合期间暂停指针淡化,避免鼠标留在旧展开区域时因过期命中矩形误触发 5% 透明度,造成收起闪烁。 Windows 触摸模式使用 Electron setShape:动画开始放开绘制区域,完成后只保留岛体及 8px 投影留白的原生区域,透明画布不再长期吞掉周围桌面输入。进入编辑、关闭常驻或关闭触摸模式时清除区域。 新增回归覆盖开合窗口矩形恒定、动画帧间隔、所有运行节点无滚动容器、360×260 窄画布与 2.5 倍缩放、底部右侧停靠、长正文及多通知底部动作、触摸输入区域收敛。测试保持隔离配置与本地通知,不连接真实班级。 本轮视觉验收 123/123,通过连续快速开合与停靠命中检查;1.8 秒取得 109 个动画采样,帧间隔 P95 为 16.8ms,原生窗口矩形只有 1 个。 最终补充收起期间错误淡化的回归后,后台检查 49/49;桌面类型检查、全仓 lint、构建及产物凭据门禁、格式和文档严格构建通过。 视觉记录与截图为 `.cache/resident-rewrite-final-visual.json`、`.cache/resident-rewrite-final-shots/`,最终构建后台记录为 `.cache/resident-rewrite-delivery-quick.json`。 ## 十项自检 1. 第三周周三第二节:ClassPlan 的规则与 ClassPlanEntry 的 timeLayoutItemId → `resolveActiveClassPlan` → `loadTermContext/resolveCurrentWeek` → `projectPlan` → `/schedules/active-week` → `refreshIslandSchedule` 的整周 IndexedDB 缓存 → `recomputeClassState` 发送当天课程 → `ComponentNode.vue` 课程表组件。 2. 编辑时间点起止时,所有引用该 TimeLayoutItem 外键的课随之改变;只排序改变 position,不改变外键。旧单节时间编辑会先克隆班级日作息,避免误改共享时间表。 3. 拖到第二行后,保存到 `/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 秒。 ## 常驻岛并入自定义灵动岛(最新行为) 后续产品要求统一两种自定义显示方式,本节取代上文关于可选常驻、铃铛优先与独立通知卡的历史行为。 `layout` 仍为 fixed/custom;custom 直接显示常态组件,`residentEnabled` 只保留为旧配置兼容字段,归一化为 custom 时开启。 设置页不再提供独立的常驻开关。编辑器保留常态 rows 与无通知展开 expandedRows,两套内容随本机方案保存。 自定义运行时统一为 `CustomIslandRuntime.vue`,不再将旧卡片嵌入组件行。 经典岛与自定义岛共同使用 `ClassicNotifications.vue` 和 `classic-island.css`,通知类型、排序、徽标、正文、按钮与已读动作只有一份实现。 纯黑、玻璃、主题色、透明度、字号、圆角、动画开关与速度均来自同一份 island 外观设置;方案保留组件缩放、行距和布局。 自定义组件行、消息正文与通知操作区居中对齐。无通知展开时显示 expandedRows,有通知时显示共用经典通知内容。 自定义模式不再提前截获所有消息并只亮铃铛:与经典岛走相同的 pushNotification/setClassState 状态机。 紧急通知和紧急叫人立即展开,课中普通通知排队、下课后展开;主动点击铃铛仍可查看待处理消息。 收起和处理完通知后恢复自定义常态内容。铃铛固定占位,收到消息不横移。空白桌面输入穿透与触摸区域裁剪继续保留。 外壳使用经典岛同源的 SpringValue 与 squirclePath;宽高和圆角每帧同时推进,保留零过冲钳制、动画速度与后台吸附兜底。 原生画布固定,隐藏渲染器不运行旧形变;统一心跳不随布局切换重复注册。 课表更新链路做了以下修正: - 课表变更事件立即取 active-week,学期接口在旁路校正,避免慢学期请求串行挡住新课表。 - 网络结果先同步到岛,再异步完成缓存写入;请求序号在请求开始前分配,迟到响应和已切换账号的结果不会覆盖当前内容。 - 实时连接建立及重连时立即刷新;周期补偿从五分钟缩至三十秒,课程边界每秒复算。跨午夜即使上课状态未改变,也同步新一天的课程。 - 主进程保留最新日课表快照;窗口启动、重建时补发,渲染器通过受限 getSchedules IPC 读回,登出时清空。 - 自适应同时接收原生窗口尺寸通知,防止隐藏窗口的浏览器 resize 延迟而沿用旧宽度;订阅在组件退出时释放。 验证使用隔离配置、本地消息和替换的本地 API,覆盖慢学期接口、变更事件、重连刷新、迟到请求隔离、课表 IPC 即时更新及窗口重新加载恢复。 桌面视觉回归覆盖经典岛原有状态,以及自定义岛单通知、多通知、常态、空闲展开、窄屏、长文本、触摸和固定原生窗口动画。