简介:面向计算机及相关专业专科、本科毕业生的原创毕业论文,选题为基于JavaWeb的实习管理系统设计与实现。论文完整覆盖实习管理的需求分析、MVC系统架构设计、数据库表结构规划、前后端界面设计,以及功能编码、系统测试与部署维护等环节,可作为毕业设计或论文撰写的直接参考。包体为1份docx文档,压缩后约32KB,打开即可查看正文和目录结构,编辑修改方便。据了解,文档已通过原创性与重复率检查,内容篇幅达万字,完整性较好。目前已有133人学习下载。这份论文的价值在于,它按软件工程流程讲清了实习管理系统从零到落地的全过程,便于读者理解JavaWeb项目开发中Servlet、JSP、JDBC及Spring、MyBatis等技术的实际搭配方式,同时提供实习生信息管理、单位管理、计划安排、评价反馈等模块的设计思路和登录权限控制要点,能为类似管理系统的设计与实现提供可复用的框架与文档模板。
1. 一款基于Javaweb的实习管理系统能解决什么:从Excel轰炸到在线流转
又到实习季,信息学院每年有 400 多名学生要分散到各地企业,教务老师手里攥着一份共享 Excel,登记岗位、回收周报、核对成绩,一到月底就因为漏登和版本冲突翻车。把这条流程搬到线上,就是一套基于 Javaweb 的实习管理系统:学生在线提交申请和周报,指导教师在后台审核评分,管理员统一管理岗位并导出成绩。技术栈选用 Servlet + JSP + MySQL 这套 Javaweb 最常见的老三样,覆盖登录鉴权、角色分流、文件上传、分页查询等完整 Web 开发链路,非常适合做课程设计、毕业设计或者想拿完整案例练手的人。下面按选型、建表、编码、IDEA 部署、排错五个环节展开,每一步都给出能直接复现的配置和代码。
2. 系统设计与技术选型:为什么这套组合值得复刻
2.1 Servlet+JSP 还是 SpringMVC:毕设场景下的选型逻辑
实习管理系统连上管理员一共三类角色,核心动作就是发布岗位、提交申请、写周报、打成绩。这类系统的并发量通常只有几十个人同时在线,数据库规模也就几万行,用 Spring Boot + MyBatis Plus 当然能做,但对课程设计和本科毕设来说,会让答辩现场"解释一下项目里最难的点"变成一场灾难。Spring 容器在做自动装配时,把你和底层细节隔开了,背完面试题都说不清一个请求到底是怎么从 Tomcat 进到 Controller 的。而 Servlet + JSP 这套东西,过滤器和 Servlet 的注册路径全部自己控制,请求在哪一层、做了哪些操作,在答辩现场指着代码就能讲透。
网上那些 javaweb 项目完整案例 mysql 老项目,反而能帮你理解 HTTP、Session、Cookie 这些 Web 基础,黑马 javaweb 笔记数据里也是先讲透 Servlet 再讲框架,这个路径和下面要写的一致。我还特意避开了直接用 SSM 框架:SSM 的好处是贴近企业开发,坏处是配置文件太多,spring-mvc.xml、spring-dao.xml、mybatis-config.xml 三个文件互相引用,一个 namespace 写错就启动失败,排查成本比业务代码本身还高。用 Servlet 做控制层后,我把数据源、事务、过滤器集中到少量配置里,整个项目在 IDEA 里启动一次就能跑通。
最终选型如下:Servlet 3.1 + JSP 2.3 + JDBC 原生封装 + MySQL 5.7 + Druid 连接池,前端用 JSP + Bootstrap 4 做后台布局,不做前后端分离。原因有两点:第一,这类管理系统页面多但结构简单,服务端渲染能少建一堆接口和请求处理代码;第二,课程设计和毕设评审看重功能完整度和能否现场演示,前后端分离会多出 node 构建步骤,无端增加翻车概率。这个选择也留了升级余地:Servlet 类里的逻辑和 SpringMVC 的 Controller 方法几乎一一对应,后面想换框架,改动集中在方法签名和返回值处理上,业务代码基本能平移。
2.2 六张核心表的字段设计与表关系
数据模型是整个系统最不能省的一步。网上能搜到的实习管理类案例经常只给用户表和岗位表,周报和成绩全靠一张表硬塞,后期想按周次查都不好写 SQL。我把数据拆成六张表:用户、岗位、实习申请、周报、成绩、公告,字段以够用为准,不做过度设计。
用户表承担三种角色的登录信息,密码字段要留足长度,因为后面要做加盐哈希而不是存明文。岗位表记录企业发布的实习职位,用 status 字段控制发布状态。申请表把学生和岗位关联起来,同时背负整个审核流程的状态流转。周报表挂在申请下面而不是直接挂学生,因为一个学生可能申请多个岗位,直接挂 student_id 会分不清这篇周报属于哪次实习。成绩表与申请表是一对一关系,教师审核通过后顺手初始化一条空成绩记录,后续再填分数。
CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(128) NOT NULL COMMENT '加盐哈希后的密码', real_name VARCHAR(32) NOT NULL, role VARCHAR(16) NOT NULL COMMENT 'student/teacher/admin', student_no VARCHAR(20) COMMENT '学号或工号', college VARCHAR(64), class_name VARCHAR(64), phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_internship_post ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(128) NOT NULL COMMENT '岗位标题', company VARCHAR(128) NOT NULL, position VARCHAR(64) NOT NULL, location VARCHAR(64), headcount INT DEFAULT 1, requirement TEXT, status TINYINT DEFAULT 0 COMMENT '0草稿 1发布 2结束', publisher_id INT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_apply ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, post_id INT NOT NULL, status TINYINT DEFAULT 0 COMMENT '0待审核 1通过 -1拒绝', apply_reason VARCHAR(500), review_comment VARCHAR(500), review_time DATETIME, KEY idx_student (student_id), KEY idx_post (post_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_weekly_report ( id INT PRIMARY KEY AUTO_INCREMENT, apply_id INT NOT NULL, week_no INT NOT NULL COMMENT '第几周', content TEXT, attachment_url VARCHAR(255), submit_time DATETIME DEFAULT CURRENT_TIMESTAMP, teacher_reply VARCHAR(500), KEY idx_apply (apply_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_score ( id INT PRIMARY KEY AUTO_INCREMENT, apply_id INT NOT NULL UNIQUE, score DECIMAL(5,2), comment VARCHAR(500), evaluator_id INT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_announcement ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(128) NOT NULL, content TEXT, publisher_id INT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;编码统一用 utf8mb4,因为 MySQL 5.7 默认 utf8 是 utf8mb3,存不了生僻字和某些特殊符号,学生姓名里出现冷僻字时整个插入会报错。外键我没有建物理约束,只建了普通索引。这是很多实训项目的常规做法:物理外键在数据量上来后会拖慢写入,而且删除用户时会被外键卡住;更合适的方式是在 service 层做逻辑校验,比如插入申请前先查学生和岗位是否存在。毕业论文里写物理外键也能解释清楚,但实际演示和后续扩展都不如索引加逻辑校验灵活。
2.3 分层约定:controller/service/dao 的职责边界
包结构按 controller、service、dao、model、filter、util 拆开,这是老牌 javaweb 项目完整案例里最常见的组织方式,也符合后面讲到的所有代码的位置约定。
src/main/java/com/edu/internship/ ├── controller/ # Servlet,接收请求、返回视图 │ ├── LoginServlet.java │ ├── PostServlet.java │ ├── ApplyServlet.java │ └── WeeklyReportServlet.java ├── service/ # 业务校验、事务控制 │ ├── UserService.java │ ├── PostService.java │ └── ApplyService.java ├── dao/ # JDBC 操作,一个方法对应一条 SQL │ ├── UserDao.java │ ├── PostDao.java │ ├── ApplyDao.java │ └── WeeklyReportDao.java ├── model/ # 实体类,和表字段一一对应 ├── filter/ # 角色权限过滤器 └── util/ # DBUtil、EncryptUtil、FileUploadUtil为什么把 service 拆出来而不是让 Servlet 直接写 JDBC?这个系统表只有六张,但很多操作不止查一张表。比如教师审核一份申请时,既要更新 t_apply 的状态,又要在 t_score 里插入一条成绩记录,如果这两步放在 Servlet 里,一旦中间抛异常就会出现申请状态改了但成绩没生成的情况。把两步放进 ApplyService 的 auditApply 方法里,用事务包住,就能避免数据不一致。
请求的流转顺序是:JSP 表单提交触发对应 Servlet,Servlet 从 request 里拿参数并转成实体对象,调用 Service 的业务方法,Service 内部完成校验、事务、组合多张表的操作,把结果放回 request 或 session,再 forward 或 redirect 到 JSP。这个链路跟 SpringMVC 的 Controller-Service-Mapper 结构几乎一样,区别只是少了依赖注入。所以后续做扩展时,把 Servlet 里的手工 new 对象换成 Spring 的注入即可,改动范围可控。
3. 核心功能实现:从登录到周报上传的完整链路
3.1 登录与角色权限:用 Filter 做三个角色的访问分流
登录是所有页面的入口。翻那些 javaweb 项目完整案例 mysql 会发现,很多登录逻辑在 Servlet 的 doPost 里狂写一二十个 if,把验证码校验、用户查询、角色跳转全糊在一起。更常规的做法是让 LoginServlet 只做两件事:校验验证码,然后查用户并写入 Session;角色跳转放在前端页面,通过 Session 里的 loginUser 字段区分菜单。
先看 LoginServlet 的核心代码:
@WebServlet("/login") public class LoginServlet extends HttpServlet { private UserService userService = new UserService(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); String verifyCode = req.getParameter("verifyCode"); String codeInSession = (String) req.getSession().getAttribute("code"); // 验证码不区分大小写,先于用户查询校验,减少无效数据库请求 if (verifyCode == null || !verifyCode.equalsIgnoreCase(codeInSession)) { req.setAttribute("msg", "验证码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); return; } User user = userService.login(username, password); if (user == null) { req.setAttribute("msg", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); return; } HttpSession session = req.getSession(); session.setAttribute("loginUser", user); // 按角色跳到不同首页,前端再根据 loginUser.role 细化左侧菜单 String role = user.getRole(); if ("student".equals(role)) { resp.sendRedirect(req.getContextPath() + "/student/index.jsp"); } else if ("teacher".equals(role)) { resp.sendRedirect(req.getContextPath() + "/teacher/index.jsp"); } else { resp.sendRedirect(req.getContextPath() + "/admin/index.jsp"); } } }UserService 里的 login 方法负责把明文密码做加盐哈希,再和数据库里存的哈希串比对。真正的密码摘要逻辑放在后面的工具类里,LoginServlet 本身不写 JDBC,只负责把 request 参数翻译成调用。这里注意 req.setCharacterEncoding("UTF-8") 必须放在读取任何参数之前,否则中文用户名会直接乱码,后面排查时会展开讲。
权限控制的主载荷放在 Filter 上。三个角色对应三套 URL 前缀:/student/、/teacher/、/admin/*,每个前缀挂一个 Filter。用 Filter 而不是在 JSP 里判断角色,是因为 JSP 判断总能被绕过——直接输入不带校验的 .jsp 路径就能刷开页面。下面贴学生角色的 Filter,其他两个改一下角色字符串即可:
@WebFilter("/student/*") public class StudentFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; User user = (User) req.getSession().getAttribute("loginUser"); if (user == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } if (!"student".equals(user.getRole())) { resp.setContentType("text/html;charset=UTF-8"); resp.getWriter().write("<h3>403 无权访问学生功能</h3>"); return; } chain.doFilter(request, response); } }每个角色的 Filter 只有两个判断:有没有登录、角色是否匹配。两个条件都不满足就中断请求,满足则放行。这里要特别小心 Filter 的 URL 匹配范围,@WebFilter("/student/*") 只拦以 /student/ 开头的路径,不会误伤登录接口和静态资源,这也避免了后面避坑章里登录后反复跳回登录页的问题。
3.2 岗位发布与申请:状态字段驱动的一对多流程
岗位发布和申请是系统的业务主轴。管理员把岗位信息写入 t_internship_post,status 置为 1 表示发布;学生查看已发布岗位提交 t_apply;教师在待审核列表里通过或拒绝。整个过程用 status 这一个字段推动,不设计复杂状态机。学生发起申请的逻辑如下:
@WebServlet("/apply/add") public class ApplyAddServlet extends HttpServlet { private ApplyService applyService = new ApplyService(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session = req.getSession(); User user = (User) session.getAttribute("loginUser"); if (user == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } int postId = Integer.parseInt(req.getParameter("postId")); String reason = req.getParameter("applyReason"); try { applyService.addApply(user.getId(), postId, reason); resp.sendRedirect(req.getContextPath() + "/student/applyList.jsp"); } catch (BusinessException e) { req.setAttribute("msg", e.getMessage()); req.getRequestDispatcher("/student/apply.jsp").forward(req, resp); } } }addApply 的业务逻辑有三步:先查 t_apply 里是否存在同一学生同一岗位的记录,存在就抛 BusinessException 提示不能重复申请;再检查岗位是否仍处于发布状态,防止管理员下线岗位后学生继续提交;最后插入记录,status 默认 0。把查询、校验、插入放在同一个 service 方法里,用事务包住,避免并发请求下产生重复申请。
教师审核是反向操作。审核页面列出 status=0 的记录,教师点通过或拒绝时,Servlet 调 ApplyService.auditApply(applyId, result, comment)。这个方法在同一个事务里更新申请表状态,并在通过时往 t_score 插入一条空成绩,最后把审核意见写回 t_apply。这里有个容易踩的细节:审核意见不能只放在 t_score 里,因为 t_apply 里的 review_comment 要用于列表页直接展示,两张表各自存一份,避免审核列表要连表查成绩表才能看到意见。
3.3 周报文件上传:MultipartConfig 与磁盘路径存储
实习周报通常包含 Word 或 PDF 附件,上传功能用 Servlet 3.0 的 @MultipartConfig 注解就能实现,不需要引入 Commons-FileUpload 这种老库。但存储路径要提前想清楚:直接把文件写到项目部署目录,下次重新部署 WAR 包会被清空;我一般会在 Tomcat 外部配置一个物理路径,比如 /data/internship/uploads,再用虚拟目录映射到 /uploads 供页面访问。
@WebServlet("/weekly/add") @MultipartConfig( maxFileSize = 10 * 1024 * 1024, maxRequestSize = 20 * 1024 * 1024, fileSizeThreshold = 1024 * 1024 ) public class WeeklyReportServlet extends HttpServlet { private WeeklyReportService reportService = new WeeklyReportService(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); Part filePart = req.getPart("attachment"); String weekNo = req.getParameter("weekNo"); String content = req.getParameter("content"); HttpSession session = req.getSession(); User user = (User) session.getAttribute("loginUser"); // 用 getSubmittedFileName 拿到的可能是完整客户端路径,必须重新取文件名 String originName = ""; if (filePart != null && filePart.getSize() > 0) { String submitted = filePart.getSubmittedFileName(); originName = Paths.get(submitted).getFileName().toString(); } String storeName = System.currentTimeMillis() + "_" + originName; String uploadPath = "/data/internship/uploads"; File dir = new File(uploadPath); if (!dir.exists()) { dir.mkdirs(); } if (filePart != null && filePart.getSize() > 0) { filePart.write(uploadPath + File.separator + storeName); } reportService.addWeeklyReport(user.getId(), Integer.parseInt(weekNo), content, storeName); resp.sendRedirect(req.getContextPath() + "/student/weeklyList.jsp"); } }从 Part 取上传文件名时用 Paths.get(submitted).getFileName().toString() 重新取一遍,是为了处理 Windows 浏览器可能提交的 C:\fakepath\xxx.docx 这类值,直接拼接会导致写入路径变成 C:\fakepath\ 加在你的 uploadPath 里,文件写到错误位置,打开自然损坏。存储文件名加 System.currentTimeMillis() 前缀是防重名的常规做法;数据库里存的是 storeName 而不是全路径,因为前端显示时通过虚拟目录访问,物理路径完全对外屏蔽。maxFileSize 是单个文件上限,超过后会抛 IllegalStateException,要在外层统一 catch 后提示学生压缩文件,而不是让 Tomcat 直接弹 500 页面。
4. 在 IDEA 里把 JavaWeb 项目跑起来:配置与部署实操
4.1 Maven 项目初始化:pom.xml 里最容易被坑的依赖
新建一个 Maven Web 工程后,第一步是把 pom.xml 改成一套与 Tomcat 9、MySQL 5.7 匹配的依赖组合。很多照着老教程配置的人会卡在 Servlet API 版本上,要么启动时 NoClassDefFoundError,要么打包后在别的 Tomcat 里跑不起来。这里直接给出当前可用的组合:
<project xmlns="http://maven.apache.org/POM/4.0.0"> <modelVersion>4.0.0</modelVersion> <groupId>com.edu</groupId> <artifactId>internship-system</artifactId> <version>1.0</version> <packaging>war</packaging> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- Servlet API,Tomcat 9 对应 4.0.x --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <!-- JSP API --> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.3.3</version> <scope>provided</scope> </dependency> <!-- JSTL 标签库 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <!-- MySQL 驱动,注意版本和数据库版本匹配 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> </dependency> <!-- Druid 连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.8</version> </dependency> </dependencies> <build> <finalName>internship-system</finalName> </build> </project>javax.servlet-api 的 scope 必须设成 provided,意思是打包 WAR 时不带这份依赖,因为 Tomcat 自己提供 Servlet 实现。如果漏掉这个 scope,WAR 里会塞一份 Servlet API 类,和 Tomcat 的类冲突,轻则方法签名不一致报错,重则连启动都过不去。MySQL 驱动版本要看数据库版本:本机装 MySQL 5.7 配 5.1.49,装 MySQL 8.0 就要换 8.0.29 并把驱动类改成 com.mysql.cj.jdbc.Driver。这里案例按 5.7 写,后面初始化数据库时别弄混。
4.2 Tomcat 配置与 Artifacts:浏览一直 404 的修复路径
在 IDEA 里运行老式 Web 项目,最让人心里没底的是 Artifacts 配置。很多人第一次接触时按教程点完 Tomcat 后,Deployment 页签里什么都没有,启动也不报错,但浏览器一直 404。原因是 IDEA 不知道把哪个构建产物部署给 Tomcat。
操作路径是:Run 菜单选 Edit Configurations,左上角加号选 Tomcat Server -> Local,在 Server 页填 Tomcat 安装目录并确认端口;切到 Deployment 页签,点加号选 Artifact,选择 internship-system:war exploded。如果 Artifact 列表为空,需要先打开 File -> Project Structure -> Artifacts,点加号选 Web Application:Archive,把项目模块关联进去,再回到 Deployment 选择。最后把 Application context 设为 /internship,浏览器访问 http://localhost:8080/internship/login.jsp 就能看到页面。
war exploded 是未压缩的调试模式,改完 JSP 刷新浏览器就能生效;war 是压缩包模式,每次修改代码都要重新构建再部署,调试期没必要用。Application context 这个路径决定所有项目内跳转的前缀,如果改成 /,原来带 /internship 前缀的绝对路径全部 404。所以 Servlet 和 JSP 里统一用 req.getContextPath() 拼接路径,写死 /internship 的话,换前缀时全局翻车。
4.3 数据库初始化与连接池:MySQL 版本不对引发的连锁问题
IDEA 里能启动 Tomcat 不代表数据层能连上。初始化数据库最容易翻车的是连接串和驱动版本不匹配,其次是连接池配置文件写错但无提示。先贴 Druid 配置文件和连接工具类:
driverClassName=com.mysql.jdbc.Driver url=jdbc:mysql://localhost:3306/internship_db?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai username=root password=123456 initialSize=3 maxActive=10 minIdle=1 maxWait=5000public class DBUtil { private static DruidDataSource dataSource; static { try (InputStream in = DBUtil.class.getClassLoader() .getResourceAsStream("druid.properties")) { Properties props = new Properties(); props.load(in); dataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }Druid 有一个隐蔽特性:properties 文件里如果漏了 driverClassName 或 username,它会静默改用默认值,不报错,等真正 getConnection 才失败。所以连接池配完一定要先用一个简单的 main 方法测一次 getConnection,而不是直接启动 Tomcat 去猜问题。数据库初始化按第 2.2 节的 SQL 在 Navicat 或 IDEA Database 面板执行建表语句,再插入几个测试账号。MySQL 5.7 建库时如果没有指定 utf8mb4,建表后要检查表结构,否则中文登录名和姓名会变成问号。
5. 避坑指南:从 IDEA 运行 JavaWeb 项目配置到入库的五个翻车现场
下面这个项目里最容易翻车的 5 个现场,我连续帮四批做课设的同学排查过,问题集中在依赖导入、编码、权限范围这三处,每一条都按现象、原因、解决展开。
5.1 ClassNotFoundException: com.mysql.jdbc.Driver:依赖没进 WAR
现象是 Tomcat 启动后,第一次请求数据库就抛 ClassNotFoundException: com.mysql.jdbc.Driver。去 pom.xml 里看依赖明明加上了,IDEA 的外部库列表里也能看到 mysql-connector-java。
原因多数不是 Maven 依赖缺失,而是 IDEA 的 Artifacts 没把依赖带进最终部署单元的 WEB-INF/lib。Maven 引入的包在编译期可用,但运行时 Tomcat 是从 Artifacts 的 lib 目录加载类的,两套目录是分离的。解决方法是打开 Project Structure -> Artifacts,在输出目录里确认存在 WEB-INF/lib/mysql-connector-java-5.1.49.jar;不存在就右键 Artifacts 选 Put into Output Root,再重新构建部署。这个坑在老教程里也常见,核心思路是运行时类路径和编译期类路径不是一回事。
5.2 数据库里中文变成问号:四段编码必须统一
现象是页面标题正常,但表单提交的中文用户名、申请理由存进数据库后全是问号,或者浏览器显示乱码。
原因是编码链条断了一环。JSP 声明了 UTF-8,Servlet 里却没调用 req.setCharacterEncoding;连接串没带 characterEncoding=UTF-8;MySQL 表用了 utf8mb3。四个环节只要有一个不一致,中文数据就会在传输中途损坏。解决方法是把四段全部统一:JSP 头部写 pageEncoding="UTF-8" 和 contentType="text/html;charset=UTF-8",Servlet 里所有读取参数前先调 setCharacterEncoding,JDBC 连接串加 useUnicode=true&characterEncoding=UTF-8,建表语句用 utf8mb4。如果这样还有问题,检查 Tomcat 的 server.xml 里 Connector 是否设置了 URIEncoding="UTF-8",Tomcat 8 以上默认就是 UTF-8,老配置不要画蛇添足。
5.3 登录成功又跳回登录页:Filter 把登录请求一起拦了
现象是用户输入正确的账号密码,浏览器地址栏短暂跳转后又被弹回登录页,特别在首次部署时高频出现。
原因多为 Filter 的拦截路径写得太宽。比如为了省事把 @WebFilter 配置成 /*,登录请求本身还没有 loginUser 属性,Filter 判断 user == null 就重定向回 login.jsp,形成死循环。解决方法是先用注释或控制台日志确认 Filter 拦截的 URL:在 Filter 开头打印 req.getRequestURI(),如果包含 /login 就放行。更稳的做法是沿用第 2.3 节的约定,Filter 只挂在角色路径的前缀上,登录接口用单独路径,两者天然隔离。
5.4 上传的 Word 打不开:getSubmittedFileName 被完整路径污染
现象是附件上传后,数据库里存了一条记录,但下载或打开时文件损坏,目录里找不到对应文件,或文件名变成一长串奇怪路径。
原因是前端文件域 getSubmittedFileName() 在多数浏览器里返回的是完整客户端路径或 C:\fakepath\xxx.docx,直接拼到 uploadPath 后面,目录不存在文件就写失败。解决方法是解析文件名时用 Paths.get(submitted).getFileName().toString() 过滤掉路径部分,再拼 System.currentTimeMillis() 前缀生成唯一 storeName。另外,如果上传目录在 Tomcat 内部且未授权写入,会报 FileNotFoundException,把 uploadPath 指到 Tomcat 外的绝对路径并设置目录读写权限即可避免。
5.5 Tomcat 端口 8080 被占用:上一次进程没退干净
现象是 IDEA 点完运行,控制台报 Address already in use: JVM_Bind,或者提示 8080 端口被占用。
原因一般是上一次 Tomcat 异常关闭,残留了 Java 进程,或者本机装了其他服务占用 8080。解决方法是命令行执行 netstat -ano | findstr 8080 找到 PID,taskkill /F /PID 对应进程,再重新启动。也可以把 IDEA 里 Tomcat 的 HTTP port 改成 8081 一劳永逸,但改完后要同步改项目里的跳转前缀和前端请求地址,不然会陷入改了端口页面仍 404 的迷惑现场。这个属于最没技术含量的玄学问题,但浪费的时间最多,养成用完关闭 Tomcat 的习惯比查端口更快。
6. 进阶:把实习管理系统做稳的优化与验证技巧
系统能跑通只是第一步,要扛得住答辩和实际用的还有三件事:密码加盐、分页查询和按角色走通验收路径。
密码不能存明文。前面 UserService 里只调了加密工具,这里看一下实现。加盐的作用是让同一密码在不同账号下哈希结果不同,防止彩虹表反推:
public class EncryptUtil { public static String sha256WithSalt(String password, String salt) { try { MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] bytes = md.digest((salt + password).getBytes(StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(); for (byte b : bytes) { sb.append(String.format("%02x", b)); } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } }登录校验时先从库里查出该用户的 salt,再把用户输入的密码拼 salt 哈希后比对。salt 可以直接用 username 字符串,或者注册时生成的随机串。这种方案比 Spring Security 轻量得多,适合课设,本质逻辑没有差别。
分页是管理系统躲不开的优化点。岗位列表、周报列表、待审核列表都要分页,SQL 统一用 LIMIT offset, pageSize 的写法,pageSize 设 10,offset 等于 (currentPage - 1) * pageSize。数据量到几万行时,用 COUNT(*) 子查询统计总数,再配合 idx_student、idx_apply 索引,响应时间能控制在毫秒级。不要用 PageHelper 这类框架,手写分页能让答辩老师看到你理解 SQL。
最后是验收路径:管理员发布岗位、学生申请、教师审核通过、学生传周报、教师打分,五个环节各角色走一遍,确认状态流转和权限拦截都正确。我做这套系统时踩过最深的坑不是技术本身,而是低估了数据一致性。当时教师审核申请时,我把更新申请表和插入成绩表写了两个 try 块,用户反馈有时审核了但成绩没了,后来把它们合并到一个事务里再加了日志,问题才彻底消失。如果你正拿这套系统做毕设或课设,记住先把业务闭环跑通再谈优化,交付前自己按三个角色各演示一遍。希望帮到你。
本文还有配套的精品资源,点击获取