每年带软件工程实训课,最让我头疼的往往不是学生写不出代码,而是几十份项目文档的流转和统计。选题申报表一张Excel、开题报告塞进邮箱、中期检查的文件用聊天软件传来传去,结题评分又要在纸质表上手动算平均分。去年我终于下决心做了这套基于SSM的程序设计实践项目管理系统,把课程设计从发布通知到结题归档的完整流程都收进一个网页端里。折腾了大概三周,从需求梳理到上线部署都跑通了,学生和助教用下来反馈不错。
这篇文章不是我第一次写项目总结,但这次我想把设计和实现过程拆得更开一些——为什么我把管理系统拆成三类角色、为什么坚持用SSM而不是直接上全家桶、数据库的9张表是怎么一步步推出来的、以及我在文件上传和事务回滚上踩过的几个实在坑。如果你正打算做课程设计类管理系统,或者想练手一下SSM的实际工程写法,这篇文章应该能给你省下不少试错时间。
1. 程序设计实训管理的真实痛点与需求收敛
1.1 手工流程里最折磨人的三个细节
动手之前,我先把旧流程里的问题列了一个清单。过去的实训管理几乎全靠人工:管理员发一份Excel模板让学生填选题信息,学生填完发回邮箱,老师再手动整理成汇总表。这里面的第一个折磨点是文件命名。十几个人交上来的文件叫"新建文档.docx"、"项目申报表(1).docx"都可以出现,助教对着名单一个个改名字,一改就是一下午。
第二个折磨点是项目状态的不可见。某小组到底有没有提交中期报告?教师评分录到哪一步?管理员问谁谁都说不清楚,只能挨个打开邮箱去翻记录。第三个折磨点是评审口径不统一。有的老师按百分制,有的按等级制,最后归档时还需要人工换算,稍不注意成绩就录错了。课程设计本身应该聚焦在代码和报告上,但管理成本却把指导老师的精力占掉了很大一块。
1.2 需求收敛:三个角色、五段流程
和学生聊了一圈之后,我意识到这类系统最忌功能大而全。有人建议做聊天模块,有人建议做查重接口,我最后全部砍掉,只保留最刚需的部分。
系统的使用角色收敛为三种:学生负责选题申报、填写项目信息、按阶段上传报告;教师负责审核申报、给出过程性评价和结题评分;管理员负责维护用户、调整项目分组、导出统计结果。业务流转围绕五段流程展开:选题申报、指导教师确认、开题阶段、中期检查、结题评分。这五个阶段之间用状态字段串联,每个阶段都有对应的表单和文件要求。
功能模块也对应地收敛成五个:登录与权限控制、项目申报与分组管理、阶段报告提交与审核、评分管理、后台基础数据管理。至于站内消息、通知推送、在线编辑文档这些,我明确标记为"二期再议"。很多时候少即是多,先把流程的主干跑通,比一开始就堆功能重要得多。
1.3 我把这些需求变成了系统边界
需求收敛最关键的是把"不要做什么"写清楚。我当时给自己定了几条边界:系统不做代码托管,不做自动查重,不做复杂协同编辑,也不做移动端适配。代码提交继续沿用原有的代码托管平台,本系统只负责管理项目申报、过程文档和评分结果。这样的边界让表结构清晰了很多,也让我能在三周内交付一个真正能用的系统,而不是一个永远在开发的半成品。
2. SSM框架选型逻辑与环境搭建:三周交付的前提
2.1 为什么选SSM而不是直接上Spring Boot
这可能是我最常被问到的问题。作为一个新项目,Spring Boot明显更省事,自动配置、内嵌Tomcat,几乎可以零配置启动。但选SSM我有三条理由。
第一,这是实训课程的真实要求。课程设计大纲里明确要求学生理解Spring、SpringMVC和MyBatis的分层整合过程,如果直接采用Spring Boot,学生看到的就只是注解和自动配置,框架底层的Bean加载、视图解析、SqlSessionFactory构建这些关键概念很容易被跳过去。第二,这套系统未来要作为教学案例示范,SSM的显式配置文件更便于讲解Controller调Service、Service调Mapper的完整调用链。第三,我自己的技术积累也主要在SSM,团队维护起来更有把握。
这并不代表SSM比Spring Boot优越。如果是从纯业务系统角度出发,我完全支持Spring Boot。选型这件事没有绝对的对错,关键看系统的定位和团队熟悉度。系统是要上生产的,不是用来炫技的。
2.2 环境准备清单与关键配置说明
我在自己电脑上搭建了一套可复制的工程环境。因为目标环境是实验室的Windows机器,所以选型上刻意避开了Docker、容器化这些在实验室内网不太好维护的东西。
| 组件 | 版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 实验室环境存量版本,兼容性最稳 |
| Maven | 3.6.3 | 统一依赖管理,离线插件包也已备好 |
| MySQL | 5.7 | 使用InnoDB引擎,字符集utf8mb4 |
| Tomcat | 8.5 | 以war包部署 |
| MyBatis | 3.5.x | 配合mybatis-spring,避免与Spring版本冲突 |
pom.xml中需要特别留意Spring和MyBatis的兼容版本。我的做法是先锁定Spring 5.1.9.RELEASE,再配上mybatis-spring 2.0.1,这样MyBatis的Mapper接口扫描和Spring的事务管理能平稳整合。有些同学直接用了较新版本的mybatis-spring,结果因为与Spring旧版不兼容,启动时一直报BeanCreationException,那个排查过程非常费时间。
2.3 一个可以直接复制的项目骨架
项目我采用的是标准的Maven单模块写法,没有额外拆多模块,原因很简单:多模块虽然分层清晰,但对一个教务管理类系统来说反而增加了构建成本。工程内的包结构分成了controller、service、mapper、entity、interceptor、common几层。启动入口在webapp目录下,spring-mvc.xml负责包扫描与视图解析,spring-mybatis.xml负责数据源与事务管理。
project-system ├── pom.xml ├── src/main/java │ ├── com.example.project.controller │ ├── com.example.project.service.impl │ ├── com.example.project.mapper │ ├── com.example.project.entity │ ├── com.example.project.interceptor │ └── com.example.project.common └── src/main/resources ├── jdbc.properties ├── mybatis-config.xml ├── spring-mvc.xml └── spring-mybatis.xml这个骨架我建议保留下来。只要把包名改成你自己的域名前缀,复制一份新的pom.xml,往里边加业务代码就行。SSM项目最麻烦的其实是前期脚手架搭建里的版本冲突,骨架定好之后,后面省掉的时间非常可观。
3. 数据库设计:从业务规则推演出9张表
3.1 用户与权限:为什么我拆成了一张用户表加一个角色字段
用户管理设计上有两种常见做法:一种是根据角色拆成学生表、教师表、管理员表,另一种是统一用户表加角色字段。我选了后者。
原因在于这个系统的角色并不是割裂的。同一个指导教师,可能同时是某个项目组的组长,也可能被管理员设置为课程的评审专家;如果拆成多张表,跨角色的信息查询就要频繁join,反而不好维护。最终我设计了sys_user表,包含user_id、username、password、real_name、role_type、department等字段,其中role_type用0表示管理员,1表示教师,2表示学生。密码使用MD5加盐存储,虽然不算顶级安全强度,但对课设系统已经够用。
3.2 项目表与状态流转:用status字段驱动整个流程
整个系统的核心是project_info表,表里的字段包括了申报信息、小组信息、指导教师、当前阶段和整体状态。我没有为每个阶段单独建表,而是在同一张表上用status字段驱动状态机,阶段流转的每一步操作都在Service层校验状态是否合法。
| status | 含义 | 允许的下一步 |
|---|---|---|
| 0 | 草稿/待提交 | 1 |
| 1 | 待教师审核 | 2或退回0 |
| 2 | 审核通过/进行中 | 3 |
| 3 | 已提交开题材料 | 4 |
| 4 | 已提交中期材料 | 5 |
| 5 | 已提交结题材料 | 6 |
| 6 | 已评分/归档 | - |
这样的设计在写入时只需要update一个整数,查询时也能用索引高效过滤。代价是每次状态更新都必须写清楚转换规则,不能让学生直接从0跳到5。我把状态转换的逻辑全部封装在Service层,Controller不允许直接操作status字段,这样就堵住了前端越权提交的可能。
3.3 文档表与阶段记录的拆分
一个项目需要提交多种类型的文件:申报表、开题报告、中期报告、结题报告和最终成果物。如果每种文件建一张表,后面加一种文件类型就得加一张表,非常不灵活。我的解决办法是建一张document_file表,字段包含file_id、project_id、stage_type、file_name、file_path、upload_time和uploader_id。这样无论系统以后增加什么材料,都只需要在代码里新增一种stage_type常量,不需要改表结构。
另外还设计了一张stage_record表,记录每个项目在每个阶段的提交和审核时间,相当于操作日志。比如后期需要统计"有多少项目在中期阶段逾期提交",直接查stage_record按时间过滤就行,不用翻历史文件。
3.4 索引设计上的两个小教训
第一,不要在每个字段上都加索引。我一开始给project_info表的project_name也加了索引,想着方便按名称搜索。但实际业务中用户搜索更多是组合条件,项目名称搜索频率并不高,反而因为索引维护拖慢了写入。第二,联合索引要贴合查询模式。我最终只保留了三组核心索引:status单列索引、teacher_id + create_time联合索引、student_id在项目成员关联表上的单列索引。查询性能在几百条数据量下就是毫秒级,完全够用了。
4. 核心功能实现:登录拦截、项目流程和文件上传
4.1 登录鉴权与三级拦截:一个HandlerInterceptor的成长史
SSM的登录鉴权核心是SpringMVC的拦截器。我实现了一个AuthInterceptor,它实现了HandlerInterceptor接口,在preHandle方法里获取当前Session中的登录用户,判断是否为空,如果为空就重定向到登录页。
矛盾在于:登录接口本身、静态资源、以及少数几个公开的接口不应该被拦截。我的处理是在spring-mvc.xml里配置了mvc:interceptors,对登录接口和静态资源路径单独排除。同时,因为系统有三类角色,拦截器里除了判断登录态,还会用@RequireRole注解配合角色码做权限校验,比如只有教师和管理员能访问评分页面。这个方法虽然没有Spring Security那么完备,但放在课设系统里足够清晰,也方便学生理解权限控制的基本原理。
4.2 项目申报与状态流转的服务层设计
项目申报流程是系统的主干逻辑,我从一开始就要求自己把事务边界放在Service层。一个典型的申报操作会同时做两件事:插入project_info记录、插入项目成员关联记录。如果Controller里分别调用两次Mapper方法,中间一旦出异常,就会出现"项目表有数据,成员表没数据"的光头数据。我的做法是在ProjectServiceImpl里提供一个createProjectWithMembers方法,用@Transactional(rollbackFor = Exception.class)标注,方法内完成两条insert。这样任一步骤抛异常,事务都可以回滚。
阶段审核逻辑也走同样的思路。教师提交审核结果时,需要同时更新项目状态、写入stage_record审核记录、如果需要退回还可能要清空学生提交的文件标记。这三个操作在同一个事务里完成,才能避免"状态变成审核通过,评分记录却没有写进去"的尴尬情况。
4.3 文件上传:本地存储策略和防重名设计
文件上传应该算是这个系统里最容易出岔子的模块。我采用了本地磁盘存储的方案,没有引入OSS或FastDFS,因为实验室内网环境没有公网存储可用。存储路径的规划是:在上传目录里按年份和项目号建立二级目录,比如/upload/2025/project-03/,每个项目的所有阶段性材料集中在这个目录下。
文件命名我处理得比较细致。用户上传的原始文件名是中文,操作系统层面没问题,但Tomcat在某些编码配置下很容易出乱码;如果用原始文件名直接覆盖保存,两个不同阶段上传同名文件时就会互相覆盖。最终我统一改成项目ID_阶段类型_时间戳.扩展名的格式,例如105_2_20250410143015.pdf。文件名里保留用户原文件名是在document_file表的original_name字段里,这样归档时还能看到最初的命名,也不会造成磁盘冲突。
同时,上传接口限制了单个文件不超过20MB,并在Service层校验文件格式白名单,只允许pdf、doc、docx、zip和rar。限制用校验一个白名单,简单有效。
4.4 MyBatis动态SQL处理"多条件组合+分页"的典型场景
项目列表页是最容易写出复杂SQL的地方。学生要看自己参与的项目,教师要看自己指导的项目,管理员要看全部项目,还要同时支持按状态过滤和按关键词搜索。如果为每个场景写一个固定SQL,Mapper接口会爆炸。我使用的是MyBatis的<where>标签配合<if>标签做条件拼接。
<select id="selectProjectList" resultMap="ProjectResultMap"> SELECT * FROM project_info <where> <if test="status != null and status != ''"> AND status = #{status} </if> <if test="keyword != null and keyword != ''"> AND (project_name LIKE CONCAT('%', #{keyword}, '%') OR project_no LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="teacherId != null"> AND teacher_id = #{teacherId} </if> </where> ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} </select>有一点必须提醒:MyBatis在做模糊查询时,LIKE CONCAT('%', #{keyword}, '%')是最稳妥的写法,避免直接拼%${keyword}%,否则会产生SQL注入隐患。列表分页我采用的是简单的LIMIT offset, pageSize,数据量目前百级规模够用。等数据量到万级以上,再换PageHelper或其他分页组件也不迟。
5. 踩坑实录:从白屏到数据错乱的排查链路
5.1 现象一:Mapper接口报"Parameter 'xxx' not found"
系统刚搭建完时,我一运行列表接口就在后台看到org.apache.ibatis.binding.BindingException: Parameter 'status' not found。排查后发现是因为selectProjectList接收了两个以上参数,我最初在Mapper接口里写的实现是这样的:
List<ProjectInfo> selectProjectList(Integer status, String keyword, Integer teacherId);MyBatis在处理多参数方法时,如果参数没有加@Param注解,它无法把参数名和SQL中的#{status}对应起来。解决方案是在接口方法参数上加上:
List<ProjectInfo> selectProjectList(@Param("status") Integer status, @Param("keyword") String keyword, @Param("teacherId") Integer teacherId);这是SSM入门最常见的坑,但排查起来需要一点经验:先看控制台错误,错误信息里其实已经明确说了缺少参数,顺着思路找接口签名即可,不要去检查xml的namespace拼写。
5.2 现象二:事务回滚失效,异常被"吞"成数据错乱
测试阶段发现了一个很隐蔽的问题:当Service层调用同一个类内部的另一个@Transactional方法时,事务竟然没有生效。一个项目插入操作如果走到中途抛异常,数据库里还是会多出一条没有成员信息的孤儿数据。
这个问题的根因是Spring的事务是通过AOP代理实现的。同一个类内部调用自己的另一个方法,调用的是原生对象的方法,不会经过代理对象,所以事务注解不生效。我的改造方法是把需要独立事务的操作拆到不同的Service类中,由Controller层调用两个Bean,或者在ServiceImpl注入自身的代理对象。更推荐前一种做法,职责也更清晰。从此我给自己定了一条规矩:事务注解放在外部可调用的Service接口实现上,绝不在同类内部方法之间做事务调用。
5.3 现象三:中文文件名上传后变成乱码
文件模块上线后,有同学反馈从系统下载的文件名是乱码,但文件内容完好。这个问题和Tomcat的URI编码有关。Tomcat 8.5默认使用的是UTF-8,但如果连接器没有配置URIEncoding="UTF-8",POST请求中的中文文件名在各环节流转时就可能被错误解码。
我的排查过程分了三步:先看浏览器发起的请求头编码,再看SpringMVC接收到的MultipartFile.getOriginalFilename()返回值,最后发现前端Content-Type里少了charset=UTF-8。修复方案有两处:一是在SpringMVC配置文件里配置multipartResolver编码为UTF-8,二是在Tomcat的server.xml连接器配置里加上URIEncoding="UTF-8",两者缺一不可。
5.4 现象四:上百个学生同时提交,数据库连接被抽干
这个坑是临近验收时才暴露的。课程集中提交日,机房几十台机器同时访问系统,后台日志出现了Too many connections。原因在于数据源使用的是默认连接池,没有配置最大连接数,也没有设置合理的等待超时。我给spring-mybatis.xml中的DruidDataSource增加了配置,把初始连接数设为5,最小空闲连接数调整为5,最大活跃连接数设为50,maxWait等待时间设置为6000毫秒。同时把MySQL端的max_connections从默认值调到了200。调整之后,压力测试中并发50的情况下,连接不再被打满,响应时间也稳定在可接受范围内。
6. 部署测试与我对这套系统的使用评价
6.1 从war包到机房可用:部署过程中的几个细节
部署环节我用Maven打成war包,放在Tomcat的webapps目录下启动。第一次部署时我图省事直接把包丢进去,结果访问/路径时只能看到Tomcat默认页面,后来才发现需要将项目命名为ROOT.war才能直接使用根路径访问。如果你不想改文件名,也可以访问/项目名/来进入系统,只是学生每次都要记路径,体验差一些。
另外一个值得提前准备的细节是数据库初始化脚本。我在sql目录里放了init.sql和sample_data.sql两份脚本,前者建库建表和写入基础字段类型,后者插入几个模拟项目组数据用于功能演示。机房部署时直接执行source init.sql就完成了数据库初始化,不用在服务器上逐步点选,省了很多沟通成本。
测试方面,我用了轻量级的JUnit测试覆盖了核心Service层的几个状态流转用例。测试的重点放在:非法状态跳转是否会被拦截、重复提交文件是否会被拒绝、教师评分后项目状态是否自动变为归档。UI层面的回归测试反倒是手工完成的,因为系统没有复杂交互,手工过一遍主要流程比写自动化测试更快。
6.2 真实使用效果与我的复盘
这套系统在真实课程中跑完整轮之后,给我最直观的感受是:**项目状态的可见性大大提升。**以前教师需要反复催促学生提交材料,现在系统首页按状态做了统计,哪些项目卡在选题审核、哪些项目逾期未提交中期材料,管理员一眼就能看到。学生也学会了主动看状态,而不是一直发消息问"老师我交了吗"。
当然,不完美的地方也很多。我最想改进的是通知功能,如果能做到材料过期的自动提醒,整个系统才算真正闭环。还有评分模块目前只支持平均分计算,如果后续能按多位评委的权重系数生成最终成绩,会更贴近真实教务规则。不过作为一次限时交付的实训管理系统,它已经在很大程度上解决了旧流程的痛点。
6.3 如果让我再重写,我会在这些点上直接动手
最后说说复盘得到的优化方向。第一,我会把前端从JSP改成一个单纯的HTML+静态资源,再用AJAX调用接口,目前JSP和SpringMVC耦合得有点紧,换前端工程师维护成本会偏高。第二,我会在项目创建时同步生成一个唯一的访问码,学生通过访问码加入项目组,而不是依赖管理员手动配成员,能省下不少基础数据维护的工作。第三,我会把status字段从int改成更可读的枚举类,减少到处用魔法数字的问题。
这些建议不一定适合所有场景,如果你只是拿来做课程设计或者内部工具,能跑通主干流程就已经赢了。如果你是在团队里长期维护,我建议至少把前两条纳入迭代计划。
总之,用SSM做这类管理系统,最重要的不是堆砌新框架,而是先把业务流程弄清楚,把状态流转建模做严谨。框架只是实现手段,真正决定系统好不好用的是你对流程的理解深度。这套系统的完整代码我已经整理成了起步模板,接下来可以单独写一篇关于Excel导入导出和系统提醒功能的后续扩展文章,感兴趣的话可以继续关注。