1. 项目核心需求拆解与整体设计思路
1.1 竞赛管理究竟在解决什么问题
做了这么多年开发,我见过太多高校里的竞赛组织方式还停留在“Excel表格 + 微信群 + 人工统计”的阶段。报名信息靠接龙,作品提交靠邮箱,评审打分靠纸质表格汇总,成绩公示靠贴公告栏。大学生竞赛管理系统的核心价值,就是把这个链条上的每一个环节搬到线上,让管理员、指导教师、参赛学生三方都在同一个平台上协作,避免“通知靠转发、统计靠熬夜”的尴尬局面。
从我拿到这个项目需求的第一反应来看,它至少应该覆盖以下几条核心业务线:
- 管理员端:创建竞赛、设置报名时间窗口、分配评审专家、审核报名资格、管理作品归档、发布最终成绩与公示文件。
- 教师/评审端:查看被分配的作品、在线打分与填写评语、查看历史评审记录。
- 学生端:浏览竞赛公告、在线组队报名、上传参赛作品、查看审核状态与最终排名。
这个定位决定了系统不是一个简单的 CRUD 练习,而是要处理“多角色权限 + 竞赛全生命周期 + 评审打分流程”这些真实业务逻辑。毕业设计选这个题目有一个天然优势——业务场景足够具体,需求文档好写,数据库表结构能设计出水平,代码也能展示出一定的复杂度,不会像“图书管理系统”那样千篇一律。
1.2 技术选型:为什么锁死 Spring Boot 全家桶
这个项目标题直接点名了 Spring Boot,这既是毕业设计的主流选择,也是企业级 Java 开发的事实标准。选择 Spring Boot 而不是 Servlet + JSP 或者 SSM 手写配置,核心原因有三个:
第一,开发效率极高。Spring Boot 的自动配置机制帮我们省掉了大量 XML 配置。以前 SSM 项目要写数据源配置、MyBatis 工厂、事务管理器、视图解析器,Spring Boot 只需要在application.yml里写几行参数,其余交给自动配置完成。这就是热词里大家反复搜“springboot自动装配原理”的原因——它确实是这个框架最核心、也最能体现设计水平的机制。
第二,生态极其成熟。权限控制可以用 Spring Security 或 Sa-Token,持久层可以用 MyBatis-Plus,接口文档可以用 Knife4j,文件存储可以用 MinIO 或本地磁盘。这个项目所有需要的能力在 Spring Boot 生态里都有现成的成熟方案,我们不需要去造轮子。
第三,前后端分离是当下主流。毕设如果想要亮点,建议不要做传统的服务端渲染页面,而是用 Spring Boot 做纯后端接口,搭配 Vue 3 + Element Plus 做管理后台,再配一个移动端适配的 H5 页面给学生使用。这也是热词里“基于springboot vue购物管理系统”这类项目非常多的原因——前后端分离已经是标准姿态。
我实际做这个项目时采用的技术栈清单如下,供直接参考:
| 层次 | 选型 | 说明 |
|---|---|---|
| 后端框架 | Spring Boot 2.7.x | 稳定且资料多,避免直接用 3.x 踩坑 |
| 持久层 | MyBatis-Plus 3.5.x | 自带分页插件和条件构造器,省时省力 |
| 权限认证 | Sa-Token 1.34+ | 比 Spring Security 易上手,适合毕设展示 |
| 数据库 | MySQL 5.7 / 8.0 | 5.7 兼容性更好,8.0 功能更强 |
| 文件存储 | 本地磁盘 + Nginx 映射 | 毕设阶段不需要上 MinIO,简单可靠 |
| 接口文档 | Knife4j 4.x | 自动生成 Swagger 文档,答辩加分 |
| 前端 | Vue 3 + Element Plus + Vite | 管理后台开发效率高,组件丰富 |
提示:如果你的毕业设计时间特别紧张,后端直接用 Spring Boot 2.7.x 而不是 3.x,因为网上能搜到的资料和踩坑记录绝大多数都是基于 2.x 的。等你的项目稳定跑通了,再去折腾升级不迟。
1.3 角色权限体系的顶层设计
竞赛管理系统的用户角色比一般的管理系统多一层复杂性。我把它分为三类共四种角色,数据库层面用role字段区分,权限拦截在接口层面做:
- 系统管理员(admin):用户管理、竞赛创建、全局配置、评审分配。
- 竞赛负责人(manager):可以理解为二级管理员,负责某个具体竞赛的报名审核、作品归档、成绩发布。
- 评审专家(reviewer):只能看到分配给自己的待评审作品,打分提交后不可随意修改。
- 参赛学生(student):注册登录、浏览竞赛、报名参赛、上传作品、查看成绩。
有一个设计细节需要注意:如果初期不想把角色分得太细,可以先用admin和student两种角色把主流程跑通,后面再扩展manager和reviewer。但数据库表设计时一定要预留角色字段的扩展空间,不要用布尔类型的is_admin字段,否则改造成本很高。
2. 数据库表结构设计与关联建模
2.1 核心数据表清单
数据库设计是这个项目的灵魂。我见过太多毕设项目代码写得还行,但数据库表设计一塌糊涂——该拆的表不拆,该建的索引不建,关联查询全是笛卡尔积。竞赛管理系统的数据模型其实非常典型,可以归纳为“用户-竞赛-报名-作品-评审”五张核心表,外加公告、字典、日志等辅助表。
下面是我实际抽出来的表结构核心字段,按业务重要性排列:
用户表sys_user
CREATE TABLE `sys_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录账号', `password` varchar(100) NOT NULL COMMENT 'BCrypt加密后的密码', `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名', `student_no` varchar(30) DEFAULT NULL COMMENT '学号', `teacher_no` varchar(30) DEFAULT NULL COMMENT '工号', `role` varchar(20) NOT NULL DEFAULT 'student' COMMENT '角色:admin/manager/reviewer/student', `email` varchar(100) DEFAULT NULL, `phone` varchar(20) DEFAULT NULL, `status` tinyint(1) DEFAULT '1' COMMENT '1启用 0禁用', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;用户名必须唯一索引,这个不用多解释。角色字段用字符串而不是数字枚举,是为了代码里可读性更强,this.role.equals("student")一眼就能看懂。
竞赛表comp_competition
竞赛表是整个系统的核心主数据,它需要记录竞赛的基础信息,并支撑状态流转。关键的字段设计如下:
competition_name竞赛名称,比如“第十七届全国大学生智能汽车竞赛校内选拔赛”。level竞赛级别,包括校级、市级、省级、国家级,这个字段后面做统计报表时很有用。type竞赛类型,比如学科竞赛、创新创业、文体比赛,方便前台分类展示。signup_start_time和signup_end_time报名时间窗口,系统要在这个时间段内开放报名按钮,过期自动关闭。contest_start_time和contest_end_time竞赛时间。max_team_members最大组队人数,单人参赛设为 1。cover_image封面图 URL。status状态字段,对应草稿、报名中、评审中、已结束,用0/1/2/3四个数字表示。
核心建表 SQL 骨架:
CREATE TABLE `comp_competition` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `competition_name` varchar(200) NOT NULL, `level` varchar(20) DEFAULT 'school', `type` varchar(50) DEFAULT NULL, `description` text COMMENT '竞赛简介', `cover_image` varchar(255) DEFAULT NULL, `signup_start_time` datetime DEFAULT NULL, `signup_end_time` datetime DEFAULT NULL, `contest_start_time` datetime DEFAULT NULL, `contest_end_time` datetime DEFAULT NULL, `max_team_members` int(11) DEFAULT '1', `status` tinyint(1) DEFAULT '0' COMMENT '0草稿 1报名中 2评审中 3已结束', `create_by` bigint(20) DEFAULT NULL COMMENT '创建人ID', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_status` (`status`), KEY `idx_signup_end` (`signup_end_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;status和signup_end_time建索引,是因为系统高频查询场景是“找出所有报名中且未截止的竞赛列表”,这两个字段联合起来走索引扫描效率极高。
2.2 报名、作品与评审的关联建模
报名表comp_signup
报名表解决的是“谁参加了哪个竞赛”的问题。设计时有一个关键决策点:一个学生报名一个竞赛,是创建一条记录,还是团队所有成员各自创建一条记录?我推荐一个团队一条主报名记录,团队成员用子表存储,这样后期审批操作针对的是“队伍”而不是个人。
主表字段:id、competition_id、captain_id(队长用户ID)、team_name(团队名称)、status(待审核/已通过/已驳回)、signup_time。子表comp_signup_member字段:id、signup_id、user_id、role_in_team。主表和子表通过signup_id关联,一对一报名单人参赛时子表只插入一条记录。
这个设计的好处是:查询“某个竞赛有多少队伍报名”只需要count主表;查询“某个学生参加了哪些竞赛”需要 join 子表反查,SQL 也能写得很干净。
作品表comp_work
作品表和报名表是 1:1 的关系。一个通过审核的报名记录,对应一份作品提交记录。字段大致如下:
CREATE TABLE `comp_work` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `competition_id` bigint(20) NOT NULL, `signup_id` bigint(20) NOT NULL, `work_title` varchar(200) DEFAULT NULL, `work_type` varchar(50) DEFAULT NULL COMMENT '论文/实物/软件/视频', `summary` text COMMENT '作品摘要', `file_url` varchar(500) DEFAULT NULL COMMENT '文件下载地址', `submit_status` tinyint(1) DEFAULT '0' COMMENT '0未提交 1已提交 2已撤回', `submit_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_signup` (`signup_id`), KEY `idx_competition` (`competition_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里注意signup_id上加了唯一索引,防止一份报名记录提交多份作品。业务层面还要再加一层校验:只有报名状态为“已通过”的队伍才能上传作品。
评审表comp_review
评审表的设计最能体现这个项目的专业性。它的关联关系是:一个评审专家被分配到某个竞赛的多份作品,每份作品可以由多个专家打分,最终成绩取平均分。因此设计两张表:comp_review_task(评审任务分配表)和comp_review_score(打分记录表)。
任务分配表字段:id、competition_id、reviewer_id、work_id、assign_time、status(待评审/已完成)。打分表字段:id、task_id、reviewer_id、work_id、score(百分制)、comment(评语)、review_time。一张任务记录只能对应一条打分记录,通过task_id加唯一索引控制。
2.3 状态流转的设计心得
状态管理看起来简单,但实际踩坑最多。我强烈建议竞赛状态、报名状态、作品提交状态全部用显式数字枚举,并且在代码里写成常量类或枚举类,而不是散落各处的魔法数字。比如:
public class CompetitionStatus { public static final int DRAFT = 0; public static final int SIGNING = 1; public static final int REVIEWING = 2; public static final int FINISHED = 3; }竞赛的完整状态流转是:草稿(DRAFT) -> 报名中(SIGNING) -> 评审中(REVIEWING) -> 已结束(FINISHED)。报名结束后系统自动进入评审中,管理员分配评审任务后,评审专家才能看到待评审列表。这个流转可以在管理员的“结束报名”按钮里触发,也可以用 Spring 的@Scheduled定时任务扫描时间自动触发。我建议毕设阶段用按钮触发更直观,也方便演示时控制节奏。
3. 核心功能模块的代码实现与要点解析
3.1 登录认证与权限控制
认证方案我选择了 Sa-Token,理由很简单:对于前后端分离项目,Sa-Token 的登录鉴权代码量比 Spring Security 少得多,而且中文文档友好,答辩时你能把每个方法的原理说清楚。
登录接口核心逻辑
@RestController @RequestMapping("/api/auth") public class AuthController { @Autowired private SysUserService userService; @PostMapping("/login") public Result login(@RequestBody LoginDTO dto) { SysUser user = userService.getByUsername(dto.getUsername()); if (user == null) { return Result.error("用户名不存在"); } // BCrypt 密码校验 if (!BCrypt.checkpw(dto.getPassword(), user.getPassword())) { return Result.error("密码错误"); } if (user.getStatus() == 0) { return Result.error("账号已被禁用,请联系管理员"); } StpUtil.login(user.getId()); // 返回 token 和用户基本信息 return Result.ok(StpUtil.getTokenInfo()); } }Sa-Token 的StpUtil.login()会在服务端创建一个会话,并把 token 返回给前端。前端把 token 存在 localStorage 中,之后每个请求在请求头里带上satoken这个字段即可。
权限拦截器配置
@Configuration public class SaTokenConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor(handle -> { StpUtil.checkLogin(); })).addPathPatterns("/**") .excludePathPatterns("/api/auth/login", "/api/auth/register", "/api/competition/list", "/api/competition/detail/**"); } }这里有一个容易被忽略的细节:如果使用 Sa-Token 默认的拦截器,所有请求都会强制登录,所以必须把“竞赛列表查询”和“竞赛详情查询”这两个前台页面不需要登录就能访问的接口放进白名单。否则游客连竞赛公告都看不了,体验很差。
3.2 竞赛发布与报名模块
竞赛发布是管理员功能。核心代码并不复杂,就是往comp_competition表插入一条记录,同时做几个参数校验:报名结束时间要晚于当前时间,竞赛开始时间要晚于报名结束时间。这里要注意的是时间字段的格式化处理,前端传过来的是"2025-05-20 18:00:00"这样的字符串,后端用@DateTimeFormat注解接收后转成LocalDateTime。
报名模块的核心难点在于防重复提交和状态校验。一个学生不能对同一竞赛重复报名,这个不能只靠前端按钮置灰,后端必须做兜底校验。我当时的实现逻辑是:
public Result signUp(SignUpDTO dto) { // 1. 校验竞赛是否存在且在报名时间内 CompCompetition comp = competitionMapper.selectById(dto.getCompetitionId()); if (comp == null || comp.getStatus() != 1) { return Result.error("当前不在报名时间范围内"); } LocalDateTime now = LocalDateTime.now(); if (now.isBefore(comp.getSignupStartTime()) || now.isAfter(comp.getSignupEndTime())) { return Result.error("报名时间已截止"); } // 2. 队长查重,防止重复报名 Long userId = StpUtil.getLoginIdAsLong(); long count = signupMapper.selectCount(new LambdaQueryWrapper<CompSignup>() .eq(CompSignup::getCompetitionId, dto.getCompetitionId()) .eq(CompSignup::getCaptainId, userId)); if (count > 0) { return Result.error("你已报名该竞赛,请勿重复操作"); } // 3. 成员查重,防止一个学生被多个团队拉入同一竞赛 if (dto.getMemberIds() != null && !dto.getMemberIds().isEmpty()) { long memberCount = signupMemberMapper.selectCount(...); if (memberCount > 0) { return Result.error("团队中存在已报名该竞赛的成员"); } } // 4. 插入主记录和成员子记录,事务由@Service加@Transactional保证 }这段代码里面,步骤 2 和步骤 3 是两个很容易遗漏的校验点。很多毕设项目只做了步骤 2,结果出现“一个学生被 A 队和 B 队同时拉进同一竞赛”的数据脏问题。答辩时把这个坑点主动讲出来,反而是加分项。
3.3 作品上传与文件存储
文件上传这个功能看起来简单,但真要做好有几个细节值得注意。首先是存储路径的规划。我在application.yml中做了如下配置:
file: upload-path: /data/contest-files/ access-path: /files/**后端把文件写到/data/contest-files/目录,文件名用 UUID 重新命名,保留原始文件扩展名,然后通过一个文件访问的WebMvcConfigurer把/files/**映射到物理路径:
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/files/**") .addResourceLocations("file:" + fileUploadPath); }这样前台上传成功后拿到的是/files/xxx.pdf这样的相对路径,拼接上后端地址就能访问。为什么要用 UUID 重命名?因为学生上传的作品文件名可能是“最终版(1)(2)(3).zip”这种千奇百怪的名字,直接落盘可能触发操作系统文件名特殊字符问题,UUID 统一命名可以彻底规避。
文件类型和大小校验也必须做。毕设阶段至少要限制扩展名为zip/rar/pdf/doc/docx/jpg/png/mp4,单文件大小上限建议100MB,可以在application.yml里配置 Spring MVC 的上传限制:
spring: servlet: multipart: max-file-size: 100MB max-request-size: 100MB3.4 评审打分模块与成绩统计
评审模块是竞赛管理系统区别于普通报名系统的最核心功能,也是我写代码时最花心思的地方。业务流程如下:
- 管理员在竞赛进入“评审中”状态后,进入评审分配页面,选择评审专家,勾选需要分配的作品,点击“批量分配”。
- 系统为每对“专家-作品”生成一条
comp_review_task记录。 - 评审专家用自己的账号登录,只能看到分配给自己的、状态为“待评审”的任务列表。
- 点击“评审”按钮,进入打分页面,填写分数和评语,提交后任务状态变为“已完成”。
分配任务的代码逻辑里有一个注意点——重复分配问题。如果同一作品已经分配给同一专家,再次点击分配需要给出友好提示而不是直接报错。我当时的做法是插入前先查重,存在则跳过,并在返回结果里提示“3条任务已创建,2条跳过(重复分配)”。
成绩统计的核心 SQL 是分组聚合查询:
SELECT work_id, ROUND(AVG(score), 2) AS avg_score, COUNT(*) AS review_count FROM comp_review_score WHERE competition_id = #{competitionId} GROUP BY work_id ORDER BY avg_score DESC这里有一个业务约定需要跟用户确认:最终成绩是取平均分还是去掉最高最低分后的均值?如果竞赛规模较大,专家打分尺度可能不一致,去掉最高最低分更稳妥。我实现时在系统参数表里加了一个review_mode配置项,默认取平均分,管理员可以切换模式。这个设计体现了你对业务的理解深度,面试或答辩时能讲出这个东西就是亮点。
3.5 前台展示页面的接口设计
学生端前台页面虽然技术上不复杂,但接口设计要尽量贴合使用习惯。首页轮播图下方是“进行中的竞赛”列表,每张卡片展示竞赛封面、名称、级别、报名截止时间倒计时。这个列表的查询接口需要返回每个竞赛的已报名队伍数,如果用 Java 代码遍历查询每个竞赛的报名数,会出现 N+1 查询问题,性能很差。
更好的做法是写一条关联子查询:
SELECT c.*, (SELECT COUNT(*) FROM comp_signup s WHERE s.competition_id = c.id) AS signup_count FROM comp_competition c WHERE c.status = 1 ORDER BY c.signup_end_time ASCMyBatis-Plus 的QueryWrapper不支持这种子查询,但可以结合@Select注解写在 Mapper 方法里。对于毕设项目,数据量几百条,这种 SQL 的性能完全没问题,而且语义清晰。
4. 开发与部署中的常见问题排查实录
4.1 Spring Boot 版本与依赖冲突
我在开发过程中遇到的第一个大坑就是版本问题。如果直接用 Spring Initializr 生成项目,默认可能是 Spring Boot 3.x,而 3.x 把javax.servlet换成了jakarta.servlet,很多网上搜到的老代码片段直接拷贝会报程序包javax.servlet不存在的错误。我的建议是毕设项目统一用 Spring Boot 2.7.x,等以后工作了再慢慢拥抱 3.x。
另一个高频问题是 MyBatis-Plus 和 Spring Boot 的兼容性。MyBatis-Plus 3.5.x 需要对应的mybatis-plus-boot-starter版本,不要装错成mybatis-plus(那是非 Spring Boot 版本)。如果启动时报Error creating bean with name 'xxxMapper',优先检查 Mapper 接口上是否加了@Mapper注解,或者在启动类上加了@MapperScan。
4.2 分页查询的坑
MyBatis-Plus 的分页需要先配置MybatisPlusInterceptor:
@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }忘记配这个 Bean 会导致page方法返回的total恒为 0,但数据能查出来——这是个非常隐蔽的 bug。排查方式是看控制台 SQL 日志,如果 SQL 后面没有LIMIT ?,基本就是拦截器没生效。
4.3 定时任务与状态自动流转
我用@Scheduled实现了一个每 30 秒执行一次的定时任务:扫描所有状态为“报名中”但当前时间已超过signup_end_time的竞赛,把状态改为“评审中”。这里要特别注意,定时任务所在的类上要加@EnableScheduling注解才能生效。另外,定时任务方法里要加日志输出,方便排查“时间到了但状态没变”的问题。
4.4 文件上传跨域与路径问题
前后端分离模式下,前端把文件 POST 到后端的/api/file/upload接口,最容易遇到跨域问题。如果前端是 Vite 开发服务器(默认端口 5173),后端是 8080 端口,必须解决跨域。我用的是全局 CORS 配置类:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }还要注意一个问题:allowCredentials(true)和allowedOriginPatterns("*")必须搭配使用,如果用了allowedOrigins("*")会与allowCredentials(true)冲突,导致浏览器拦截响应。
4.5 打包部署和演示环境的坑
毕设最终需要提交可运行的项目包。我建议用 Maven 打包成 jar 包,在服务器上用java -jar方式运行,不要用 IDEA 直接运行来演示——答辩现场网络和数据库环境不可控,提前在服务器上打好包、连好数据库,演示时用nohup java -jar contest-system.jar &启动,再把前端dist目录放到 Nginx 下并配置反向代理:
location /api/ { proxy_pass http://localhost:8080/api/; } location /files/ { proxy_pass http://localhost:8080/files/; }注意 Nginx 配置里的proxy_pass路径末尾是否有斜杠,这个是老生常谈的坑了。带斜杠表示替换匹配到的前缀,不带斜杠则保留原路径,搞错了接口会 404。
5. 答辩演示技巧与源码学习方法
5.1 演示流程如何设计才出彩
我见过很多同学做毕设,项目功能做得挺全,但演示时东点一下西点一下,老师看得一头雾水。结合我做这类项目的经验,演示流程建议按“一条完整业务线”来走,效果最好:
第一步,用管理员账号登录,创建一个“2025年度大学生创新大赛”,设置报名起止时间,系统进入“报名中”状态。
第二步,切换到学生账号,浏览到刚创建的竞赛,组队报名(现场添加两个成员),上传一份作品文件。
第三步,切回管理员账号,审核报名通过,结束报名,系统进入“评审中”状态。
第四步,给评审专家分配任务,换个账号登录评审端,在线打分提交。
第五步,管理员查看成绩排名,发布最终结果,学生端能看到自己的排名。
这条流程走下来,五个核心模块(用户管理、竞赛管理、报名管理、评审管理、成绩管理)全部展示到了,评委能很直观地看到系统的完整性。
5.2 拿到源码之后怎么快速上手
标题里提到“附源码”,很多同学拿到源码第一个动作是急着改代码。我的经验是,先别急着写任何代码,花一下午把这三件事做完:第一,看数据库脚本,把表结构和字段含义弄明白,这是理解业务最快的方式;第二,看启动类和相关配置文件,搞清楚端口号、数据库连接、文件路径都配在什么地方;第三,跑通一次完整业务流程,从前台注册到后台配置,再到学生报名,亲手点一遍,远比读代码高效。
然后才是“按功能点去读代码”,从 Controller 层看接口,从 Service 层看业务逻辑,从 Mapper 层看 SQL。不急着全看懂,先把登录认证这一条线(Aspect/Interceptor -> Controller -> Service -> Mapper)理清楚,后面就顺了。
5.3 从毕设到真实项目的差距在哪里
最后说几句实在话。竞赛管理系统作为毕业设计,难度和工作量都恰到好处,但如果你以后想走开发这条路,要知道它和企业级项目的差距主要在三块:分布式事务(报名和扣减库存这种跨库操作)、高并发削峰(报名瞬间大量写入)、容器化交付(Docker + K8s)。这些在毕设阶段不一定需要做,但可以在论文的“不足与展望”里提一下,体现你有技术视野。
我在开发这个系统的过程中踩过的坑已经整理在上面的“问题排查实录”中了——版本冲突、分页拦截器丢失、跨域与凭证冲突、Nginx 反代路径斜杠,每个都是实际项目中高频出现的问题。把这些经验消化成自己的东西,无论答辩还是将来面试,聊起来都会比别人多一分底气。