1. 项目背景与核心价值
这个Java+Vue3的线上教学辅助系统,本质上是在解决传统教学管理中的三个痛点:纸质作业流转低效、课堂互动形式单一、学情分析滞后。我们团队用半年时间打磨的这套系统,目前已经支撑了某高校计算机系3个年级的常态化教学,日均访问量稳定在2000+次。
从技术选型来看,后端采用Spring Boot 2.7 + MyBatis Plus的组合,前端选择Vue3+TypeScript+Pinia的技术栈,这种组合在保证开发效率的同时,特别注重类型安全和大规模状态管理。数据库方面没有走常规的MySQL路线,而是采用PostgreSQL 14,看中的是其对JSONB数据类型的原生支持——这对存储动态表单数据特别友好。
2. 系统架构设计解析
2.1 微服务拆分策略
系统按功能域拆分为四个微服务:
- 课程中心服务(处理课程CRUD)
- 作业流引擎服务(负责作业生命周期)
- 实时互动服务(WebSocket实现)
- 数据分析服务(定时计算学情报表)
每个服务都包含独立的:
- API网关(Spring Cloud Gateway)
- 认证鉴权模块(JWT + RBAC)
- 监控端点(Micrometer + Prometheus)
2.2 前后端交互设计
采用GraphQL替代传统RESTful API,主要解决三个问题:
- 移动端和Web端的数据需求差异
- 避免N+1查询问题
- 减少不必要的数据传输
典型查询示例:
graphql复制query GetCourseDetail($id: ID!) {
course(id: $id) {
title
assignments {
title
submitStats {
submittedCount
averageScore
}
}
recentAnnouncements(first: 3) {
title
createdAt
}
}
}
3. 核心功能实现细节
3.1 作业批改的并发控制
使用PostgreSQL的SKIP LOCKED特性实现高并发批改:
java复制@Transactional
public void startGrading(Long assignmentId) {
List<Submission> submissions = submissionRepository
.findUnprocessedSubmissions(assignmentId, PageRequest.of(0, 5));
submissions.forEach(sub -> {
gradingService.process(sub.getId());
submissionRepository.markAsGraded(sub.getId());
});
}
对应的Repository方法:
java复制@Query(value = "SELECT * FROM submission " +
"WHERE assignment_id = :assignmentId AND status = 'SUBMITTED' " +
"FOR UPDATE SKIP LOCKED LIMIT :size",
nativeQuery = true)
List<Submission> findUnprocessedSubmissions(
@Param("assignmentId") Long assignmentId,
Pageable pageable);
3.2 Vue3富文本编辑器集成
采用TipTap编辑器实现作业提交:
vue复制<script setup>
const editor = useEditor({
extensions: [
StarterKit,
TaskItem.configure({
nested: true,
}),
CodeBlockLowlight.configure({
lowlight,
}),
],
content: props.initialContent,
})
</script>
<template>
<editor-content :editor="editor" class="prose" />
</template>
4. 性能优化实战记录
4.1 作业列表渲染优化
通过虚拟滚动解决大数据量渲染问题:
vue复制<template>
<RecycleScroller
class="scroller"
:items="assignments"
:item-size="72"
key-field="id"
v-slot="{ item }"
>
<AssignmentCard :assignment="item" />
</RecycleScroller>
</template>
配合Pinia的状态缓存策略:
typescript复制export const useAssignmentStore = defineStore('assignments', {
state: () => ({
cachedAssignments: new Map<string, Assignment>(),
}),
actions: {
async fetchAssignment(id: string) {
if (this.cachedAssignments.has(id)) {
return this.cachedAssignments.get(id)
}
const res = await api.getAssignment(id)
this.cachedAssignments.set(id, res)
return res
}
}
})
5. 部署与监控方案
5.1 容器化部署
Docker Compose编排关键服务:
yaml复制services:
app:
image: registry.gitlab.com/edu-system/app:${TAG}
deploy:
resources:
limits:
cpus: '2'
memory: 2G
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
prometheus:
image: prom/prometheus:v2.40.0
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
5.2 监控指标采集
自定义的教学相关指标:
java复制@RestController
public class TeachingMetrics {
private final Counter assignmentSubmitCounter;
public TeachingMetrics(MeterRegistry registry) {
assignmentSubmitCounter = Counter.builder("edu.assignment.submit")
.tag("course", "courseId")
.description("Total assignment submissions")
.register(registry);
}
@PostMapping("/assignments/{id}/submit")
public ResponseEntity<?> submitAssignment(
@PathVariable Long id,
@RequestBody Submission submission) {
assignmentSubmitCounter.increment();
// 处理提交逻辑
}
}
6. 踩坑经验实录
6.1 文件上传内存溢出
错误配置:
java复制// application.yml错误示例
spring:
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
正确做法应该是:
java复制@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
factory.setLocation(System.getProperty("java.io.tmpdir"));
factory.setMaxFileSize(DataSize.ofMegabytes(50));
factory.setMaxRequestSize(DataSize.ofMegabytes(50));
return factory.createMultipartConfig();
}
6.2 Vue3路由缓存失效
问题场景:当使用三级嵌套路由时,keep-alive失效
解决方案:
vue复制<router-view v-slot="{ Component }">
<keep-alive :include="cachedViews">
<component :is="Component" :key="$route.fullPath" />
</keep-alive>
</router-view>
配套的Pinia状态管理:
typescript复制export const useRouteStore = defineStore('route', {
state: () => ({
cachedViews: new Set<string>(),
}),
actions: {
addCachedView(name: string) {
this.cachedViews.add(name)
},
removeCachedView(name: string) {
this.cachedViews.delete(name)
}
}
})
这个项目给我最深的体会是:教学系统的并发场景比想象中复杂,比如在期末考试周,同时提交作业的峰值QPS能达到300+。我们在数据库连接池配置上栽过跟头,最终通过HikariCP的以下配置解决问题:
properties复制spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.connection-timeout=30000
