ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于SSM框架的学生实习管理系统开发:从数据库设计到部署上线

基于SSM框架的学生实习管理系统开发:从数据库设计到部署上线 每年到了毕业设计验收季我都会收到一大批“实习管理系统”相关的问题。这个题目表面上就是个标准增删改查但真正把学生、指导教师、企业信息、实习材料、审核流程这几条线理顺的项目十个里面能有两三个算优秀就不错了。今天这篇文章我就以基于SSM框架的计算机专业学生实习系统为例把从需求拆解、数据库设计、核心代码落地到部署上线的完整思路讲一遍。如果你正打算拿它做毕业设计或者想通过一个完整的SSM项目把Spring、SpringMVC、MyBatis三者到底怎么协作弄明白照着这条脉络走基本不会跑偏。做个简单的背景补全SSM是Spring SpringMVC MyBatis的组合也是Java Web方向在Spring Boot全面流行之前最主流的一套后端技术栈。哪怕是今天很多院校的课程设计和毕业设计仍然指定用SSM原因在于它比Spring Boot更“暴露本质”——IoC容器、AOP切面、DispatcherServlet分发、SqlSessionFactory装配这些核心概念全都需要你亲手配置一遍。搞懂SSM再去看Spring Boot的自动配置就会有种“原来是在这层封装”的豁然开朗感。下面直接进入正题。1. 这个系统到底在管什么从“实习材料满天飞”到三类用户一条线很多同学拿到“学生实习系统”这类题目第一反应是建学生表、建教师表然后开始写登录、写CRUD。但真正的业务难点不在表而在“状态流转”。实习不是学生选个企业填张表就结束了它是一连串有时间顺序、有角色参与、有材料依附的流程学生提交实习申请指导教师审核审核通过后学生上传实习材料教师定期查看周报和考勤最后管理员统计全系统数据并导出报表。任何一个环节断了系统就不好用。1.1 三类核心角色与各自的真实需求这个系统里至少要有三种角色学生、教师通常兼任指导教师或辅导员、系统管理员。它们各自关心的数据完全是不同维度的。学生最关心的是“我的申请到哪一步了”“实习材料有没有被驳回”“我需要提交几份周报”。教师关心的是“有哪些学生分配给了我”“他们提交的材料是否合格”“实习单位是否真实”。管理员关心的是“全系有多少人在实习”“哪些学生还没落实实习单位”“不同企业的接收人数怎么统计”。所以在设计菜单和权限时我不建议只做“用户表加一个role字段”这种偷懒方案。角色不同首页展示的数据看板就应该不同。学生的首页应该是待办提醒教师的首页应该是待审核数量管理员的首页应该是一张全系统状态汇总表。1.2 完整业务主链路从申请到归档的七个步骤我一般会在需求文档里把这套流程画成一条直线然后再去建表写代码。这条链路是学生选择实习类型分散实习、集中实习还是自主联系企业。学生在系统内登记实习单位信息或从教师发布的企业库中选择。提交实习申请此时状态变为“待审核”。指导教师登录后看到待审核列表通过或驳回。驳回必须填写理由。审核通过后学生按时间节点上传实习材料比如安全承诺书、实习协议扫描件、实习照片等。实习期间学生按周提交周报教师可以批注也可以做签到或打卡。实习结束后教师给出实习成绩管理员归档数据并生成统计报表。这里的关键设计是“实习申请”和“实习材料”不要混在一张表里。申请是流程数据材料是文件数据两者一对多。如果一开始就只建一张apply表后面加材料上传功能时你会被迫改表结构改到怀疑人生。1.3 这类项目真正考察的能力点如果你是拿这个系统做毕业设计答辩时老师大概率会问三个问题这个项目的事务是怎么控制的权限是怎么实现的数据量变大之后怎么优化这三个问题对应到SSM项目里分别落在Spring声明式事务、HandlerInterceptor拦截器、以及MyBatis的SQL优化与索引设计上。所以不要只顾着把功能做出来要在代码里留出这些“答题点”。后面几个章节我会逐一把这些点写清楚。2. 为什么这类项目依然选SSM框架分工与工程结构拆解这几年后台开发基本被Spring Boot统一了新项目很少从零搭SSM。但在教学项目和毕业设计里SSM依然是出现频率极高的存在。原因很朴素它逼着你把每一层配置写出来而这份“麻烦”恰恰是理解Java Web后端最好的教材。2.1 SSM三兄弟各自干哪摊活用一个相对生活的类比来解释Spring是公司的“行政管理层”负责统一调度所有的对象——所有组件都交给Spring容器创建和管理谁依赖谁由Spring注入这叫IoC需要记录日志、做权限校验、事务控制这类横切逻辑通过AOP来实现。SpringMVC是“前台接待”所有HTTP请求先到它这里它看URL把请求分发给对应的Controller再把Controller返回的结果解析成页面或JSON。MyBatis是“数据专员”负责和数据库打交道把Mapper接口里的方法映射到XML里的SQL再把结果集自动封装成Java对象。这三个框架的分工决定了你的代码分包结构一定是围绕三层架构来做的Controller层接收参数和返回视图Service层写业务逻辑并加事务Mapper层写数据库操作。2.2 工程目录拿到项目先看这几个包一个标准的SSM项目拿到手先看src/main/java下面的包结构。我自己习惯的分类方式是这样的com.example ├── controller // SpringMVC的控制器只做参数接收和视图跳转 ├── service // 业务接口 │ └── impl // 业务实现类事务注解加在这里 ├── mapper // MyBatis的Mapper接口 ├── entity // 实体类对应数据库表 ├── interceptor // 登录拦截器、权限拦截器 ├── common // 统一返回结果、常量、分页工具 └── util // MD5加密、文件上传、日期工具类资源目录下则是三份核心配置spring-mvc.xml负责Controller扫描和视图解析器spring-service.xml负责Service层扫描、数据源、事务管理mybatis-config.xml负责MyBatis全局配置比如下划线转驼峰、日志输出。mapper目录放各个Mapper的XML文件。2.3 从“八股文”视角理解SSM为什么值得学如果你正在准备Java校招或实习面试SSM几乎是绕不开的考察点。面试官问“Spring的Bean生命周期”“SpringMVC的处理流程”“MyBatis的一级缓存和二级缓存区别”这些问题的答案就藏在这个项目里。与其死记硬背八股文不如亲手把项目从零搭一遍。当你自己配过DispatcherServlet、自己解决过Mapper扫描不到的问题面试时讲出来的细节深度是完全不一样的。3. 数据库设计是重头戏把实习申请、审核、材料流转建模清楚数据库设计决定了这个项目的上限。我在帮学生看代码时只要扫一眼数据库脚本基本就知道这人有没有真做过项目。很多人的表结构清一色是“用户表业务表”业务表里塞一个大text字段存所有内容这种设计做完就是个玩具。3.1 核心表清单与设计意图针对计算机专业学生实习系统我建议至少设计以下核心表表名主要字段设计意图student学号、姓名、密码、专业、班级、电话学生基础信息学号做唯一索引teacher工号、姓名、密码、院系、职称教师基础信息admin账号、密码、姓名管理员数量少单表足够company企业名称、统一社会信用代码、行业、地址实习单位库支持教师预发布apply学生ID、企业ID、岗位、开始/结束时间、状态、审核意见核心流程表状态机所在material申请ID、材料名称、文件路径、类型、上传时间实习材料一对多挂到申请下weekly_report申请ID、周报标题、内容、批注、提交时间实习过程记录evaluation申请ID、成绩、评语、评定教师、时间实习成绩评定notice标题、内容、发布人、发布时间系统公告管理员和教师可发这里有一个很重要的设计细节apply表里的status字段不要用varchar存中文比如“待审核”“已通过”。虽然看起来直观但后续统计、筛选、写代码判断都非常痛苦。推荐用tinyint存数字前端再映射成中文。0代表待提交1代表待审核2代表已通过3代表已驳回。如果需要扩展“已归档”状态再加4即可。3.2 状态机设计申请审核是核心链路实习申请的状态流转是这套系统的灵魂。学生创建草稿时状态为0点击提交后变为1教师审核通过变为2驳回则变为3并且必须填写审核意见。这里有一个容易忽略的点被驳回之后学生修改再提交状态应该回到1而不是直接覆盖原记录。为了实现这个回退需要在修改逻辑里判断当前状态是否为3只有驳回状态才允许再次提交。第一次提交后就不能再随意修改企业信息和时间这也是很多学生做系统时没想到的细节。我会在Controller里做一层校验前端隐藏修改按钮后端再判断一次状态双重保险。3.3 建表SQL的几个关键写法给出核心表的建表SQL片段你自己在上面扩展字段即可CREATE TABLE student ( id INT NOT NULL AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL COMMENT 学号, password VARCHAR(64) NOT NULL COMMENT MD5加密后的密码, name VARCHAR(50) NOT NULL COMMENT 姓名, major VARCHAR(50) DEFAULT NULL COMMENT 专业, class_name VARCHAR(50) DEFAULT NULL COMMENT 班级, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_student_no (student_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE apply ( id INT NOT NULL AUTO_INCREMENT, student_id INT NOT NULL COMMENT 学生ID, company_id INT DEFAULT NULL COMMENT 企业ID, position VARCHAR(100) DEFAULT NULL COMMENT 实习岗位, start_date DATE DEFAULT NULL COMMENT 开始日期, end_date DATE DEFAULT NULL COMMENT 结束日期, status TINYINT DEFAULT 0 COMMENT 0待提交 1待审核 2已通过 3已驳回, audit_comment VARCHAR(255) DEFAULT NULL COMMENT 审核意见, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_student_id (student_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建议统一使用utf8mb4字符集不要问为什么不用utf8——utf8mb4才是真正的四字节UTF-8能存emoji和生僻字兼容性更好。所有表都加上create_time和update_time这几乎是实操标准了。3.4 表关联查询避免N1在列表页展示申请记录时通常需要显示学生姓名、企业名称。我的建议是在Mapper的XML里用JOIN查询一次性带出比如left join student和left join company而不是在Java代码里循环查库。这一点在答辩时也可以主动讲出来属于明显的性能优化意识。4. 核心代码实现登录、申请审核、动态查询与文件上传功能代码这一块我挑几个最核心、也最容易被问到的点展开。注意我不打算把全部代码贴出来而是把关键思路和踩坑点讲透。4.1 登录与鉴权拦截器Session是SSM的常规方案先做一个LoginController接收用户名、密码、角色三个参数按角色分别去对应的Service里查库。密码在存库时不要用明文用MD5加密。虽然MD5在真正生产环境不够安全但应对课程设计和毕设已经够用如果想让答辩更有亮点可以换成BCrypt。Controller RequestMapping(/login) public class LoginController { Autowired private StudentService studentService; RequestMapping(/doLogin) public String doLogin(String username, String password, String role, HttpSession session, Model model) { if (student.equals(role)) { Student student studentService.login(username, MD5Util.md5(password)); if (student ! null) { session.setAttribute(loginUser, student); session.setAttribute(role, student); return redirect:/student/index; } } // teacher/admin 同理 model.addAttribute(msg, 用户名或密码错误); return login; } }登录成功之后的鉴权用一个HandlerInterceptor实现。声明拦截器时注意排除登录接口和静态资源否则CSS、JS全被拦截页面会白板。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); Object loginUser session.getAttribute(loginUser); if (loginUser null) { response.sendRedirect(request.getContextPath() /login); return false; } return true; } }在spring-mvc.xml里注册这个拦截器excludePathPatterns记得加上/login、/login/doLogin、/css/、/js/、/images/、/static/这些路径。4.2 审核流程的Service事务防止重复审核教师审核申请时Service层的update方法必须加事务注解并且在更新SQL里带上状态条件比如“update apply set status 2 where id #{id} and status 1”。这样即使两个管理员同时点击审核也只有一个能成功另一个受影响行数为0不会覆盖第一次的审核结果。这就是乐观锁的思路虽然这里没有版本号字段但用状态条件达到了同样的效果。Override Transactional(rollbackFor Exception.class) public void audit(Integer applyId, Integer result, String comment) { Apply apply new Apply(); apply.setId(applyId); apply.setStatus(result); apply.setAuditComment(comment); int count applyMapper.auditApply(apply); // sql里带 and status 1 if (count 0) { throw new RuntimeException(该申请已被处理请勿重复审核); } }4.3 MyBatis动态SQL后台列表筛选的利器管理后台最常见的操作就是多条件组合查询按学生姓名、专业、实习状态、企业名称筛申请记录。如果用Java代码拼SQL字符串既容易出错又难看。MyBatis的whereif标签就是为这个场景设计的select idselectApplyList parameterTypemap resultTypemap SELECT a.id, s.name AS studentName, s.major, c.name AS companyName, a.position, a.start_date, a.end_date, a.status, a.create_time FROM apply a LEFT JOIN student s ON a.student_id s.id LEFT JOIN company c ON a.company_id c.id where if teststudentName ! null and studentName ! AND s.name LIKE CONCAT(%, #{studentName}, %) /if if teststatus ! null AND a.status #{status} /if if testkeyword ! null and keyword ! AND (c.name LIKE CONCAT(%, #{keyword}, %) OR a.position LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY a.create_time DESC /select这样页面上的搜索框、下拉框天然就能组合使用。注意resultType直接用map在开发阶段最灵活不用为了展示一个列表创建一堆VO类。4.4 文件上传配置CommonsMultipartResolver实习材料上传是系统的高频操作之前有人踩过坑form表单忘记加enctypemultipart/form-data结果文件永远传不上去。SpringMVC里要先在spring-mvc.xml配置MultipartResolverbean idmultipartResolver classorg.springframework.web.multipart.commons.CommonsMultipartResolver property namemaxUploadSize value10485760/ property namedefaultEncoding valueUTF-8/ /beanController里用MultipartFile接收文件保存路径用一个时间戳加UUID拼接新文件名避免重名覆盖PostMapping(/uploadMaterial) public String uploadMaterial(RequestParam(file) MultipartFile file, Integer applyId, HttpServletRequest request) throws IOException { if (file.isEmpty()) { throw new RuntimeException(请选择文件); } String realPath request.getServletContext().getRealPath(/upload/material); File dir new File(realPath); if (!dir.exists()) { dir.mkdirs(); } String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String newName System.currentTimeMillis() _ UUID.randomUUID().toString().replace(-, ) ext; file.transferTo(new File(dir, newName)); materialService.addMaterial(applyId, /upload/material/ newName); return redirect:/student/materialList; }这里有一个非常重要的教训上传路径不要写死成“D:/upload”因为部署到Linux服务器后这个路径必然不存在。用request.getServletContext().getRealPath()拿到的是当前Web应用的实际目录跟着项目走不会因为环境变化出问题。5. 调试实录跑SSM项目最常见的五个报错与排查思路SSM项目第一次启动能一次成功的概率说实话很低。我自己调试这类项目时踩过的坑、帮学生远程看过的错翻来覆去就是那几类。与其等出了问题再焦虑不如直接把这几个高频问题提前排掉。5.1 “Invalid bound statement (not found)”——先查四个地方这个报错应该是SSM项目里出现频率最高的现象是调用Mapper接口方法时报错。核心原因是MyBatis没找到Mapper对应的XML。排查顺序固定为四步第一步看Mapper接口和XML文件的包名是否一致XML的namespace必须是接口的全限定名第二步看mybatis-config.xml里有没有配置mapper-locations指向XML目录第三步看XML里每个标签的id是否和接口方法名完全一致第四步检查编译后的target目录里有没有生成XML文件——很多IDEA项目里XML放在src/main/java目录下Maven默认不会把它编译进去需要在pom.xml里加resource配置或者干脆把XML统一放到src/main/resources/mapper下最省心。5.2 启动报错BeanCreationException——多半是配置扫描冲突Spring容器启动时报BeanCreationException或NoSuchBeanDefinitionException十有八九是组件扫描配置出了问题。常见的坑有两个一个是在spring-mvc.xml和spring-service.xml里都扫描了com.example.service导致Service被创建了两次另一个是漏配了注解驱动的标签比如没加mvc:annotation-driven/导致ResponseBody注解不生效。我的习惯是spring-mvc.xml只扫描controller包spring-service.xml只扫描service包和mapper包。职责边界越清晰出问题的概率越低。5.3 页面中文乱码——过滤器顺序一票否决如果你发现页面输出中文变成“???”或者写入数据库的中文乱码先不要怀疑代码先检查web.xml里的CharacterEncodingFilter有没有配置在DispatcherServlet之前。Spring的官方编码过滤器必须注册在DispatcherServlet前面请求才能先经过编码设置再进入Handler。同时确保数据库连接URL加了characterEncodingutf8JSP页面顶部也有pageEncodingUTF-8。5.4 MySQL 8.0驱动连不上——Driver类和时区一起换2024年新开的项目基本都用MySQL 8.0了但很多人还在复制MySQL 5.7时代的配置。关键差别有两个驱动类从com.mysql.jdbc.Driver换成了com.mysql.cj.jdbc.Driver连接URL要带serverTimezoneAsia/Shanghai否则报时区错误。在pom.xml里引入依赖时注意版本对应mysql-connector-java 8.x的groupId是mysqlartifactId是mysql-connector-java或com.mysql:mysql-connector-j都行版本建议8.0.33。5.5 静态资源被拦截或改了不生效SpringMVC默认拦截所有请求如果没放行静态资源页面会光秃秃地找不到CSS。在spring-mvc.xml里配置mvc:resources映射mvc:resources location/static/ mapping/static/**/改了前端样式不生效的问题大多数时候是浏览器缓存。CtrlF5强刷或者开无痕窗口测试。如果确认代码改了但页面还是旧的检查IDEA有没有把前端资源同步到target目录clean之后再重新部署一次。6. 从开发机到服务器war包打包与线上部署的关键操作开发环境跑通只是第一步毕业设计通常还需要一个“部署上线”的演示环节。这里把SSM项目从本地环境搬到服务器的最短路径讲清楚。6.1 Maven打包先clean再package在项目根的pom.xml里确认打包方式是warpackagingwar/packaging然后在IDEA右侧Maven面板执行clean再执行package。也可以直接用命令行mvn clean package -DskipTests生成的war包在target目录下。注意skipTests是必要的不然连数据库的测试类跑起来会拖半天甚至因为本地环境连不上测试库直接构建失败。6.2 部署到Tomcatwar包放对位置把war包复制到Tomcat的webapps目录启动Tomcat后它会自动解压。如果你希望项目通过IP:8080直接访问而不带项目名可以把war包改名为ROOT.war再放进去。改完后记得先停掉Tomcat进程覆盖旧包之前最好把原来的解压目录一并删掉避免新旧文件混在一起出现诡异问题。6.3 线上数据库初始化服务器上如果还没有MySQL先创建数据库和账号然后导入sql脚本。这里最容易栽的坑是本地MySQL是8.0服务器上是5.7SQL脚本里用到utf8mb4没问题但如果有字段用了ON UPDATE CURRENT_TIMESTAMP这种写法5.7也支持基本兼容。要注意的是执行脚本前先设置好字符集CREATE DATABASE IF NOT EXISTS ssm_intern DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;然后把jdbc.properties里的连接地址改成服务器的内网或公网IP账号密码改成线上账号。改完重启Tomcat访问登录页测试一遍登录、提交申请、上传材料、教师审核四个链路都通了才算部署完成。6.4 提个容易被忽略的服务器安全配置如果你用的是云服务器记得在安全组里放行8080端口或者你修改后的Tomcat端口。很多同学本地跑得好好的一上服务器就访问不了排查到最后发现是安全组没放行这类问题在答辩现场特别尴尬。7. 还能往哪升级毕业设计之后的三条落地路线如果你的时间精力允许或者想在答辩时展示更强的工程能力下面这几个方向可以挑一个做深。7.1 权限控制从“拦截器”升级到“Spring Security或Shiro”SSM里的Session拦截器方案简单直观但大型系统里角色权限的颗粒度远不止“登录”和“未登录”两种状态。Spring Security提供完整的认证、授权、Session管理、CSRF防护链路但学习曲线陡峭。Shiro相对轻量和SSM整合也更顺滑。如果你只是想给答辩加一个亮点用Shiro替换手写拦截器实现“学生只能访问学生端、教师只能访问教师端”的细粒度控制效果立竿见影。7.2 报表统计从“手写SQL”升级到“ECharts可视化”管理员的首页如果只有表格说服力有限。用ECharts做一个柱状图展示各专业实习人数饼图展示企业行业分布折线图展示每周提交周报数量从技术实现上并不难Controller返回JSON前端ajax请求后交给ECharts渲染。但视觉效果和答辩印象分会明显不一样。7.3 数据访问从“JDBC直连”升级到“Redis缓存”实习类型、企业列表、公告通知这类变化频率很低但访问频率很高的数据非常适合加一层Redis缓存。流程是首次请求查询数据库并写入Redis后续请求直接读缓存管理员修改数据时删除对应缓存。把Redis引入SSM项目既能体现你对中间件的了解又不需要大改业务代码。最后再分享一个我实际带项目时经常提醒学生的小技巧SSM项目调试阶段强烈建议开启MyBatis的SQL日志输出在mybatis-config.xml里加上logImpl配置这样控制台会打印每一条执行的SQL。排查动态SQL拼接是不是多了个AND、参数传没传进去一眼就能看出来。等你把日志关掉的时候基本也就说明项目已经稳定了。
返回列表