首页 / 资讯中心 / 文章详情

JavaWeb学生成绩管理系统:三层架构与ECharts统计实战

JavaWeb学生成绩管理系统:三层架构与ECharts统计实战 ★ FEATURED ARTICLE
简介这是一套面向 JavaWeb 入门与课程设计场景的学生成绩管理系统源码基于 ServletJSPMySQL 分层实现适合计算机专业学生完成课设、毕设也适合自学者练习 Servlet 与 JDBC 的完整业务链路。升级版在原有增删改查基础上补充了 ECharts 统计图表可将成绩数据以可视化方式呈现。压缩包共 344 个文件、约 3.59MB包含 86 个 java 源文件与对应的 86 个 class 编译文件、37 个 jsp 页面、62 个 js 脚本和 28 个 css 样式另有 6 个 jar 依赖与数据库 sql 脚本源文件、页面、静态资源与运行库分目录存放便于按模块阅读。项目按 Dao、ServiceImpl、Servlet 分层组织覆盖学生、教师、课程、成绩与用户管理等模块并包含文件上传下载实现。目前已有 6724 人学习下载可作为理解分层架构、复用统计图表与二次开发的参考。1.做 javaweb 学生成绩管理系统的人十有八九是从「增删改查」四个字开始的学生表、课程表、成绩表各来一套 JSP能录能查就算完事。真到答辩现场老师往往不看你的录入页而是问一句「这个班这次考试的平均分多少、及格率多少、跟上学期比是升了还是降了」——答不上来前面写的成绩管理功能就都只是基础框架。这套基于 ServletJSPMySQL 的方案把重点放在两个环节一是把成绩的录入、校验、权限和数据一致性处理扎实二是用 ECharts 把分散在数据表里的分数变成能直接讲出来的图。它适合正在做毕设或课程设计、需要一套完整可运行结构的同学照着抄也适合要交一份能当场演示的系统的从业者参考。下面按架构、数据、业务、图表、排错、验收的顺序展开每一段都对应到具体文件和可执行的操作。2. 总体架构与数据模型三层链路、七个包与六张核心表的职责划分2.1 从浏览器到 MySQL一次请求经过的四道关这套系统跑在 Tomcat 上没有 Spring没有 MyBatis纯 Servlet JSP JDBC。很多人一听到「不用框架」就觉得简陋其实恰恰相反正因为没有框架自动完成映射和依赖注入每一层的职责边界反而必须自己划定否则代码很快就会变得混乱。我一般会把一次「教师查询某班某课成绩」的请求拆成下面这条链路浏览器发起GET /score/list?classId3courseId7pageNo1LoginFilter先拦下来检查 session 里有没有登录用户、当前用户角色能不能进这个路径前缀ScoreServlet只做三件事取参数、做基础校验、调用 Service然后把结果放进 request 域转发给 JSPScoreService组装查询条件、决定要不要做缓存或二次计算ScoreDao拼 SQL、设参数、拿ResultSet映射成ScoreVO列表JSP 用 JSTL 遍历渲染EL 表达式取值。这条链路上每一层的「不该做的事」比「该做的事」更重要。层主要职责常见越界写法Filter编码、登录态、角色校验在里面查数据库做业务判断Servlet收参、校验、转发、返回 JSON直接写 SQL、直接拼 HTMLService事务边界、业务规则、多表组装返回ResultSet给上层DAOSQL、参数绑定、结果映射抛业务异常、做权限判断JSP只负责展示用% %写 JDBC 代码提示JSP 里写 JDBC 是课程设计里最常见的错误做法。页面一多同样的连接代码要复制十几遍改一个字段名就得全项目搜索。把这部分抽到 DAO 里哪怕只抽一层后面加统计功能时你会省下大量时间。这套分层带来的直接好处是新增 ECharts 统计模块时只需要在 DAO 里加几个聚合查询在 Servlet 里加一个返回 JSON 的接口原有页面一行都不用动。2.2 包结构七个包各自的职责目录结构定下来之后后面所有代码都有明确的书写位置不会出现「这个工具类到底放哪」的争论。我一般会按下面这个结构组织src/main/java/com/example/sms/ ├── filter/ # LoginFilter、EncodingFilter ├── servlet/ # LoginServlet、StudentServlet、ScoreServlet、StatsServlet ├── service/ # ScoreService、StatsService ├── dao/ # BaseDao、ScoreDao、StudentDao ├── entity/ # Student、Course、Score、User ├── vo/ # ScoreVO、PageResult、ChartData └── util/ # DBUtil、JsonUtil、DateUtil src/main/webapp/ ├── WEB-INF/ │ ├── web.xml │ └── views/ # 所有 JSP 放这里禁止浏览器直接访问 ├── static/ # echarts.min.js、css、图片 └── login.jspentity和vo分开是有讲究的Score对应score表的字段一个成绩记录就是一条ScoreVO是给页面看的里面多带了学生姓名、班级名、课程名这些联表字段。混在一起写用不了多久实体类里就会多出七八个数据库里根本不存在的属性等到做 JSON 序列化时就会出问题。views目录放在WEB-INF下面是为了防止学生绕过 Servlet 直接访问/views/scoreList.jsp那样 session 校验就形同虚设。这类越权访问正是答辩时最容易被要求现场演示的环节。2.3 六张核心表字段、约束与索引数据模型围绕「谁能考、考什么、考了多少」三件事展开。用户表和角色合并成一张用role字段区分学生、教师单独建表避免用户表里塞进大量只有学生才有的字段。-- 1. 登录账号表三种角色共用 CREATE TABLE sys_user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE COMMENT 学号或工号, password VARCHAR(64) NOT NULL COMMENT 加盐后的摘要不存明文, role TINYINT NOT NULL COMMENT 1管理员 2教师 3学生, real_name VARCHAR(32) NOT NULL, status TINYINT DEFAULT 1 COMMENT 1启用 0停用 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 2. 班级表 CREATE TABLE clazz ( class_id INT PRIMARY KEY AUTO_INCREMENT, class_name VARCHAR(32) NOT NULL, grade VARCHAR(8) NOT NULL COMMENT 如 2023, major VARCHAR(64) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 3. 学生表user_id 关联登录账号 CREATE TABLE student ( student_id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(32) NOT NULL, gender TINYINT DEFAULT 1, class_id INT NOT NULL, user_id INT, KEY idx_class (class_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 4. 课程表一门课由一个教师负责 CREATE TABLE course ( course_id INT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL UNIQUE, course_name VARCHAR(64) NOT NULL, credit DECIMAL(3,1) DEFAULT 2.0, teacher_id INT, KEY idx_teacher (teacher_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 5. 成绩表全系统的核心 CREATE TABLE score ( score_id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, term VARCHAR(16) NOT NULL COMMENT 如 2024-2025-1, exam_type TINYINT NOT NULL DEFAULT 1 COMMENT 1期末 2期中 3补考, score DECIMAL(5,2) NOT NULL, input_by INT NOT NULL COMMENT 录入人 user_id, input_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_stu_course (student_id, course_id, term, exam_type), KEY idx_course_term (course_id, term), CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES student(student_id), CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES course(course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几个关键决策值得单独说清楚成绩字段用DECIMAL(5,2)而不是FLOAT或DOUBLE。浮点数在求和、求平均时会出现79.99999999这种结果页面上四舍五入看着没事但你要做「平均分 80 分以上班级」的筛选时边界数据就会莫名其妙地掉出去。这类问题极难复现排查起来很麻烦。DECIMAL(5,2)最多支持到999.99对百分制成绩绰绰有余。唯一索引uk_stu_course承担的是业务约束。同一个学生、同一门课、同一学期、同一考试类型只能有一条成绩记录。这个约束写在数据库里比在 Service 里写if (exists) update else insert靠谱得多——后者在两个人同时录入时会出现并发写入两条记录都能插进去事后清理数据非常麻烦。idx_course_term是为统计查询准备的。ECharts 的每个图背后几乎都是「按课程 学期聚合」没有这个联合索引数据量一上千条统计接口就开始出现明显卡顿。3. 核心业务实现登录拦截、成绩录入与分页查询的三段实现3.1 一次登录管三个角色会话与路径级拦截系统的角色区分没有做得很复杂靠 URL 前缀就够了/admin/*管理员/teacher/*教师/student/*学生。拦截逻辑集中在一个 Filter 里避免每个 Servlet 都写一遍if (session.getAttribute(user) null)。WebFilter(urlPatterns {/admin/*, /teacher/*, /student/*}) public class LoginFilter implements Filter { // 白名单登录页、登录接口放行否则会自己把自己拦死 private static final SetString WHITE new HashSet(Arrays.asList( /login.jsp, /login, /logout)); Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; // 去掉上下文路径只比较业务路径 String uri request.getRequestURI().substring(request.getContextPath().length()); if (WHITE.contains(uri) || uri.startsWith(/static/)) { chain.doFilter(req, resp); return; } HttpSession session request.getSession(false); User user (session null) ? null : (User) session.getAttribute(loginUser); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } // 角色与路径前缀不匹配时直接 403不给二次跳转的机会 boolean adminOnly uri.startsWith(/admin/) user.getRole() ! 1; boolean teacherArea uri.startsWith(/teacher/) user.getRole() 3; if (adminOnly || teacherArea) { response.sendError(HttpServletResponse.SC_FORBIDDEN, no permission); return; } chain.doFilter(req, resp); } }这段代码里有三处容易写错的地方。getSession(false)不能传true否则每个未登录请求都会在服务端生成一个空 session用户多了内存占用会升高。WHITE白名单必须包含登录页本身否则重定向过去又被拦浏览器会报「重定向次数过多」。角色判断要用sendError(403)而不是sendRedirect因为 403 是明确的语义前端看到就知道是权限问题不用猜。登录接口本身没什么花哨的用户名查出来之后比对摘要成功就把User对象塞进 session然后按角色跳不同首页User user userDao.findByUsername(username); if (user null || !PasswordUtil.matches(password, user.getPassword())) { request.setAttribute(msg, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); return; } // 登录成功后重置 sessionId防止会话固定 request.changeSessionId(); request.getSession().setAttribute(loginUser, user); response.sendRedirect(request.getContextPath() (user.getRole() 3 ? /student/index : /teacher/index));changeSessionId()这一步很多人会省略。它的作用是登录成功后换一个新的会话标识防止攻击者事先拿到一个 sessionId 再诱导用户登录。答辩时如果被问到「你的系统怎么防会话固定」这一行就是现成的答案。3.2 成绩录入先把校验写死再谈批量提交成绩录入有两个入口教师单条录入以及从 Excel 整理好之后批量提交。不管哪个入口落到 DAO 上的都是同一个批量插入方法因为逐条insert在几十条数据时就会明显变慢事务边界也不好控制。public int batchSave(ListScore list) throws SQLException { // 唯一索引冲突时改为更新避免重复录入报错 String sql INSERT INTO score(student_id, course_id, term, exam_type, score, input_by) VALUES(?,?,?,?,?,?) ON DUPLICATE KEY UPDATE score VALUES(score), input_by VALUES(input_by), input_time NOW(); Connection conn null; PreparedStatement ps null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 一批成绩要么全进要么全退 ps conn.prepareStatement(sql); for (Score s : list) { ps.setInt(1, s.getStudentId()); ps.setInt(2, s.getCourseId()); ps.setString(3, s.getTerm()); ps.setInt(4, s.getExamType()); ps.setBigDecimal(5, s.getScore()); ps.setInt(6, s.getInputBy()); ps.addBatch(); } int[] rows ps.executeBatch(); conn.commit(); return rows.length; } catch (SQLException e) { if (conn ! null) { conn.rollback(); // 任何一条失败整批回滚 } throw e; } finally { DBUtil.close(ps, conn); } }ON DUPLICATE KEY UPDATE是这段代码的核心。它把「判断存不存在」和「插入或更新」合并成一条原子语句交给数据库自己保证一致性Service 层不需要先select再分支。MySQL 5.7 和 8.x 都支持这种写法VALUES(score)在 8.0.20 之后会有废弃警告但功能正常如果用的是较新版本可以换成AS new的行别名写法。真正容易出问题的是校验环节。分数必须是 0 到 100 之间不能是负数不能是null不能是「缺考」这种中文。我的做法是在 Service 入口一次性校验完全部合法才进 DAOfor (Score s : list) { BigDecimal v s.getScore(); if (v null || v.compareTo(BigDecimal.ZERO) 0 || v.compareTo(new BigDecimal(100)) 0) { throw new BizException(学号 s.getStudentNo() 的成绩不在 0~100 区间); } }注意这里的错误信息带上了学号。批量导入时如果只提示「成绩不合法」教师根本不知道是哪一条出的问题只能一行行翻体验很差。带上定位信息前端弹窗直接显示改一行重传就行。注意不要在前端用 JS 做唯一校验。前端能改、能绕过真正的守门人必须是数据库约束加后端校验前端校验只是为了少一次请求。3.3 成绩查询动态条件拼接与物理分页查询页的筛选条件有四个班级、课程、学期、学号或姓名关键字。这四个都是可选的用户可能只填其中一个也可能一个都不填。用拼接 SQL 是必然的关键是参数必须走PreparedStatement不能拼进字符串。public PageResultScoreVO page(ScoreQuery q) throws SQLException { StringBuilder where new StringBuilder( WHERE 11 ); ListObject params new ArrayList(); if (q.getClassId() ! null) { where.append( AND st.class_id ? ); params.add(q.getClassId()); } if (q.getCourseId() ! null) { where.append( AND s.course_id ? ); params.add(q.getCourseId()); } if (StringUtils.isNotBlank(q.getTerm())) { where.append( AND s.term ? ); params.add(q.getTerm()); } if (StringUtils.isNotBlank(q.getKeyword())) { where.append( AND (st.name LIKE ? OR st.student_no LIKE ?) ); String like % q.getKeyword().trim() %; params.add(like); params.add(like); } String baseFrom FROM score s JOIN student st ON s.student_id st.student_id JOIN course c ON s.course_id c.course_id JOIN clazz cl ON st.class_id cl.class_id ; // 先查总数再查当页数据分页组件需要 total long total count(baseFrom where, params); int offset (q.getPageNo() - 1) * q.getPageSize(); String listSql SELECT s.score_id, st.student_no, st.name, cl.class_name, c.course_name, s.term, s.exam_type, s.score baseFrom where ORDER BY st.student_no LIMIT ?, ?; params.add(offset); params.add(q.getPageSize()); // ... 执行查询并映射成 ScoreVO }用WHERE 11开头是为了让后面每个条件都能以AND起手不用判断「这是不是第一个条件」。这种写法看起来有点粗糙但在动态查询里是最不容易出错的。ORDER BY必须显式指定否则 MySQL 返回的顺序不确定翻页时会出现同一条数据在两页都出现的情况。分页用LIMIT offset, size做物理分页而不是把所有数据查出来在 Java 里subList。课程设计的数据量小两种写法看不出差别但只要成绩表过了几万条全量查询会把 Tomcat 的内存迅速消耗干净页面直接卡死。这个习惯建议从第一天就养成。4. ECharts 统计图模块从聚合 SQL 到前端渲染的数据链路4.1 统计接口一个 Servlet 按 type 分发四类数据统计模块是整个系统里唯一需要向前端返回 JSON 的地方。我没有为每种图表单独建一个 Servlet而是用一个/api/stats/*加路径参数分发减少重复代码WebServlet(/api/stats/*) public class StatsServlet extends HttpServlet { private final StatsService statsService new StatsService(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setContentType(application/json;charsetUTF-8); String type req.getPathInfo(); // /courseAvg、/distribution 等 String term req.getParameter(term); Integer courseId parse(req.getParameter(courseId)); Object data; switch (type null ? : type) { case /courseAvg: data statsService.courseAvg(term); break; case /classAvg: data statsService.classAvg(term); break; case /distribution: data statsService.distribution(courseId, term); break; case /trend: data statsService.termTrend(courseId); break; default: resp.sendError(404); return; } resp.getWriter().write(JsonUtil.toJson(data)); } }接口返回结构统一成两个字段categories存横轴标签series存数值数组。这样前端拿到就能直接用不用为每个图写一套解析逻辑。{ categories: [高等数学, 大学英语, 数据结构, 操作系统], series: [78.35, 81.20, 74.60, 79.05] }提示resp.setContentType(application/json;charsetUTF-8)这一行不能省。少了它浏览器会按 HTML 解析前端response.json()直接抛异常控制台报的是「Unexpected token 」看着像接口 404实际是编码问题。如果项目里没有引入 Gson 或 Jackson用StringBuilder手拼也能用但要注意数值不要加引号、字符串里的双引号要转义。数据里有中文课程名时转义这一步漏掉图表标签就会变成空白。4.2 聚合 SQL 与分数段分桶四张图背后是四段聚合查询。课程平均分这张用得最多它同时给出平均分、最高分、最低分和及格率SELECT c.course_name, COUNT(*) AS cnt, ROUND(AVG(s.score), 2) AS avg_score, MAX(s.score) AS max_score, MIN(s.score) AS min_score, ROUND(SUM(CASE WHEN s.score 60 THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS pass_rate FROM score s JOIN course c ON s.course_id c.course_id WHERE s.term ? AND s.exam_type 1 GROUP BY c.course_id, c.course_name ORDER BY avg_score DESC;GROUP BY里带上c.course_id是有原因的。如果只按course_name分组两门同名但不同编号的课会被合并成一条数据就对不上了。虽然实际中很少出现同名课程但这种「看起来没错」的写法一旦出问题排查成本极高。分数段分布图用的是CASE WHEN分桶这段 SQL 有个细节容易被忽略SELECT CASE WHEN score 60 THEN 不及格 WHEN score 70 THEN 60-69 WHEN score 80 THEN 70-79 WHEN score 90 THEN 80-89 ELSE 90-100 END AS bucket, COUNT(*) AS cnt FROM score WHERE course_id ? AND term ? AND exam_type 1 GROUP BY bucket ORDER BY FIELD(bucket, 不及格, 60-69, 70-79, 80-89, 90-100);ORDER BY FIELD(...)是必须的。MySQL 对GROUP BY别名的排序默认按字符串字典序结果是「60-69、70-79、80-89、90-100、不及格」——「不及格」会排到最后柱状图看起来完全不符合直觉。用FIELD显式指定顺序图表的横轴才和人的认知一致。学期趋势图则按term分组用来回答「这个班成绩是在进步还是退步」。这四类查询都建在idx_course_term索引上几百上千条数据基本是毫秒级返回。4.3 前端 option柱状图作为主图平均线作参照图表容器的高度必须在 CSS 里写死或给最小值这是 ECharts 最常见的「不显示」原因。容器高度是 0init能成功但画布高度为零页面上什么都看不到。// 课程平均分柱状图 全局平均线 const dom document.getElementById(courseAvgChart); const chart echarts.init(dom); fetch(ctx /api/stats/courseAvg?term encodeURIComponent(term)) .then(resp resp.json()) .then(data { const all data.series; const avg all.reduce((a, b) a b, 0) / (all.length || 1); chart.setOption({ tooltip: { trigger: axis }, grid: { left: 40, right: 20, bottom: 60, top: 40 }, xAxis: { type: category, data: data.categories, axisLabel: { interval: 0, rotate: 30 } // 课程名长倾斜避免遮挡 }, yAxis: { type: value, min: 0, max: 100, name: 分数 }, series: [{ name: 平均分, type: bar, data: all, barMaxWidth: 40, itemStyle: { color: #4a7fd4 }, markLine: { silent: true, symbol: none, data: [{ type: average, name: 总平均 }] }, label: { show: true, position: top, formatter: {c} } }] }); }); // 窗口尺寸变化时重绘否则侧边栏收起后图表会错位 window.addEventListener(resize, () chart.resize());axisLabel.interval设为 0 表示强制显示所有标签不设的话 ECharts 会自动抽稀课程一多就只剩几个名字答辩时被问到「这个柱子是哪门课」会很尴尬。markLine用type: average让 ECharts 自己算平均线省得在 Java 端再算一次。饼图和折线图的配置结构基本相同区别只在series.type和数据维度。四张图共用一套resize监听和一个统一的term下拉框切换学期时把四个接口重新请求一遍即可。5. 避坑与常见问题排查五个最容易在答辩前夜翻车的点5.1 中文乱码三条链路要一起改现象表单里输入的中文姓名存进数据库变成???或者页面上显示成æŽå°。原因乱码可能发生在三个位置——请求体解码、数据库连接、表字符集。只改一处另外两处照样出问题。Tomcat 对 POST 请求默认按 ISO-8859-1 解码JDBC 连接串不写字符集就用服务端默认值建表时没指定utf8mb4又会用库的默认字符集。解决三处一起改。第一在 Filter 链最前面加一个EncodingFilter执行request.setCharacterEncoding(UTF-8)注意必须排在LoginFilter之前第二JDBC URL 加上?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai第三建表和建库统一DEFAULT CHARSETutf8mb4。改完之后要清一次 Tomcat 的work目录因为编译过的 JSP 会被缓存旧编码的结果还会继续出现。5.2 404 与 500映射没生效还是依赖发生冲突现象浏览器访问/score/list报 404或者进了页面直接 500堆栈里出现ClassNotFoundException: javax.servlet.http.HttpServlet。原因404 多半是WebServlet没生效——web.xml里写了metadata-completetrue会关闭注解扫描注解就变成摆设。500 里的ClassNotFoundException则是把servlet-api.jar一起打进了WEB-INF/lib与 Tomcat 自带的版本冲突容器加载时找不到正确的实现。解决先看web.xml的metadata-complete属性用注解就设成false或不写再检查WEB-INF/lib下有没有servlet-api.jar和jsp-api.jar有就删掉编译时用provided作用域引入。另外JSP 放在WEB-INF/views下时浏览器访问/WEB-INF/views/xx.jsp一定是 404这是规范限制只能通过 Servlet 转发进去。5.3 重复录入唯一索引比 if 判断更可靠现象同一个学生的同一门课成绩表里出现了两条记录统计出来的平均分偏低。原因Service 层的写法是「先select判断有没有没有就insert」。两个人同时提交、或者教师连点两次提交按钮两个线程都查到「不存在」然后都执行了插入。解决把约束下沉到数据库建UNIQUE KEY uk_stu_course (student_id, course_id, term, exam_type)插入语句改用ON DUPLICATE KEY UPDATE。同时在前端提交按钮上做一次禁用防止连击。这是唯一能真正解决问题的组合——前端防的是误操作数据库防的是并发。已经存在重复数据的老库要先按唯一键分组、保留score_id最大的一条再补建索引。5.4 图表一片空白九成是接口没返回 JSON现象页面其他部分正常只有 ECharts 那块是白的控制台报Unexpected token 。原因最常见的是统计接口被LoginFilter拦了返回的是一个 302 重定向到登录页前端拿到的是 HTMLresponse.json()解析失败。其次是 Servlet 的urlPatterns写成/api/stats而不是/api/stats/*getPathInfo()拿到null直接走到default分支返回 404。第三是容器 CSS 没给高度。解决在浏览器开发者工具的 Network 面板里看那个请求如果状态是 302 或者响应体是 HTML就是拦截问题把/api/*加进 Filter 的放行判断或者在统计接口内部自行校验 session 并返回 401 JSON。如果getPathInfo()为null检查urlPatterns末尾的*。最后在 CSS 里给容器写死height: 360px别用height: 100%去撑一个没有高度的父元素。5.5 连接泄漏页面越点越慢的元凶现象刚启动时响应很快用了十几分钟后页面开始转圈最后整个应用无响应日志里出现Timeout waiting for idle object。原因DAO 里拿了Connection和PreparedStatement但只在正常分支里close()catch块里忘了关或者ResultSet压根没关。每次异常都漏掉一个连接连接池很快就被占满。解决把所有资源的关闭统一放到finally里或者干脆用 try-with-resources。如果没上连接池直接用DriverManager每次新建连接性能会很差建议至少引入一个轻量连接池把最大连接数控制在 20 到 50 之间。排查阶段可以临时在DBUtil.getConnection()里打印获取和归还的计数两个数字差距持续扩大就说明有地方在漏。6. 验收与答辩一份能照着念的演示脚本与一张验收清单系统做完不等于能交。答辩现场只有十来分钟顺序错了或者中间卡一次后面全乱。我的习惯是提前写一份演示脚本按「角色切换 功能递进」排好每一屏都对应一个能说出口的设计点。步操作路径演示要点预期结果1管理员登录 → 用户管理密码不入库明文、角色字段区分列表分页正常新增学生可登录2教师登录 → 成绩录入输入 105 分时的校验提示前端拦截 后端报错不入库3同一学生同一课再录一次唯一索引与覆盖更新表里仍是一条记录分数已更新4成绩查询 → 组合三个条件动态 SQL 与物理分页结果正确翻页不重复5统计页 → 切换学期四张图联动刷新柱状图、饼图、折线图数据一致6用学生账号访问/teacher/score/input权限拦截403不跳登录页7直接访问/WEB-INF/views/scoreList.jsp目录保护404演完这套流程基本把「权限、校验、并发、统计、安全」五个点都覆盖到了。剩下的时间留给提问常见的问题无非这几类为什么不用框架、Filter 和拦截器的区别、唯一索引为什么放在数据库、ECharts 的数据从哪来。前三个用上面的分层表和uk_stu_course就能答最后一个直接打开StatsServlet指出switch分发的四个分支再切到对应的聚合 SQL比嘴上解释清楚得多。有个细节值得单独提醒演示前一定要用另开一个浏览器的无痕窗口登录学生账号。同一个浏览器里 session 是共享的用教师账号登录后再开标签页访问学生路径看起来像越权成功了实际上是 session 没切换现场很容易被问住解释起来也尴尬。血泪经验是我每次交东西前都会强制走一遍「换角色把所有菜单点一遍」的流程特别是那些平时不用的入口。上一次翻车就出在这里一个只在教师端出现的导出按钮参数拼的是班级 id而测试数据里恰好没有那个班级点下去白屏。这种问题在开发阶段几乎不可能发现只有按角色完整走一遍才会遇到。希望这份结构能帮到你把系统跑通只是起点把每个设计决策讲清楚才是验收的关键。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站