十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

SSM框架作业提交批改系统:权限控制与核心流程详解

SSM框架作业提交批改系统:权限控制与核心流程详解 简介基于SSM框架的作业提交与批改程序是一套面向高校计算机专业学生的Java毕业设计项目主要解决传统作业收发效率低、批改反馈不及时的痛点。系统按角色划分为管理员、学生和老师学生可维护个人资料、查看成绩并提交作业老师负责发布作业、批改打分管理员统筹系统用户、学生和老师的管理。技术栈采用Java、MySQL与SSM框架业务闭环完整适合课程设计、毕业设计答辩或SSM整合开发实战入门。资源包共470个文件以Java源码、JSP页面、class文件、XML配置和jar依赖为主同时包含CSS、JS前端资源及SQL数据库脚本压缩包大小约37.39MB。演示视频能帮助快速掌握部署运行流程源码中Controller、Service、Mapper层次分明便于阅读和二次开发适合用来自学排错或作为毕业设计的基础模板。已有123人浏览学习。1. 毕业设计选型为什么用 SSM 做作业提交与批改答辩时最怕被问SSM 框架在你这项目里到底做了什么 如果答不上来后面聊什么都虚。作业提交与批改程序就是用来把这个问题讲清楚的项目——学生传作业、老师批改、管理员管人和课功能不花哨但 Spring 的 Bean 管理、SpringMVC 的请求流转、MyBatis 的 SQL 映射全都用上了。它是适合已经入门 Java 但还没完整做过一个 WEB 项目的人拿来练手的源码也适合准备 Java 面试的人拿来做项目复盘。下文按我拆这个项目的顺序把数据库设计、登录鉴权、作业流转和部署排错逐段拆开。每一处都会写出参数和可能踩的坑而不是只贴类名。2. SSM 三层职责拆分与数据库表结构设计2.1 Spring、SpringMVC、MyBatis 各自的边界看过这个项目源码的人会看到一堆直接以功能命名的 ControllerZuoyexinxiController、ZuoyepingfenController、KechengxinxiController。这种命名不优雅但对毕业设计来说反而好讲类名即模块名。真正的理解难点在三者怎么配合请求先进 DispatcherServletSpringMVC 的 HandlerMapping 根据 RequestMapping 找到对应 Controller 方法Controller 里注入的是 Spring 容器管理的 ServiceService 再调 MyBatis 的 Mapper 接口。注意这里的 Mapper 接口没有实现类MyBatis 在启动时会用 JDK 动态代理为它生成一个代理对象调用方法名去匹配 XML 里的 SQL id。讲清楚这个答辩时就能解释 IoC 和 AOP 的实际位置IoC 体现在你不用担心 Service 在哪儿 new体系里统一交给 SpringAOP 体现在后续要做的登录拦截、事务控制上Spring 在代理层把逻辑插进去业务代码里看不到。这比背一句IoC 是控制反转要有说服力。2.2 数据模型六张核心表与角色关系从文件名能推断出项目至少包含课程信息、教师信息、学生信息、作业信息、作业评分、用户注册这几类数据。为了讲清角色关系我一般会把核心表拆成三组用户与角色、基础资料、作业流转。下面是一个常见的 MySQL 建表方案表名沿用项目里的拼音风格方便和 Controller 对应。CREATE TABLE allusers ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 登录名, pwd VARCHAR(64) NOT NULL COMMENT 密码, role VARCHAR(20) DEFAULT 管理员 COMMENT 角色标记, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明allusers 是项目里留给后台管理员登录用的表role 字段可以直接写成字符串管理员。因为管理员数量少密码散列后再存更好如果是课堂演示项目至少要对 pwd 做一次 MD5避免答辩现场被问密码是明文吗。参数上username 加唯一索引是为了防止注册时并发插入重复用户。学生和教师分别放在独立表里而不是和账号混在一张表因为这两个角色要存的字段不同学生需要学号、班级、专业老师需要工号、职称、所属院系。这样设计后登录表只负责认证业务表只负责资料两边通过唯一业务编号关联。作业相关的两张表最简设计如下CREATE TABLE zuoyepingfen ( id INT NOT NULL AUTO_INCREMENT, zuoye_id INT NOT NULL COMMENT 作业ID, xuesheng_id INT NOT NULL COMMENT 学生ID, score DECIMAL(5,1) DEFAULT NULL COMMENT 分数, pingyu VARCHAR(255) DEFAULT NULL COMMENT 教师评语, tigai_status TINYINT DEFAULT 0 COMMENT 0未批改 1已批改, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里我用 zuoyepingfen 对应项目里的 ZuoyepingfenController 含义。score 用 DECIMAL(5,1) 可以存 0.0 到 9999.9 的分值平时百分制足够。tigai_status 用来做批改状态流转比每次都去判断 score 是否为 null 要快也方便列表页显示待批改红点。下表汇总了核心表的作用与关键字段供设计时对照表名表用途关键字段与作业流程的关系allusers管理员/后台账号登录username, pwd, role独立于学生、老师的登录入口xueshengxinxi学生资料xuehao, name, banji作为作业成绩的外键来源jiaoshixinxi教师资料gonghao, name, zhicheng教师登录后关联课程与批改kechengxinxi课程信息kecheng, kechengmingcheng作业挂到具体课程下zuoyexinxi作业基本信息course_id, title, deadline提交与批改的参照zuoyepingfen评分记录zuoye_id, xuesheng_id, score一次提交对应一条评分记录表与表之间不要建太多物理外键MyBatis 里通过关联查询去维护逻辑关系更方便。原因有两个一是物理外键在批量导入和删除时会带来额外锁二是毕设项目通常会改字段逻辑外键改起来只需要动 XML 里的 SQL。2.3 用户体系合并allusers 与学生/教师表的分工很多 SSM 毕设会在一张 allusers 表里放所有角色的账号。这个项目既然单独有学生和教师表说明作者想让管理员、学生、老师走不同的注册入口。这个设计有它的代价学生表和教师表要各维护一份密码和 allusers 的密码逻辑重复。如果将来要接统一登录需要额外做账号绑定。我拆的时候会建议保留现有结构但把登录入口统一到同一个 Controller再多传一个用户类型参数。这样拦截器校验时只查会话里的 role 字段业务数据再从对应资料表读取。相比把所有角色塞进一张表然后靠 role 区分这种拆法更贴合真实项目里多身份系统的扩展方式。3. 基于拦截器的三角色登录鉴权与权限控制3.1 登录状态与角色标记登录逻辑看起来简单查用户名和密码比对成功就写 session。但角色标记必须和 session 一起写入否则后续每个请求都要重新查库。项目里 AllusersController 负责后台登录学生和老师走注册登录关键点在于登出时要让 session 失效防止用户按返回键绕过拦截。这里有个 Java 面试题里常考的点session 的存储依赖 Cookie 里的 JSESSIONID如果项目部署在集群环境默认 session 会有丢失风险。毕设不需要处理分布式会话但你要知道这个边界。我拆这个项目时看到 AllusersController 里有独立的登录验证方法。它先查用户名是否存在再比对密码。这里有一个小坑如果先查用户再比对密码查询结果为空时也要走同一个提示信息否则别人可以通过返回信息判断账号是否存在。和 SSM 的登录体系配合时最好把账号检查、密码校验、session 写入拆成三个私有方法每个方法只做一件事测试时也好写单元用例。3.2 自定义拦截器实现 URL 级防护SSM 中做权限拦截最常见的做法是继承 HandlerInterceptorAdapter 重写 preHandle在 SpringMVC 配置文件里注册要拦截的路径。下面是一个按角色校验的拦截器模板。public class LoginInterceptor extends HandlerInterceptorAdapter { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); String role (String) session.getAttribute(role); String uri request.getRequestURI(); // 放行登录、注册、静态资源 if (uri.contains(/login) || uri.contains(/register) || uri.endsWith(.js)) { return true; } if (role null) { response.sendRedirect(request.getContextPath() /login.jsp); return false; } // 细分角色管理员进 admin教师进 teacher学生进 student if (uri.contains(/admin/) !管理员.equals(role)) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return false; } return true; } }说明preHandle 返回 false 表示中断请求后续 Controller 不会执行。这里先放行所有未带路径规则的 URL再用前缀规则挡住敏感目录。注意登录页本身不要被拦截否则会出现跳到登录页又要求登录的死循环。response.sendRedirect 需要写上下文路径直接用 request.getContextPath() 拼避免部署时改了应用名导致跳转 404。配置注册拦截器时注意 SpringMVC 的 mvc:interceptors 声明顺序先声明的拦截器先执行。如果同时配置了多个拦截器preHandle 按声明顺序执行postHandle 相反这个顺序在排查为什么没进拦截器时很有用。另一个坑是拦截器默认拦截所有路径包括 SpringMVC 的静态资源处理所以静态资源要单独放行否则页面样式全红。提示拦截器做权限控制只能防手滑不能防恶意攻击。真正的权限校验要在 Service 层再做一次对象归属判断。毕业设计答辩时如果能主动说出这层面试官会认为你理解层次不同。3.3 权限矩阵与注册入口过滤在这个项目里管理员、教师、学生三类角色的操作范围差别很大。我一般会在设计文档里先画一张权限矩阵再写进代码注释方便答辩时展示URI 前缀管理员教师学生/admin/**允许拒绝拒绝/teacher/**允许允许拒绝/student/**允许拒绝允许/zuoyepingfen/**只读批改不可见上面的拦截器只做了前缀校验业务方法内部的细粒度控制还需要配合一个简单注解比如定义RequiresRole(教师)。因为拦截器拿的是请求路径注解拿的是方法级签名二者互补路径拦截挡住不该来的 URL注解挡住参数越权。比如某个学生把请求 URL 改成 /zuoyepingfen/updateScore即使路径匹配方法内也要再校验当前 session 的 role 或对象的归属关系。注册入口也要拦截。项目里有 YonghuzhuceController说明学生和教师是通过注册进来。如果放开发布通常要加图形验证码和邮箱验证在毕设里至少要用一个防止机器人提交的隐藏字段。做这些不是丢功能而是展示你对网络安全的基础认知。4. 作业上传、批改与成绩回写的核心流程实现4.1 学生作业提交表单、文件上传与 SQL 写入作业提交的关键是文件上传和关联表插入。SSM 中通常用 MultipartFile 接收文件先判断文件大小和类型再写到服务器本地磁盘。文件名要重新生成不能直接用用户原始文件名否则不同班级上传同名文件会互相覆盖。生成规则我习惯用当前时间戳加下划线再加原文件名。Controller RequestMapping(/student) public class StudentZuoyeController { Value(${upload.path}) private String uploadPath; PostMapping(/submitZuoye) public String submit(RequestParam(zuoyeId) Integer zuoyeId, RequestParam(xueshengId) Integer xueshengId, RequestParam(file) MultipartFile file, RedirectAttributes attr) throws Exception { if (file.isEmpty()) { attr.addFlashAttribute(msg, 请选择要上传的文件); return redirect:/student/zuoyeList; } String original file.getOriginalFilename(); String newName System.currentTimeMillis() _ original; File target new File(uploadPath File.separator newName); // 作业目录不存在时先创建 if (!target.getParentFile().exists()) { target.getParentFile().mkdirs(); } file.transferTo(target); // 插入作业提交记录 zuoyeService.saveSubmit(zuoyeId, xueshengId, newName); attr.addFlashAttribute(msg, 提交成功); return redirect:/student/zuoyeList; } }说明Value 从 properties 读取上传目录避免把绝对路径写死在代码里。transferTo 是 MultipartFile 提供的标准方法底层会处理临时文件清理如果用了 getInputStream 手动读记得在 finally 里关闭流。RedirectAttributes 用来实现重定向后的消息传递比直接 forward 更符合 PRGPost/Redirect/Get模式刷新页面时不会重复提交作业。这里要注意文件路径必须存到数据库下载时重新拼接服务器路径而不是把客户端完整路径存进去否则换浏览器文件就丢了。项目的 VeDate 类在这个环节也派得上用场它就是封装日期时间格式化的工具判断截止时间、计算提交超时都可以用它转字符串后比较做演示时不会出现时间格式不统一的问题。4.2 教师批改评分更新与成绩回写教师进入待批改列表看到的是学生提交的文件名、提交时间点击批改后填写分数和评语。批改动作对应的 SQL 通常是 update而不是先删后插。因为一条提交记录在没有批改时就已经存在教师反复保存只更新 score 和 pingyu。UPDATE zuoyepingfen SET score #{score}, pingyu #{pingyu}, tigai_status 1, p_person #{teacherId}, p_time NOW() WHERE zuoye_id #{zuoyeId} AND xuesheng_id #{xueshengId}分别解释这几个参数score 是教师输入的百分制分数pingyu 是评语tigai_status 置为 1 表示已批改p_person 记录批改教师 ID用于追责和统计工作量NOW() 由数据库生成时间避免各服务器本地时钟偏差。WHERE 条件同时包含 zuoye_id 和 xuesheng_id是因为一对提交记录只有这两个字段能唯一确定一行如果只按 zuoye_id 更新会把一个班所有学生的作业都改成同一个分数。在 Service 层执行这个更新时要注意事务边界。批改除了更新评分表往往还要更新作业表的批改进度字段两步操作如果没加 Transactional第二步抛异常时第一步已经提交数据就不一致了。我把这个场景单独拿出来分析是因为它在面试里常被包装成一个学生提交和教师批改同时发生怎么办的问题。4.3 成绩列表的关联查询与状态机学生端查看成绩时需要同时展示作业名称、课程名称、得分、评语、教师姓名。这些数据分布在四张表里zuoyepingfen、zuoyexinxi、kechengxinxi、jiaoshixinxi。MyBatis 里用联表查询一次拿回来比逐条 set 再查要稳定。public ListZuoyeScoreVO getScoreList(Integer studentId) { return zuoyeMapper.selectScoreByStudentId(studentId); }select idselectScoreByStudentId resultTypecom.example.vo.ZuoyeScoreVO SELECT f.score, f.pingyu, f.tigai_status, z.title AS zuoyeTitle, c.kechengmingcheng AS courseName, j.name AS teacherName FROM zuoyepingfen f LEFT JOIN zuoyexinxi z ON f.zuoye_id z.id LEFT JOIN kechengxinxi c ON z.kecheng_id c.id LEFT JOIN jiaoshixinxi j ON f.p_person j.id WHERE f.xuesheng_id #{studentId} ORDER BY f.create_time DESC /select参数说明studentId 是当前登录学生的 IDVO 里的字段用别名对齐 SQL 返回的列名MyBatis 会自动映射驼峰属性到带下划线的列。这里用 LEFT JOIN 是为了防止教师还没批改时联到 jiaoshixinxi 上没有记录导致整行丢失。成绩状态建议做成一个简单的常量枚举0 未提交、1 已提交待批改、2 已批改、3 打回重交。在列表页用状态数字切换按钮而不是用嵌套 if 散落逻辑代码才能短。成绩列表常常遇到的性能问题是 N1先查到学生的所有提交记录再循环里逐条查作业名和课程名。上面的 SQL 把关联放到一次查询里一次 IO 完成这是最直接的优化。如果数据量超过十万再考虑在建索引时把 xuesheng_id 与 zuoye_id 的联合索引加上。5. 部署排错与 MyBatis 日志调优5.1 中文乱码与上传大小配置SSM 项目部署到 Tomcat 后最常见的问题是中文乱码。三个位置都要检查JSP 的 pageEncoding、SpringMVC 的 CharacterEncodingFilter、MySQL 的连接字符集。连接串里加上 characterEncodingutf8useSSLfalse 能解决大部分乱码源。上传大作业文件时要注意 Tomcat 的 maxPostSize 和 SpringMVC 的 multipart 参数是两层先看控制台报的FileUploadException是超大小还是临时目录不存在。5.2 Transactional 失效与事务边界自调用是 Transactional 失效的重灾区同类里一个方法调另一个 Transactional 方法时代理对象没有经过注解不生效。拆项目时建议把批改更新和进度更新放在不同 Service 或通过注入自身代理调用来解决。另外 MySQL 的 MyISAM 引擎不支持事务建表要确认是 InnoDB。还有把 checked 异常抛出去时Spring 默认不回滚需要 rollbackForException.class检查事务是否生效最简单的办法是直接在方法里抛一个 RuntimeException看数据库有没有变回去。5.3 打开 MyBatis SQL 日志定位问题调试作业批改流程时最有效率的手段是把 MyBatis 打印的 SQL 打开。在 logback 或 log4j 里做如下配置logger namecom.example.mapper levelTRACE/这行配置的含义是对 com.example.mapper 包下的所有 Mapper 接口开启 TRACE 级日志控制台会滚动输出每条 SQL 的 Preparing、Parameters 和 Total 时间。配合 4.2 的更新语句你能直接看到参数是否被正确绑定也能看出是 WHERE 条件没拼对还是参数类型不匹配。把映射包名换成你项目实际的 mapper 包即可。参数顺序问题会在这里暴露得很明显MyBatis 报There is no getter for property named时先检查 XML 里 #{} 的名字和参数对象的属性是否一致再检查是否用了 Param 注解。日志里如果看到 LIMIT 后面出现字符串八成是 MySQL 方言配置没生效。把 stdOutLogging 打开后回到演示视频里的批改流程用日志对照一遍基本上一条作业成绩查不对的问题半小时内能定位清楚。本文还有配套的精品资源点击获取
返回列表