2026/9/10 19:18:40

SpringBoot+Vue+MySQL+MyBatis代驾管理系统设计与实现

SpringBoot+Vue+MySQL+MyBatis代驾管理系统设计与实现 SpringBoot Vue MySQL MyBatis 这套组合做代驾管理系统应该算得上Java全栈方向里非常典型的毕业设计/课程设计选题了。业务场景贴近生活技术上又有后端、有前端、有数据库用来梳理完整的开发流程很合适。如果你正打算拿这个题目做毕设或者想在一个实际项目里把 SpringBoot 和 Vue 这块内容彻底打通这篇内容应该能帮你省不少时间。代驾管理系统本身解决的是一个很具体的问题平台怎么管理司机、用户怎么下单、订单怎么流转、费用怎么计算、后台怎么审核司机资质。这套东西拆开来看其实就是几个基础模块的堆叠但把它们组织成一套完整的系统涉及的技术点就多了——接口设计、权限控制、状态管理、地图能力对接、数据统计每一步都有讲究。我在这篇里会按照实际开发时的思路把项目的整体设计、技术选型的理由、数据库表结构、后端核心模块、前端页面实现以及我实操时踩过的一些坑完整过一遍。代码不是贴一大段就完事关键是搞清楚每段代码解决了什么问题。1. 项目定位与核心业务设计1.1 代驾行业的真实业务需求代驾系统不是简简单单做几个页面出来就行的。在动手写代码之前首先要搞清楚这个系统到底服务哪些人每个人的诉求是什么。从用户角度来说用户喝了酒要回家打开小程序或者App发起一个代驾订单系统自动匹配附近的司机司机接单后开车送用户到目的地最后按里程和时间计费用户支付、评价整个流程就结束了。从司机角度来说司机需要随时上下线能看到附近的订单接单后开始服务、结束服务还要能查看自己的收入流水和提现情况。从平台管理方来说管理方要审核司机的入驻资质处理用户的投诉查看整体订单数据和营收情况。这三个角色对应的就是系统里的三端用户端、司机端、管理后台。实际做毕设的时候用户端和司机端往往会合并成一个项目通过不同角色登录来区分界面和权限管理后台再单独做一个。这样既能节省工作量也便于演示完整流程。我这次设计的思路就是一套前端工程根据登录角色动态渲染不同页面管理后台单独拆出来。1.2 系统角色与核心流程拆解整个系统的核心角色有三个我先拆解一下各自的权限范围免得后面做权限控制的时候一脸懵平台管理员登录管理后台负责司机资质审核、用户管理、订单查询、基础数据统计、投诉处理。代驾司机司机端小程序或H5页面可以上下线查看附近订单、抢单/接单、开始服务、结束服务、查看今日收入。普通用户发起代驾订单、查看司机实时位置、确认到达、在线支付、订单评价。订单流程是最关键的我建议画状态流转图之前先在脑子里把一条订单从生到死过一遍然后你会发现所有的功能模块其实都围绕这个流程展开。用户创建订单 → 系统自动匹配附近司机 → 司机抢单/确认接单 → 司机到达起点 → 确认开始服务 → 司机送达终点 → 用户确认支付/自动扣费 → 用户评价 → 订单结束。中间任何一步都可能出现异常无司机接单、司机取消订单、用户取消订单、费用争议这些都是系统要考虑的状态分支。业务设计是整个系统的地基地基没打好后面写代码会反复推翻重来。我的经验是写代码之前先把订单状态用数字或枚举定义清楚数据库里用一个小字段标记界面根据状态展示操作按钮这样逻辑会非常清爽。2. 技术选型背后的考量2.1 为什么是SpringBoot而不是传统SSH现在做Java后端项目SpringBoot基本是默认选择但选它不只是因为它火而是它有实打实的优势。SpringBoot 最核心的价值是帮我们搞定了一堆繁琐的配置。以前的SSH或者SSM项目配置文件多到头皮发麻Spring的XML配置一大坨MyBatis的Mapper配置、事务配置、数据源配置每一项都是细节活稍微写错一个坐标就启动失败。SpringBoot 利用自动配置机制把常用的组件全都默认配置好了你只需要按需覆盖就可以了。另外SpringBoot内置了Tomcat不用单独部署War包到外部容器。这对做毕设来说特别省事因为最终演示和答辩的时候可能就一台普通电脑环境不一定齐全。打成一个jar包java -jar直接跑起来方便太多了。2.2 后端分层与MyBatis的定位后端我用的是经典的三层架构Controller层接收参数、Service层处理业务逻辑、Mapper层操作数据库。Controller只负责参数校验和结果封装Service层是业务核心Mapper层就做简单的数据读写。很多人会纠结到底用MyBatis还是JPA还是MyBatis-Plus。这个项目标题明确写了MyBatis那就按MyBatis来。其实MyBatis最大的好处是SQL自己控制尤其像订单系统这种涉及多表关联、复杂统计的场景写原生SQL反而更直观。比如统计司机今日订单量、计算用户消费总额直接用SQL写非常清晰用JPA拼条件反而别扭。我实际操作的时候会顺手把MyBatis的代码生成器用起来。根据数据库表结构一键生成实体类、Mapper接口、XML映射文件节省了不少体力活。生成的代码虽然简单但作为基础层完全够用。2.3 Vue版本与UI组件库的选择Vue这边我建议直接考虑实际安装的Node环境和最终部署的浏览器兼容性。这里有个很容易纠结的点用Vue2还是Vue3。Vue3在性能和组合式API上的优势是明显的但配套的Element Plus一些组件的用法和Vue2的Element UI有些区别网上大部分旧教程和毕设参考都是Vue2的遇到问题查解决方案的时候经常发现版本对不上。如果你对Vue还不算太熟我比较建议用Vue2 Element UI生态成熟、资料多、坑基本都被踩完了。如果导师要求用Vue3那就用Vue3 Element Plus遇到差异时多看官方文档。前端工程我用的是Vue CLI创建的因为Vue CLI创建的项目结构清晰webpack配置不需要自己操心。路由用Vue Router状态管理用Vuex不过这个项目状态不算复杂Vuex用得不算多主要是存一下用户信息和登录状态。3. 数据库设计与核心表关系3.1 表结构设计思路数据库设计这个环节不能跳过因为在动手写后端之前所有业务逻辑最终都要落地到表结构上。我设计的时候遵循一个原则每个角色一张主表每类核心业务一张业务表数据统计尽量用查询实现不额外建冗余表。用户表(user)、司机表(driver)、订单表(orders)、评价表(comment)、投诉表(complaint)、管理员表(admin)、司机提现记录表(withdraw_record)。考虑到司机和用户都需要实名信息两张表里都有姓名、手机号、身份证号、头像这些字段但它们本质上属于两个不同的角色不该合并成一张表否则后续扩展会很麻烦。订单表是整个系统的核心字段比较多我把关键字段列在下面字段名类型说明idbigint主键order_novarchar订单编号user_idbigint下单用户IDdriver_idbigint接单司机IDstart_addressvarchar出发地址end_addressvarchar目的地址start_longitudedecimal(10,6)出发经度start_latitudedecimal(10,6)出发纬度end_longitudedecimal(10,6)目的经度end_latitudedecimal(10,6)目的纬度distancedecimal(10,2)预估里程(公里)estimate_amountdecimal(10,2)预估费用real_amountdecimal(10,2)实际费用statustinyint订单状态create_timedatetime下单时间finish_timedatetime完成时间注意经纬度字段一定要用 decimal不要用 float 或 double因为经纬度需要很高的精度浮点数在MySQL里会有精度丢失的问题。这个是我实际开发中踩过坑的坐标一旦偏了地图显示的位置就会偏离实际很多。3.2 订单状态机的设计订单状态是整个系统里最容易写乱的地方我强烈建议在设计阶段就把状态定义清楚后面前后端都按这套状态来写。我定义了以下几种订单状态0 - 待接单 1 - 已接单 2 - 服务中 3 - 已完成 4 - 已取消 5 - 已退款/已申诉为什么要单独把“已退款/已申诉”列出来因为在实际代驾场景中用户对订单有异议的时候司机的费用可能被冻结状态就不能简单地归为已完成或已取消。把这个状态拆出来后续做投诉处理会方便很多。状态流转要约束好只有待接单状态可以取消已接单状态司机可以开始服务服务中状态司机可以结束服务。前端按钮的显示和禁用全部根据这个状态来判断。后端Service层也要加一道校验。后端一定不能只依赖前端传的状态值自己要去数据库查一下当前真实状态再做更新否则就会出现重复接单、非法流转的问题。3.3 表关联关系说明表之间的关系并不复杂订单表通过 user_id 关联用户表通过 driver_id 关联司机表评价表和投诉表都通过 order_id 关联订单表。司机提现记录表通过 driver_id 关联司机表。实际开发中我的建议是不要给表加太多外键约束逻辑关联就好。外键约束在数据量大的情况下会影响插入性能而且毕设项目里用代码控制关联已经足够。查询的时候用JOIN或子查询把它们关联起来就行一样能保证数据一致性。4. 后端核心模块实现4.1 项目的初始化与基础配置先用Spring Initializr创建一个SpringBoot项目这里有一个非常关键的细节SpringBoot版本选择。我建议用2.7.x这个稳定版本不要一上来就选3.x因为SpringBoot 3要求JDK 17而且很多老版本依赖兼容性还没有完全跟上。我自己用2.7.18配合JDK 8跑得非常稳定参考资料也多。在pom.xml里添加必要依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.21/version /dependency数据库连接我用了Druid连接池因为Druid自带监控页面答辩的时候可以打开监控页面展示SQL执行状况这个挺加分的。application.yml里的配置记得加上时区参数不然数据库连接会报时区错误spring: datasource: url: jdbc:mysql://localhost:3306/dd_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver4.2 统一返回格式与全局异常处理前后端分离的项目接口返回数据必须统一格式不然前端处理数据时会非常痛苦。我的统一返回类是public class ResultT { private Integer code; private String msg; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMsg(success); result.setData(data); return result; } public static T ResultT error(Integer code, String msg) { ResultT result new Result(); result.setCode(code); result.setMsg(msg); return result; } }全局异常处理用RestControllerAdvice注解拦截所有Controller层抛出的异常。业务异常统一抛自定义的BizException参数异常抛MethodArgumentNotValidException未知异常打日志后返回友好提示。前端只需要判断code是不是200然后再取data不用反复去试错。4.3 JWT登录鉴权实现登录鉴权是管理系统的标配我选择了JWTJSON Web Token方式。JWT的好处是无状态服务器不用存Session适合前后端分离场景。用户登录成功后后端生成一个token返回给前端前端每次请求在Header里带上token后端通过拦截器校验token合法性。Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equals(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token null || token.isEmpty()) { response.setStatus(401); return false; } try { Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); return true; } catch (Exception e) { response.setStatus(401); return false; } } }拦截器写好之后还需要在WebMvcConfigurer里注册并且设置好放行的路径比如登录接口、注册接口、首页数据接口。动态放行对毕设来说够用了更复杂的可以按角色区分权限用自定义注解实现但一般不需要。4.4 下单与计价逻辑代驾计价逻辑是用户最关心的也是整个业务里最容易出算法题的地方。我实现的方案是起步价 里程费 时长费。按照大连本地辽B的场景的代驾价格我设的起步价是18元包含前6公里超出部分每公里3元晚间时段22:00-06:00整体加收30%的夜间服务费。public BigDecimal calculateAmount(Double distance) { // 起步价 BigDecimal base new BigDecimal(18.00); BigDecimal baseKm new BigDecimal(6); BigDecimal perKm new BigDecimal(3.00); // 超出部分的里程 BigDecimal distanceBig BigDecimal.valueOf(distance); if (distanceBig.compareTo(baseKm) 0) { return base; } BigDecimal extraKm distanceBig.subtract(baseKm); return base.add(extraKm.multiply(perKm)); }计算的时候这里有一个非常容易踩坑的细节使用BigDecimal而不是double做金额运算。double在计算0.10.2的时候会得到0.30000000000000004金额算错哪怕一分钱都很尴尬所以涉及钱的地方一律用BigDecimal。距离怎么获取前端用地图API的路径规划接口拿到起点和终点的距离然后提交给后端。后端只负责计算费用不自己算距离因为地图的路径规划需要大量路网数据后端做不了。但如果演示环境没有前端地图我们可以在后端根据两点的经纬度用高德Web服务API计算直线距离做一个兜底方案。4.5 派单逻辑实现派单逻辑是代驾系统里比较有业务味道的地方。我的实现比较简单但足够演示查找当前状态为在线的司机计算这些司机与用户起点之间的距离选出距离最近的司机推送订单。系统里我用了简单的方式模拟在线状态司机表里有一个status字段1代表在线0代表离线。司机登录司机端后手动点击“开始接单”状态变为在线。后端查询在线且没有被其他订单占用的司机然后用Haversine公式根据经纬度计算距离。public Double calculateDistance(Double lat1, Double lng1, Double lat2, Double lng2) { double earthRadius 6371.0; double dLat Math.toRadians(lat2 - lat1); double dLng Math.toRadians(lng2 - lng1); double a Math.sin(dLat / 2) * Math.sin(dLat / 2) Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2); double c 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return earthRadius * c; }查询到最近司机后把订单状态置为“待司机接单”并设置订单的driver_id。这里有个小问题如果只派给一个司机司机不点接单订单就永远不会被处理。所以需要加一个超时机制比如2分钟内没有司机接单订单自动取消并提示用户重新下单。这个超时可以用定时任务实现也可以用延迟队列毕设项目里我用一个简单的定时任务扫描每30秒扫一次待接单且超过2分钟的订单自动取消。4.6 司机端与管理后台的部分接口司机端的核心接口有查询附近订单、接单、开始服务、结束服务、查看今日收入、提现。这些接口的实现逻辑都比较类似核心是控制订单状态的流转这里不展开全部代码重点说一个容易忽略的地方接单接口要做并发控制。如果有两个司机同时抢同一单后端必须保证只有一个司机能抢成功。最简单的做法是使用数据库的乐观锁。在订单表加一个version字段更新订单时加上WHERE version #{oldVersion}如果更新影响行数为0说明版本被其他事务修改了就提示“订单已被抢”。这个处理虽然在毕设里不一定有并发压力但写出来用来体现并发意识面试答辩都是加分项。管理后台的接口相对比较直观司机审核列表、用户列表、订单列表和统计看板。统计看板我用SQL做了几个简单统计比如每日订单量、总营收、司机数量、用户数量用ECharts展示成折线图或柱状图。这些数据接口就是简单的聚合查询SELECT DATE(create_time) AS day, COUNT(*) AS order_count, SUM(real_amount) AS total_amount FROM orders WHERE status 3 AND create_time DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE(create_time)5. 前端核心页面实现5.1 Vue项目初始化与路由结构前端我用Vue CLI创建项目然后安装Element UI、Axios、Vue Router、ECharts这几个依赖。路由结构上我把系统分成两个大区用户端/司机端页面和管理后台页面。用户端路径带/app前缀后台路径带/admin前缀。通过路由守卫拦截未登录的请求判断token是否存在不存在就跳登录页。同时根据用户角色在路由的meta信息里标注哪些路由需要什么角色才能访问const routes [ { path: /app, component: AppLayout, children: [ { path: home, component: Home, meta: { roles: [user, driver] } }, { path: order/create, component: OrderCreate, meta: { roles: [user] } }, { path: order/list, component: OrderList, meta: { roles: [user, driver] } } ] }, { path: /admin, component: AdminLayout, meta: { roles: [admin] }, children: [ { path: dashboard, component: Dashboard }, { path: driver/audit, component: DriverAudit }, { path: order/list, component: AdminOrderList } ] } ]5.2 登录页与用户身份管理登录页是最简单的页面之一但也有一些细节需要注意。登录表单提交后后端返回token和用户信息前端把token存到Cookie或者localStorage里然后把用户信息存到Vuex里。这里一定要记得token要放在请求拦截器里统一带上而不是每个接口单独传。我封装了一个axios实例const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token getToken() if (token) { config.headers[Authorization] token } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { if (res.code 401) { // 重新登录 router.push(/login) } return Promise.reject(new Error(res.msg)) } return res }, error { return Promise.reject(error) } )在开发环境下因为前端工程运行在8080端口后端运行在9090端口存在跨域问题。我在后端加了全局CORS配置允许所有来源访问这样前端直接请求后端接口就不会被浏览器拦截。生产环境里前端打包后放在Nginx里通过Nginx反向代理到后端就没有跨域问题了。5.3 地图组件的接入地图是这个项目的核心视觉元素也是答辩时能拿出来讲半天的亮点。我接入的是高德地图JavaScript API。如果团队里没有高德开发者账号也可以换腾讯地图流程类似。创建订单页面需要地图选点功能我的实现是页面初始化时加载地图渲染城市中心点用户点击地图上的任意位置逆地理编码得到详细地址作为起点。选择目的地时再点击地图选终点并调用路径规划API绘制路线、显示距离。// 初始化地图 const map new AMap.Map(mapContainer, { zoom: 12, center: [121.6147, 38.9140] // 大连市中心坐标 }) // 点击事件获取经纬度 map.on(click, (e) { const { lng, lat } e.lnglat AMap.plugin(AMap.Geocoder, () { const geocoder new AMap.Geocoder() geocoder.getAddress([lng, lat], (status, result) { if (status complete) { // 把result.regeocode.formattedAddress填充到表单 } }) }) })司机端的订单列表里可以嵌入地图展示从起点到终点的导航路径。司机接单后页面上可以定时轮询司机当前GPS位置用Marker标记在地图上实现类似实时追踪的效果。毕设项目里用轮询就够了不需要上WebSocket。5.4 管理后台的页面搭建管理后台我同样用Vue Element UI搭建布局是经典的侧边栏导航加内容区域。侧边栏菜单包括数据看板、用户管理、司机管理、订单管理、投诉管理、提现管理。数据看板页面用ECharts做图表展示我做了两个核心图表近7天订单趋势折线图和实时订单状态分布饼图。ECharts的配置项不算复杂拿到后端返回的数据直接塞进去就行。司机审核页面是一个非常典型的表格操作页面表格展示待审核司机的实名信息和上传的驾驶证照片管理员可以点击通过或驳回。这里有一个细节图片上传到服务器后前端要能通过URL访问到。我的方案是配置一个虚拟路径映射到本地上传目录spring: mvc: static-path-pattern: /uploads/** resources: static-locations: file:E:/upload/这样前端直接访问http://localhost:9090/uploads/xxx.jpg就能看到图片。5.5 前端的权限控制明细前端权限和路由守卫配合主要做两层控制。第一层是页面级别的控制就是前面讲的路由meta里的roles字段用户访问不具备权限的路由时会被重定向到403页面。第二层是按钮级别的控制比如用户端页面上只有在用户角色下才显示“发起订单”按钮司机角色下显示“开始接单”按钮。这个用Vue自定义指令实现Vue.directive(hasRole, { inserted: function (el, binding) { const roles store.state.user.roles if (roles !roles.includes(binding.value)) { el.parentNode.removeChild(el) } } })前端的权限控制不是安全问题只是提升用户体验真正的安全保障还是后端的拦截器。前端隐藏了按钮如果用户手动请求接口后端没有权限校验数据照样会泄露。所以我前后端都会校验缺一不可。6. 常见问题与排查技巧实录6.1 MyBatis容易出现的问题MyBatis用起来简单但有几个坑几乎每个新手都会踩一遍。最常见的就是XML映射文件里的#{}和${}用混。#{}是预编译方式会生成占位符能有效防止SQL注入${}是字符串拼接当需要动态传入表名、列名这种SQL片段时才会用到。我在项目里强制要求所有条件值都用#{}只有排序字段这种白名单场景才用${}而且还要做合法性校验。还有一个坑是Mapper接口和XML文件的namespace或方法名对不上一启动就报Invalid bound statement (not found)。排查思路是检查XML文件的namespace是否等于接口全限定名检查方法ID是否等于接口方法名检查Mapper接口是否被Spring扫描到检查XML文件是否在resources目录下且被编译到classes目录。6.2 前后端联调时接口报错的问题联调阶段最容易遇到的问题是想不通为什么PostMan能通、浏览器就报错。这是典型的CORS跨域问题。浏览器是基于同源策略的如果前端端口是8080、后端端口是9090浏览器会判定为跨域请求。解决办法有很多我在后端直接加了CORS配置。另一个容易踩的坑是参数格式不一致。后端接口接收JSON对象前端axios传递数据时如果没有设置正确的Content-Type或者传了一个FormData对象后端拿到的参数就是null。这个调试技巧是在浏览器F12的Network面板里看请求Payload确认是JSON格式再往后排查。6.3 启动和部署的坑用SpringBoot做项目启动时经常会遇到端口占用。后端改了端口号之后前端axios的baseURL也要同步修改不然接口全部404。还有一个部署相关的问题前端构建后如何部署。我的项目在最终交付时是打成jar包前端dist文件夹复制到SpringBoot的static目录下这样访问后端端口就能直接打开整个系统方便导师演示。实现方式很简单在Maven构建时执行插件拷贝前端构建产物或者手动复制都行。需要注意如果用了前端路由的history模式刷新页面会出现404需要配置一个路由回退到index.htmlConfiguration public class WebConfig implements WebMvcConfigurer { Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(/{path:[^\\.]*}).setViewName(forward:/index.html); } }用hash模式就不会有这个问题但URL上会带个#不好看。我建议直接用hash模式省事毕设里完全够用。6.4 数据库相关常见问题数据库乱码是很常见的问题。根源是数据连接串没指定字符集或者建表时表结构默认字符集不是utf8。解决办法是在MySQL建库时指定CREATE DATABASE dd_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;utf8mb4是真正的UTF-8比utf8多支持了一些特殊字符比如emoji。虽然业务里不一定用到但既然做数据库设计直接按标准来就好。还有一个问题是MySQL 8.x的默认认证方式是caching_sha2_password一些旧版的JDBC驱动不兼容。解决办法是使用新版驱动或者创建用户时指定mysql_native_password认证方式。我直接用的新版驱动一劳永逸。6.5 订单并发场景的细节最后再说一个容易被忽视的并发问题。当两个司机同时点击“接单”时如果后端没有对订单表的状态做并发控制两个人可能都会接单成功导致一个订单被两个司机占用。我实现的是乐观锁方案更新订单时带上versionUPDATE orders SET driver_id #{driverId}, status 1, version version 1 WHERE id #{orderId} AND status 0 AND version #{version}如果返回影响行数为0说明订单已经被别人抢了就返回“手慢了订单已被其他司机接走”。这段代码虽然简单但体现了对并发问题的思考面试或者答辩的时候被问到订单并发问题可以直接讲这套方案。7. 写在最后的实操体会做完这套系统最大的感觉是一个看似“常规”的管理系统真正从零搭起来的时候每一步都要做很多取舍和决策。比如技术选型时用Vue2还是Vue3、用原生MyBatis还是MyBatis-Plus、用JWT还是Session没有绝对的对错关键是想清楚自己的场景和成本。最后再分享一个小技巧项目里可以加一个初始化的数据库脚本里面预置一批模拟数据——10个用户、5个司机、几十条订单记录方便随时演示和调试。管理员账号、司机账号、用户账号都预设好放进README里谁拿到项目都能快速跑起来。如果你正在做类似的选题或者准备拿这个项目去面试建议不要把精力全花在堆页面上把订单状态流转、计价逻辑、派单策略、并发控制这几个核心点讲清楚比做一百个没用的列表页都要有价值。项目是死的但你对业务和技术的理解是活的这几处能讲明白就已经超过大部分人。