上一篇写到教务系统「能用了」:门户、三角色、选课压测、成绩推送。但拿它对照学校在用的正方教务逐项打勾时,六大模块里有整整一个没做——毕业设计;而监考编排、学业预警、证明打印这些「真教务标配」也只做了半截。于是有了这篇续篇:一口气补齐,目标是让人随便点开哪个页面,都像那么回事。
课堂签到:这次不做花架子
最先做的是全项目最出效果的功能——课堂签到,因为它要把 WebSocket 推送从「通知」推进到「互动」。
流程设计成这样:
- 教师在课程卡片点「发起签到」→ 弹窗显示一个超大发光的 6 位签到码(投影用),配全班名单实时刷新;
- 后端向该教学班全部选课学生的 WebSocket 通道推送「签到开始」;
- 在线学生的工作台顶部立刻弹出琥珀色横幅(带闪烁脉冲点),输入投影上的 6 位码完成签到;
- 教师端名单里那个学生实时从「未签到」变成「✓ 已签到」,进度条跟着走。
有个刻意的设计:签到码只投影、不推送。推送里只带任务号和课程名,码必须学生抬头看屏幕手输——这样「人在床上让室友代签」在语义上就不成立了。加上 5 分钟限时、一人一次、同班同时只允许一个活动任务,一个演示级签到该有的边界就都齐了。
双浏览器实测这一步很有意思:一个浏览器开学生工作台,另一个开教师端点「发起签到」,学生屏幕上的横幅同一秒弹出来。这种「两个浏览器互相说话」的瞬间,比任何功能清单都有说服力。
顺便把整个实时通道做了协议化:原来是单一的成绩推送,现在是三种消息类型(score-published / notice-published / checkin-start)走同一条 WS 连接,掉线 8 秒自动重连,离线错过的由消息中心未读兜底。
毕业设计:把唯一没做的模块补上
正方教务的六大模块对照下来,唯一完全没碰的就是毕业设计。而按学期设定,2023 级本学期正好大四上——时机严丝合缝。
三端一次做完:
- 学生端「课题超市」:按方向筛选、看名额余量、点「选择此课题」;已选题后变成进度页——四阶段步骤条(待开题 → 开题通过 → 中期检查通过 → 答辩通过)加一条指导教师动态时间线;
- 教师端:出题、设名额、截止日期,查看选题名单,按序推进阶段(想从「待开题」直接跳「答辩通过」会被拒绝)并留下推进意见;
- 管理端:全校课题统计(名额利用率、方向分布、阶段分布)和强制退选。
数据层的两个小心思:thesis_selection 对 student_id 做了唯一约束,一人一题写进了数据库而不是业务判断;选题扣名额用的是条件更新(selected_count + 1 WHERE selected_count < quota),和选课库存同款防并发思路。
消息中心实时化 + 两条审批流
申请审核原来是「学生交上去 → 管理员处理 → 学生自己来看」,现在补上了中间的「通知」环节:管理员点通过/驳回,结果实时弹到学生屏幕,离线则进未读角标。
顺着这条线又补了两条审批流:学生在考试安排页对某场考试一键申请缓考;教师在课程页发起调停课申请,管理端多了一个审核 Tab,结果同样进教师消息中心。审批这种事,闭环差一步体验就差一截。
证明打印:双语成绩单与一次打印 CSS 的坑
个人成绩单和在读证明做了正式单据样式:校名抬头、双线分隔、学籍信息表、编号(JW-TRANSCRIPT-2026-00001)、电子章落款,每类限打 3 次(正方也这么管)。成绩单还支持中英切换——26 门课补了英文名,Operating Systems、Design and Analysis of Algorithms,连考试性质都有中英映射。
这里踩了个值得记的坑:window.print() 只想打印弹窗里的单据,第一版用 display: none 隐藏页面其余部分——结果弹窗本身也是「页面其余部分」,一起消失了。正确姿势是 visibility 穿透:祖先全部 hidden,单据分支单独 visible,再绝对定位到左上角。
冲突检测:一个纯函数的三次复用
排课冲突判定我抽成了一个纯函数 ScheduleConflict(同星期 ∧ 单双周相容 ∧ 周次相交 ∧ 节次相交),配了 12 例矩阵单测。之后它被复用了三次:
- 学生选课时的冲突提醒(原有);
- 管理端新建教学班前的教师时间冲突 / 教室占用校验——实测把「赵敏 周一 1-2 节已排课」精准拦下来;
- 监考编排的「同日同时段已有监考任务」拒绝(同构逻辑)。
一个函数写对三次复用,比三个函数各写一遍舒服太多。
数据大屏:给答辩准备的答案
管理端加了一个固定深色的数据大屏(不随主题切换):六个 KPI 加六张图——学院学生分布、全体有效成绩分布(不及格那段用红色单独标出)、选课人数 TOP 8(已选/容量双层条)、毕设方向玫瑰图、近 7 日登录趋势、预警等级环形图。后端一个聚合接口一次给全,前端 ECharts 渲染,时钟和呼吸灯负责「大屏感」。
大屏上连「当前在线学生」都是真的——直接数 WebSocket 会话表。
小功能,但都看得见
- 学业预警:有效成绩不及格学分分级(≥5 黄 / ≥10 橙 / ≥20 红),学生端横幅、管理端名单同源;
- 毕业条件预审:五项逐条核对(总学分、各模块、学位绩点、无预警、毕设答辩),差多少写清楚;
- 考试时间冲突检测:同日重叠的两场考试互相标记 ⚠,附一键缓考;
- 新闻浏览量:详情自增、列表 🔥 热度标记;选课页余量低于 20% 打「🔥 抢手」标签;
- 账号解锁:连续输错密码锁定 10 分钟后,管理员可一键解锁。
工程底座又踩了三个新坑
MySQL 便携版读不了中文路径。上个坑是 subst X: 虚拟盘解决,这次加深了认知:不是编码参数能救的(GBK 的 my.ini、相对路径、8.3 短名全试过),mysqld 会把路径截断在第一个中文字符。另外 Flyway 10 连 MySQL 要额外加 flyway-mysql 依赖,OPEN 还是 MySQL 8 保留字——建表列名 open 直接迁移失败,改成 is_open。
后端重启会让 Vite 的 WS 代理卡死。症状很迷惑:浏览器明明发了 WS 请求,后端却毫无动静。重连机制、代理配置都没问题——是 Vite 的 http-proxy 在后端重启窗口进入坏状态后不自愈,重启 Vite 才恢复。排查时还发现 server.log 是 GBK 编码,grep 中文日志 永远零命中,得先 decode('gbk')。
Git Bash 的 curl 会把中文按 GBK 发送。接口测试时中文 JSON 一律 500,Jackson 报 Invalid UTF-8——前端 axios 从没出过这种事。解法是先用脚本把 body 写成 UTF-8 文件再 --data-binary @file。
现在的样子
数据库已经迁移到 V12,三端页面加起来四十多个,从门户到签到、从毕设到大屏全部连通。复现方式不变:tools/ 里便携全家桶,start.bat 一键拉起,删库重启即重置演示数据。
两篇文章做完这个项目,最大的体会是:「像真的」不是把界面做像,而是把真实系统里那些没人注意的边界——代签、超卖、重复审核、漏通知——一个一个堵上。 功能清单只能决定下限,边界处理才决定上限。