java Web方向做毕设,选什么题一直是很多同学纠结的地方。如果不想碰太冷门的技术,又希望系统有完整业务场景、有权限控制、有报表统计、答辩能讲清楚,高校学生竞赛成果管理系统是一个很稳的选题。这类基于Web的系统,核心是把学生学科竞赛的申报、审核、统计全流程搬到线上,正好卡在Java Web项目“比增删改查高级一点,又不至于复杂到做不完”的位置。这篇把我做这个项目的完整思路、表结构、关键实现和踩坑记录都写出来,给打算选这个题的同学一个可以直接参考的版本。
1. 项目定位:这个系统到底解决什么问题
1.1 竞赛成果管理到底管什么
高校里的学科竞赛成果,包括“挑战杯”、数学建模、程序设计大赛、电子设计大赛等等,每年数量不小。过去很多学校是靠学生填Excel表、发邮件给辅导员,再由学院教务员汇总成一张张大表,报到教务处。看起来很常规,实际一操作全是火坑:学生填的获奖名称五花八门,文件名格式不统一,证书图片要么不传要么传错,同一份成果学院报了一遍、学生自己又交一遍,教务处统计时还得手动去重。竞赛成果管理系统要解决的就是这些具体问题,把“学生申报—教师审核—管理员终审—数据统计”这条线做成闭环。
这一类系统的典型用户分三类:
- 学生:登录后填报个人竞赛成果,上传证书、成绩单附件,查看审核进度。
- 指导教师/学院审核人:审核本学院学生的申报材料,通过或驳回,驳回时写原因。
- 系统管理员/教务处人员:管理竞赛信息、审核最终结果、按学院按年份按竞赛级别统计导出。
整个系统的业务核心并不是信息展示,而是状态流转。一份成果从草稿到提交、待审核、审核通过、被驳回、定稿归档,每一步都要有明确的状态和角色操作边界。
1.2 为什么这个选题性价比高
从毕设选题的角度,这个项目有几个很实际的优势。
第一,业务场景真实。竞赛成果管理在几乎所有高校都存在,需求不是凭空捏造的,你做出来的功能点每一处都能找到对应的现实场景,写开题报告和论文时素材非常充足。
第二,技术点覆盖度适中。它有单点登录、JWT或Session会话管理、RBAC权限控制,也涉及文件上传、表单校验、父子表数据维护,再加上统计报表的聚合查询,能放进论文里的技术亮点不少。
第三,工作量可控。这个系统没有复杂的算法,没有高并发,没有消息队列,核心就是CRUD加状态机加统计,一个人做一个月到两个月完全来得及,不像电商系统那样容易做成一锅粥。
我也见过有同学擅自加了一堆功能,比如站内信、在线聊天,结果自己都不知道怎么讲,答辩时把重点放在无关功能上,反而把主线的审核流程讲淡了。做毕设,先保证主线清晰,这是最重要的一条原则。
1.3 角色权限划分先行
做这个系统之前,先把权限边界想清楚,因为后面的菜单、接口、页面都要围绕角色来设计。我在项目里定义了三种角色,没有再细分。
| 角色 | 核心操作 |
|---|---|
| 学生 | 申报成果、修改未审核信息、查看审核结果 |
| 审核教师 | 查看本学院学生申报、审核或驳回、按学院查询统计 |
| 管理员 | 竞赛信息维护、终审、全校统计、用户管理 |
这里要注意一个细节:指导教师到底按学院过滤还是按指导老师本人过滤,需求里要定义清楚,否则代码改起来很麻烦。我做的是按学院过滤,因为大部分高校的竞赛审核确实是学院教务办统一负责的。
2. 技术选型:从后端到前端的一次性规划
2.1 后端框架选择
主流的组合有两种:SSM(Spring + SpringMVC + MyBatis)和 Spring Boot。现在做毕设我建议直接用 Spring Boot,原因不是SSM不行,而是Spring Boot把配置简化了,项目结构更清爽,答辩时你能用更多时间讲业务逻辑而不是讲XML配置。
版本上注意一下,我用的是 Spring Boot 2.7.x + JDK 1.8。为什么不追新?因为学校机房、老师的运行环境很多还是JDK8,你用一个JDK17的版本,现场演示时环境配不起来是很尴尬的。字节码版本过高导致无法启动的问题,我在帮别人看项目时遇到过太多次。
持久层我用 MyBatis,很多教程推 MyBatis-Plus,说实话MyBatis-Plus在开发效率上确实高,BaseMapper自带CRUD,省很多时间,条件构造器写统计查询也方便。如果代码量控制得好,这个选题用 MyBatis-Plus 和 MyBatis 区别不大,你觉得哪个把握大就选哪个,关键是理清楚表关系和SQL逻辑。
2.2 前端和服务端渲染
前端我用的 Bootstrap + jQuery + Thymeleaf 服务端渲染,统计页面用 ECharts 画图表。选这套方案是因为它是毕设场景里容错率最高的组合。前后端分离对毕业设计不是不行,但你需要额外处理跨域、前端构建、开发环境依赖Node.js,现场演示时要同时开前端服务和后端服务,不确定性多。
服务端渲染的思路是后端Controller返回视图名称,Thymeleaf负责把数据渲染到HTML里。它比JSP好在语法简洁,比Vue项目好在不需要构建工具,对Java背景的同学友好很多。页面用Bootstrap栅格布局,整体观感不会差,而且响应式兼容,老师用不同分辨率的屏幕演示也不会翻车。
2.3 后端目录结构与分层
我的后端按经典三层架构来组织,controller、service、mapper一层层分清楚。讲解时需要理由充分。
com.example.competition ├── controller # 接口和页面跳转控制 ├── service # 业务逻辑,事务边界在这里 ├── mapper # MyBatis的Mapper接口 ├── entity # 数据库实体对象 ├── dto # 视图模型,避免实体直接暴露给前端 ├── config # 拦截器、WebMvc配置、文件上传配置 ├── interceptor # 登录/权限拦截器 ├── common # 统一结果封装、异常处理、工具类 └── CompetionApplication.java这样一个结构,答辩时老师问“你的项目怎么分层的”,你可以很清晰地答出controller不写业务、service管事务、mapper只做数据访问,每一层职责明确。
2.4 环境准备清单
开发环境我列一个对照表,第一次接触的同学可以照着准备:
| 组件 | 推荐版本 | 备注 |
|---|---|---|
| JDK | 1.8 | 兼容性最好 |
| IDE | IntelliJ IDEA 2024 | 社区版够用 |
| MySQL | 5.7 或 8.0 | 8.0 注意驱动名不同 |
| Maven | 3.8+ | 配置阿里云镜像加速 |
| Spring Boot | 2.7.x | 稳定且教程多 |
| 前端 | Bootstrap 4 + jQuery + ECharts | 不需要额外构建工具 |
IDEA 2024 创建Web项目时,直接选 Spring Initializr,依赖勾选 Spring Web、Thymeleaf、MyBatis(选MyBatis-Plus就自己加依赖)、MySQL Driver。不需要手动建Web目录,Spring Boot内置Tomcat,这点和传统打war包的老项目不一样。
3. 数据库设计:竞赛成果系统的地基
3.1 核心表结构
数据库设计是这个项目最值得花时间的部分。表建得好,后面所有查询都很顺;表里有明显冗余或者缺字段,后期就得返工。
我设计了六张核心表:
用户表 tb_user
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint 主键自增 | 用户ID |
| username | varchar(50) 唯一 | 学号/工号 |
| password | varchar(255) | MD5或BCrypt加密保存 |
| real_name | varchar(50) | 真实姓名 |
| role | tinyint | 0学生 1教师 2管理员 |
| college_id | bigint | 所属学院ID |
| phone | varchar(20) | 联系方式 |
| create_time | datetime | 创建时间 |
学院表 tb_college
字段就三个,id、name、code。这里我多说一句,学院id是很重要的外键,学生和教师都关联到学院,后面“按学院统计”全靠这个字段做关联。
竞赛信息表 tb_competition
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 竞赛ID |
| name | varchar(200) | 竞赛名称 |
| level | tinyint | 0国家级 1省级 2校级 |
| organizer | varchar(200) | 主办单位 |
| competition_date | date | 比赛时间 |
| register_deadline | date | 报名截止日期 |
| remark | varchar(500) | 备注 |
成果申报表 tb_result,这是全系统最核心的表。
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 成果ID |
| student_id | bigint | 申报学生用户ID |
| competition_id | bigint | 关联竞赛 |
| award_name | varchar(200) | 获奖名称 |
| award_level | tinyint | 一等奖/二等奖/三等奖/优秀奖 |
| award_date | date | 获奖日期 |
| certificate_no | varchar(100) | 证书编号 |
| attachment_id | bigint | 证书附件ID |
| status | tinyint | 0草稿 1待审核 2已通过 3已驳回 |
| submit_time | datetime | 提交时间 |
| audit_user_id | bigint | 审核人用户ID |
| audit_time | datetime | 审核时间 |
| audit_reason | varchar(500) | 审核意见/驳回原因 |
附件表 tb_attachment
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 附件ID |
| result_id | bigint | 关联成果ID |
| file_name | varchar(200) | 原始文件名 |
| file_url | varchar(500) | 存储路径 |
| file_size | bigint | 文件大小(字节) |
| upload_time | datetime | 上传时间 |
审核日志表 tb_log
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 日志ID |
| user_id | bigint | 操作人 |
| action | varchar(50) | 操作行为,如submit、approve、reject |
| target_id | bigint | 操作对象,如成果ID |
| detail | varchar(500) | 操作说明 |
| create_time | datetime | 操作时间 |
3.2 字段类型与命名上的细节
用户名、角色、状态这些尽量用数字编码而不是字符串,因为字符串一旦拼接或者拼写不一致,统计就会出错。比如角色用0/1/2,成果状态用0/1/2/3,后面写SQL时用数字比较即可。
时间字段统一用datetime,不要在Java端用字符串传时间,容易出时区问题。MySQL 8.0连接时jdbcUrl要设置serverTimezone,例如:
jdbc:mysql://localhost:3306/competition_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false3.3 去重校验的逻辑
竞赛成果系统特别容易反复提交。同一个学生同一个竞赛同一个奖项,理论上只能申报一次。这块我的做法是在tb_result表上加了唯一索引:
alter table tb_result add unique index uk_student_competition (student_id, competition_id);同时Service层在新增时先做一次查询校验,给用户明确提示“该竞赛成果已申报,请勿重复提交”。数据库索引是最后一道防线,业务代码里先拦一道,双保险。
4. 核心功能实现:申报、审核、统计一条线
4.1 登录与权限控制
这个项目的登录用Session即可,不需要上Spring Security,除非你想把安全框架作为一个答辩亮点。我用的是一个简单的拦截器加角色判断,实现容易讲清楚,代码量也不大。
拦截器主要做两件事:检查用户是否登录,检查当前角色的访问范围。
public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); User user = (User) session.getAttribute("loginUser"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login"); return false; } String requestUri = request.getRequestURI(); if (requestUri.startsWith("/admin/") && user.getRole() != 2) { response.setStatus(403); return false; } if (requestUri.startsWith("/teacher/") && user.getRole() == 0) { response.setStatus(403); return false; } return true; } }从代码可以看到,我是按URL前缀区分角色区域的。生产系统会用更细粒度的方法,但毕设以逻辑清晰、能答辩为主,这个设计足够。
4.2 成果申报与文件上传
学生的申报页面是一个表单加一个文件上传控件。这里有两个非常常见的坑,我提前提示一下:
第一,文件不能直接扔到项目根目录。很多人做文件上传,直接存成D:/temp/xxx.jpg,然后把绝对路径存进数据库,妥妥的大型翻车现场。因为项目换台电脑部署,路径就不对了。正确做法是配置一个本地上传目录,把相对路径存到数据库,访问时通过虚拟映射来读取。
在application.yml中配置:
upload: dir: ${user.dir}/uploads/然后加一个WebMvc配置,把本地目录映射成URL路径:
@Configuration public class WebConfig implements WebMvcConfigurer { @Value("${upload.dir}") private String uploadDir; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/uploads/**") .addResourceHandler("file:" + uploadDir); } }这样文件保存下来后,数据库中只需要记录/uploads/2025/03/xx.pdf这样的路径,浏览器直接可以访问,换服务器也没问题。
第二,文件名一定要重命名。学生上传的证书图经常是“微信图片_202405138123.jpg”这种,文件名包含中文和空格,路径里出现这些字符有时候会404。我的做法是统一用UUID或时间戳重命名,后缀保留原扩展名:
public String storeFile(MultipartFile file) { String original = file.getOriginalFilename(); String ext = original != null ? original.substring(original.lastIndexOf(".")) : ""; String newName = UUID.randomUUID().toString().replace("-", "") + ext; // 按日期分目录存储 String datePath = new SimpleDateFormat("yyyy/MM").format(new Date()); File dir = new File(uploadDir + datePath); if (!dir.exists()) dir.mkdirs(); file.transferTo(new File(dir, newName)); return "/uploads/" + datePath + "/" + newName; }4.3 审核流程的状态机
审核流程是业务核心,也是最值得写进论文的模块。状态流转定义清楚了,代码逻辑就清晰了:
学生提交后,status从0变为1;教师审核时,可以改为2通过,也可以改为3驳回;管理员终审同理。驳回必须填写原因,这些原因在学生的详情页展示。
审核的Service方法必须加事务控制,否则审核状态改了但日志没写进去,出了问题无法追溯。这段代码我简化一下:
@Transactional(rollbackFor = Exception.class) public void audit(Integer resultId, Integer status, String reason, User operator) { Result result = resultMapper.selectById(resultId); if (result == null || result.getStatus() != 1) { throw new BizException("当前成果不在待审核状态,请刷新后操作"); } // 审核人只能处理本学院的成果 if (!result.getCollegeId().equals(operator.getCollegeId()) && operator.getRole() != 2) { throw new BizException("无权限审核该成果"); } resultMapper.updateStatus(resultId, status, reason, operator.getId(), new Date()); logMapper.insert(new Log(operator.getId(), "audit", resultId, status == 2 ? "审核通过" : "驳回:" + reason, new Date())); }这个状态机看起来简单,但实际开发时你还会遇到一个业务边界问题:被驳回的成果,学生修改后是直接回到待审核状态,还是重新走一遍流程?两种都行,但必须保证每次审核的日志都有据可查。我采用的是驳回后学生可以修改再提交,提交后status重新变为1。
4.4 统计分析模块的实现
统计功能是最容易出效果的部分,也是答辩时最吸引老师的点。我用ECharts做了两个统计图:一个是“各学院获奖数量柱状图”,一个是“竞赛级别占比饼图”,再加一张按年份、学院、级别组合条件查询的明细表。
统计查询的SQL要提前写好,演示时才不会卡壳。柱状图的SQL大概是:
select c.name as college_name, count(r.id) as award_count from tb_college c left join tb_user u on u.college_id = c.id left join tb_result r on r.student_id = u.id and r.status = 2 group by c.id, c.name order by award_count desc注意这里用了left join而不是inner join,目的是把没有获奖记录的学院也查出来,数量为0也要显示在图表上,图形完整才不突兀。
饼图的逻辑是按竞赛级别统计通过成果数量:
select case when (select level from tb_competition cc where cc.id = r.competition_id) = 0 then '国家级' when (select level from tb_competition cc where cc.id = r.competition_id) = 1 then '省级' else '校级' end as level_name, count(*) as cnt from tb_result r where r.status = 2 group by level_name子查询的性能其实不需要担心,数据量小,重点是想清楚关联关系。
5. 常见问题与排查实录
5.1 IDEA 2024创建Web项目的坑
现在很多同学用IDEA 2024创建项目,如果直接选了默认的Spring Boot 3.x,那JDK最低要求是17。如果你的机器装的是JDK8,项目启动直接报错。建议创建项目时把Spring Boot版本降为2.7.x,或者先把JDK换成17。不要在这个问题上纠结,毕设稳定压倒一切。
创建成功后,如果发现没有application.yml文件,项目一样能跑,但建议自己新建一个,配置文件统一放这里,比properties格式看着舒服,层级也清晰。
5.2 端口被占用
Spring Boot默认端口是8080,如果被其他程序占了,启动日志会报Port 8080 was already in use。解决办法很简单,两种:
一是找到占用进程杀掉,Windows命令:
netstat -ano | findstr 8080 taskkill /pid 1244 /f二是在application.yml里改端口,比如改成8081:
server: port: 8081我建议用第二种,项目不会因为环境差异现场翻车。
5.3 中文乱码问题
中文乱码在项目里通常有两个来源。一个数据库连接没设置utf8,这个用我之前写的带characterEncoding的jdbcUrl就能解决。另一个是文件上传时中文文件名乱码,这个在文件重命名后就不存在了,因为你根本不用原始文件名做存储。还有一点,如果使用Thymeleaf模板,页面中文乱码记得在HTML的meta标签统一设置UTF-8。
5.4 上传的文件在浏览器里打不开
这个问题出现频率相当高。如果数据库存的是绝对路径,比如D:\java_project\uploads\1.jpg,浏览器访问时是没法直接拼接成URL的。你需要做的是相对路径加映射,即我在4.2里写的addResourceHandlers方式。如果页面重写了WebMvcConfigurer导致映射失效,注意@EnableWebMvc注解不要乱加,它会关闭Spring Boot的自动Web配置,静态资源全部失效。
5.5 分页查询总数据量不对
列表页加分页时,学生端只能看自己的,教师端只能看本学院的,但管理员可以看到全部。这里的权限过滤如果只写在SQL的where条件里,分页count查询也要带上同样的条件。很多人count和list两个SQL条件没对齐,就会导致“总条数和实际数据对不上”的问题。
6. 答辩讲解与项目亮点提炼
6.1 答辩时候最值得讲的四个点
答辩现场老师问你的问题不会太多,但会集中在“这个系统有什么亮点”“你做了哪些工作”上。我建议从这四个方向准备:
第一,权限设计。讲清楚为什么用三种角色、怎么通过拦截器实现的权限控制、为什么管理员和教师看到的菜单不同。可以进一步解释RBAC模型,哪怕只是用到了一部分,也能说明你有设计思维。
第二,状态机与审核流程。讲清楚一份成果的完整生命周期:草稿—提交—审核—通过/驳回,哪个角色在哪个状态能做什么操作,为什么用事务保证数据一致性。
第三,文件上传与访问方案。讲清楚为什么不存绝对路径,怎么做到换一台机器部署依然能访问附件。这个点虽然不复杂,但能体现你对实际部署场景的考虑。
第四,统计分析的SQL设计。讲清楚怎么用聚合查询加条件筛选得到图表数据,为什么用left join保持数据完整,在数据量较小时避免子查询的替代方案。
6.2 项目演示的准备技巧
演示这个系统时,我建议准备一组演示数据,把每个角色的操作流程都过一遍。比如事先用管理员账号建好三个竞赛,用学生账号提交一份成果并上传一张证书图片,教师账号审核通过,再提交一份驳回的,到管理员账号终审,然后打开统计页面展示图表变化。整个流程跑通,比背代码更能说服人,老师看到的是一个能实际运转的管理系统,而不是一个半成品。
纸面项目报告里,我建议把表结构放到附录,把核心SQL和审核流程放到正文,论文的核心逻辑围绕“申报—审核—统计”展开。很多同学的论文把大量篇幅写环境搭建,但那些东西不是论文重点,业务设计和流程实现才是。
7. 最后再聊几句
做这个项目时,我有一个很深的体会:毕设项目不需要炫技,需要的是完整。把登录、分页、文件上传、审核流、统计报表这几条线扎扎实实走通,再配上清晰的项目结构和测试数据,就已经超过大多数同类选题了。你不需要把所有新技术都堆上去,而是要把自己写过的每一行代码的逻辑都讲明白,对老师提出的每一个“为什么”都能接得住。
如果你正打算用这个题目做毕设,建议先把表结构设计好,再动手写代码。表结构就是地基,地基稳了,后面的开发会顺很多。也建议在设计表结构时就把“唯一索引防重复申报”和“附件相对路径存储”考虑进去,这两处细节会在你后期开发时帮你省下不少返工的时间。祝你项目顺利。