news 2026/9/17 4:25:20

SpringBoot+Vue+MyBatis+MySQL构建高校竞赛管理系统全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+Vue+MyBatis+MySQL构建高校竞赛管理系统全解析

1. 项目背景与技术选型:为什么竞赛管理系统最终选了SpringBoot+Vue+MyBatis+MySQL

1.1 大学竞赛管理的真实痛点

每年3月到6月,是各高校学科竞赛最密集的周期。互联网+、挑战杯、数学建模、程序设计大赛、大英赛、电子设计竞赛……通知从教务处发到学院,学院辅导员再发到班级群,学生填完Excel交回去,管理员再手工汇总、核对格式、催收材料。整个流程里,光“名单版本”就能出现七八个,更别说评审阶段评委分数是汇总在一张共享表格里,经常出现误覆盖、重复打分、排名公式被手滑改掉的问题。

我接到这个项目的时候,用户的诉求非常具体:做一个同时面向学生、教师、学院管理员的竞赛管理系统,把赛事发布、在线报名、材料审核、专家评审、成绩公示、数据统计这些环节全部搬到线上。系统的访问量不会特别大,一天可能就几千人报名,但业务链条长、角色多、状态流转复杂,而且学生个人信息和成绩数据都涉及隐私,不能有一丝马虎。

我当时就是在这样的背景下,把SpringBoot+Vue+MyBatis+MySQL这套技术栈落到了这个系统里。写这篇文章,不是把项目文档再念一遍,而是想重点聊聊工程实现里的关键决策、最容易踩坑的地方,以及那些从真实跑批和上线维护中沉淀出来的经验。无论你是正在做毕业设计,还是给学校或机构搭建类似平台,这篇内容应该都能给你一些实际参考。

1.2 技术选型的理由与取舍

技术选型阶段,我也犹豫过是不是直接用Python+Django,或者直接用若依这种脚手架快速生成。但最终整套方案固定为SpringBoot+Vue+MyBatis+MySQL,理由很实际:

  • Java生态稳定。学校服务器上安装Java、MySQL、Nginx的环境非常成熟,后续维护的人也好找。Python好上手,但真要长期维护一个多人协作的中大型管理系统,Java的工程化优势很明显。
  • SpringBoot开发效率够高。内嵌Tomcat、自动配置、Starter机制,不需要重复搭建环境。项目中期想加定时任务、消息推送、WebSocket实时通知,SpringBoot生态里都有非常现成的整合方式。
  • Vue组件化适合中后台。竞赛管理系统的前端页面其实主要就三大块:学生端报名页、管理端后台、评委批分页。Vue的组件化、路由和状态管理正好匹配这种场景,开发和后期调整都比传统jQuery时代的写法舒服得多。
  • MyBatis对SQL可控性极强。系统的统计报表部分,比如“各学院参赛人数排行”“获奖积分排名”“评委打分均分对比”,这些查询逻辑复杂,直接用MyBatis写SQL最直观,可以精确控制每一行查询结果,不会像JPA那样被自动生成的SQL绕晕。
  • MySQL完全满足数据量和可靠性要求。一个高校一年最多几千条赛事报名记录、几万条评分记录,InnoDB事务、索引、备份恢复机制都完全够用。没有必要为这个规模上PostgreSQL或者上分库分表,那是给自己找麻烦。

另外,项目里我没有一开始就引入Redis、消息队列这些组件。原因是:这套系统最核心的一致性要求在于“报名不能重复”“分数不能错乱”,这些用数据库唯一索引和事务就能解决,过早引入缓存和异步中间件只会增加部署和排查成本。后来在赛程通知、首页热点数据之类的位置,我才按需加了Redis,下文会细说。

1.3 系统功能模块全景

我把系统按角色和业务拆分成了下面几个核心模块:

角色核心功能
学生赛事浏览、在线报名、材料上传、报名状态查看、成绩与奖项查询
教师/评委赛事评审、打分、提交评审意见、查看自己参与评审的赛事
学院管理员审核本学院学生的报名信息、导出本学院名单、查看学院维度统计数据
系统管理员赛事创建与流程配置、用户与权限管理、全局状态流转控制、系统参数设置

模块间的关系可以这样理解:系统管理员发布一个“互联网+校赛”,设置报名开始和结束时间;学生在截止前报名,上传项目申报书、PPT、作品视频;学院管理员逐条审核;审核通过后进入评审阶段,评委登录系统打分;管理员确认成绩并公示。整个流程就是一个状态机,从“立项”到“报名中”,再到“审核中”“评审中”“已结束”。

这套模块拆分在项目早期就定下来了。我见过很多管理系统,一开始只想着把功能堆出来,没有考虑角色的操作边界,最后“所有功能都在同一个菜单里”,连按钮权限都是一锅粥。这个项目里,后端每个接口都做了权限注解,前端菜单也按角色动态加载,后面才没有因为权限漏洞反复返工。

2. 数据库设计要点与MyBatis实践:竞赛系统的“骨架”怎么搭

2.1 核心表结构设计与索引规划

竞赛系统的数据库我起名contest_db,字符集使用utf8mb4,排序规则用utf8mb4_general_ci。之所以不用utf8,是因为学生填信息的时候偶尔会在备注里写emoji表情,老版本的utf8会插入报错,utf8mb4能完整支持。

核心表大致有这几张:

  • sys_user:用户表,字段包含id、username、password、real_name、role、college_id、phone、email、avatar、status。role用0、1、2、3分别表示学生、教师/评委、学院管理员、系统管理员。
  • college:学院表,id、name、code、create_time。
  • competition:赛事表,id、title、description、category、level、signup_start_time、signup_end_time、max_team_members、status、creator_id、create_time。
  • competition_signup:报名表,id、competition_id、user_id、team_id、materials_summary、audit_status、audit_comment、create_time、update_time。
  • attachment:附件表,id、signup_id、file_name、file_url、file_type、upload_time。
  • score_record:评分表,id、competition_id、signup_id、judge_id、score、comment、update_time。

以报名表为例,建表SQL里最关键的约束是唯一索引:

CREATE TABLE competition_signup ( id BIGINT PRIMARY KEY AUTO_INCREMENT, competition_id BIGINT NOT NULL, user_id BIGINT NOT NULL, team_id BIGINT DEFAULT NULL, materials_summary VARCHAR(500) DEFAULT NULL, audit_status TINYINT NOT NULL DEFAULT 0 COMMENT '0待审核 1通过 2驳回', audit_comment VARCHAR(500) DEFAULT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_comp_user (competition_id, user_id), KEY idx_audit_status (audit_status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这个唯一索引是解决“重复报名”问题的第一道防线。后端代码里判断“是否已报名”永远可能有并发间隙,但数据库唯一索引不会说谎,一旦两个请求同时插入相同赛事和学生,第二个插入会抛DuplicateKeyException,我们捕获后直接提示“您已报名过该赛事”,就能保证数据错不了。

成绩表同样要加唯一索引:

CREATE TABLE score_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, competition_id BIGINT NOT NULL, signup_id BIGINT NOT NULL, judge_id BIGINT NOT NULL, score DECIMAL(5,2) NOT NULL, comment VARCHAR(500) DEFAULT NULL, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_signup_judge (signup_id, judge_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DECIMAL(5,2)意味着最大整数位是三位、小数位保留两位,足够覆盖100分制。成绩字段坚决不用floatdouble,否则做平均分时会出现0.999999这种浮点误差,排名一算就错。

索引数量也需要克制。我只在频繁查询的字段上建索引,比如报名表的(competition_id, user_id)audit_status,评分表的(signup_id, judge_id)。索引不是越多越好,因为每次插入和更新都要同步维护索引,索引太多反而拖慢写入性能。

2.2 MyBatis的XML与注解怎么分工

MyBatis是这套系统里最值得聊的一层。很多初学者习惯把所有SQL都写在Mapper接口的注解里,但一旦出现动态条件、多表联查、批量插入,注解的可读性会急剧下降。我的分工是:

  • 单表简单CRUD、无动态SQL:用@Select@Insert@Update@Delete注解;
  • 多表关联、动态条件、批量操作:使用XML文件;
  • 一条SQL超过三行:就放进XML。

赛事管理页面的多条件筛选是典型动态SQL:

<select id="selectCompetitionPage" resultMap="CompetitionResultMap"> SELECT c.*, u.real_name AS creator_name FROM competition c LEFT JOIN sys_user u ON c.creator_id = u.id <where> <if test="keyword != null and keyword != ''"> AND (c.title LIKE CONCAT('%', #{keyword}, '%') OR c.category LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="status != null"> AND c.status = #{status} </if> <if test="collegeId != null"> AND c.college_id = #{collegeId} </if> </where> ORDER BY c.create_time DESC </select>

使用<where>标签可以自动处理前缀的AND,不需要再写where 1=1这种略丑的写法。需要注意XML中><需要转义,否则MyBatis解析XML时可能报错。

批量插入附件时使用<foreach>

<insert id="batchInsertAttachments"> INSERT INTO attachment(signup_id, file_name, file_url, file_type, upload_time) VALUES <foreach collection="list" item="item" separator=","> (#{item.signupId}, #{item.fileName}, #{item.fileUrl}, #{item.fileType}, NOW()) </foreach> </insert>

批量插入时我一般会控制批次大小,单批不要超过500条,因为MySQL在一条SQL里处理过多VALUES时,可能触发max_allowed_packet限制,数据量一大就会报错。实际项目里一个赛事报名附带两三份材料,一次批量插入不会太多,但如果以后要做Excel批量导入,这个坑一定要提前避开。

2.3 MyBatis缓存与拦截器的实践

缓存这个问题,我在这个项目里吃过亏,后面专门在踩坑章节细讲。这里先给结论:成绩表、报名表这些敏感和高一致性要求的业务,MyBatis二级缓存直接关闭;只对字典表、学院表这类更新频率极低、非敏感的基础数据开启。

一级缓存的坑也要小心。一级缓存默认开启,作用范围是同一个SqlSession。Spring框架里,如果你在一个事务方法内先查询一条记录,然后更新它,再查询同一条记录,第二次查询可能会拿到缓存里的旧值。解决方案就是在这类查询上加@Options(flushCache = Options.FlushCachePolicy.TRUE),或者更新语句里强制刷新。不要觉得“有缓存是好事”,在成绩录入这种场景里,数据一致性远比那几次数据库查询的性能重要。

MyBatis拦截器我用于两件事:自动填充创建时间和更新时间,以及打印慢SQL日志。自定义拦截器的核心很简单,实现org.apache.ibatis.plugin.Interceptor接口,用@Intercepts注解指定要拦截的对象和方法。慢SQL日志的记录思路是:在Executor执行query和update前后记录当前时间,执行耗时超过500ms就输出到日志文件,并打印SQL语句。这个功能上线后帮我发现了好几条忘记建索引导致的全表扫描SQL,非常实用。

如果只是为了分页,也可以直接用PageHelper,但我在另一套小项目里被PageHelper的线程复用问题坑过。考虑到这个系统的分页需求并不复杂,我写了一个简单拦截器,把原SQL包装成select count(*)select ... limit offset, size两条,逻辑透明,也更便于二次维护。

3. Vue前端与接口联调:从Mock数据到真实环境,最难的不是页面

3.1 前端工程结构与路由权限设计

前端部分使用Vue 2 + Vue Router + Vuex + Element UI,虽然现在Vue 3已经普及,但Vue 2生态仍然非常成熟,尤其Element UI的后台组件齐全,开发这类管理端效率很高。如果从零开始,用Vue 3 + Element Plus也可以,整体思路一致。

src目录结构建议这样分:

src ├── api │ ├── user.js │ ├── competition.js │ ├── signup.js │ └── score.js ├── assets ├── components ├── router │ └── index.js ├── store │ └── modules ├── utils │ ├── request.js │ └── auth.js ├── views │ ├── student │ ├── admin │ └── judge ├── App.vue └── main.js

路由权限不能只靠前端“隐藏菜单”。真正的做法是:登录后调用后端/user/info接口,拿到当前用户角色,动态生成该角色可见的路由表,再通过router.addRoutes注入。这样学生端即使手动在地址栏输入/admin/score,也会因为路由不存在而落到404页,后端接口层的@PreAuthorize又会再做一次校验,双保险。

菜单渲染也基于同一份动态路由配置。我封装了一个generateRoutes方法,根据角色映射配置表,把meta.titlemeta.iconcomponent映射成菜单项。后续新增页面,只需要在路由配置里加一条和角色字段,就可以控制谁能看到,不需要在菜单组件里写一堆v-if

3.2 axios封装与文件上传实现

前端请求统一封装在utils/request.js,核心逻辑包括:

  • 请求拦截器:从localStorage读取token,放到请求头Authorization
  • 响应拦截器:判断HTTP状态码和后端业务码;如果是401,尝试用refreshToken刷新token,刷新成功则重放原请求,刷新失败则清除登录态并跳转到登录页;
  • 统一错误提示:后端返回{code:500, message:'服务器异常'}时,用Element UI的Message组件弹出错误信息,业务代码里不需要到处写catch。

文件上传是竞赛报名的高频操作,我使用el-upload的自定义上传方法。需要特别注意的是,上传接口的Content-Type必须是multipart/form-data,不能把文件放JSON里。前端组装FormData时,把竞赛ID、报名人ID和文件对象一起append进去,后端用MultipartFile接收文件字段,其他参数用@RequestParam接收。

const formData = new FormData(); formData.append('file', file); formData.append('competitionId', this.competitionId); formData.append('userId', this.userId); axios.post('/api/signup/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' }, timeout: 60000, onUploadProgress: (event) => { this.uploadPercent = Math.round((event.loaded / event.total) * 100); } });

上传超时时间一定要设置得大一些,尤其是学生上传几十MB的项目申报书、答辩视频时,默认的10秒超时几乎必然失败。我一般设置到60秒或更长。

3.3 Vue打包后布局异常与刷新404的排查处理

有一次项目测试反馈:本地开发环境一切正常,但打包部署后前端样式全乱了,页面像没有加载CSS一样。我排查了很久,最后发现是publicPath配置的问题。

Vue CLI默认的publicPath/,打包后资源路径会指向根目录。如果部署在http://host/contest/这个子路径下,CSS、JS文件就全部404,浏览器拿到的是没有样式和脚本的页面。解决办法是在vue.config.js里设置:

module.exports = { publicPath: './', outputDir: 'dist', assetsDir: 'static' };

这样资源路径会变成相对路径./static/css/...,部署在任意子目录都不容易出问题。当然,如果前端部署在独立域名根路径,publicPath仍然不需要改,保持/也没有关系。

另一个高频问题是Vue Router使用history模式,部署后刷新页面变成404。这个问题的根源在于nginx直接根据URL去找服务器上的文件,找不到对应目录就返回404。解决方法是让nginx在找不到静态文件时回退到index.html

location / { try_files $uri $uri/ /index.html; }

如果部署在子目录,则把/index.html改成/contest/index.html。这个配置在前后端分离项目里几乎是必配项,早配置早安心。

4. 从源码到环境:MySQL、SpringBoot配置与前后端分离部署实操

4.1 MySQL 8.0安装与初始化

数据库环境我使用的是MySQL 8.0.34。这里不展开安装包每一步,重点说几个容易出错的配置点。

Windows下安装时,在字符集选择界面务必选utf8mb4。如果用的是免安装的zip包,初始化命令是:

mysqld --initialize-insecure mysql -uroot ALTER USER 'root'@'localhost' IDENTIFIED BY 'your_password';

新版MySQL的默认认证插件是caching_sha2_password,但项目里如果JDBC驱动版本比较旧,就会在连接时报Public Key Retrieval is not allowed。解决方法是直接用ALTER USER把认证插件改回mysql_native_password

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_password'; FLUSH PRIVILEGES;

如果是新项目且JDBC驱动使用的是最新版,不修改认证插件也可以,但要在JDBC连接串里加allowPublicKeyRetrieval=true。我自己的项目为了兼容性,还是改成mysql_native_password比较省心。

数据库初始化建议直接导入SQL脚本,一次性建好库和表,并插入初始管理员账号。初始密码不要用明文,在后端项目里用BCrypt加密后再写入。这里强调一句:管理员初始密码必须强制修改,这是竞赛系统最基本的安全底线。

4.2 application.yml核心配置

后端工程里application.yml是整个系统环境配置的核心。我用的是多环境配置方式:application-dev.ymlapplication-prod.yml,启动时通过--spring.profiles.active=prod切换。

生产环境的配置示例:

server: port: 8080 servlet: context-path: / spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/contest_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: xxxxxx servlet: multipart: max-file-size: 100MB max-request-size: 200MB redis: host: localhost port: 6379 timeout: 3000ms mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.contest.entity configuration: map-underscore-to-camel-case: true

有几个细节:

  • serverTimezone必须设置,不设置容易出现时间差8小时的问题,尤其在国内服务器上;
  • useSSL=false是必需的,否则MySQL8默认会尝试SSL连接,本地开发时带来一些无谓的握手延迟;
  • 上传大小限制根据实际文件大小调整。如果学生要传比赛视频,max-file-size至少要给到100MB;
  • map-underscore-to-camel-case打开后,数据库的create_time会自动映射到Java类的createTime,省掉大量写resultMap的时间。

Redis在这个项目里并不是一开始就必须的,我是在首页统计、赛程信息这类读多写少的数据上用了它。比如“当前报名人数”“参赛人数实时统计”这种数据,每次刷新页面都查数据库会比较浪费,放在Redis里设置5分钟过期,性能提升明显。Redisson的分布式锁在报名场景也可以使用,但大多数情况下数据库唯一索引+事务已经足够,不要为了造轮子而造轮子。

4.3 前后端分离部署:nginx + jar + systemd

部署环境是CentOS 7。后端打包成jar,前端打包成静态文件,两项分别部署。

后端打包命令:

mvn clean package -DskipTests

拿到contest-server.jar后上传到服务器/opt/contest/目录,创建一个专用用户,用systemd管理进程,避免手动java -jar后关闭终端服务就停了。

systemd服务文件:

[Unit] Description=Contest Server After=network.target [Service] User=contest ExecStart=/usr/bin/java -Xmx512m -Xms256m -jar /opt/contest/contest-server.jar --spring.profiles.active=prod Restart=on-failure SuccessExitStatus=143 [Install] WantedBy=multi-user.target

内存这里不要给太多,512MB足够这个系统使用,留出内存给MySQL和nginx。如果使用SpringBoot 2.7 + JDK8,启动JVM时还可以加上-XX:+UseG1GC,GC更平滑,也不会明显增加内存占用。

前端部署比较简单:

  1. npm run build,生成dist目录;
  2. 将dist上传到nginx的/usr/share/nginx/html/contest/目录;
  3. nginx配置一个server块,同时处理前端页面和后端API转发。
server { listen 80; server_name contest.example.com; location /contest/ { alias /usr/share/nginx/html/contest/; try_files $uri $uri/ /contest/index.html; } 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 /api/配合proxy_pass http://127.0.0.1:8080/会把前端的/api/signup/upload映射成后端的/signup/upload,因为nginx会把匹配的前缀/api替换成/。如果后端接口不带/api前缀,也可以直接在SpringBoot里配置server.servlet.context-path=/api,保持前后端的接口路径一致。我更推荐所有后端接口统一加/api前缀,以后要做网关改造也方便。

5. 踩坑记录与性能调优:这些细节在源码中值得反复看

5.1 MyBatis缓存引发的成绩“不变”问题

这是真实发生过的场景:评委A录入成绩85分,评委B打开同一份作品详情,显示的仍然是之前的75分。数据表里已经被更新为85分,但查询结果一直是旧值。当时排查了很久,最后定位到MyBatis二级缓存。

问题出在一个查询Mapper命名空间上加了<cache>配置,导致该namespace下的查询结果被缓存。虽然执行update操作默认会清缓存,但查询走的是另一个mapper namespace,清不到那个缓存,于是读到了旧数据。成绩是决定学生获奖的关键数据,任何“旧值”都是不可接受的。

我的处理方式很干脆:将成绩表、报名表、用户表这些高一致性要求的数据的二级缓存全部关闭,只给学院表、字典表这类几乎不改动的基础数据开。另外在需要保证数据实时性的查询方法上,统一加@Options(flushCache = Options.FlushCachePolicy.TRUE),确保每次查询都走数据库。性能上会降低一些,但这类系统并发量不大,换来的是数据正确性,非常值。

5.2 SpringBoot版本过高导致的启动失败

项目中期有同事试过将SpringBoot升级到3.0以上,结果发现很多老依赖不兼容,启动直接ClassNotFoundException。后来我把版本锁在2.7.18,这个版本是目前Java Web项目最稳的选项,既能兼容JDK8,也能运行在JDK11、JDK17上,长期维护也比较放心。

如果你也想锁版本,在pom里这样配置:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent>

还有一个高频坑是mybatis-spring-boot-starter版本和SpringBoot 2.7不兼容,启动时报Invalid value type for attribute 'factoryBeanObjectType'。解决办法是把MyBatis starter升级到2.3.1或更高版本。排查依赖冲突时,mvn dependency:tree几乎是最有用的命令,哪个依赖传递了什么包、哪个版本被覆盖,一目了然。

5.3 报名并发与成绩精度的控制

报名阶段的并发问题主要发生在截止日最后几小时。学生集中点“提交报名”,如果后端只做“先select再insert”的判断,两个并发请求可能同时通过“未报名”检查,然后都执行插入,产生重复数据。解决办法我在设计数据库时已经提到:加唯一索引。同时,插入报名记录时捕获DuplicateKeyException,将其转换为友好提示:

try { signupMapper.insert(signup); } catch (DuplicateKeyException e) { throw new BusinessException("您已报名过该赛事,请勿重复提交"); }

不要小看这个细节,没有唯一索引的时候,一旦生产环境出现重复数据,清理起来极麻烦,还可能影响后续审核和评分的关联查询。

成绩精度方面,使用DECIMAL(5,2)配合BigDecimal。如果在Java里直接使用Double计算平均分,很可能出现类似86.999999的结果,再做排名时就会出错。我专门在工具类里封装了分数计算逻辑,统一使用BigDecimaladddivide,并且保留两位小数,避免了大量隐性bug。

5.4 竞赛附件与m3u8视频播放的补充方案

很多竞赛作品要求上传视频文件,尤其是微课、舞蹈、实验演示这类内容。视频文件体积通常很大,绝对不能存进数据库BLOB字段,否则数据库膨胀不说,备份和迁移都会变成噩梦。我的方案是:文件存本地目录,数据库只存路径。

上传目录我用/data/contest/upload,通过nginx映射成/upload静态路径对外提供访问。学生上传的文件名统一使用UUID重命名,避免中文文件名和路径注入问题。附件表记录了文件原始名、存储路径、文件大小、上传人等字段,需要下载时后端校验权限后才能拿到完整路径,不做裸的静态下载。

至于m3u8播放,主要用于赛后答辩视频回放或者在线评审。Vue前端播放m3u8用video.jsvideojs-contrib-hls插件,初始化时指定视频地址和type: 'application/x-mpegURL'就可以播放。生产环境上这类视频一般通过nginx直接提供静态文件访问,后端只需要保证文件路径可访问即可。如果播放时出现跨域问题,还要给nginx配置add_header Access-Control-Allow-Origin *,并正确处理预检请求。

6. 源码之外:二次开发与长期维护的真实感受

6.1 让代码可以被其他人接手的小习惯

源码交付以后,最大的问题不是“能不能跑”,而是“后来的人能不能改得懂”。我在项目里定了几个很朴素的规范:service层方法必须写JavaDoc注释;所有状态的魔法数字全部用常量类封装,不许在业务代码里直接写if(status==2);后端返回给前端的日期格式统一为yyyy-MM-dd HH:mm:ss;接口返回结构统一使用Result<T>,包含code、message、data三个字段。

这些习惯一开始看起来会增加一点点工作量,但后期维护时收益巨大。因为竞赛管理系统的需求变化非常频繁,今年要增加“校级立项”功能,明年可能要改“跨校联合报名”,没有清晰注释和统一规范,每次改需求都像考古一样折磨人。

6.2 数据统计与报表的优化方向

学校领导最关心的报表往往不是细颗粒度的报名明细,而是趋势和排名。比如“今年参赛人次增长了多少”“哪些学院连续三年参赛率最高”。这类统计SQL如果每次实时count,数据量小还好,数据量过万之后,复杂的联表统计就会明显变慢。

我的做法是增加一张stat_competition_summary汇总表,每天凌晨通过定时任务预计算各维度统计结果,页面展示时直接查这张汇总表。定时任务用Spring自带的@Scheduled即可,不需要引入额外的分布式调度中间件。这样统计报表页面的查询速度可以控制在几十毫秒内,同时避免给主库增加压力。

6.3 关于“企业级”和这套系统的边界

最后想说一点对“企业级”这三个字的理解。一套系统能不能称得上企业级,不在代码里堆了多少设计模式,也不在用了多少中间件,而在于它能不能在真实环境中稳定地解决业务问题,在权限、数据安全、并发、可维护性这些维度上经得起考验。

竞赛管理系统虽然规模不算大,但麻雀虽小五脏俱全:它涉及多人多角色权限、流程状态机、文件存储、附件解析、并发报名、成绩精度、数据统计,几乎覆盖了一个业务系统会遇到的大部分通用问题。抽茧剥丝之后,它最核心的价值就是那一套“赛事配置+报名流程+评审权限”的抽象模型。把这个模型理解透,往后不管做学术活动管理系统、志愿服务时长系统,还是品质项目申报系统,你都会发现很多设计和代码是可以复用的。

我在实际项目中最大的体会是:先想清楚业务边界,再动键盘写代码。数据库表设计多花半天,后面能省下两个星期的返工时间。这套系统的源码也是一样,它不是给你应付答辩的玩具,而是一套能真正跑到服务器上、扛住一个学期报名高峰的完整工程。如果你正在做类似的系统,可以从我上面提到的几个关键点入手,先把报名唯一索引和成绩Decimal精度处理好,再把权限和状态流转理顺,剩下的更多是时间问题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 4:23:55

VSCode + LaTeX 环境配置指南:掌握编译输出目录与排错技巧

拖了好几年&#xff0c;我终于把写毕业论文的战场从 Overleaf 彻底搬回了 VsCode。不是因为网页版不好用&#xff0c;而是当文档越来越长、章节越来越多&#xff0c;我在本地反复编译时&#xff0c;根目录里堆满了.aux、.log、.toc这些中间文件&#xff0c;看着就烦躁。更让人抓…

作者头像 李华
网站建设 2026/9/17 4:19:30

通达信选股+QMT下单:构建量化交易自动化信号链路

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

作者头像 李华
网站建设 2026/9/17 4:18:19

AR+AI双引擎架构:腾讯云如何撑起消费级AR眼镜的跨端融合

上周我把一台雷鸟AR眼镜交给一位完全不懂技术的朋友&#xff0c;他戴上后问了个很实在的问题&#xff1a;“这东西跟我手机有什么区别&#xff1f;”我没法用一句话回答。真正让它在AI时代变得有用的&#xff0c;不只是AR显示本身&#xff0c;而是雷鸟把AI能力跟AR深度融合之后…

作者头像 李华
网站建设 2026/9/17 4:17:49

Cryptomator 使用指南:云盘文件如何做到只有自己能读

Cryptomator 使用指南&#xff1a;云盘文件如何做到只有自己能读 【免费下载链接】cryptomator Cryptomator for Windows, macOS, and Linux: Secure client-side encryption for your cloud storage, ensuring privacy and control over your data. 项目地址: https://gitco…

作者头像 李华