基于Spring Boot + Vue的试听课管理系统的设计与实现
一、 项目背景与意义
随着在线教育的蓬勃发展,各类培训机构、学校及知识付费平台对试听课(体验课)的运营管理需求日益增长。试听课作为潜在学员了解课程质量、体验教学风格、建立信任的关键环节,其管理效率直接影响招生转化率。传统的人工排课、手动记录、电话或即时通讯工具沟通的方式,存在信息分散、流程繁琐、数据难以追踪分析等问题。
因此,设计并实现一个基于Spring Boot + Vue的试听课管理系统,具有重要的现实意义:
- 提升运营效率:将试听课的预约、排课、提醒、签到、反馈收集等流程线上化、自动化,减少人工操作,降低出错率。
- 优化用户体验:为学员提供便捷的在线预约、课程提醒、在线听课入口,提升试听体验和满意度。
- 赋能数据决策:系统化记录试听课的预约、出勤、转化等数据,为课程优化、营销策略调整提供数据支持。
- 技术实践价值:Spring Boot + Vue是当前企业级Web应用开发的主流技术栈之一,该项目涵盖了前后端分离架构、RESTful API设计、数据库建模、权限控制等核心技能,具有很高的学习和实践价值。
二、 技术栈选型
本项目采用前后端分离架构,后端提供RESTful API,前端负责页面渲染和用户交互。
2.1 后端技术栈 (Spring Boot)
- 核心框架:Spring Boot 2.x
- Web框架:Spring MVC
- 数据持久层:Spring Data JPA (或 MyBatis-Plus)
- 数据库:MySQL 8.0
- 缓存:Redis (用于会话管理、验证码、热点数据缓存)
- 安全框架:Spring Security + JWT (JSON Web Token)
- API文档:Swagger2 / Knife4j
- 构建工具:Maven 或 Gradle
- 其他:Lombok (简化代码),Hutool (工具类库),Quartz (定时任务,用于发送提醒)
2.2 前端技术栈 (Vue)
- 核心框架:Vue 3 (Composition API) 或 Vue 2
- 构建工具:Vite 或 Vue CLI
- UI组件库:Element Plus (Vue 3) 或 Element UI (Vue 2)
- 状态管理:Pinia (Vue 3) 或 Vuex (Vue 2)
- 路由:Vue Router
- HTTP客户端:Axios
- 工具库:Day.js (日期处理),Lodash (实用函数)
2.3 开发与部署
- 版本控制:Git
- 接口联调:Postman / Apifox
- 部署:后端打包为Jar,使用Docker容器化部署;前端静态资源部署至Nginx。
三、 系统核心功能模块设计
系统主要分为后台管理端和学员端。
3.1 后台管理端
- 用户与权限管理:管理员、课程顾问、讲师等角色管理,基于RBAC的权限控制。
- 课程管理:创建、编辑、上架/下架试听课程,设置课程信息(标题、简介、封面、讲师、时长等)。
- 试听课排期管理:为每门试听课程设置可预约的时间段(日期、开始时间、结束时间、最大人数)。
- 预约管理:查看所有预约记录,支持按状态(待确认、已确认、已取消、已完成)筛选,手动确认或取消预约。
- 签到与反馈:查看学员签到情况,收集并查看学员试听后的反馈评价。
- 数据统计:可视化仪表盘,展示预约趋势、出勤率、课程热度、转化率等关键指标。
3.2 学员端 (Web/H5)
- 课程浏览与搜索:查看所有可试听的课程列表,支持按分类、讲师筛选。
- 在线预约:选择心仪的课程和时间段,填写个人信息(姓名、手机号)进行预约。
- 我的预约:查看个人预约记录及状态,接收预约成功、上课提醒等通知。
- 在线听课:在预约时间通过系统提供的链接(如腾讯会议、Zoom房间号)进入线上教室。
- 课后反馈:试听结束后,可对课程进行评分和填写文字反馈。
四、 数据库核心表设计
-- 用户表 (sys_user) CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(255) NOT NULL COMMENT '密码', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `role` varchar(20) NOT NULL COMMENT '角色 (ADMIN, CONSULTANT, LECTURER, STUDENT)', `status` tinyint DEFAULT '1' COMMENT '状态 (0-禁用,1-启用)', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表'; -- 试听课程表 (trial_course) CREATE TABLE trial_course ( id bigint NOT NULL AUTO_INCREMENT, title varchar(200) NOT NULL COMMENT '课程标题', cover_image varchar(500) DEFAULT NULL COMMENT '封面图URL', description text COMMENT '课程描述', lecturer_id bigint DEFAULT NULL COMMENT '讲师ID', duration int DEFAULT NULL COMMENT '课程时长(分钟)', category varchar(50) DEFAULT NULL COMMENT '分类', is_active tinyint DEFAULT '1' COMMENT '是否上架', create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='试听课程表'; -- 试听课排期表 (trial_schedule) CREATE TABLE trial_schedule ( id bigint NOT NULL AUTO_INCREMENT, course_id bigint NOT NULL COMMENT '课程ID', start_time datetime NOT NULL COMMENT '开始时间', end_time datetime NOT NULL COMMENT '结束时间', max_capacity int DEFAULT 50 COMMENT '最大预约人数', current_bookings int DEFAULT 0 COMMENT '当前已预约人数', online_room varchar(500) DEFAULT NULL COMMENT '线上会议室链接/ID', status tinyint DEFAULT 1 COMMENT '状态 (1-可预约, 0-已满, -1-已取消)', PRIMARY KEY (id), KEY idx_course_time (course_id, start_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='试听课排期表'; -- 预约记录表 (booking_record) CREATE TABLE booking_record ( id bigint NOT NULL AUTO_INCREMENT, schedule_id bigint NOT NULL COMMENT '排期ID', user_id bigint NOT NULL COMMENT '学员用户ID', student_name varchar(50) NOT NULL COMMENT '学员姓名(预约时填写)', student_phone varchar(20) NOT NULL COMMENT '学员手机号', status varchar(20) DEFAULT 'PENDING' COMMENT '状态(PENDING, CONFIRMED, CANCELLED, COMPLETED)', check_in_time datetime DEFAULT NULL COMMENT '签到时间', feedback_score tinyint DEFAULT NULL COMMENT '反馈评分(1-5)', feedback_comment text COMMENT '反馈文字', create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_schedule_user (schedule_id, user_id), KEY idx_user (user_id), KEY idx_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='预约记录表';五、 核心代码示例
5.1 后端核心代码 (Spring Boot)
1. 预约试听课接口 (BookingController.java)
@RestController @RequestMapping("/api/booking") @RequiredArgsConstructor public class BookingController { private final BookingService bookingService; /** 学员预约试听课 */ @PostMapping public Result<BookingRecordVO> createBooking(@RequestBody @Valid BookingRequest request) { // 获取当前登录学员ID (从JWT Token中) Long currentUserId = SecurityUtil.getCurrentUserId(); BookingRecordVO booking = bookingService.createBooking(currentUserId, request); return Result.success(booking); } /** 管理员确认/取消预约 */ @PutMapping("/{bookingId}/status") @PreAuthorize("hasRole('ADMIN') or hasRole('CONSULTANT')") public Result<Void> updateBookingStatus(@PathVariable Long bookingId, @RequestParam String status) { bookingService.updateBookingStatus(bookingId, status); return Result.success(); } }2. 预约业务逻辑层 (BookingServiceImpl.java)
@Service @Slf4j @RequiredArgsConstructor public class BookingServiceImpl implements BookingService { private final TrialScheduleRepository scheduleRepository; private final BookingRecordRepository bookingRepository; private final RedisTemplate<String, String> redisTemplate; @Override @Transactional(rollbackFor = Exception.class) public BookingRecordVO createBooking(Long userId, BookingRequest request) { // 1. 校验排期是否存在且可预约 TrialSchedule schedule = scheduleRepository.findById(request.getScheduleId()) .orElseThrow(() -> new BusinessException("试听课排期不存在")); if (!ScheduleStatus.AVAILABLE.equals(schedule.getStatus())) { throw new BusinessException("该场次暂不可预约"); } if (schedule.getCurrentBookings() >= schedule.getMaxCapacity()) { throw new BusinessException("该场次预约已满"); } // 2. 防止重复预约 (同一用户同一场次) boolean exists = bookingRepository.existsByScheduleIdAndUserId(request.getScheduleId(), userId); if (exists) { throw new BusinessException("您已预约过该场次"); } // 3. 创建预约记录 BookingRecord record = new BookingRecord(); record.setScheduleId(request.getScheduleId()); record.setUserId(userId); record.setStudentName(request.getStudentName()); record.setStudentPhone(request.getStudentPhone()); record.setStatus(BookingStatus.PENDING); bookingRepository.save(record); // 4. 更新排期已预约人数 (使用乐观锁) int updated = scheduleRepository.incrementBookingCount(schedule.getId()); if (updated == 0) { throw new ConcurrentBookingException("预约冲突,请重试"); } // 5. 发送预约成功通知 (异步) sendBookingNotification(record); return BookingConverter.INSTANCE.toVO(record); } private void sendBookingNotification(BookingRecord record) { // 实现短信、站内信或邮件通知逻辑,可使用消息队列异步处理 log.info("预约成功通知已发送,预约ID: {}", record.getId()); } }5.2 前端核心代码 (Vue 3 + Element Plus)
1. 试听课预约页面组件 (TrialBooking.vue)
<template> <div class="trial-booking"> <el-card> <template #header> <div class="card-header"> <span>{{ course.title }} - 试听预约</span> </div> </template> <!-- 课程基本信息 --> <div class="course-info"> <el-image :src="course.coverImage" fit="cover"></el-image> <div class="info-text"> <h3>{{ course.title }}</h3> <p>讲师:{{ course.lecturerName }}</p> <p>时长:{{ course.duration }}分钟</p> <p>{{ course.description }}</p> </div> </div> <el-divider></el-divider> <!-- 可选时间表 --> <h4>可选时间</h4> <div class="schedule-list"> <el-radio-group v-model="selectedScheduleId"> <el-radio v-for="schedule in availableSchedules" :key="schedule.id" :label="schedule.id" :disabled="schedule.currentBookings >= schedule.maxCapacity" border > <div class="schedule-item"> <div>{{ formatDate(schedule.startTime, 'MM-DD HH:mm') }} 开始</div> <div class="capacity"> 已预约:{{ schedule.currentBookings }} / {{ schedule.maxCapacity }} </div> </div> </el-radio> </el-radio-group> </div> <el-divider></el-divider> <!-- 预约表单 --> <h4>填写预约信息</h4> <el-form :model="form" :rules="rules" ref="formRef" label-width="80px"> <el-form-item label="姓名" prop="studentName"> <el-input v-model="form.studentName" placeholder="请输入您的姓名" /> </el-form-item> <el-form-item label="手机号" prop="studentPhone"> <el-input v-model="form.studentPhone" placeholder="请输入手机号" /> </el-form-item> <el-form-item> <el-button type="primary" @click="submitBooking" :loading="loading"> 立即预约 </el-button> </el-form-item> </el-form> </el-card> </div> </template> <script setup> import { ref, onMounted } from 'vue' import { useRoute } from 'vue-router' import { ElMessage } from 'element-plus' import { getCourseDetail, getSchedules, createBooking } from '@/api/trial' import { formatDate } from '@/utils/date' const route = useRoute() const courseId = route.params.id const course = ref({}) const availableSchedules = ref([]) const selectedScheduleId = ref(null) const form = ref({ studentName: '', studentPhone: '' }) const formRef = ref() const loading = ref(false) const rules = { studentName: [{ required: true, message: '请输入姓名', trigger: 'blur' }], studentPhone: [ { required: true, message: '请输入手机号', trigger: 'blur' }, { pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' } ] } // 加载课程详情和可预约时间 const loadData = async () => { try { const [courseRes, scheduleRes] = await Promise.all([ getCourseDetail(courseId), getSchedules(courseId) ]) course.value = courseRes.data availableSchedules.value = scheduleRes.data.filter(s => s.status === 'AVAILABLE') } catch (error) { ElMessage.error('数据加载失败') } } // 提交预约 const submitBooking = () => { formRef.value.validate(async (valid) => { if (!valid) return if (!selectedScheduleId.value) { ElMessage.warning('请选择预约时间') return