简介:一份适合JavaWeb初学者的教务管理系统项目,基于MVC设计模式与DAO分层思想,整合Servlet、JSP、Filter及MySQL数据库,可通过完整业务场景理解Web请求处理与权限控制。系统含登录、找回密码、修改密码、注销等机制,并按学生、教师、教务员、系统管理员四类角色提供差异化功能:学生可成绩查询、选修与考级报名、学籍维护及考级成绩查询;教师维护个人信息;教务员管理成绩与报名名单;管理员负责用户与权限管理,覆盖教务核心流程。资源共223个文件,含82个Java源码与对应class文件、38个JSP页面、6个Jar依赖包、SQL脚本及XML配置,压缩包仅2.6MB,代码量适中,配合README可快速启动运行。该项目已有11657人学习下载,源码注释相对清晰,既可作课程设计参考,也便于二次开发完善功能。
1. 教务管理系统javaweb项目:为什么很多“能跑”的Demo一到答辩就翻车
“教务管理系统javaweb项目”在实训周、毕设开题和转行简历里出现频率极高。但真正动手的时候,十个里面有九个是拿过需求就写增删改查,数据填满三张表才发现:成绩该由谁录入、选课冲突怎么算、教师删了课程为什么页面直接报错。这些才是教务系统真正的难点,而不是表的数量。这篇笔记不讲某个现成代码包,也不吹框架,只按照我实际带过和做过这类项目的路子,把需求拆分、表结构设计、登录权限、成绩业务、高频踩坑和上线前加固一步步拆开。适合正在做毕设、实训,或者第一次独立接JavaWeb项目的开发者:照着能复现,也能看清每条路为什么这么选。
2. 把需求拆成表结构:三种角色、五张核心表与框架选型
教务管理系统听起来很大,排课、教室、公告、课表都能算进去,但一个典型的JavaWeb项目真正要立住的,是“人—课—成绩”这三者的闭环。先把范围缩小,系统才能做完,否则很容易陷在无穷的后台配置页里。
2.1 三种角色与权限边界:管理员、教师、学生各能做什么
要给系统定权限,先给角色列一张操作清单。管理员维护基础数据:学生、教师、课程的新增、修改、删除,还能查看所有选课和成绩记录,必要时替学生退选。教师只关心自己的课程:查看授课课程列表、录入成绩、修改自己课程的成绩。学生只关心自己的学习:浏览课程目录、选课、退选、查看个人成绩单。整理成矩阵会更直观:
| 功能 | 管理员 | 教师 | 学生 |
|---|---|---|---|
| 学生/教师/课程维护 | 可操作 | 不可操作 | 不可操作 |
| 查看成绩 | 全部 | 本人所授课程 | 本人成绩 |
| 录入/修改成绩 | 可 | 本人课程可 | 不可 |
| 选课/退选 | 可替学生退选 | 不可 | 自己选退 |
这张矩阵最好在写代码之前就定下来,因为后面所有Controller方法的权限校验都围着它转。实际项目里常见的返工,是功能做完了才发现教师账号能打开学生管理菜单,就是因为菜单和按钮只在前端隐藏、后端没有同步校验。权限不是前端菜单的“显隐开关”,是后端接口的访问门槛,这个观点到第3章还会反复用到。
2.2 五张核心表的字段设计与建表SQL
明确了权限边界,表结构可以落到SQL。常见的核心表是五张:student、teacher、course、course_selection。成绩不单建表,而是放在选课表里作为成绩字段,这样学生和课程的关系与成绩在同一行,查询成绩单时少一次JOIN,也便于保证一条选课记录只有一份成绩。
-- 学生表 CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', student_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', name VARCHAR(50) NOT NULL COMMENT '姓名', password VARCHAR(64) NOT NULL COMMENT '密码MD5加盐后密文', major VARCHAR(50) COMMENT '专业', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生表'; -- 教师表 CREATE TABLE teacher ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', teacher_no VARCHAR(20) NOT NULL UNIQUE COMMENT '工号', name VARCHAR(50) NOT NULL COMMENT '姓名', password VARCHAR(64) NOT NULL COMMENT '密码MD5加盐后密文', title VARCHAR(30) COMMENT '职称', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='教师表'; -- 课程表 CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', course_no VARCHAR(20) NOT NULL UNIQUE COMMENT '课程编号', name VARCHAR(100) NOT NULL COMMENT '课程名称', credit DECIMAL(3,1) NOT NULL DEFAULT 0 COMMENT '学分', max_student INT NOT NULL DEFAULT 50 COMMENT '选课人数上限', teacher_id INT COMMENT '授课教师,可为空', CONSTRAINT fk_course_teacher FOREIGN KEY (teacher_id) REFERENCES teacher(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程表'; -- 选课与成绩表 CREATE TABLE course_selection ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', student_id INT NOT NULL COMMENT '学生ID', course_id INT NOT NULL COMMENT '课程ID', score DECIMAL(5,2) COMMENT '成绩,教师录入,可空', select_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '选课时间', UNIQUE KEY uk_student_course (student_id, course_id), CONSTRAINT fk_selection_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_selection_course FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生选课成绩表';关键点是course_selection表里的唯一约束:uk_student_course (student_id, course_id)。它保证同一个学生同一门课只能有一条选课记录,这是选课业务并发安全的第一道防线。course表里的teacher_id外键体现“一个教师开多门课”的关系,设置为可空是兼容还没有分配教师的课程。密码字段长度64是为了容纳MD5加盐后的十六进制字符串,明文也能存,但别明文存,原因第3章细说。所有表统一用InnoDB和utf8mb4,后者才能正常存中文和特殊符号,排序和比较也可靠。
为什么成绩放选课表而不是单独一张score表?单独建表的好处是成绩记录有独立生命周期,但在这个范围内会多一次JOIN,而且容易产生“有选课记录却没有成绩记录”或者反过来的孤儿数据。把score直接放进选课表,录入成绩就只是一个UPDATE,删选课记录时成绩也随之消失,语义更干净。真正有复杂重修补考业务的系统才值得拆表,教学项目没必要。
2.3 为什么用SSM而不是纯Servlet:一个JavaWeb项目的选型判断
“JavaWeb项目”这个题目有几种常见落地方式:原生Servlet+JSP、SSM(Spring+SpringMVC+MyBatis)、Spring Boot。我在带人做这类项目时,默认建议SSM,理由是它兼顾了课程考核要求和工程效率。
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| Servlet+JSP | 原理清晰,不依赖框架 | 代码冗余,一个CRUD要写一堆类 | 快速演示、入门课 |
| SSM | 分层清楚、资料多、面试常问 | 配置略多,需要理解代理和容器 | 毕设、实训、中小型系统 |
| Spring Boot | 上手快、内置Tomcat、开发效率高 | 封装太深,解释不清底层容易露怯 | 前后端分离、有Java基础 |
SSM的项目里,Spring管Bean和事务,SpringMVC管请求分发,MyBatis管SQL,正好把控制器、服务、数据访问拆成Controller、Service、Mapper三层。教务系统的登录权限、成绩事务、分页查询这些需求,用这三层放下去很自然。如果目标只是快速出页面,Spring Boot当然更好,但很多课程大纲和答辩要求都还停留在“JavaWeb+SSM+JSP”这个组合,选它最稳妥。
用Maven做依赖管理时,核心依赖通常就是spring-context、spring-webmvc、mybatis、mybatis-spring、mysql-connector-java,以及一方JSP标准标签库。具体版本跟你本地的JDK版本走就行,没必要追求最新。我一般会让服务端打成war包部署到Tomcat,而不是用Spring Boot的内嵌容器,这样更贴近“javaweb项目”这个题目的预期。第3章开始的代码,也默认你在SSM工程里。
3. 登录认证与权限拦截:给教务系统装上第一道门
教务系统第一个要处理的问题不是CRUD,而是“谁在访问”。没有登录校验时,任何人知道一个URL就能打开成绩列表,这是很多开发新手最容易漏掉的漏洞。本章用最小代码把这道门立起来。
3.1 用Filter拦住每一个未登录请求:一来就重定向到登录页
登录状态最朴素的做法是:用户登录成功后,把当前用户对象放进HttpSession,后续请求通过Filter判断Session里有没有这个对象。下面这个LoginFilter就能覆盖绝大多数页面请求。
public class LoginFilter implements Filter { @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(); // 登录页、登录接口、静态资源不拦截,否则会死循环 if (uri.endsWith("/login") || uri.endsWith("/login.jsp") || uri.contains("/static/")) { chain.doFilter(request, response); return; } // 没有登录就重定向到登录页 if (req.getSession().getAttribute("currentUser") == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } chain.doFilter(request, response); } }这段代码的核心逻辑是两步:先放行登录相关的URL,再检查Session中是否有currentUser属性。放行条件必须准确,否则会出现“未登录访问登录页也被拦,然后重定向到登录页变成死循环”的翻车现场。uri.endsWith("/login")是放行登录接口,uri.endsWith("/login.jsp")是放行登录页面,uri.contains("/static/")是放行CSS、JS、图片这些资源。如果你用了SpringMVC的静态资源映射,路径可能是/resources目录,要根据实际情况调整。注册Filter可以在web.xml里配置,也可以用Servlet 3.0的@WebFilter注解并配上扫描包,前者兼容性更好,我习惯用XML。
登录成功时,Controller里只要一行:session.setAttribute("currentUser", user)。这里的user不要只存一个用户名,应该存带角色的完整对象,因为下一节的权限校验要从这个对象里取角色。注意Session默认有效期是30分钟,如果系统要求“办公时间别自动掉线”,就需要在web.xml里调整session-timeout,这个到第6章再说。
3.2 基于角色的接口控制:同一条URL不能让任何人访问
Filter只解决“是否登录”,不解决“是否有权”。教师录入成绩的接口,如果学生直接访问,后端必须拒绝,而不是靠前端把按钮藏起来。常见做法是给Controller方法加一个自定义注解,再用SpringMVC拦截器去检查角色。
// 自定义注解:标注某个接口要求什么角色 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequireRole { String value(); }public class RoleInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod method = (HandlerMethod) handler; RequireRole role = method.getMethodAnnotation(RequireRole.class); if (role != null) { User user = (User) request.getSession().getAttribute("currentUser"); if (user == null || !user.getRole().equals(role.value())) { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"message\":\"权限不足\"}"); return false; } } } return true; } }用法是在Controller方法上写@RequireRole("teacher")。拦截器拿到当前请求对应的HandlerMethod,读取注解里要求的角色,再和Session里的用户角色比较,不匹配就直接返回JSON,而不是跳转页面。这里最容易被忽略的是handler instanceof HandlerMethod,因为SpringMVC里handler可能是资源请求处理器,不做类型判断直接强转会在访问静态资源时报ClassCastException。返回内容我习惯用统一JSON格式,这样Ajax请求能直接拿到错误消息,页面请求需要另做处理。
要记住:前端菜单按角色显隐,只是用户体验;后端接口按角色拦截,才是权限。这个校验必须放在服务端,否则任何人打开浏览器的开发者工具,把请求路径改一下,就能绕过界面直达接口。
3.3 密码安全:MD5加盐代替明文存储的落地写法
很多教学项目和毕设都把密码直接存明文,这属于“能跑但不敢拿出来见人”的写法。数据库泄露一次,所有账密一起完蛋。在JavaWeb项目里,最实用的兜底是用MD5加盐,虽然它不是最顶级的方案,但比明文强得多,代码也就十几行。
public class PasswordUtil { public static String md5Salt(String password, String salt) { String input = salt + password + salt; try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] digest = md.digest(input.getBytes("UTF-8")); StringBuilder sb = new StringBuilder(); for (byte b : digest) { String hex = Integer.toHexString(0xff & b); if (hex.length() == 1) { sb.append('0'); } sb.append(hex); } return sb.toString(); } catch (Exception e) { throw new RuntimeException("密码加密失败", e); } } }加盐的思路是:把盐和密码拼成固定顺序的字符串,再计算MD5,这样同样的明文密码因为盐不同,落库后的密文也不同,能挡掉常见的彩虹表攻击。盐可以取用户名,也可以在注册时生成随机串单独存一列。验证登录时不需要解密,只用同样规则把用户输入的密码和盐重新计算,再比对数据库里的密文是否一致。这里有个默认前提:注册时就把盐和密文都存进去。所以student表和teacher表的password字段设计长度为64,正好装下32位十六进制密文,如果以后升级到SHA-256,长度也够用。更安全的BCrypt也不是不能用,但需要额外引依赖,绝大多数学期项目用MD5+盐已经及格。
4. 成绩录入与选课管理:教务系统最核心的两块业务
登录做好之后,真正的业务压力集中在两个动作上:学生选课、教师录成绩。这两个动作都涉及多人并发、数据一致性,处理不好就会出现重复选课、成绩只存一半、退选后成绩还在等问题。
4.1 成绩录入加事务:几十个学生的成绩不能一条一条提交
教师录入成绩的场景通常是一张表格,一个班几十个学生一次提交。如果Controller里循环调用更新方法,一旦第20个学生保存失败,前面19个已经入库,页面报错之后教师不知该重新提交哪一部分。解决办法是把批量更新包进一个事务,要么全部成功,要么全部回滚。
@Service public class GradeService { @Autowired private CourseSelectionMapper courseSelectionMapper; @Transactional(rollbackFor = Exception.class) public void saveScores(List<ScoreInput> scores) { for (ScoreInput input : scores) { // 按照学生ID和课程ID定位选课记录,更新成绩 int rows = courseSelectionMapper.updateScore( input.getStudentId(), input.getCourseId(), input.getScore()); // 一条都没有更新,说明该学生没有选这门课 if (rows == 0) { throw new RuntimeException("学号" + input.getStudentId() + "未选修该课程"); } } } }updateScore对应的Mapper SQL判断依据是选课表里同时存在student_id和course_id:
<update id="updateScore"> UPDATE course_selection SET score = #{score} WHERE student_id = #{studentId} AND course_id = #{courseId} </update>@Transactional(rollbackFor = Exception.class)这行很关键。Spring默认只对运行时异常回滚,如果直接写@Transactional,遇到受检异常(比如IOException)事务是不会回滚的。显式指定rollbackFor为Exception.class,等于告诉事务管理器“任何异常都回滚”,这是我在项目里养成的习惯,能少踩很多坑。另外事务一定要放在Service层,而不是Controller层。Controller里的方法是SpringMVC管理的,Service里的方法才被Spring事务代理接管,放错了位置会得到“事务没生效”的玄学问题,其实看调用栈就很清楚。输入参数ScoreInput可以用一个简单POJO,里面只有studentId、courseId、score三个字段,一次提交对应一条JSON记录。
4.2 选课的唯一约束与退选逻辑:并发场景下怎么不做重复数据
选课是典型的高并发写入场景。学生提交选课请求时,如果业务代码先SELECT判断有没有选过,再INSERT,两个请求同时执行时中间有竞态窗口:两个SELECT都返回不存在,两个INSERT都执行,于是产生重复记录。解决这个问题最可靠的办法不是锁代码,而是让数据库挡住重复写入。
@Transactional public void selectCourse(Integer studentId, Integer courseId) { try { courseSelectionMapper.insert(studentId, courseId); } catch (DuplicateKeyException e) { // 数据库唯一约束兜底,重复选课直接变成业务提示 throw new BusinessException("你已经选过这门课了"); } }这里能生效的前提是第2章建表时给course_selection加了唯一约束uk_student_course (student_id, course_id)。有了它,并发下后到的INSERT会被数据库拒绝,抛出DuplicateKeyException,Service捕获后翻译成友好提示。这种“直接插、让约束兜底”的做法比先查再插更简单,也更符合实际工程里“数据库是最后一道防线”的原则。
退选逻辑和要求相似:先按学生和课程删除,根据影响行数判断结果。
public void cancelCourse(Integer studentId, Integer courseId) { int rows = courseSelectionMapper.delete(studentId, courseId); if (rows == 0) { throw new BusinessException("没有找到这条选课记录,可能已经退选"); } }delete返回0说明SELECT不出来对应记录,原因可能是选课不存在,也可能之前已经退过。这里不做二次查询,直接抛错,页面提示“可能已经退选”,既省一次数据库查询,也避免用户反复点击造成误判。选课人数上限max_student的校验也在这里做,好的方式是UPDATE语句里带条件,比如实现原子性的“名额-1”,而不是先查count再判断。
4.3 用MyBatis关联查询拼出成绩列表:一张成绩单连起三张表
学生查看成绩单时,要显示课程名、授课教师、学分、成绩。这些字段分布在course、teacher、course_selection三张表里,Mapper里用一个JOIN查询解决。
<select id="findScoreList" resultType="map"> SELECT course.name AS course_name, course.credit, teacher.name AS teacher_name, course_selection.score FROM course_selection JOIN course ON course_selection.course_id = course.id LEFT JOIN teacher ON course.teacher_id = teacher.id WHERE course_selection.student_id = #{studentId} ORDER BY course_selection.select_time DESC </select>用JOIN而不是子查询,是因为这里一次取多行结果,JOIN可以让数据库在连接阶段直接过滤无关数据,一次扫描拿回全部需要的列。teacher用LEFT JOIN是因为course.teacher_id允许为空,如果学生恰好选了一门还没分配教师的课程,普通INNER JOIN会把这条记录整个丢掉,列表里就少一门课。查询结果用resultType="map"最简单,Service层拿到List<Map<String, Object>>就能直接扔给前端渲染,不需要为这个只读场景专门建VO类。如果后续要加导出Excel,再把这些map转成对象写入文件也不迟。
这里有个性能注意点:student表只有几千人时,JOIN查询是完全没压力的。但如果将来课程表有几十万条,就要给course_selection的student_id和course_id分别建索引,联合唯一索引虽然能覆盖(student_id, course_id),但单独按student_id查成绩单时,MySQL能否用上还需要看执行计划。在索引没建好时,EXPLAIN一下就会看到全表扫描。很多人把这当成玄学,其实就是少建了一个普通索引。
5. 教务管理系统高频踩坑的5个排查清单
这部分是我在帮别人排故障时遇到最多的问题,每条都按现象、原因、解决三步记录。遇到类似问题,优先套用。
5.1 Tomcat启动闪退:先查端口、再查jar包冲突
现象:启动Tomcat,控制台日志刚打几行就异常退出,或者页面一直转圈。
原因:最常见的是8080端口被占用,另一个进程占了默认端口;其次是依赖里的servlet-api包跟Tomcat自带的重复,启动时出现ClassCastException或NoClassDefFoundError。
解决:先用命令查端口,再查jar。
lsof -i:8080 kill -9 进程ID如果是jar冲突,检查pom.xml里javax.servlet-api或servlet-api的scope是不是provided,只有Tomcat本身运行时才需要。打包war时,Tomcat的lib目录已经有这些类,war里再带一份就会冲突。我一般会把这类依赖统一改成provided,防止打进war。
5.2 中文乱码:JSP、过滤器、数据库、连接参数少一处就乱
现象:页面显示中文变成问号,或者数据库里存的是乱码,又或者查询条件中文匹配不到数据。
原因:请求参数解码、JSP页面编码、数据库表字符集、连接串里缺characterEncoding,这四处只要有一处不一致,中文就出问题。
解决:先把四处全部统一为UTF-8。JSP页面顶部写pageEncoding="UTF-8",数据库连接串带上参数,Filter里对POST请求统一设置编码。
jdbc:mysql://localhost:3306/edu?useUnicode=true&characterEncoding=UTF-8request.setCharacterEncoding("UTF-8");需要注意GET请求的参数乱码通常不是Filter能解决的,字符在容器层就已经按默认编码解码了。常见做法是在Tomcat的server.xml里给Connector加URIEncoding="UTF-8",或者在SpringMVC里用StringHttpMessageConverter统一处理。排查时先看数据库表是不是utf8mb4,再看连接串,因为这两处错了,界面层再对也没用。
5.3 连接池报错Connection is not available:连接用完一定要还
现象:系统运行一段时间后,日志出现“Connection is not available, request timed out”或“Connection pool exhausted”,重启Tomcat才能恢复。
原因:数据库连接没有释放,借了不还。原生JDBC时代,是Statement、ResultSet用完没close;MyBatis时代,是SqlSession在使用后没有关闭,或者手动获取连接后漏了finally释放。
解决:用MyBatis时,正常通过Mapper操作数据库会自动归还连接。手动使用SqlSession时必须放到try-finally或者try-with-resources里。排查时可以打印连接池的activeCount,看是不是持续增长。连接池的maxActive也不宜设得过大,通常20、50就够一个教学项目,设太大反而会拖垮数据库。
5.4 删除课程被外键拦住:先退选再删课,或者用状态位代替物理删除
现象:管理员在课程管理里删除一门课,页面报Cannot delete or update a parent row,删不掉。
原因:course表被course_selection表外键引用,学生选课记录还挂在课程上,数据库拒绝删除父表记录。
解决:在Service层加一个带事务的方法,先删除这门课的所有选课记录,再删课程。
@Transactional public void deleteCourse(Integer courseId) { courseSelectionMapper.deleteByCourseId(courseId); courseMapper.delete(courseId); }但更好的思路是别物理删除。教务系统的成绩和选课记录有历史价值,课程状态改为“停开”比直接删掉更合理。做法是在course表加一个status字段,0表示正常,1表示停开,查询时用status过滤。这样既不需要级联删数据,也能保留历史成绩,避免期末统计时发现成绩单里少了一门课。
5.5 改了代码页面不变:target目录里的老class和Tomcat缓存
现象:改了Controller或JSP,刷新浏览器还是旧页面,有时连重启Tomcat都没用。
原因:IDE没有触发自动重新编译,target/classes里还是旧的class文件;JSP会被Tomcat编译缓存在work目录;浏览器也可能缓存了静态资源。
解决:Maven项目先清理再重新编译,这是最有效的动作。
mvn clean package如果还不行,看Tomcat的work目录里有没有对应应用名的缓存目录,删掉重启。浏览器端按Ctrl+Shift+R强制刷新,或者打开开发者工具的Disable cache。这一套下来基本能解决所有“改了没生效”的问题,剩下的检查是不是代码分支没合并这种低级原因。
6. 从“能跑”到“能交付”:教务系统上线前的四个加固动作
功能都跑通之后,离交付还差几步。这里挑四个性价比最高的动作,按顺序做一遍,系统状态会完全不同。
6.1 登录验证码与Session超时
没有验证码的登录页很容易被脚本暴力撞库,尤其学生密码很多是弱口令。简单的做法是写一个Servlet生成四位随机数字的验证码图片,把正确答案存进Session,登录时比对。代码量不大,但能挡住绝大多数简单攻击。同时把Session超时从默认30分钟调成20分钟,项目里学生机经常忘关页面,超时太短会被中途踢出,太长又占内存,20分钟是比较实用的值。
<session-config> <session-timeout>20</session-timeout> </session-config>6.2 分页查询:别让一次查询把所有成绩都带出来
学生、课程、成绩记录过几百条后,一次性全查出来页面会变卡。最简单的分页是MySQL的limit offset,Mapper里接start和pageSize两个参数。想省事用PageHelper插件,但要注意它会在当前线程上绑定分页参数,写多表查询和复杂SQL时要格外小心,查询很容易被多个Mapper误分页。教学项目我更推荐手写limit,参数清晰,逻辑完全可控。
6.3 上线前用脚本快速冒烟:登录、选课、录成绩走一遍
发布前我会用一条curl配合几个SQL,把主流程最快时间走一遍。全部通过才算能交付:
| 步骤 | 操作 | 预期 |
|---|---|---|
| 登录 | POST /login,带账号密码 | 返回302到首页 |
| 选课 | 学生选择一门课 | 选课表新增一条记录 |
| 重复选课 | 学生再选同一门 | 提示已选 |
| 录成绩 | 教师提交一次成绩批量 | 成绩字段更新 |
| 退选 | 学生退选 | 记录删除 |
我最初做这类系统时,总把“能跑”当成完成,后来被数据一多就慢、并发一写就乱的教训折腾过几次,才习惯把这套清单当成验收标准。权限、事务、异常处理、数据校验,这些“看不见”的部分才是教务系统真正值钱的地方。宁可多花半天把这些加固做完,也别让用户在页面上看到一个500。希望帮到你。
本文还有配套的精品资源,点击获取