Spring Boot高校学科竞赛平台开发实战

1. 项目拆解:高校学科竞赛平台到底在解决什么问题

做这个项目之前,我其实先观察了一个很现实的现象:很多高校的学科竞赛管理还停留在“Excel登记 + 群公告通知 + 人工统计”的阶段。学生要找到适合自己的竞赛项目,得反复翻通知群;老师要收集报名信息,得一个个汇总;管理员要统计全校的参赛数据,更是费时费力。你说它不能跑吗?能跑,但效率确实低,而且数据一多就容易出错。

所以这个“基于Spring Boot的高校学科竞赛平台”,本质上就是在做一个信息流转和业务闭环的数字化底座。它要解决的核心问题有三个:

第一,把竞赛信息从一个被动通知变成主动展示。学生不用再翻聊天记录,登录系统就能看到正在报名的竞赛、截止时间、参赛要求。第二,把报名审核流程从线下来回跑变成线上多角色协作。学生提交报名,指导老师审核,竞赛负责人确认,每一步都有留痕。第三,把数据从Excel孤岛变成结构化存储。谁报了什么赛、哪个学院参赛率最高、历年获奖情况如何,全部可以统计和回溯。

从技术角度看,这个项目选择了Spring Boot作为后端框架,我觉得是非常合理的选择。Spring Boot最大的优势在于低成本地整合主流的Java生态组件——Spring MVC处理接口、Spring Security做权限控制、MyBatis或Spring Data JPA操作数据库,只需要简单的配置就能跑起来。对于高校场景来说,Spring Boot也是Java技术栈里最主流的选择,后续无论交给学生维护还是交给公司开发继续扩展,团队的熟悉度都有保障。

这个项目适合谁去研究?我觉得有两个人群最合适。一种是正在做毕业设计或课程设计的计算机相关专业学生,因为这个项目麻雀虽小五脏俱全,涵盖了权限、CRUD、审核流程、文件上传等大量通用模块,很适合作为完整工程能力的练手项目。另一种是学校信息中心或教务部门的老师、技术人员,想自建一套竞赛管理工具,用来替代手工表格。前者能学到的是一种标准化的业务系统开发范式,后者能获得一套可以直接运行、二次修改的骨架。

我自己的体会是,这类项目最核心的价值不在于代码本身有多惊艳,而在于它完整地示范了“一个真实的业务需求是如何通过分层架构落地成代码”的过程。所以这篇文章我不会只讲设计思路,会把每一个关键模块的实际实现细节、表结构设计、踩过的坑都拿出来讲一遍。

2. 系统整体设计与技术选型思路

2.1 角色权限模型设计:三种角色如何影响整个系统架构

高校学科竞赛平台里有一个天然的多角色协作场景。我梳理下来,至少需要三类角色:学生、教师、管理员。有些学校还会细化出“竞赛负责人”或“学院管理员”这样的子角色,但核心权限模型可以先从三角色起步。

学生的操作范围很明确:浏览竞赛公告、查看竞赛详情、在线报名、上传作品或论文附件、查看自己的报名状态和成绩公示。教师的操作范围是:审核自己所带学生的报名信息、录入或修改参赛成绩、发布竞赛相关通知。管理员的权限最重:维护竞赛项目分类、发布竞赛通知、指定竞赛负责人、管理所有学生的报名记录、审核教师提交的成绩、导出全校数据报表。

这三类角色直接决定了系统的权限控制策略。我采用的是RBAC(基于角色的访问控制)模型,也就是用户-角色-权限三层结构。具体落地时,你在数据库里要有user表、role表、user_role关联表,再加上权限点(可以细到某个接口、某个按钮)。Spring Security负责拦截请求,登录成功之后把当前用户的所有权限点加载进来,每次访问受限资源时做判断。

设计权限模型时有一个典型的坑:很多同学会把角色信息硬编码到if else里,比如if("admin".equals(role))。这种做法在小demo里问题不大,但一旦教师兼任管理员、或者新增一个学院管理员角色时,你就要改业务代码。所以我在实际设计时会把“角色”和“权限”分离,代码里只判断“你有没有这个操作权限”,而不是判断“你是不是某个角色”。权限数据放到数据库里可动态维护,这样后续增加角色不需要改动代码逻辑。

2.2 后端技术栈与Java版本选择

后端这块,我的选型是这样的:

  • Spring Boot 2.7.x:稳定成熟的一个版本,社区资料多,遇到问题容易搜到解决方案。如果你的JDK用的是8或11,这个版本非常合适。如果你想上Spring Boot 3.x,那JDK需要17以上,这个看你的部署环境来决定。
  • MyBatis-Plus:做单表CRUD非常省事,比如自动填充创建时间、逻辑删除、分页查询这些高频功能,MyBatis-Plus都有现成的注解和插件支撑。
  • MySQL 8.0:InnoDB引擎,utf8mb4字符集。
  • Redis(可选):如果项目需要做会话共享、缓存热点数据,可以引入。如果学校机房部署条件有限,不引入也完全够用。
  • Maven:项目依赖管理和打包,必须用。

这里说一个很多教程没提的点:Spring Boot的版本和JDK的版本一定要匹配。我见过很多同学把Spring Boot 3.x的代码copy到JDK8的环境里跑,一启动就报UnsupportedClassVersionError。这个坑排查起来其实不难,但特别耗时间。所以在项目最开始就要把环境版本对齐,别贪新。

前端方面,如果重点是渲染后端能力,直接用一套基于Bootstrap或Element UI的管理后台模板(比如用Thymeleaf服务端渲染)就够了。如果你想做成前后端分离的形式,前端可以选Vue 3 + Element Plus + Axios,后端只提供JSON接口。两种方案我都实践过,对于毕业设计展示项目来说,我会更倾向于前后端分离,因为答辩的时候前端交互效果更直观,而且Vue生态是现代前端的主流,对后续找工作也有帮助。

2.3 前端页面结构与学生端体验设计

从实际使用场景看,学生端是访问量最大、频次最高的,它的体验很大程度上决定了整个平台的口碑。很多系统做出来老师不爱用、学生不愿意用,原因就是页面太丑、操作太绕。

学生端首页应该展示什么?我的设计是有三个核心模块:正在进行中的竞赛推荐、赛事通知公告、个人报名状态。

竞赛列表页是整个系统的第一个关键页面。学生需要快速筛选出“适合自己”的赛事,所以筛选条件很重要。我的表设计里竞赛有category(竞赛分类)、level(国家级/省级/校级)、status(报名中/已截止/已结束)三个核心维度。列表页上做成可以按分类、级别、状态过滤的筛选栏,比纯粹按时间倒序排更实用。

报名页是操作最重的页面。学生要填写的信息包括:团队/个人参赛类型、团队成员名单、指导教师、参赛项目说明等。如果竞赛需要提交作品,还需要支持附件上传。这个页面的设计要注意交互提示,比如某个字段是否必填、上传格式限制是什么、报名截止时间还剩多少天,这些信息在页面上要给足,不要等学生提交的时候才知道有问题。

管理端的页面相对简单,核心就是数据表格:报名列表按赛事分组、审核状态筛选、批量操作(批量通过/驳回)、成绩录入弹窗、数据导出按钮。我做管理端的时候有一个原则:能用表格解决的事情不要做复杂的可视化图表,因为管理员的日常任务是快速处理数据,不是做数据分析。等你把表格做得足够顺手了,再考虑增加统计图表模块。

3. 数据库设计与核心表结构说明

3.1 从业务需求推导表结构设计的全过程

数据库设计是这个项目里最不能糊弄的部分。很多代码写得很漂亮的项目,数据库表设计得一塌糊涂,后面做统计、做扩展的时候会非常痛苦。我讲一下我是怎么从业务需求一步步推导出表结构的。

第一步,梳理核心实体。这个系统里,最重要的实体有这些:用户(学生/教师/管理员是用户的不同角色,在RBAC模型里不拆表)、竞赛、竞赛分类、报名单、成绩、公告、附件。

第二步,确定实体之间的关系。一个竞赛分类下有多个竞赛,一对多;一个竞赛下有多个报名记录,一对多;一个学生可以报名多个竞赛,一个竞赛也可以有多个学生报名,这是多对多。多对多关系落到数据库里,需要一个关联表(比如student_competition表,或者更业务化的叫signup_record表,把报名的附加信息都放在这张表里)。一个竞赛最终有一个成绩结果,可以单独建成绩表,也可以在报名表上扩展成绩字段。我建议单独建成绩表,因为成绩可能有初赛/复赛/决赛多个阶段,放在一张Extension表里更好扩展。

第三步,细化字段。以用户表为例,基础字段是id、用户名、密码(加密存储)、姓名、学号/工号、学院、专业、邮箱、手机号、头像地址、账号状态(正常/禁用)、创建时间、更新时间。以竞赛表为例,字段是id、竞赛名称、竞赛分类id、竞赛级别、主办单位(这里指校内组织单位)、竞赛简介、报名开始时间、报名结束时间、竞赛开始时间、竞赛封面图、创建人id、状态、创建时间、更新时间。以报名表为例,字段是id、竞赛id、学生id、团队名称(可选)、指导教师id(可选)、报名说明、附件路径列表(JSON格式存多个)、审核状态(待审核/通过/驳回)、审核意见、审核人id、审核时间、提交时间。

这里面有一个经验点:涉及到“状态”的字段,我强烈建议用数字枚举值(比如0待审核、1通过、2驳回)而不是直接存中文,同时在代码里定义常量类或枚举类对应。这样做的好处是数据库查询的时候用数字判断速度快,而且避免出现“待审”、“审核中”、“已审核”这种同义不同形的脏数据。

3.2 核心表的SQL实战参考

下面是我实际项目里的一张核心报名表SQL,你可以直接参考这个结构去建库:

CREATE TABLE `signup_record` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '报名记录ID', `competition_id` bigint(20) NOT NULL COMMENT '所属竞赛ID', `student_id` bigint(20) NOT NULL COMMENT '报名学生ID', `team_name` varchar(100) DEFAULT NULL COMMENT '团队名称(个人参赛可空)', `teacher_id` bigint(20) DEFAULT NULL COMMENT '指导教师ID', `project_intro` text COMMENT '项目说明/参赛作品说明', `attachment_paths` varchar(1000) DEFAULT NULL COMMENT '附件路径,多个用英文逗号分隔', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '审核状态:0待审核 1已通过 2已驳回', `audit_comment` varchar(500) DEFAULT NULL COMMENT '审核意见', `auditor_id` bigint(20) DEFAULT NULL COMMENT '审核人ID', `audit_time` datetime DEFAULT NULL COMMENT '审核时间', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '提交时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_competition` (`competition_id`), KEY `idx_student` (`student_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='竞赛报名记录表';

几个细节解释一下:

attachment_paths字段我设计成了varchar存英文逗号分隔的文件路径。这种设计适合附件数量有限(比如最多3个)的业务场景,查出来之后在Java里用split拆一下就好。如果你需要为附件做细粒度的管理(比如每个附件都要单独审核、单独下载计权限),那就要单独建一张attachment表。竞赛平台里我选择了冗余存储,主要是为了简化数据模型。

索引设计上,我加了三组索引:按竞赛查、按学生查、按状态查。这三组索引覆盖了95%以上的查询场景。很多同学在建表的时候不关心索引,等到数据量到十万级才开始补救,那时候回头加索引,成本和风险都比一开始设计高不少。

3.3 为什么附件存储要单独规划

这个点我在很多项目的实施过程中被反复教育过,所以必须单独拿出来说。竞赛报名通常需要上传作品文档、项目计划书、演示图片,甚至视频。附件存储方案有几种:

最简单的是把文件直接存到服务器本地目录,数据库只存路径。这种方式部署简单,但文件没有备份机制,服务器挂了文件就没了。而且如果后续系统用Docker部署,容器重建时本地文件会丢失。

稍微好一点的是用云存储,数据库里存的是云上的URL。好处是访问速度快、有CDN可用,扩展性好。坏处是需要额外配置云服务密钥,内网部署的场景下不可用。

我最终的做法是:设计一个FileController提供统一的上传和下载接口,本地存储路径通过application.yml配置,在Linux环境下把路径指到独立的挂载盘或者Docker的volume目录。这样即使应用重新部署,文件也还在。如果你要做分布式部署(多实例),再把存储层替换成MinIO或者FastDFS这类分布式文件系统,这个我在后文会再说。

4. 实战搭建:从空项目到可运行系统

4.1 初始化Spring Boot项目并配置核心依赖

我直接用IDEA的Spring Initializr来创建项目。注意,在这一步就把下列核心依赖勾选好,省得后面手动补:

  • Spring Web
  • Spring Security
  • MyBatis-Plus(Initializr里没有的话,手动在pom.xml添加依赖)
  • MySQL Driver
  • Lombok
  • Validation

关键的pom.xml依赖长这样:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.14</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

我遇到的一个入门级高频问题是:mybatis-plus 3.5.x的包名从com.baomidou变成了com.baomidou.mybatisplus,一些老教程里用的是老包名。这个虽然不是大问题,但如果直接复制老代码就会编译报错。所以建议直接用我上面列的这个最新版本坐标。

4.2 application.yml中容易踩的坑

核心配置大概是这个结构:

server: port: 8080 servlet: context-path: /api spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/competition_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password servlet: multipart: max-file-size: 50MB max-request-size: 200MB mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

这里有一个必须提前处理的细节:MySQL 8.x的驱动类名是com.mysql.cj.jdbc.Driver,而MySQL 5.x时代用的是com.mysql.jdbc.Driver。很多教程没讲清楚,如果你数据库是8.x却用了老驱动名,启动时会有WARN但还能跑;反过来你数据库是5.x却用8.x驱动,就可能会出问题。版本对齐是这类问题最常见的根源。

另一个坑是context-path这个配置。我刻意把接口前缀设置为/api,这样前端所有请求都走/api路径,将来如果要做网关统一代理或者接口版本控制,处理起来方便很多。但这种模式下,Spring Security的路径匹配也要带上/api前缀才能生效。

4.3 实体类与通用返回结构的封装

实体类的设计我采用和数据库字段一一对应的方式。以竞赛实体为例:

@Data @TableName("competition") public class Competition { @TableId(type = IdType.AUTO) private Long id; private String name; private Long categoryId; private Integer level; private String organizer; private String description; private LocalDateTime registerStartTime; private LocalDateTime registerEndTime; private LocalDateTime competitionTime; private String coverImage; private Long createUserId; private Integer status; private LocalDateTime createTime; private LocalDateTime updateTime; }

这里我不用多说Lombok的@Data注解,重点说一个容易被忽略的设计:通用返回结构。我封装了一个结果类,所有接口统一返回这个类型:

@Data public class ApiResult<T> { private Integer code; private String message; private T data; public static <T> ApiResult<T> success(T data) { ApiResult<T> result = new ApiResult<>(); result.code = 200; result.message = "success"; result.data = data; return result; } public static <T> ApiResult<T> error(Integer code, String message) { ApiResult<T> result = new ApiResult<>(); result.code = code; result.message = message; return result; } }

统一返回结构的好处,有过前后端联调经验的人最有体会。如果每个接口的返回格式都不一样(有的返回data直接是对象,有的直接返回字符串),前端处理逻辑就会很痛苦,永远在写if else判断返回类型。统一之后,前端只要判断code是不是200就够了。

4.4 登录认证与token机制详解

这部分是整个系统的核心,Spring Security的配置也都在这里。我推荐使用JWT(JSON Web Token)方式做无状态认证。为什么不用传统的Session?因为Session依赖服务端存储,如果将来要做多实例部署或者前后端分离,Session的跨节点共享就是个麻烦事。

JWT的设计思路是这样的:用户登录成功后,服务端生成一个包含用户id、用户名、角色信息的令牌,把令牌返回给前端。前端每次请求时在HTTP头部带上Authorization: Bearer 。服务端有一个过滤器拦截请求,校验token的合法性和有效期,从token里解析出当前用户信息。

Spring Security的过滤器链配置是这样组织的:

@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/api/auth/**").permitAll() .antMatchers("/api/competition/list", "/api/competition/detail/**").permitAll() .antMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated() .and() .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }

一个很重要的细节我放在这里讲:密码加密务必使用BCryptPasswordEncoder,不要用MD5。MD5这种散列算法天生不适合做密码存储,因为它计算太快,暴力破解的成本极低。BCrypt内置盐值,同一密码两次加密后的结果不同,安全性要高很多。Spring Security默认就提供了BCrypt实现,直接用就行。

在实际开发中我还发现一个比较隐蔽的问题:JWT token生成的时候,我经常把整个用户对象放进去。这是不对的。Token里只应该放必要的信息(用户id、用户名、角色的字符串标识),不要放手机号、邮箱这些用户敏感信息。因为JWT虽然防篡改,但它是header.payload.signature三段式结构,中间那段payload只是Base64编码,前端decode一下就能看到内容。如果放了敏感字段,等于把用户信息明文暴露在了前端。

5. 核心功能模块的实现全解析

5.1 竞赛管理模块:发布、编辑、上下架完整流程

竞赛管理是管理员的核心操作场景。我从竞赛发布开始梳理。

管理员进入后台,点击“新增竞赛”,需要填写的信息包括:竞赛名称、竞赛分类(从下拉框选择)、竞赛级别、主办单位、竞赛简介(富文本)、报名起止时间、竞赛开始时间、封面图上传。这些字段提交之后会在后端Controller接收成Competition实体。我在这里做了一个校验,发布之前必须确认报名截止时间晚于当前时间,否则学生没法在规定时间内报名。

从上代码角度讲,核心的保存逻辑不复杂,就是插入一条记录。但聪明的做法是把这个操作设计成状态驱动。竞赛的状态我分为三个:0未发布(草稿)、1报名中、2已结束。当管理员创建了一个竞赛但还没点发布时,状态是0,学生端不展示。点击发布后,状态变成1,前端首页就能看到了。到了设置的报名截止时间,系统定时任务或者懒加载检查时会把状态更新为2。这种状态机的设计让“发布-报名-结束”的流程变得很清晰。

这里分享一个我实际遇到的需求:“某个竞赛的报名时间已经过了截止日期,但因为特殊情况需要重新开放报名。”按照状态机设计,管理员只需要在后台把报名截止时间改成未来时间,同时把状态改成报名中,一键实现。如果是硬编码的if else逻辑,这种需求往往要改代码才能搞定。

5.2 报名模块:数据校验、防重复提交与文件上传

学生报名模块是用户操作频率最高的地方,我总结出三个最常见的问题,每个都值得单独处理。

第一个问题是重复报名。同一个竞赛,学生手抖点了两次提交,就生成了两条报名记录。这个问题我在数据库层面做了约束:在signup_record表上建一个唯一索引,字段是competition_id和student_id。这样数据库层面上就保证了同一学生对同一竞赛只能有一条报名记录。加上代码层面的先查询再插入,双重保险。

第二个问题是报名数据的合法性校验。比如报名截止时间已经过了,或者竞赛状态不是报名中,学生就不应该能提交。这些校验不能只在前端做,因为前端的校验可以被绕过。后端在Controller接口里需要主动查一次竞赛状态,时间不在有效范围内直接返回业务错误。我在代码里专门写了一个报名校验方法,把这些规则集中在一起:

private void validateSignup(Competition competition) { if (competition == null) { throw new BusinessException("竞赛不存在或已被删除"); } if (competition.getStatus() != 1) { throw new BusinessException("当前竞赛不在报名阶段"); } LocalDateTime now = LocalDateTime.now(); if (now.isBefore(competition.getRegisterStartTime()) || now.isAfter(competition.getRegisterEndTime())) { throw new BusinessException("当前不在报名时间范围内"); } }

第三个问题是文件上传。上传接口我会单独做一个file/upload接口,接收MultipartFile,返回文件路径。前端在上传附件的时候,先调用这个接口拿到URL,再随报名参数一起提交。这样报名主流程不需要处理二进制流,逻辑更干净。上传接口里要限制文件类型和大小。常见的做法是白名单限制扩展名,不要用黑名单。我限定的就是jpg、png、pdf、docx、zip、rar这几个类型,其他的一律拒绝。文件保存的目录结构用日期分目录,便于管理和排查:

/upload/2025/01/16/20250116103022_1234.pdf

5.3 成绩管理模块:教师录入与公示可见性控制

成绩管理模块有个比较微妙的地方——可见性控制。竞赛成绩在公示之前不应该对所有人可见,而公示之后学生应该能在自己的报名记录里看到成绩。

我设计的流程是:竞赛管理员在后台指定一位或多位教师作为评审,评审教师登录后可以从“待录入成绩”列表看到自己负责的竞赛学生报名单,然后逐条录入成绩(按分数或者按奖项等级,比如一、二、三等奖)。成绩录入后进入“待公示”状态,此时只有后台可见。等到竞赛管理员确认所有成绩录入完毕,点击“发布公示”,学生的端口的报名状态页面就会显示奖项结果。

数据库层面我用的是单独的一张score表,字段包含:id、competition_id、student_id、signup_id、score_value(分数或奖项字符串)、stage(初赛/决赛)、status(0未录入 1已录入待公示 2已公示)、录入教师id、录入时间、创建时间、更新时间。

需要注意的就是查询成绩时必须加状态条件。在没有做权限控制前,有的同学直接查报名记录时把score字段也查出来返回前端了,这就不小心造成了未公示成绩泄露。我在成绩模块的Controller接口里设计了两套查询接口:一套给评委和管理员用(能看所有状态成绩),另一套给学生用(只能看到status=2已公示的成绩)。接口权限分开,比在同一个接口里做判断干净得多。

5.4 数据统计模块:用SQL聚合代替复杂逻辑

管理员后台除了处理日常事务,还有一个汇报需求,比如:本月新增了多少个竞赛项目、全校有多少人参赛、各学院参赛覆盖率如何。这类统计如果靠Java代码遍历数据库记录去计算,数据量一大效率很低。正确的方法是用SQL聚合函数在数据库里直接算。

举一个实际的例子:统计各学院参赛人数。

SELECT u.college, COUNT(DISTINCT sr.student_id) AS participants FROM signup_record sr LEFT JOIN user u ON sr.student_id = u.id WHERE sr.status = 1 GROUP BY u.college ORDER BY participants DESC

我把几个常用的统计查询封装在StatisticMapper里,用MyBatis-Plus的@Select注解直接写SQL,返回一个List<Map<String, Object>>给前端渲染表格或图表。这样做的好处是:统计逻辑在数据库内部完成,充分利用了数据库的索引和聚合优化,Java层只是拿到最终结果,不需要做中间计算。

关于图表展示,如果你要用ECharts之类的前端图表库,配置数据格式的时候注意后端返回的字段命名要和图表库的字段对应。比如ECharts的饼图要求数据格式是[{name: "计算机学院", value: 120}, {name: "经管学院", value: 85}],那后端Map的key就设计成name和value,前端拿过来直接用,省得再转换一遍。

6. 权限落地细节与接口安全加固

6.1 后端接口越权漏洞的排查思路

在竞赛平台里,最容易出安全问题的地方就是越权。我给你还原一个真实场景:报名记录的更新接口是/admin/signup/update,管理员可以调用它来修改审核状态。但接口如果只是根据id去更新记录,没有检查操作人的角色,那一个普通学生仿造这个请求,也可以把任意一条报名记录改成“已通过”。这就是水平越权的一种典型表现。

要对付这类问题,思路是在业务逻辑层就做数据归属校验:

  • 如果是学生修改自己的报名信息,必须先比对signup_record.student_id是否等于当前登录用户id,不一致就直接拒绝。
  • 如果是教师审核报名,要校验该教师是否被分配了管理该竞赛的权限。
  • 如果是管理员操作,要校验该用户确实拥有管理员的角色标识。

在Spring Security基础上,我习惯在Controller层写一个简单的权限检查工具方法,或者用自定义注解来处理。项目结构不复杂的情况下,我倾向于直接在每个接口的业务逻辑里主动校验,虽然代码重复一点,但可读性最高。

6.2 登录接口的防暴力破解与验证码机制

竞赛平台的学生用户数量多,账号密码的安全不容忽视。我做了两层防护:第一层是登录接口加入验证码校验,第二层是连续失败N次后账号短暂锁定。

验证码我这里推荐直接用开源的Hutool工具库里的CaptchaUtil,一个Java类就能生成图形验证码。实现流程是:登录页先请求一个/captcha接口,服务端生成验证码图片并且把正确答案存在Redis里(key用UUID),前端提交登录时带上这个UUID和用户输入的验证码,后端从Redis取出正确答案比对。验证码有效期5分钟,用过即删。

账号锁定的实现方式也很简单:用Redis维护一个login:fail:{username}的计数器,每次登录失败加1,同时设置过期时间为15分钟。计数器超过5次,登录接口就直接返回“尝试次数过多,请15分钟后再试”,不再继续验证密码。

6.3 文件上传的安全校验细节

文件上传最怕什么?最怕用户上传可执行的恶意脚本文件,比如伪装成图片的JSP后门。虽然这个系统部署在内网,但安全习惯必须养成。

我的上传校验做了一个组合拳:

  • 校验Content-Type头是否是允许的MIME类型。
  • 校验文件扩展名是否在白名单里,同时把文件名里的特殊字符(比如../、..\)全部清洗掉。
  • 重要的一步:不要用用户上传的文件名作为服务器保存文件名,而是直接用UUID生成随机文件名,保留原有扩展名。

这样即使有人恶意构造一个“a.jsp”或者“shell.php.jpg”这样的文件名,也不能直接执行或者造成路径穿越。

7. 问题排查实录:我踩过并成功解决的坑

7.1 MyBatis-Plus分页查询失效问题

MyBatis-Plus的分页插件有一个专门的配置类,很多新手第一次用的时候发现分页根本不生效,返回的总条数是0,数据也不是按预期分页返回。这个问题的根源通常是:分页插件没有注册进MyBatis-Plus的拦截器链中。

正确的写法是要在配置类里加一个@Bean:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

加了这段配置之后分页才会真正生成limit语句。我在带毕业生做项目的时候发现,这个点十个人里有六个人会漏掉。漏掉的时候并不会报错,只是分页数据不对,排查起来很麻烦。

7.2 LocalDateTime序列化格式错乱

用Java 8的LocalDateTime做日期字段时,后端返回给前端的数据默认会带有T,比如“2025-01-16T10:30:00”,前端如果想要“2025-01-16 10:30:00”的格式,直接解析会出现格式错乱。

我的解决方案是在application.yml里全局配置JSON序列化格式:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

如果是Spring Boot 2.x,这样配置就可以覆盖大多数场景。如果你是用Fastjson做序列化的,那要在@JSONField注解上指定format字段才能生效。这个看似小问题,但在前后端联调时经常会浪费大量时间,提前统一格式是明智的选择。

7.3 Spring Security放行接口却不生效

另一个高频问题是Spring Security配置了某些接口为permitAll,但实际访问时仍然返回401或302。造成这个问题的原因有两个高频可能:

第一种是过滤器链顺序问题。我在前面给出的配置里使用了.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)。如果你把JWT过滤器添加到了所有过滤链的最前面,并且这个过滤器内部对所有没有token的请求直接抛异常,那即使后面配置了放行规则也没有用,因为过滤器在进入授权规则判断之前就把请求拦了。

第二种是路径匹配规则不对。Spring Security的antMatcher匹配的是请求路径的精确前缀。如果你的接口全部在/api下面,而你配置的是/api/auth/**放行,那/api/competition/list就不会被放行。用前缀匹配时,一定要确保路径格式一致。

解决方法是:JWT过滤器里对白名单路径(比如登录、注册、验证码)直接跳过token校验,只对需要认证的路径做解析。这个白名单定义在一个静态常量列表里,和SecurityConfig里的permitAll路径保持一致。两条路径规则重复维护虽然麻烦,但避免过滤器误杀更值得。

7.4 跨域配置问题汇总

前后端分离部署时,跨域问题几乎是百分百会遇到。最常见的一种报错是:Access-Control-Allow-Origin。解决方案是在后端配置一个全局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不能设置为“*”,必须指定具体的前端域名或者用allowedOriginPatterns通配符。我在最初联调时前端跑在localhost:8081,后端跑在localhost:8080,如果不加这段配置,浏览器怎么都不让请求通过。加了之后,用自定义的“allowedOriginPatterns”来匹配前端地址,问题迎刃而解。

还有一个容易被忽略的点:集群部署或前后端不同域时,Spring Security的csrf默认是开启的。如果你没有显式关闭csrf,POST请求会被403拦截。这类问题通常在本地开发时不会出现(因为同源),一旦前后端分离部署就暴露出来了。我在SecurityConfig里明确调用了csrf.disable(),同时把跨域配置注册到HttpSecurity上,两者配合才能保证接口正常访问。

8. 项目部署与文档撰写经验分享

8.1 Linux服务器部署实操

对于高校项目来说,最终能不能被真正用起来,部署环节的复杂度是一个不可忽视的变量。我推荐的部署方案是:用Docker跑MySQL和Redis,用systemd托管Java的Jar包,Nginx做前端静态文件服务与反向代理。

打包的时候注意,pom.xml里要配置Spring Boot Maven插件:

<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>

执行mvn clean package后,在target目录下生成可执行jar。启动命令:

java -jar competition-platform.jar --spring.profiles.active=prod

生产环境的配置放在application-prod.yml里,数据库连接、文件上传目录等通过环境变量注入,避免把敏感信息写在代码仓库里。

Nginx配置反向代理的关键片段:

server { listen 80; server_name your-domain; location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { root /var/www/competition; index index.html; try_files $uri $uri/ /index.html; } }

这个配置的精髓在于:前端页面由Nginx直接托管,后端接口通过/api前缀被反向代理到Java服务。前端项目里所有请求的baseURL都设置为/api,这样前端代码里不需要写死后端地址,部署时移动性强很多。

8.2 Docker方式部署的服务编排

如果你的服务器资源不紧张,我更推荐用docker-compose把整个环境编排起来。这样可以当你拿到一台新服务器时,一条命令就能把MySQL、Redis、应用全部拉起,不需要手动装环境。

version: '3.8' services: mysql: image: mysql:8.0 container_name: competition-mysql environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: competition_system ports: - "3306:3306" volumes: - ./mysql-data:/var/lib/mysql - ./init.sql:/docker-entrypoint-initdb.d/init.sql restart: always app: build: . container_name: competition-app depends_on: - mysql ports: - "8080:8080" environment: SPRING_PROFILES_ACTIVE: prod SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/competition_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai restart: always

注意一个细节:容器内Java服务访问数据库的地址,不能写成localhost,要写成mysql(服务名)。因为每个容器有独立的网络栈。这个点我在团队协作的时候经常提醒同事,因为很多人在Docker部署阶段都卡过这里。

8.3 项目文档怎么写得专业且容易交付

一个优秀的开发项目,除了代码还有文档。毕业设计或者课程设计评审时,文档质量往往占了一半的分值。但很多人写的都是流水账,我讲讲怎么组织文档结构。

文档的核心结构应该是:背景与需求分析、系统设计、功能实现、测试与部署、总结展望。其中系统设计部分要画出系统架构图、功能模块图,数据库设计要附上ER图及关键表结构说明。功能实现部分要对每个核心模块做“设计思路 + 关键代码 + 效果图”三段式呈现,让评审老师能快速看到你的完整工作过程。

关于源码文档的配套,我建议把一个初始化的SQL脚本单独放在doc目录下,里面不仅要建表,还要插入一个默认管理员账号(用户名admin,初始密码要用BCrypt加密后的字符串,不要写明文),以及几组基础测试数据。这样别人拿到源码后,先执行SQL再启动项目,几分钟就能看到效果,这个体验对开源分享或作业评审来说非常加分。

8.4 源码与文档的整理心得

在真实的项目交付里,源码和文档的整理往往被看作“最后的收尾”,但实际上它是决定项目能否被人快速看懂的钥匙。我有几个习惯:

第一,项目里的包结构清晰。按controller、service、mapper、entity、config、utils、constant这种分层组织,避免所有类扔在一起。

第二,所有实体类和核心方法上加注释。注释要写“业务含义”,不是写“getXXX”。比如报名状态status这个字段,注释写成“报名审核状态:0待审核、1通过、2驳回”,而不是简单的“状态”。

第三,README文件里写清楚三件事:怎么初始化数据库、怎么配置环境启动项目、默认账号是什么。别人拿到你的代码,照着README走一遍就能跑起来。

9. 这套系统还能往哪些方向扩展

很多同学做毕业设计或者项目练手的时候,做完基础功能就停了。但实际上这个竞赛平台的可扩展空间很大,值得聊一聊。

第一个方向是增加竞赛大数据分析。随着报名数据逐年积累,你可以根据历年参赛人数、各学院获奖情况、指导教师参与度做趋势分析。这些数据对教务部门制定政策很有参考价值。技术上可以对接ECharts做可视化大屏,也可以在后台增加一个统计分析报表页。

第二个方向是引入消息推送与提醒。现在学生需要自己登录系统查看报名审核结果,体验不够主动。你可以接入邮件通知(Java MailSender),或者接入微信公众号模板消息。当审核状态变化时,自动推送结果给学生,学生的使用体验会有质的提升。

第三个方向是智能组队推荐。学科竞赛很多是团队赛,学生经常遇到“找不到队友”的困境。如果系统能记录每个学生的专业、技能标签、参赛经历,那么在报名组队阶段,就可以根据学生填写的团队需求推荐互补型队友。这个功能涉及一些推荐算法的实现,但业务逻辑本身并不复杂,在数据基础上做简单的标签匹配就能有一个初步效果。

扩展的方向虽然很多,但我要给大家提个醒:系统的边界感很重要。如果你做的事情是课程设计或者毕业设计,合理控制功能范围,把核心业务闭环做扎实,比盲目加一堆花哨功能要好得多。先把报名、审核、成绩、权限这些基础能力做稳定,再考虑扩展。我在带项目时常说一句话:“一个系统不怕功能少,就怕它不稳定。你按照评审的要求把核心流程跑通,本身就是优秀的成果。”这句话送给你,也作为这篇分享的收尾。

最后再分享一个小技巧:这个项目做完之后,记得把你的建表SQL、初始数据脚本、README文档打包在一起。将来无论是写简历项目经历,还是分享给同学学习,这一套东西都能帮别人极大地降低理解门槛。我当时就是这样整理的,后来反过头去看自己写的代码,也能很快回忆起当时的业务设计逻辑,维护成本低了不少。

相关新闻

Bambu Studio 3D打印切片软件:从拖入模型到拿到 GCode 的三步

Bambu Studio 3D打印切片软件:从拖入模型到拿到 GCode 的三步

Bambu Studio 3D打印切片软件&#xff1a;从拖入模型到拿到 GCode 的三步 【免费下载链接】BambuStudio PC Software for BambuLab and other 3D printers 项目地址: https://gitcode.com/GitHub_Trending/ba/BambuStudio Bambu Studio 是免费开源的 3D 打印切片软件&am…

2026/10/20 3:21:02 阅读更多 →
多数组排序三种实现:冒泡排序、重写sort方法与栈的灵活运用

多数组排序三种实现:冒泡排序、重写sort方法与栈的灵活运用

刷到一个作业题&#xff0c;编号7-1&#xff0c;分值12分&#xff0c;要求写"多数组排序"&#xff0c;而且点名了三种做法&#xff1a;冒泡排序、重写sort方法、利用sort和栈。第一眼觉得简单&#xff0c;排序嘛&#xff0c;哪个后端没写过。但仔细一想&#xff0c;这…

2026/10/20 3:20:02 阅读更多 →
YOLOv8自定义数据集训练全流程:从数据标注到部署的关键避坑指南

YOLOv8自定义数据集训练全流程:从数据标注到部署的关键避坑指南

第一次用YOLOv8训练自己的数据集&#xff0c;大多数人不是倒在模型上&#xff0c;而是倒在数据上。这句话我反复跟身边想跑目标检测项目的朋友说&#xff0c;他们一开始都不信&#xff0c;觉得既然框架都封装好了&#xff0c;训练不就是敲一条命令的事&#xff1f;结果真到自己…

2026/10/20 3:20:02 阅读更多 →

最新新闻

STM32F407单相PWM整流双闭环控制实战

STM32F407单相PWM整流双闭环控制实战

简介&#xff1a;本资源是针对2022年全国大学生电子设计竞赛A题‘单相交流电子负载’的完整嵌入式实现方案&#xff0c;面向电赛备赛学生、STM32初/中级开发者及电力电子方向实践者&#xff0c;重点解决单相系统下高精度锁相与PWM整流控制这一核心难点。工程基于STM32F407VET6主…

2026/10/20 11:09:04 阅读更多 →
斯坦福ToddlerBot——用于行走操控且与机器学习兼容的0.56m小人形:6000美刀成本下,可VR遥操、可RL训练

斯坦福ToddlerBot——用于行走操控且与机器学习兼容的0.56m小人形:6000美刀成本下,可VR遥操、可RL训练

前言 我司q4的几个侧重点&#xff1a; 在深圳手搓人形世界模型及WAM叠衣服继续双足全身vla/WAM继续强化我司「插拔/装配、搬运/分拣」垂直模型 本点中的“插拔/装配”&#xff0c;涉及RSI的落地/实践不断强化RL运控实力 故持续欢迎敢闯、敢干的年轻人&#xff0c;随我们每周…

2026/10/20 11:09:04 阅读更多 →
Citra 免费 3DS 模拟器:完整上手指南

Citra 免费 3DS 模拟器:完整上手指南

Citra 免费 3DS 模拟器&#xff1a;完整上手指南 【免费下载链接】citra A Nintendo 3DS Emulator 项目地址: https://gitcode.com/GitHub_Trending/ci/citra Citra 是开源的任天堂 3DS 模拟器&#xff0c;支持 Windows、macOS 与 Linux 三大平台。本文带你走完"认…

2026/10/20 11:09:04 阅读更多 →
网关架构:知乎千万级长连接网关架构

网关架构:知乎千万级长连接网关架构

知乎千万级长连接网关架构 知乎长连接网关要服务的不是单一业务&#xff0c;而是多个 App 和十几个形态各异的长连接业务。因此它的核心目标不是“为某个业务做一套专用长连接”&#xff0c;而是做一套共享、可水平扩展、业务解耦的通用长连接系统。 一、发布订阅模型 多客户端…

2026/10/20 11:09:04 阅读更多 →
STM32F407单相DQ锁相+PWM整流实时控制实战

STM32F407单相DQ锁相+PWM整流实时控制实战

简介&#xff1a;本资源是针对2022年全国大学生电子设计竞赛A题‘单相交流电子负载’的完整嵌入式实现方案&#xff0c;面向STM32初学者及电赛备赛学生&#xff0c;聚焦单相PWM整流控制中的核心难点——高精度锁相环设计。方案基于STM32F407VET6主控&#xff0c;采用STM32CubeM…

2026/10/20 11:09:04 阅读更多 →
蒙特卡洛法在电力系统状态估计与风险评估中的MATLAB实现

蒙特卡洛法在电力系统状态估计与风险评估中的MATLAB实现

简介&#xff1a;已有307人学习的这份Matlab资源&#xff0c;聚焦蒙特卡洛法在电力系统状态估计与风险评估中的应用&#xff0c;适合电力系统研究人员、运行工程师及电气专业高年级学生使用。资源从建模出发&#xff0c;覆盖节点电压、线路潮流等状态量的最可能估计&#xff0c…

2026/10/20 11:08:03 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →