diff --git a/CHANGELOG.md b/CHANGELOG.md
index daf26e2..2028986 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,15 @@
## 未发布
+## v0.4.0 — 2026-11-13
+
+- 桌面「呼叫老师」悬浮球不再始终置顶:PPT 放映或最大化窗口会盖住它,回到桌面仍可见;悬浮球加大、改为醒目的橙色和白色粗体大字,确认面板与按钮同步放大。
+- 大屏主界面重新对齐:页眉、内容、页脚使用同一页宽;「呼叫老师」卡片补齐样式;整体字号加大;修复绑定页装饰图压住步骤文字、「READY TO CONNECT」上方多出横线的问题。
+- 网络稳定性:教师网页实时连接随心跳续期令牌,不再每 15 分钟断开;重连后补齐所有数据。大屏连接 10 秒无响应即重试,网络恢复时立即重连。
+- 服务端:登录、注册、改密的密码哈希移出全局锁并限制并发;不存在的账号与错误密码耗时一致。通知列表改用「时间 + ID」分页,同一时刻的记录不再漏掉。撤回通知不再把「失败」「过期」改写为「已关闭」;解绑屏幕时未完成的投递立即标记失败。教师建班校验年级长度。发送过快的提示按实际冷却秒数显示。
+- 数据量与性能:学生呼叫保留 90 天、审计日志 180 天,已用或过期的协作邀请自动清理;写入时按行编码、失败时从上次提交还原,每次写操作少一次整库序列化。
+- 清理:移除 0.1.3 之前的共享刷新 Cookie 兼容与已废弃的会话字段。**从 0.1.2 及更早版本直接升级的浏览器需重新登录一次。**
+
## v0.3.5 — 2026-11-13
- 新增学生呼叫:Windows 桌面悬浮入口二次确认后呼叫本班全部关联教师;教师工作台新增「学生呼叫」、未读红色数量、在线实时提醒和手动共享已读,保留处理老师与时间。记录随服务持久化及备份,支持离线后补查;同班 30 秒防重复呼叫。不使用 Web Push,Windows 系统集成仍需实机验收。
diff --git a/api/openapi.yaml b/api/openapi.yaml
index 7cedb24..5d7183b 100644
--- a/api/openapi.yaml
+++ b/api/openapi.yaml
@@ -2,7 +2,7 @@
"openapi": "3.1.0",
"info": {
"title": "课堂叮咚 API",
- "version": "0.3.5"
+ "version": "0.4.0"
},
"servers": [
{
diff --git a/api/ws/frame.schema.json b/api/ws/frame.schema.json
index f8279f4..6b150c4 100644
--- a/api/ws/frame.schema.json
+++ b/api/ws/frame.schema.json
@@ -611,7 +611,11 @@
},
"d": {
"type": "object",
- "properties": {},
+ "properties": {
+ "token": {
+ "type": "string"
+ }
+ },
"required": []
}
},
diff --git a/deploy/.env.example b/deploy/.env.example
index 5511347..c5d84a2 100644
--- a/deploy/.env.example
+++ b/deploy/.env.example
@@ -1,5 +1,5 @@
DOMAIN=classding.example.com
-CLASSDING_IMAGE=ghcr.io/alumos/classding-server:v0.3.5
+CLASSDING_IMAGE=ghcr.io/alumos/classding-server:v0.4.0
# openssl rand -hex 32
JWT_SECRET=
# 仅首次部署设置;创建成功后清空用户名/密码并重新创建容器
diff --git a/deploy/compose.1panel.yaml b/deploy/compose.1panel.yaml
index 669e3b0..ffcf119 100644
--- a/deploy/compose.1panel.yaml
+++ b/deploy/compose.1panel.yaml
@@ -1,7 +1,7 @@
# 粘贴到 1Panel 的编排编辑器。首次部署请填写两项密码;升级保留原值和原编排名称。
services:
classding:
- image: ghcr.io/alumos/classding-server:0.3.5
+ image: ghcr.io/alumos/classding-server:0.4.0
platform: linux/amd64
restart: unless-stopped
ports:
diff --git a/deploy/compose.yaml b/deploy/compose.yaml
index 1269a89..c385cb2 100644
--- a/deploy/compose.yaml
+++ b/deploy/compose.yaml
@@ -1,6 +1,6 @@
services:
server:
- image: ${CLASSDING_IMAGE:-ghcr.io/alumos/classding-server:v0.3.5}
+ image: ${CLASSDING_IMAGE:-ghcr.io/alumos/classding-server:v0.4.0}
restart: unless-stopped
environment:
DATA_DIR: /data
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 385b817..f50bb40 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -18,7 +18,7 @@
## 主服务(`server/internal/app`)
-整个数据库以 `State` 的形式常驻内存,由 `App.mu` 保护;每次成功的写操作后,`repo.Store` 只把变化的行写回 SQLite。
+整个数据库以 `State` 的形式常驻内存,由 `App.mu` 保护;每次成功的写操作后,`State.rows()` 逐行编码,`repo.Store` 只把变化的行写回 SQLite。命令失败或保存失败时,内存状态由 `repo.Store.Restore` 从最近一次提交还原,成功路径不再整份拍快照。
| 文件 | 职责 |
| --- | --- |
@@ -27,10 +27,12 @@
| `routes.go` | 全部路由,一行一个接口,标明访问级别与读写 |
| `app.go` | 启动、旧数据升级、限流、审计、会话撤销 |
| `auth.go`、`security.go`、`validate.go` | 登录与刷新会话、令牌与密码哈希、输入校验 |
+| `passwords.go` | Argon2id 在取得 `App.mu` 之前计算(并发上限受控),登录、注册、改密不阻塞其他请求;不存在的账号也做同样耗时的校验 |
+| `state.go`、`cursor.go` | 按表逐行编码状态;分页游标(创建时间 + ID,同一时刻的记录不漏不重) |
| `classes.go`、`devices.go`、`messages.go`、`homework.go`、`student_calls.go`、`audio.go`、`tools.go` | 各业务领域 |
| `admin.go`、`settings.go`、`backup.go` | 管理后台、站点设置、备份与恢复 |
| `realtime.go`、`hub.go` | WebSocket 协议与连接池 |
-| `maintenance.go` | 定时清理过期会话、消息和录音 |
+| `maintenance.go` | 定时清理:过期会话;消息与学生呼叫保留 90 天、审计 180 天、录音 7 天;已用或过期的协作邀请 |
| `migration.go` | 0.3.0 之前的大屏迁移核对接口 |
几条约定:
@@ -49,9 +51,12 @@
| `storage.go` | 以 Windows DPAPI 加密保存前端状态 |
| `displays.go`、`volume.go`、`network.go` | 显示器选择、播报时的系统音量、内网 IPv4 |
| `homework*.go` | 桌面作业卡片(Win32 GDI 绘制) |
+| `student_call*.go` | 桌面「呼叫老师」悬浮球:普通窗口层级(不置顶,PPT 放映或最大化窗口会盖住它),点击后才提到前台展开二次确认 |
| `frontend/src/service.ts` | 连接、离线队列、播报、服务器切换与回退的状态机 |
-`service.ts` 中所有状态修改都经 `mutate()` 串行执行并立即持久化,失败时整体回滚。每次重连使 `generation` 加一,旧连接上迟到的消息会被丢弃。
+`service.ts` 中所有状态修改都经 `mutate()` 串行执行并立即持久化,失败时整体回滚。每次重连使 `generation` 加一,旧连接上迟到的消息会被丢弃。连接 10 秒内未收到欢迎帧即放弃重试;系统报告网络恢复(`online` 事件)时立即重连,不等退避。
+
+教师网页的实时连接每 25 秒心跳时附带当前访问令牌,令牌临近过期先刷新,服务端据此续期,连接不再每 15 分钟断开一次;重连后同时刷新作业、消息、班级、设备和学生呼叫。
## 网页端
diff --git a/docs/INTEGRATION.md b/docs/INTEGRATION.md
index df28ae4..199516e 100644
--- a/docs/INTEGRATION.md
+++ b/docs/INTEGRATION.md
@@ -14,7 +14,7 @@
- AuthResponse: {access_token,user};refresh token 放 HttpOnly Cookie。
## REST
-GET config => {speech_enabled:false,registration_enabled:true,version:'0.3.5'}
+GET config => {speech_enabled:false,registration_enabled:true,version:'0.4.0'}
POST auth/register {username,password,display_name} => AuthResponse
POST auth/login {username,password} => AuthResponse
POST auth/refresh => AuthResponse;POST auth/logout => {ok:true}
@@ -33,7 +33,7 @@ GET device/me => {device,class:{id,name},volume,sequence,homeworks,homework_revi
GET device/homeworks => {items:Homework[],homework_revision},设备 Bearer 令牌认证。
GET homeworks?class_id= => {items:Homework[]};POST homeworks {class_id,subject,text,visible,always_on_top} => Homework;PATCH homeworks/:id {subject?,text?,visible?,always_on_top?} => Homework;DELETE homeworks/:id => {ok:true}。
POST messages {class_ids,kind,text,student_name,signature,style,display_sec,repeat,chime,play_mode,audio_id?} => Message;repeat 为朗读或播放遍数:文字 0–3(0 只显示不朗读),原声 1–3
-GET messages?class_id=&cursor= => {items:Message[],next_cursor:string|null}
+GET messages?class_id=&cursor= => {items:Message[],next_cursor:string|null}(cursor 原样传回上一页的 next_cursor,按创建时间 + ID 倒序,同一时刻不漏不重)
GET messages/:id => Message;POST messages/:id/recall => Message;POST messages/:id/resend => Message
GET phrases => Phrase[];POST phrases {text} => Phrase;PATCH phrases/:id {text} => Phrase;DELETE => {ok:true}
POST audio multipart file => {audio_id,url,transcript?}
diff --git a/docs/LOCAL_TESTING.md b/docs/LOCAL_TESTING.md
index 4101db5..24394dd 100644
--- a/docs/LOCAL_TESTING.md
+++ b/docs/LOCAL_TESTING.md
@@ -30,7 +30,7 @@
2. 两位教师在线打开工作台时均应看到「学生呼叫」红色未读数量和提醒;进入页面不消除未读。任一教师手动标记已读后,两端同步消除该条未读,记录保留处理老师和时间。
3. 另一教师关闭页面期间发起呼叫,重新登录后仍可查看未读;重启服务、刷新网页后记录和已读状态保留。未加入班级的账号不得读取或处理该班记录。
4. 30 秒内新呼叫应提示稍后再试;断网不得显示发送成功,重试同一请求不产生重复记录。解绑后悬浮入口不可发送。
-5. 在真实 Windows 上检查主窗口隐藏/托盘驻留、悬浮球拖动、多屏工作区与 DPI 缩放、确认/取消、发送中防重复、全屏通知期间隐藏及恢复。浏览器兜底入口和交叉编译不能替代这项实机验收。
+5. 在真实 Windows 上检查主窗口隐藏/托盘驻留、悬浮球不置顶(PPT 放映时被覆盖,回到桌面可见)、悬浮球拖动、多屏工作区与 DPI 缩放、确认/取消、发送中防重复、全屏通知期间隐藏及恢复。浏览器兜底入口和交叉编译不能替代这项实机验收。
6. 手机 320/390px、桌面及 150% 字号检查六项导航和未读徽标,不应溢出或遮挡操作。
## 已自动验证与待验证
diff --git a/docs/releases/v0.4.0.md b/docs/releases/v0.4.0.md
new file mode 100644
index 0000000..9065121
--- /dev/null
+++ b/docs/releases/v0.4.0.md
@@ -0,0 +1,39 @@
+# 课堂叮咚 v0.4.0
+
+一次以稳定性和体验为主的大版本:桌面呼叫悬浮球不再挡住 PPT,大屏界面重新对齐并放大字号,教师端实时连接更稳,服务端更省资源。
+
+## 大屏客户端
+
+- **悬浮球不再始终置顶**:「呼叫老师」悬浮球处于普通窗口层级。PPT 放映、最大化的窗口会正常盖住它;回到桌面或在普通窗口旁仍可看到和点击。只有点击后展开的确认面板会提到最前。
+- **更大更醒目**:悬浮球加大到 112×92(按缩放比例放大),橙色底、深橙描边、白色粗体大字「呼叫 / 老师」并居中;确认面板、标题、正文和按钮都同步加大。
+- **界面对齐**:页眉、内容和页脚使用同一页宽,左右边缘对齐;「呼叫老师」卡片补齐样式并与悬浮球同色;正文、设置项、按钮字号整体加大;修复绑定页铃铛装饰压住步骤文字、卡片小标题上方多出横线的问题。
+- **断网恢复更快**:连接 10 秒无响应会放弃并重试,不再长时间停在「正在连接」;Windows 报告网络恢复时立即重连。
+
+## 教师与管理端
+
+- 实时连接在心跳中续期登录令牌,不再每 15 分钟断开一次;重连后同时刷新作业、发送记录、班级、设备和学生呼叫。
+- 发送记录分页改为「时间 + ID」,同一时刻发出的多条通知不会被漏掉。
+- 撤回通知时,已经「失败」或「过期」的投递保留原状态。
+
+## 服务端
+
+- 密码哈希在全局锁外计算并限制并发,登录高峰不再拖慢大屏回执和其他请求;不存在的账号与密码错误耗时一致。
+- 学生呼叫保留 90 天、审计日志保留 180 天,已用或过期的协作邀请自动清理,数据库不再无限增长。
+- 写入按行编码、失败时从上次提交还原,每次写操作少一次整库序列化。
+- 解绑屏幕时,未完成的投递立即标记为失败;教师建班时校验年级长度(最多 40 字);发送过快的提示显示实际冷却秒数。
+
+## 升级
+
+Linux amd64 镜像:`ghcr.io/alumos/classding-server:0.4.0`。保留现有编排和数据卷,仅更新镜像后重建。旧数据库和旧版备份可直接使用;首次运行的定时清理会删除超出保留期的学生呼叫、审计记录和失效邀请。
+
+Windows amd64 安装包:`ClassDingScreen-amd64-installer.exe`。悬浮球与界面调整需要**大屏升级到 v0.4.0**。
+
+## 不兼容变更
+
+- 移除了 0.1.3 之前共用的刷新 Cookie(`classding_refresh`)。只有从 0.1.2 及更早版本直接升级、且浏览器仍持有旧 Cookie 的用户需要重新登录一次。
+- 发送记录的分页游标改为不透明字符串;直接调用接口的脚本应原样传回 `next_cursor`,不要自行构造时间戳。
+
+## 已知限制
+
+- 悬浮球层级、字号与界面对齐需在真实 Windows 设备(不同缩放比例、多显示器、PPT 放映)上验收。
+- 学生呼叫仍只在教师网页打开时提醒,不使用 Web Push。
diff --git a/package.json b/package.json
index 0d0f050..f263fd1 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "classding",
"private": true,
- "version": "0.3.5",
+ "version": "0.4.0",
"type": "module",
"packageManager": "pnpm@11.25.0",
"engines": {
diff --git a/screen/README.md b/screen/README.md
index 60bba12..6fa2b2f 100644
--- a/screen/README.md
+++ b/screen/README.md
@@ -52,7 +52,7 @@ wails build -platform windows/amd64 -nsis -installscope user
## 学生呼叫老师
-绑定后 Windows 桌面左侧会显示「呼叫老师」原生悬浮入口;主窗口关闭到托盘后仍可使用,不依赖网页中的浮层。拖动可移到其他显示器;松手停在最近显示器工作区内,避开任务栏,按显示器 DPI 调整大小。悬浮入口平时不抢焦点,只有点击展开二次确认时才激活;取消或 Escape 不发送,默认键盘焦点在取消按钮,Tab 可切换到确认按钮。全屏通知、课堂工具展示期间临时隐藏,结束后恢复,不移动或改变已有作业窗。
+绑定后 Windows 桌面左侧会显示橙色「呼叫老师」原生悬浮球(白色粗体大字)。它处在普通窗口层级,不会一直置顶:PPT 放映、最大化窗口会正常盖住它,回到桌面或其他窗口旁边时仍可见;主窗口关闭到托盘后仍可使用,不依赖网页中的浮层。拖动可移到其他显示器;松手停在最近显示器工作区内,避开任务栏,按显示器 DPI 调整大小。悬浮入口平时不抢焦点,只有点击展开二次确认时才激活;取消或 Escape 不发送,默认键盘焦点在取消按钮,Tab 可切换到确认按钮。全屏通知、课堂工具展示期间临时隐藏,结束后恢复,不移动或改变已有作业窗。
确认后由原有 WebView service 复用设备认证和 loopback 代理发送 `POST /api/v1/device/student-calls`,Go 原生层不保存另一份密钥。发送中禁止重复提交;成功须收到有效的服务器记录才显示「呼叫已发送」。本班所有关联老师在打开的教师网页收到提醒,任一老师标记已读后共享为已处理。服务端按同班 30 秒限流,客户端显示 `Retry-After` 等待时间。网络异常或响应丢失会显示结果未确认,用户主动重试沿用同一个幂等编号;不会离线排队或自动补发,退出程序后不保留未确认编号。
diff --git a/screen/frontend/package.json b/screen/frontend/package.json
index 9d0af0f..7af1e76 100644
--- a/screen/frontend/package.json
+++ b/screen/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "@classding/screen",
- "version": "0.3.5",
+ "version": "0.4.0",
"private": true,
"type": "module",
"scripts": {
diff --git a/screen/frontend/src/main.tsx b/screen/frontend/src/main.tsx
index 50ebe59..c8f0705 100644
--- a/screen/frontend/src/main.tsx
+++ b/screen/frontend/src/main.tsx
@@ -250,7 +250,7 @@ function App() {
-
READY TO CONNECT
+
READY TO CONNECT
绑定到班级
打开教师端「班级」,获取 6 位绑定码
@@ -373,16 +373,25 @@ function App() {
- 需要老师来教室?
- 向本班所有关联老师发送呼叫。Windows 关闭主窗口后仍可使用桌面悬浮入口。
+
+
+
+
+
需要老师来教室?
+
向本班所有关联老师发送呼叫。关闭主窗口后,桌面上的橙色悬浮球仍可呼叫。
+ {state.studentCallMessage && (
+
+ {state.studentCallMessage}
+
+ )}
+
- {state.studentCallMessage && {state.studentCallMessage}
}