简介这是一套基于ServletJSPMySQL开发的学生成绩管理系统完整源码面向JavaWeb初学者、课程设计及毕业设计人群帮助读者在真实项目中理解MVC分层、数据库访问与前后端交互系统在原有版本上新增ECharts统计图表可对学生成绩分布、课程数据等进行可视化展示适合作为功能扩展与图表集成的参考案例。压缩包共344个文件约3.59MB以86个Java源文件及对应的class文件、37个JSP页面、62个JavaScript脚本和28个CSS样式为主另含数据库SQL脚本、jar依赖以及字体、图片等静态资源覆盖实体、Dao、Service、Servlet与前端页面等层次结构完整导入后便于按模块阅读和调试。目前已有6724人学习下载读者可获得可运行的工程源码、建库脚本以及成绩管理、文件上传下载等典型功能实现既能用于课程作业快速搭建也适合在此基础上改造界面、补充统计维度作为JavaWeb综合实践的参考。1. 从一张汇总不完的成绩表说起这套系统到底要替谁干活每到期末任课老师手里都会多出一张自己维护的成绩 Excel教务那边再收上来手工合并班里几十门课、几百个学生版本一多就没人说得清哪张才是最终的。这个场景下需要的不是什么高深架构而是一套能把「录入—查询—统计」三件事收拢到同一个入口的 javaweb 学生成绩 成绩管理系统。Servlet 负责请求分发和业务编排JSP 承担页面渲染MySQL 存下学生、课程、成绩这些结构化数据最后用 ECharts 把分数段分布、课程平均分、及格率画出来替掉手工统计这一环。这套组合适合两类人正在做课程设计、需要一套完整可运行工程的在校学生以及需要快速交付一个内部成绩管理原型的小团队。它不追求技术栈新颖追求的是每一层都看得见、改得动、答辩时讲得清。2. 技术选型与总体架构ServletJSPMySQL 的三层结构与包约定动手写第一行代码之前先把「谁负责什么」定下来比后面写多少代码都重要。学生成绩管理这类系统的业务复杂度不高真正的麻烦在于角色多、页面多、数据关系多如果没有清晰的分层写到第三个模块就会开始互相调用、SQL 散落各处最后连自己都不敢改。这一章把选型理由、包结构和表结构一次性定死后面三章都是在这套骨架里填肉。2.1 为什么这套作业型项目仍然值得用 ServletJSP 打底很多人第一反应是「都什么年代了还用 JSP」。如果目标是做一个高并发的互联网产品这个质疑成立但如果是课程设计、毕设或者内部小系统ServletJSP 反而有几个别的东西替代不了的好处。第一是链路上没有任何黑匣子。一个请求从浏览器发出经过 Filter、Servlet、Service、DAO最后落到 JDBC 和 MySQL每一环你都能打断点、打日志、看清参数是怎么变的。用惯了高度封装框架的人一旦遇到「明明传了值却查不出来」往往要在框架源码里泡半天而在这套结构里多半就是request.getParameter拼错了名字五分钟能定位。第二是部署成本低。一个 war 包扔进 Tomcat 的webapps目录就能跑数据库建好表、改一下jdbc.properties里的四行配置就完事。答辩现场不用装一堆运行时评委机器上有个 Tomcat 和 MySQL 就能演示。第三是它逼着你想清楚 MVC 的边界。JSP 只做展示Servlet 只做接收参数和跳转Service 管事务和业务规则DAO 管 SQL。这条线一旦守住后面加统计图、加导出、加批量导入都是往固定位置塞代码不会把已有逻辑搅乱。技术版本上通常的做法是 Tomcat 8.5 或 9、JDK 8、MySQL 5.7 或 8.0、Servlet 3.1。选 Servlet 3.0 以上是因为可以用WebServlet、WebFilter注解替代 web.xml 里那一大段配置少写几十行 XML改起来也直观。2.2 三层结构怎么切entity / dao / service / servlet / jsp 的职责边界包结构建议按下面这样切不要按「所有 Servlet 一个包、所有 JSP 一个文件夹」这种图省事的方式堆com.sms ├── entity // 实体类与表字段一一对应 │ ├── Student.java │ ├── Course.java │ ├── Score.java │ └── User.java ├── dao // 数据访问只写 SQL 和结果集映射 │ ├── BaseDao.java │ ├── StudentDao.java │ ├── CourseDao.java │ └── ScoreDao.java ├── service // 业务规则与事务边界 │ ├── ScoreService.java │ └── StatService.java ├── servlet // 接收请求、校验参数、调用 service、转发视图 │ ├── LoginServlet.java │ ├── ScoreServlet.java │ └── StatServlet.java ├── filter // 登录拦截、编码统一 │ └── LoginFilter.java └── util // 连接池、JSON 输出、分页封装 ├── DBUtil.java └── Page.java职责边界用一句话概括Servlet 不写 SQLDAO 不做业务判断JSP 不写 Java 逻辑。具体到一次成绩查询请求ScoreServlet从request里取课程号、学号、页码做非空和类型校验然后调ScoreService.queryByPage(...)Service 里判断「如果是学生角色强制把学号替换成当前登录人」再调ScoreDaoDAO 拼WHERE条件、执行LIMIT分页、把ResultSet映射成ListScoreServlet 拿到结果后request.setAttribute(page, page)forward到score_list.jspJSP 里只用 JSTL 的c:forEach遍历。这条线守住了好处在后期特别明显。比如要加一个「按班级筛选」只需要在 DAO 的条件拼装里加一个分支、在 JSP 里加一个下拉框其他层一行不动。反过来如果把 SQL 写在 JSP 的% %里加这个功能就要翻遍所有页面。提示entity类不要图省事用MapString,Object代替。字段名写错在编译期抓不到运行期才报空指针这是最常见的返工来源。2.3 数据库表设计与约束六张表撑起成绩管理成绩管理的核心关系其实就一句话一个学生选多门课一门课被多个学生选成绩是学生和课程之间的多对多关联。落到表上通常需要这六张表名作用关键字段约束t_user登录账号id, username, password, roleusername 唯一t_class班级id, class_name, gradeclass_name 唯一t_student学生id, sno, name, class_idsno 唯一class_id 外键t_course课程id, course_name, credit, teacher_idcourse_name 唯一t_score成绩id, student_id, course_id, score, term(student_id, course_id, term) 联合唯一t_teacher教师id, tno, nametno 唯一成绩表的建表语句是这个系统的地基值得单独写清楚CREATE TABLE t_score ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, student_id BIGINT NOT NULL COMMENT 学生ID, course_id BIGINT NOT NULL COMMENT 课程ID, score DECIMAL(5,1) DEFAULT NULL COMMENT 成绩保留一位小数, term VARCHAR(16) NOT NULL COMMENT 学期如 2024-2025-1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_stu_course_term (student_id, course_id, term), KEY idx_course_term (course_id, term) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT成绩表;这里有三个设计点需要说明。score用DECIMAL(5,1)而不是FLOAT或DOUBLE。成绩是精确值FLOAT存 89.5 可能变成 89.499999后面统计平均分时误差会累积导出到 Excel 还会出现一串小数位。DECIMAL(5,1)表示总共 5 位、小数 1 位够存 100.0 以内的分数。联合唯一索引uk_stu_course_term是防重复录入的最后一道闸。业务代码里当然也会先查一遍但并发提交、页面重复点击这类情况防不胜防数据库约束是唯一可靠的兜底。注意要把学期带上否则同一个学生重修同一门课就没法录第二次。idx_course_term这个普通索引是给统计查询准备的。ECharts 那几张图基本都是「按课程 学期聚合」没有这个索引几万条成绩做GROUP BY会明显卡顿。索引不是越多越好成绩表写入频繁每多一个索引写入就多一份开销所以只给真正高频的查询条件建。注意外键约束在课程设计里建议加上答辩时能体现完整性但如果你打算做批量导入几万条数据导入前要先关掉外键检查导完再打开否则逐行校验会慢到怀疑人生。3. 核心业务实现登录鉴权、成绩录入与分页查询的三段落地骨架搭好之后真正决定这套系统「能不能用」的是三个动作谁能进来、成绩怎么安全地写进去、几百条记录怎么让人看得下去。这一章按这三个动作各给一段可复现的实现。3.1 登录与会话Filter Session 把住入口登录本身很简单查一次t_user比对密码就完了。真正容易出问题的是「登录之后」。如果每个 Servlet 开头都写一遍if (session.getAttribute(user) null) { 跳登录页 }代码会重复到吐而且只要漏写一个那个接口就是裸奔的。统一的做法是用 Filter 拦截// LoginFilter.java —— 统一登录拦截白名单直接放行 WebFilter(urlPatterns /*) public class LoginFilter implements Filter { // 白名单登录相关页面与静态资源不需要登录态 private static final String[] WHITE_LIST { /login.jsp, /login, /css/, /js/, /img/, /fonts/ }; Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; // 去掉上下文路径得到 /score/list 这样的相对路径 String uri request.getRequestURI() .substring(request.getContextPath().length()); for (String white : WHITE_LIST) { if (uri.startsWith(white)) { chain.doFilter(req, resp); // 白名单命中直接放行 return; } } Object loginUser request.getSession().getAttribute(loginUser); if (loginUser null) { // 未登录重定向到登录页用 sendRedirect 让地址栏同步变化 response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }几个参数和行为需要说清楚。urlPatterns /*表示拦截所有请求包括 JSP 和 Servlet所以白名单必须显式列出登录页和静态资源否则登录页自己也被拦会陷入无限重定向。uri.substring(contextPath.length())是为了去掉项目名让白名单写成/css/这种与部署路径无关的形式换一个 context path 也不用改代码。用sendRedirect而不是forward是因为跳转后地址栏要变成登录页的真实地址用户刷新才不会又提交一次原请求。登录成功时往 session 里放的不只是用户名通常是一个精简的User对象含 id、username、role。role 这个字段是后面所有权限判断的源头学生进来只能看自己的成绩教师能录自己教的课管理员什么都能干。这个判断不要写在 Filter 里Filter 只管「有没有登录」具体到「这个学生能不能看这条成绩」应该放在 Service 层因为那属于业务规则。session 超时时间在web.xml里配session-config session-timeout30/session-timeout !-- 单位分钟 -- /session-config30 分钟是个折中值。太短演示到一半要重新登录很尴尬太长机房公用电脑上别人接着用你的账号。答辩场景建议调到 60正式部署再改回去。3.2 成绩录入的事务控制与批量导入单个成绩录入是一条INSERT没什么好说的。麻烦的是两种情况教师一次性给整个班录一门课几十条记录要么全成功要么全失败以及修改成绩时「先删后插」这种容易把自己坑进去的写法。先看批量录入。JDBC 的addBatch()比循环执行executeUpdate()快一个数量级因为减少了网络往返。但批量操作必须包在事务里// ScoreService.java —— 批量录入成绩全部成功才提交 public int batchSave(Long courseId, String term, MapLong, BigDecimal stuScoreMap) throws SQLException { Connection conn null; PreparedStatement ps null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交开启事务 String sql INSERT INTO t_score(student_id, course_id, score, term) VALUES(?, ?, ?, ?) ON DUPLICATE KEY UPDATE score VALUES(score); ps conn.prepareStatement(sql); for (Map.EntryLong, BigDecimal e : stuScoreMap.entrySet()) { ps.setLong(1, e.getKey()); // 学生ID ps.setLong(2, courseId); // 课程ID ps.setBigDecimal(3, e.getValue()); // 成绩可能为 null 表示缺考 ps.setString(4, term); // 学期 ps.addBatch(); // 攒着不立即执行 } int[] rows ps.executeBatch(); // 一次性发给数据库 conn.commit(); // 全部成功提交 return rows.length; } catch (SQLException ex) { if (conn ! null) { conn.rollback(); // 任何一条失败整体回滚 } throw ex; } finally { DBUtil.close(ps, conn); // 关闭顺序先语句后连接 } }这里有几个细节值得展开。ON DUPLICATE KEY UPDATE配合前面的联合唯一索引把「新增」和「修改」合并成一条语句。教师重复提交同一门课的成绩时不会报主键冲突而是直接覆盖。这比「先DELETE再INSERT」安全得多——后者如果在两步之间出错成绩就凭空消失了而且删除操作在 binlog 里很难追。setAutoCommit(false)之后一定要在finally里保证连接被关闭否则这个连接会带着未提交的事务回到连接池下一个借到它的人会看到脏数据或者莫名锁等待。这是 JDBC 事务里最容易翻车的点之一。成绩值允许为null表示缺考。setBigDecimal(3, null)是合法的插入后数据库里就是NULL统计平均分时AVG()会自动忽略NULL正好符合「缺考不计入平均分」的口径。如果业务上要求缺考记 0 分那就在 Service 里把null换成BigDecimal.ZERO这个策略要提前定不要中途改。提示批量录入前一定要在后端再校验一次分数范围0 到 100。前端input typenumber的min/max谁都能改掉后端不校验等于没校验。3.3 多条件分页查询的 SQL 拼装与参数绑定成绩列表页是最考验体验的地方。一个学期下来几千条记录不分页浏览器直接卡死条件筛选做不好教务想查「某班某门课不及格的」要翻半天。分页的核心是两条 SQL一条查总数一条查当前页数据。参数用LIMIT ?, ?绑定不要字符串拼接// ScoreDao.java —— 多条件分页查询条件用占位符绑定 public PageScore queryByPage(ScoreQuery q, int pageNo, int pageSize) throws SQLException { StringBuilder where new StringBuilder( WHERE 11 ); ListObject params new ArrayList(); 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.getClassName())) { where.append( AND c.class_name ? ); params.add(q.getClassName()); } if (q.getMinScore() ! null) { where.append( AND s.score ? ); params.add(q.getMinScore()); } // 第一条统计总数供前端算总页数 String countSql SELECT COUNT(*) FROM t_score s JOIN t_student st ON s.student_id st.id JOIN t_class c ON st.class_id c.id where; int total count(countSql, params.toArray()); // 第二条取当前页按成绩倒序同分按学号升序保证稳定 String dataSql SELECT s.id, st.sno, st.name, co.course_name, s.score, s.term FROM t_score s JOIN t_student st ON s.student_id st.id JOIN t_class c ON st.class_id c.id JOIN t_course co ON s.course_id co.id where ORDER BY s.score DESC, st.sno ASC LIMIT ?, ? ; params.add((pageNo - 1) * pageSize); params.add(pageSize); ListScore list query(dataSql, params.toArray()); return new Page(list, total, pageNo, pageSize); }WHERE 11这种写法很多人嫌丑但它在动态拼条件时确实省事——每个条件都能以AND开头不用判断是不是第一个条件。参数全部走?占位符是防 SQL 注入的底线别图省事用 q.getTerm() 拼进去。ORDER BY里加了st.sno ASC这个次级排序是为了让结果稳定。只按score DESC排同分记录的先后顺序由数据库决定翻页时可能出现某条记录在第 1 页和第 2 页都出现、另一条凭空消失的「玄学」现象。这不是 bug是不稳定排序的正常表现但用户会当成 bug 报给你。LIMIT的偏移量(pageNo - 1) * pageSize要在这里算好。页码从 1 开始偏移量从 0 开始这个差一是最常见的翻页错误来源。前端 JSP 侧用 JSTL 渲染不要在页面里写% for (...) %c:forEach items${page.list} vars varStatusst tr td${st.index 1 (page.pageNo - 1) * page.pageSize}/td td${s.sno}/td td${s.name}/td td${s.courseName}/td tdfmt:formatNumber value${s.score} pattern0.0//td td${s.term}/td /tr /c:forEach序号列要算上前面已翻过的页否则每页都从 1 开始看着很怪。成绩用fmt:formatNumber统一格式化成一位小数避免89.5和89.50混着显示。4. ECharts 统计模块从聚合 SQL 到前端图表的完整链路统计图是这套系统里最容易被低估的部分。很多人以为「查出来数据丢给图表库就完了」实际做下来会发现统计口径没定清楚图画出来是对的但业务上没用JSON 格式差一个引号图表就是一片空白数据量一大页面加载慢到超时。这一章按「定口径 → 出数据 → 画图」的顺序走一遍完整链路。4.1 统计口径先定死分数段、平均分、及格率三个指标写 SQL 之前先把要回答的问题写清楚。成绩管理场景下真正有人看的图其实就三张分数段分布回答「这门课考得怎么样」。分段通常是 90-100、80-89、70-79、60-69、60 以下五档。分段边界要写进需求否则不同人算出来的数对不上。注意score 90和score 90差一个人教务对账时能跟你较半天。课程平均分对比回答「哪个班/哪门课整体偏低」。这个是横向比较横轴是课程或班级纵轴是平均分。AVG()会自动跳过NULL也就是缺考不计入这一点要跟使用者说清楚。及格率回答「达标情况」。及格线定 60COUNT(score 60) / COUNT(*)注意分母要不要排除缺考两种口径结果不一样得选一种。对应的聚合 SQL-- 某课程某学期的分数段分布 SELECT CASE WHEN score 90 THEN 90-100 WHEN score 80 THEN 80-89 WHEN score 70 THEN 70-79 WHEN score 60 THEN 60-69 WHEN score IS NOT NULL THEN 60以下 END AS segment, COUNT(*) AS cnt FROM t_score WHERE course_id ? AND term ? GROUP BY segment ORDER BY FIELD(segment, 90-100,80-89,70-79,60-69,60以下);ORDER BY FIELD(...)这一步很关键。GROUP BY出来的顺序是不保证的如果不显式排序柱子可能按字母序排成「60以下、60-69、70-79…」看着别扭还可能被误读。FIELD按给定顺序返回位置序号是 MySQL 里做自定义排序的常用手段。WHEN score IS NOT NULL THEN 60以下这个写法把缺考NULL排除在分段之外。如果把NULL也归进「60以下」及格率会被拉低但那不是真实的学业表现。4.2 后端 JSON 输出一个 Servlet 对应一组图表数据Servlet 这边的工作是把ResultSet变成前端能直接用的 JSON。手写字符串拼接容易漏逗号、漏引号建议用 Gson 或者 Jackson这里以 Gson 为例// StatServlet.java —— 输出分数段分布的 JSON WebServlet(/stat/segment) public class StatServlet extends HttpServlet { private final StatService statService new StatService(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType(application/json;charsetUTF-8); Long courseId Long.valueOf(req.getParameter(courseId)); String term req.getParameter(term); // 返回 LinkedHashMap保证 key 顺序与 SQL 的排序一致 MapString, Object result new LinkedHashMap(); try { ListMapString, Object rows statService.segmentStat(courseId, term); ListString segments new ArrayList(); ListInteger counts new ArrayList(); for (MapString, Object row : rows) { segments.add(String.valueOf(row.get(segment))); counts.add(((Number) row.get(cnt)).intValue()); } result.put(code, 0); result.put(segments, segments); result.put(counts, counts); } catch (Exception e) { result.put(code, 500); result.put(msg, 统计查询失败); getServletContext().log(segment stat error, e); } resp.getWriter().write(new Gson().toJson(result)); } }几个要点。resp.setContentType(application/json;charsetUTF-8)必须在getWriter()之前调用顺序反了设置不生效中文课程名会变成问号。把数据拆成segments和counts两个平行数组是因为 ECharts 的xAxis.data和series.data就是这个结构前端拿到直接赋值中间少一层转换。异常也要返回 JSON不能让它抛成 HTML 错误页。前端$.getJSON拿到一坨 HTML 会直接进error回调控制台只报「Unexpected token 」排查起来很费劲。用code字段区分成功失败前端按code判断比看 HTTP 状态码更可控。4.3 前端渲染与联动ECharts option 的关键配置页面里引入 ECharts 的脚本后初始化一个容器把后端数据喂进 option// stat_segment.jsp 中的图表初始化 var chart echarts.init(document.getElementById(segmentChart)); function loadSegment(courseId, term) { $.getJSON(ctx /stat/segment, { courseId: courseId, term: term }, function (res) { if (res.code ! 0) { alert(res.msg || 数据加载失败); return; } chart.setOption({ tooltip: { trigger: axis, axisPointer: { type: shadow } }, grid: { left: 3%, right: 4%, bottom: 3%, containLabel: true }, xAxis: { type: category, data: res.segments, // 分数段顺序由后端 SQL 决定 axisLabel: { interval: 0 } // 强制显示所有标签避免被自动隐藏 }, yAxis: { type: value, name: 人数, minInterval: 1 // 人数是整数不要出现 0.5 这种刻度 }, series: [{ type: bar, data: res.counts, barWidth: 45%, itemStyle: { // 60 以下标红其余标蓝一眼看出不及格人数 color: function (p) { return p.name 60以下 ? #e74c3c : #4a90d9; } }, label: { show: true, position: top } }] }, true); // 第二个参数 true 表示不合并每次全量替换 }); }axisLabel: { interval: 0 }是必须加的。不加的话分数段标签在窄屏上会被自动隐藏只剩两根柱子用户根本不知道代表什么。minInterval: 1保证纵轴刻度是整数人数不会出现 2.5 这种值。setOption(option, true)的第二个参数控制是否合并。切换课程时如果传false旧的系列数据可能残留出现两根重叠的柱子。传true表示全量替换每次都是干净的状态。如果要做「点击柱子下钻到学生名单」在 option 里加chart.on(click, function(p){ ... })拿到p.name分数段再发一次请求查明细。这个联动是答辩时比较出彩的点实现成本却很低。5. 避坑与常见问题排查这套组合最容易翻车的五个地方这一章是血泪经验汇总。下面五个问题几乎每一个做过这套技术栈的人都至少踩过一次按「现象 → 原因 → 解决」写清楚遇到时可以直接对着查。5.1 中文乱码请求、响应、数据库三个不同的源头现象表单提交的中文姓名入库变成????或者查询出来的课程名在页面上显示成乱码或者 JSON 接口返回的中文变成问号。原因乱码有三个完全独立的源头很多人只堵了一个就以为解决了。第一是请求体编码POST 表单默认按 ISO-8859-1 解需要在读参数之前request.setCharacterEncoding(UTF-8)第二是响应编码response.setContentType(text/html;charsetUTF-8)要写在getWriter()之前第三是数据库连接JDBC URL 里要带useUnicodetruecharacterEncodingutf8同时表本身得是utf8mb4。解决最省事的做法是加一个编码 Filter放在所有 Filter 最前面统一处理前两项WebFilter(urlPatterns /*) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); // 必须在读参数前 resp.setCharacterEncoding(UTF-8); chain.doFilter(req, resp); } }数据库那项在jdbc.properties的 URL 里配好jdbc:mysql://localhost:3306/sms?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai。serverTimezone不加的话MySQL 8 驱动会直接抛时区异常这是另一个高频报错。5.2 数据库连接未关闭连接池耗尽的前兆现象系统跑一段时间后所有页面都转圈日志里出现Timeout waiting for idle object或者Too many connections重启 Tomcat 又能撑一会儿。原因某个 DAO 方法在异常路径上没有关闭Connection。正常流程走finally关掉了但一旦抛出SQLException跳到 catch 块如果 catch 里没有关闭逻辑连接就泄漏了。每泄漏一个连接池就少一个池子大小通常只有 10 到 20泄漏够数就彻底卡死。解决Connection、PreparedStatement、ResultSet的关闭一律放finally并且按「后开的先关」顺序。更好的办法是写个工具方法统一收口public static void close(ResultSet rs, Statement st, Connection conn) { try { if (rs ! null) rs.close(); } catch (SQLException ignored) {} try { if (st ! null) st.close(); } catch (SQLException ignored) {} try { if (conn ! null) conn.close(); } catch (SQLException ignored) {} }每个catch里单独 try-catch是因为关ResultSet失败不应该影响关Connection。这个细节看起来啰嗦但正是它保证了「一定关得掉」。5.3 JSP 里堆 Java 代码改一个字段要动五个文件现象页面里% %越写越多一个列表页三百行一半是 Java 逻辑。后来要给成绩加个「等级」字段得在五个 JSP 里各改一遍改漏一个就显示不一致。原因图省事把本该在 Service 里算的东西写到了页面里。JSP 编译成 Servlet 之后再排查问题堆栈指向_jspService这种自动生成的方法根本看不出是哪一行。解决JSP 里只允许出现 JSTL 标签和 EL 表达式禁止% %声明引入除外。所有计算逻辑上移到 Service 或 entity 的 getter 里。比如「根据分数算等级」在Score实体里加一个getLevel()方法页面直接${s.level}改规则只改一处。5.4 成绩的浮点数0.10.2 的经典陷阱现象平均分算出来是 78.69999999999999导出 Excel 后一列全是长小数或者两门课平均分明明一样比较却不相等。原因FLOAT/DOUBLE是二进制浮点无法精确表示 0.1 这类十进制小数累加和除法会引入误差。用float存成绩、在 Java 里用double算平均分误差就出来了。解决存储用DECIMALJava 侧用BigDecimal除法必须指定精度和舍入模式BigDecimal total new BigDecimal(0); for (BigDecimal s : scoreList) { total total.add(s); } // 保留两位小数四舍五入除数不能为 0提前判断 BigDecimal avg total.divide(new BigDecimal(scoreList.size()), 2, RoundingMode.HALF_UP);divide不指定精度时遇到除不尽的数比如 10 除以 3会直接抛ArithmeticException这个报错很突然一定要带精度参数。5.5 统计图空白数据为空、类型不匹配与浏览器缓存现象页面打开了图表容器是空的控制台没有明显报错或者偶尔刷新一下又出来了。原因常见的有三种。一是查询结果为空segments和counts都是空数组ECharts 画了个空坐标系二是后端返回的counts是字符串数组而不是数字数组ECharts 无法定位柱子高度三是 GET 请求被浏览器缓存切换课程后拿到的还是上一次的 JSON。解决空数据要在前端显式提示不要留白数字类型在后端intValue()转换时就要保证缓存问题给请求加个时间戳参数_t: new Date().getTime()或者后端响应头加Cache-Control: no-store。排查时最快的办法是直接浏览器打开接口地址/stat/segment?courseId1term2024-2025-1看返回的 JSON 结构对不对。JSON 对了就是前端问题JSON 不对就是 SQL 或 Service 问题一步就把范围切一半。6. 验收与答辩一份能自证的测试清单和演示动线代码写完不等于项目做完。答辩现场最怕的不是被问到不会的而是系统在你手上当场翻车——点了没反应、数据不对、图表空白。这一章给一份可以自己先跑一遍的验收清单以及一条八分钟讲完的演示动线。6.1 功能验收清单按角色把关键路径走一遍每条都要有明确的预期结果不能靠「大概能用」验收项操作路径预期结果判定未登录拦截直接访问 /score/list跳转登录页通过/不通过登录失败输入错误密码停留登录页并提示通过/不通过角色隔离学生账号访问成绩录入页拒绝并提示无权限通过/不通过重复录入同一学生同一课程同一学期提交两次覆盖而非报错通过/不通过分数校验录入 101 分后端拒绝并提示通过/不通过分页边界翻到最后一页再点下一页按钮置灰或停在末页通过/不通过空数据图表查询无成绩的课程显示「暂无数据」而非空白通过/不通过中文完整性录入含中文的姓名后查询前后端显示一致通过/不通过清单里最容易被跳过的是「重复录入」和「分页边界」。前者涉及唯一索引和ON DUPLICATE KEY后者涉及偏移量计算都是出问题概率最高的地方。6.2 答辩演示动线八分钟讲完一套系统演示最忌讳从登录页开始一步步点讲到第五分钟还在录数据。按这条线走八分钟能覆盖所有亮点第 1 分钟讲架构图。三层结构、六个模块、技术栈一句话带过。重点说清楚「为什么这么分层」而不是「用了什么技术」。第 2 到 3 分钟用管理员登录展示成绩列表的多条件筛选和分页。故意选一个组合条件让评委看到 SQL 拼接的效果。第 4 到 5 分钟切到教师角色批量录入一门课的成绩当场提交然后回到列表验证数据已更新。这一步要提前准备好数据别现场手打。第 6 到 7 分钟打开统计页切换课程看图表联动点柱子看下钻明细。这是整个演示的高潮务必放在数据已经准备好的状态下。第 8 分钟说一条自己踩过的坑和怎么解决的。选真实发生过的比如「分页翻页时记录重复」或者「图表切换后数据叠加」讲清楚现象、定位过程、最终改了什么。这一段比任何功能演示都有说服力因为它证明这套系统是你自己写出来的。6.3 现场被追问的三个问题怎么答「为什么不用 Spring」——答清楚适用场景这个项目的规模不需要 IoC 容器和 AOPServlet 直接 new Service 已经足够清晰少一层抽象反而好调试。如果被追问「那你会不会」就说说如果换成 Spring 之后DAO 层会怎么改。「数据量大了怎么办」——答索引和分页。前面建的idx_course_term就是为这个准备的统计查询走覆盖索引列表查询强制分页不一次性捞全表。「统计口径怎么定的」——这是最容易出彩的一题。把缺考不计入平均分、及格率分母包含缺考等几个决策说清楚说明你是想过业务而不是照着模板抄的。从那以后我每次交课程设计前都会把这份清单从头到尾自己走一遍包括故意输入错误数据、故意点重复提交、故意断网看错误提示——翻车的成本在演示前是零在答辩现场是全部。这套工程的完整代码、建表脚本和 ECharts 配置我都整理好了需要的可以拿去对照着改。希望帮到你。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
JavaWeb学生成绩管理系统:三层架构与ECharts统计实战 简介:这是一套面向 JavaWeb 入门与课程设计场景的学生成绩管理系统源码,基于 ServletJSPMySQL 分层实现,适合计算机专业学生完成课设、毕设,也适合自学者练习 Servlet 与 JDBC 的完整业务链路。升级版在原有增删改查基础上补充了 … · 2026/11/2 15:36:09
Python版DenseFusion六自由度位姿估计:从RGB-D到抓取落地 简介:这份Python-DenseFusion 6D物体姿态估计项目源码包,面向计算机视觉、机器人抓取与AR/VR方向的学习者和开发者,用于理解和复现基于RGB-D图像的端到端6D位姿估计流程。压缩包共55个文件,约3.51MB,以20个Python源码为… · 2026/11/2 15:36:09
人脸识别工程流水线:YOLOv5检测、ArcFace对齐比对与活体检测 简介:这是一份围绕人脸识别全流程搭建的学习实践包,面向具备一定 Python 与深度学习基础、希望把课堂理论落到可运行项目的开发者与学生。内容以 YoloV5 完成人脸检测,结合 ArcFace 的角度间隔损失提取高区分度特征,并接入活体检测… · 2026/11/2 15:36:09
HeidiSQL数据迁移优化与实战技巧 1. HeidiSQL数据迁移实战指南作为一款老牌MySQL管理工具,HeidiSQL在数据导入导出场景中始终保持着独特的优势。我至今记得第一次用HeidiSQL将5GB的订单数据从测试环境迁移到生产环境时,那种丝滑流畅的操作体验——相比命令行工具更直观,比同类… · 2026/11/3 4:59:53
数据清洗技术:原理、实践与工程化解决方案 1. 数据清洗为何成为大数据处理的生死线三年前我接手过一个电商用户行为分析项目,团队花费两周跑出的报表显示"广东用户凌晨3点下单率最高",差点让市场部启动"夜猫子专享促销"。直到复查原始日志才发现,是爬虫程序在深圳… · 2026/11/3 4:59:53
2026城市NOA实测:智能驾驶技术对比与优化方向 1. 项目背景与核心价值2026年主流车企城市NOA(Navigate on Autopilot)试驾报告,是对智能驾驶技术在实际道路场景中表现的一次系统性检验。这份报告的价值在于,它首次将不同品牌的城市领航辅助驾驶功能放在同一测试标准下进行横向对… · 2026/11/3 4:59:53
MyBatis批量插入性能优化实战与原理剖析 1. 为什么MyBatis批量插入需要优化在数据量较大的业务场景中,批量插入操作是数据库性能优化的关键点。很多开发者习惯使用MyBatis的foreach标签来实现批量插入,这其实是一个典型的性能陷阱。我曾在电商平台的订单系统中,因为这个问题导致高峰… · 2026/11/3 4:59:53
从零开始学AI工程:数据到部署的完整闭环 如果你也想走“从零开始”学AI工程这条路,先别急着收藏一堆论文。ai-engineering-from-scratch这个词我盯了很久,它看起来像一个开源仓库名,实际上更像一种学习思路:不靠调用现成服务“假装会了”,而是把数据、训练、部… · 2026/11/3 4:59:53
高校学工一体化平台架构设计与智能应用实践 1. 学工一体化平台的核心定位与价值学工一体化平台是高校学生工作数字化转型的核心载体,它通过整合教务、学籍、资助、心理、就业等十余个业务模块,实现了学生从入学到毕业的全生命周期管理。这种系统不同于传统的教务管理系统,其核心差异在于… · 2026/11/3 4:58:53
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18