Near-stable Explainable Student-housing maTching(安居) 厦门大学嘉庚学院 · 大学生创新创业训练计划项目
把「宿舍分配」从按学号顺序排房改造成一个可求解、可解释、可申诉的匹配问题: 在容量约束下求近稳定室友匹配,对每一个分配结果给出四维解释,并为不服的学生提供反事实申诉通道。 系统同时承载一套完整的学工业务平台(48 个页面),宿舍分配是其中的算法内核。
| 项 | 值 |
|---|---|
| 后端 | Spring Boot 3.2.3 · MyBatis 3.0.3 · MySQL · Java 17 |
| 前端 | 48 个静态页面 · 零框架 · 零 CDN · daisyUI 4 + Tailwind(本地构建) |
| 规模 | 292 个主源文件 / 27 Controller / 36 Service / 62 Mapper / 61 份 Mapper XML |
| 测试 | mvn test → 1,649 通过 / 0 失败 |
| 部署形态 | 校园内网离线可用 · PWA 可安装 · 手机 H5 适配 |
传统做法是按学号、按班级顺序排房,结果是室友关系全靠运气,且学生无处申诉。
NEST 把问题形式化为「带容量的近稳定室友匹配」:
- 输入:学生(性别、专业、年级、作息、生活习惯、志愿)+ 房间(楼栋、容量、年级/特殊需求限制)
- 约束:性别分楼、容量不超、年级不混住、特殊需求专用房 —— 全部是可关闭的开关,不写死在代码里
- 目标:最大化房内两两相容度,同时压住「木桶效应」(最差的那一对)与志愿违背
- 输出:一份分配方案 + 指标 + 每个学生的四维解释 + 零阻塞对
硬约束校验
→ 块分解:按「性别 × 楼栋类型(普通/特殊) × 专业大类 × 年级」切块,块间并行(无锁)
→ 块内:志愿簇预分组 → 最难的人优先贪心构造
→ 局部搜索(含木桶守卫,每轮扫邻域移动)
→ 阻塞对修复(容忍极小目标损失的局部交换)
→ 全局违规复核 → 兜底补齐 → 四维解释预生成
求解器标识:nest-block-greedy-ls-repair-v1(src/main/java/com/umi/nest/dorm/solver/NestSolver.java)
-
容量约束下「近稳定」的可计算定义 零阻塞对在带容量时是 NP-hard,所以不宣称「稳定」,而是最小化阻塞对数量,并用 CP-SAT 在小规模上求出上界来量化 gap(
mvn -Pcpsat构建后的OptimalJudge)。 -
可解释 + 反事实申诉 每个学生四层解释:① 相容度逐维贡献 ② 命中/未命中哪些约束 ③ 志愿层 ④ 对比层("换一种分法会差多少")。 申诉不是"驳回/同意"二选一,而是反事实重解:满足你的要求要牺牲谁,代价多大。三级结构(房间级毫秒 / 块级秒 / 跨块几十秒),采纳只改相关房间、不重跑全局。
-
块分解 按硬属性切块并行,是万级规模唯一跑得动的办法;块内亲密度天然更高。
硬约束违规(恒为 0) · 未分配人数(应为 0) · 均值相容度 · 最差那一对 · 房内方差 · 志愿满足率 · 阻塞对数 · 运行耗时
展开"诊断信息"还能看到每一级的耗时与动作次数:greedyMsTotal / localSearchMsTotal / localSearchAcceptedMoves / blockingRepairsApplied / blockCount / smallBlocks / blocksByPool / occupancy …
指标基准:合成数据带隐真值(存在
dorm_ground_truth,求解器读不到),指标是「真值基准」; 导入真实名单没有隐真值,指标降级为「模型预测值」,接口会返回metricBasisWarning主动声明。 真实数据路径下我们不宣称"相容度提升了 X%",只做模型侧自检。
不是"让大模型排房"(那既不可复现也不可解释),而是用大模型把非结构化素材变成算法能吃的特征:
学生的一句自述 → 11 维性格/作息特征 → 喂给相容度函数。
| 环节 | 做法 |
|---|---|
| 素材 | 自述 / 聊天记录 / 文章 / 辅导员观察(录入时即带授权开关) |
| 提取 | EnsembleExtractor:规则提取 + 大模型提取融合 |
| 融合规则 | 问卷作答永远优先 → 两路都有值时取高置信度(差 <0.10 取均值)→ 只有一路就用那一路 |
| 采用 | 低于 apply.min-confidence(默认 0.35)不回写特征表,保留原值 |
| 可验证 | 「提取器评测」:反写真特征 → 只喂文字 → 比 MAE / Pearson / Spearman |
离线降级是设计的一部分:
nest.psyche.llm.api-key 为空或服务器不通外网时,PsycheProperties.llmReady() 为 false,整个模块自动退化为纯本地规则提取;连得上但调用失败时,LlmExtractor 兜底返回规则结果并保持 method=RULE 不伪装。系统不崩、不静默降级成假数据。
除宿舍分配外,平台覆盖学生工作全流程(48 页 / 27 Controller):
| 域 | 模块 |
|---|---|
| 宿舍 | 智能分配 · 房间视图 · 四维解释 · 申诉处理 · 公平性审计 · 补采建议 · 心理特征提取 · 学生门户 |
| 日常 | 请假离校(含外出/晚归)· 学生档案 · 辅导员名册 · 综合素质测评(德智体美劳)· 综测排名 |
| 奖惩资助 | 评奖评优 · 困难认定(含异议复议)· 生源地助学贷款 · 勤工助学 |
| 成长 | 第二课堂成绩单 · 志愿服务 · 学生社团年审 · 校园文化 |
| 政工 | 学生党建(五阶段台账 + 党校 + 公示)· 心理健康(普查 / 关注 / 转介 / 预约)· 社区管理 |
| 平台 | 公告 · 邀请码 · 用户管理 · 权限矩阵 · 数据级授权 · 审计日志 · 验证码 |
权限有两层:框架层粗筛 ApiRoleGuardInterceptor(24 条路由规则,默认拒绝;另有 DormStaffInterceptor 专管 /api/dorm/**)+ 服务层数据级校验(辅导员只能操作自己名下的学生,否则 403)。
- JDK 17+
- Maven 3.9+
- MySQL 8.0+(开发用 9.5.0 验证)
- Node.js 18+(仅重建 CSS 时需要,运行时不依赖)
建库 nest,字符集 utf8mb4。
⚠️ sql/目录里同时躺着已下线教学模块的旧建表脚本(course_tables.sql、quiz_tables.sql、materials.sql等)。 执行它们会把 11 张已删除的教学表建回来。下面没列到的.sql一律不要执行。
按顺序导入(顺序不能颠倒,后者依赖前者的表):
sql/nest_init.sql 基础表 + 邀请码种子
sql/nest_verify_code.sql 验证码
sql/nest_audit.sql 审计日志 + 登录失败锁定列
sql/nest_dorm.sql 宿舍分配 10 张表
sql/nest_psyche.sql 心理特征表
sql/nest_psyche_v2.sql 特征表补列 + 评测留档
sql/nest_appeal_v3.sql 申诉跨块试算列
sql/nest_xuegong.sql 学工域 7 张表
sql/nest_xuegong_seed.sql 综测指标种子
sql/nest_p8_teaching_offline.sql 公告改学工口径 + DROP 11 张教学表
sql/nest_workstudy.sql 勤工助学 4 张表
sql/nest_aid_review.sql 困难认定 + 级别口径迁移
sql/nest_honor.sql 评奖评优 3 张表
sql/nest_rank.sql 综测排名 5 张表
sql/nest_psyche_work.sql 心理健康 8 张表
sql/nest_community.sql 社区管理 7 张表
sql/nest_party.sql 学生党建 7 张表
sql/nest_volunteer.sql 志愿服务 5 张表
sql/nest_club.sql 学生社团 5 张表
sql/nest_secondclass.sql 第二课堂 3 张表
sql/nest_loan.sql 生源地贷款 1 张表
复制 src/main/resources/application-example.properties 为 application.properties(该文件已被 .gitignore 忽略),至少改这三项:
spring.datasource.url=jdbc:mysql://<主机>:3306/nest?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8&allowPublicKeyRetrieval=true
spring.datasource.username=<用户名>
spring.datasource.password=<密码>
# 验证码哈希盐 —— 默认值是公开的,必须换(验证码本体不落库,只存 HMAC)
nest.verify.pepper=<至少 32 位随机串>
# 头像落盘目录
app.upload.dir=<部署目录>/uploads/avatars大模型凭据不要写进上面这个文件(它会进版本库)。放在同目录的 config/local.properties(已 gitignore):
nest.psyche.llm.api-key=sk-xxxxxxxx不配也能跑 —— 见上文「离线降级」。
# 开发运行
mvn -B -ntp spring-boot:run
# 打包(跳过测试)
mvn -B -ntp clean package -DskipTests
java -jar target/nest-0.0.1-SNAPSHOT.jar
# 含 CP-SAT 最优性校验(需要 OR-Tools,仅小规模)
mvn -Pcpsat -B -ntp clean package# 方式一:一键脚本(复位 → 按序加载 12 个 demo 文件 → 自检)
powershell -ExecutionPolicy Bypass -File tools/demo-seed.ps1
# 方式二:手工按序导入
sql/nest_seed.sql 管理员 / 教师 / 学生账号,密码统一 123456
sql/nest_xuegong_demo.sql 学工档案 + 辅导员分派
sql/nest_xuegong_eval_demo.sql 综测自评
# …其余 *_demo.sql 按 docs/给学校的部署说明.md §3 清单
sql/nest_demo_reset.sql 复位脚本(幂等,只删 E2E / 探针残留)演示账号(密码均为 123456):
| 账号 | 角色 |
|---|---|
admin |
管理员 |
zhangsan / lisi |
辅导员(各自名下学生不同) |
wangxm / zhaoxh / chendw |
学生 |
演示宿舍分配请走「生成合成数据」而不是「手工导入名单」—— 只有合成数据带隐真值,指标才是真值基准,否则会弹出
metricBasisWarning声明指标降级。
# 单元 / 集成测试(1,649 项)
mvn -B -ntp test
# 页面契约与静态检查
node tools/check-dom-ids.mjs src/main/resources/static # id 引用一致性
node tools/check-hidden-display.mjs src/main/resources/static # hidden/display 冲突
node tools/check-mapper-columns.mjs # entity 字段 ↔ mapper XML 可读性
node tools/check-inline-js.mjs <页面文件> # 内联脚本语法(逐页)
powershell -File tools/check-motion.ps1 # 动效 8 项
powershell -File tools/check-unify.ps1 # 视觉统一 26 页
powershell -File tools/check-redesign.ps1 # 页面结构 48 页
powershell -File tools/mobile-audit.ps1 -Widths 375,768 # 移动端溢出与触控目标
# 改动 CSS 后必须重建(前端零 CDN,样式全部本地构建)
powershell -File tools/webbuild/build-web-assets.ps1
powershell -File tools/webbuild/verify-css.ps1 # 期望输出 ALL HITsrc/main/java/com/umi/nest/
├── dorm/ ★ 宿舍分配算法内核
│ ├── solver/ 块分解 · 贪心构造 · 局部搜索 · 阻塞对修复 · 稳定性审计
│ ├── compat/ 相容度函数(接口 + 规则实现 + 学习型基线)
│ ├── explain/ 四维解释 · 申诉 · 跨块反事实
│ ├── psyche/ AI 辅助:素材 → 特征(规则 + LLM 融合)
│ ├── eval/ 评测:基线对比 · CP-SAT 最优上界 · 信息价值实验
│ └── web/ REST 接口
├── controller/ 学工业务 27 个 Controller
├── service/ mapper/ entity/ dto/ 业务分层
├── config/ 权限拦截器、安全配置等
└── verify/ wechat/ ai/ 验证码、微信、教学模块遗留 AI 客户端
src/main/resources/static/ 48 个页面 + 8 个脚本 + 本地 CSS(零框架)
src/test/java/ 36 个测试类
sql/ 建表与演示数据(注意旧脚本陷阱)
tools/ 门禁脚本 + CSS 构建链
docs/ 22 份设计/部署/答辩文档
| 想了解 | 看这里 |
|---|---|
| 部署到学校 | docs/给学校的部署说明.md(1,295 行,含数据库初始化逐条清单、配置项核对表、HTTPS/PWA、已知限制) |
| 算法设计与论证 | docs/superpowers/specs/2026-09-27-nest-dorm-assignment-design.md |
| 创新点与文献对照 | docs/创新点-不确定感知的宿舍分配.md · docs/宿舍分配算法-文献对照.md · docs/大创/创新点与文献差异化.md |
| 给老师演示 | docs/结题材料/网页演示方案.md(网页版,12 分钟五幕)· docs/结题材料/演示脚本.md(CLI 版) |
| 老师自己试用 | docs/老师试用剧本.md(三条路线,约 25 分钟) |
| 前端设计系统 | docs/前端设计系统说明.md |
| 手机端 H5 | docs/手机端H5适配交接.md(含真机验证清单与 12 条坑) |
| 权限与授权 | docs/权限与数据级授权审计.md |
| 答辩 / 结题 | docs/大创/答辩大纲.md · docs/大创/结题报告.md · docs/结题材料/答辩提纲.md |
- 真实数据 = 0:演示与评测全部使用合成数据(带隐真值)。真实数据没有隐真值,相容度指标只能是模型预测值,接口会主动声明,不能与合成数据的真值指标并排比较。
- AI 依赖公网:大模型走
api.deepseek.com,校园内网不通时模块自动降级为纯规则提取(功能可用,语义理解能力下降)。 - 无短信 / 邮件网关:验证码接口返回
delivered=false,码是真的但不投递。实现VerificationCodeSender并标@Primary即可接入。 - 权限是接口级粗筛:
SecurityConfig对请求permitAll,实际拦截靠ApiRoleGuardInterceptor的 24 条路由规则 + 服务层数据级校验,不是方法级细粒度鉴权。 - 申诉是局部重解:界面如实显示"在我找过的范围内不能满足",不是全局重解。
- CP-SAT 最优校验只覆盖小规模:OR-Tools 裁判需
mvn -Pcpsat构建,用于量化与最优解的 gap。 - 心理量表是占位:SCL-90 / SDS 的题项、常模、划界值等属专业内容,不臆造,只留结构与流程。
- 第二课堂学分阈值留空:学校未定,建表刻意不预置任何数字,三列可为 NULL。
- 微信登录未配置:
appid/secret为空,相关能力未启用。
本项目由课堂教学管理系统改造而来,教学模块已于 P8 下线(sql/nest_p8_teaching_offline.sql 会 DROP 那 11 张表)。课设报告.md 属于该阶段的历史文档。
前端依赖全部本地化(内网断网可用),不使用任何 CDN:
| 组件 | 许可 |
|---|---|
| daisyUI 4.12.24 · Tailwind CSS 3.4 | MIT |
| Open Props · Radix Colors | MIT |
| Instrument Serif | SIL OFL 1.1 |
| Lenis · tsParticles · Splitting · embla-carousel · @formkit/auto-animate | MIT |
| paper-design/shaders | Apache-2.0 |
| OR-Tools(CP-SAT,可选 profile) | Apache-2.0 |
明确未使用:GSAP(非 OSI 许可)、typed.js / TypeIt / ScrollReveal(GPL)、ReactBits(附加限制)、Origin UI(AGPL)、three.js。