Add React experiences for academic planning simulations, published physical fitness results, degree award decisions, and graduation clearance tracking. Wire student navigation and document the migrated API contracts.
明序校园综合服务平台
面向普通高校教学管理、学业服务与校园生活的一体化综合服务平台。后端使用 ASP.NET Core 10、EF Core 10,前端使用 Vue 3、TypeScript 和 Element Plus。
学校名称、平台全称、简称、定位语和介绍统一配置在
src/Jiaowu.Api/appsettings.json 的 Branding 节。其他学校部署时只需修改这一处并重新
构建前端;浏览器标题、登录页、PWA、Capacitor 应用、Swagger、课表与成绩分析导出等
品牌信息会同步更新。程序集名、数据库标识和 App 包名属于兼容性标识,不随展示名称变化。
官方电子凭证支持学生自助申请和教务代签,由服务端生成成绩单与学籍状态证明 PDF,并提供唯一凭证编号、二维码公开验真、下载记录、失效和重签;生产部署应通过 OfficialDocuments__PublicBaseUrl 配置二维码使用的最终 HTTPS 根地址。
当前已实现系统登录与角色权限、基础数据、用户管理、教师档案、学生档案、课程库、培养方案、教学任务、排课课表、学生选课、成绩管理、考试考场、学籍异动、毕业审核、学位授予、毕业离校和首页统计。人员及课程列表支持组合筛选、服务端分页和完整增删改查;培养方案支持课程模块、专业年级版本、复制新版本、发布与旧版本归档,已发布版本可继续维护名称、学分说明和课程结构,适用专业、入学年级及版本号保持锁定;教学任务支持学期课程开设、多教师、合班、容量校验、发布与结课,公共课由校级教务负责、专业必修/专业选修/实践课下放课程所属学院管理,并支持教师按学期申报授课科目、学院审核授课资格、公共课按若干行政班合并教学班,以及在审核通过的教师池中随机均衡分配后批量生成草稿;排课支持学期作息维护、单双周与周次节次、课程可用时间、校区/教学楼/指定教室约束、不占用教室课程、教室容量、教师/行政班/教室冲突校验、自动生成、手工微调和版本化发布;教师和学生可启用个人教学日历订阅,将固定课程、考试/监考、补考及灵活课程提醒同步到支持 iCalendar 的客户端,并可重置或停用订阅地址;选课支持批次时间窗、投放范围、容量与学分上限、重复课程与课表冲突校验、退课截止时间、满员候补、顺位查询、退课后资格复核与自动递补,以及正式名单和候补队列管理;成绩管理支持分项比例、批量录入、特殊考试状态、自动总评与绩点、教师提交、学院审核、校级发布和学生成绩单;考试管理支持考试计划、场次、考场容量、监考教师、考生名单以及考场/监考/学生时间冲突校验;学籍异动支持休学、复学、退学申请,辅导员、学院、学校三级顺序审核,学生撤回,以及最终审批后自动同步学籍状态;毕业审核按入学年级匹配已发布培养方案,以正式成绩计算总学分、必修通过和未解决不及格课程,支持学院范围查看、人工复核、校级锁定发布和学生结果查询;学位授予以已发布毕业资格为来源,按正式成绩加权平均绩点生成规则结论,支持学院人工复核、校级发布锁定和学生结果查询;毕业离校支持自定义事项与责任部门,按校级、学院、辅导员角色分工办理,强制数据范围校验,学生进度查询,以及必办事项全部完成后的批次锁定。
课程库支持下载标准模板后批量导入 .xlsx,按课程编码新增或更新,并在整批校验失败时不写入任何课程;授课资格既支持教师申报后审核,也支持学院在本院教师范围内直接分配;学生可在“我的培养方案”中查看本人适用的已发布方案,并按已完成、在读、重修中、未通过、待完成和未修读状态核对课程与学分进度。
权限采用后端强制校验的角色与数据范围模型。多角色账号按 All > College > Class > Self 取最高数据范围:校级角色可访问全校数据,院系管理员限定本学院,辅导员通过稳定的账号 ID 绑定所带行政班,教师和学生限定本人及当前教学关系;前端菜单和路由限制仅作为交互辅助,不替代 API 授权。
系统提供统一插件平台。SuperAdmin 可以从“组织与权限 → 插件中心”启用或停用内置业务插件;状态变更同时作用于菜单、前端路由和对应后端 API,停用不会删除插件已有数据。插件只控制业务能力是否开放,原接口的角色与数据范围校验仍然有效。生产环境升级后需先执行数据库迁移,再启动新版本服务。
插件平台同时支持经过受信任发布者 RSA-PSS/SHA-256 签名的 SDK 插件包。包上传后只进入暂存区,管理员标记激活或回滚后必须重启服务;启动时系统会再次校验签名、Host API、清单和包哈希,然后装载程序集并按事务执行插件迁移。SDK、示例插件、发布者公钥配置和打包流程见 docs/plugin-sdk.md。不受信任的第三方代码仍不得作为进程内插件安装。
人员档案与登录账号分开维护。新增或 Excel 导入学生、教师档案时不会自动创建账号,也不会在修改档案时同步账号。学生首次使用时可以在登录页进入“自助激活”,填写姓名、学号、学院、专业、年级和行政班;全部匹配在籍档案后自行设置密码,系统才创建 Identity 登录账号并关联学生角色。AspNetUsers 作为 ASP.NET Core Identity 的内部安全存储,负责密码哈希、登录锁定、角色和令牌。
本地开发:热更新模式
本地开发固定使用 SQLite。首次启动会自动创建空的
src/Jiaowu.Api/data/jiaowu-dev.sqlite,只初始化系统角色和课程分类,不再写入
演示组织、人员、课程、业务记录或内置测试账号。
$env:SeedAdmin__UserName = 'admin'
$env:SeedAdmin__Password = '请替换为本机开发密码'
$env:SeedAdmin__DisplayName = '系统管理员'
dotnet run --project src/Jiaowu.Api
另开一个终端:
Set-Location web
npm install
npm run dev
访问 http://localhost:5173。首次创建管理员后可以清除三个
SeedAdmin__* 环境变量;后续账号和基础数据均通过管理界面维护。
本地开发:单服务模式
不需要前端热更新时,可以先把 Vue 编译进 API 的 wwwroot:
npm --prefix web run build
dotnet run --project src/Jiaowu.Api
访问 http://localhost:5255。/api 和静态页面由同一个 ASP.NET Core 服务提供,/base-data 等前端路由刷新时也会回退到 index.html。
Capacitor Android App
web/.env.capacitor 配置 App 使用的 HTTPS API 与公开站点地址。生成或更新
Android 工程前先构建并同步原生插件:
Set-Location web
npm ci
npm run build:capacitor
npm run cap:sync
npm run cap:open:android
学生在 App 的“我的考勤”中可调用原生相机扫描教师展示的签到二维码;二维码由服务端 签名、每 10 秒刷新并在 20 秒后失效,扫码后先显示课程和签到时限,仍需学生确认才 提交。教师可直接在手机 App 发起定位签到,以教师手机的原生精确位置作为签到点; 教室电脑没有定位模块时不影响该流程。服务端校验课程名单、签到时间、距离和定位精度, 并记录签到设备摘要、IP、失败次数和异常频率,供任课教师在考勤明细中复核。Android 最低版本为 API 26;相机和精确位置权限均按需申请。
Web/PWA 应用
浏览器通过 HTTPS 打开系统后,可使用浏览器的“安装应用”功能将平台固定到桌面或 开始菜单。PWA 会缓存应用界面与静态资源;接口请求始终走网络,不会离线缓存登录态、 成绩或其他业务数据。发现新版本时由用户确认刷新,避免正在填写表单时被强制中断。 Capacitor Android 构建会自动禁用 PWA Service Worker,仍使用原生能力和独立的 OTA 更新流程。
App 前端热更新
App 内置自建 OTA 更新器。它只更新 dist 中的 HTML、JavaScript、CSS 和静态资源;
新增或升级 Capacitor 插件、修改原生权限、Android/iOS 工程或原生版本号时,仍必须
重新构建并安装 App。首次启用更新器也需要发布一次包含更新插件的新 App,之后普通
前端修复不再需要重新打包。
生成更新 ZIP:
Set-Location web
npm ci
npm run ota:package
ZIP 会生成到 .artifacts/app-updates,根目录直接包含 index.html,其版本固定取自
versions.props 的 JiaowuFrontendVersion(传入 --version 时也必须一致)。使用
SuperAdmin 进入“运维与审计 → App 前端热更新”,上传 ZIP,填写目标平台、通道和
兼容的原生版本后先保存为草稿,再执行发布。当前 Android 工程的 versionName 为
2.6.0,因此对应更新包的“兼容原生版本”应填写 2.6.0。
App 每次启动、网络恢复以及从后台回到前台时,都会向 /api/app-updates/latest 检查版本,
在后台下载并校验服务端提供的 SHA-256。下载完成后,用户可在常驻提示中确认立即重新加载;
若选择稍后,切到后台或下次启动时自动切换,并保留当前页面地址。
新资源若未能成功启动,原生更新器会自动回滚。再次发布
已归档版本即可回滚正式通道;不同原生版本、Android/iOS、测试/正式通道彼此隔离。
更新版本元数据和 ZIP 保存在数据库中,部署新服务端版本前必须先执行
--migrate-only。
Android 开屏、快捷入口与桌面组件
Android App 在系统静态启动页之后显示智能问候:优先使用当前登录姓名和春节、端午、 中秋、国庆等节日文案,其次按早上、中午、下午和晚上展示问候;轻触可立即跳过,并 遵守系统“减少动画”设置。
长按 App 图标提供“我的课表、考试安排、课堂签到、消息中心”四个快捷入口。“课堂 签到”会按当前角色将学生带到扫码/定位签到,将教师带到发起签到。桌面组件提供“今日 课表”和“近期考试”,展示 App 最近一次成功加载并安全写入 Android 本地缓存的数据; 退出账号时会清空组件,跨日且尚未打开 App 刷新时不会继续展示过期的今日课表。
原生 Java、清单和组件资源模板保存在 web/native/android。每次运行
npm run cap:sync 后,configure-capacitor.mjs 会把模板同步到被 Git 忽略的
web/android 生成目录。上述能力涉及 Android 原生代码,首次加入或以后修改时必须
重新构建 App,不能通过前端 OTA 单独下发。
MySQL 8.4 生产部署
非 Development 环境只允许使用 MySQL。构建发布包与数据库配置相互独立:
dotnet publish 不需要数据库连接串、JWT 密钥或生产环境变量,也不会把这些配置写入发布包。
它会自动执行 npm ci 和 npm run build,并将 Vue 静态文件放入发布目录的
wwwroot:
dotnet publish src/Jiaowu.Api -c Release -o .artifacts/publish
如需在特殊流水线中跳过自动前端构建,可传入 -p:BuildFrontendOnPublish=false。
将发布包复制到目标服务器后,把发布包中的 .env.example 复制为 .env,并填写
真实配置。应用会在启动时自动读取可执行文件所在目录的 .env:
$copyParams = @{
LiteralPath = '.artifacts\publish\.env.example'
Destination = '.artifacts\publish\.env'
}
Copy-Item @copyParams
.env 使用 KEY=VALUE 格式,允许空行、以 # 开头的注释、可选的 export 前缀,
以及单引号或双引号值。双引号值支持 \n、\r、\t、\\ 和 \";不执行变量
替换或命令。真实进程环境变量的优先级高于 .env,因此 Windows 服务、Docker、
Kubernetes 或密钥管理系统仍可覆盖文件中的值。
如需把配置文件放到其他位置,通过 JIAOWU_ENV_FILE 指定绝对路径;相对路径按进程
当前工作目录解析。显式指定但文件不存在、行格式错误或引号没有闭合时,应用会拒绝
启动。不要把真实 .env 提交到仓库或打进发布包;Linux/macOS 建议设置权限
chmod 600 .env,Windows 应通过 ACL 只允许服务账号和管理员读取。连接串中的证书
路径必须是运行服务器上的实际路径。
Keycloak 单点登录(可选)
系统支持 Keycloak 的 OpenID Connect 授权码流程。Keycloak 只负责验证身份;账号是否
启用、角色和学院数据范围仍以本系统 Identity 数据为准。首次 SSO 登录会用
preferred_username(可通过 Sso__UserNameClaim 修改)优先匹配已有登录账号并记录
外部账号绑定。如果 Keycloak 用户名与平台账号不同,认证后会进入账户绑定页,用户
需要再输入一次现有平台账号和密码;验证成功后建立永久绑定并直接登录。绑定不会
自动创建本地账号、修改人员档案或从 Keycloak 导入高权限角色。同一 Keycloak 身份不能
绑定多个本地账号,同一本地账号也不能绑定多个 Keycloak 身份。原账号密码登录和学生
自助激活入口不受影响。
在 Keycloak 中创建 OpenID Connect 客户端,并至少配置:
- Valid redirect URI:
https://jiaowu.example.edu.cn/signin-keycloak - Valid post logout redirect URI:
https://jiaowu.example.edu.cn/*(若后续启用 Keycloak 全局退出) - Standard flow:开启;Implicit flow:关闭;PKCE:
S256
然后在 .env 中配置:
Sso__Enabled=true
Sso__DisplayName=学校统一身份认证
Sso__Authority=https://sso.example.edu.cn/realms/mingxu
Sso__ClientId=jiaowu-web
Sso__ClientSecret=REPLACE_WITH_KEYCLOAK_CLIENT_SECRET
Sso__UserNameClaim=preferred_username
Sso__RequireHttpsMetadata=true
Sso__LinkExistingUsersByUserName=true
Sso__FrontendBaseUrl=https://jiaowu.example.edu.cn
Sso__CallbackUrl=https://jiaowu.example.edu.cn/signin-keycloak
前后端同域时 Sso__FrontendBaseUrl 可以留空。本地 Vite 开发默认回到
http://localhost:5173,Keycloak 测试客户端需同时允许
http://localhost:5255/signin-keycloak。Sso__CallbackUrl 是应用实际发送给 Keycloak
的 redirect_uri,必须与客户端的 Valid redirect URI 完全一致;建议生产环境始终显式
配置它,避免反向代理导致 scheme 或 host 推导错误。个人账户页的“管理员配置参考”也会
显示当前生效的完整回调地址。多实例部署应配置 Redis,以便任意实例都能兑换两分钟内
有效、使用后即删除的 SSO 登录码及五分钟内有效的绑定意图。
Android App 使用系统浏览器完成 Keycloak 登录,再通过 https://eis.biss.click/sso/callback
或 /sso/bind 的 Android App Link 回到应用;Keycloak 的 Valid redirect URI 仍然只配置
Sso__CallbackUrl(即 /signin-keycloak),不要配置 mingxu://。正式发布前,将 Play
App Signing 证书的 SHA-256 指纹写入 web/public/.well-known/assetlinks.json,并确保该文件
以 application/json 在 https://eis.biss.click/.well-known/assetlinks.json 可匿名访问。当前
配置的包名为 edu.mingxu.jiaowu;站点域名或正式签名证书变更时必须同时更新此文件和 Android
Manifest 后重新签名发布 APK/AAB,此类变更不能通过 OTA 下发。
本机通过 Android Studio 或 cap run 安装的 debug APK 使用不同的调试证书;其 SHA-256 也已
列在 assetlinks.json 中,仅用于本机调试。若改用其他机器、其他 JKS 或直接签名的 release APK,
必须把该 APK 实际签名证书的 SHA-256 一并加入后重新部署站点。
用户登录后可从页面右上角进入“个人账户”,主动绑定或解除 Keycloak 账号。主动绑定先 使用当前 JWT 创建五分钟有效的一次性绑定意图,再跳转 Keycloak;回调只能绑定到发起该 意图的本地账号。解绑需要再次验证本地密码,避免仅凭未锁屏的登录会话解除身份关联。
Linux systemd 服务
仓库提供 deploy/systemd/jiaowu.service,适用于
使用 systemd 的 Linux 发行版。示例假定自包含发布包位于 /opt/jiaowu,服务使用
无登录权限的 jiaowu 账号。程序会自动读取 /opt/jiaowu/.env,因此服务单元没有再
配置 EnvironmentFile=。
以 Debian/Ubuntu 为例,先创建账号并设置文件权限:
sudo useradd --system \
--home-dir /opt/jiaowu \
--shell /usr/sbin/nologin \
jiaowu
sudo chown -R root:jiaowu /opt/jiaowu
sudo chmod 0750 /opt/jiaowu
sudo chmod 0750 /opt/jiaowu/Jiaowu.Api
sudo chmod 0640 /opt/jiaowu/.env
sudo install -d \
--owner=jiaowu \
--group=jiaowu \
--mode=0700 \
/var/lib/jiaowu/backups
sudo apt-get install default-mysql-client
如果账号已存在,useradd 会报错,可以跳过该命令。RHEL 系发行版的 nologin 通常
位于 /sbin/nologin,请按服务器实际路径调整。如果连接串使用私有 CA,还要确保
jiaowu 组对 SslCa 指向的证书文件具有读取权限。首次启动前,先停止旧实例,并以
服务账号执行迁移;仅专用空演示库需要执行第二条灌数命令:
sudo -u jiaowu /opt/jiaowu/Jiaowu.Api --migrate-only
sudo -u jiaowu /opt/jiaowu/Jiaowu.Api \
--seed-demo-data \
--confirm-production-demo-data
演示数据成功后,从 /opt/jiaowu/.env 删除临时的 SeedAdmin__* 配置。随后安装并
验证服务单元:
sudo install \
--owner=root \
--group=root \
--mode=0644 \
deploy/systemd/jiaowu.service \
/etc/systemd/system/jiaowu.service
sudo systemd-analyze verify /etc/systemd/system/jiaowu.service
sudo systemctl daemon-reload
sudo systemctl enable --now jiaowu.service
查看状态、实时日志和数据库就绪探针:
sudo systemctl status jiaowu.service --no-pager
sudo journalctl --unit=jiaowu.service --follow
curl --fail http://127.0.0.1:8080/health/ready
更新程序时应先 sudo systemctl stop jiaowu.service,替换发布文件并执行
--migrate-only,确认迁移成功后再运行 sudo systemctl start jiaowu.service。
服务以失败重启策略运行,但不会在正常退出或管理员主动停止后自行拉起。
数据库应明确使用 utf8mb4;MySQL 8.4 的默认排序规则为
utf8mb4_0900_ai_ci。新建数据库时可执行:
CREATE DATABASE `jiaowu`
CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
首次部署或版本升级时,从 .env 复制一份不纳入版本控制的 .env.migrate,只将
连接串改成具备 DDL 权限的迁移账号,然后单独执行迁移:
$env:JIAOWU_ENV_FILE = (Resolve-Path -LiteralPath '.artifacts\publish\.env.migrate').Path
& '.artifacts\publish\Jiaowu.Api.exe' --migrate-only
迁移成功后删除 .env.migrate,清除 JIAOWU_ENV_FILE,应用便会读取发布目录中的
.env;其中应配置仅具备应用所需 DML 权限的运行账号:
Remove-Item -LiteralPath 'Env:JIAOWU_ENV_FILE'
& '.artifacts\publish\Jiaowu.Api.exe'
Database:ApplyMigrationsOnStartup 默认关闭。普通启动会检查待执行迁移并在架构落后时
直接失败,避免多实例同时执行 DDL。只有明确接受启动期 DDL 风险的单实例部署才应将
Database__ApplyMigrationsOnStartup 设为 true。
生产环境不会创建默认管理员。首次部署可以临时配置
SeedAdmin__UserName、SeedAdmin__Password 和 SeedAdmin__DisplayName,
账号创建后立即移除这些配置。
独立的生产演示环境
如需验证 Production 配置和 MySQL 8.4 部署链路,请新建专用的空数据库(例如
jiaowu_demo),不要向准备承载真实业务的数据库插入演示数据。先按前述步骤执行
--migrate-only,停止该环境的应用实例,再从 .env.example 复制并编辑
.env.demo,配置演示数据库、运行账号和临时管理员:
$env:JIAOWU_ENV_FILE = (Resolve-Path -LiteralPath '.artifacts\publish\.env.demo').Path
& '.artifacts\publish\Jiaowu.Api.exe' --seed-demo-data --confirm-production-demo-data
Remove-Item -LiteralPath 'Env:JIAOWU_ENV_FILE'
该命令仅允许在非 Development 环境运行,且必须同时提供确认参数。它会再次检查迁移 状态,只在没有校区、学院、专业、班级、师生、课程、学期、教室和教学任务等业务数据 的空库中写入;检测到任何已有业务数据都会直接终止。写入完成后命令退出,不会启动 Web 服务,也不会在以后启动时自动补写或重复写入。
演示数据包含校区、学期、教学楼与教室、16 个学院/教学单位、52 个专业、104 个行政班、 128 名教师、3,640 名学生、148 门课程和授课资格,不包含内置通用密码或学生/教师登录 账号。演示数据 DML 在一个事务中提交,失败会回滚;迁移 DDL 必须保持为前置独立步骤, 因为 MySQL 8.4 的 DDL 会触发隐式提交。参见 MySQL 8.4 事务说明和 隐式提交语句清单。
生产数据库使用 MySQL 专用 EF Core 迁移。部署前先恢复仓库工具并检查迁移:
dotnet tool restore
dotnet ef migrations list --no-connect --project src/Jiaowu.Api --startup-project src/Jiaowu.Api
不要使用当前提供程序生成的 dotnet ef migrations script --idempotent 作为 MySQL
部署脚本;其条件块不是 MySQL 8.4 可直接执行的语法。应使用上述
--migrate-only 入口,或在明确知道目标迁移状态时生成非幂等脚本并先做备份。
MySQL 的 DDL 会隐式提交,迁移不能依赖外层事务整体回滚。
SQLite 只用于本地开发:新库通过 EnsureCreated 建立,已有开发库通过轻量、版本化的
本地升级脚本补齐结构,不需要手动删除数据文件。SQLite 文件不能用于生产。
服务探针:
/health/live:只检查进程存活。/health、/health/ready:实际检查数据库连接,失败时返回 HTTP 503。/health/cache:检查可选 Redis;未配置 Redis 时返回disabled,Redis 故障不会影响数据库就绪探针。/health/messaging:检查后台任务传输;单机内存队列返回memory,启用 RabbitMQ 时实际检查代理连接,同时返回各 Outbox 状态的积压数、过期租约数和 最老未完成任务的等待时间。
查询缓存与 Redis
应用使用 HybridCache 统一管理进程内一级缓存和可选 Redis 二级缓存。目前缓存范围为 学生激活/基础数据选项、匿名可访问的已发布课表、仪表盘以及统计分析摘要。统计缓存键 包含有效数据范围、学院和规范化筛选条件,避免跨学院复用;统计 Excel 导出仍实时查询。 选课容量、成绩写入、考勤、审批、通知未读数、权限和后台任务状态仍直接以 MySQL 为准。
不配置 ConnectionStrings__Redis 时,开发和单机部署仍使用进程内缓存,不要求安装
Redis。生产环境使用 Redis 时,通过环境变量配置连接串,例如:
ConnectionStrings__Redis=redis.internal:6380,user=jiaowu,password=REPLACE_ME,ssl=true,abortConnect=false
仪表盘和统计摘要默认在 Redis 中缓存 3 分钟、进程内缓存 30 秒,可分别通过
Cache__AnalyticsExpirationMinutes 和 Cache__AnalyticsLocalExpirationSeconds
调整。该类汇总采用短 TTL 控制数据新鲜度,不要求每个业务写入点同步清理缓存。
Redis 只作为可丢弃的查询缓存。连接失败时应用回源数据库,普通启动和
/health/ready 不依赖 Redis;可以单独检查 /health/cache。缓存键自动包含运行环境,
同一 Redis 可以安全承载 Development、Staging 和 Production,但生产环境仍建议使用
独立实例、私有网络、ACL 和 TLS。
compose.example.yml 包含不暴露宿主机端口的 Redis 服务,限制为 256 MB 并使用
allkeys-lfu 淘汰策略,不启用持久化。compose.app.example.yml 不创建 Redis;
如需连接外部 Redis,在 .env 中配置上述连接串即可。
慢接口与慢查询基线
应用不依赖 OpenTelemetry。启用 Observability 后,会为每个 /api 请求写入结构化的
方法、路径、终结点、状态码、耗时和请求号;超过阈值或返回 5xx 的请求会提升为 Warning。
EF Core 数据库命令超过阈值时同样写入查询名称、SQL 模板哈希、数据库类型、耗时和相同的
请求号。用日志平台按 DurationMs 聚合即可得到真实的 P50/P95/P99 和慢接口排行。
Observability__Enabled=true
Observability__ServiceName=jiaowu-api
Observability__LogAllApiRequests=true
Observability__SlowRequestThresholdMilliseconds=1000
Observability__SlowQueryThresholdMilliseconds=500
为关键 EF 查询添加 TagWith("模块.查询名") 后,慢 SQL 日志会直接显示稳定名称;无标签
查询只显示操作类型和 SQL 模板哈希。默认 Observability__IncludeSqlText=false,不会把
SQL 模板、参数值或连接串写入日志。仅在受控诊断窗口内临时启用 SQL 模板记录,并限制日志
访问权限与保留时间。
应用侧请求号用于关联接口和查询名称;生产 MySQL 还应由数据库管理员启用慢查询
日志,并将 long_query_time 设为与应用阈值一致。先按查询哈希/标签汇总高频慢查询,再
对脱敏后的 SELECT 在测试库或只读副本执行 EXPLAIN ANALYZE,根据实际扫描行数和循环
次数决定是否补组合索引或改写投影。EXPLAIN ANALYZE 会真实执行语句,不能直接用于生产
写操作。参考 MySQL 慢查询日志
和 MySQL 8.4 EXPLAIN。
如部署环境另行提供 Prometheus 兼容指标源,超级管理员仍可在“组织与权限 → 运维与审计 → 系统性能”查看其汇总数据。该页面只读查询外部指标源,浏览器不会接触其地址或令牌;结果 默认缓存 30 秒。应用本身不会再通过 OpenTelemetry 向该指标源写入数据。
PerformanceReporting__Enabled=true
PerformanceReporting__PrometheusBaseUrl=https://prometheus.example.edu.cn/
PerformanceReporting__BearerToken=REPLACE_WITH_READ_ONLY_TOKEN
PerformanceReporting__GrafanaBaseUrl=https://grafana.example.edu.cn/
PerformanceReporting__CacheSeconds=30
PerformanceReporting__TimeoutSeconds=10
PrometheusBaseUrl 必须指向可访问 /api/v1/query 和 /api/v1/query_range 的
Prometheus 兼容接口,令牌应仅具有查询权限。未启用、未配置或指标源暂时不可用时,页面
会显示明确的空状态,不会改查业务数据库或拖慢正常请求。若 Collector/Prometheus 对
指标名或 service_name 标签做了转换,可通过 PerformanceReporting 下对应的
*MetricName 和 ServiceNameLabel 配置项适配,无需改前端。
ClickHouse 分析读模型
ClickHouse 仅用于考勤、操作审计和成绩趋势的多维聚合,MySQL 仍是所有教务业务的唯一写入源。默认关闭;启用后,后台工作器以可重试的滚动窗口投影 MySQL 当前事实到 ReplacingMergeTree 表,重复投递不会改变读结果。
生产环境请为分析库创建独立账号,并限制其只能访问 ClickHouseAnalytics__Database。推荐通过 HTTPS 或内网连接:
ClickHouseAnalytics__Enabled=true
ClickHouseAnalytics__Endpoint=https://clickhouse.example.edu.cn:8443
ClickHouseAnalytics__Database=jiaowu_analytics
ClickHouseAnalytics__UserName=jiaowu_analytics
ClickHouseAnalytics__Password=REPLACE_WITH_A_STRONG_PASSWORD
分析概览通过 GET /api/clickhouse-analytics/overview 提供;学院管理员只能读取本学院的考勤和成绩趋势,跨学院的操作审计仅对全校数据范围角色开放。ClickHouse 暂时不可用时,业务写入不会失败,工作器会在下一个周期重试。
后台任务与 RabbitMQ
自动排课、课表发布和补考自动生成使用数据库 Outbox 保存任务消息。创建业务任务与 Outbox 消息在同一次 MySQL 提交中完成,后台发布器再将消息投递给任务 Worker;重复 投递通过 Outbox 处理租约和唯一任务键抑制。任务状态表仍是前端查询进度与错误信息的 唯一来源。
开发和单机部署默认使用有界进程内队列,不需要 RabbitMQ:
BackgroundJobs__Transport=InMemory
多实例生产部署应切换为 RabbitMQ,并配置独立账号、虚拟主机和 TLS:
BackgroundJobs__Transport=RabbitMq
BackgroundJobs__AutomaticScheduleConcurrency=1
BackgroundJobs__SchedulePublishConcurrency=1
BackgroundJobs__MakeupExamAutoConcurrency=1
BackgroundJobs__ExamArrangementConcurrency=1
RabbitMq__HostName=rabbitmq.example.edu.cn
RabbitMq__Port=5671
RabbitMq__UserName=jiaowu
RabbitMq__Password=REPLACE_WITH_A_STRONG_PASSWORD
RabbitMq__VirtualHost=/jiaowu
RabbitMq__UseTls=true
RabbitMq__TlsServerName=rabbitmq.example.edu.cn
RabbitMQ 传输使用持久消息、发布确认、手动消费确认、每种任务独立队列和死信队列。 默认创建 Quorum Queue,重任务的消费者预取数为 1。三类任务各自至少有一个消费者, 因此不同类型的任务不会再互相阻塞;单类任务的并发度可独立设置为 1-16。自动排课 通常最消耗 CPU,建议先保持为 1,再根据 CPU、数据库连接池和任务等待时间逐级调到 2 或 3;不要只提高预取数。
Outbox 租约恢复改为按维护周期执行,避免积压发布时每条消息都额外扫描数据库;已完成
消息默认保留 14 天并按每批 500 条清理,可使用 CompletedRetentionDays、
MaintenanceIntervalSeconds 和 CleanupBatchSize 调整。应用暴露
Jiaowu.BackgroundJobs Meter,其中包含发布量、处理量、发布耗时、处理耗时和清理量,
可由现有日志平台或运行时指标采集器汇总。MySQL 或 RabbitMQ 暂时不可用时,未完成
消息会根据 Outbox 状态和租约继续补投。迁移服务应先应用
BackgroundJobOutbox 数据库迁移,再启动应用实例。
运维与审计控制台
超级管理员可从“组织与权限 → 运维与审计”查看系统性能,查询写操作日志、三类失败后台
任务、数据库、缓存与任务通道健康状态,并查看由 5xx、失败/重试任务、健康探针和备份
时效汇总出的异常告警。查询接口和备份操作均在后端强制要求 SuperAdmin,不能只依赖
前端菜单隐藏。
SQLite 开发环境直接使用在线备份 API。MySQL 环境需要在服务器安装 mysqldump 与
mysql(容器镜像已包含对应的 mariadb-dump 与 mariadb 客户端),并配置独立的
ConnectionStrings__OperationsMySql。该账号不得复用日常业务账号:它需要读取业务库,
并只应被授权创建和删除名称为 jiaowu_restore_drill_* 的临时演练库。恢复演练不会覆盖
当前业务库,流程是“校验 SHA-256 → 恢复到随机临时库 → 检查表结构 → 删除临时库”。
备份目录必须是仅服务账号可写的持久化目录。示例配置使用
/var/lib/jiaowu/backups;Compose 已挂载独立命名卷。启用 MySQL TLS 时,还要通过
Operations__MySqlAdditionalArguments__N 传入与所选命令行客户端匹配的 CA 与主机名
校验参数。例如 Oracle MySQL 客户端使用 --ssl-mode=VERIFY_IDENTITY 和
--ssl-ca=/etc/jiaowu/mysql-ca.pem,容器内 MariaDB 客户端使用 --ssl、
--ssl-ca=... 与 --ssl-verify-server-cert。
跨平台发布与 Docker
.gitea/workflows/publish.yml 只在推送 v* 标签或手动运行时执行,普通分支 push
不会触发耗时发布。默认生成以下自包含程序包,目标服务器无需另装 .NET:
- Windows x64:
.zip - Linux x64、Linux ARM64:
.tar.gz SHA256SUMS:所有压缩包的 SHA-256 校验值
手动运行时启用 include_extended_platforms,还会生成 Windows ARM64、macOS x64
和 macOS ARM64。Windows 使用 Jiaowu.Api.exe 启动,Linux/macOS 使用
./Jiaowu.Api;各压缩包都包含 .env.example。
工作流同时使用 Buildx 构建 linux/amd64、linux/arm64 镜像并推送至 Gitea
Container Registry:
git.biss.click/biss/academic-affairs-system
仓库的 Actions 权限必须允许内置 GITEA_TOKEN 写入 Packages 和 Releases。版本标签
会创建 Gitea Release;手动运行只保留工作流产物并推送
manual-<run-number>、sha-<commit> 镜像标签。
简单 Compose:连接外部 MySQL
已有独立 MySQL 8.4 时,使用
compose.app.example.yml。该文件只有一个 app 服务,
不会创建 MySQL 容器或数据库卷。先将 .env.example 复制为 .env,填写外部数据库、
JWT、域名和跨域配置:
Copy-Item -LiteralPath '.env.example' -Destination '.env'
.env 中的 MySQL 主机必须是容器可以访问的地址,不能把宿主机数据库写成
localhost。Docker Desktop 可按实际环境使用 host.docker.internal;远程数据库应
填写其 DNS 名称。使用私有 CA 时,把证书放到 certs/mysql-ca.pem,并取消 Compose
文件底部的只读挂载配置注释。
本地构建、迁移和启动:
$composeFile = 'compose.app.example.yml'
$buildArgs = @('--file', $composeFile, 'build', 'app')
& docker compose @buildArgs
if ($LASTEXITCODE -ne 0) { throw 'Docker 镜像构建失败。' }
$migrateArgs = @('--file', $composeFile, 'run', '--rm', 'app', '--migrate-only')
& docker compose @migrateArgs
if ($LASTEXITCODE -ne 0) { throw '数据库迁移失败。' }
$upArgs = @('--file', $composeFile, 'up', '--detach', 'app')
& docker compose @upArgs
if ($LASTEXITCODE -ne 0) { throw '应用启动失败。' }
Linux/macOS 使用相同的 Compose 文件:
cp .env.example .env
docker compose -f compose.app.example.yml build app
docker compose -f compose.app.example.yml run --rm app --migrate-only
docker compose -f compose.app.example.yml up -d app
如使用 Gitea 已发布镜像,可在 .env 末尾添加
JIAOWU_IMAGE=git.biss.click/biss/academic-affairs-system:1.0.0,然后跳过 build。
宿主机端口可通过 JIAOWU_PORT 调整,默认是 8080。
只有连接到专用空演示数据库时,才执行:
$demoArgs = @(
'--file', 'compose.app.example.yml'
'run', '--rm', 'app'
'--seed-demo-data', '--confirm-production-demo-data'
)
& docker compose @demoArgs
if ($LASTEXITCODE -ne 0) { throw '演示数据写入失败。' }
该命令不会启动长期运行的 Web 容器。演示数据写入成功后删除 .env 中临时使用的
SeedAdmin__* 配置,再执行正常的 up --detach app。
一体化 Compose:MySQL 8.4 演示环境
仓库提供跨 Windows、Linux 和 macOS Docker Desktop 使用的
compose.example.yml。它包含 MySQL 8.4、一次性数据库迁移、
Web 服务和一个默认关闭的演示数据工具服务。数据库数据保存在命名卷中,MySQL 端口不
暴露到宿主机;容器日志默认轮转为 3 个 10 MB 文件。
先复制并编辑配置。PowerShell:
Copy-Item -LiteralPath '.env.docker.example' -Destination '.env.docker'
Linux/macOS:
cp .env.docker.example .env.docker
把 .env.docker 中所有密码和 JWT 空值填成独立随机值;JWT_KEY 至少使用 64 个
随机十六进制字符。该文件已被
.gitignore 排除,不要提交;Compose 通过 --env-file 在运行时插值并把应用需要的
配置传入容器,镜像构建过程不需要也不会得到这些值。
如需在当前机器构建镜像,保留 JIAOWU_IMAGE=jiaowu:local 并执行:
$composeArgs = @(
'--file', 'compose.example.yml'
'--env-file', '.env.docker'
'build', 'app'
)
& docker compose @composeArgs
if ($LASTEXITCODE -ne 0) { throw 'Docker 镜像构建失败。' }
也可以把 JIAOWU_IMAGE 改成工作流发布的明确版本标签,例如
git.biss.click/biss/academic-affairs-system:1.0.0。正常启动不会写入演示数据:
$composeArgs = @(
'--file', 'compose.example.yml'
'--env-file', '.env.docker'
'up', '--detach', 'app'
)
& docker compose @composeArgs
if ($LASTEXITCODE -ne 0) { throw 'Compose 启动失败。' }
Compose 会先等待 MySQL 健康,再执行 migrate;只有迁移成功才启动 app。首次需要
演示数据时,应在尚未启动 app 的全新空库上单独执行:
$composeArgs = @(
'--file', 'compose.example.yml'
'--env-file', '.env.docker'
'run', '--rm', 'demo-data'
)
& docker compose @composeArgs
if ($LASTEXITCODE -ne 0) { throw '演示数据写入失败。' }
demo-data 使用 tools profile,因此普通 docker compose up 不会执行它;显式指定
该服务时 Compose 会自动启用其 profile,并启动 MySQL、执行迁移后再写入数据。应用自身
还会强制检查确认参数和空业务库,检测到已有业务数据会拒绝写入。成功后,从
.env.docker 删除三个 SEED_ADMIN_* 配置,再启动 app。
Linux/macOS 的 Compose 参数完全相同,可直接运行:
docker compose -f compose.example.yml --env-file .env.docker build app
docker compose -f compose.example.yml --env-file .env.docker run --rm demo-data
docker compose -f compose.example.yml --env-file .env.docker up -d app
docker compose -f compose.example.yml --env-file .env.docker logs --follow app
示例中应用与 MySQL 位于 Compose 私有网络,所以内部连接使用
SslMode=Disabled,MySQL 未映射宿主机端口。这适合独立演示环境;正式生产连接外部
MySQL 时仍应使用前文的 SslMode=VerifyFull 和 CA,并拆分具备 DDL 权限的迁移账号与
仅具备业务 DML 权限的应用账号。官方 MySQL 镜像通过 MYSQL_USER 创建的账号会获得
示例数据库的全部权限,因此这个一体化 Compose 不能替代生产环境的最小权限设计。
Docker 镜像不包含 .env、数据库密码或 JWT 密钥。应用容器以非 root 用户运行并监听
8080 端口;生产环境仍应由反向代理负责 HTTPS、访问日志和请求大小限制。
验证
dotnet test Jiaowu.slnx
npm --prefix web run build
人员、课程、学期与成绩归档
人员和课程列表默认显示未归档记录,可切换“已归档”或清空筛选查看全部;成绩管理提供同样的归档筛选。学期管理保留当前和历史学期展示,并可按归档状态筛选。各页面均提供单条、批量归档及恢复,提交前展示阻断事项和处理入口,归档或恢复原因必填,每批最多 100 条。批量请求逐项重新校验和提交,显示每条记录的处理结果。
- 人员:学生毕业/退学、教师退休/离职后可归档;归档前检查相关未结课任务、未发布成绩、学生待审批事项和必办离校事项。恢复不会变更学籍、任职状态或登录账号状态。
- 课程:未结束教学任务,以及在读学生适用的已发布培养方案会阻止归档。已归档课程退出新开课和课程申报选项;已有培养方案、教学任务和成绩引用保留。课程启停状态独立于归档状态。
- 学期:当前学期不可归档;开放选课批次、未结束教学任务、未建册或未发布成绩需要先处理。恢复后仍为历史学期,不会自动设为当前。归档不会连带归档人员、课程,也不冻结补考和成绩更正。
- 成绩:以教学班成绩单为单位,仅允许已发布且没有待审批成绩更正的成绩单归档。归档标记独立于发布状态,成绩单、绩点、毕业审核和历史统计继续使用原正式成绩;更正仍走既有审批流程。
已归档人员、课程和学期不能直接编辑或删除;人员、课程和学期 Excel 导入不能覆盖已归档记录,导出遵循列表归档筛选。教学任务发布和撤销结课会检查已归档的学期、课程与教师。归档操作按原有角色及学院范围授权,课程继续执行公共课校级维护规则,学期由校级管理员管理。每次状态变化保留操作者、时间和原因,恢复不删除历史日志。
统一接口为 POST /api/archives/{kind}/preview、POST /api/archives/{kind}/apply 和 GET /api/archives/{kind}/{id}/history?page=1,其中 kind 为 teachers、students、courses、terms、grades。原学期归档接口继续可用并复用同一校验流程。历史日志按每页 20 条读取,旧归档记录不会伪造操作日志。
数据库新增 20260905093721_UnifiedArchiving MySQL 迁移;SQLite 开发库由 DevelopmentSqliteMigrator 补齐字段和归档日志表,已有记录默认未归档,已有学期归档状态保留。部署时应按现有数据库升级流程应用迁移。