在高校信息化建设进程中,竞赛管理系统的数字化转型已成为提升教学管理效率的关键环节。作为一名参与过多个高校信息化项目的全栈开发者,我深刻理解传统纸质化竞赛管理存在的痛点:报名表堆积如山、评审进度不透明、成绩统计易出错等。本文将基于SpringBoot+Vue+MyBatis技术栈,详细拆解一套企业级竞赛管理系统的实现方案。
这套系统主要解决三个维度的核心问题:
特别值得注意的是状态机设计(如竞赛状态的0/1/2枚举),这种设计模式使得业务流程具有明确的阶段划分,便于后续扩展审计日志等企业级功能。
选择SpringBoot+Vue+MyBatis组合主要基于以下考量:
这里特别说明一个踩坑经验:早期版本尝试用JPA实现,但在处理多表关联查询时遭遇N+1查询问题,最终切换为MyBatis的动态SQL方案。
竞赛信息表(contest_info)的设计体现了几个关键设计原则:
sql复制CREATE TABLE `contest_info` (
`contest_id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`contest_name` VARCHAR(50) NOT NULL COMMENT '竞赛名称',
`sponsor_info` VARCHAR(100) DEFAULT '' COMMENT '主办方信息',
`start_time` DATETIME NOT NULL COMMENT '开始时间',
`end_time` DATETIME NOT NULL COMMENT '结束时间',
`max_team_size` INT DEFAULT 5 COMMENT '最大团队人数',
`status` TINYINT DEFAULT 0 COMMENT '0未开始/1进行中/2已结束',
`create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
设计亮点:
在高并发报名场景下,我们为团队报名表(team_registration)添加了复合索引:
sql复制ALTER TABLE `team_registration`
ADD INDEX `idx_contest_leader` (`contest_id`, `leader_id`);
这个索引设计解决了两个性能瓶颈:
实测表明,在10万级数据量下,查询性能提升约15倍。这里有个重要经验:高校系统往往初期数据量小,但必须提前考虑毕业设计答辩等集中使用时的峰值压力。
主启动类的配置要点:
java复制@SpringBootApplication
@MapperScan("com.contest.mapper")
@EnableTransactionManagement // 关键注解:启用声明式事务
public class ContestApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(ContestApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(ContestApplication.class, args);
}
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor(); // MyBatis分页插件
}
}
关键配置说明:
评审打分服务包含几个技术要点:
java复制@Service
@Transactional
public class ReviewService {
@Autowired
private ReviewMapper reviewMapper;
public void submitReview(ReviewDTO dto) {
// 校验评审时间是否在竞赛周期内
Contest contest = contestMapper.selectById(dto.getContestId());
if (LocalDateTime.now().isAfter(contest.getEndTime())) {
throw new BusinessException("竞赛已结束,不能继续评审");
}
// 防止重复评审
Integer count = reviewMapper.selectCount(
new QueryWrapper<Review>()
.eq("registration_id", dto.getRegistrationId())
.eq("judge_id", dto.getJudgeId()));
if (count > 0) {
throw new BusinessException("您已评审过该作品");
}
// 保存评审记录
Review entity = new Review();
BeanUtils.copyProperties(dto, entity);
reviewMapper.insert(entity);
// 更新团队平均分(异步处理)
updateTeamAverageScore(dto.getRegistrationId());
}
@Async
public void updateTeamAverageScore(Long registrationId) {
// 计算平均分逻辑...
}
}
业务防护要点:
采用模块化结构组织代码:
code复制src/
├── api/ # API请求封装
│ ├── contest.js # 竞赛相关接口
│ └── review.js # 评审相关接口
├── components/
│ ├── common/ # 通用组件
│ └── business/ # 业务组件
├── store/ # Vuex状态管理
│ ├── modules/
│ └── index.js
└── views/
├── admin/ # 管理端页面
└── student/ # 学生端页面
优化实践:
使用Element UI实现带校验的评审表格:
vue复制<template>
<el-form :model="form" :rules="rules" ref="reviewForm">
<el-table :data="submissions">
<el-table-column prop="teamName" label="团队名称"/>
<el-table-column label="评分">
<template #default="{row}">
<el-form-item
:prop="'scores.' + row.id + '.value'"
:rules="[{validator: validateScore, trigger: 'blur'}]">
<el-input-number
v-model="form.scores[row.id].value"
:min="0" :max="100" :step="1"/>
</el-form-item>
</template>
</el-table-column>
</el-table>
</el-form>
</template>
<script>
export default {
data() {
return {
form: { scores: {} },
rules: {
'comments': [{ required: true, message: '请填写评语' }]
}
}
},
methods: {
validateScore(rule, value, callback) {
if (value < 60 && !this.form.comments) {
callback(new Error('低分必须填写评语'));
} else {
callback();
}
}
}
}
</script>
交互细节:
推荐使用Docker Compose编排服务:
yaml复制version: '3'
services:
mysql:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
volumes:
- ./mysql-data:/var/lib/mysql
ports:
- "3306:3306"
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- mysql
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/contest
frontend:
build: ./frontend
ports:
- "80:80"
关键配置项:
SpringBoot Actuator配置示例:
properties复制# application-prod.properties
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always
management.metrics.tags.application=contest-system
配合Prometheus+Grafana实现可视化监控:
现象:团队报名时出现重复成员
解决方案:采用分布式锁控制并发
java复制public boolean joinTeam(Long teamId, String studentId) {
String lockKey = "team:lock:" + teamId;
try {
// 尝试获取锁(TTL 3秒)
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 3, TimeUnit.SECONDS);
if (locked != null && locked) {
// 核心业务逻辑
return doJoinTeam(teamId, studentId);
}
throw new BusinessException("操作过于频繁");
} finally {
redisTemplate.delete(lockKey);
}
}
常见错误:Nginx配置不当导致大文件上传中断
解决方案:
nginx复制# nginx.conf
client_max_body_size 50M;
proxy_read_timeout 300s;
同时需要在SpringBoot中配置:
properties复制spring.servlet.multipart.max-file-size=50MB
spring.servlet.multipart.max-request-size=50MB
通过uniapp实现移动端报名:
基于ECharts实现:
javascript复制// 竞赛参与度统计
function initParticipationChart() {
const chart = echarts.init(document.getElementById('chart'));
chart.setOption({
dataset: {
source: [
['竞赛', '报名数', '提交数'],
['数学建模', 120, 95],
['程序设计', 200, 180]
]
},
xAxis: { type: 'category' },
yAxis: {},
series: [
{ type: 'bar', encode: { x: '竞赛', y: '报名数' }},
{ type: 'bar', encode: { x: '竞赛', y: '提交数' }}
]
});
}
这套系统在实际部署后,某高校的竞赛管理效率提升了70%以上。最大的收获是验证了适度的技术选型(不盲目追求新技术)对高校信息化项目的必要性。对于想要二次开发的同行,建议先从评审流程定制入手,这是最能体现各校特色的模块。