大学生创新创业竞赛作为高校人才培养的重要环节,每年都会吸引大量学生参与。但在实际操作中,从项目申报到最终路演评审的全流程管理,往往面临诸多痛点:
我们团队在参与过三次省级双创竞赛组织工作后,决定开发这套管理系统。系统采用前后端分离架构,后端使用SpringBoot+MyBatis,前端基于Vue.js+ElementUI,数据库选用MySQL 8.0。这种技术组合既保证了系统性能,又便于后续功能扩展。
后端技术栈:
前端技术栈:
提示:选择MyBatis-Plus而非JPA是考虑到竞赛业务中存在较多复杂查询场景,需要更灵活的SQL控制。

用户中心模块
项目管理模块
评审管理模块
路演管理模块
数据统计模块
系统采用RBAC模型设计权限体系,通过Spring Security实现接口级权限控制:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/student/**").hasRole("STUDENT")
.requestMatchers("/api/judge/**").hasRole("JUDGE")
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
权限分配策略:
前端采用多步骤表单设计,使用Vue的keep-alive保持表单状态:
vue复制<template>
<el-steps :active="activeStep">
<el-step title="基本信息" />
<el-step title="项目详情" />
<el-step title="附件上传" />
</el-steps>
<keep-alive>
<component :is="currentStepComponent" @next="handleNext" />
</keep-alive>
</template>
后端接口设计考虑文件分片上传,支持大文件稳定传输:
java复制@PostMapping("/upload")
public ResponseEntity<String> uploadFile(
@RequestParam("file") MultipartFile file,
@RequestParam("chunkNumber") int chunkNumber,
@RequestParam("totalChunks") int totalChunks) {
// 实现文件分片存储与合并逻辑
fileService.saveChunk(file, chunkNumber, totalChunks);
return ResponseEntity.ok("Chunk uploaded");
}
评审表单包含以下要素:
评分计算采用加权平均算法:
sql复制SELECT
p.project_id,
p.project_name,
AVG(r.innovation_score) * 0.3
+ AVG(r.feasibility_score) * 0.4
+ AVG(r.presentation_score) * 0.3 AS total_score
FROM projects p
JOIN reviews r ON p.project_id = r.project_id
GROUP BY p.project_id, p.project_name
ORDER BY total_score DESC
projects表(项目信息)
sql复制CREATE TABLE `projects` (
`project_id` bigint NOT NULL AUTO_INCREMENT,
`project_name` varchar(100) NOT NULL,
`college` varchar(50) NOT NULL COMMENT '所属学院',
`category` enum('科技创新','文化创意','社会服务') NOT NULL,
`abstract` text NOT NULL,
`status` enum('DRAFT','SUBMITTED','REVIEWED','ROADSHOW') DEFAULT 'DRAFT',
`submit_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`project_id`),
KEY `idx_college` (`college`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
reviews表(评审记录)
sql复制CREATE TABLE `reviews` (
`review_id` bigint NOT NULL AUTO_INCREMENT,
`project_id` bigint NOT NULL,
`judge_id` bigint NOT NULL,
`innovation_score` decimal(3,1) NOT NULL CHECK (`innovation_score` BETWEEN 0 AND 10),
`feasibility_score` decimal(3,1) NOT NULL,
`presentation_score` decimal(3,1) NOT NULL,
`comments` text,
`is_confirmed` tinyint(1) DEFAULT '0',
`review_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`review_id`),
UNIQUE KEY `uk_project_judge` (`project_id`,`judge_id`),
CONSTRAINT `fk_review_project` FOREIGN KEY (`project_id`) REFERENCES `projects` (`project_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
索引策略:
查询优化:
java复制@Select("SELECT p.*, AVG(r.total_score) as avg_score " +
"FROM projects p LEFT JOIN reviews r ON p.project_id = r.project_id " +
"WHERE p.status = #{status} " +
"GROUP BY p.project_id " +
"ORDER BY p.submit_time DESC")
List<ProjectWithScore> findProjectsByStatus(@Param("status") String status);
缓存设计:
推荐部署方案:
ini复制[mysqld]
innodb_buffer_pool_size = 1G
innodb_log_file_size = 256M
max_connections = 200
使用Docker Compose编排服务:
yaml复制version: '3.8'
services:
backend:
build: ./backend
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
depends_on:
- redis
- mysql
frontend:
build: ./frontend
ports:
- "80:80"
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:7.0-alpine
ports:
- "6379:6379"
properties复制management.endpoints.web.exposure.include=health,metrics,info
management.metrics.export.prometheus.enabled=true
javascript复制// 使用Sentry捕获前端错误
import * as Sentry from '@sentry/vue';
Sentry.init({
app,
dsn: 'YOUR_DSN',
tracesSampleRate: 0.2
});
遇到的问题:
初期直接使用Spring MultipartFile接收大文件,导致内存溢出
解决方案:
properties复制spring.servlet.multipart.max-file-size=2GB
spring.servlet.multipart.max-request-size=2GB
java复制@PostMapping("/upload")
public String handleUpload(@RequestParam MultipartFile file) {
Path tempFile = Files.createTempFile("upload-", ".tmp");
file.transferTo(tempFile);
// 处理文件...
}
场景:
多位评委同时评审同一项目时可能产生冲突
实现方案:
java复制@Transactional
public synchronized ReviewResult submitReview(ReviewRequest request) {
// 检查是否已评审
if(reviewRepository.existsByProjectIdAndJudgeId(
request.projectId(), getCurrentJudgeId())) {
throw new BusinessException("已提交过评审");
}
// 保存评审记录
Review review = new Review();
// ...设置评审属性
reviewRepository.save(review);
// 更新项目状态
projectService.updateReviewStatus(request.projectId());
return new ReviewResult(review.getId());
}
使用WebSocket推送路演安排变更:
java复制@Controller
public class NotificationController {
@Autowired
private SimpMessagingTemplate messagingTemplate;
public void notifyRoadshowChange(Long projectId) {
RoadshowUpdate update = roadshowService.getUpdateInfo(projectId);
messagingTemplate.convertAndSend(
"/topic/roadshow/" + projectId,
update);
}
}
前端订阅通知:
javascript复制const socket = new SockJS('/ws');
const client = Stomp.over(socket);
client.connect({}, () => {
client.subscribe(`/topic/roadshow/${projectId}`, (message) => {
const update = JSON.parse(message.body);
// 更新UI...
});
});
智能冲突检测:
评审进度可视化:
vue复制<template>
<el-progress
:percentage="completedPercentage"
:color="customColors"
:show-text="false"
/>
</template>
移动端适配:
多维度数据分析:
AI辅助评审:
虚拟路演厅:
区块链存证:
多校联盟支持:
在实际部署过程中,我们发现系统的响应速度与评委数量成正比增长。通过引入Redis缓存评审统计结果,将平均响应时间从1200ms降低到300ms左右。对于文件导出等耗时操作,建议采用异步处理模式,通过WebSocket通知用户下载准备就绪。