1. 先把这个系统的边界划清楚
1.1 班级事务管理系统到底管什么
带过毕业设计的人都有个共识:题目里带“管理系统”四个字的,十个有八个最后做成了一张表加增删改查。班级事务管理系统尤其容易滑向这个坑,因为“班级事务”这个说法太宽了——考勤算不算事务?奖助学金评定算不算?班费收支算不算?宿舍卫生检查算不算?如果一开始不划边界,做到中期你会发现表越加越多,功能越铺越散,最后交上去的东西自己都说不清主线是什么。
我的理解是,班级事务管理系统的核心不是“记录”,而是“流转”。一次请假申请从学生提交、班长初审、辅导员终审到销假归档,这条链路上的状态变化才是系统的价值所在;一次班级活动的报名、名额分配、签到确认、活动总结,也是一条链路。把这些链路抽象成统一的“事务单”模型,配上角色权限和状态机,系统就有了骨架,剩下的考勤、通知、统计都是挂在这个骨架上的枝干。
所以这个项目适合谁来参考?如果你是计算机相关专业正在做毕设的同学,这套东西能直接给你提供需求分析、表结构、核心代码和开题报告写作思路;如果你是刚接触前后端分离开发、想找一个业务不算复杂但五脏俱全的练手项目,它也挺合适,毕竟班级场景你天天在经历,需求不用编。不适合的是想找高并发、分布式练手的人,这个系统的量级撑死几百人,别硬套微服务。
前100字必须把关键词落地,那我就直说:高校班级事务管理系统,本质上是一个带审批流的轻量级业务中台,服务对象是辅导员、班委和普通学生三类角色,核心价值是把散落在聊天群里的通知、接龙、表格收敛成结构化数据。
1.2 三类角色决定了整个系统的权限骨架
角色划分这件事,很多人画用例图的时候画得很漂亮,一写代码就开始偷懒,搞一个role字段存个 0/1/2 就完事,结果做到后面发现“班长能审批本班但不能审批别班”这种带数据范围的权限根本兜不住。
我的做法是角色和数据范围分开设计。角色表里存三类身份:学生、班委(含班长、团支书等)、辅导员。数据范围单独用一个字段或者一张关联表来描述,比如班委的scope_type是CLASS,scope_id是本班 ID;辅导员的scope_type是GRADE或者MAJOR,scope_id是对应的年级或专业 ID。这样判断权限的时候就是两步:先看角色能不能做这个动作,再看这条数据在不在他的范围内。
这种设计的直接好处是,后面如果要加一个“学院管理员”角色,不用改业务代码,只要插一条角色记录和一个范围记录就行。坏处是实现上比单纯的 if-else 麻烦一点,但这点麻烦在后期联调阶段会成倍地还给你。
1.3 三个最容易把项目做废的坑
第一个坑是功能贪多。开题报告里雄心壮志写了十二个模块,做到第五个就开始复制粘贴,最后交上去的代码里有一半是没跑通的僵尸接口。我的建议是主功能控制在五到六个,每个都跑通全流程,剩下的用“预留扩展”在报告里说明,反而显得你有规划。
第二个坑是权限校验只做前端。菜单藏起来就以为安全了,后端接口裸奔。答辩的时候老师随手用接口工具一调就穿了,这个场面很难看。校验必须在后端做,前端隐藏只是体验优化。
第三个坑是不做数据初始化。交上去的系统打开一片空白,老师第一眼就扣分。至少准备一份种子数据脚本,造三个班级、几十个学生、十几条不同类型的事务记录,让系统打开就是“活”的。
2. 技术选型:为什么最后落到这套组合
2.1 后端为什么选 Spring Boot 而不是别的
毕设场景下选后端的逻辑跟企业里不太一样,企业考虑的是团队栈、运维成本、生态成熟度,毕设考虑的是“能不能在两个月内跑通并且答辩时讲得清楚”。Spring Boot 在这两点上都是稳妥解:自动装配省掉了大量 XML 配置,起步一个main方法就能跑;生态足够大,你遇到的九成问题网上都有现成答案;分层结构清晰,Controller-Service-Mapper 三层摆出来,老师一看就知道你懂分层。
也有人问能不能用 Python 的 Flask 或者 Node 的 Express,当然可以,代码量还更少。但答辩的时候有个现实问题:国内大部分院校的评审老师对 Java 技术栈更熟悉,你讲 Spring Boot 的拦截器、AOP、事务管理,他能接得上话;你讲 Express 的中间件洋葱模型,他可能要愣一下。这不是技术优劣,是沟通成本。
具体版本上,我倾向于 JDK 8 或 JDK 11 配 Spring Boot 2.7.x。JDK 17 配 Spring Boot 3.x 也不是不行,但 Spring Boot 3 把javax.*全换成了jakarta.*,很多老教程的代码直接抄会报错,对新手不友好。MyBatis-Plus 用来省掉基础 CRUD 的样板代码,分页插件直接用它的,省得自己写。MySQL 5.7 或 8.0 都行,字符集统一utf8mb4,这个别省,不然学生姓名里的生僻字会变成问号。
JWT 做无状态认证,配一个拦截器统一校验。文件上传单独抽一个工具类,本地存upload/目录下,数据库只存相对路径,别把二进制往库里塞。
2.2 前端和部署方案的取舍
前端选 Vue 3 加 Element Plus,理由很实际:组件库现成,表格、表单、弹窗、分页全都有,省下来的时间可以放在业务逻辑上。用 Vue 2 也行,生态更老更稳,但 Vue 3 的组合式 API 在写权限判断这种复用逻辑的时候确实清爽。
前后端分离的部署方式有两种。一种是前端npm run build出静态文件,丢到 Nginx 里,Nginx 反向代理/api到后端端口,这种方式最接近生产环境,答辩演示也稳;另一种是偷懒把打包产物塞进 Spring Boot 的static目录,打成一个大 jar,双击就能跑,演示方便但不够规范。我的建议是开发期用第一种练手,交付的时候额外准备一个“一键启动包”作为兜底,万一现场网络或者 Nginx 出问题,不至于手忙脚乱。
数据库连接池用 HikariCP,Spring Boot 默认就是它,配置里把maximum-pool-size设成 10 就够了,别照抄网上的 50、100,几百人的系统用不上,反而容易把本地 MySQL 的连接数打满。
2.3 项目目录结构建议这么切
src/main/java/com/example/classaffair ├── common # 统一返回、异常、常量、工具 ├── config # 拦截器、跨域、MyBatis 配置 ├── controller ├── service │ └── impl ├── mapper ├── entity ├── dto # 入参 ├── vo # 出参 └── handler # 全局异常处理DTO 和 VO 分开这件事,很多新手会嫌麻烦,直接用实体类接收和返回。小项目确实能跑,但一旦涉及到“创建事务单时不需要传 id 和创建时间”或者“返回列表时不希望把密码字段带出去”,你就得回头改。一开始分开,后面省事。
3. 数据库设计:这个系统真正的骨架
3.1 核心表与字段设计
数据库设计是这类项目里最能体现功底的部分。我不建议一上来就画 E-R 图,先把几个问题在纸上写清楚:有哪几类“东西”需要独立存在?它们之间是一对多还是多对多?哪些状态是会变化的?
这个系统的核心实体大致是这些:
| 表名 | 作用 | 关键字段 |
|---|---|---|
sys_user | 统一用户表 | id, username, password, real_name, role, class_id, status |
sys_class | 班级 | id, class_name, grade, major, counselor_id |
biz_affair_type | 事务类型字典 | id, type_name, need_approve, sort |
biz_affair | 事务主表 | id, type_id, title, content, creator_id, class_id, status, create_time |
biz_affair_flow | 审批流转记录 | id, affair_id, operator_id, action, remark, create_time |
biz_notice | 通知公告 | id, title, content, publisher_id, scope, publish_time |
biz_notice_read | 通知已读记录 | id, notice_id, user_id, read_time |
biz_attachment | 附件 | id, biz_type, biz_id, file_name, file_path |
用户表用一张sys_user统管所有角色,靠role字段区分,而不是建三张表。理由是登录入口只有一个,三张表会逼你在登录逻辑里写三段查询,维护起来烦。代价是学生和辅导员的字段差异(比如学生有学号、辅导员有工号)得放在同一张表里,用字段冗余解决。这点冗余可以接受。
biz_affair表的设计是重点。它把请假、活动报名、班费申请、设备借用这些业务都收进一张表,靠type_id区分,公共字段(标题、内容、状态、创建人、所属班级)统一,差异化字段用ext_json存 JSON 字符串。这种“宽表 + JSON 扩展”的做法在业务类型不多、字段差异不大的场景下非常划算,省掉了为每种事务建一张表的麻烦。缺点是 JSON 里的字段没法直接建索引查询,如果某个类型的事务需要按扩展字段筛选,那就得在建表的时候单独拆出去。
3.2 状态机的设计要提前想明白
状态机是审批流的核心。我见过太多项目把状态存成status字段,值是 0、1、2、3,然后代码里到处写if (status == 1),三个月后自己都忘了 2 代表什么。
我的做法是用一个枚举类把状态和流转规则都固定下来:
public enum AffairStatus { DRAFT("草稿", 0), PENDING("待审批", 1), APPROVED("已通过", 2), REJECTED("已驳回", 3), FINISHED("已完成", 4), CANCELED("已撤销", 5); private final String label; private final int code; // 构造、getter 省略 public static boolean canTransfer(int from, int to) { // 状态流转白名单 Map<Integer, List<Integer>> rules = new HashMap<>(); rules.put(0, Arrays.asList(1, 5)); rules.put(1, Arrays.asList(2, 3, 5)); rules.put(2, Arrays.asList(4)); return rules.getOrDefault(from, Collections.emptyList()).contains(to); } }有了这个白名单,Service 层每次改状态前先调一次canTransfer,非法流转直接抛业务异常。这一步能拦掉大量脏数据,比如“已驳回的单子被改成已通过”或者“已完成的单子又被重新审批”。
biz_affair_flow这张流转记录表也别省。每一次状态变化都插一条记录,记录操作人、动作、备注和时间。这既是审计日志,也是给学生端展示“审批进度条”的数据来源。答辩时老师问“你的审批流怎么实现的”,你把这个表一展示,比讲十分钟代码都管用。
3.3 索引和查询上的几个实操细节
数据量不大不代表不用管索引。biz_affair表上至少要有这几个索引:idx_class_status (class_id, status)用于班委查看本班待办,idx_creator (creator_id)用于学生查看自己提交的记录,idx_create_time (create_time)用于列表倒序分页。
分页这里有个容易踩的坑:MySQL 的LIMIT offset, size在 offset 很大时会变慢。虽然这个系统的数据量根本到不了那种程度,但写代码的时候顺手用 ID 游标分页或者干脆限制最大页数,是个好习惯。至少别把size参数直接透传给 SQL,用户传个 100000 进来,一次查询就把内存拉爆了。在 Controller 层做一次参数校验,size超过 100 就强制改成 100。
还有一个细节是逻辑删除。sys_user和biz_affair都建议加一个deleted字段,用 MyBatis-Plus 的@TableLogic注解,删除操作自动变成更新。物理删除在演示阶段看不出问题,但一旦误删了数据要恢复,就只能重装数据库。
4. 核心功能落地:一条完整链路的实现
4.1 登录认证与权限拦截
登录流程本身不复杂:前端传用户名密码,后端查库比对,比对用的是 BCrypt 加密后的密文,别用 MD5,更别明文存。校验通过后生成 JWT,payload 里放userId、role、classId,有效期设成 2 小时,前端存 localStorage,每次请求带在Authorization头里。
真正的活在于拦截器。我写了一个AuthInterceptor,实现HandlerInterceptor,在preHandle里做三件事:解析 token、把用户信息塞进ThreadLocal、校验当前请求路径是否在白名单里。
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } String token = request.getHeader("Authorization"); if (StringUtils.isBlank(token)) { throw new BizException(401, "未登录"); } Claims claims = JwtUtil.parse(token); UserContext.set(new LoginUser( claims.get("userId", Long.class), claims.get("role", Integer.class), claims.get("classId", Long.class) )); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.remove(); // 必须清理,否则线程池复用会串数据 }afterCompletion里的remove()是必须的。Tomcat 的线程是复用的,你不清理,下一个请求可能拿到上一个用户的身份,这个 bug 诡异到能让人查一整天。
更细粒度的权限用一个自定义注解@RequireRole配合 AOP 实现,标在 Controller 方法上,切面里比对当前用户角色。数据范围校验放在 Service 层做,因为只有 Service 才知道这条数据属于哪个班。
4.2 事务申报与审批流的实现
这是整个系统的主干,我按一次请假申请来拆。
学生端填表单,选事务类型(请假),填开始时间、结束时间、事由,上传证明材料,点提交。后端收到后做三件事:校验时间区间合法性(结束时间必须晚于开始时间,且不能早于当前时间)、校验该学生在此时间段内没有其他未完结的请假、插入biz_affair记录并把状态置为PENDING。
这里有个业务细节值得说:请假这种事务往往有“时长阈值”的概念,比如三天以内班长批就行,三天以上要辅导员批。这个规则不要硬编码在 if-else 里,做成一张biz_approve_rule配置表,字段是type_id、min_days、max_days、approver_role。Service 里根据申请时长去查对应的审批角色,把待办推给对应的人。这样以后规则变了改数据就行,不用改代码。当然,毕设层面用配置类写死也可以接受,但报告里提一句“预留了规则可配置化能力”,是加分项。
审批人登录后看到待办列表,列表查询条件是status = PENDING且class_id在自己的数据范围内。点同意,后端先校验当前状态确实是PENDING,再校验当前用户确实有这个权限,然后更新状态、插入流转记录、给学生发一条站内通知。三个操作放在同一个@Transactional方法里,保证要么全成要么全败。
驳回的逻辑要多一个字段:驳回原因。这个原因要写进流转记录的remark里,学生端能看到,否则学生只知道被拒了但不知道为什么,体验极差。
4.3 通知与已读状态的处理
通知模块看着简单,但有个经典问题:怎么知道谁读了、谁没读。
笨办法是biz_notice表加一个read_user_ids字段存逗号拼接的 ID,查询的时候用FIND_IN_SET。数据量小的时候能跑,但这是典型的反范式设计,一旦要统计已读率或者做“未读红点”,就会很难受。
正规做法是单独一张biz_notice_read关联表,(notice_id, user_id)建唯一索引。前端拉通知列表的时候,用LEFT JOIN关联这张表,read_time为 null 的就是未读。红点数量就是未读条数,一个count查询解决。
通知的可见范围也要设计。scope字段存ALL、CLASS、GRADE,配合scope_id。查询时拼条件:ALL的直接可见,CLASS的要求scope_id等于当前用户班级,GRADE的要求年级匹配。这个条件建议写成一个 MyBatis 的<sql>片段复用,避免每个查询都手写一遍拼错。
4.4 统计看板与数据导出
统计模块是答辩时最容易出彩的地方,因为它能直观展示“系统产生了什么价值”。
我做了三块统计:事务类型分布(饼图)、近三十天事务提交趋势(折线图)、各班级事务处理及时率(柱状图)。前两个用一条GROUP BY查询就够,第三个稍微复杂点,需要计算每条事务从PENDING到终态的时间差,再按班级聚合。
SELECT a.class_id, c.class_name, ROUND(AVG(TIMESTAMPDIFF(HOUR, a.create_time, f.create_time)), 1) AS avg_hours, COUNT(*) AS total FROM biz_affair a JOIN biz_affair_flow f ON f.affair_id = a.id AND f.action IN ('APPROVE', 'REJECT') JOIN sys_class c ON c.id = a.class_id WHERE a.create_time >= DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY a.class_id, c.class_name ORDER BY avg_hours ASC;导出功能用 EasyExcel,别用 POI 原生 API,那个写起来太啰嗦。导出的时候注意两点:一是大数据量要分页查再写,别一次selectList全捞出来;二是字段名用中文表头,日期格式化好,导出的表格能直接给人看才有意义。
5. 开题报告怎么写才不像模板
5.1 研究背景和意义别写成套话
开题报告的第一段,十份里有八份是“随着信息技术的飞速发展,高校信息化建设不断深入……”。这种开头评审老师一天看几十份,直接跳过。
我建议从具体问题切入,写你观察到的真实场景。比如:班级事务长期依赖即时通讯群,一条通知发出去,回复“收到”的人刷屏,真正重要的信息被淹没;请假靠私聊,辅导员手机里几百条记录没法归档;班费收支用表格记录,学期末对账费时费力。这些都是可以写进报告的具体痛点,比空泛的“提高效率”有说服力得多。
研究意义分两层写就够了:实践层面是解决上述具体问题,理论层面是探索轻量级审批流在班级这种小规模组织中的适配方案。别硬扯“填补领域空白”,一个班几十人的系统填补不了什么空白,老老实实写“具备一定的工程实践参考价值”反而得体。
5.2 国内外研究现状的写法
这部分最容易被写废,因为大部分同学根本没读过外文文献,硬编会露馅。
我的建议是分三类来写:通用办公协作平台(这类产品功能全但重,班级场景用不上那么多)、校园信息化系统(这类系统通常由学校统一建设,颗粒度到院系,班级层面的个性化需求覆盖不到)、开源的学生管理系统(这类项目往往功能齐全但技术栈陈旧或者文档缺失,二次开发成本高)。
每一类写清楚“它做了什么、它没解决什么”,最后自然过渡到“所以需要一个轻量、聚焦班级场景、可自主部署的方案”。这个逻辑链条是站得住的,而且不需要你捏造文献。参考文献里放几篇国内期刊的班级管理系统论文,再放两三本 Spring Boot 和 Vue 的技术书,加上官方文档链接,足够了。
5.3 研究内容和技术路线要能对上
很多开题报告的毛病是前面说要做智能推荐、大数据分析,后面的技术路线里全是 CRUD,前后对不上。评审老师一眼就能看出是凑字数。
我的原则是:研究内容写几条,技术路线就对应几条,功能模块和表结构也能对应上。比如研究内容第一条是“班级事务申报与多级审批流程设计”,那技术路线里就要有状态机设计、流转记录表设计、审批规则配置这几项,数据库设计里就要有biz_affair、biz_affair_flow这些表。三处能对上,说明你真的想清楚了。
进度安排按周来排,别排得太满。留出两周机动时间给联调和 Bug 修复,这个预留不是浪费时间,是给自己留活路。答辩前一周专门用来准备演示数据和 PPT,这个环节的重要性不亚于写代码。
可行性分析分技术、经济、操作三个维度写。技术可行性讲技术栈成熟、资料丰富;经济可行性讲全部使用开源免费组件,一台普通电脑就能跑;操作可行性讲界面遵循常见交互习惯,用户不需要培训。三个维度各写一小段,别写太长,这部分不是重点。
6. 踩过的坑和排查实录
6.1 环境与依赖类问题
中文乱码是最常见的。三个地方要统一:数据库建库时指定utf8mb4,JDBC 连接的 URL 里加characterEncoding=utf8,前端请求头设置Content-Type: application/json;charset=UTF-8。三处都对了还乱码,就检查 Nginx 有没有配置charset utf-8。
跨域问题在前后端分离开发阶段必现。开发期在配置类里加一个CorsConfig放行本地端口,生产环境用 Nginx 同源代理就不存在跨域了。注意allowedOrigins不要图省事写*同时开allowCredentials,浏览器会直接拒绝。
时间字段差八小时,这是时区问题。JDBC URL 里加serverTimezone=Asia/Shanghai,实体类用LocalDateTime而不是Date,Jackson 序列化配置spring.jackson.time-zone=GMT+8。这三步做完基本就不会错了。
6.2 业务逻辑类问题
并发提交重复审批。两个辅导员同时点同意,可能都会通过。解决办法是在更新状态时加乐观锁,SQL 写成UPDATE biz_affair SET status = ? WHERE id = ? AND status = ?,用影响行数判断是否更新成功,返回 0 就说明被别人抢先了,提示“该事务已被处理”。
分页总数不对。用 MyBatis-Plus 的分页插件时,如果 SQL 里自己写了UNION或者复杂的子查询,总数统计可能出错。遇到这种情况就把 count 查询单独写一个 SQL,别依赖自动生成。
文件上传后访问 404。Spring Boot 默认不允许直接访问项目外的目录,需要配置WebMvcConfigurer把上传目录映射成静态资源路径。或者干脆把文件存到resources/static下的子目录,但这种做法在打成 jar 之后会有问题,不推荐。
6.3 答辩时可能被问到的问题
整理了一张表,提前准备比临场发挥靠谱:
| 问题 | 回答思路 |
|---|---|
| 为什么不用 Redis | 系统并发量在百人级别,本地缓存加数据库索引足够;引入 Redis 会增加部署复杂度,不符合轻量定位 |
| 审批流怎么支持多级 | 当前实现了两级(班委、辅导员),流转记录表已按多级设计,扩展时只需在规则表里增加层级配置 |
| 数据安全怎么保证 | 密码 BCrypt 加盐存储;接口层做 JWT 校验和角色鉴权;敏感操作记流转日志 |
| 如果班级合并了怎么办 | 班级表设计了年级和专业字段,合并时批量更新用户的class_id,历史事务记录保留原class_id不受影响 |
| 系统的瓶颈在哪 | 在统计报表的聚合查询上,数据量增长后可以通过加汇总表或者定时预计算解决 |
这些问题背后的逻辑,其实就是让你证明自己不是照着教程抄的。答不上来不可怕,胡编才可怕,坦诚说“这部分目前是简化处理,后续可以这样扩展”是完全能接受的答案。
7. 交付与演示的最后一步
7.1 打包部署的完整流程
前端在项目根目录执行npm run build,产物在dist目录。把dist整个拷到服务器,Nginx 配置这样写:
server { listen 80; server_name localhost; location / { root /opt/app/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files那一行不能少,否则前端路由刷新会 404。
后端用mvn clean package -DskipTests打包,产出的 jar 用nohup java -jar xxx.jar --spring.profiles.active=prod > app.log 2>&1 &启动。生产配置里把数据库密码、JWT 密钥放到单独的application-prod.yml,不要提交到代码仓库。
7.2 演示前的检查清单
演示当天出问题的项目,八成倒在下面这几条上,提前挨个过一遍:
- 数据库连接池能不能正常拿到连接,重启一次 MySQL 试试
- 种子数据是否完整,学生账号能不能登录,待办列表里有没有数据
- 上传附件的目录是否有写权限
- 浏览器缓存是否清过,前端静态资源版本对不对
- 关键流程是否准备了第二条数据,第一条演示失败还有备份
另外准备一个纯静态的演示视频作为终极兜底,五分钟,把主流程录一遍。现场投影仪分辨率不对、网络断了、电脑蓝屏,这些事都发生过,有视频至少不会全场尬住。
我个人在带这类项目时有个习惯:交稿前一周,让完全没参与开发的同学按着使用手册独立操作一遍。他卡住的地方,就是文档要补的地方,也是答辩时老师可能问的地方。这个笨办法屡试不爽,比自己对着一遍遍点点点有效得多。
这个系统后续真要扩展,方向其实挺清楚:把审批规则做成可视化配置界面、给事务类型加自定义表单设计器、接入消息推送让待办不再依赖用户主动登录查看。但这些都是后话,先把主干跑通、把报告写实,比什么都重要。