网约车订单系统实战:Vue+Spring Boot解耦与状态机设计
简介这是一套完整的前后端分离网约车预约系统实战项目源码面向Java与Vue全栈初学者及课程设计开发者解决城市出行场景下的乘客约车、司机接单与平台统一管理三大核心需求。资源包含742个文件涵盖99个Java后端业务逻辑与控制器、35个Vue组件页面、162个JS交互脚本、51个CSS样式文件、79个GIF动效资源及1个SQL数据库脚本整体压缩包仅13.5MB轻量易部署。已有218人学习下载适合快速掌握Spring Boot RESTful接口开发、VueElementUI动态表单与权限路由、多角色状态流转乘客下单/司机履约/管理员审核等关键技能。项目结构规范含完整登录鉴权、余额支付、订单生命周期管理及新闻公告模块预览可见IndexMain.vue.bak等主视图备份文件与3-build.bat等一键构建脚本便于二次开发与教学演示。1. 这不是又一个“仿饿了么”Demo它用真实网约车业务流倒逼前后端解耦深度你见过的 Vue Spring Boot 项目大多停在「用户列表增删改查」层面。但这个网约车预约系统源码从乘客下单扣款、司机接单状态流转、到管理员多维度订单统计整套业务闭环全部落地——它不模拟打车而是按真实运营逻辑设计账户余额、订单生命周期待接单→已接单→行程中→已完成→已取消、车辆类型分级快车/专车/拼车和司机资质审核流程。技术上它没用任何低代码平台或脚手架魔改而是用 Element UI 的el-tableel-pagination实现高并发订单列表的分页缓存用 Vue Router 的路由守卫 Axios 拦截器统一处理 token 过期跳转后端用 Spring Boot 的Transactional Redis 分布式锁保障「司机抢单」场景下的库存一致性。适合想脱离 CRUD、真正理解「订单状态机」「前后端权限隔离」「支付与账户联动」的 Java 全栈开发者尤其对刚接手网约车类 SaaS 系统维护的工程师这份源码就是现成的业务语义字典。2. 前端工程结构解析Element UI 如何支撑高交互网约车界面2.1 目录结构与核心组件职责拆解项目前端基于 Vue CLI 3.x 构建src/views下严格按角色划分视图passenger/乘客中心、driver/司机工作台、admin/管理后台。关键组件命名直接体现业务意图OrderList.vue不仅渲染订单表格还内嵌「取消订单」按钮状态控制逻辑VehiclePublish.vue封装了车型选择、车牌OCR识别占位符、运营证上传校验三重表单验证。特别注意IndexMain.vue.bak和IndexAsideStatic.vue.bak这类带.bak后缀的文件——它们是原始开发过程中保留的布局备份实际运行时由main.js中的路由配置动态加载IndexMain.vue主内容区和IndexAsideStatic.vue静态侧边栏避免因菜单权限变更导致整个布局重绘。BreadCrumbs.vue组件通过$route.matched动态生成路径当访问/passenger/order/detail?id123时自动显示「乘客中心 订单管理 订单详情」而非硬编码面包屑。提示.bak文件不是冗余垃圾而是历史迭代痕迹。部署前需确认vue.config.js中pages配置指向的是无.bak后缀的正式入口文件否则npm run serve会报Cannot find module ./src/main.js错误。2.2 Element UI 关键组件实战配置2.2.1 表格分页与状态筛选联动乘客订单列表使用el-table时必须解决「状态过滤后分页总数不准」问题。源码在OrderList.vue的data()中定义data() { return { tableData: [], pagination: { currentPage: 1, pageSize: 10, total: 0, pageSizes: [10, 20, 50] }, filters: { status: , // 表示全部pending 待接单completed 已完成 dateRange: [] // 时间范围筛选 } } }关键在methods.fetchData()中分页参数与筛选条件合并请求fetchData() { const params { page: this.pagination.currentPage, size: this.pagination.pageSize, status: this.filters.status, startTime: this.filters.dateRange.length ? this.filters.dateRange[0] : , endTime: this.filters.dateRange.length ? this.filters.dateRange[1] : } this.$http.get(/api/order/list, { params }).then(res { this.tableData res.data.list this.pagination.total res.data.total // 后端返回真实总条数 }) }注意Element UI 的el-pagination的total必须由后端返回真实值不能用tableData.length计算。因为筛选后前端只拿到当前页数据total是筛选后的全局总数否则翻页会丢失数据。2.2.2 弹窗表单的字段级校验策略司机发布车辆信息时el-form对「车牌号」采用双校验前端正则预判 后端唯一性校验。rules配置如下rules: { licensePlate: [ { required: true, message: 请输入车牌号, trigger: blur }, { pattern: /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-Z0-9]{5}[A-Z0-9挂学警港澳]{1}$/, message: 车牌号格式不正确, trigger: blur } ], vehicleType: [ { required: true, message: 请选择车型, trigger: change } ] }提交时调用this.$refs.form.validate()触发校验但源码额外增加防重复提交逻辑submitForm() { this.$refs.form.validate((valid) { if (valid !this.submitting) { this.submitting true this.$http.post(/api/vehicle/publish, this.form).then(() { this.$message.success(发布成功) this.dialogVisible false }).finally(() { this.submitting false // 必须重置否则按钮永远禁用 }) } }) }3. 后端核心模块实现Spring Boot 如何保障订单状态机原子性3.1 订单状态流转与事务边界设计网约车订单状态机包含 5 个核心状态CREATED已创建、PENDING待接单、ACCEPTED已接单、IN_PROGRESS行程中、COMPLETED已完成。源码中OrderService.java用Transactional显式声明事务但关键点在于状态变更必须携带前置状态校验Transactional public void updateStatus(Long orderId, String newStatus, String oldStatus) { // 1. 先查再更新避免幻读 Order order orderMapper.selectById(orderId); if (!order.getStatus().equals(oldStatus)) { throw new BusinessException(订单状态异常当前为 order.getStatus()); } // 2. 更新状态并记录操作日志 order.setStatus(newStatus); order.setUpdateTime(LocalDateTime.now()); orderMapper.updateById(order); // 3. 发送状态变更事件如短信通知乘客 eventPublisher.publishEvent(new OrderStatusChangeEvent(orderId, newStatus)); }提示oldStatus参数不可省略。若直接UPDATE order SET statusACCEPTED WHERE id?当司机A和司机B同时抢单时可能两个线程都读到PENDING状态导致同一订单被两个司机接单。强制校验旧状态是乐观锁的轻量级实现。3.2 账户余额扣款与充值的幂等性处理乘客下单扣款逻辑在PaymentService.java中核心是利用数据库唯一索引保证幂等// 创建支付流水表唯一索引 CREATE UNIQUE INDEX uk_order_id ON payment_record (order_id); Transactional public void deductBalance(Long userId, BigDecimal amount, Long orderId) { // 1. 检查余额是否充足 UserAccount account accountMapper.selectByUserId(userId); if (account.getBalance().compareTo(amount) 0) { throw new BusinessException(余额不足); } // 2. 扣减余额先update再insert避免超扣 account.setBalance(account.getBalance().subtract(amount)); accountMapper.updateById(account); // 3. 插入支付记录唯一索引防止重复扣款 PaymentRecord record new PaymentRecord(); record.setOrderId(orderId); record.setUserId(userId); record.setAmount(amount); record.setType(DEDUCT); record.setCreateTime(LocalDateTime.now()); paymentRecordMapper.insert(record); // 若orderId已存在抛DuplicateKeyException }3.3 Redis 缓存与数据库一致性策略司机接单后需实时更新订单状态并推送消息。源码采用「Cache-Aside Pattern」 「延迟双删」// 接单接口 public void acceptOrder(Long driverId, Long orderId) { // 1. 先删缓存防止脏读 redisTemplate.delete(order: orderId); // 2. 更新数据库 updateOrderStatus(orderId, ACCEPTED, PENDING); // 3. 再删缓存确保最终一致 redisTemplate.delete(order: orderId); // 4. 异步推送给乘客通过MQ或WebSocket notifyPassenger(orderId, 司机已接单); }缓存 key 设计为order:{id}value 是OrderVO对象序列化。redisTemplate使用Jackson2JsonRedisSerializer避免StringRedisTemplate的类型转换问题。4. 前后端联调关键配置Token 传递与跨域处理4.1 Vue Axios 请求拦截器统一注入 Token前端所有请求必须携带Authorization头源码在utils/request.js中配置// request.js import axios from axios const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, // 从 .env.development 读取 http://localhost:8080 timeout: 5000 }) // 请求拦截器 service.interceptors.request.use( config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token // 注意 Bearer 前缀 } return config }, error Promise.reject(error) ) // 响应拦截器 service.interceptors.response.use( response response, error { if (error.response?.status 401) { // token 过期清空本地存储并跳转登录页 localStorage.removeItem(token) localStorage.removeItem(userInfo) router.push(/login) } return Promise.reject(error) } ) export default service注意process.env.VUE_APP_BASE_API必须在.env.development中明确定义如VUE_APP_BASE_APIhttp://localhost:8080/api。若漏写VUE_APP_前缀Vue CLI 无法注入环境变量。4.2 Spring Boot 跨域与 Token 解析配置后端WebMvcConfig.java开启跨域Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:8080) // 前端地址 .allowCredentials(true) .maxAge(3600); } }JWT Token 解析在JwtAuthenticationFilter.java中实现public class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token getTokenFromRequest(request); if (token ! null jwtUtil.validateToken(token)) { String username jwtUtil.getUsernameFromToken(token); UsernamePasswordAuthenticationToken auth new UsernamePasswordAuthenticationToken(username, null, getAuthorities()); SecurityContextHolder.getContext().setAuthentication(auth); } filterChain.doFilter(request, response); } private String getTokenFromRequest(HttpServletRequest request) { String bearerToken request.getHeader(Authorization); if (bearerToken ! null bearerToken.startsWith(Bearer )) { return bearerToken.substring(7); // 截取 Bearer 后的 token } return null; } }4.3 数据库初始化与环境适配项目提供1-install.batWindows和1-install.shLinux/macOS一键初始化数据库。执行前需修改application.yml中的数据库配置spring: datasource: url: jdbc:mysql://localhost:3306/ride_share?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver提示MySQL 8.0 必须指定serverTimezoneAsia/Shanghai否则启动报The server time zone value XXX is unrecognized。若使用 MySQL 5.7需将driver-class-name改为com.mysql.jdbc.Driver。5. 生产部署避坑指南从 IDEA 启动到 Nginx 反向代理5.1 IDEA 中正确运行前后端分离项目5.1.1 后端启动步骤在 IDEA 中打开pom.xml右键 →Maven→Reload project确认application.yml中spring.profiles.activedev运行RideShareApplication.java主类观察控制台输出Started RideShareApplication in 3.2 seconds (JVM running for 3.8) Tomcat started on port(s): 8080 (http)访问http://localhost:8080/swagger-ui.html验证 API 文档可访问5.1.2 前端启动步骤进入frontend目录源码中该目录需自行解压通常与后端同级执行npm install安装依赖注意 Node.js 版本 ≥ 14.0修改vue.config.js中的代理配置匹配后端端口module.exports { devServer: { proxy: { /api: { target: http://localhost:8080, // 后端地址 changeOrigin: true, pathRewrite: { ^/api: /api // 保持路径前缀一致 } } } } }运行npm run serve浏览器访问http://localhost:8080注意前端端口是 8080后端也是 8080但通过代理区分注意若出现Invalid Host header错误在vue.config.js中添加disableHostCheck: true仅开发环境。5.2 Nginx 反向代理配置要点生产环境需将前端静态资源与后端 API 统一到同一域名下。nginx.conf示例server { listen 80; server_name ride-share.example.com; # 前端静态资源 location / { root /var/www/ride-frontend; try_files $uri $uri/ /index.html; } # API 接口代理 location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }关键点location /api/的斜杠必须与后端RequestMapping(/api)保持一致否则proxy_pass会多拼一层路径导致 404。5.3 数据库表结构验证技巧项目提供ride_share.sql初始化脚本。导入后用以下 SQL 快速验证核心表关系-- 检查订单与用户关联 SELECT o.id, o.status, u.username FROM order o JOIN user u ON o.passenger_id u.id WHERE o.status COMPLETED LIMIT 5; -- 检查司机车辆绑定 SELECT d.name, v.license_plate, v.vehicle_type FROM driver d JOIN vehicle v ON d.id v.driver_id WHERE v.status ACTIVE;若查询结果为空检查user表中role字段是否为PASSSENGER/DRIVER/ADMIN这是前端路由守卫的权限依据。6. 真实业务场景调试技巧模拟司机抢单并发与订单超时关闭6.1 用 JMeter 模拟高并发司机抢单网约车最典型压力场景是「热区订单被多个司机同时抢单」。用 JMeter 验证OrderService.updateStatus()的状态校验有效性创建线程组设置线程数 100Ramp-Up 时间 1 秒添加 HTTP 请求路径/api/order/acceptBody Data{orderId:123,driverId:1001}添加响应断言检查 JSON 响应中code:200是否存在运行后观察数据库order表status字段只有 1 条记录变为ACCEPTED其余返回{code:500,msg:订单状态异常}提示若所有请求都成功说明状态校验逻辑未生效需检查updateStatus()方法中oldStatus是否传入PENDING且数据库中该订单初始状态确为PENDING。6.2 订单超时自动关闭的定时任务实现源码中OrderTimeoutJob.java使用Scheduled每分钟扫描Component public class OrderTimeoutJob { Scheduled(fixedRate 60000) // 每分钟执行 public void closeTimeoutOrders() { // 查找创建超过15分钟且状态仍为CREATED的订单 ListOrder timeoutOrders orderMapper.selectList( new QueryWrapperOrder() .eq(status, CREATED) .lt(create_time, LocalDateTime.now().minusMinutes(15)) ); for (Order order : timeoutOrders) { // 更新状态为CANCELLED并退还冻结金额 order.setStatus(CANCELLED); order.setUpdateTime(LocalDateTime.now()); orderMapper.updateById(order); // 解冻乘客账户此处简化实际需调用账户服务 accountService.unfreeze(order.getPassengerId(), order.getAmount()); } } }验证方法手动插入一条测试订单statusCREATEDcreate_time设为 16 分钟前等待 1 分钟后查询该订单状态是否变为CANCELLED。6.3 Element UI 表格列宽自适应技巧当订单列表中「出发地」「目的地」字段过长导致表格横向滚动时源码在OrderList.vue中使用 CSS 覆盖/* 解决 el-table 列宽溢出 */ .el-table .cell { word-break: break-all; white-space: normal; } /* 设置特定列最小宽度 */ .el-table__body-wrapper .el-table__body tr td:nth-child(3) { min-width: 180px; /* 出发地列 */ } .el-table__body-wrapper .el-table__body tr td:nth-child(4) { min-width: 180px; /* 目的地列 */ }若仍显示异常检查el-table是否设置了:max-height500该属性会强制截断内容应改为max-heightcalc(100vh - 300px)动态计算。执行npm run build后dist目录下index.html的base href/标签必须与 Nginx 的location /路径匹配否则路由跳转 404。本文还有配套的精品资源点击获取