1. 项目概述:企业级在线教育平台的技术选型与架构设计
这套基于SpringBoot+Vue+MyBatis+MySQL的企业级在线教育平台管理系统,是当前在线教育行业数字化转型的典型技术解决方案。我在实际教育信息化项目实施中发现,这种技术组合能够很好地平衡开发效率、系统性能和可维护性需求。
核心架构采用前后端分离模式:SpringBoot作为后端服务框架提供RESTful API,Vue.js构建响应式前端界面,MyBatis处理数据持久化,MySQL存储业务数据。这种架构选择源于三个实际需求:首先,教育平台的用户并发量通常在数千到数万级别,需要支持课程直播、资料下载等高IO操作;其次,业务模块复杂(课程管理、学员管理、支付系统等),需要清晰的代码组织;最后,教育行业对数据安全有严格要求,需要完善的权限控制和审计功能。
提示:选择SpringBoot而非传统Spring MVC的主要考虑是其自动配置特性可以快速搭建微服务架构,这对需要对接第三方直播服务、支付接口的教育平台尤为重要。
2. 技术栈深度解析与实战配置
2.1 SpringBoot后端核心配置
教育平台的后端需要特殊关注高并发和事务管理。建议采用以下配置(application.yml示例):
yaml复制server:
port: 8080
tomcat:
max-threads: 200 # 直播场景需要更高线程数
min-spare-threads: 20
spring:
datasource:
url: jdbc:mysql://localhost:3306/edu_platform?useSSL=false&serverTimezone=UTC
username: root
password: 加密密码
hikari:
maximum-pool-size: 50 # 根据MySQL配置调整
connection-timeout: 30000
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8 # 教育平台需要统一时区
特别注意:教育系统常遇到的时区问题可以通过在MySQL连接串添加serverTimezone=Asia/Shanghai解决,避免课程时间显示错误。
2.2 Vue前端工程化实践
教育平台前端需要处理的核心难点包括:
- 视频播放器集成(支持m3u8格式直播流)
- 实时聊天组件
- 复杂的表单验证(课程报名、资料上传等)
推荐使用以下vue.config.js配置优化构建:
javascript复制module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
ws: true, // 支持WebSocket用于实时通知
changeOrigin: true
}
}
},
chainWebpack: config => {
config.optimization.splitChunks({
chunks: 'all',
cacheGroups: {
videoPlayer: {
test: /[\\/]node_modules[\\/](video\.js|vue-video-player)[\\/]/,
name: 'video-player',
priority: 10
}
}
})
}
}
2.3 MyBatis优化策略
教育平台的数据访问层面临的主要挑战是:
- 动态课程查询条件组合
- 大数据量分页性能
- 敏感数据脱敏
建议采用MyBatis-Plus增强功能,配置示例:
java复制@Configuration
@MapperScan("com.eduplatform.mapper")
public class MyBatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件(优化百万级数据分页)
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL){
@Override
public void beforeQuery(...) {
// 教育平台特有的分页逻辑
}
});
// 数据权限插件(不同角色看到不同课程数据)
interceptor.addInnerInterceptor(new DataPermissionInterceptor());
return interceptor;
}
}
3. 数据库设计与性能优化
3.1 核心表结构设计
教育平台的MySQL数据库设计需要特别注意以下几点:
sql复制CREATE TABLE `edu_course` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`title` varchar(100) NOT NULL COMMENT '课程名称',
`cover_url` varchar(255) DEFAULT NULL COMMENT '封面图',
`teacher_id` bigint(20) NOT NULL COMMENT '讲师ID',
`subject_id` int(11) DEFAULT NULL COMMENT '学科分类',
`lesson_num` int(11) DEFAULT '0' COMMENT '课时数',
`price` decimal(10,2) DEFAULT '0.00' COMMENT '价格',
`status` tinyint(4) DEFAULT '0' COMMENT '状态',
`view_count` bigint(19) DEFAULT '0' COMMENT '浏览数量',
`version` bigint(20) DEFAULT '1' COMMENT '乐观锁版本号',
`is_deleted` tinyint(1) DEFAULT '0' COMMENT '逻辑删除',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_teacher_id` (`teacher_id`),
KEY `idx_subject_id` (`subject_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程表';
关键设计要点:
- 使用utf8mb4字符集支持emoji等特殊字符(学员评论常用)
- 为高频查询字段建立复合索引
- 添加乐观锁字段避免并发修改问题
- 逻辑删除而非物理删除
3.2 查询性能优化实战
教育平台典型的复杂查询场景及解决方案:
场景1:多条件课程筛选
xml复制<select id="selectCourseByCondition" resultType="CourseVO">
SELECT * FROM edu_course
<where>
<if test="title != null and title != ''">
AND title LIKE CONCAT('%',#{title},'%')
</if>
<if test="teacherId != null">
AND teacher_id = #{teacherId}
</if>
<if test="subjectParentId != null">
AND subject_id IN (
SELECT id FROM edu_subject WHERE parent_id = #{subjectParentId}
)
</if>
<if test="priceOrder != null">
ORDER BY price ${priceOrder}
</if>
</where>
</select>
注意:使用${}直接拼接SQL片段时(如排序字段),必须在前端或服务层做严格参数校验,防止SQL注入。教育平台尤其要注意安全审计。
场景2:大数据量分页优化
sql复制-- 传统分页(数据量大时性能差)
SELECT * FROM edu_comment WHERE course_id = 101 LIMIT 100000, 10;
-- 优化方案:使用索引覆盖+延迟关联
SELECT * FROM edu_comment t1
JOIN (SELECT id FROM edu_comment WHERE course_id = 101 LIMIT 100000, 10) t2
ON t1.id = t2.id;
4. 典型业务模块实现
4.1 视频点播与直播集成
教育平台的核心功能是视频服务,推荐技术方案:
- 点播:七牛云/阿里云OSS存储 + 自研CDN分发
- 直播:腾讯云直播API + WebRTC备用方案
SpringBoot集成示例:
java复制@RestController
@RequestMapping("/api/video")
public class VideoController {
@Value("${txlive.pushKey}")
private String pushKey;
@PostMapping("/generateUrl")
public Result generateLiveUrl(@RequestParam Long courseId) {
String streamId = "edu_" + courseId + "_" + System.currentTimeMillis();
String txTime = Long.toHexString(System.currentTimeMillis()/1000 + 3600).toUpperCase();
String input = pushKey + streamId + txTime;
String txSecret = DigestUtils.md5Hex(input);
return Result.success()
.data("pushUrl", "rtmp://push.example.com/live/" + streamId)
.data("playUrl", "https://play.example.com/live/" + streamId + ".m3u8");
}
}
前端Vue播放器集成要点:
vue复制<template>
<video-player
:options="{
autoplay: false,
controls: true,
sources: [{
type: 'application/x-mpegURL',
src: playUrl
}],
poster: coverUrl
}"
@ready="onPlayerReady"
/>
</template>
<script>
import 'video.js/dist/video-js.css'
import { videoPlayer } from 'vue-video-player'
export default {
components: { videoPlayer },
methods: {
onPlayerReady(player) {
// 处理教育平台特有的播放器事件
player.on('error', () => {
this.$message.error('视频加载失败,请检查网络')
})
}
}
}
</script>
4.2 支付系统设计与安全
教育平台的支付模块需要特别注意:
- 多支付渠道接入(微信、支付宝、银联)
- 订单状态一致性
- 退款流程处理
建议的支付状态机设计:
java复制public enum OrderStatus {
UNPAID(0, "待支付"),
PAID(1, "已支付"),
CANCELLED(2, "已取消"),
REFUNDING(3, "退款中"),
REFUNDED(4, "已退款");
// 状态转换校验逻辑
public static boolean canTransfer(OrderStatus from, OrderStatus to) {
switch (from) {
case UNPAID:
return to == PAID || to == CANCELLED;
case PAID:
return to == REFUNDING;
// 其他状态转换规则...
}
}
}
支付回调处理的核心要点:
java复制@Transactional
public String handleWxPayNotify(Map<String, String> params) {
// 1. 验证签名
if (!WxPayUtil.isSignatureValid(params, wxPayKey)) {
throw new RuntimeException("签名验证失败");
}
// 2. 处理幂等(防止重复通知)
String orderNo = params.get("out_trade_no");
Order order = orderMapper.selectByOrderNo(orderNo);
if (order.getStatus() != OrderStatus.UNPAID) {
return "<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>";
}
// 3. 更新订单状态
order.setStatus(OrderStatus.PAID);
order.setPaymentTime(new Date());
orderMapper.updateById(order);
// 4. 发放课程权限
courseService.grantAccess(order.getUserId(), order.getCourseId());
return "<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>";
}
5. 部署与运维实践
5.1 生产环境部署方案
教育平台的典型部署架构:
code复制前端Nginx(静态资源+负载均衡)
↑
SpringBoot集群(2-4节点)
↑
MySQL主从(1主2从)+ Redis缓存
↑
文件存储(OSS/NFS)
Docker Compose开发环境示例:
yaml复制version: '3'
services:
mysql:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: edu_platform
ports:
- "3306:3306"
volumes:
- ./mysql/data:/var/lib/mysql
- ./mysql/conf:/etc/mysql/conf.d
redis:
image: redis:6
ports:
- "6379:6379"
volumes:
- ./redis/data:/data
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- mysql
- redis
environment:
SPRING_PROFILES_ACTIVE: dev
frontend:
build: ./frontend
ports:
- "80:80"
volumes:
- ./frontend:/app
- /app/node_modules
5.2 性能监控与调优
教育平台需要重点监控的指标:
- 直播推流成功率
- 支付接口响应时间
- 课程详情页加载速度
SpringBoot监控配置示例:
java复制@Configuration
public class MonitorConfig {
@Bean
public MeterRegistryCustomizer<PrometheusMeterRegistry> metricsCommonTags() {
return registry -> registry.config().commonTags(
"application", "edu-platform",
"region", System.getenv("REGION")
);
}
@Bean
public ServletRegistrationBean<StatViewServlet> druidStatViewServlet() {
ServletRegistrationBean<StatViewServlet> bean =
new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
// 教育平台Druid监控白名单配置
bean.addInitParameter("allow", "admin.example.com");
return bean;
}
}
前端性能监控方案:
javascript复制// 使用web-vitals库监控核心Web指标
import {getCLS, getFID, getLCP} from 'web-vitals';
function sendToAnalytics(metric) {
const body = JSON.stringify({
...metric,
page: location.pathname,
userId: getCurrentUserId()
});
// 使用navigator.sendBeacon保证教育平台页面关闭时也能发送
navigator.sendBeacon('/api/web-vitals', body);
}
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);
6. 安全防护专项
6.1 常见漏洞防护
教育平台需要特别注意的安全风险及解决方案:
- SQL注入防护
- 使用MyBatis预编译语句
- 动态SQL避免直接使用${}
- 添加Druid的WallFilter
java复制@Bean
public FilterRegistrationBean<Filter> filterRegistrationBean() {
FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();
bean.setFilter(new WebStatFilter());
bean.addUrlPatterns("/*");
WallFilter wallFilter = new WallFilter();
wallFilter.setConfig(wallConfig());
bean.setFilter(wallFilter);
return bean;
}
@Bean
public WallConfig wallConfig() {
WallConfig config = new WallConfig();
config.setDropTableAllow(false); // 禁止删表语句
config.setMergeAllow(false); // 禁止MERGE语句
return config;
}
- XSS防护
- 前端使用vue-sanitize处理富文本
- 后端添加XSS过滤器
java复制public class XssFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
// 教育平台特殊路径排除(如富文本编辑器)
if (request.getRequestURI().startsWith("/api/richtext")) {
chain.doFilter(req, res);
return;
}
chain.doFilter(new XssHttpServletRequestWrapper(request), res);
}
}
6.2 教育行业合规要求
在线教育平台需要满足的特殊合规要求:
- 等保2.0三级要求
- 个人信息保护法(PIPL)
- 未成年人保护措施
技术实现建议:
java复制// 敏感数据脱敏处理
public class DataMaskingUtil {
public static String maskPhone(String phone) {
if (StringUtils.isEmpty(phone)) return "";
return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
}
// 教育平台特有的身份证号脱敏
public static String maskIdCard(String idCard) {
if (StringUtils.isEmpty(idCard)) return "";
return idCard.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1**********$2");
}
}
// 在MyBatis结果处理器中自动应用
@Intercepts(@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}))
public class DataMaskingInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
List<Object> results = (List<Object>) invocation.proceed();
results.forEach(this::maskData);
return results;
}
private void maskData(Object object) {
if (object == null) return;
// 通过注解识别需要脱敏的字段
Arrays.stream(object.getClass().getDeclaredFields())
.filter(field -> field.isAnnotationPresent(Masking.class))
.forEach(field -> {
try {
field.setAccessible(true);
Object value = field.get(object);
if (value instanceof String) {
field.set(object, DataMaskingUtil.mask((String) value));
}
} catch (IllegalAccessException e) {
log.error("数据脱敏处理失败", e);
}
});
}
}
7. 项目扩展与二次开发
7.1 微服务化改造
当教育平台规模扩大时,建议的微服务拆分方案:
-
服务划分
- 用户服务(account-service)
- 课程服务(course-service)
- 订单服务(order-service)
- 支付服务(payment-service)
- 内容服务(content-service)
-
SpringCloud Alibaba技术选型
- 注册中心:Nacos
- 配置中心:Nacos Config
- 服务调用:OpenFeign
- 熔断降级:Sentinel
- 网关:SpringCloud Gateway
课程服务接口示例:
java复制@FeignClient(name = "course-service", path = "/api/course")
public interface CourseServiceClient {
@GetMapping("/{id}")
Result<CourseVO> getCourseById(@PathVariable Long id);
@PostMapping("/{courseId}/enroll")
Result<Void> enrollCourse(
@PathVariable Long courseId,
@RequestParam Long userId);
}
// 在Gateway中的路由配置
spring:
cloud:
gateway:
routes:
- id: course-service
uri: lb://course-service
predicates:
- Path=/api/course/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100 # 教育平台限流配置
redis-rate-limiter.burstCapacity: 200
7.2 AI能力集成
现代教育平台的AI增强方向:
- 智能推荐系统
- 基于学员学习行为的课程推荐
- 使用协同过滤算法
python复制# Python服务示例(可通过gRPC与Java集成)
from surprise import Dataset, KNNBasic
def train_recommend_model():
data = Dataset.load_builtin('ml-100k')
trainset = data.build_full_trainset()
sim_options = {'name': 'cosine', 'user_based': False}
algo = KNNBasic(sim_options=sim_options)
algo.fit(trainset)
return algo
def recommend_courses(user_id, algo, n=5):
inner_uid = algo.trainset.to_inner_uid(user_id)
neighbors = algo.get_neighbors(inner_uid, k=n)
return [algo.trainset.to_raw_uid(i) for i in neighbors]
- 直播内容审核
- 使用腾讯云或阿里云的内容安全API
- 实时检测违规内容
java复制public class LiveContentModeration {
public boolean checkText(String text) {
// 调用AI内容安全接口
ContentModerationRequest request = new ContentModerationRequest();
request.setText(text);
request.setScenes(Arrays.asList("antispam"));
try {
ContentModerationResponse response = client.getAcsResponse(request);
return "pass".equals(response.getData().getResult());
} catch (Exception e) {
log.error("内容审核失败", e);
return false;
}
}
}
8. 项目实战经验分享
8.1 典型问题排查案例
案例1:课程直播卡顿问题排查
现象:晚间高峰时段直播卡顿率上升至15%
排查过程:
- 检查网络监控:发现出口带宽达到90%利用率
- 分析Nginx日志:大量海外IP访问直播流
- 检查CDN配置:未开启区域访问限制
解决方案:
nginx复制# 在Nginx配置中添加地域限制
geo $forbidden {
default 0;
包含敏感国家代码 1;
}
server {
location ~ \.m3u8$ {
if ($forbidden) {
return 403;
}
# 原有配置...
}
}
实施后:卡顿率下降至3%,同时节省30%带宽成本
案例2:MyBatis缓存导致数据不一致
现象:学员看到的课程价格与实际不符
根因分析:
- 本地二级缓存未正确更新
- 分布式环境下缓存不同步
解决方案:
java复制@Configuration
public class MyBatisCacheConfig {
@Bean
public Cache eduCourseCache(RedisTemplate<String, Object> redisTemplate) {
return new RedisCache("eduCourse",
redisTemplate,
30, // 过期时间(分钟)
true // 缓存空值防穿透
) {
@Override
public void clear() {
// 教育平台特有的缓存清理逻辑
super.clear();
updatePriceVersion();
}
};
}
}
8.2 性能优化实战技巧
- MySQL批量插入优化
教育平台常见的批量导入场景优化方案:
java复制@Transactional
public void batchImportCourses(List<Course> courses) {
// 传统方案:逐条插入(性能差)
// courses.forEach(courseMapper::insert);
// 优化方案:批量插入
SqlSession session = sqlSessionTemplate.getSqlSessionFactory().openSession(ExecutorType.BATCH);
CourseMapper mapper = session.getMapper(CourseMapper.class);
try {
courses.forEach(mapper::insert);
session.commit();
} finally {
session.close();
}
// 后续处理(如更新缓存)
courseSearchService.refreshIndex();
}
- Vue组件性能优化
大型课程列表页的渲染优化技巧:
vue复制<template>
<div>
<!-- 使用虚拟滚动优化万级课程列表 -->
<RecycleScroller
class="scroller"
:items="courses"
:item-size="120"
key-field="id"
>
<template v-slot="{ item }">
<CourseCard :course="item" />
</template>
</RecycleScroller>
</div>
</template>
<script>
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
export default {
components: { RecycleScroller },
data() {
return {
courses: [] // 从API获取
}
},
async created() {
// 分页加载数据
this.courses = await this.$api.getCourses({
page: 1,
size: 1000
})
}
}
</script>
<style>
.scroller {
height: calc(100vh - 120px); /* 根据实际布局调整 */
}
</style>
9. 项目演进路线建议
9.1 技术债管理与迭代规划
教育平台常见的技术债及处理建议:
- 数据库分库分表
- 当课程表超过500万行时考虑分表
- 按学科ID哈希分片
java复制// 使用ShardingSphere实现分表
spring:
shardingsphere:
datasource:
names: ds0
sharding:
tables:
edu_course:
actual-data-nodes: ds0.edu_course_$->{0..7}
table-strategy:
inline:
sharding-column: subject_id
algorithm-expression: edu_course_$->{subject_id % 8}
- 前端微前端改造
- 将管理端、教师端、学生端拆分为独立子应用
- 使用qiankun框架集成
9.2 新兴技术适配
教育平台值得关注的技术趋势:
-
WebAssembly应用
- 在线编程实验室的代码沙箱
- 视频解码性能优化
-
WebRTC深度集成
- 实现低延迟互动课堂
- 屏幕共享与白板协作
javascript复制// WebRTC一对一教学示例
async function startCall(teacherId) {
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
localVideo.srcObject = stream;
const peer = new SimplePeer({
initiator: true,
stream: stream
});
// 通过教育平台的信令服务器交换SDP
peer.on('signal', data => {
socket.emit('signal', {
to: teacherId,
data: data
});
});
socket.on('signal', data => {
peer.signal(data);
});
peer.on('stream', remoteStream => {
remoteVideo.srcObject = remoteStream;
});
}
10. 项目完整部署指南
10.1 开发环境搭建
- 后端环境准备
bash复制# 安装JDK 11
sudo apt install openjdk-11-jdk
# 安装Maven
wget https://mirrors.tuna.tsinghua.edu.cn/apache/maven/maven-3/3.8.4/binaries/apache-maven-3.8.4-bin.tar.gz
tar -xzf apache-maven-3.8.4-bin.tar.gz
sudo mv apache-maven-3.8.4 /opt/maven
# 配置环境变量
echo 'export MAVEN_HOME=/opt/maven' >> ~/.bashrc
echo 'export PATH=$MAVEN_HOME/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
- 前端环境配置
bash复制# 安装Node.js 16.x
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -
sudo apt-get install -y nodejs
# 安装Yarn
npm install -g yarn
# 验证安装
node -v
npm -v
yarn -v
10.2 生产环境部署
使用Docker Compose部署全套服务
yaml复制version: '3.8'
services:
mysql:
image: mysql:5.7
container_name: edu-mysql
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: edu_platform
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
- ./mysql/conf:/etc/mysql/conf.d
ports:
- "3306:3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 10s
retries: 3
redis:
image: redis:6-alpine
container_name: edu-redis
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 3
backend:
build:
context: ./backend
dockerfile: Dockerfile
image: edu-backend:${TAG:-latest}
container_name: edu-backend
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
environment:
SPRING_PROFILES_ACTIVE: prod
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/edu_platform?useSSL=false&characterEncoding=utf8
SPRING_DATASOURCE_USERNAME: ${DB_USER}
SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
SPRING_REDIS_HOST: redis
SPRING_REDIS_PASSWORD: ${REDIS_PASSWORD}
ports:
- "8080:8080"
restart: unless-stopped
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
image: edu-frontend:${TAG:-latest}
container_name: edu-frontend
ports:
- "80:80"
depends_on:
backend:
condition: service_started
restart: unless-stopped
volumes:
mysql_data:
redis_data:
部署步骤:
- 创建
.env文件配置环境变量 - 构建镜像:
docker-compose build - 启动服务:
docker-compose up -d - 初始化数据库:
docker-compose exec backend ./init-db.sh
11. 项目二次开发指南
11.1 模块扩展实践
添加新模块:在线考试系统
- 数据库表设计
sql复制CREATE TABLE `edu_exam` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`course_id` bigint(20) NOT NULL COMMENT '关联课程ID',
`title` varchar(100) NOT NULL COMMENT '考试名称',
`start_time` datetime NOT NULL COMMENT '开始时间',
`end_time` datetime NOT NULL COMMENT '结束时间',
`duration` int(11) NOT NULL COMMENT '考试时长(分钟)',
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态',
PRIMARY KEY (`id`),
KEY `idx_course_id` (`course_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `edu_exam_question` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`exam_id` bigint(20) NOT NULL,
`type` tinyint(4) NOT NULL COMMENT '题型',
`content` text NOT NULL COMMENT '题目内容',
`options` text COMMENT '选择题选项',
`answer` text NOT NULL COMMENT '参考答案',
`score` int(11) NOT NULL DEFAULT '0' COMMENT '分值',
`sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序',
PRIMARY KEY (`id`),
KEY `idx_exam_id` (`exam_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- 后端API开发
java复制@RestController
@RequestMapping("/api/exam")
public class ExamController {
@Autowired
private ExamService examService;
@PostMapping
public Result<Long> createExam(@RequestBody ExamDTO dto) {
return Result.success(examService.createExam(dto));
}
@GetMapping("/{examId}/questions")
public Result<List<QuestionVO>> getExamQuestions(
@PathVariable Long examId,
@RequestParam(required = false) Long userId) {
return Result.success(examService.getQuestions(examId, userId));
}
@PostMapping("/submit")
public Result<Integer> submitExam(
@RequestBody ExamSubmissionDTO submission) {
return Result.success(examService.submitExam(submission));
}
}
- 前端页面集成
vue复制<template>
<div class="exam-container">
<h2>{{ exam.title }}</h2>
<div class="time-remaining">
剩余时间: {{ formatTime(remainingTime) }}
</div>
<div v-for="(q, index) in questions" :key="q.id" class="question">
<h3>第{{ index + 1 }}题 ({{ q.score }}分)</h3>
<div v-html="q.content"></div>
<template v-if="q.type === 1"> <!-- 单选题 -->
<el-radio-group v-model="answers[q.id]">
<el-radio
v-for="(opt, optIdx) in q.options"
:key="optIdx"
:label="optIdx">
{{ opt }}
</el-radio>
</el-radio-group>
</template>
<!-- 其他题型处理... -->
</div>
<el-button type="primary" @click="submit">提交试卷</el-button>
</div>
</template>
<script>
export default {
data() {
return {
exam: {},
questions: [],
answers: {},
remainingTime: 0,
timer: null
}
},
async created() {
const examId = this.$route.params.id
const res = await this.$api.get(`/api/exam/${examId}/questions`, {
params: { userId: this.$store.state.user.id }
})
this.exam = res.exam
this.questions = res.questions
this.startTimer(this.exam.duration * 60)
},
methods: {
startTimer(seconds) {
this.remainingTime = seconds
this.timer = setInterval(() => {
if (this.remainingTime <= 0) {
clearInterval(this.timer)
this.autoSubmit()
return
}
this.remainingTime--
}, 1000)
},
async submit() {
await this.$api.post('/api/exam/submit', {
examId: this.exam.id,
answers: this.answers
})
this.$router.push(`/exam/result/${this.exam.id}`)
}
}
}
</script>
11.2 主题定制方案
教育平台UI主题定制的最佳实践:
- SCSS变量覆盖方案
scss复制// 前端styles/theme.scss
$--color-primary: #1890ff; // 主色
$--color-success: #52c41a; // 成功色
$--color-warning: #faad14; // 警告色
$--color-danger: #f5222d; // 危险色
$--font-path: '~element-ui/lib/theme-chalk/fonts';
@import "~element-ui/packages/theme-chalk/src/index";
// 教育平台特有的样式覆盖
.edu-header {
background: linear-gradient(135deg, $--color-primary 0%, #1DA57A 100%);
}
.edu-card {
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
}
- 动态主题切换实现
javascript复制// theme.js
export function changeTheme(themeName) {
const link = document.getElementById('theme-style')
const href = `/static/theme/${themeName}/index.css`
if (!link) {
const styleLink = document.createElement('link')
styleLink.rel = 'stylesheet'
styleLink.href = href
styleLink.id = 'theme-style'
document.head.appendChild(styleLink)
} else {
link.href = href
}
// 保存到本地存储
localStorage.setItem('edu_theme', themeName)
}
// 在main.js中初始化
const savedTheme = localStorage.getItem('edu_theme') || 'default'
changeTheme(savedTheme)
