这个基于SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0技术栈的在线互动学习系统,是我在2022年为一个职业教育机构开发的线上教学平台。相比传统网课系统,它最大的特点是实现了实时问答、代码协同编辑和自动评测三大核心互动功能。系统上线后机构学员的课程完课率提升了47%,这让我深刻体会到技术选型对教学效果的实际影响。
整套系统采用前后端分离架构,后端基于SpringBoot2.7.3构建RESTful API,前端使用Vue3的组合式API开发管理后台和学员端界面,数据层采用MyBatis-Plus 3.5.2实现ORM映射,MySQL8.0提供事务支持和JSON字段存储。特别值得一提的是,我们利用WebSocket实现了教学过程中的实时互动,这是提升在线学习体验的关键设计。
选择SpringBoot2而非3.x版本是经过严格测试后的决定。在2022年项目启动时,SpringBoot3刚发布不久,其强制的Java17要求与机构现有服务器环境存在兼容性问题。实测表明,2.7.x版本在Tomcat9下的QPS能达到3200左右,完全满足300人同时在线学习的并发需求。
MyBatis-Plus的引入极大简化了数据操作代码。例如在课程管理模块中,通过继承BaseMapper只需几行代码就实现了复杂的分页查询:
java复制public Page<Course> getCoursesByCategory(Long categoryId, Integer pageNum) {
return courseMapper.selectPage(
new Page<>(pageNum, 10),
new LambdaQueryWrapper<Course>()
.eq(Course::getCategoryId, categoryId)
.orderByDesc(Course::getCreateTime)
);
}
Vue3的组合式API让前端代码组织更加灵活。在开发实时问答组件时,我们将WebSocket连接、消息处理和状态管理都封装在useChat模块中:
javascript复制// src/composables/useChat.js
export function useChat() {
const messages = ref([])
const socket = ref(null)
const connect = (roomId) => {
socket.value = new WebSocket(`wss://api.example.com/chat/${roomId}`)
socket.value.onmessage = (event) => {
messages.value.push(JSON.parse(event.data))
}
}
return { messages, connect }
}
这种设计使得问答功能可以轻松复用到不同页面,同时保持了良好的类型提示(配合TypeScript使用)。
WebSocket服务端采用Spring的STOMP协议实现,关键配置如下:
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")
.setAllowedOriginPatterns("*")
.withSockJS();
}
}
前端连接时需要处理断线重连,这是很多初学者容易忽略的点。我们的解决方案是:
javascript复制let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
function connect() {
const socket = new SockJS('/ws');
const stompClient = Stomp.over(socket);
stompClient.connect({}, () => {
reconnectAttempts = 0;
// 订阅逻辑...
}, () => {
if(reconnectAttempts < maxReconnectAttempts) {
setTimeout(() => {
reconnectAttempts++;
connect();
}, 1000 * Math.pow(2, reconnectAttempts));
}
});
}
采用Operational Transformation算法解决多人编辑冲突问题。服务端维护每个文档的版本历史,关键数据结构如下:
sql复制CREATE TABLE `code_documents` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`content` LONGTEXT,
`version` INT NOT NULL DEFAULT 0,
`operations` JSON DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
当收到客户端操作时,服务端会先进行变换(transform)再应用:
java复制public Operation transform(Operation clientOp, Operation serverOp) {
if(clientOp.getPosition() <= serverOp.getPosition()) {
return new Operation(
clientOp.getType(),
clientOp.getPosition() + serverOp.getLength(),
clientOp.getText()
);
}
return clientOp;
}
利用JSON字段存储动态表单数据,比如作业提交的附加信息:
sql复制CREATE TABLE `homework_submissions` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`user_id` BIGINT NOT NULL,
`homework_id` BIGINT NOT NULL,
`content` TEXT NOT NULL,
`attachments` JSON DEFAULT NULL,
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
INDEX `idx_user_homework` (`user_id`, `homework_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
查询时可以使用JSON_EXTRACT函数:
sql复制SELECT
id,
JSON_EXTRACT(attachments, '$.video_url') AS video_url
FROM homework_submissions
WHERE homework_id = 123;
针对课程列表页的N+1查询问题,我们采用MyBatis-Plus的@TableField注解实现延迟加载:
java复制@Data
@TableName("courses")
public class Course {
@TableId
private Long id;
private String title;
@TableField(exist = false)
private List<Chapter> chapters;
public List<Chapter> getChapters() {
if(chapters == null) {
chapters = chapterMapper.selectByCourseId(this.id);
}
return chapters;
}
}
配合Redis缓存课程基本信息,QPS从150提升到2100:
java复制@Cacheable(value = "courses", key = "#id")
public Course getById(Long id) {
return courseMapper.selectById(id);
}
Docker Compose文件整合了后端、前端和MySQL服务:
yaml复制version: '3.8'
services:
backend:
build: ./backend
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
depends_on:
- mysql
frontend:
build: ./frontend
ports:
- "80:80"
mysql:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=rootpass
- MYSQL_DATABASE=learning_system
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:
特别提醒:MySQL8默认使用caching_sha2_password认证,如果客户端不支持需要在my.cnf中添加:
code复制default_authentication_plugin=mysql_native_password
采用Spring Boot Actuator暴露健康检查端点,配合Prometheus监控:
java复制@Configuration
public class ActuatorConfig {
@Bean
public MeterRegistryCustomizer<PrometheusMeterRegistry> configureMetricsPrefix() {
return registry -> registry.config().commonTags("application", "learning-system");
}
}
日志收集使用ELK栈,Logback配置关键部分:
xml复制<appender name="ELK" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>logstash:5044</destination>
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>{"app":"learning-system","env":"${spring.profiles.active}"}</customFields>
</encoder>
</appender>
现象:移动端频繁断开连接
排查过程:
nginx复制location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
}
现象:表格数据更新后视图不刷新
原因:直接给reactive对象赋新值破坏了响应性
正确做法:
javascript复制// 错误方式
state.list = newList;
// 正确方式
state.list.splice(0, state.list.length, ...newList);
采用Guava RateLimiter实现方法级限流:
java复制@Aspect
@Component
public class RateLimitAspect {
private final RateLimiter limiter = RateLimiter.create(100); // 100次/秒
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
if(limiter.tryAcquire()) {
return joinPoint.proceed();
}
throw new BusinessException("请求过于频繁");
}
}
MyBatis-Plus虽然使用预编译语句,但自定义SQL仍需注意:
java复制// 不安全写法
@Select("SELECT * FROM users WHERE name = '${name}'")
List<User> findByName(@Param("name") String name);
// 安全写法
@Select("SELECT * FROM users WHERE name = #{name}")
List<User> findByName(@Param("name") String name);
完整的文档包含:
API文档:Swagger UI + 离线Markdown部署手册:涵盖Windows/Linux/Docker部署二次开发指南:环境配置、代码规范说明数据库字典:表结构详细说明Swagger配置示例:
java复制@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.learning.system"))
.paths(PathSelectors.any())
.build()
.apiInfo(new ApiInfoBuilder()
.title("在线学习系统API")
.version("1.0")
.license("Apache 2.0")
.build());
}
在开发过程中我们积累了一套完整的代码生成工具,可以快速生成Controller/Service/Mapper层代码,大幅提升开发效率。这套工具会根据数据库表结构自动生成包含Swagger注解的Java实体类、带参数校验的DTO对象以及对应Vue3的前端表单组件。