高校竞赛管理系统是连接教务处、二级学院、指导教师和学生四方的枢纽平台。传统的人工管理方式存在报名信息混乱、材料审核滞后、成绩统计误差等问题。这套基于SpringBoot+Vue+MyBatis的企业级系统,通过流程引擎和角色权限控制,实现了从竞赛发布、团队报名、中期检查到结果公示的全生命周期管理。
我在实际部署中发现,系统特别适合50人以上规模的高校使用。某理工类院校应用后,年度竞赛项目处理效率提升300%,材料归档完整率达到100%。系统采用前后端分离架构,前端Vue组件库选用Element-UI,后端SpringBoot版本为2.7.3,MySQL建议使用5.7以上版本以保证事务完整性。
系统采用经典的三层架构:
特别值得注意的是权限控制方案:采用RBAC模型扩展,除常规角色外,增加了"竞赛负责人"临时角色,解决跨部门协作时的权限动态分配问题。权限标识符设计示例:
java复制@PreAuthorize("hasRole('contest_admin') ||
hasPermission(#contestId,'contest:manage')")
public void updateContest(Long contestId) {...}
MySQL表结构设计中,这几个表关系值得关注:
sql复制CREATE TABLE `contest` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(100) COLLATE utf8mb4_bin NOT NULL,
`max_team_member` tinyint DEFAULT '5',
`sign_up_start` datetime NOT NULL,
`sign_up_end` datetime NOT NULL,
`status` enum('draft','published','processing','finished')
COLLATE utf8mb4_bin DEFAULT 'draft',
PRIMARY KEY (`id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
采用状态机模式实现审核流程,典型流程包括:
状态转换使用策略模式实现:
java复制public interface ContestStateHandler {
void handle(ContestContext context);
}
@Service
@RequiredArgsConstructor
public class ContestPublishHandler implements ContestStateHandler {
private final EmailService emailService;
@Override
@Transactional
public void handle(ContestContext context) {
// 状态校验逻辑
if (!context.getContest().getStatus().equals("approved")) {
throw new IllegalStateException("只有已审核竞赛可发布");
}
// 业务处理
context.getContest().setStatus("published");
context.getContest().setPublishTime(LocalDateTime.now());
// 通知相关教师
emailService.sendPublishNotice(context.getContest());
}
}
针对竞赛材料的特殊性,文件系统设计要点:
前端上传组件关键配置:
vue复制<el-upload
:action="uploadUrl"
:headers="authHeaders"
:data="{ contestId, phaseType }"
:before-upload="checkFileType"
:on-success="handleSuccess"
:limit="5"
:file-list="fileList">
<el-button type="primary">点击上传</el-button>
<template #tip>
<div class="el-upload__tip">
支持格式:PDF/DOCX/PPTX,单个文件不超过50MB
</div>
</template>
</el-upload>
在热门竞赛开放报名时,采用以下优化策略:
java复制public boolean joinContest(Long contestId, Long userId) {
String lockKey = "contest:join:" + contestId;
// 获取分布式锁(设置3秒超时)
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 3, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("操作过于频繁,请稍后重试");
}
try {
// 乐观锁更新
int updated = contestMapper.reduceQuota(
contestId,
"remaining_quota > 0");
return updated > 0;
} finally {
redisTemplate.delete(lockKey);
}
}
通过自定义数据权限拦截器实现:
java复制@Interceptor
@RequiredArgsConstructor
public class DataPermissionInterceptor implements HandlerInterceptor {
private final DeptService deptService;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
// 获取当前用户权限范围
List<Long> accessibleDeptIds = deptService.getAccessibleDepts();
// 存入ThreadLocal
DataPermissionHolder.setDeptIds(accessibleDeptIds);
return true;
}
}
// MyBatis拦截器片段
@Intercepts(@Signature(type= Executor.class, method="query",
args={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class DataPermissionFilter implements Interceptor {
public Object intercept(Invocation invocation) {
// 解析原始SQL
BoundSql boundSql = ((MappedStatement)invocation.getArgs()[0])
.getBoundSql(parameterObject);
// 添加部门过滤条件
String newSql = addDeptCondition(boundSql.getSql());
resetSql(invocation, newSql);
return invocation.proceed();
}
}
推荐使用Docker Compose部署:
yaml复制version: '3.8'
services:
backend:
image: openjdk:11-jre
ports:
- "8080:8080"
volumes:
- ./application-prod.yml:/config/application.yml
depends_on:
- redis
- mysql
frontend:
image: nginx:1.21
ports:
- "80:80"
volumes:
- ./dist:/usr/share/nginx/html
- ./nginx.conf:/etc/nginx/conf.d/default.conf
mysql:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS}
MYSQL_DATABASE: contest
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:6-alpine
ports:
- "6379:6379"
volumes:
mysql_data:
bash复制-Xms4g -Xmx4g -XX:MaxMetaspaceSize=512m
-XX:+UseG1GC -XX:MaxGCPauseMillis=200
ini复制[mysqld]
innodb_buffer_pool_size = 2G
innodb_log_file_size = 256M
max_connections = 300
微信通知集成:
java复制@Service
@RequiredArgsConstructor
public class WechatNoticeService {
private final WechatClient wechatClient;
@Async
public void sendTemplateMessage(String openId,
TemplateMessage message) {
// 构建消息体
WechatMsg msg = new WechatMsg();
msg.setTouser(openId);
msg.setTemplate_id("竞赛状态通知模板ID");
msg.setData(Map.of(
"status", new Value(message.getStatus()),
"remark", new Value(message.getTips())
));
// 调用微信接口
wechatClient.send(msg);
}
}
数据分析看板:
sql复制-- 院系竞赛统计视图
CREATE VIEW dept_contest_stats AS
SELECT
d.name AS dept_name,
COUNT(c.id) AS total_contests,
SUM(CASE WHEN c.status='finished' THEN 1 ELSE 0 END) AS finished_contests,
AVG(t.member_count) AS avg_team_members
FROM department d
LEFT JOIN contest c ON d.id = c.dept_id
LEFT JOIN (
SELECT contest_id, COUNT(*) AS member_count
FROM team_member
GROUP BY contest_id
) t ON c.id = t.contest_id
GROUP BY d.id;
code复制src/
├── views/
│ ├── contest/
│ │ ├── CreateContest.vue
│ │ ├── ContestList.vue
│ ├── team/
│ │ ├── TeamManage.vue
系统已在多个高校稳定运行2年以上,处理过单日最高3000+报名请求的业务峰值。建议新部署时重点关注院系数据隔离和文件存储方案,这两个模块最容易出现权限漏洞和性能瓶颈。