三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

基于Spring Boot + Vue的试听课管理系统的设计与实现

基于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> &lt;!-- 课程基本信息 --&gt; &lt;div class="course-info"&gt; &lt;el-image :src="course.coverImage" fit="cover"&gt;&lt;/el-image&gt; &lt;div class="info-text"&gt; &lt;h3&gt;{{ course.title }}&lt;/h3&gt; &lt;p&gt;讲师:{{ course.lecturerName }}&lt;/p&gt; &lt;p&gt;时长:{{ course.duration }}分钟&lt;/p&gt; &lt;p&gt;{{ course.description }}&lt;/p&gt; &lt;/div&gt; &lt;/div&gt; &lt;el-divider&gt;&lt;/el-divider&gt; &lt;!-- 可选时间表 --&gt; &lt;h4&gt;可选时间&lt;/h4&gt; &lt;div class="schedule-list"&gt; &lt;el-radio-group v-model="selectedScheduleId"&gt; &lt;el-radio v-for="schedule in availableSchedules" :key="schedule.id" :label="schedule.id" :disabled="schedule.currentBookings &gt;= schedule.maxCapacity" border &gt; &lt;div class="schedule-item"&gt; &lt;div&gt;{{ formatDate(schedule.startTime, 'MM-DD HH:mm') }} 开始&lt;/div&gt; &lt;div class="capacity"&gt; 已预约:{{ schedule.currentBookings }} / {{ schedule.maxCapacity }} &lt;/div&gt; &lt;/div&gt; &lt;/el-radio&gt; &lt;/el-radio-group&gt; &lt;/div&gt; &lt;el-divider&gt;&lt;/el-divider&gt; &lt;!-- 预约表单 --&gt; &lt;h4&gt;填写预约信息&lt;/h4&gt; &lt;el-form :model="form" :rules="rules" ref="formRef" label-width="80px"&gt; &lt;el-form-item label="姓名" prop="studentName"&gt; &lt;el-input v-model="form.studentName" placeholder="请输入您的姓名" /&gt; &lt;/el-form-item&gt; &lt;el-form-item label="手机号" prop="studentPhone"&gt; &lt;el-input v-model="form.studentPhone" placeholder="请输入手机号" /&gt; &lt;/el-form-item&gt; &lt;el-form-item&gt; &lt;el-button type="primary" @click="submitBooking" :loading="loading"&gt; 立即预约 &lt;/el-button&gt; &lt;/el-form-item&gt; &lt;/el-form&gt; &lt;/el-card&gt; </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

← 返回列表