ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java课程设计实战:学生成绩查询系统完整开发指南

Java课程设计实战:学生成绩查询系统完整开发指南 每个学校Java课程设计名单里几乎都有学生成绩查询系统这个题目。我这些年带过的学生选这个课题的不少于二十个题目看着简单但真正能拿得出手的没几个。多数人交上来的东西是能跑但不敢演示——数据库连接串写死在代码里、老师一输入非法分数就报错、统计结果和手算对不上更别提权限控制了。这篇文章我把从需求梳理到答辩演示的完整过程拆开讲适合正在做Java课程设计的在校生、想用Java练手但不清楚系统怎么搭的初学者也适合需要带毕设的同行参考。你要的不会是一堆能复制粘贴就跑的源码而是一套能让你在答辩现场从容应对追问的思路。1. 这个选题怎么回事先别急着写代码把需求边界想清楚很多同学拿到学生成绩查询系统这个题目第一反应是简单第二反应是赶紧写。但我带过这么多学生明确告诉大家凡是前期需求没理清就开始建表的后期几乎一定会返工。一个最常见的翻车案例有的同学把系统做成了学生信息管理系统学生增删改查、课程增删改查、成绩增删改查还有班级管理、专业管理、教师管理七大模块堆在一起。功能确实多但老师问这个系统解决什么问题他答不上来。原因很简单需求没有聚焦。成绩查询系统的核心价值是让合适的人在合适的时间看到合适的成绩而不是做一个大而全的教务系统。1.1 三种角色三种完全不同的查成绩诉求我们从使用者的角度拆一下。学生是整个系统最底层的用户。他的诉求非常单一登录之后看到自己的成绩列表按学期筛选知道每门课多少分、是否及格、学分多少、自己在班级里排第几。他不需要看到别人的分数也不需要改任何数据。教师是成绩数据的生产者。他的诉求是登录之后只能看到自己任课的课程进入某门课程后能看到选课学生名单、录入成绩、修改成绩最好还能看到这门课的平均分和分数段分布。教师不应该看到其他教师的课程数据。管理员是系统维护者。他的诉求是维护学生、教师、课程基础信息必要时重置密码查看整个系统运行情况做跨班级、跨专业的统计报表。这三种角色的数据范围天然是逐层包含的。学生只看得见自己教师看得见自己的课程管理员什么都看得见。很多课程设计做砸不是因为功能不会写而是权限范围没有想明白最后表现出来就是学生能把全班成绩都查出来或教师账号能打开管理员页面。为了不遗漏我习惯用一个简单的表格把角色和功能范围固定下来写代码之前先对一遍角色可见数据范围可执行操作不可执行操作学生本人成绩查询、筛选、看统计修改/删除/查看他人成绩教师本人任教课程下的成绩录入、修改、查询、统计操作非本人课程/学生信息管理员全部数据基础信息维护、全局统计无开放所有管理功能这张表贴在项目文档第一页答辩时直接能拿出来讲比临时想强得多。1.2 功能优先级怎么排一档二档三档确定了角色还要确定到底做哪些功能。这里我给出建议的分级方式按不做会被扣分、做了能加分来排。第一档是保命功能缺一个都不行登录认证三种角色用各自的账号密码登录Session保存登录状态学生成绩查询学生登录后能查看自己的成绩列表支持按学期过滤教师成绩录入与修改教师能给自己的课程录入成绩、修改错录的成绩管理员基础维护管理员能维护学生、课程、任课教师关系权限控制学生不能访问教师接口教师不能访问管理员接口第二档是区分及格和良好的功能教师按课程查看学生成绩列表与平均分管理员组合查询学号姓名班级学期分页展示避免一次查出几千行成绩统计最高分、最低分、平均分、及格率简单的按班级/课程汇总第三档是加分亮点时间充裕再上按分数段分布生成柱状图或饼图导出Excel成绩单密码修改与找回操作日志记录很多第一次做系统的同学有个误区想一口气把第三档也做完。结果第一档还没做扎实就卡在图表上最后演示时连查询都出问题。我的建议永远是先把第一档完整跑通再谈第二档第三档作为锦上添花写在演示文档里。答辩老师看的是系统整体质量不是某个炫技功能。需求分析到这里就够用了不需要画那种复杂的UML用例图当然你愿意画可以画但心里这张角色-权限-功能的表格必须清楚。接下来才轮得到技术选型。2. 技术选型怎么定课程设计场景下的Java技术栈搭配技术选型是另一个容易纠结的点。尤其是当你刷了一圈Java学习路线、Java面试八股文之后看到别人用微服务、Redis做课程设计立刻怀疑自己是不是太简单。这里我要泼一盆冷水课程设计的评分标准是功能完整、结构清晰、代码可读、答辩能讲不是技术栈越新越好。用一个分布式的架构做一个成绩查询系统反而容易被老师追问为什么要拆服务数据一致性怎么保证——你要是答不上来就是给自己挖坑。2.1 为什么是Spring Boot MyBatis而不是SSH或裸Servlet我做课程设计指导时默认给出的组合是Spring Boot 2.7.x MyBatis MySQL 8.0 Thymeleaf。这组选择不是拍脑袋是综合考虑了学习成本、开发效率、演示稳定性后得出的结论。Spring Boot的优势在于开箱即用。内嵌Tomcat打包后一个java -jar就能启动不依赖外部服务器。对于课程设计来说答辩电脑环境不固定最怕现场配Tomcat、配classpath。Spring Boot直接把这个问题解决掉了。MyBatis的优势在于SQL可控。成绩查询这种系统查询条件变化多端学生按学期查教师按课程查管理员按组合条件查。MyBatis的动态SQL可以一个方法覆盖多种条件组合代码量少而且SQL是明写的答辩时你能指着SQL讲清楚逻辑。相比之下Hibernate虽然也能做但它的HQL、懒加载、一级缓存这些概念对没接触过的学生来说是额外的认知负担调试起来更痛苦。为什么不建议裸Servlet/JSP不是不行我自己早期也写过。但你要明白Servlet时代的大量代码花在request.getParameter、类型转换、HttpServletResponse输出这些重复劳动上业务逻辑反而没时间打磨。你如果只是想练基本功当然可以写一版Servlet的但课程设计有截止日期效率很重要。版本上特别注意如果电脑装的是JDK8就用Spring Boot 2.7.x如果装了JDK17可以用Spring Boot 3.x。千万不要JDK8配Spring Boot 3启动会直接报错。另外Java环境变量配置错误导致的java 不是内部或外部命令也是高频问题装完JDK之后先java -version验证一下再说。2.2 环境准备与工程结构开发环境我推荐这套都是免费工具学生机完全够用JDK 8 或 17安装后配置JAVA_HOME和PATHIntelliJ IDEA Community 或 EclipseIDEA社区版足够Maven 3.8本地仓库如果拉取依赖慢换成阿里云镜像MySQL 8.0配合 Navicat 或 MySQL Workbench浏览器推荐Chrome调试方便工程结构这里再强调一下每个包的作用答辩时这就是你的目录讲解包/目录职责说明entity实体类数据库表对应的JavaBean字段与表字段一一对应mapper数据访问层接口定义 XML实现SQL一个表一个Mapperservice业务层处理业务规则如重复成绩校验、权限校验controller控制层接收HTTP请求调用service返回页面或JSONcommon通用类统一返回结果、全局异常处理、常量config配置拦截器注册、密码加密器等resources/mapperMyBatis XML放所有Mapper的XML文件resources/templatesThymeleaf模板页面文件这套结构最大的好处是职责单一。controller里基本看不到SQLmapper里基本没有业务判断。老师问你成绩录入时怎么判断这个学生是不是选课了你可以说在service里先查选课关系再走mapper插入一句话就把层次讲清楚。2.3 前后端方案到底选哪一种前后端分离Vue REST接口这几年很流行但课程设计我不推荐一上来就上。原因很简单前后端分离意味着你要同时维护两套工程接口文档、跨域配置、联调成本对单人开发者来说非常消耗时间。而且答辩老师翻代码时前端一个node_modules几千个文件解释成本极高。如果你是第一个人独立开发且只有一两周时间我更推荐用Thymeleaf做服务端渲染页面直接由Controller返回数据在服务端拼好前端代码量少整体逻辑连贯。后面如果确实想加个图Thymeleaf页面里用ECharts引入一个JS文件就行不影响主体。当然如果你已经熟练使用Vue而且时间充裕前后端分离也是可以的。只是要提前想好两个工程怎么一起演示跨域问题怎么解决打包后前端静态资源怎么放到后端一起发布把这些想清楚了再开工也不迟。技术选型的本质是在搞定需求和体现水平之间找到平衡。以你现在的阶段能把Spring Boot MyBatis这套组合讲透已经比绝大多数同学强了没必要为技术而技术。3. 数据库设计成绩表才是这个系统的灵魂功能清单定完、技术栈定完接下来是数据库设计。很多同学在这步翻车因为他们把数据库设计理解成建几张表忽略了表结构其实承载了系统的所有业务规则。学生成绩查询系统听名字是以学生为中心但真正动手设计时你会发现学生、课程、教师三张表都是配角成绩表才是全系统的灵魂。3.1 四张核心表的字段设计与关系我直接给出我常用的建表SQLMySQL 8.0每一张表的注释都标清楚。你复制过去改改表名前缀就能用。CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 学生ID主键, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender CHAR(1) DEFAULT 男 COMMENT 性别, class_name VARCHAR(50) NOT NULL COMMENT 班级名称如计科2201, major VARCHAR(50) COMMENT 专业, password VARCHAR(64) NOT NULL COMMENT 密码存加密后结果, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) COMMENT 学生表;教师表简单一些工号和密码是核心字段CREATE TABLE teacher ( id INT PRIMARY KEY AUTO_INCREMENT, teacher_no VARCHAR(20) NOT NULL UNIQUE COMMENT 工号, name VARCHAR(50) NOT NULL COMMENT 姓名, password VARCHAR(64) NOT NULL ) COMMENT 教师表;课程表里用teacher_id表示任课教师这是一个外键它决定了教师只能操作自己的课程这个权限能在数据库层面落地CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL UNIQUE COMMENT 课程编号, course_name VARCHAR(100) NOT NULL COMMENT 课程名称, credit DECIMAL(3,1) NOT NULL COMMENT 学分, teacher_id INT COMMENT 任课教师ID, CONSTRAINT fk_course_teacher FOREIGN KEY (teacher_id) REFERENCES teacher(id) ) COMMENT 课程表;成绩表是核心我把它单独拆开讲CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,1) COMMENT 分数范围0~100, semester VARCHAR(20) NOT NULL COMMENT 学期如2023-2024-1, exam_type VARCHAR(20) DEFAULT 期末考试 COMMENT 考试类型平时/期中/期末, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_student_course (student_id, course_id, semester, exam_type), CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES course(id) ) COMMENT 成绩表;这个设计里有几个点可以用一句话回答老师最常问的问题为什么单独一个自增主键id因为学生同一门课可能有期中和期末两次成绩如果用(student_id, course_id)联合主键期中和期末就会冲突。自增主键 唯一索引让两条记录可以共存同时又不会重复。为什么score用DECIMAL(5,1)因为成绩绝大多数是一位小数或整数DECIMAL没有浮点误差还顺手限制了数据长度。为什么semester单独存如果你不存学期一个学生重修一门课的成绩就完全没法区分这在大学场景里是绕不开的。表之间的关系用文字描述就是一个学生选修多门课程一门课程被多个学生选修所以学生和课程是多对多关系多对多需要通过中间表score来维护一个教师任教多门课程所以教师和课程是一对多关系。至于为什么没有单独的选课表因为在这个系统里成绩记录本身就已经暗示了这个学生选了这门课并参加考试选课和成绩合并到一张表对课程设计来说足够不用再拆。3.2 约束、索引与类型取舍光有表还不够字段类型、约束、索引的选择里全是答辩考点。性别用CHAR(1)还是VARCHAR(20)我用CHAR(1)。数据库的字段类型本身就是一道校验如果你用VARCHAR(20)页面传入一个男同志也会被存进去而CHAR(1)从源头限制了长度。这不是强迫症是让数据库承担一部分输入校验工作。密码列长度设64是因为MD5的32位、BCrypt的60位都放得下。不要用VARCHAR(20)存密码那意味着明文存储一看就是外行。外键约束加还是不加我的答案是加。有些人说课程设计数据库不要外键影响性能。但你这个系统的数据量撑死几千条外键带来的性能损耗完全可以忽略。外键的价值是当你试图给一个不存在的student_id插入成绩时MySQL会直接报错而不是把一条无主成绩塞进表里。从教学角度外键是数据完整性最直观的体现。唯一索引uk_student_course是整个防重复机制的关键。没有它同一学生同一门课同一学期你可以插入100条成绩。有了它数据库层面就堵死了。很多同学把防重复逻辑写在Java里先查再插但并发或者页面重复提交时Java里的判定是不安全的。唯一索引在数据库层面保证才叫真正落地。3.3 一个绕不开的查询按班级算平均分表设计完之后最好拿一个真实需求验证一下统计某个班级某门课的平均分。如果是规范化表结构SQL是这样的SELECT s.class_name, ROUND(AVG(sc.score), 1) AS avg_score FROM score sc JOIN student s ON sc.student_id s.id JOIN course c ON sc.course_id c.id WHERE c.course_no CS101 AND sc.semester 2023-2024-1 GROUP BY s.class_name;如果当初图省事把class_name冗余到了score表里查询确实不用JOIN但代价是一旦学生从计科2201转到计科2202你得同步更新历史成绩表里的所有班级记录一旦漏更新统计结果就是错的。这就是规范化的意义——用一次JOIN换取数据一致性几万条数据毫秒级返回这买卖划算。数据库设计这关过了后面写代码会非常顺。不但SQL写起来简单连Service层的业务逻辑都薄了一层因为你把很多规则交给了数据库。4. 核心功能的实现路径登录、权限、查询、统计数据库设计好之后代码开发按从登录到统计的顺序推进。这个顺序很关键我的经验是先搞定登录和权限再做业务功能因为后续所有页面都依赖登录状态你总不想每写一个页面就回头改登录。4.1 登录与会话管理登录接口是所有角色的统一入口。前端表单提交用户名、密码、角色学生/教师/管理员后端根据角色去对应表查用户。核心代码如下PostMapping(/login) public String login(RequestParam String username, RequestParam String password, RequestParam String role, HttpSession session, Model model) { LoginUser user loginService.checkLogin(username, password, role); if (user null) { model.addAttribute(error, 账号或密码错误); return login; } session.setAttribute(loginUser, user); session.setAttribute(role, role); return redirect:/index; }密码校验这里有个原则要讲清楚数据库中存的是密文所以校验时是把用户输入的密码做同样的加密后再和密文比对而不是把密文解密出来对比。MD5可以用DigestUtils.md5DigestAsHex(password.getBytes())BCrypt则是用bcryptPasswordEncoder.matches(raw, encoded)。无论哪种数据库里永远不要明文密码这个习惯从课程设计就要养成。登录之后还有一个细节同一账号能不能重复登录课程设计不用做得太复杂但如果被老师问起你可以说目前同一账号允许多端登录如果想限制可以在登录时把旧Session作废或者把会话ID存到Redis。知道有这个扩展方向就够了。4.2 权限拦截的落地方式登录之后的拦截逻辑我强烈建议用Spring MVC的拦截器而不是在每个Controller方法里手动判断。拦截器是集中式管理代码清晰答辩也好讲。核心代码如下public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); Object loginUser session.getAttribute(loginUser); if (loginUser null) { response.sendRedirect(/login); return false; } // 根据请求路径前缀判断角色是否匹配 String role (String) session.getAttribute(role); String uri request.getRequestURI(); if (uri.startsWith(/teacher/) !teacher.equals(role)) { response.sendRedirect(/403); return false; } if (uri.startsWith(/student/) !student.equals(role)) { response.sendRedirect(/403); return false; } return true; } }别忘了注册拦截器并且放行静态资源Configuration public class WebConfig implements WebMvcConfigurer { Resource private AuthInterceptor authInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns(/**) .excludePathPatterns(/login, /css/**, /js/**, /images/**); } }这里最容易踩的坑是静态资源被拦截导致CSS彻底失效页面变成纯文字。很多同学查了一晚上最后发现是拦截器把/css/**也拦了。所以排除路径一定要写全。除了拦截器Controller方法级别的权限也可以加注解比如Spring Security的PreAuthorize(hasRole(teacher))可以做到更细粒度。但课程设计用拦截器已经足够而且你能把拦截器 vs 过滤器的区别讲明白已经算深入了。4.3 成绩查询的三条业务主线成绩查询是这个系统的门面我把三条主线的SQL和逻辑拆开讲。学生查自己的成绩是精确查询。逻辑是从Session里拿到登录用户的ID作为查询条件。注意这里不能信任前端传过来的studentId因为学生可能篡改参数去查别人的成绩。应该直接从Session取ID这样权限天然安全。select idselectStudentScores resultTypemap SELECT c.course_name, c.credit, sc.score, sc.semester, sc.exam_type, CASE WHEN sc.score 60 THEN 及格 ELSE 不及格 END AS pass_status FROM score sc JOIN course c ON sc.course_id c.id WHERE sc.student_id #{studentId} if testsemester ! null and semester ! AND sc.semester #{semester} /if ORDER BY sc.semester DESC /select教师查课程成绩逻辑要分两步先查出当前教师ID对应的所有课程ID列表再查询这些课程ID下的成绩明细。为什么不做成一个大JOIN因为中间需要做一层权限校验确保这个教师确实有资格查看该课程。代码上可以在service层先调用courseMapper.selectByTeacherId再拿课程列表去查成绩。管理员查全局成绩是典型的组合条件查询。学号、姓名、班级、课程、学期都是可选条件这就是MyBatis动态SQL的擅长场景where加if可以优雅地拼接不会出多一个AND的问题。4.4 统计SQL与分数段分布成绩统计看起来高大上其实就是几条聚合函数。核心SQL给你SELECT COUNT(*) AS total, MAX(score) AS max_score, MIN(score) AS min_score, ROUND(AVG(score), 1) AS avg_score, ROUND(SUM(CASE WHEN score 60 THEN 1 ELSE 0 END) / COUNT(*) * 100, 1) AS pass_rate FROM score WHERE course_id #{courseId} AND semester #{semester};讲两个容易出错的点。 一是及格率的写法很多人习惯先查出一个及格人数再查总人数在Java里除法。但这会产生两个问题两次查询的结果可能因为中间有人插入成绩而不一致Java除法如果不注意会把整数除法搞出0。直接在SQL里用CASE WHEN一次算完逻辑清晰还不会有并发不一致。 二是AVG结果是多位小数比如 83.333333页面直接显示很难看要用ROUND(..., 1)保留一位。如果要做分数段分布0-59、60-69、70-79、80-89、90-100SQL可以这样SELECT SUM(CASE WHEN score 60 THEN 1 ELSE 0 END) AS segment_below60, SUM(CASE WHEN score 60 AND score 70 THEN 1 ELSE 0 END) AS segment_60_69, SUM(CASE WHEN score 70 AND score 80 THEN 1 ELSE 0 END) AS segment_70_79, SUM(CASE WHEN score 80 AND score 90 THEN 1 ELSE 0 END) AS segment_80_89, SUM(CASE WHEN score 90 THEN 1 ELSE 0 END) AS segment_90_100 FROM score WHERE course_id #{courseId};这段SQL返回一行五列控制器拿到之后转成JSON给ECharts用一个简单的柱状图就出来了。里面积累的CASE WHEN思路在Excel导出时也能复用。5. 代码实现中的关键片段与踩坑记录开发过程中我踩过的坑不少有些是技术问题有些是设计问题。这一节把最有代表性的几个挑出来说都是真实翻车现场。5.1 批量录入成绩与事务控制教师录成绩最朴素的做法是在页面上写一个表格每行一个学生后面一个输入框点提交之后后端循环insert。这么写的缺点有两个一是100个学生要执行100条insert慢二是中间任意一条失败前面成功的已经入库了数据就处于半对半错状态。一定要用批量插入加事务。事务注解Transactional看似简单其实有几个细节。第一它默认只对运行时异常回滚如果你抛了一个自定义的BusinessException它继承Exception不回滚成绩照样入库那就糟糕了。第二同类内部方法调用this.xxx()时注解会失效因为Spring的AOP代理没经过。第三批量插入前要对集合做空校验否则foreach生成空SQL直接语法报错。Transactional(rollbackFor Exception.class) public void saveScoreBatch(ListScore scoreList) { if (scoreList null || scoreList.isEmpty()) { throw new IllegalArgumentException(成绩列表不能为空); } // 业务校验同一学生同一课程同一学期同一考试类型只能有一条 for (Score s : scoreList) { if (s.getScore() ! null (s.getScore() 0 || s.getScore() 100)) { throw new IllegalArgumentException(成绩必须在0~100之间); } } scoreMapper.batchInsert(scoreList); }rollbackFor Exception.class值得写原因上面说了。对应的MyBatis批量插入XMLinsert idbatchInsert INSERT INTO score (student_id, course_id, score, semester, exam_type) VALUES foreach collectionlist items separator, (#{s.studentId}, #{s.courseId}, #{s.score}, #{s.semester}, #{s.examType}) /foreach /insert注意list不能为空foreach前做一次空集合校验否则SQL语法错误。批量插入还有一条经验单条SQL大小受MySQL的max_allowed_packet限制但成绩表一次一两百条完全没问题不用考虑分拆。5.2 数据库连接、中文乱码与MySQL版本坑数据库连接串是初学者最常出问题的位置。MySQL 8.0之前和8.0之后的驱动配置完全不同。现在教室和宿舍的电脑普遍装MySQL 8.0你如果还在用com.mysql.jdbc.Driver会直接报驱动不存在。8.0要用com.mysql.cj.jdbc.Driver。连接串里还有几个参数建议固定spring.datasource.driver-class-namecom.mysql.cj.jdbc.Driver spring.datasource.urljdbc:mysql://localhost:3306/grademanage?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrueserverTimezone不写MySQL 8.0会报时区错误那个乱码消息非常劝退。characterEncodingutf8不写插入的中文可能出现问号。useSSLfalse不写本地会有一堆SSL握手警告。这三个参数属于不写也能跑但是跑得难受的类型建议直接抄。另外还有一个allowPublicKeyRetrievaltrueMySQL 8.0.12之后如果使用caching_sha2_password认证连接工具可能会报Public Key Retrieval is not allowed加上这个参数就解决了。你要是用Navicat连不上本地库八成就和这个有关。5.3 SQL注入、分页与日期格式SQL注入是答辩必问项。MyBatis里#{...}是预编译?占位安全${...}是直接拼字符串危险。如果你确实需要动态传排序字段比如ORDER BY ${sortField}那必须做白名单String[] allowedFields {score, student_no, course_name, semester}; if (!Arrays.asList(allowedFields).contains(sortField)) { sortField score; // 默认值 }这个写法虽然只有两行但老师问你怎么防止SQL注入时你能说出预编译占位符排序字段白名单基本就是一个加分答案。分页方面我建议别引入PageHelper虽然它很方便因为课程设计里分页需求非常简单。自己在Service里算好offset和limitpublic PageResultScore queryByPage(int pageNum, int pageSize, QueryCondition condition) { int offset (pageNum - 1) * pageSize; ListScore list scoreMapper.selectPage(offset, pageSize, condition); int total scoreMapper.count(condition); int pages (total pageSize - 1) / pageSize; return new PageResult(list, pageNum, pages, total); }(total pageSize - 1) / pageSize是向上取整的经典写法别写成分页组件那套复杂逻辑。前端收到之后渲染页码按钮就行。日期格式问题如果你用LocalDateTime并且是前后端分离返回JSONJackson默认会把LocalDateTime序列化成[2024,6,1,10,30,0]这样的数组相信我没一个前端喜欢这个格式。解决办法是配置全局格式或者干脆在VO里用字符串返回。课程设计用统一字符串返回最省事。5.4 答辩高频追问的准备在答辩现场老师不太会一行行看代码更多是通过追问来验证你到底做没做以及你懂不懂原理。我整理了几个必问的题每个都给了回答思路问成绩会不会重复录入答数据库层面加了(student_id, course_id, semester, exam_type)唯一索引重复插入会直接报错Service层也做了前置校验给出友好提示。问学生能直接改URL看到别人的成绩吗答不能。学生查询接口里的学生ID取自Session不信任前端传参拦截器校验了路径前缀与角色是否匹配。问删除一个学生时成绩表怎么办答由于外键约束直接删会失败。目前设计是物理删除需要先删该学生在成绩表里的记录或者采用逻辑删除增加is_deleted字段管理员界面只显示未删除的数据。问统计结果为何和Excel手算不一致答统计SQL里及格率定义为60平均分保留一位小数口径统一之后不可能不一致如果不一致通常是过滤条件不同比如没按学期过滤。问这个系统能扛多少并发答这是课程设计数据量和并发都很小单机完全够如果要扩展可以从数据库连接池、缓存、读写分离方向谈但不会在实际项目中硬上。这些问题不是要你背答案而是提醒你代码里的每个关键设计都要能说出一句为什么。能说出为什么就说明你真的做了。6. 测试、演示与打包部署课程设计答辩前要做的准备代码写到最后一步马上要答辩了这时候最忌讳代码能跑就万事大吉。我见过太多学生到台上才发现数据库没启动、端口被占用、演示数据太假、字太小看不清。这一节讲的全是实践里踩过的现场教训。6.1 用JUnit和Postman把关键流程过一遍测试这件事很多课程设计队伍会自动跳过。但我不建议完全不做至少把三个核心场景写成JUnit测试因为写过测试在答辩时可以直接说老师对工程素养的认可度完全不同。第一个场景登录成功与失败。第二个场景重复成绩插入被拒绝。第三个场景统计结果正确性。使用SpringBootTest启动完整上下文来测虽然慢但最接近真实。SpringBootTest class ScoreServiceTest { Autowired private ScoreService scoreService; Test void insertScore_whenDuplicate_shouldThrow() { Score score new Score(); score.setStudentId(1L); score.setCourseId(1L); score.setSemester(2023-2024-1); score.setExamType(期末考试); score.setScore(85.5); scoreService.insertScore(score); assertThrows(DuplicateKeyException.class, () - scoreService.insertScore(score)); } }这个测试跑通意味着你的唯一索引和异常处理都是可预期的。Postman的用途则是验证HTTP层登录后拿到Session带上Cookie去访问受保护页面检查是否有权限拦截效果。把常见接口按正常调用 越权调用两条线过一遍。6.2 打包与启动的命令行细节打包这一步我强烈建议不要用IDE的绿色运行按钮。课程设计答辩现场正确的交付物是一个可执行的jar包你双击也好命令行也好能独立跑起来才叫完事。命令如下mvn clean package -DskipTests java -jar target/grademanage-0.0.1-SNAPSHOT.jar如果你懒得用Maven命令行IDEA右侧Maven面板里的package也能打包但效果一样。打包前记得改三处配置application.properties里数据库账号密码填你演示用的那套端口号不要固定8080或者至少允许--server.port8081覆盖如果数据库不在本机连接串要改成对方能访问的IP如果老师指定必须打war包部署到外部Tomcat你需要把启动类做改造SpringBootApplication public class GradeManageApplication extends SpringBootServletInitializer { Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(GradeManageApplication.class); } public static void main(String[] args) { SpringApplication.run(GradeManageApplication.class, args); } }同时pom.xml里把packagingjar/packaging改成war再把spring-boot-starter-tomcat的scope设为provided。这些细节很碎但现场出问题时会非常狼狈。提前准备好你答辩时只需打开命令行敲一行java -jar观感完全不同。6.3 演示数据、演示节奏和现场心态演示数据建议准备两套。第一套是常规数据20个学生、5门课程、每门课一个学期成绩尽量有高有低统计结果平均分、及格率你可以提前用Excel手算一遍演示时老师问这数字对吗你心里有底。第二套是边界数据一个缺考学生score为NULL、一个不及格学生、一个重复学号、一门暂无成绩的课程。这些数据专门用来演示异常处理是你和只会按流程跑的同学拉开差距的地方。演示节奏上我给一个经过验证的顺序登录页正常登录切换三种角色各登录一次学生视角查询本学期成绩演示学期筛选教师视角进入任课课程录入一条成绩再修改一次管理员视角组合查询 统计报表异常演示重复录入成绩、非法分数100、越权访问URL展示系统不会崩最后展示测试用例和项目结构整个流程控制在8-10分钟每个演示动作前心里默念一下下一步点哪里。很多学生翻车不是因为系统不行而是演示时手忙脚乱点了不该点的按钮。你提前练三遍肌肉记忆比背台词靠谱。如果现场问你以后这个系统怎么扩展这是一个很好的加分题。比较自然的回答是目前是单机系统如果后续要上生产我会考虑引入Redis缓存热点查询、用Spring Security替换手写拦截器、成绩导出改成异步任务。核心业务模型不变因为表和接口设计已经预留了扩展空间。 这一段话既展示了你的知识面又说明了你对现状有清醒的认知比空吹我用了微服务强得多。最后分享一个我自己的体会。学生成绩查询系统这个项目真正难的地方不在于Java语法、不在于框架API而在于把零散的页面、表、接口组织成一个有规则的整体。我见过很多同学拿到题目就到处搜现成源码恨不得整段复制结果到答辩时被问得哑口无言。反过来那些按我上面这套流程从需求表、数据库设计、分层结构一步步自己走下来的人哪怕功能少了点答辩时眼里有光老师也愿意给高分。你能把一个经典系统做出自己的味道这就是课程设计最大的收获。
返回列表