news 2026/9/24 22:14:45

SpringBoot+Vue3+MyBatis+MySQL高校竞赛管理系统设计与实现全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+Vue3+MyBatis+MySQL高校竞赛管理系统设计与实现全解析

做高校竞赛管理系统这件事,坦白说是被学校教务老师"逼"出来的。之前学校组织各类竞赛,报名信息靠Excel汇总,作品提交靠邮件,评审打分靠纸质表,一个赛程下来光催材料就得催三四天。后来我直接用Java SpringBoot+Vue3+MyBatis+MySQL这套经典组合,做了一个前后端分离的高校竞赛管理系统,两周多时间从数据库设计到前后端联调全部落地。这篇文章把我整个设计和实现过程完整记录下来,包括数据库表怎么拆、MyBatis分页插件怎么用、Vue3后台管理端怎么搭、前后端联调时踩了哪些坑,正在做类似项目或者准备做毕业设计的,可以直接参考。

1. 项目概述:这套高校竞赛管理系统到底解决什么问题

1.1 系统面向的场景与核心功能

高校里的竞赛管理,听着简单,实际一梳理全是琐碎流程。一次竞赛从发布通知、学生报名、指导教师审核、作品提交,到评委打分、成绩公示、证书发放,每个环节都涉及不同角色:管理员要管竞赛全程,教师既要指导学生报名也要参与评审,学生要能报名、传作品、查成绩。线下Excel模式最大的问题是信息不同步——谁报名了、谁没提交、评审到哪一步,全靠人工催问。

所以我设计这套系统时,核心功能就围绕这条业务链展开:竞赛管理(创建、发布、归档)、用户管理(学生、教师、管理员三角色)、报名管理(在线报名与审核)、作品管理(上传、查看、统计)、评审管理(打分、评语、汇总)、数据统计(参赛人数、作品数量、成绩分布)。这些都是高校竞赛管理最基础也是最刚需的功能,先把这些跑通,再扩展证书管理、消息通知都不难。

1.2 技术选型:为什么是SpringBoot + Vue3 + MyBatis + MySQL

这个组合其实是经过对比后的选择。SpringBoot作为后端框架,最大优势是自动配置和生态成熟,一个工程建好就能快速开发接口,不用像传统SSH那样写大量XML配置。MyBatis作为持久层框架,胜在SQL完全可控,竞赛管理这种业务里有很多复杂查询(比如按竞赛类型统计报名人数、按角色查待办事项),直接用SQL写出来最直观,排查问题也方便,比JPA那种自动生成SQL的方式更好定位问题。

前端选Vue3而不是Vue2,一方面是因为组合式API(Composition API)写起来逻辑更集中,另一方面Element Plus等组件库全面拥抱Vue3,后台管理系统的表格、表单、弹窗这些常用组件一套就有。Vue3配合Vite开发环境下热更新非常快,联调体验比老版本好了不少。MySQL在这个场景下完全够用,竞赛管理系统的并发量不会特别高,MySQL稳定、轻量、运维简单,学校环境里也不需要上更重的数据库。

提示:这套组合的学习资料非常丰富,但版本兼容问题确实存在。SpringBoot 2.x和3.x的差异、Vue3和Element Plus的版本匹配、MyBatis与分页插件的版本对应,后面章节我会把容易出问题的坑单独拿出来写清楚。

2. 系统整体设计与数据库建模

2.1 模块划分与角色权限设计

整个系统的模块划分,我按业务角色和使用场景拆成了几个相对独立的部分。管理端是给管理员使用的,包括竞赛管理、用户管理、数据统计、系统配置。教师端要能审核报名、评审作品、查看自己指导的学生。学生端则是报名竞赛、上传作品、查看个人成绩和通知公告。三个端共用同一套后端接口,通过登录用户的角色标识来控制可访问的功能。

权限控制这里,前端路由守卫和后端接口校验要同时做。前端根据登录用户角色动态生成可访问的路由菜单,避免用户看到无权限的操作入口,但真正的安全屏障在后端,每个需要权限的接口都要用拦截器校验角色。我见过不少项目只在前端隐藏按钮,结果有人直接调接口就绕过了权限,这在竞赛场景里会造成学生篡改成绩这类严重问题。

角色权限的具体实现,我用了比较轻量的方式:用户表里存角色字段,后端写一个自定义注解@RequireRole("admin")加在需要权限的Controller方法上,再加一个拦截器读取注解并判断当前登录用户的角色,实现起来简单,也不引入Spring Security这种重量级框架。如果后续权限逻辑复杂了,再升级成Spring Security或Sa-Token也不迟。

2.2 核心表结构设计与字段取舍

数据库设计是整个系统的基础,表结构设计不好,后面写接口和页面都会很痛苦。我核心设计了五张主表:用户表、竞赛表、报名表、作品表、评审表,外加一张通知公告表。每张表的字段取舍,都是在实践中趟过坑的,说说几个关键设计。

用户表(sys_user)除了账号、密码、姓名、角色这些基础字段,建议加上avatar头像字段和phone手机号,高校场景下很多通知是通过手机号联系的。密码不要明文存,用BCrypt加密,这是老生常谈但真的别省。竞赛表(competition)的字段里,status状态字段非常关键,我设计了DRAFT(草稿)、PUBLISHED(报名中)、ONGOING(进行中)、FINISHED(已结束)四个状态,竞赛的每个状态决定了用户能否报名、能否提交作品,这个状态机逻辑必须清晰。

CREATE TABLE `competition` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `title` varchar(200) NOT NULL COMMENT '竞赛名称', `type` varchar(50) NOT NULL COMMENT '竞赛类型:学科/科技/文艺等', `level` varchar(20) NOT NULL COMMENT '竞赛级别:校级/市级/省级/国家级', `organizer` varchar(100) DEFAULT NULL COMMENT '主办方', `description` text COMMENT '竞赛介绍', `start_time` datetime NOT NULL COMMENT '报名开始时间', `end_time` datetime NOT NULL COMMENT '报名截止时间', `status` varchar(20) NOT NULL DEFAULT 'DRAFT' COMMENT '状态:DRAFT/PUBLISHED/ONGOING/FINISHED', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='竞赛信息表';

报名表(enrollment)我加了一个team_name字段,虽然很多竞赛是个人报名,但团体赛的存在让我在设计时把团队信息考虑进去了。status字段区分PENDING(待审核)、PASSED(通过)、REJECTED(拒绝),报名是否通过由教师或管理员审核。作品表和评审表之间通过work_id关联,评审表里存储评委的分数和评语,分数用DECIMAL(5,2)类型,避免浮点数精度问题。

2.3 竞赛审核流转的状态模型

最开始我以为状态管理很简单,就是一个字段存字符串,但实际开发中发现,状态流转的逻辑如果不理顺,会出现很多莫名其妙的Bug。比如学生提交了作品,但竞赛还没到评审阶段,这时候评委不应该看到这个作品;再比如竞赛已经结束了,学生还能报名,这明显不合理。所以我把整个竞赛生命周期定义成一条明确的状态链:

DRAFT(草稿)→ PUBLISHED(报名中)→ ONGOING(作品提交与评审)→ FINISHED(已结束)

后端封装一个状态流转校验的方法,每次更新竞赛状态时都检查当前状态能否合法转到目标状态。比如只有PUBLISHED状态且当前时间在报名时间窗口内才允许学生报名;只有ONGOING状态才允许提交作品。这个状态机逻辑我写在了Service层,而不是散落在各个Controller里,这样确保所有入口都走同一个校验流程。

前端也需要注意状态联动的展示逻辑:竞赛卡片上要显示当前状态标签,按钮要根据状态禁用或显示。比如竞赛还是草稿状态时,前端要隐藏"去报名"按钮;已结束时显示"查看结果"。Vue3里这个可以用计算属性根据竞赛状态返回不同的按钮配置,比在每个模板里写一堆v-if要清爽得多。

3. 后端工程落地:SpringBoot与MyBatis的关键细节

3.1 项目结构与SpringBoot核心配置

后端工程我采用了标准的三层架构:Controller层接收请求、Service层处理业务逻辑、Mapper层操作数据库。额外加了一层DTO/VO对象用于参数接收和响应返回,避免直接把实体类暴露给前端。目录结构大致是controllerservicemapperentitydtovoconfiginterceptorcommon这几个包,看起来常规,但对后续维护来说非常关键。

SpringBoot的核心配置都在application.yml里,数据源、MyBatis、文件上传限制、日志级别都在这里配置。一个容易踩坑的地方是SpringBoot版本太高导致部分配置项不兼容,比如spring.datasource的配置从SpringBoot 2.x到3.x就发生了变化。我用的SpringBoot 2.7.x配合MyBatis Spring Boot Starter 2.2.x,这是比较稳定的一套组合。

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: 123456 servlet: multipart: max-file-size: 50MB max-request-size: 50MB jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.competition.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

注意map-underscore-to-camel-case这个配置,数据库字段是create_time这种下划线风格,Java属性是createTime驼峰风格,不开启这个配置,查询结果映射到实体类时字段会对不上,全是null。这个配置我刚开始漏了,排查了半小时才找到原因。生产环境建议把log-impl关掉,不然每个SQL都会打印到日志里,开发环境开着倒方便调试。

3.2 MyBatis分页插件的正确用法

竞赛管理系统的列表页非常多:竞赛列表、报名列表、作品列表,每页显示10条或20条数据,这就必须要做分页。手动写SQL里的LIMIT当然可行,但每次都要计算起始偏移量,而且需要额外查询总数来做分页组件,代码重复率太高。我的做法是引入PageHelper分页插件,它是国内用得最多的MyBatis分页方案。

PageHelper的使用方式很直接,在Mapper查询方法之前调用PageHelper.startPage(pageNum, pageSize),然后紧接着执行查询,插件会在SQL执行前自动拼接LIMIT子句,并且通过拦截器查询总条数,最后用PageInfo对象封装分页结果。一个需要注意的地方是startPage只能作用于接下来执行的第一个查询,如果你在调用后还执行了别的查询,分页会加到那个查询上,逻辑就乱了。所以一定要写成"调用startPage后立刻执行分页查询"这种紧挨着的格式。

public PageResult<CompetitionVO> getCompetitionPage(int pageNum, int pageSize, String keyword) { // 使用PageHelper.startPage开启分页,必须是查询前的最后一句 PageHelper.startPage(pageNum, pageSize); List<CompetitionVO> list = competitionMapper.selectCompetitionList(keyword); // PageInfo里包含了pageNum、pageSize、total、pages等信息 PageInfo<CompetitionVO> pageInfo = new PageInfo<>(list); PageResult<CompetitionVO> result = new PageResult<>(); result.setList(pageInfo.getList()); result.setTotal(pageInfo.getTotal()); result.setPageNum(pageInfo.getPageNum()); result.setPageSize(pageInfo.getPageSize()); result.setPages(pageInfo.getPages()); return result; }

还有一点要记住,PageHelper.startPage如果接收到的pageNum小于1,需要自己处理,不然可能会出现负数偏移量的SQL。我通常会做一个参数的统一校验:pageNum < 1时重置为1,pageSize > 100时限制为100,防止有人恶意传大参数拖垮数据库。分页插件还有一个多数据源时的注意事项,但竞赛管理系统只有一个数据库,这个暂时用不上。

3.3 缓存机制与拦截器扩展

MyBatis的缓存机制分两级,理解清楚对优化查询很有帮助。一级缓存是SqlSession级别,同一个SqlSession内,同样的查询不会重复查数据库,但Spring管理下每次Mapper操作都会新建SqlSession,所以一级缓存作用有限。二级缓存是namespace级别,也就是Mapper级别的缓存,开启后同一个Mapper的查询结果可以跨SqlSession复用,对竞赛公告这种几乎不变化的数据很有效。

开启MyBatis二级缓存放在对应的Mapper XML文件里加一行<cache/>,同时实体类要实现Serializable接口。但要注意,开启二级缓存后,如果对表做了增删改操作,缓存会自动失效,对实时性要求高的数据(比如报名人数)不要缓存。我在项目里只给字典类数据设置了二级缓存,比如竞赛类型、级别这些枚举值对应的字典表。

另外我自定义了一个MyBatis拦截器,用于自动填充创建时间和更新时间。一开始我在每张表的插入和更新SQL里手动写NOW(),后来发现太容易漏。用@Intercepts注解标注拦截updateinsert操作,在SQL执行前动态设置字段值即可。这个思路和MyBatis-Plus的自动填充功能类似,但不依赖额外框架,一旦用顺手了,后续新表加字段都省心。

@Intercepts({ @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}) }) @Component public class AutoFillInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { MappedStatement ms = (MappedStatement) invocation.getArgs()[0]; Object parameter = invocation.getArgs()[1]; if (parameter == null) return invocation.proceed(); // 实体类存在createTime字段则自动填充 setFieldIfExists(parameter, "createTime", LocalDateTime.now()); return invocation.proceed(); } }

拦截器这块还有一个常见用途是做数据权限过滤。比如教师只能看见自己指导的竞赛和报名记录,这个可以在拦截器里动态拼接SQL条件,也可以直接在Mapper XML里通过调用UserContext.getCurrentUserId()来实现。我在实际项目中用了后一种方式,简单直观,缺点是SQL里出现了业务逻辑,但现阶段用着更稳妥。

4. 前端Vue3后台管理端的落地细节

4.1 工程初始化与整体架构

前端工程我用Vite创建的Vue3项目,相比Vue CLI,Vite的冷启动速度快非常多,开发体验完全不是一个级别。初始化命令很简单:npm create vite@latest competition-frontend -- --template vue-ts,不过如果你是纯JavaScript党,也可以去掉vue-ts使用JS版本。我建议带TypeScript,虽然初期写类型定义会多花一点时间,但项目变大后组件的props、接口返回数据这些类型校验能省去大量排错时间。

整个前端选型是Vue3 + Vue Router + Pinia + Element Plus + Axios。Pinia是Vue3官方推荐的状态管理库,比Vuex的写法简洁很多,去掉了mutations的概念,直接在store里定义state和actions,用起来非常顺手。我项目里的用户登录状态、用户信息都放在Pinia的userStore里,刷新页面后从localStorage恢复。

目录结构上,src/views下按模块建子目录,比如views/adminviews/teacherviews/studentsrc/api目录集中管理所有接口请求函数,这个习惯非常重要。我见过很多项目把axios.get直接写在组件里,结果接口路径散落各处,改个后端地址要全局搜索替换。集中管理后,每个模块一个JS文件,比如api/competition.js里导出所有竞赛相关的请求方法,组件里引用即可。

4.2 核心页面与接口联调实现

竞赛列表页是整个系统的核心页面,集中体现了Vue3组合式API的优势。页面逻辑包含三块:搜索条件表单、数据表格、分页组件。搜索条件是竞赛名称和状态,表格展示竞赛标题、类型、级别、报名时间、状态、操作按钮,操作按钮根据状态显示"编辑"、"发布"、"查看报名"等不同动作。

用组合式API,我可以把搜索条件、表格数据、分页信息、加载方法放在一个setup函数里,逻辑内聚且清晰。一个核心点是onMounted时调用加载方法,但搜索条件的监听要用watch而不是在搜索按钮上手动绑定事件,这样更符合响应式思维。表格列里如果展示状态,我会用Element Plus的el-tag标签,通过一个映射函数把状态码转成对应的标签类型和文字,比如DRAFT显示灰色"草稿",PUBLISHED显示蓝色"报名中",一眼就能看出竞赛当前处于哪个阶段。

接口联调上,前端请求后端接口时,统一走封装好的request.js,这个模块基于Axios封装了baseURL、请求拦截器、响应拦截器。请求拦截器在每次请求前从localStorage读token并加到请求头;响应拦截器判断HTTP状态码和后端返回的code,如果code表示未登录(比如401),就跳转登录页并给出提示。这样每个请求方法只要关注业务数据本身,不需要重复处理错误提示和登录失效逻辑。

// api/competition.js import request from '@/utils/request' export function getCompetitionPage(params) { return request({ url: '/api/competition/page', method: 'get', params }) } export function createCompetition(data) { return request({ url: '/api/competition', method: 'post', data }) }

4.3 Axios封装与权限控制

Axios封装这块,最容易出问题的是响应数据格式统一。后端接口返回的数据我固定用{ code, message, data }这个结构,code为200表示成功,非200则前端弹出message作为错误提示。这个规范必须在后端接口设计时统一好,不然后端每个接口返回格式五花八门,前端拦截器写起来非常痛苦。

权限控制方面,我实现了路由级别的动态菜单。用户登录后,后端返回该用户的角色和权限列表,前端根据权限列表过滤静态路由表,生成当前用户可访问的动态路由。Vue Router提供了addRoute方法,可以在登录成功后动态添加路由。学生登录时看不到管理后台的竞赛审核菜单,教师登录时看不到用户管理菜单。

前端权限只能起到优化体验的作用,真正的安全校验一定在后端。我在前端会把用户角色存在Pinia和localStorage里,用于控制页面和按钮的显示,但每个接口调用后端时,后端的拦截器还会再校验一次token和角色。之前遇到过一种情况是前端漏加了路由守卫,用户不通过登录页直接访问某个URL,页面虽然能打开,但接口返回401触发axios拦截器自动跳回登录页,这种体验依然是有保护的。

5. 从零跑通完整流程:环境准备与联调实录

5.1 开发环境准备与初始化步骤

要把这套系统跑起来,开发环境主要有四样:JDK 8或11、MySQL 5.7或8.0、Node.js 14以上、Maven 3.6以上。JDK版本不要追新,SpringBoot 2.7.x配JDK 8是久经考验的组合,用JDK 17以上可能遇到一些不兼容的依赖。MySQL建议装8.0,驱动类名注意要写com.mysql.cj.jdbc.Driver,这是MySQL 8.0的驱动类,老版com.mysql.jdbc.Driver在8.0里已经废弃了。

前端我用的是Element Plus,版本上要确认是2.x以上才支持Vue3。如果你用npm install element-plus直接安装,默认就是最新版,没什么问题。但要注意如果项目初始化时选了Vue2的模板,Element Plus会安装失败或运行报错,这个在项目初始化时就要确认清楚。用Vite创建项目后,官方的模板选择里要选vuevue-ts,不会生成Vue2项目,这个坑相对少见。

数据库初始化,我把建库建表脚本放在sql/init.sql里,包含所有主表和初始管理员账号,用户名admin、密码123456,用BCrypt加密后写死在SQL的insert语句里,方便首次启动就能登录系统。同时预置一部分竞赛类型字典数据,比如学科竞赛、创新创业、文体竞赛等,避免第一次登录后系统里空荡荡的。

5.2 一条完整业务链路的接口与页面联调

我以一个典型的校级编程竞赛为例,说明从后端到前端、从管理员到学生的完整业务链路怎么走通。这条链路同时也是我测试系统功能是否完善的标准,一头走到尾,基本功能就齐了。

第一步管理员登录后创建竞赛,填写竞赛名称"2024校园程序设计大赛"、类型选"学科竞赛"、级别"校级"、组织者"计算机学院"、报名开始和截止时间,状态先存草稿。后端对应POST /api/competition接口,CompetitionService里将status设为DRAFT并插入数据库。前端竞赛列表页刷新后,这条记录以灰色"草稿"标签展示。

第二步管理员发布竞赛,状态从DRAFT流转到PUBLISHED。前端点击"发布"按钮,调用PUT /api/competition/{id}/publish,后端校验当前状态必须是DRAFT才能发布,发布成功后竞赛在学生端的"进行中的竞赛"列表中出现,学生可以报名。第三步学生注册登录后浏览竞赛详情,点击"立即报名",填写团队名称和联系方式后提交。报名接口写入enrollment表,状态为PENDING。第四步教师端收到待审核报名列表,审核通过后学生端报名状态变为"已通过",学生可以上传作品。

第五步竞赛截止后,管理员把竞赛状态更新为ONGOING,评委可以查看所有已通过报名学生的作品并打分。每个评委对每个作品只能评一次,评审表通过work_idreviewer_id联合唯一约束来保证。第六步评审完成后管理员把状态改为FINISHED,系统自动汇总每个作品的平均分或总分,学生端可以看到自己的成绩和排名。这条链路全部走通,说明后端接口、前端页面、数据库表之间的关联关系都是正确的。

6. 常见问题速查与个人避坑心得

6.1 高频问题排查表

开发过程中踩坑无数,很多问题在网上查资料时都有人遇到但解法分散,这里把我遇到的高频问题整理成一个速查表,按错误现象、原因、解决方案三个维度列出来,方便你直接对照排查。

错误现象根本原因解决方案
数据库连接失败,报Access denied for user 'root'@'localhost'MySQL账号密码错误或root账号不允许远程连接本地环境检查密码;远程连接需授权,GRANT ALL PRIVILEGES ON *.* TO 'root'@'%'
查询结果返回字段全是null未开启MyBatis驼峰映射mybatis.configuration.map-underscore-to-camel-case: true
分页查询total始终为0未在同一条Mapper操作前调用startPage,或分页前有别的查询确保startPage后紧跟分页查询,中间不执行其他查询
前端请求接口跨域被拦截前后端不同端口,未配置CORS后端添加CORS配置类,允许指定来源,allowedOriginPatterns("*")
Vue3引入Element Plus组件不生效版本不兼容或未完整安装插件完整引入:app.use(ElementPlus);检查element-plus版本为2.x
文件上传报MaxUploadSizeExceededExceptionSpringBoot默认上传限制为1MBapplication.yml中配置spring.servlet.multipart.max-file-size大小
com.mysql.jdbc.Driver找不到MySQL 8.0驱动类名已变使用com.mysql.cj.jdbc.Driver并检查驱动依赖版本
token失效但前端没有跳转登录页响应拦截器未处理401状态码在axios响应拦截器中判断HTTP状态码为401时清除本地用户信息并跳转/login

6.2 我踩过的几个坑

数据库连接串的时区问题,记得加到serverTimezone=Asia/Shanghai。我第一次用MySQL 8.0时,没加时区参数,项目启动报错信息像天书一样,搜了半天才发现是时区引起的。加了这个参数再配合JVM时区设置,时间存储和展示才正常。如果不用JDBC连接的serverTimezone参数,也可以在MySQL端执行SET GLOBAL time_zone = '+8:00',但推荐在连接串里写清楚,工程迁移时不会因为数据库环境不同而报错。

前端报错排查时,Vue3的报错信息比Vue2要详细,但也更啰嗦。我的习惯是看编译信息里的第一条,忽略后面的堆栈,绝大多数是组件里某个属性没定义或某个方法不存在。Element Plus表单校验时,v-model绑定的字段名要和rules里的字段名一致,不一致的表单会不触发校验,但又不会报错,这种静默失败最让人头疼。

再分享一个后端的事务经验:报名操作包含两步,插入报名记录和更新竞赛当前报名人数,这两步必须放在同一个@Transactional事务里。一开始没加事务注解,出现并发报名时数据对不上,加了之后才能保证要么都成功要么都回滚。所有涉及多表写入操作的接口,务必要加上事务控制,这是最容易被忽略但非常重要的习惯。

还有一个关于接口参数校验的教训。我最初创建竞赛时,前端传过来的start_timeend_time没有做非空和先后顺序校验,结果有一条数据报名结束时间比开始时间还早,导致前端时间选择器逻辑混乱,学生端明明已经过了截止时间但状态还显示"报名中"。后来在后端CompetitionService里加了时间校验,如果endTime早于startTime,直接抛出业务异常并提示"报名截止时间不能早于开始时间"。这类业务规则校验一定要在后端做,不能依赖前端,因为API完全可能被绕过,数据合法性和业务正确性必须由后端兜底。

根据我个人经验,如果你打算把这类项目投入到正式使用,还有两个细节值得提前考虑。一个是文件存储,作品上传如果直接用本地磁盘存储,服务器重启后文件路径可能变化,建议在配置里单独定义上传目录,最好用对象存储服务替代本地磁盘。另一个是数据备份,MySQL定时备份很重要,哪怕最简单的方式,每天凌晨用mysqldump导出一次SQL文件,也能在关键时刻保住整个竞赛周期的数据。这些都是上线之后才会意识到的问题,最好在开发阶段就规划进去。

项目做到现在,我的体会是,高校竞赛管理系统这类业务本质上是"信息流转"系统,把合适的消息在合适的时间推给合适的人,技术上没有什么高不可攀的地方,真正考验人的是对业务流程的理解和细心的数据建模。如果你正打算开发类似系统,强烈建议先把业务流程图手画一遍,再开始写数据库SQL,思路理顺了,后面的开发会顺畅很多。

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

SSI-COV随机子空间模态识别:Matlab实现与工程实践

做结构振动测试的人&#xff0c;十有八九遇到过这种尴尬&#xff1a;测了一堆加速度响应数据&#xff0c;激励力却测不到。风、车流、环境微振动一直在激励结构&#xff0c;我们根本没有办法记录真实的输入。这时候想做模态参数识别&#xff0c;传统的频响函数法直接失效&#…

作者头像 李华
网站建设 2026/9/24 22:12:51

软考网络工程师教程第六版核心解读与考点精析

我无法根据当前输入生成符合要求的博文。原因如下&#xff1a;项目正文为空&#xff1a;输入中项目正文: ""&#xff0c;即没有任何实质性描述。而我的核心任务是基于“项目标题、项目正文、关键词和摘要描述”四要素进行深度拆解与延展。其中&#xff0c;项目正文是…

作者头像 李华
网站建设 2026/9/24 22:12:10

WorkBuddy实战:AI智能工作台如何实现自动化工作流与效率提升

1. WorkBuddy 到底是什么&#xff1f;先搞清这个工具的核心定位1.1 为什么大家突然都在聊 WorkBuddyWorkBuddy 最近在跨境电商、自媒体运营和效率工具圈子里几乎成了高频词。不管是逛社区还是刷群里消息&#xff0c;你都能看到类似“我用 WorkBuddy 把订单表格搞定了”“WorkBu…

作者头像 李华
网站建设 2026/9/24 22:12:10

WorkBuddy实战:知识库问答、自动化工作流与权限隔离全解析

《WorkBuddy 实战蓝皮书》这个系列写到第五篇&#xff0c;前面已经把产品定位、核心功能、底层原理和基础配置都过了一遍。到了这一篇&#xff0c;我不打算再讲概念&#xff0c;直接拉出三个真实业务场景&#xff0c;从零开始把 WorkBuddy 完整跑一遍&#xff1a;智能客服知识库…

作者头像 李华
网站建设 2026/9/24 22:11:55

高胜算60分钟副图指标:三重确认与通达信公式实战解析

1. 先聊聊为什么我盯上60分钟级别这些年做短线,我吃过不少亏,也慢慢摸索出一套自己的节奏。很多人一打开行情软件就直奔日线,看MACD金叉、死叉,或者干脆盯分时图来回折腾。结果是什么?日线反应太慢,等你看到金叉进场,行情可能已经走完一大半;分时图又太敏感,一个假突破就能把你…

作者头像 李华
网站建设 2026/9/24 22:11:36

NeoHorse-1黑马解析:Harness与RSI如何让Agent稳定自愈

1. 这匹“黑马”到底踩中了什么痛点AI 圈每隔几个月就会冒出一个新名字&#xff0c;但大多数热闹三天就散了。NeoHorse-1 这次能被讨论&#xff0c;我认为核心不在于它跑分多高&#xff0c;而在于它把RSI、Harness、Agent这三个原本各说各话的概念拧成了一股绳。先说结论&#…

作者头像 李华