简介面向JavaWeb课程设计、毕业设计入门及二次开发学习者的一套学生成绩管理系统源码包技术栈为ServletjspMySQL在原版基础上加入ECharts统计图表可用于成绩录入、查询、修改及可视化分析等典型教学场景。压缩包共344个文件约3.59MB含86个java源文件与86个class文件、37个jsp页面、62个js脚本、28个css样式另有6个jar依赖、1个sql数据库脚本及图片字体等静态资源覆盖后端分层、前端交互与建库部署所需。项目按Dao、Service、Servlet等层次组织便于理解用户、学生、教师、课程与成绩模块的调用关系也能参考文件上传下载等实现方式对课程作业、功能演示和二次开发具有实际参考价值。目前已有6724人学习下载适合需要完整案例对照练习或在此基础上扩展统计功能的读者。1. 学生成绩管理系统从一张 Excel 汇总表说起的 JavaWeb 实战项目学期末最典型的场景是教务老师手里有三四个版本的 Excel 成绩表班主任手里有另一份任课老师又交来一份格式不同的最后谁也不敢说哪份是最新的。这个基于 Servlet JSP MySQL 的学生成绩管理系统解决的就是这类手工汇总问题——把学生、课程、成绩、班级、用户五个维度落到数据库里登录后按角色分配菜单成绩录入、条件查询、分页展示、及格率统计一路走完最后用 ECharts 把平均分、分数段分布、班级对比画成图。它不是什么企业级中台而是一个结构完整、代码量可控的 JavaWeb 单体应用适合正在做课程设计、毕业设计或者想从零把 MVC 分层跑通的同学。技术栈很朴素Servlet 做控制器JSP 做视图JDBC 直连 MySQL前端 JQuery 发请求ECharts 负责统计图渲染。下面按「项目长什么样 → 怎么跑起来 → 核心链路怎么写的 → 图表模块怎么接的 → 踩过哪些坑 → 怎么二次开发」的顺序拆开讲。2. 技术选型与工程结构一个能直接跑起来的 Maven Web 工程2.1 Servlet 与 JSP 的分工边界谁该干什么这个项目用的是最经典的 Model2 模式也就是 Servlet 只负责收参数、调业务、转发视图JSP 只负责把数据渲染成 HTML两者之间用 request 域或者 Session 传值。很多同学写 JavaWeb 容易写成「Servlet 里 out.print 拼 HTML」那样改一次页面就要动 Java 代码维护成本极高。这个项目的边界划得比较清楚所有/score/*、/student/*的请求都走 ServletServlet 处理完之后request.getRequestDispatcher(/WEB-INF/views/score/list.jsp).forward(request, response)页面放在 WEB-INF 下浏览器直接访问不到只能由 Servlet 转发顺带就挡住了绕过登录直接敲 URL 的情况。为什么不直接用 JSP 里写jsp:useBean或者脚本片段查数据库因为那样 SQL 会散落在几十个 JSP 里改一张表结构要全站搜索。分层之后DAO 层只有一处写 SQLServlet 只有一处做参数校验出问题的时候排查路径是明确的。这也是我一般建议新手先照着抄一遍的原因——先体会「改动一个字段要碰几个文件」再谈框架。2.2 目录结构Maven 标准 Web 工程长什么样项目按 Maven 的src/main/java、src/main/webapp、src/main/resources三件套组织下面是实际落地的目录树照着建就不会出现「类编译了但 Tomcat 找不到」的问题student-score/ ├── pom.xml └── src/main/ ├── java/com/example/score/ │ ├── controller/ # Servlet 控制器 │ │ ├── LoginServlet.java │ │ ├── StudentServlet.java │ │ ├── ScoreServlet.java │ │ └── ChartServlet.java │ ├── service/ # 业务逻辑 │ │ ├── ScoreService.java │ │ └── ChartService.java │ ├── dao/ # JDBC 数据访问 │ │ ├── BaseDao.java │ │ ├── StudentDao.java │ │ └── ScoreDao.java │ ├── entity/ # 实体类字段与表一一对应 │ │ ├── Student.java │ │ ├── Course.java │ │ └── Score.java │ ├── filter/ # 登录拦截、编码过滤 │ │ ├── LoginFilter.java │ │ └── EncodingFilter.java │ └── util/ # 连接池、JSON 工具 │ ├── DBUtil.java │ └── JsonUtil.java ├── resources/ │ └── db.properties # 数据库连接参数 └── webapp/ ├── WEB-INF/ │ ├── web.xml │ └── views/ # JSP 视图外部不可直接访问 │ ├── login.jsp │ ├── score/list.jsp │ └── chart/index.jsp ├── static/ │ ├── js/echarts.min.js │ ├── js/jquery.min.js │ └── css/style.css └── index.jsp # 入口重定向到登录这里有个细节值得说static目录放前端资源WEB-INF/views放 JSP。前者可以被浏览器直连后者不行。这样划分之后静态资源交给 Tomcat 默认 Servlet 处理JSP 只做内部转发权限控制就集中在一个 Filter 里不用在每个页面顶部写「判断 Session 是否存在」。2.3 依赖与配置pom.xml 与 db.properties依赖控制在四个以内避免新手被依赖冲突拖住Servlet APIprovided因为 Tomcat 自带、JSTL页面循环和格式化、MySQL 驱动、JSON 库手写拼 JSON 也行但用库更稳。连接池用 Druid 或者 HikariCP项目里常见做法是 Druid配置简单监控页面也方便排查连接泄漏。dependencies !-- Servlet API 由 Tomcat 提供scope 必须是 provided否则打包进 war 会和容器冲突 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- JSTL 用于 JSP 中的 c:forEach、fmt:formatNumber -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- MySQL 8 驱动注意驱动类名是 com.mysql.cj.jdbc.Driver -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- Druid 连接池负责连接的复用与回收 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency /dependencies参数说明scopeprovided表示编译期可见、运行期由容器提供这是 Servlet 项目最常见的翻车点之一MySQL 8 的useSSL、serverTimezone两个参数不写会在启动时抛时区异常。数据库配置单独放db.properties由DBUtil静态块加载避免把账号密码写死在 Java 代码里jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/score_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.password123456useUnicodetruecharacterEncodingutf8这两个参数决定了写入的中文姓名会不会变成问号serverTimezoneAsia/Shanghai解决 8 小时时差useSSLfalse是本地开发常用写法生产环境应当开启。配置文件放在src/main/resources下编译后自动进入WEB-INF/classesDBUtil用getResourceAsStream(/db.properties)就能读到不用关心部署路径。3. 数据库与成绩业务链路五张表撑起整个系统3.1 表结构设计用户、学生、课程、成绩、班级成绩管理看起来简单但表设计一旦做歪后面统计图基本没法写。核心是五张表sys_user存登录账号和角色student存学号姓名班级course存课程名和学分score是中间表存学生、课程、分数clazz存班级名。关键是score表上要建(student_id, course_id)联合唯一索引防止同一门课被录两次。-- 用户表role 区分管理员、教师、学生三种角色 CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role TINYINT NOT NULL DEFAULT 3 COMMENT 1管理员 2教师 3学生, ref_id INT COMMENT 学生角色时关联 student.id ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 学生表class_id 关联班级后续按班级分组统计 CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, sno VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, gender TINYINT DEFAULT 1, class_id INT NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 成绩表联合唯一索引防止重复录入 CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2) NOT NULL DEFAULT 0, term VARCHAR(20) NOT NULL COMMENT 学期如 2024-2025-1, UNIQUE KEY uk_stu_course_term (student_id, course_id, term), KEY idx_course (course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;参数说明DECIMAL(5,2)比FLOAT更适合存分数避免 59.999999 这种比较尴尬的误差term字段冗余在成绩表里而不是另建学期表是为了让分页查询少一次 JOIN代价是学期名称变更时要批量更新属于这个体量项目可以接受的取舍。idx_course索引是给统计查询用的按课程聚合平均分时会走这个索引。3.2 连接池与基类DBUtil 和 BaseDao 怎么写JDBC 裸写最容易出两个问题连接忘了关、SQL 拼接被注入。项目用 Druid 做连接池DBUtil只负责返回DataSource所有Connection的关闭交给BaseDao里的模板方法统一处理子类只关心 SQL 和参数。public class DBUtil { private static DataSource ds; static { Properties p new Properties(); try (InputStream in DBUtil.class.getResourceAsStream(/db.properties)) { p.load(in); DruidDataSource dataSource new DruidDataSource(); dataSource.setDriverClassName(p.getProperty(jdbc.driver)); dataSource.setUrl(p.getProperty(jdbc.url)); dataSource.setUsername(p.getProperty(jdbc.username)); dataSource.setPassword(p.getProperty(jdbc.password)); dataSource.setInitialSize(5); // 初始化连接数 dataSource.setMaxActive(20); // 最大连接数超出会等待 dataSource.setMaxWait(3000); // 等待超时 3 秒避免线程长时间挂起 ds dataSource; } catch (Exception e) { throw new ExceptionInInitializerError(数据库连接池初始化失败: e.getMessage()); } } public static Connection getConnection() throws SQLException { return ds.getConnection(); } }BaseDao提供两个泛型方法queryList和update。前者接收 SQL、参数数组和一个RowMapper回调后者返回受影响行数。这样ScoreDao里只剩下 SQL 语句和字段映射没有一行try-catch-finally。public abstract class BaseDao { // 查询多行mapper 负责把 ResultSet 当前行转成实体 protected T ListT queryList(String sql, RowMapperT mapper, Object... params) { ListT list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); // 下标从 1 开始别写错 } try (ResultSet rs ps.executeQuery()) { while (rs.next()) { list.add(mapper.map(rs)); } } } catch (SQLException e) { throw new RuntimeException(查询失败: sql, e); } return list; } // 增删改统用返回影响行数 protected int update(String sql, Object... params) { try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(更新失败: sql, e); } } public interface RowMapperT { T map(ResultSet rs) throws SQLException; } }参数说明setObject的下标从 1 开始这是 JDBC 规范写 0 会直接抛异常try-with-resources保证Connection、PreparedStatement、ResultSet三层都能关闭连接池里的连接会被归还而不是真正销毁RowMapper用函数式接口调用方可以直接传 Lambda省掉一堆匿名内部类。这套模板一旦写好后面加十张表都只写 SQL。3.3 登录与权限过滤Session Filter 的组合登录流程是LoginServlet校验账号密码成功后把userId、role、realName塞进 Session然后重定向到首页。LoginFilter拦截所有/score/*、/student/*、/chart/*请求检查 Session 里有没有用户没有就跳回登录页。这里要注意 Filter 的放行名单登录页、登录接口、静态资源必须排除否则会陷入重定向死循环。WebFilter(urlPatterns {/score/*, /student/*, /chart/*}) public class LoginFilter implements Filter { // 无需登录即可访问的路径前缀 private static final ListString WHITE_LIST Arrays.asList( /login, /static/, /api/login ); 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()); for (String prefix : WHITE_LIST) { if (uri.startsWith(prefix)) { chain.doFilter(req, resp); return; } } Object user request.getSession().getAttribute(loginUser); if (user null) { // 区分 AJAX 请求和页面请求AJAX 返回 401 让前端跳转 if (XMLHttpRequest.equals(request.getHeader(X-Requested-With))) { response.setStatus(401); } else { response.sendRedirect(request.getContextPath() /login); } return; } chain.doFilter(req, resp); } }参数说明urlPatterns用通配符匹配一整组路径比在 web.xml 里逐个配置清晰X-Requested-With是 JQuery 的$.ajax默认带上的请求头用它区分 AJAX 和整页跳转避免 AJAX 请求收到一整段 HTML 登录页导致前端解析报错。编码过滤EncodingFilter要排在登录过滤之前统一request.setCharacterEncoding(UTF-8)否则 POST 表单里的中文姓名会在 Filter 阶段就已经乱码后面再转都来不及。3.4 条件分页查询一条 SQL 同时解决筛选和翻页成绩列表页支持按学号、姓名、课程、学期筛选还要分页。新手容易写成「先查全量再在内存里 subList」数据量一大就完蛋。正确做法是两次查询一次COUNT(*)拿总数一次带LIMIT拿当前页数据两个查询共用同一段WHERE条件。public PageResultScoreVO pageQuery(ScoreQuery query, int pageNo, int pageSize) { StringBuilder where new StringBuilder( WHERE 11 ); ListObject params new ArrayList(); if (StringUtils.isNotBlank(query.getSno())) { where.append( AND s.sno LIKE ? ); params.add(% query.getSno() %); } if (StringUtils.isNotBlank(query.getCourseName())) { where.append( AND c.name LIKE ? ); params.add(% query.getCourseName() %); } if (StringUtils.isNotBlank(query.getTerm())) { where.append( AND sc.term ? ); params.add(query.getTerm()); } String from FROM score sc JOIN student s ON sc.student_id s.id JOIN course c ON sc.course_id c.id ; // 第一条总数用于算总页数 long total queryCount(SELECT COUNT(*) from where, params.toArray()); // 第二条当前页数据ORDER BY 保证分页稳定 String dataSql SELECT s.sno, s.name, c.name AS courseName, sc.score, sc.term from where ORDER BY s.sno, c.id LIMIT ?, ?; params.add((pageNo - 1) * pageSize); params.add(pageSize); ListScoreVO rows queryList(dataSql, rs - { ScoreVO vo new ScoreVO(); vo.setSno(rs.getString(sno)); vo.setName(rs.getString(name)); vo.setCourseName(rs.getString(courseName)); vo.setScore(rs.getBigDecimal(score)); vo.setTerm(rs.getString(term)); return vo; }, params.toArray()); return new PageResult(total, pageNo, pageSize, rows); }逻辑说明WHERE 11是为了后面统一用AND拼接不用判断「是不是第一个条件」所有用户输入都走?占位符彻底杜绝 SQL 注入这个项目里没有任何一处字符串拼接 SQL 值ORDER BY必须加否则 MySQL 的LIMIT在无排序时返回顺序不稳定会出现「第二页出现第一页的数据」这种玄学问题PageResult里同时算好totalPagesJSP 只负责循环页码按钮。4. ECharts 统计图模块后端出 JSON前端只管渲染4.1 图表数据接口把聚合 SQL 转成 ECharts 能吃的结构新增的统计图模块是这个项目最有价值的部分因为它把「数据库聚合」和「前端可视化」串起来了。核心思路是ChartServlet接收type参数courseAvg / classPass / scoreRange调用ChartService执行对应的聚合 SQL把结果拼成 JSON 返回JSP 里用$.getJSON拿到数据后setOption。后端不生成任何 HTML只输出数据这样将来换成 Vue 或者小程序接口可以原样复用。WebServlet(/chart/data) public class ChartServlet extends HttpServlet { private final ChartService chartService new ChartService(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setContentType(application/json;charsetUTF-8); String type req.getParameter(type); String term req.getParameter(term); // 可选按学期过滤 MapString, Object result new HashMap(); try { switch (type null ? : type) { case courseAvg: result chartService.courseAvg(term); break; case classPass: result chartService.classPassRate(term); break; case scoreRange: result chartService.scoreDistribution(term); break; default: result.put(msg, 未知的图表类型); resp.setStatus(400); } result.put(code, 200); } catch (Exception e) { resp.setStatus(500); result.put(code, 500); result.put(msg, 统计查询失败); } resp.getWriter().write(JsonUtil.toJson(result)); } }参数说明type决定走哪条聚合逻辑term是可选过滤条件为空则统计全部学期resp.setContentType(application/json;charsetUTF-8)必须显式设置否则浏览器会按 HTML 解析前端$.getJSON直接进 error 回调异常时返回 500 和code字段前端可以统一提示而不是把堆栈打到页面上。4.2 聚合 SQL三种图表背后的三条查询课程平均分图需要「每门课的平均分」班级及格率图需要「每个班的及格人数除以总人数」分数段分布图需要把分数按区间分组。三条 SQL 都不复杂但都有坑平均分要ROUND保留两位及格率要防止除零分数段用CASE WHEN分组后要补全没有数据的区间。public MapString, Object courseAvg(String term) { String sql SELECT c.name AS courseName, ROUND(AVG(sc.score), 2) AS avgScore FROM score sc JOIN course c ON sc.course_id c.id (StringUtils.isNotBlank(term) ? WHERE sc.term ? : ) GROUP BY c.id, c.name ORDER BY avgScore DESC; ListMapString, Object rows queryForMapList(sql, term); return buildBarData(rows, courseName, avgScore); } public MapString, Object scoreDistribution(String term) { // 分五档统计CASE WHEN 的顺序必须从高到低否则 90 会先命中 60 那档 String 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 ELSE 0-59 END AS rangeName, COUNT(*) AS cnt FROM score sc (StringUtils.isNotBlank(term) ? WHERE sc.term ? : ) GROUP BY rangeName; ListMapString, Object rows queryForMapList(sql, term); return buildBarData(rows, rangeName, cnt); }参数说明GROUP BY c.id, c.name同时带上 id是因为课程名可能重复只按 name 分组会合并两门同名课CASE WHEN从上到下匹配 90必须写在最前面写反了所有 90 分以上都会落进 60那一档queryForMapList是BaseDao里另加的一个方法把ResultSet转成ListMap专门给图表用省去为每条统计 SQL 建实体类。4.3 前端渲染JSP 里挂载容器与异步加载JSP 页面本身很薄只放三个div容器初始化交给 ECharts。关键是echarts.init必须在容器有宽高之后调用并且窗口resize时要调chart.resize()否则侧边栏收起或窗口缩放后图表会变形。// chart/index.jsp 中的脚本片段 var courseChart echarts.init(document.getElementById(courseAvg)); function loadBar(url, chart, title) { $.getJSON(ctx url, function (res) { if (res.code ! 200) { alert(res.msg); return; } chart.setOption({ title: { text: title, left: center }, tooltip: { trigger: axis }, grid: { left: 3%, right: 4%, bottom: 3%, containLabel: true }, xAxis: { type: category, data: res.xAxis, axisLabel: { interval: 0, rotate: 20 } // 课程名长时倾斜避免重叠 }, yAxis: { type: value }, series: [{ type: bar, data: res.series, label: { show: true, position: top } }] }); }); } // 页面加载完成后再初始化保证容器已有尺寸 $(function () { loadBar(/chart/data?typecourseAvg, courseChart, 各课程平均分); loadBar(/chart/data?typescoreRange, scoreChart, 分数段分布); window.onresize function () { courseChart.resize(); scoreChart.resize(); }; });逻辑说明ctx是 JSP 里通过${pageContext.request.contextPath}注入的上下文路径直接写/chart/data在非根路径部署时会 404axisLabel.interval: 0强制显示所有类目名课程数量少的时候必须加rotate: 20是血泪经验课程名一长横轴标签就会叠成一团黑。这套「后端返回xAxisseries两个数组」的约定让三种图表共用同一个loadBar函数只有饼图需要单独写一个loadPie因为它的series.data是{name, value}对象数组。5. 踩坑排查五类高频故障的现象、原因与处置5.1 中文全部变成问号或者乱码现象录入的学生姓名在列表页显示正常但写入数据库后用客户端查是???或者反过来页面显示乱码。原因通常有三层JSP 页面没写% page contentTypetext/html;charsetUTF-8 %、POST 请求没设setCharacterEncoding(UTF-8)、数据库连接串缺characterEncodingutf8。解决顺序是先查连接串再查EncodingFilter是否在所有 Filter 之前执行最后确认表字符集是utf8mb4而不是默认的latin1。三步都对了还乱码基本就是 Tomcat 的server.xml里 Connector 没配URIEncoding不过新版本 Tomcat 默认就是 UTF-8一般不用动。5.2 连接池报「wait millis 3000, active 20」然后整站卡死现象系统跑一段时间后所有查询都超时日志里反复出现连接池等待超时。原因只有一个——某处Connection没有关闭。BaseDao用了 try-with-resources 不会漏但手动写的方法、或者ChartService里临时加的查询很容易忘记。解决方式是打开 Druid 的监控页面/druid/index.html看ActiveCount是不是只增不减再在DBUtil里加removeAbandonedtrueremoveAbandonedTimeout60让池子强制回收超过 60 秒未关闭的连接先把线上稳住再回去补close。5.3 ECharts 图表区域一片空白控制台无报错现象接口返回了正确的 JSON但页面上什么都不显示。原因通常不是 ECharts 本身而是容器没有高度。div idcourseAvg/div这种写法div 的高度是 0echarts.init虽然不报错但画布高度为 0自然什么都看不到。解决方式是给容器写死高度stylewidth:100%;height:400px;或者用 CSS 类统一设置。第二个常见原因是echarts.init在$(function(){})之外执行DOM 还没加载完拿到的是 null这种会报错反而好排查。5.4 分页点第二页出现重复数据或数据缺失现象第一页 10 条第二页还是那 10 条或者中间少了几条。原因是没有ORDER BY的LIMIT在 MySQL 里顺序不稳定尤其是带 JOIN 的查询。解决方式是给dataSql加一个唯一性足够的排序字段组合比如ORDER BY s.sno, c.id如果业务上要求按分数排序就用ORDER BY sc.score DESC, sc.id ASC把主键作为最后的兜底排序保证任何时候顺序都是确定的。5.5 本地跑得好好的部署到服务器后静态资源 404现象页面样式全丢ECharts 不加载F12 看是/static/js/echarts.min.js返回 404。原因通常是 war 包部署在非根路径比如/score而页面里写的是绝对路径/static/...。解决方式是把所有静态资源引用改成${pageContext.request.contextPath}/static/...或者在 JSP 顶部用c:set varctx value${pageContext.request.contextPath}/统一变量。另外检查web.xml里 DispatcherServlet 的映射是不是url-pattern写成了/把静态资源也拦截了这个项目没用 SpringMVC一般不会撞上但二次开发引入框架后很容易踩。6. 二次开发改造从「能跑」到「能改」的几个切入点拿到这个项目之后改字段是最常见的需求比如给student加一个「联系电话」。改动路径是先ALTER TABLE加列再在Student实体里加属性然后改StudentDao的INSERT、UPDATE、SELECT三处 SQL 和RowMapper最后在 JSP 表单里加一个input。五处漏一处就会出现「填了保存不上」或者「列表不显示」的问题我一般的做法是先把 DAO 改完用单元测试直连数据库验证一遍再动 JSP避免前后端同时改导致定位困难。第二个改造点是角色权限。现在LoginFilter只判断「有没有登录」如果要限制「只有教师能录成绩」就在 Filter 里读 Session 中的role对/score/save这类写操作路径做角色校验或者在ScoreServlet的doPost里判断。更彻底的做法是加一张role_permission表把路径和角色做配置化但项目体量不大时没必要硬编码一个if (role ! 2) { resp.sendError(403); return; }就够了代码可读性反而更好。第三个改造点是导出 Excel。这是成绩管理系统绕不开的需求思路是在ScoreServlet加一个actionexport分支用 POI 的XSSFWorkbook建表头、循环PageResult里的数据写行、response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet)、Content-Disposition设文件名最后workbook.write(response.getOutputStream())。注意文件名要做 URL 编码中文名在部分浏览器会乱码还要注意导出不能走分页得单独写一个不分页的查询否则只能导出当前页 10 条。最后一个技巧是关于 ECharts 的新增图表时不要在后端为每种图写一个 Servlet 方法而是把「数据组装」和「图表配置」分开。后端统一返回{xAxis: [], series: []}这种通用结构前端用一个renderBar(domId, url, title)函数包住。这样加一个「教师工作量统计」只需要在ChartService里加一条 SQL前端加一行调用十分钟就能上线。从那以后我每次接这类课程设计项目都强制自己先跑通「登录 → 列表 → 新增 → 图表」这一条最小链路再往上堆功能因为这条链路一旦断了后面加再多模块都是空中楼阁。希望这个拆解能帮你少走几段弯路先把项目跑起来再挑一处动手改比通读十遍代码都管用希望帮到你。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
Python学校教务系统骨架:选课并发与事务实战 简介:这是一套面向Python Web开发初学者及课程设计、毕业设计场景的学校教务系统实践包,围绕学生信息、课程安排、成绩录入与查询等日常教务流程的自动化处理展开,可用于练习后端业务逻辑、页面交互与数据库设计的完整串联。压缩包共9个文件&… · 2026/11/6 0:44:24
基于YOLOv8的多端车流检测系统:检测跟踪与越线计数落地 简介:基于YOLOv8的多端车流检测系统源码包,面向做毕业设计、课程项目或目标检测入门实践的高校学生与开发者,围绕车辆实时检测、车流统计与多端部署场景,提供从模型训练到界面展示的完整工程参考。压缩包共396个文件,约… · 2026/11/6 2:45:33
CentOS 7离线安装stress:rpm依赖解析与压测验证 简介:面向需要在无外网或内网CentOS环境中开展性能压测的运维、测试与Linux学习者,这份资料围绕stress-1.0.4的离线部署整理,重点解决压测工具及编译依赖难以在线获取的问题。压缩包共43个文件,约46.66MB,以11个rpm安装… · 2026/11/5 3:21:13
淅川哪里可以考电工证官方报名入口在哪里找 淅川哪里可以考电工证官方报名入口在哪里找 怕考不过白交培训费?别急,教你一招搞定【淅川哪里可以考电工证】官方报名入口,少走弯路!… · 2026/11/7 12:53:45
特种作业电焊操作证怎么考?多少钱?外省证能不能转过来? 特种作业电焊操作证怎么考?多少钱?外省证能不能转过来? 之前考的证在外省能不能转过来?这个问题,很多小伙伴在问。特别是像你这样已经在外地打拼,但老家考了【特种作业电焊操作证】的人,肯定最关心能不能转回来用,还有就是 多少钱… · 2026/11/7 12:53:45
三门峡电工证怎么考?提升机电工工作业绩关键看这几点 三门峡电工证怎么考?提升机电工工作业绩关键看这几点 你是不是正在为找不到工作发愁?是不是听说现在电工证是门槛?别急,咱们今天就来聊聊怎么搞定 提升机电工工作业绩 ,顺便说说怎么在 网上查询 自己的证书状态,让你快点拿到证,早点上岗。… · 2026/11/7 12:53:45
兴业电工证怎么考值不值得考?电工证在手,机会多一半 兴业电工证怎么考值不值得考?电工证在手,机会多一半 急需特种作业证才能上岗找工作,你是不是也正为这事头疼?电工证不仅是门槛,更是机会。但兴业电工证怎么考?值不值得考?这些问题成了很多人的纠结点。别急,这篇文章就从实战角度出发,告诉你电工证到… · 2026/11/7 12:53:45
证书过期了不知道怎么复审延期?简述电工作业安全规程在哪考全攻略 证书过期了不知道怎么复审延期?简述电工作业安全规程在哪考全攻略 证书过期了不知道怎么复审延期?你不是一个人!最近不少电工朋友都来问我,说自己证书快到期了,不知道在哪考复审,更不知道怎么操作。别急,这篇文章就从【简述电工作业安全规程】出发,告… · 2026/11/7 12:53:45
双极步进电机控制实战: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/7 11:25:08
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/7 12:02:18
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/6 10:29:23
澳洲水电工证考试:证书过期了不知道怎么复审延期?电子证书怎么弄? 澳洲水电工证考试:证书过期了不知道怎么复审延期?电子证书怎么弄? 证书过期了不知道怎么复审延期?别慌,很多人在拿到【澳洲水电工证考试】的电子证书后,一不小心就忽略了复审时间,结果证书失效,工作受阻,甚至被单位追责。今天就从【澳洲水电工证考试… · 2026/11/7 0:00:41
电工作业提示版通过率揭秘:怎么考才能一次过? 电工作业提示版通过率揭秘:怎么考才能一次过? 怕考不过白交培训费?别急,今天我就从驻马店应届工程类毕业生的视角,带你全面了解电工作业提示版的考试流程、备考要点和真实通过率,帮你少走弯路、提高一次通过率。… · 2026/11/7 0:00:41
电工证年审报名千万别踩坑 外省证转过来要搞清这几点 电工证年审报名千万别踩坑 外省证转过来要搞清这几点 之前考的证在外省能不能转过来?这个问题是很多电工从业者最关心的,特别是像你这样在商丘从事公路工程的人员,经常需要跨省流动,证书的 有效性 和 可迁移性… · 2026/11/7 0:00:41
双极步进电机控制实战: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/7 11:25:08
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/7 12:02:18
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/6 10:29:23