Spring Boot 3 + Vue 3 + TypeScript 全栈开发驾校预约管理系统实战

📅 2026/8/4 8:21:17 👁️ 阅读次数 📝 编程学习
Spring Boot 3 + Vue 3 + TypeScript 全栈开发驾校预约管理系统实战

在驾校业务数字化转型的浪潮中,传统的人工电话预约、手动排课模式已难以应对学员激增、教练资源调配复杂、财务对账繁琐等痛点。学员抱怨约不到心仪的教练和时间,驾校管理者则为排班混乱、数据统计滞后而头疼。本文将分享一套基于 Spring Boot 3、Vue 3 和 TypeScript 构建的现代化驾校预约管理系统,从零开始,手把手带你完成一个功能完整、前后端分离的企业级项目。无论你是想深入学习全栈开发,还是为毕业设计寻找灵感,或是为实际业务寻找解决方案,本文提供的完整代码、配置详解和避坑指南都能让你直接上手。

1. 系统核心概念与业务背景

1.1 什么是驾校预约管理系统?

驾校预约管理系统是一个旨在优化驾校核心业务流程的信息化平台。它通过数字化手段,将学员、教练、车辆、课程、场地、财务等要素进行高效连接与智能调度。其核心目标是解决传统驾校运营中的信息不对称、资源浪费和管理低效问题,为学员提供便捷透明的服务,为管理者提供精准的数据决策支持。

1.2 系统核心业务流程与价值

一个典型的预约流程包含以下环节:

  1. 学员端:注册/登录 -> 查看教练/车辆/时间资源 -> 提交预约申请 -> 在线支付(可选) -> 查看预约状态 -> 完成培训后评价。
  2. 教练端:查看个人课表 -> 确认或拒绝预约 -> 记录学员培训进度。
  3. 管理端:管理学员、教练、车辆信息 -> 设置课程套餐与价格 -> 审核预约订单 -> 排班调度 -> 查看各类统计报表(收入、预约率、教练负荷等)。

系统的价值在于:

  • 提升效率:自动化排班与预约,减少人工沟通成本。
  • 优化体验:学员可随时随地自助预约,状态实时可查。
  • 精细化管理:数据驱动决策,实现资源(教练、车辆、场地)的利用率最大化。
  • 规范财务:集成支付与对账,资金流清晰可追溯。

1.3 技术选型:为什么是 Spring Boot 3 + Vue 3 + TypeScript?

  • Spring Boot 3 (后端):作为 Java 生态中最主流的快速开发框架,其“约定大于配置”的理念能极大提升后端服务开发效率。Spring Boot 3 基于 Spring Framework 6 和 Java 17+,提供了更好的性能、更现代的编程模型(如响应式编程支持)以及对 GraalVM 原生镜像的初步支持,是构建稳健、可扩展后端 API 的理想选择。
  • Vue 3 (前端框架):以其轻量、渐进式和出色的性能著称。Vue 3 的 Composition API 提供了更灵活的逻辑复用方式,搭配<script setup>语法糖,使得组件代码更简洁、更易于维护。其活跃的生态(如 Element Plus、Vite)能快速构建出美观、交互丰富的前端界面。
  • TypeScript (前端语言):是 JavaScript 的超集,为 JavaScript 添加了静态类型系统。在 Vue 3 项目中使用 TypeScript,可以在编码阶段就捕获许多潜在的错误(如拼写错误、类型不匹配),提供更好的 IDE 智能提示和代码补全,显著提升大型前端项目的可维护性和开发体验。这正是构建复杂业务系统(如本管理系统)所必需的。

这三者结合,构成了一个技术栈现代、生态成熟、前后端职责清晰的全栈开发方案。

2. 环境准备与项目初始化

在开始编码前,请确保你的开发环境已就绪。

2.1 后端环境准备 (Spring Boot 3)

  1. JDK:确保已安装JDK 17 或更高版本。Spring Boot 3 要求最低 JDK 17。
    java -version
  2. 构建工具:推荐使用Maven 3.6+Gradle 7.x+。本文示例使用 Maven。
  3. IDE:IntelliJ IDEA(推荐)、Eclipse 或 VS Code(需安装 Java 插件)。
  4. 数据库:MySQL 5.7+ 或 8.0。我们将使用 MySQL 作为持久化存储。
  5. 初始化项目:访问 Spring Initializr 生成项目骨架。
    • Project: Maven
    • Language: Java
    • Spring Boot: 3.2.x (选择稳定版本)
    • Project Metadata:
      • Group:com.drivingschool
      • Artifact:booking-system
      • Name:booking-system
      • Package name:com.drivingschool.bookingsystem
    • Packaging: Jar
    • Java: 17
    • Dependencies: 添加Spring Web,Spring Data JPA,MySQL Driver,Lombok

下载生成的项目压缩包并解压,用 IDE 打开。

2.2 前端环境准备 (Vue 3 + TypeScript)

  1. Node.js: 安装Node.js 18+和 npm。

    node -v npm -v
  2. 包管理工具:可以使用 npm,但更推荐pnpmyarn,速度更快。本文使用 pnpm。

    npm install -g pnpm
  3. 创建 Vue 项目:使用官方脚手架create-vue,它天然支持 TypeScript。

    pnpm create vue@latest

    根据提示进行选择:

    • Project name:driving-school-frontend
    • Add TypeScript?Yes
    • Add JSX Support? No (按需)
    • Add Vue Router for Single Page Application?Yes
    • Add Pinia for state management?Yes(推荐)
    • Add Vitest for Unit Testing? No (可选)
    • Add an End-to-End Testing Solution? No (可选)
    • Add ESLint for code quality?Yes
    • Add Prettier for code formatting?Yes

    创建完成后,进入项目目录并安装依赖:

    cd driving-school-frontend pnpm install
  4. UI 组件库:我们将使用Element Plus,它是对 Vue 3 支持良好的流行组件库。

    pnpm add element-plus @element-plus/icons-vue

    同时,需要安装并配置按需导入以优化打包体积,具体配置可参考 Element Plus 官方文档。

2.3 项目整体结构预览

项目完成后,整体目录结构大致如下:

driving-school-project/ ├── booking-system/ # Spring Boot 后端项目 │ ├── src/main/java/com/drivingschool/bookingsystem/ │ │ ├── controller/ # 控制器,处理HTTP请求 │ │ ├── service/ # 业务逻辑层 │ │ ├── repository/ # 数据访问层 (JPA) │ │ ├── entity/ # 实体类,对应数据库表 │ │ ├── dto/ # 数据传输对象 │ │ ├── config/ # 配置类 (如跨域、安全) │ │ └── BookingSystemApplication.java │ ├── src/main/resources/ │ │ ├── application.yml # 主配置文件 │ │ └── ... │ └── pom.xml └── driving-school-frontend/ # Vue 3 前端项目 ├── src/ │ ├── api/ # 封装所有后端API请求 │ ├── router/ # 路由配置 │ ├── stores/ # Pinia 状态管理 │ ├── views/ # 页面组件 │ ├── components/ # 可复用组件 │ ├── utils/ # 工具函数 │ ├── App.vue │ └── main.ts ├── index.html ├── vite.config.ts # Vite 配置 ├── tsconfig.json # TypeScript 配置 ├── package.json └── ...

3. 后端核心功能实现 (Spring Boot 3)

我们将从数据库设计开始,逐步实现后端的核心业务逻辑。

3.1 数据库设计与实体类映射

首先,在 MySQL 中创建数据库driving_school_db。然后,我们设计几个核心实体。

1. 用户实体 (User):涵盖学员、教练、管理员。通过userType字段区分。

// 文件路径:src/main/java/com/drivingschool/bookingsystem/entity/User.java package com.drivingschool.bookingsystem.entity; import jakarta.persistence.*; import lombok.Data; import java.time.LocalDateTime; @Entity @Table(name = "sys_user") @Data public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String username; // 登录名 private String password; private String realName; // 真实姓名 private String phone; private String email; @Enumerated(EnumType.STRING) private UserType userType; // 枚举:STUDENT, COACH, ADMIN private LocalDateTime createTime; public enum UserType { STUDENT, COACH, ADMIN } }

2. 教练实体 (Coach):扩展用户信息,包含教练特有属性。

// 文件路径:src/main/java/com/drivingschool/bookingsystem/entity/Coach.java package com.drivingschool.bookingsystem.entity; import jakarta.persistence.*; import lombok.Data; @Entity @Table(name = "biz_coach") @Data public class Coach { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @OneToOne @JoinColumn(name = "user_id", referencedColumnName = "id", unique = true) private User user; // 关联用户 private String coachNumber; // 教练编号 private String idCard; // 身份证号 private String drivingLicense; // 驾驶证号 private Integer teachingYears; // 教龄 private String description; // 简介 private Boolean isAvailable = true; // 是否可用 }

3. 预约订单实体 (BookingOrder):系统的核心。

// 文件路径:src/main/java/com/drivingschool/bookingsystem/entity/BookingOrder.java package com.drivingschool.bookingsystem.entity; import jakarta.persistence.*; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; @Entity @Table(name = "biz_booking_order") @Data public class BookingOrder { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String orderNumber; // 订单号,可自定义生成规则 @ManyToOne @JoinColumn(name = "student_id", nullable = false) private User student; // 学员 @ManyToOne @JoinColumn(name = "coach_id", nullable = false) private Coach coach; // 教练 private LocalDateTime bookingTime; // 预约的培训时间 private Integer duration; // 课时长,单位:小时 private String location; // 训练场地 @Enumerated(EnumType.STRING) private OrderStatus status; // 状态:PENDING, CONFIRMED, COMPLETED, CANCELLED private BigDecimal amount; // 订单金额 private LocalDateTime createTime; private LocalDateTime updateTime; public enum OrderStatus { PENDING, // 待确认 CONFIRMED, // 已确认 COMPLETED, // 已完成 CANCELLED // 已取消 } }

3.2 数据访问层与业务逻辑层

使用 Spring Data JPA 可以极大简化数据库操作。

1. 创建 Repository 接口

// 文件路径:src/main/java/com/drivingschool/bookingsystem/repository/BookingOrderRepository.java package com.drivingschool.bookingsystem.repository; import com.drivingschool.bookingsystem.entity.BookingOrder; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.stereotype.Repository; import java.time.LocalDateTime; import java.util.List; @Repository public interface BookingOrderRepository extends JpaRepository<BookingOrder, Long>, JpaSpecificationExecutor<BookingOrder> { // 查询某个教练在特定时间段的预约 List<BookingOrder> findByCoachIdAndBookingTimeBetween(Long coachId, LocalDateTime start, LocalDateTime end); // 查询某个学员的预约历史 List<BookingOrder> findByStudentIdOrderByBookingTimeDesc(Long studentId); }

2. 实现 Service 业务逻辑

// 文件路径:src/main/java/com/drivingschool/bookingsystem/service/BookingService.java package com.drivingschool.bookingsystem.service; import com.drivingschool.bookingsystem.dto.BookingRequestDTO; import com.drivingschool.bookingsystem.entity.BookingOrder; import com.drivingschool.bookingsystem.entity.Coach; import com.drivingschool.bookingsystem.entity.User; import com.drivingschool.bookingsystem.repository.BookingOrderRepository; import com.drivingschool.bookingsystem.repository.CoachRepository; import com.drivingschool.bookingsystem.repository.UserRepository; import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.time.LocalDateTime; @Service @RequiredArgsConstructor // Lombok 自动生成构造函数注入 public class BookingService { private final BookingOrderRepository bookingOrderRepository; private final CoachRepository coachRepository; private final UserRepository userRepository; /** * 创建预约订单 */ @Transactional public BookingOrder createBooking(BookingRequestDTO bookingRequest) { // 1. 参数校验 User student = userRepository.findById(bookingRequest.getStudentId()) .orElseThrow(() -> new EntityNotFoundException("学员不存在")); Coach coach = coachRepository.findById(bookingRequest.getCoachId()) .orElseThrow(() -> new EntityNotFoundException("教练不存在")); // 2. 业务校验:教练是否可用、时间是否冲突等 if (!coach.getIsAvailable()) { throw new RuntimeException("该教练暂不可预约"); } // 检查时间冲突(简化示例,实际需更复杂校验) LocalDateTime startTime = bookingRequest.getBookingTime(); LocalDateTime endTime = startTime.plusHours(bookingRequest.getDuration()); boolean hasConflict = bookingOrderRepository .findByCoachIdAndBookingTimeBetween(coach.getId(), startTime, endTime) .stream() .anyMatch(order -> order.getStatus() != BookingOrder.OrderStatus.CANCELLED); if (hasConflict) { throw new RuntimeException("该时间段已被预约,请选择其他时间"); } // 3. 构建订单实体 BookingOrder order = new BookingOrder(); order.setOrderNumber(generateOrderNumber()); order.setStudent(student); order.setCoach(coach); order.setBookingTime(bookingRequest.getBookingTime()); order.setDuration(bookingRequest.getDuration()); order.setLocation(bookingRequest.getLocation()); order.setStatus(BookingOrder.OrderStatus.PENDING); // 简单计算金额:假设每小时单价固定 BigDecimal hourlyRate = new BigDecimal("100.00"); order.setAmount(hourlyRate.multiply(BigDecimal.valueOf(bookingRequest.getDuration()))); order.setCreateTime(LocalDateTime.now()); // 4. 保存到数据库 return bookingOrderRepository.save(order); } private String generateOrderNumber() { // 生成订单号逻辑,例如:DS202405210001 return "DS" + LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); } }

3.3 控制器层与 RESTful API 设计

控制器负责接收 HTTP 请求,调用 Service,并返回响应。

// 文件路径:src/main/java/com/drivingschool/bookingsystem/controller/BookingController.java package com.drivingschool.bookingsystem.controller; import com.drivingschool.bookingsystem.dto.BookingRequestDTO; import com.drivingschool.bookingsystem.entity.BookingOrder; import com.drivingschool.bookingsystem.service.BookingService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/bookings") @RequiredArgsConstructor public class BookingController { private final BookingService bookingService; @PostMapping public ResponseEntity<BookingOrder> createBooking(@Valid @RequestBody BookingRequestDTO bookingRequest) { BookingOrder newOrder = bookingService.createBooking(bookingRequest); return ResponseEntity.status(HttpStatus.CREATED).body(newOrder); } @GetMapping("/{id}") public ResponseEntity<BookingOrder> getBookingById(@PathVariable Long id) { // 这里省略了Service层的查询逻辑,实际需要实现 return ResponseEntity.ok().build(); } // 其他接口:取消预约、确认预约、查询列表等 }

对应的 DTO(数据传输对象)用于封装请求和响应数据:

// 文件路径:src/main/java/com/drivingschool/bookingsystem/dto/BookingRequestDTO.java package com.drivingschool.bookingsystem.dto; import jakarta.validation.constraints.Future; import jakarta.validation.constraints.NotNull; import lombok.Data; import java.time.LocalDateTime; @Data public class BookingRequestDTO { @NotNull(message = "学员ID不能为空") private Long studentId; @NotNull(message = "教练ID不能为空") private Long coachId; @NotNull(message = "预约时间不能为空") @Future(message = "预约时间必须是将来的时间") private LocalDateTime bookingTime; @NotNull(message = "课时长不能为空") private Integer duration; // 小时 private String location; }

3.4 关键配置:数据库与跨域

1. 数据库配置 (application.yml)

# 文件路径:src/main/resources/application.yml spring: datasource: url: jdbc:mysql://localhost:3306/driving_school_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update # 开发环境可用update,生产环境用validate或none show-sql: true properties: hibernate: format_sql: true database-platform: org.hibernate.dialect.MySQL8Dialect server: port: 8080

2. 跨域配置:允许前端项目访问后端 API。

// 文件路径:src/main/java/com/drivingschool/bookingsystem/config/WebConfig.java package com.drivingschool.bookingsystem.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig { @Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") // 允许跨域的路径 .allowedOrigins("http://localhost:5173") // 前端开发服务器地址 .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true); } }; } }

4. 前端核心功能实现 (Vue 3 + TypeScript + Element Plus)

前端将使用 Vue 3 的 Composition API 和<script setup>语法,配合 TypeScript 和 Pinia 进行开发。

4.1 项目配置与依赖

确保vite.config.tstsconfig.json配置正确。安装必要的依赖:

pnpm add axios pinia vue-router pnpm add -D @types/node # 解决 path 模块类型问题

创建src/utils/request.ts文件,使用 axios 封装统一的 HTTP 请求:

// 文件路径:src/utils/request.ts import axios from 'axios'; import { ElMessage } from 'element-plus'; import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; const service: AxiosInstance = axios.create({ baseURL: import.meta.env.VITE_APP_BASE_API || 'http://localhost:8080/api', timeout: 10000, }); // 请求拦截器 service.interceptors.request.use( (config) => { // 从 Pinia store 或 localStorage 获取 token const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => { return Promise.reject(error); } ); // 响应拦截器 service.interceptors.response.use( (response: AxiosResponse) => { const res = response.data; // 假设后端统一返回格式 { code: number, data: any, message: string } if (res.code !== 200) { ElMessage.error(res.message || 'Error'); return Promise.reject(new Error(res.message || 'Error')); } else { return res; } }, (error) => { ElMessage.error(error.message || '请求失败'); return Promise.reject(error); } ); export default service;

4.2 状态管理:使用 Pinia 管理用户状态

创建用户状态 Store:

// 文件路径:src/stores/user.ts import { defineStore } from 'pinia'; import { ref, computed } from 'vue'; import type { UserInfo } from '@/types/user'; export const useUserStore = defineStore('user', () => { // 状态 const token = ref<string>(''); const userInfo = ref<UserInfo | null>(null); // Getter const isLoggedIn = computed(() => !!token.value); // Action const setToken = (newToken: string) => { token.value = newToken; localStorage.setItem('token', newToken); }; const setUserInfo = (info: UserInfo) => { userInfo.value = info; }; const logout = () => { token.value = ''; userInfo.value = null; localStorage.removeItem('token'); }; return { token, userInfo, isLoggedIn, setToken, setUserInfo, logout, }; });

4.3 核心页面:学员预约页面

这是一个典型的 Vue 3 单文件组件,展示了如何组合使用 Composition API、TypeScript、Element Plus 组件和 Pinia。

<!-- 文件路径:src/views/student/BookingPage.vue --> <template> <div class="booking-container"> <el-card> <template #header> <span class="card-header">预约练车</span> </template> <el-form :model="bookingForm" :rules="rules" ref="formRef" label-width="100px"> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="选择教练" prop="coachId"> <el-select v-model="bookingForm.coachId" placeholder="请选择教练" filterable @change="handleCoachChange"> <el-option v-for="coach in coachList" :key="coach.id" :label="`${coach.user.realName} (${coach.coachNumber})`" :value="coach.id" /> </el-select> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="预约时间" prop="bookingTime"> <el-date-picker v-model="bookingForm.bookingTime" type="datetime" placeholder="选择日期和时间" :disabled-date="disabledDate" :shortcuts="shortcuts" value-format="YYYY-MM-DD HH:mm:ss" /> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="课时长(小时)" prop="duration"> <el-input-number v-model="bookingForm.duration" :min="1" :max="4" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="训练场地" prop="location"> <el-input v-model="bookingForm.location" placeholder="请输入训练场地" /> </el-form-item> </el-col> </el-row> <el-form-item> <el-button type="primary" @click="submitBooking" :loading="submitting">提交预约</el-button> <el-button @click="resetForm">重置</el-button> </el-form-item> </el-form> <!-- 教练信息卡片 --> <el-card v-if="selectedCoach" class="coach-info-card"> <template #header> <span>教练信息</span> </template> <div class="coach-detail"> <el-avatar :size="60" :src="selectedCoach.avatar" /> <div class="coach-text"> <h4>{{ selectedCoach.user.realName }}</h4> <p>编号:{{ selectedCoach.coachNumber }}</p> <p>教龄:{{ selectedCoach.teachingYears }} 年</p> <p>{{ selectedCoach.description }}</p> </div> </div> </el-card> </el-card> </div> </template> <script setup lang="ts"> import { ref, reactive, onMounted } from 'vue'; import type { FormInstance, FormRules } from 'element-plus'; import { ElMessage } from 'element-plus'; import { useUserStore } from '@/stores/user'; import { getCoaches, createBooking } from '@/api/booking'; import type { Coach, BookingRequest } from '@/types/booking'; // 类型定义 interface BookingForm { coachId: number | null; bookingTime: string; duration: number; location: string; } // 响应式数据 const formRef = ref<FormInstance>(); const coachList = ref<Coach[]>([]); const selectedCoach = ref<Coach | null>(null); const submitting = ref(false); const bookingForm = reactive<BookingForm>({ coachId: null, bookingTime: '', duration: 2, location: '驾校一号训练场', }); // 表单验证规则 const rules = reactive<FormRules<BookingForm>>({ coachId: [{ required: true, message: '请选择教练', trigger: 'change' }], bookingTime: [{ required: true, message: '请选择预约时间', trigger: 'change' }], duration: [{ required: true, message: '请输入课时长', trigger: 'blur' }], }); // 日期选择器快捷选项 const shortcuts = [ { text: '明天上午', value: () => { const date = new Date(); date.setDate(date.getDate() + 1); date.setHours(9, 0, 0, 0); return date; }, }, // ... 其他快捷选项 ]; // 禁用过去的日期 const disabledDate = (time: Date) => { return time.getTime() < Date.now() - 24 * 60 * 60 * 1000; }; // 方法 const handleCoachChange = (coachId: number) => { const coach = coachList.value.find(c => c.id === coachId); selectedCoach.value = coach || null; }; const loadCoaches = async () => { try { const res = await getCoaches(); coachList.value = res.data; } catch (error) { ElMessage.error('加载教练列表失败'); } }; const submitBooking = async () => { if (!formRef.value) return; const valid = await formRef.value.validate(); if (!valid) return; submitting.value = true; const userStore = useUserStore(); const requestData: BookingRequest = { studentId: userStore.userInfo?.id!, // 实际应从store获取 coachId: bookingForm.coachId!, bookingTime: bookingForm.bookingTime, duration: bookingForm.duration, location: bookingForm.location, }; try { await createBooking(requestData); ElMessage.success('预约成功!'); resetForm(); } catch (error) { ElMessage.error('预约失败,请重试'); } finally { submitting.value = false; } }; const resetForm = () => { if (!formRef.value) return; formRef.value.resetFields(); selectedCoach.value = null; }; // 生命周期 onMounted(() => { loadCoaches(); }); </script> <style scoped> .booking-container { padding: 20px; } .coach-info-card { margin-top: 20px; } .coach-detail { display: flex; align-items: center; gap: 20px; } .coach-text h4 { margin: 0 0 8px 0; } </style>

对应的 API 请求封装:

// 文件路径:src/api/booking.ts import request from '@/utils/request'; import type { AxiosPromise } from 'axios'; import type { Coach, BookingRequest, BookingOrder } from '@/types/booking'; export function getCoaches(): AxiosPromise<{ data: Coach[] }> { return request({ url: '/coaches', method: 'get', }); } export function createBooking(data: BookingRequest): AxiosPromise<BookingOrder> { return request({ url: '/bookings', method: 'post', data, }); }

4.4 路由与权限控制

使用 Vue Router 定义路由,并结合导航守卫实现简单的权限控制。

// 文件路径:src/router/index.ts import { createRouter, createWebHistory } from 'vue-router'; import { useUserStore } from '@/stores/user'; const routes = [ { path: '/', redirect: '/login', }, { path: '/login', name: 'Login', component: () => import('@/views/LoginPage.vue'), meta: { requiresAuth: false }, }, { path: '/student', name: 'StudentLayout', component: () => import('@/layouts/StudentLayout.vue'), meta: { requiresAuth: true, role: 'STUDENT' }, children: [ { path: 'booking', name: 'StudentBooking', component: () => import('@/views/student/BookingPage.vue'), }, // ... 其他学生路由 ], }, // ... 教练和管理员路由 ]; const router = createRouter({ history: createWebHistory(), routes, }); // 全局前置守卫 router.beforeEach((to, from, next) => { const userStore = useUserStore(); const isAuthenticated = userStore.isLoggedIn; if (to.meta.requiresAuth && !isAuthenticated) { // 需要登录但未登录,重定向到登录页 next({ name: 'Login', query: { redirect: to.fullPath } }); } else if (to.meta.role) { // 需要特定角色,这里简化处理,实际应对比用户角色 // if (userStore.userInfo?.userType !== to.meta.role) { // next({ name: 'Forbidden' }); // } next(); } else { next(); } }); export default router;

5. 前后端联调与部署注意事项

5.1 联调常见问题与解决思路

  1. 跨域问题 (CORS)

    • 现象:前端控制台报错Access-Control-Allow-Origin
    • 解决:确保后端已正确配置跨域(如本文 3.4 节所示),且allowedOrigins包含前端运行的地址(如http://localhost:5173)。
  2. 接口 404 或 405

    • 现象:请求后端接口返回 404 (Not Found) 或 405 (Method Not Allowed)。
    • 解决
      • 检查后端 Controller 的@RequestMapping@GetMapping/@PostMapping路径是否正确。
      • 检查前端请求的 URL 是否拼接正确(baseURL + api path)。
      • 使用浏览器开发者工具的 Network 面板,查看请求的详细 URL 和方法。
  3. 数据类型不匹配

    • 现象:后端接收到的 JSON 字段为null或类型转换错误。
    • 解决
      • 前后端统一使用相同的命名规范(如驼峰)。
      • 检查后端 DTO 的字段类型(如LocalDateTime)与前端的传值格式(字符串)是否匹配,可能需要配置全局的 JSON 序列化/反序列化格式。
  4. 身份认证失败

    • 现象:请求需要认证的接口返回 401 (Unauthorized)。
    • 解决
      • 确认登录流程正确,后端成功返回了 token(如 JWT)。
      • 确认前端在请求拦截器中正确地将 token 添加到了请求头(如Authorization: Bearer <token>)。
      • 检查 token 是否已过期。

5.2 生产环境部署要点

  1. 后端部署

    • 配置文件分离:使用application-prod.yml,并通过spring.profiles.active=prod激活。将数据库密码、密钥等敏感信息放入环境变量或配置中心。
    • 关闭开发特性:设置spring.jpa.hibernate.ddl-auto=validatenone,禁止自动更新表结构。
    • 打包与运行:使用mvn clean package -DskipTests打包,生成可执行的 JAR 文件。使用java -jar booking-system.jar --spring.profiles.active=prod运行。
    • 使用进程管理:推荐使用systemd(Linux) 或 Docker 容器化部署,实现服务自启和监控。
  2. 前端部署

    • 构建优化:运行pnpm run build生成静态文件(位于dist目录)。确保vite.config.ts中配置了正确的base(如果部署在子路径下)和生产环境 API 地址。
    • 静态资源服务:将dist目录下的文件部署到 Nginx、Apache 或对象存储(如 AWS S3、阿里云 OSS)中。
    • Nginx 配置示例
      server { listen 80; server_name your-domain.com; root /path/to/your/dist; index index.html; location / { try_files $uri $uri/ /index.html; # 支持 Vue Router 的 history 模式 } # 代理后端 API 请求 location /api/ { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

6. 系统扩展与最佳实践建议

6.1 功能扩展方向

  1. 支付集成:接入支付宝、微信支付 SDK,实现预约费用的在线支付与退款流程。
  2. 消息通知:集成短信(如阿里云 SMS)或微信模板消息,在预约成功、教练确认、课程提醒等节点自动通知学员和教练。
  3. 排班算法优化:实现更智能的自动排班,考虑教练专长(科目二、科目三)、学员进度、车辆可用性等多重约束。
  4. 数据报表与分析:使用 ECharts 等图表库,为管理员提供可视化的数据看板,展示营收趋势、教练负荷、学员转化率等。
  5. 移动端适配:基于同一套后端 API,使用 Uni-app 或 Taro 等跨端框架开发微信小程序或 App,覆盖更多使用场景。

6.2 工程最佳实践

  1. 后端

    • 统一响应封装:定义如Result<T>的通用响应体,包含codedatamessage字段。
    • 全局异常处理:使用@ControllerAdvice@ExceptionHandler捕获并统一处理异常,返回友好的错误信息。
    • 接口文档:使用Spring Doc OpenAPI自动生成 API 文档(替代旧的 Swagger 2),方便前后端协作。
    • 数据校验:在 DTO 上使用@Valid和 Jakarta Validation 注解(如@NotNull,@Size)进行入参校验。
    • 单元测试:对 Service 层核心业务逻辑编写 JUnit 单元测试,保证代码质量。
  2. 前端

    • TypeScript 严格模式:开启tsconfig.json中的strict: true,利用类型系统减少运行时错误。
    • 组件化与复用:将通用的 UI 元素(如日期选择器、教练卡片)抽离成可复用组件。
    • 状态管理规范:明确 Pinia Store 的职责,避免在组件中直接修改全局状态,通过 Actions 进行变更。
    • API 层抽象:将所有的后端请求统一放在src/api目录下管理,便于维护和 Mock。
    • 环境变量:使用.env.development.env.production管理不同环境的前端配置(如 API 地址)。
  3. 安全与性能

    • 后端安全:引入 Spring Security 实现完整的认证(如 JWT)和授权(基于角色的访问控制 RBAC)。对敏感操作(如取消预约、确认订单)进行权限校验。防止 SQL 注入(JPA 已提供一定防护)、XSS 攻击等。
    • 前端安全:对用户输入进行校验和转义,防止 XSS。敏感信息(如 token)存储在localStoragesessionStorage中需权衡利弊,考虑使用 HttpOnly Cookie。
    • 性能优化:后端可使用 Redis 缓存热点数据(如教练列表、场地信息)。前端可使用路由懒加载、组件异步加载、图片懒加载等技术优化首屏加载速度。

通过以上步骤,你已经完成了一个具备核心功能的驾校预约管理系统。从数据库设计、后端 API 开发到前端页面交互,涵盖了现代 Web 应用开发的主要环节。这个项目不仅是一个可运行的系统,更是一个学习 Spring Boot 3、Vue 3 和 TypeScript 全栈开发的优秀实践案例。在实际开发中,你可以根据具体业务需求,在此基础上不断迭代和完善。