1. 家教平台项目概述
这个基于Spring Boot和Vue.js的家教平台项目,是我最近完成的一个实战案例。它本质上是一个连接学生和教师的在线中介平台,解决了传统家教服务中信息不对称、匹配效率低的问题。平台采用前后端分离架构,后端用Spring Boot提供RESTful API,前端用Vue.js构建响应式界面,数据库选用MySQL存储核心业务数据。
从技术角度看,这个项目有几个显著特点:一是采用了JWT进行无状态认证,避免了传统Session带来的服务器资源消耗;二是实现了完整的RBAC权限控制模型,区分学生、教师和管理员三种角色;三是集成了第三方支付接口,实现了完整的课程预约和支付流程。整个开发周期约6周,涉及12个核心接口和15个前端组件。
2. 技术选型与架构设计
2.1 后端技术栈选择
选择Spring Boot作为后端框架主要基于以下几个考虑:
- 自动配置特性大幅减少了XML配置,内置Tomcat简化了部署
- 丰富的Starter依赖可以快速集成常用组件(如MyBatis、Redis)
- Actuator提供了完善的服务监控端点
- 与Spring Security天然集成,便于实现权限控制
数据库选用MySQL 8.0而非PostgreSQL,主要因为:
- 项目初期数据量预估不会太大(万级用户)
- 团队对MySQL运维经验更丰富
- InnoDB引擎完全满足事务需求
- 配套工具链(如Navicat)成熟
2.2 前端技术栈选择
Vue.js 3.x的组合式API相比选项式API更适合本项目的复杂交互场景:
- 更好的TypeScript支持
- Composition API使逻辑复用更简单
- 更小的运行时体积(约10KB gzipped)
配套选择了以下工具链:
- Pinia替代Vuex进行状态管理
- Element Plus作为UI组件库
- Axios处理HTTP请求
- Vite构建工具显著提升开发体验
2.3 系统架构设计
整体采用分层架构:
code复制表示层(Vue) → 业务逻辑层(Spring) → 数据访问层(MyBatis) → MySQL
↑
Redis缓存
关键设计决策:
- 前后端完全分离,通过JSON交互
- 接口遵循RESTful规范
- 敏感操作采用HTTPS传输
- 高频查询数据缓存到Redis
3. 核心功能实现细节
3.1 用户认证模块
采用JWT实现无状态认证,关键流程:
- 用户登录成功后,后端生成包含用户ID和角色的JWT
- 前端将JWT存储在localStorage中
- 后续请求通过Authorization头携带JWT
- 服务端通过过滤器验证JWT有效性
Spring Security配置示例:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/teacher/**").hasRole("TEACHER")
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
return http.build();
}
}
注意:实际项目中应将JWT的签名密钥存储在环境变量中,不要硬编码在代码里
3.2 课程管理模块
课程实体核心字段设计:
java复制@Entity
public class Course {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String description;
@ManyToOne
private Teacher teacher;
private BigDecimal price;
private String subject; // 学科分类
private Integer duration; // 课时长(分钟)
@Enumerated(EnumType.STRING)
private CourseStatus status;
// 省略getter/setter
}
课程搜索接口实现要点:
- 使用MyBatis动态SQL构建查询条件
- 对标题和描述字段建立全文索引
- 分页查询使用PageHelper插件
java复制public interface CourseMapper {
@SelectProvider(type = CourseSqlBuilder.class, method = "buildSearchSql")
List<Course> search(@Param("keyword") String keyword,
@Param("subject") String subject,
@Param("minPrice") BigDecimal minPrice,
@Param("maxPrice") BigDecimal maxPrice);
}
// 动态SQL构建器
class CourseSqlBuilder {
public String buildSearchSql(Map<String, Object> params) {
return new SQL() {{
SELECT("*");
FROM("courses");
if (params.get("keyword") != null) {
WHERE("(title LIKE CONCAT('%', #{keyword}, '%') OR description LIKE CONCAT('%', #{keyword}, '%'))");
}
if (params.get("subject") != null) {
WHERE("subject = #{subject}");
}
if (params.get("minPrice") != null) {
WHERE("price >= #{minPrice}");
}
if (params.get("maxPrice") != null) {
WHERE("price <= #{maxPrice}");
}
WHERE("status = 'PUBLISHED'");
}}.toString();
}
}
3.3 订单支付模块
支付流程设计:
- 学生提交预约请求 → 生成待支付订单
- 调用支付宝/微信支付接口获取支付链接
- 学生完成支付 → 支付平台回调通知
- 系统验证支付结果 → 更新订单状态
防重复支付处理方案:
- 订单表设置唯一约束(order_no)
- 支付回调接口实现幂等性
- 使用Redis分布式锁防止并发问题
支付回调验证逻辑:
java复制@RestController
@RequestMapping("/api/payment")
public class PaymentController {
@PostMapping("/callback")
public String handleCallback(@RequestBody CallbackRequest request) {
// 1. 验证签名
if (!paymentService.verifySignature(request)) {
return "failure";
}
// 2. 查询本地订单
Order order = orderService.getByOrderNo(request.getOrderNo());
if (order == null) {
return "order_not_found";
}
// 3. 检查金额是否匹配
if (order.getAmount().compareTo(request.getAmount()) != 0) {
return "amount_mismatch";
}
// 4. 处理订单状态
if ("SUCCESS".equals(request.getStatus())) {
orderService.completeOrder(order.getId());
}
return "success";
}
}
4. 前端关键实现
4.1 课程列表页实现
使用Vue 3的组合式API:
vue复制<script setup>
import { ref, onMounted } from 'vue'
import { searchCourses } from '@/api/course'
const courses = ref([])
const loading = ref(false)
const pagination = ref({
page: 1,
pageSize: 10,
total: 0
})
const fetchCourses = async () => {
loading.value = true
try {
const res = await searchCourses({
page: pagination.value.page,
pageSize: pagination.value.pageSize
})
courses.value = res.data.list
pagination.value.total = res.data.total
} finally {
loading.value = false
}
}
onMounted(fetchCourses)
</script>
<template>
<el-table :data="courses" v-loading="loading">
<el-table-column prop="title" label="课程名称" />
<el-table-column prop="teacher.name" label="教师" />
<el-table-column prop="price" label="价格">
<template #default="{row}">
¥{{ row.price.toFixed(2) }}
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
@current-change="fetchCourses"
/>
</template>
4.2 实时通信方案
对于师生聊天功能,对比了三种方案后选择WebSocket:
- 轮询:实现简单但效率低下
- 长轮询:改进版轮询,仍不够实时
- WebSocket:全双工通信,最适合聊天场景
前端实现核心代码:
javascript复制// chat.js
export function useChat() {
const socket = ref(null)
const messages = ref([])
const connect = (userId) => {
socket.value = new WebSocket(`wss://yourdomain.com/chat/${userId}`)
socket.value.onmessage = (event) => {
messages.value.push(JSON.parse(event.data))
}
}
const send = (message) => {
if (socket.value?.readyState === WebSocket.OPEN) {
socket.value.send(JSON.stringify(message))
}
}
return { connect, send, messages }
}
后端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("/chat")
.setAllowedOrigins("*")
.withSockJS();
}
}
5. 部署与性能优化
5.1 容器化部署方案
使用Docker Compose编排服务:
yaml复制version: '3.8'
services:
backend:
build: ./backend
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- DB_URL=jdbc:mysql://db:3306/tutor_platform
depends_on:
- db
- redis
frontend:
build: ./frontend
ports:
- "80:80"
depends_on:
- backend
db:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=rootpass
- MYSQL_DATABASE=tutor_platform
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:alpine
ports:
- "6379:6379"
volumes:
mysql_data:
5.2 性能优化实践
-
数据库层面:
- 为常用查询字段添加索引
- 大表进行分库分表(用户表按地区分片)
- 使用EXPLAIN分析慢查询
-
缓存策略:
- 课程详情缓存30分钟
- 教师信息缓存1小时
- 使用Redis管道批量操作
-
前端优化:
- 路由懒加载
- 图片使用WebP格式
- 启用HTTP/2服务器推送
-
JVM调参:
bash复制
java -jar -Xms512m -Xmx1024m -XX:+UseG1GC \ -XX:MaxGCPauseMillis=200 backend.jar
6. 安全防护措施
6.1 常见攻击防护
-
SQL注入:
- 使用预编译语句(MyBatis默认支持)
- 禁止拼接SQL字符串
- 定期更新依赖库
-
XSS攻击:
- 前端使用vue-dompurify-html过滤HTML
- 后端对用户输入进行转义
- 设置Content-Security-Policy头
-
CSRF防护:
- 关键操作使用POST/PUT/DELETE方法
- 验证Referer头
- 敏感操作要求二次认证
6.2 数据安全策略
-
密码存储:
java复制@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(12); } -
敏感数据加密:
- 数据库字段级加密(如手机号)
- 使用Jasypt加密配置文件
- 日志脱敏处理
-
权限最小化原则:
- 数据库用户仅授予必要权限
- 服务器禁用root远程登录
- 定期轮换访问密钥
7. 开发中的经验教训
7.1 踩坑记录
-
时区问题:
- MySQL默认时区与Java应用不一致导致时间显示错误
- 解决方案:统一使用UTC时间存储,前端按需转换
-
跨域问题:
- 开发环境需要配置代理
- 生产环境Nginx统一处理
- 避免使用@CrossOrigin("*")
-
文件上传:
- 限制文件类型和大小
- 使用OSS存储而非本地磁盘
- 生成随机文件名防止冲突
7.2 性能陷阱
-
N+1查询问题:
- 使用@ManyToOne时未配置FetchType.LAZY
- 解决方案:使用@EntityGraph或手动JOIN查询
-
大事务问题:
- 批量导入数据时未分批次提交
- 导致数据库锁等待超时
- 改为每100条提交一次
-
内存泄漏:
- 未关闭的InputStream
- 静态Map持续增长
- 使用VisualVM定期检查
这个项目从技术选型到最终上线,整个过程让我对现代Web开发有了更深入的理解。特别是前后端分离架构下,如何保证接口的稳定性和安全性是需要重点考虑的问题。下一步我计划加入Elasticsearch实现更强大的搜索功能,并尝试用Kubernetes来管理容器化部署。