1. 项目概述与技术栈选型
这个基于SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0的知识竞赛系统,是一个典型的现代化全栈Web应用。我在实际开发中发现,这种技术组合特别适合需要快速迭代的中小型Web项目,既能保证后端服务的稳定性,又能提供流畅的前端交互体验。
系统采用前后端分离架构,后端基于SpringBoot2构建RESTful API,前端使用Vue3实现响应式界面,数据持久层采用MyBatis-Plus简化数据库操作,MySQL8.0作为关系型数据库存储核心业务数据。这种技术组合在当前Java Web开发领域非常主流,我在多个实际项目中验证过其可靠性和开发效率。
提示:选择SpringBoot2而非SpringBoot3,主要是考虑到企业生产环境中Java8/11仍占主流,SpringBoot2对低版本JDK兼容性更好,第三方库生态也更成熟。
2. 系统架构设计与核心模块
2.1 后端服务分层设计
后端采用经典的三层架构,但针对知识竞赛场景做了特殊优化:
- 控制层(Controller):处理HTTP请求,我特别设计了统一的响应封装:
java复制@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType, Class selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
// 统一封装为{code,msg,data}结构
}
}
-
服务层(Service):业务逻辑核心,包含:
- 题目管理服务(QuestionService)
- 竞赛管理服务(ContestService)
- 用户成绩服务(ScoreService)
-
数据访问层(Mapper):通过MyBatis-Plus增强功能实现:
java复制public interface QuestionMapper extends BaseMapper<Question> {
@Select("SELECT * FROM question WHERE type = #{type} ORDER BY RAND() LIMIT #{count}")
List<Question> selectRandomByType(@Param("type") int type, @Param("count") int count);
}
2.2 前端模块化设计
Vue3的组合式API让前端代码组织更灵活:
javascript复制// 竞赛页面逻辑
const { contestId } = useRoute().params
const { data: contest } = useContestInfo(contestId)
const { data: questions } = useQuestions(contestId)
// 使用Pinia管理全局状态
const store = useUserStore()
const isLoggedIn = computed(() => store.isAuthenticated)
3. 关键技术实现细节
3.1 竞赛题目随机生成算法
为避免题目顺序固定,我设计了加权随机算法:
java复制public List<Question> generateQuestionSet(int contestId) {
// 1. 按题型和难度分类
Map<String, List<Question>> categorized = questionMapper.selectByContest(contestId)
.stream()
.collect(Collectors.groupingBy(q -> q.getType() + "_" + q.getDifficulty()));
// 2. 按分类权重随机选取
List<Question> result = new ArrayList<>();
categorized.forEach((key, list) -> {
int count = calculateCountByWeight(key); // 根据题型难度计算应选数量
Collections.shuffle(list);
result.addAll(list.subList(0, Math.min(count, list.size())));
});
// 3. 最终随机排序
Collections.shuffle(result);
return result;
}
3.2 实时成绩计算与排名
使用MySQL8.0的窗口函数高效计算排名:
sql复制SELECT
user_id,
SUM(score) AS total_score,
RANK() OVER (ORDER BY SUM(score) DESC) AS ranking
FROM user_answer
WHERE contest_id = #{contestId}
GROUP BY user_id
LIMIT 100;
3.3 防止重复提交的并发控制
在提交答案时采用乐观锁:
java复制@Transactional
public AnswerResult submitAnswer(AnswerDTO dto) {
// 检查是否已提交
if (answerMapper.exists(Wrappers.<UserAnswer>lambdaQuery()
.eq(UserAnswer::getUserId, dto.getUserId())
.eq(UserAnswer::getQuestionId, dto.getQuestionId()))) {
throw new BusinessException("请勿重复提交");
}
// 记录答案并计分
UserAnswer answer = new UserAnswer();
BeanUtils.copyProperties(dto, answer);
answer.setScore(calculateScore(dto));
answerMapper.insert(answer);
// 更新用户总成绩
userScoreMapper.updateScore(dto.getUserId(), dto.getContestId(), answer.getScore());
return new AnswerResult(answer.getScore());
}
4. 开发环境配置与常见问题解决
4.1 后端开发环境搭建
- JDK版本问题:
bash复制# 确保使用JDK11+
java -version
- Lombok兼容性问题:
在pom.xml中明确指定版本:
xml复制<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
- MySQL8.0连接配置:
yaml复制spring:
datasource:
url: jdbc:mysql://localhost:3306/quiz_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: yourpassword
driver-class-name: com.mysql.cj.jdbc.Driver
4.2 前端开发环境问题
- Vue3与Element Plus集成:
javascript复制import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
- 路由传参问题:
javascript复制// 正确获取路由参数
import { useRoute } from 'vue-router'
const route = useRoute()
console.log(route.params.id)
- PDF.js集成:
bash复制npm install pdfjs-dist@2.14.305
使用worker时注意:
javascript复制import * as pdfjsLib from 'pdfjs-dist/build/pdf'
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry'
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker
5. 系统安全与性能优化
5.1 安全防护措施
- JWT认证实现:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
- XSS防护:
前端使用DOMPurify净化输入:
javascript复制import DOMPurify from 'dompurify'
const clean = DOMPurify.sanitize(dirtyHtml)
5.2 性能优化实践
- Redis缓存热点数据:
java复制@Cacheable(value = "questions", key = "#contestId")
public List<QuestionDTO> getContestQuestions(Integer contestId) {
return questionMapper.selectByContest(contestId);
}
- MyBatis-Plus二级缓存:
yaml复制mybatis-plus:
configuration:
cache-enabled: true
- Vue3组件懒加载:
javascript复制const CompetitionList = defineAsyncComponent(() =>
import('./views/CompetitionList.vue')
)
6. 部署与监控方案
6.1 生产环境部署
- 后端打包与运行:
bash复制mvn clean package -DskipTests
java -jar target/quiz-system.jar --spring.profiles.active=prod
- 前端构建:
bash复制npm run build
# 输出到dist目录,配置Nginx指向该目录
- MySQL8.0优化配置:
ini复制[mysqld]
innodb_buffer_pool_size = 1G
innodb_log_file_size = 256M
query_cache_type = 1
query_cache_size = 64M
6.2 监控与日志
- SpringBoot Actuator集成:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- ELK日志收集:
yaml复制logging:
file:
name: logs/quiz-system.log
logstash:
enabled: true
host: localhost
port: 5044
- 前端错误监控:
javascript复制import * as Sentry from '@sentry/vue'
Sentry.init({
app,
dsn: 'your-dsn',
tracesSampleRate: 0.2
})
在项目开发过程中,我发现最难调试的部分是竞赛过程中的并发控制。最初使用简单的数据库锁导致性能瓶颈,后来改用Redis分布式锁+本地缓存的混合方案,既保证了数据一致性又提升了响应速度。具体实现中需要注意锁的粒度控制,过大会影响并发,过小会增加复杂度。
