1. 项目背景与核心价值
这个Java智慧题库平台本质上是一个面向教育领域的综合性试题管理系统。我在实际开发过程中发现,它最大的价值在于解决了传统题库管理的三大痛点:首先是试题分散存储导致的检索效率低下,其次是组卷过程耗时耗力,最后是缺乏智能化的学习路径推荐。
从技术架构来看,平台采用了典型的B/S模式,前端使用HTML5+CSS3+JavaScript构建响应式界面,后端基于Java EE技术栈(Spring Boot+MyBatis),数据库选用MySQL 8.0。这种组合既保证了系统性能,又便于后期扩展。
提示:选择Spring Boot框架时建议使用2.7.x稳定版本,避免最新版可能存在的兼容性问题
2. 核心功能模块详解
2.1 智能组卷引擎
组卷算法采用混合策略:
- 基于知识点的随机筛选(权重40%)
- 基于难度的梯度分布(权重30%)
- 基于历史错误率的强化训练(权重30%)
具体实现代码片段:
java复制public List<Question> generatePaper(PaperCriteria criteria) {
// 知识点筛选
List<Question> knowledgeBased = questionMapper.selectByKnowledgePoints(
criteria.getKnowledgeIds(),
(int)(criteria.getTotal() * 0.4)
);
// 难度梯度处理
List<Question> difficultyBased = questionMapper.selectByDifficulty(
criteria.getDifficultyLevels(),
(int)(criteria.getTotal() * 0.3)
);
// 错题强化
List<Question> wrongQuestionBased = getWrongQuestions(
criteria.getUserId(),
(int)(criteria.getTotal() * 0.3)
);
return mergeQuestions(knowledgeBased, difficultyBased, wrongQuestionBased);
}
2.2 多维度数据分析看板
数据可视化部分采用ECharts实现,关键指标包括:
- 知识点掌握热力图
- 答题时间分布折线图
- 正确率变化趋势图
配置示例:
javascript复制option = {
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [{
data: [820, 932, 901, 934, 1290, 1330, 1320],
type: 'line',
smooth: true
}]
};
3. 关键技术实现细节
3.1 高并发访问优化
采用三级缓存策略:
- 本地缓存(Caffeine):存储热点题目数据
- 分布式缓存(Redis):缓存试卷模板
- 数据库缓存(MySQL Query Cache)
配置参数参考:
properties复制# Caffeine配置
spring.cache.caffeine.spec=maximumSize=500,expireAfterWrite=10m
# Redis连接池
spring.redis.jedis.pool.max-active=20
spring.redis.jedis.pool.max-wait=3000ms
3.2 跨平台数据同步
通过RESTful API实现多终端数据同步,关键接口设计:
| 接口名称 | 方法 | 路径 | 参数 |
|---|---|---|---|
| 题目同步 | GET | /api/questions | page,size |
| 成绩上传 | POST | /api/scores | JSON格式成绩数据 |
| 错题本 | PUT | /api/wrong-questions | 题目ID数组 |
4. 开发环境搭建指南
4.1 基础环境配置
- JDK安装(推荐Amazon Corretto 11):
bash复制wget https://corretto.aws/downloads/latest/amazon-corretto-11-x64-linux-jdk.tar.gz
tar -xzf amazon-corretto-11-x64-linux-jdk.tar.gz
- MySQL配置优化:
sql复制# 修改innodb_buffer_pool_size
SET GLOBAL innodb_buffer_pool_size = 2G;
# 创建专用数据库用户
CREATE USER 'questionbank'@'%' IDENTIFIED BY 'StrongPassword123!';
GRANT ALL PRIVILEGES ON question_bank.* TO 'questionbank'@'%';
5. 常见问题解决方案
5.1 性能瓶颈排查
典型问题:组卷响应时间超过3秒
排查步骤:
- 检查SQL执行计划
- 分析Java线程堆栈
- 监控Redis缓存命中率
优化方案:
- 添加复合索引:
ALTER TABLE questions ADD INDEX idx_knowledge_difficulty (knowledge_id, difficulty) - 预生成试卷模板
- 启用二级缓存
5.2 移动端适配问题
解决方案:
- 使用rem替代px进行布局
- 媒体查询适配不同屏幕尺寸
- 触摸事件优化处理
CSS示例:
css复制@media (max-width: 768px) {
.question-card {
width: 100%;
padding: 0.5rem;
}
.option-btn {
min-height: 3rem;
}
}
6. 项目扩展方向
- 接入AI批改引擎:使用NLP技术实现主观题自动评分
- 构建知识图谱:通过题目关联关系构建学科知识网络
- 开发微信小程序版本:利用uni-app实现跨端发布
实现知识图谱的Neo4j示例:
cypher复制MATCH (k:KnowledgePoint)-[:RELATED_TO]->(q:Question)
WHERE k.name = '集合运算'
RETURN k, q LIMIT 50
在项目部署阶段,建议采用Docker容器化方案。这里给出完整的docker-compose.yml配置:
yaml复制version: '3.8'
services:
app:
image: openjdk:11-jre
ports:
- "8080:8080"
volumes:
- ./app.jar:/app.jar
command: java -jar /app.jar
depends_on:
- redis
- mysql
redis:
image: redis:6-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: question_bank
MYSQL_USER: questionbank
MYSQL_PASSWORD: dbpass
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
volumes:
redis_data:
mysql_data:
对于需要处理大量试题导入的场景,建议采用分段批处理策略。以下是优化后的导入流程:
- 文件预处理:
- 校验文件格式(支持Excel/JSON)
- 拆分大文件为多个1000条记录的批次
- 数据库操作:
java复制@Transactional
public void batchImport(List<Question> questions) {
int batchSize = 100;
for (int i = 0; i < questions.size(); i += batchSize) {
List<Question> batch = questions.subList(i, Math.min(i + batchSize, questions.size()));
questionMapper.batchInsert(batch); // 使用MyBatis的foreach标签实现
}
}
系统安全方面需要特别注意以下几点防护措施:
- 接口防刷:
java复制@RateLimiter(value = 10, key = "#userId")
@PostMapping("/api/submit")
public Result submitAnswer(@RequestBody AnswerDTO dto) {
// 处理提交逻辑
}
- SQL注入防护:
- 严格使用预编译语句
- 实施MyBatis的防注入检查
xml复制<select id="selectByKeyword" resultType="Question">
SELECT * FROM questions
WHERE content LIKE CONCAT('%',#{keyword},'%') <!-- 安全写法 -->
</select>
- XSS防护方案:
java复制@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers()
.xssProtection()
.and()
.contentSecurityPolicy("script-src 'self'");
}
}
在项目文档编写方面,推荐使用Swagger UI实现API文档自动化:
- 添加依赖:
xml复制<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
- 配置示例:
java复制@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.questionbank.controller"))
.paths(PathSelectors.any())
.build();
}
}
对于希望实现类似项目的开发者,我的实战建议是:
- 先从核心题库管理模块入手,逐步扩展功能
- 使用Flyway管理数据库变更
- 采用Jenkins Pipeline实现CI/CD自动化
- 性能测试推荐使用JMeter,模拟200并发用户场景
测试用例示例:
java复制@Test
@DisplayName("组卷逻辑测试")
public void testPaperGeneration() {
PaperCriteria criteria = new PaperCriteria();
criteria.setTotal(50);
criteria.setKnowledgeIds(Arrays.asList(1, 2, 3));
List<Question> paper = service.generatePaper(criteria);
assertEquals(50, paper.size());
assertTrue(paper.stream().allMatch(q ->
criteria.getKnowledgeIds().contains(q.getKnowledgeId())));
}
项目部署后,监控方案建议采用Prometheus+Grafana组合:
- 应用端配置:
properties复制management.endpoints.web.exposure.include=*
management.metrics.export.prometheus.enabled=true
- 关键监控指标:
- 平均响应时间
- JVM内存使用率
- 数据库连接池状态
- 缓存命中率
对于移动端开发,推荐使用React Native实现跨平台应用:
javascript复制import React from 'react';
import { View, Text } from 'react-native';
const QuestionItem = ({ question }) => (
<View style={styles.item}>
<Text style={styles.title}>{question.content}</Text>
{question.options.map(opt => (
<OptionButton key={opt.id} option={opt} />
))}
</View>
);
在处理用户行为分析时,可以采用埋点方案:
java复制@Aspect
@Component
public class BehaviorAspect {
@AfterReturning("execution(* com..controller.*.*(..))")
public void logUserAction(JoinPoint jp) {
UserAction action = new UserAction();
action.setMethod(jp.getSignature().getName());
action.setParams(Arrays.toString(jp.getArgs()));
action.setUserId(SecurityUtils.getCurrentUserId());
actionLogService.save(action);
}
}
数据库设计方面有几个需要特别注意的优化点:
- 试题表分库分键策略:
sql复制CREATE TABLE `questions_0` (
`id` bigint NOT NULL COMMENT '雪花算法ID',
`content` text NOT NULL,
`question_type` tinyint NOT NULL,
`knowledge_id` int NOT NULL,
`difficulty` decimal(3,1) DEFAULT 5.0,
`creator_id` bigint NOT NULL,
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_knowledge` (`knowledge_id`),
KEY `idx_creator` (`creator_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
- 答题记录表设计:
sql复制CREATE TABLE `answer_records` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`question_id` bigint NOT NULL,
`is_correct` tinyint(1) NOT NULL,
`answer_time` int NOT NULL COMMENT '答题耗时(ms)',
`user_answer` varchar(500) DEFAULT NULL,
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_question` (`user_id`,`question_id`),
KEY `idx_question` (`question_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
在开发过程中,我总结出几个提高效率的实用技巧:
- 使用Lombok减少样板代码:
java复制@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class QuestionDTO {
private Long id;
private String content;
private Integer questionType;
private List<OptionDTO> options;
}
- 利用MapStruct简化对象转换:
java复制@Mapper(componentModel = "spring")
public interface QuestionConverter {
QuestionDTO entityToDTO(Question question);
Question dtoToEntity(QuestionDTO dto);
}
- 采用Hutool工具类处理常见操作:
java复制// 密码加密
String password = SecureUtil.md5("password" + salt);
// 验证码生成
String captcha = RandomUtil.randomString(6);
// Excel导出
ExcelWriter writer = ExcelUtil.getWriter();
writer.write(questionList, true);
writer.flush(outputStream);
writer.close();
对于需要处理文件上传的功能,建议采用以下方案:
- 配置文件存储策略:
yaml复制# application.yml
file:
upload:
location: /data/uploads
max-size: 10MB
allowed-types: image/jpeg,image/png,application/pdf
- 实现文件服务:
java复制@Service
public class FileStorageService {
@Value("${file.upload.location}")
private String location;
public String store(MultipartFile file) {
String filename = UUID.randomUUID() + getExtension(file);
Path path = Paths.get(location).resolve(filename);
Files.copy(file.getInputStream(), path, StandardCopyOption.REPLACE_EXISTING);
return filename;
}
private String getExtension(MultipartFile file) {
return file.getOriginalFilename()
.substring(file.getOriginalFilename().lastIndexOf("."));
}
}
在实现搜索功能时,Elasticsearch是更好的选择:
- 索引配置:
java复制@Document(indexName = "questions")
public class QuestionES {
@Id
private Long id;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String content;
@Field(type = FieldType.Integer)
private Integer questionType;
// 其他字段...
}
- 搜索服务实现:
java复制public Page<QuestionES> search(String keyword, Pageable pageable) {
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.multiMatchQuery(keyword, "content"))
.withPageable(pageable)
.build();
return elasticsearchTemplate.queryForPage(query, QuestionES.class);
}
项目国际化方案建议采用Spring的MessageSource:
- 配置多语言文件:
properties复制# messages.properties
welcome.message=Welcome
question.empty=No questions found
# messages_zh_CN.properties
welcome.message=欢迎
question.empty=未找到题目
- 控制器中使用:
java复制@GetMapping("/welcome")
public String welcome(Locale locale) {
return messageSource.getMessage("welcome.message", null, locale);
}
对于需要定时执行的任务,如试题统计、数据备份等,推荐使用Quartz:
- 配置定时任务:
java复制@Configuration
public class QuartzConfig {
@Bean
public JobDetail statsJobDetail() {
return JobBuilder.newJob(QuestionStatsJob.class)
.withIdentity("questionStatsJob")
.storeDurably()
.build();
}
@Bean
public Trigger statsTrigger() {
CronScheduleBuilder schedule = CronScheduleBuilder.dailyAtHourAndMinute(2, 30);
return TriggerBuilder.newTrigger()
.forJob(statsJobDetail())
.withSchedule(schedule)
.build();
}
}
- 任务实现:
java复制public class QuestionStatsJob implements Job {
@Override
public void execute(JobExecutionContext context) {
// 执行统计逻辑
questionService.calculateDailyStats();
}
}
在权限控制方面,推荐使用Spring Security + 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);
}
}
- JWT工具类:
java复制public class JwtUtils {
private static final String SECRET = "your-secret-key";
private static final long EXPIRATION = 86400000; // 24小时
public static String generateToken(UserDetails user) {
return Jwts.builder()
.setSubject(user.getUsername())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
public static String getUsernameFromToken(String token) {
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
}
日志处理建议采用Logback+ELK方案:
- logback-spring.xml配置:
xml复制<configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/app.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="FILE" />
</root>
</configuration>
- 日志收集配置(filebeat.yml):
yaml复制filebeat.inputs:
- type: log
paths:
- /path/to/logs/app.*.log
fields:
app: question-bank
output.logstash:
hosts: ["logstash:5044"]
对于需要处理复杂业务逻辑的场景,建议采用领域驱动设计(DDD):
- 领域模型示例:
java复制public class Question {
private QuestionId id;
private String content;
private QuestionType type;
private Difficulty difficulty;
private List<Option> options;
public boolean validate() {
// 题目内容验证逻辑
}
public static class Builder {
// 建造者模式实现
}
}
- 领域服务:
java复制@Service
public class QuestionService {
private final QuestionRepository repository;
@Transactional
public QuestionId createQuestion(CreateQuestionCommand command) {
Question question = new Question.Builder()
.content(command.getContent())
.type(command.getType())
.options(command.getOptions())
.build();
if (!question.validate()) {
throw new InvalidQuestionException("题目验证失败");
}
return repository.save(question).getId();
}
}
在实现前端管理界面时,推荐使用Vue+ElementUI组合:
- 题目列表组件:
vue复制<template>
<el-table :data="questions" style="width: 100%">
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="content" label="题目内容"></el-table-column>
<el-table-column label="操作" width="180">
<template #default="scope">
<el-button size="mini" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="mini" type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</template>
<script>
export default {
data() {
return {
questions: []
}
},
created() {
this.fetchQuestions()
},
methods: {
async fetchQuestions() {
const res = await this.$http.get('/api/questions')
this.questions = res.data
}
}
}
</script>
- 表单验证示例:
vue复制<el-form :model="questionForm" :rules="rules" ref="form">
<el-form-item label="题目内容" prop="content">
<el-input
type="textarea"
v-model="questionForm.content"
:autosize="{ minRows: 2 }"
></el-input>
</el-form-item>
<el-form-item label="题目类型" prop="type">
<el-select v-model="questionForm.type">
<el-option
v-for="item in typeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
></el-option>
</el-select>
</el-form-item>
</el-form>
<script>
export default {
data() {
return {
questionForm: {
content: '',
type: 1
},
rules: {
content: [
{ required: true, message: '请输入题目内容', trigger: 'blur' },
{ min: 5, max: 500, message: '长度在5到500个字符', trigger: 'blur' }
]
}
}
}
}
</script>
对于需要处理复杂状态管理的场景,建议使用Vuex:
- store配置:
javascript复制import { createStore } from 'vuex'
export default createStore({
state: {
questionList: [],
currentPage: 1,
total: 0
},
mutations: {
SET_QUESTIONS(state, payload) {
state.questionList = payload.list
state.total = payload.total
},
SET_PAGE(state, page) {
state.currentPage = page
}
},
actions: {
async fetchQuestions({ commit }, params) {
const res = await api.getQuestions(params)
commit('SET_QUESTIONS', {
list: res.data.list,
total: res.data.total
})
}
}
})
- 组件中使用:
vue复制<script>
import { mapState, mapActions } from 'vuex'
export default {
computed: {
...mapState(['questionList', 'currentPage', 'total'])
},
methods: {
...mapActions(['fetchQuestions']),
handlePageChange(page) {
this.fetchQuestions({ page })
}
},
created() {
this.fetchQuestions({ page: 1 })
}
}
</script>
在实现实时通信功能(如在线考试监控)时,WebSocket是不错的选择:
- 服务端配置:
java复制@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOrigins("*")
.withSockJS();
}
}
- 前端连接示例:
javascript复制const socket = new SockJS('/ws')
const stompClient = Stomp.over(socket)
stompClient.connect({}, () => {
stompClient.subscribe('/topic/progress', (message) => {
const progress = JSON.parse(message.body)
updateProgressBar(progress)
})
})
function sendAnswer(answer) {
stompClient.send("/app/answer", {}, JSON.stringify(answer))
}
对于需要处理文件导出的功能,推荐使用Apache POI(Excel)和iText(PDF):
- Excel导出服务:
java复制public void exportQuestionsToExcel(List<Question> questions, OutputStream out) {
Workbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet("Questions");
// 创建表头
Row headerRow = sheet.createRow(0);
headerRow.createCell(0).setCellValue("ID");
headerRow.createCell(1).setCellValue("内容");
// 填充数据
for (int i = 0; i < questions.size(); i++) {
Row row = sheet.createRow(i + 1);
row.createCell(0).setCellValue(questions.get(i).getId());
row.createCell(1).setCellValue(questions.get(i).getContent());
}
workbook.write(out);
workbook.close();
}
- PDF导出服务:
java复制public void exportPaperToPDF(Paper paper, OutputStream out) throws DocumentException {
Document document = new Document();
PdfWriter.getInstance(document, out);
document.open();
// 添加标题
Paragraph title = new Paragraph(paper.getTitle(),
FontFactory.getFont(FontFactory.HELVETICA_BOLD, 18));
title.setAlignment(Element.ALIGN_CENTER);
document.add(title);
// 添加题目
for (Question q : paper.getQuestions()) {
Paragraph question = new Paragraph(q.getContent(),
FontFactory.getFont(FontFactory.HELVETICA, 12));
document.add(question);
}
document.close();
}
在实现自动化测试时,建议采用以下策略:
- 单元测试(JUnit 5):
java复制@ExtendWith(MockitoExtension.class)
class QuestionServiceTest {
@Mock
private QuestionRepository repository;
@InjectMocks
private QuestionService service;
@Test
void shouldCreateQuestion() {
CreateQuestionCommand cmd = new CreateQuestionCommand(
"测试题目", QuestionType.SINGLE_CHOICE);
when(repository.save(any())).thenAnswer(inv -> inv.getArgument(0));
QuestionId id = service.createQuestion(cmd);
assertNotNull(id);
verify(repository).save(any());
}
}
- 集成测试(Testcontainers):
java复制@Testcontainers
@SpringBootTest
class QuestionIntegrationTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
}
@Test
void shouldSaveAndRetrieveQuestion() {
// 测试数据库操作
}
}
- API测试(RestAssured):
java复制public class QuestionApiTest {
@Test
void shouldReturnQuestions() {
given()
.param("page", 1)
.param("size", 10)
.when()
.get("/api/questions")
.then()
.statusCode(200)
.body("content", hasSize(greaterThan(0)));
}
}
对于需要处理大量数据的场景,建议采用批处理和异步处理:
- Spring Batch批处理配置:
java复制@Configuration
public class BatchConfig {
@Bean
public Job importQuestionsJob(JobBuilderFactory jobs, Step step1) {
return jobs.get("importQuestionsJob")
.incrementer(new RunIdIncrementer())
.flow(step1)
.end()
.build();
}
@Bean
public Step step1(StepBuilderFactory steps, ItemReader<Question> reader,
ItemProcessor<Question, Question> processor,
ItemWriter<Question> writer) {
return steps.get("step1")
.<Question, Question>chunk(100)
.reader(reader)
.processor(processor)
.writer(writer)
.build();
}
}
- 异步处理示例:
java复制@Service
public class QuestionImportService {
@Async("taskExecutor")
public CompletableFuture<Void> importQuestionsAsync(List<Question> questions) {
// 执行耗时导入操作
return CompletableFuture.completedFuture(null);
}
}
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("Async-");
executor.initialize();
return executor;
}
}
在项目文档方面,推荐使用Asciidoctor生成专业文档:
- 示例文档结构:
asciidoc复制= 智慧题库平台开发文档
:doctype: book
:toc: left
:numbered:
== 系统架构
=== 技术栈
* 后端: Spring Boot 2.7 + MyBatis 3.5
* 前端: Vue 3 + Element Plus
* 数据库: MySQL 8.0
== API参考
=== 题目管理
[source,http,numbered]
----
GET /api/questions HTTP/1.1
Host: example.com
Accept: application/json
----
- Maven集成配置:
xml复制<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<version>2.2.1</version>
<executions>
<execution>
<id>generate-docs</id>
<phase>prepare-package</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
</execution>
</executions>
</plugin>
对于需要处理地理位置信息的场景(如线下考试点管理),可以使用PostGIS扩展:
- 数据库配置:
sql复制CREATE EXTENSION postgis;
CREATE TABLE exam_sites (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
address TEXT,
location GEOGRAPHY(POINT, 4326),
capacity INTEGER
);
- 空间查询示例:
java复制@Repository
public interface ExamSiteRepository extends JpaRepository<ExamSite, Long> {
@Query(value = "SELECT * FROM exam_sites WHERE ST_DWithin(location, ST_MakePoint(:lng, :lat)::geography, :radius)",
nativeQuery = true)
List<ExamSite> findNearby(@Param("lng") double longitude,
@Param("lat") double latitude,
@Param("radius") double radiusInMeters);
}
在实现多租户功能时,可以采用以下方案:
- 租户识别拦截器:
java复制public class TenantInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
String tenantId = request.getHeader("X-Tenant-ID");
if (StringUtils.isEmpty(tenantId)) {
throw new TenantException("租户标识缺失");
}
TenantContext.setCurrentTenant(tenantId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
TenantContext.clear();
}
}
- 动态数据源路由:
java复制public class TenantDataSourceRouter extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TenantContext.getCurrentTenant();
}
}
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "tenants")
public Map<Object, Object> tenantDataSources() {
return new HashMap<>();
}
@Bean
public DataSource dataSource() {
TenantDataSourceRouter router = new TenantDataSourceRouter();
router.setTargetDataSources(tenantDataSources());
router.setDefaultTargetDataSource(tenantDataSources().values().iterator().next());
return router;
}
}
对于需要版本控制的实体(如试题修改历史),可以采用Envers实现审计:
- 配置审计:
java复制@Configuration
@EnableJpaAuditing
@EnableJpaRepositories
@EntityScan
public class JpaConfig {
@Bean
public AuditorAware<String> auditorAware() {
return () -> Optional.of(SecurityUtils.getCurrentUsername());
}
}
@Entity
@Audited
public class Question {
@Id
@GeneratedValue
private Long id;
private String content;
@CreatedBy
private String createdBy;
@LastModifiedBy
private String modifiedBy;
}
- 查询历史记录:
java复制public List<Question> getQuestionHistory(Long questionId) {
AuditReader reader = AuditReaderFactory.get(entityManager);
List<Number> revisions = reader.getRevisions(Question.class, questionId);
return revisions.stream()
.map(rev -> reader.find(Question.class, questionId, rev))
.collect(Collectors.toList());
}
在实现全文检索功能时,除了Elasticsearch,也可以考虑使用数据库内置功能:
- MySQL全文索引:
sql复制ALTER TABLE questions ADD FULLTEXT INDEX ft_content (content);
SELECT * FROM questions
WHERE MATCH(content) AGAINST('java 集合' IN NATURAL LANGUAGE MODE);
- PostgreSQL全文搜索:
sql复制ALTER TABLE questions ADD COLUMN search_vector tsvector;
UPDATE questions SET search_vector = to_tsvector('english', content);
CREATE INDEX idx_search_vector ON questions USING gin(search_vector);
SELECT * FROM questions
WHERE search_vector @@ to_tsquery('english', 'java & collection');
对于需要处理动态表单的场景(如自定义题型),可以采用JSON字段存储:
- JPA自定义类型处理:
java复制@Entity
public class Question {
@Id
private Long id;
@Column(columnDefinition = "json")
@Convert(converter = JsonNodeConverter.class)
private JsonNode customFields;
}
@Converter
public class JsonNodeConverter implements AttributeConverter<JsonNode, String> {
@Override
public String convertToDatabaseColumn(JsonNode attribute) {
return attribute.toString();
}
@Override
public JsonNode convertToEntityAttribute(String dbData) {
try {
return new ObjectMapper().readTree(dbData);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
- 查询JSON字段(MySQL):
java复制@Query(value = "SELECT * FROM questions WHERE JSON_EXTRACT(custom_fields, '$.difficulty') > :minDifficulty",
nativeQuery = true)
List<Question> findByCustomDifficulty(@Param("minDifficulty") int min);
在实现自动化部署时,推荐使用Ansible进行服务器配置:
- 部署playbook示例:
yaml复制- hosts: question_servers
become: yes
tasks:
- name: Install JDK
apt:
name: openjdk-11-jdk
state: present
- name: Create app directory
file:
path: /opt/question-bank
state: directory
mode: '0755'
- name: Copy application jar
copy:
src: target/question-bank.jar
