做高校竞赛管理系统这件事,坦白说是被学校教务老师"逼"出来的。之前学校组织各类竞赛,报名信息靠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对象用于参数接收和响应返回,避免直接把实体类暴露给前端。目录结构大致是controller、service、mapper、entity、dto、vo、config、interceptor、common这几个包,看起来常规,但对后续维护来说非常关键。
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注解标注拦截update和insert操作,在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/admin、views/teacher、views/student,src/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创建项目后,官方的模板选择里要选vue或vue-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_id和reviewer_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 |
文件上传报MaxUploadSizeExceededException | SpringBoot默认上传限制为1MB | 在application.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_time和end_time没有做非空和先后顺序校验,结果有一条数据报名结束时间比开始时间还早,导致前端时间选择器逻辑混乱,学生端明明已经过了截止时间但状态还显示"报名中"。后来在后端CompetitionService里加了时间校验,如果endTime早于startTime,直接抛出业务异常并提示"报名截止时间不能早于开始时间"。这类业务规则校验一定要在后端做,不能依赖前端,因为API完全可能被绕过,数据合法性和业务正确性必须由后端兜底。
根据我个人经验,如果你打算把这类项目投入到正式使用,还有两个细节值得提前考虑。一个是文件存储,作品上传如果直接用本地磁盘存储,服务器重启后文件路径可能变化,建议在配置里单独定义上传目录,最好用对象存储服务替代本地磁盘。另一个是数据备份,MySQL定时备份很重要,哪怕最简单的方式,每天凌晨用mysqldump导出一次SQL文件,也能在关键时刻保住整个竞赛周期的数据。这些都是上线之后才会意识到的问题,最好在开发阶段就规划进去。
项目做到现在,我的体会是,高校竞赛管理系统这类业务本质上是"信息流转"系统,把合适的消息在合适的时间推给合适的人,技术上没有什么高不可攀的地方,真正考验人的是对业务流程的理解和细心的数据建模。如果你正打算开发类似系统,强烈建议先把业务流程图手画一遍,再开始写数据库SQL,思路理顺了,后面的开发会顺畅很多。