ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Servlet+JSP+MySQL学生成绩管理系统与ECharts统计实践

Servlet+JSP+MySQL学生成绩管理系统与ECharts统计实践

简介:面向JavaWeb课程设计、毕业实践与后端入门进阶的学习者,这是一套基于Servlet+JSP+MySQL的学生成绩管理系统完整源码,并在原项目基础上新增ECharts统计图,可用于成绩管理类项目的搭建、改造与答辩演示。压缩包共344个文件,约3.59MB,以Java源文件与编译类为主,配合JSP页面、JavaScript脚本、CSS样式、JAR依赖及SQL建库脚本,较完整地呈现了Servlet控制层、DAO数据访问层、Service业务层与JSP视图层的组织方式。目前已有6724人学习,说明其作为教学案例具有较高的参考价值。读者可据此理解学生、教师、课程、成绩与用户等模块的数据流转,参考文件上传下载、分页查询和ECharts图表接入的实现思路,并借助数据库脚本快速还原运行环境;分层目录也便于按功能定位代码、进行二次开发或撰写设计文档。

1. 从一张成绩单说起:这套系统要解决什么

每年带课设,总有人拿着题目来问,JavaWeb 学生成绩管理系统做成什么样才算完整。我一般先反问,不看框架文档,你能不能把一次成绩录入从浏览器提交到数据库落表、中间经过哪几个类说清楚。这道题筛掉一大半人。Servlet + JSP + MySQL 这套组合看上去老派,好处是请求链路、会话、SQL 全摊在明面上,正好用来补这块底子,答辩时也不至于只会说“框架帮我做了”。整套方案要解决三件事:学生、课程、成绩三类数据的增删改查;按班级和课程维度算及格率、平均分、分数段分布;用 ECharts 把统计结果画成能放进答辩 PPT 的图。适合正在做 JavaWeb 课设或毕设、需要一套能跑通又能讲清楚的作品的人。

2. 总体架构:三层结构与六个包的职责边界

2.1 分层不是形式主义,是给答辩留解释空间

课设里最常见的一种写法,是把 JDBC 代码直接写进 JSP 的<% %>里,页面顶上 import 一堆 java.sql,下面一边查一边拼表格。这种项目能跑,但改一个字段要全局搜索,加一个统计要复制三份 SQL,答辩老师追问一句“你的业务逻辑写在哪”,基本就卡住了。分层的价值不在代码好看,而在于每个类只有一个改的理由:Servlet 负责收参数、调服务、转发页面;Service 负责成绩口径、事务边界;Dao 只负责 SQL 和结果集映射;JSP 只负责展示。

我一般按六个包来切,职责边界写死在目录里:

层次典型包名职责不该做的事
控制层com.sms.servlet接收请求、参数校验、页面跳转不写 SQL、不算总评
服务层com.sms.service成绩口径、事务、业务校验不碰 request/response
数据层com.sms.daoSQL、结果集映射不判断权限
实体层com.sms.entity与表对应的 POJO、VO不写业务方法
工具层com.sms.util数据库连接、JSON 输出、日期处理不依赖上层
过滤层com.sms.filter编码、登录校验不做业务判断

这样切的好处很直接:统计图要加一个维度,只动 Dao 和 Service;页面换皮,只动 JSP;老师问“权限在哪做的”,直接指 Filter。代价是类变多,一个成绩查询要走四五个文件,所以目录命名要一眼能看懂,别用Test1、Util2这种名字给自己挖坑。

2.2 技术栈选型:Servlet + JSP + JDBC + ECharts 各自的角色

题目限定了 Servlet + JSP + MySQL,这里不讨论换框架,只讨论每一层用什么、版本怎么定。JDK 8 起步,Tomcat 8 以上,MySQL 5.7 以上都能跑,Servlet 规范用 3.x 的注解方式注册,省掉 web.xml 里成片的<servlet-mapping>。JDBC 直接用原生驱动加一个DBUtil工具类,不引连接池也能交作业,但如果想让演示更稳,建议加一个轻量连接池,避免每次请求都重新握手的开销。

ECharts 这一块要注意引入方式。很多项目在 JSP 里写一行 CDN 地址就完事,结果答辩教室网络一断,三张图全白,场面很难看。通常的做法是把echarts.min.js下载到webapp/static/js/下本地引用,页面里用${pageContext.request.contextPath}拼路径,换部署目录也不会 404。图表类型按统计目标选:分数段分布用柱状图,及格率用饼图,班级平均分对比用折线图或横向条形图,不要三张图都用饼图,视觉上分不出重点。

2.3 目录结构与一次请求的完整走向

下面这套目录结构是我带项目时反复用过的,JSP 全部放在WEB-INF下,浏览器不能直接访问,只能通过 Servlet 转发进入,登录校验才有意义:

sms/ ├── src/main/java/com/sms/ │ ├── entity/ # Student、Course、Score、ScoreVO │ ├── dao/ # StudentDao、ScoreDao、StatDao │ ├── service/ # ScoreService、StatService │ ├── servlet/ # LoginServlet、ScoreServlet、StatServlet │ ├── filter/ # EncodingFilter、AuthFilter │ └── util/ # DBUtil、JsonUtil ├── src/main/webapp/ │ ├── WEB-INF/jsp/ # list.jsp、edit.jsp、stat.jsp │ ├── static/js/ # echarts.min.js、stat.js │ └── login.jsp └── src/main/resources/ └── db.properties # 连接串、账号、密码

一次“按姓名查成绩”的请求走向是这样的:用户在list.jsp输入关键字提交,ScoreServlet拿到action=list和keyword,调ScoreService.list(),Service 做空值处理后再调ScoreDao.findByKeyword(),Dao 用PreparedStatement执行带LIKE的联表查询,结果集逐行映射成ScoreVO,一路返回给 Servlet,Servlet 把列表塞进request域,forward到list.jsp渲染。这条链路每个环节都能单独加日志,出问题看日志就知道断在哪一层,比在 JSP 里打断点强得多。

注意:JSP 放在WEB-INF下之后,forward的路径要写/WEB-INF/jsp/xxx.jsp,写成相对路径会在不同 Servlet 下解析出不同结果,这是新人常踩的坑。

3. 数据库与后端:五张表、一条请求链路和三个易错点

3.1 表结构设计:学生、课程、成绩怎么关联

成绩管理系统的数据模型不复杂,但字段类型和外键约束一旦定错,后面统计和删除都会跟着出问题。核心是五张表:班级、学生、教师、课程、成绩,再加一张登录用户表。学生和班级是多对一,课程和教师是多对一,成绩表是学生和课程的多对多中间表,额外挂上学期的字段,因为同一门课不同学期要分开统计。

CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, sno VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', name VARCHAR(50) NOT NULL, gender TINYINT DEFAULT 1 COMMENT '1男2女', class_id INT NOT NULL, INDEX idx_class (class_id), CONSTRAINT fk_student_class FOREIGN KEY (class_id) REFERENCES class(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, usual_score DECIMAL(5,2) DEFAULT 0 COMMENT '平时分', exam_score DECIMAL(5,2) DEFAULT 0 COMMENT '期末分', total_score DECIMAL(5,2) DEFAULT 0 COMMENT '总评', term VARCHAR(20) NOT NULL COMMENT '如2024-2025-1', UNIQUE KEY uk_stu_course_term (student_id, course_id, term), INDEX idx_course_term (course_id, term), CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

几个参数值得单独说明。DECIMAL(5,2)表示总共五位、两位小数,最大 999.99,够放百分制成绩,用它而不是FLOAT是因为浮点数做等值比较和求平均会出现59.999999这种结果,统计及格率时少数一两个人,排查起来很折磨。UNIQUE KEY卡住“同一学生同一课程同一学期只能有一条成绩”,防止重复录入。idx_course_term是给统计查询准备的,按课程和学期过滤时能走索引,不然数据量一上来图表要转好几秒。建库统一用utf8mb4,学生姓名里出现生僻字也不会变成问号。

3.2 成绩口径:总评到底在哪一层算

总评的计算规则必须在项目里写死一处,通常是“平时分占 30%、期末分占 70%,四舍五入保留两位”。我见过有项目在录入页面用 JavaScript 算一遍、在列表页用 SQL 再算一遍、在统计里又用 Java 算一遍,三处口径不一致,演示时同一个学生的总评出现三个数,老师一眼就看出来。正确做法是只在 Service 层算,录入和修改都走同一个方法:

public BigDecimal calcTotal(BigDecimal usual, BigDecimal exam) { if (usual == null) usual = BigDecimal.ZERO; if (exam == null) exam = BigDecimal.ZERO; // 平时30% + 期末70%,HALF_UP 四舍五入 return usual.multiply(new BigDecimal("0.3")) .add(exam.multiply(new BigDecimal("0.7"))) .setScale(2, RoundingMode.HALF_UP); }

total_score字段是冗余存储,目的是让统计 SQL 不用每次现算,查询和排序都更快。代价是修改权重后要批量重算历史数据,所以权重值建议放在配置文件里,而不是散落在代码字符串中。另一个容易忽略的点是缺考和缓考:如果exam_score允许为NULL,统计平均分时要用AVG(COALESCE(exam_score,0))或者先过滤掉缺考记录,否则平均分会被拉低,这个口径要在论文或说明书里写一句,答辩时能加分。

3.3 Servlet 分发与 DAO 封装:一个类管一组动作

按功能建 Servlet 是最省事的写法,学生一个、课程一个、成绩一个、统计一个,每个 Servlet 内部用action参数分发到不同方法。这样既避免一个功能一个类导致类爆炸,也不用引入复杂路由。分发骨架大概是这样:

@WebServlet("/score") public class ScoreServlet extends HttpServlet { private final ScoreService scoreService = new ScoreService(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doPost(req, resp); // 查询也走 POST 分发,逻辑统一 } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action = req.getParameter("action"); if (action == null) action = "list"; switch (action) { case "list": list(req, resp); break; case "save": save(req, resp); break; case "delete": delete(req, resp); break; default: resp.sendError(HttpServletResponse.SC_NOT_FOUND); } } private void list(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String keyword = req.getParameter("keyword"); req.setAttribute("scoreList", scoreService.list(keyword)); req.getRequestDispatcher("/WEB-INF/jsp/score/list.jsp").forward(req, resp); } // save、delete 方法结构相同:收参 → 调 Service → 重定向回列表 }

分发模式的关键是“方法只做三件事”:取参数、调服务、决定跳转。参数校验放在 Service 里做,跳转统一用forward展示、redirect提交后回列表,避免刷新页面重复提交。doGet直接委托给doPost,是为了让列表页的链接也能正常访问,不用为每个动作写两遍。

Dao 层统一用PreparedStatement,拼接字符串的写法在课设里是扣分项,也是 SQL 注入的入口。下面这个方法把关键字查询、联表取学生和课程名、自动关资源三件事做在一起:

public List<ScoreVO> findByKeyword(String keyword) { String sql = "SELECT s.id, st.sno, st.name AS studentName, c.name AS courseName, " + "s.usual_score, s.exam_score, s.total_score, s.term " + "FROM score s " + "JOIN student st ON s.student_id = st.id " + "JOIN course c ON s.course_id = c.id " + "WHERE st.name LIKE ? OR st.sno LIKE ? " + "ORDER BY s.id DESC"; List<ScoreVO> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { String like = "%" + keyword + "%"; ps.setString(1, like); ps.setString(2, like); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { ScoreVO vo = new ScoreVO(); vo.setId(rs.getInt("id")); vo.setSno(rs.getString("sno")); vo.setStudentName(rs.getString("studentName")); vo.setCourseName(rs.getString("courseName")); vo.setTotalScore(rs.getBigDecimal("total_score")); vo.setTerm(rs.getString("term")); list.add(vo); } } } catch (SQLException e) { throw new RuntimeException("查询成绩失败", e); } return list; }

try-with-resources保证Connection、PreparedStatement、ResultSet按逆序关闭,即使中途抛异常也不会泄漏连接。LIKE的占位符两边拼%放在 Java 里做,SQL 里只留?,这是防注入的标准姿势。联表查询直接返回ScoreVO而不是Score实体,是因为页面要展示学生姓名和课程名,让实体类背着这些非表字段反而不清晰。

3.4 登录、Session 与权限过滤

系统有三类角色:管理员管学生、教师、课程和全部成绩;教师管自己课程的成绩录入;学生只能查自己的成绩。角色不同,能访问的 Servlet 也不同,这件事不该在每个 Servlet 里写 if 判断,交给 Filter 统一处理。登录成功后把用户对象和角色写进HttpSession,AuthFilter拦所有请求,放行登录页、登录接口和静态资源,其余请求检查 session 里有没有用户,没有就重定向回登录页。

@WebFilter("/*") public class AuthFilter implements Filter { private static final Set<String> WHITE_LIST = Set.of("/login.jsp", "/login", "/static/"); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; String uri = req.getRequestURI().substring(req.getContextPath().length()); boolean free = WHITE_LIST.stream().anyMatch(uri::startsWith); if (free || req.getSession().getAttribute("loginUser") != null) { chain.doFilter(request, response); } else { resp.sendRedirect(req.getContextPath() + "/login.jsp"); } } }

白名单用startsWith匹配前缀,静态资源整个目录放行。这里最常见的翻车是拦截路径写成/*却忘了放行登录接口,结果登录提交也被拦,页面在登录页和登录页之间无限重定向,浏览器报“重定向次数过多”。另一个细节是 session 超时时间,web.xml里默认 30 分钟,演示前最好确认一下,讲着讲着 session 过期被踢回登录页,很尴尬。

4. 统计报表:三类 ECharts 图表的数据链路

4.1 统计接口返回什么:categories 与 series 的约定

统计图和普通列表的区别在于,前端要的不是一行行记录,而是“分类 + 数值”这种能直接喂给图表的结构。所以统计接口的返回体要提前约定好,不要一个图一个格式。我一般统一成{code, msg, data:{categories:[], series:[{name, values:[]}]}},柱状图、饼图、折线图都能套。code用 200 表示成功,非 200 时前端只弹msg,不渲染图表,避免脏数据把图画出奇怪的形状。

后端统计 SQL 用CASE WHEN把分数分桶,一次查询拿到五个区间的计数,比查五次快得多:

SELECT CASE WHEN total_score >= 90 THEN '90-100' WHEN total_score >= 80 THEN '80-89' WHEN total_score >= 70 THEN '70-79' WHEN total_score >= 60 THEN '60-69' ELSE '60以下' END AS bucket, COUNT(*) AS cnt FROM score WHERE course_id = ? AND term = ? GROUP BY bucket ORDER BY FIELD(bucket, '60以下','60-69','70-79','80-89','90-100');

FIELD()用来固定输出顺序,否则GROUP BY出来的顺序不保证,柱状图的横轴会忽左忽右。查询参数必须带课程和学期,不允许“全库统计”,数据量大了图表加载会明显变慢。Servlet 端把结果转成两个数组,categories放桶名,values放计数,缺的桶补 0,前端就不用做容错判断了。

4.2 三张图的 option 配置与参数含义

分数段分布用柱状图,横轴是桶名,纵轴是人数,柱子宽度设成百分比比固定像素更适应不同屏幕。及格率用饼图,两个扇区就够,不要按分数段切五块饼再算及格率,视觉上反而说不清。班级平均分对比用折线图,横轴是班级,纵轴是平均分,yAxis.min设成 50 左右能让差异更明显,但要在标题里注明坐标轴不是从 0 开始,避免误导。

// 分数段分布:柱状图 const distChart = echarts.init(document.getElementById('distChart')); distChart.setOption({ title: { text: '分数段人数分布', left: 'center' }, tooltip: { trigger: 'axis' }, grid: { left: '8%', right: '5%', bottom: '10%' }, xAxis: { type: 'category', data: ['60以下', '60-69', '70-79', '80-89', '90-100'] }, yAxis: { type: 'value', name: '人数', minInterval: 1 }, series: [{ type: 'bar', data: [3, 8, 12, 9, 4], barWidth: '45%', itemStyle: { color: '#4a7ebb' }, label: { show: true, position: 'top' } // 柱顶显示人数 }] });

minInterval: 1保证纵轴刻度不出现 0.5 人这种小数。label.show打开后不需要鼠标悬停也能读出数值,答辩投影时更清楚。饼图的radius建议设成['40%','70%']做成环形,中间放及格率文字,比实心饼图信息密度高。折线图的smooth设成false,成绩对比用直线更准确,平滑曲线会让人误以为中间有连续数据。

4.3 JSP 注入数据的两种方式与选择

前端拿数据有两条路。第一条是 JSP 渲染时直接把 JSON 写进<script>,写法是const data = <%= request.getAttribute("chartJson") %>;。这条路快,但 JSON 里只要出现引号、反斜杠或者中文被转义,整段脚本就语法错误,控制台报Unexpected token,图一片空白。第二条是页面加载完用fetch请求统计 Servlet,拿到 JSON 再setOption,数据和视图彻底分开,出问题看 Network 面板就能定位。

// 推荐:异步取数,先渲染骨架再填数据 document.addEventListener('DOMContentLoaded', function () { const courseId = document.getElementById('courseSelect').value; fetch(`${ctx}/stat?type=distribution&courseId=${courseId}`) .then(resp => resp.json()) .then(res => { if (res.code !== 200) { alert(res.msg); return; } distChart.setOption({ xAxis: { data: res.data.categories }, series: [{ data: res.data.values }] }); }) .catch(err => console.error('统计接口异常', err)); });

DOMContentLoaded保证 DOM 已经就绪再init,否则getElementById拿到null,echarts.init直接抛错。ctx是 JSP 里提前用${pageContext.request.contextPath}赋好的上下文路径,不要硬编码项目名。切换课程时重新fetch一次并调setOption,ECharts 会做增量更新,不需要销毁重建实例;只有容器尺寸变化时才需要resize(),比如侧边栏折叠或者窗口缩放。

4.4 统计的边界:权限与性能

统计接口和列表接口一样要过权限。学生角色请求班级平均分接口应该被拒绝,或者只返回自己所在班级的数据,这件事在StatServlet里根据 session 里的角色做一次判断,不要指望前端隐藏按钮。教师角色只能统计自己任教的课程,SQL 里加teacher_id = ?条件。管理员才允许看全量。

性能方面,统计查询一定要带course_id和term条件,走idx_course_term索引;如果确实要做全校维度的看板,考虑把统计结果定时写进一张汇总表,页面读汇总表而不是每次现算。课设数据量小感觉不出来,但答辩老师经常会问“数据量大了怎么办”,能答出“加索引、做汇总、加缓存”这三层,基本就够了。

5. 常见问题排查:六个最容易翻车的点

5.1 图表一片空白,控制台报 echarts is not defined

现象:页面其他部分正常,图表区域白的,F12 控制台第一行红字Uncaught ReferenceError: echarts is not defined。

原因:脚本没加载成功。要么 CDN 断网,要么src路径写错,要么<script>放在echarts.init之后。还有一种隐蔽情况,JSP 里用了相对路径,从/score?action=list和/stat两个入口进来解析出的基准路径不同,一个能加载一个 404。

解决:把echarts.min.js放本地,src用${pageContext.request.contextPath}/static/js/echarts.min.js绝对路径。脚本标签放在</body>前,初始化代码包在DOMContentLoaded里。排查时先看 Network 面板该文件是不是 200,再看 Console 有没有报错,两步就能定位。

5.2 中文变问号,录入的学生姓名全是 ???

现象:表单提交后列表里中文显示成问号,或者数据库里查出来是乱码,英文和数字正常。

原因:编码链路上有三处可能断:请求体解码、响应输出、数据库连接。只设了response.setCharacterEncoding("UTF-8")而没处理请求,POST 中文照样乱;JDBC URL 没带characterEncoding=utf8,驱动会用平台默认编码;建库时用了latin1,存进去就已经坏了。

解决:加一个EncodingFilter在最前面,统一request.setCharacterEncoding("UTF-8")和response.setContentType("text/html;charset=UTF-8");JDBC URL 写成jdbc:mysql://localhost:3306/sms?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai;建库建表统一utf8mb4。三处都对齐之后再测,别只改一处就以为好了。

5.3 总评和明细对不上,同一条记录两个页面显示不同分数

现象:录入页显示总评 78.4,列表页显示 78.39,统计平均分又是另一个数。

原因:三处各算各的。前端 JS 用浮点数算,后端 Java 用double算,SQL 里又用表达式算,精度和舍入规则都不一样。

解决:总评只在 Service 层算一次,用BigDecimal加HALF_UP,算完写进total_score字段,其他所有地方只读这个字段,不再重算。前端如果需要即时预览,也调一个后端接口算,别在 JS 里复制一份公式。

5.4 Tomcat 跑一阵就报 Too many connections

现象:刚开始正常,点几十次之后页面 500,日志里一堆Communications link failure或者Too many connections。

原因:Dao 里Connection用完没关,或者只在正常流程关、异常分支漏了。每次请求泄漏一个连接,MySQL 默认最大连接数很快被占满。

解决:所有数据库操作统一用try-with-resources,把Connection、PreparedStatement、ResultSet都放进 try 的括号里。检查一遍有没有在 Dao 里new了连接却只在方法末尾close()的写法。改完重启 Tomcat,在 MySQL 里show processlist确认空闲连接数不会持续上涨。

5.5 删除学生时报外键约束失败

现象:点删除,页面 500,日志里Cannot delete or update a parent row: a foreign key constraint fails。

原因:该学生在score表里还有成绩记录,外键挡住了删除。

解决:两种做法选一种。物理删除就在 Service 里开事务,先delete from score where student_id=?再删学生,两步都成功才提交;逻辑删除就给学生表加status字段,删除只改状态,列表查询过滤掉已删除的。课设里逻辑删除更稳,答辩时还能讲一句“保留历史成绩的可追溯性”。

5.6 登录成功刷新又回登录页

现象:登录后跳到首页,一刷新或者点任意菜单就被踢回登录页。

原因:session 里没存住用户,或者AuthFilter的白名单写错把登录接口也拦了,或者重定向路径少了上下文。

解决:登录成功后req.getSession().setAttribute("loginUser", user),确认没有在别处invalidate()。Filter 白名单用startsWith匹配/login、/login.jsp、/static/,重定向统一用req.getContextPath() + "/login.jsp"。测试时打开浏览器开发者工具的 Application 面板,看JSESSIONID有没有生成、session 里有没有那个属性,比猜快得多。

6. 验收与答辩:一份能照着走的演示脚本

验收前我会把系统按“一条主流程 + 三个追问”过一遍,主流程是:管理员登录 → 新增学生 → 新增课程 → 录入成绩 → 查列表 → 看统计图 → 退出。这条线走通,功能分基本拿满。三个追问通常是:SQL 注入怎么防的、session 怎么管的、统计图的数据从哪来。前两个在 Dao 和 Filter 里有现成答案,第三个要能说清“Servlet 查库 → 转 JSON → 前端 fetch → setOption”这条链路。

下面这张自查表,演示前一天逐项打勾,比临时抱佛脚管用:

检查项操作通过标准
冷启动重启 Tomcat 后直接访问登录页3 秒内出页面,无 500
中文新增一个三字姓名学生列表和数据库均为中文
重复录入同一学生同一课程同一学期录两次提示已存在,不产生两条
边界成绩录入 0 分和 100 分总评计算正确,统计桶包含
权限学生账号访问统计接口被拦截或只返回本人数据
图表断网状态打开统计页三张图正常渲染
删除删除有成绩的学生有提示,不报 500

答辩时还有一个细节:老师让你“现场改一个东西”的概率不低,常见的是改总评权重或者加一个分数段。提前把权重抽成常量、把分桶 SQL 写成一处,改起来就是一行的事,改完当场刷新统计图,效果比讲十分钟架构都有说服力。我从带第一届课设起就养成了一个习惯:演示前把数据库备份一份,任何现场操作翻车,两分钟能恢复原状,这个后悔药值得每个做演示的人备着。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进