SpringBoot+Vue汽车服务管理系统毕设项目完整开发实践
1. 这个汽车服务管理系统到底解决了什么问题1.1 从毕设题目的角度拆解核心需求SpringBootVue这套组合做Java Web毕业设计已经成了很多同学的默认选择。前几天有人问起汽车服务管理系统完整项目源码的事正好我把手头这套系统的开发过程完整整理了一遍。说实话汽车服务管理听起来范围很大但如果从毕设的角度拆开看核心其实就三件事客户车辆信息要能管起来服务流程从预约到维修完成要能流转起来配件和结算数据要能统计出来。很多同学拿到这类题目后容易犯迷糊是因为不知道该把规模做到多大。我一开始也差点把项目做成一个类似汽车4S店全链路ERP的东西后来及时刹车。毕设级别的系统不需要你覆盖汽车保险、二手车交易、违章查询那些分支业务你只要把车辆进店—预约—派工—维修—领料—结算—离店这条主线跑通再加上几个必要的辅助管理模块就已经是完成度非常高的作品了。我最终确定的模块清单是这样的系统管理用户登录、角色权限、员工账号管理客户管理客户档案、车辆档案、车辆与客户的关联预约管理客户在线预约或前台登记预约支持时间段判断维修工单管理工单创建、派工、维修进度、完工确认配件管理配件信息、入库、出库、库存预警结算管理工单结算、工时费、配件费、支付状态统计报表维修收入、热门服务项目、配件消耗排行这套模块说多不多说少也不少但每个模块之间的数据关系非常清楚客户拥有车辆车辆产生预约预约转化成工单工单消耗配件工单最后结算。这正好是一个完整的业务闭环用来做毕业设计再合适不过。1.2 系统的角色划分与业务流程汽车服务管理系统和一般的CRUD管理系统有个明显区别它天然是多种角色协作的场景。如果没有角色划分所有功能堆在一个页面上老师看了会觉得你没有理解业务。我在这套系统里设计了四种角色角色能做什么不能做什么系统管理员员工账号管理、基础数据维护、查看所有数据不参与具体维修业务前台接待客户登记、车辆登记、预约确认、工单创建不能操作配件出库、不能修改结算金额维修技师查看分配到自己的工单、更新维修进度、申请领料不能创建工单、不能删除工单店长/经理查看所有工单、审核结算、查看统计报表日常业务操作通常不亲自做这个角色划分的意义在于前端路由和后端接口都要跟着它走。比如维修技师登录后前端左侧菜单只显示我的工单和我的领料后端接口在查询工单时会自动追加当前登录用户ID作为过滤条件而不是把全店工单都返回给技师。整个核心流程是这样的客户到店或者打电话预约前台在系统里登记预约信息记录客户、车辆、预计到店时间和服务项目。到了预约时间前台把预约转成维修工单指定维修技师工单状态变成已指派。技师在工单里填写检查结果如果需要更换配件就发起领料申请配件库存扣减。维修完成后技师把工单状态改成待结算店长确认工时费和配件费客户付款工单变成已完成。这套流程走下来每个环节都有状态记录后期做统计报表就有了真实的数据来源。2. 后端SpringBoot工程结构每个包都放在哪里2.1 分层架构的常规布局后端工程我采用的是标准的三层架构Controller、Service、Mapper配合实体类entity和DTO/VO。很多同学喜欢把业务逻辑直接写在Controller里这样代码是能跑但后面写接口文档时你会很痛苦因为方法职责不清晰接口参数也没法规范。我这套系统的包名结构是这样的com.xxx.carservice ├── controller # 接口层只做参数接收和结果封装 ├── service # 业务层放接口和实现类 │ ├── impl ├── mapper # MyBatis的Mapper接口 ├── entity # 数据库实体类 ├── dto # 接收前端参数的封装对象 ├── vo # 返回给前端的结果对象 ├── config # 拦截器、跨域配置、WebMvc配置 ├── utils # JWT工具、统一返回结果工具 ├── common # 全局异常处理、常量类分包看起来是小事但对后期答辩很关键。老师翻代码时如果第一眼看到的是清晰的分层结构印象分立刻就不一样。我习惯在Controller里不写任何SQL相关逻辑只负责调用ServiceService里判断业务规则Mapper只放SQL或者MyBatis-Plus的BaseMapper。比如创建工单的接口Controller里就是接收一个WorkOrderCreateDTO调用service的createOrder方法返回插入后的工单ID。至于工单编号怎么生成、工时费怎么算、通知消息怎么发这些都在Service里。这样每个方法看一眼就知道是干什么的接口文档和代码也能一一对应上。2.2 核心实体的设计思路数据库表结构决定了这个项目最后能走多远。我最后设计的表不算多一共十几张但每张表的字段都经过思考。这里拿几个最核心的实体说说思路。客户表除了常见的姓名、电话、地址之外我加了客户来源字段下拉值是线上预约和到店登记这个字段做统计报表的时候很管用。还有一个最近到店时间不是手动维护的而是每次工单完成时自动更新。这种冗余字段有人会说违反第三范式但在实际系统里非常常见为了查询时少一次关联。车辆表最关键的字段是车牌号、车架号VIN、品牌、型号、里程数。车牌号要加唯一索引因为一辆车反复进店维修你不能每次新建一条车辆记录。VIN是车辆的唯一身份识别码查维保记录时全靠它。里程数则是每次维修后要更新的。维修工单表是整张业务表的中心。字段包括工单号、预约ID、客户ID、车辆ID、技师ID、状态、故障描述、诊断结果、完工时间等。工单号我采用WO日期四位流水号的格式比如WO202406150001。这个编号不要用数据库自增ID直接暴露给前端一是安全问题二是显示不专业。配件表里有个字段容易搞错就是库存数量。我在设计时把库存拆成了总库存和可用库存因为有些配件可能已经预留给某张工单但还没实际出库。不过后来考虑到毕设复杂度我简化成了单一库存字段。如果你想做得更细可以加一个预留数量字段出库时只扣可用库存。2.3 认证与权限JWT还是Session毕设项目里关于登录认证我强烈推荐用JWT而不是传统的Session。原因很简单JWT是无状态的前端拿到token之后存起来每次请求放在Header里后端拦截器校验通过就行。Session的话前端是Vue跨域和CORS配置会麻烦一些而且Session默认存在内存里服务器重启就得重新登录。我用的JWT流程是这样的用户登录成功后后端生成一个token里面包含userId、username、role有效期设24小时。前端把token存到localStorage每次axios请求时从localStorage取出token放到请求头Authorization字段。后端写一个拦截器拦截所有/api/**的请求除了/login和少数不需要登录的接口之外统一校验token。具体代码大致是这样public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getMethod().equals(OPTIONS)) { return true; } String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { token token.substring(7); try { Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); return false; } } response.setStatus(401); return false; } }权限控制只做到角色级别就够了。也就是在Service层判断当前用户的角色能不能执行某个操作。比如删除工单只有管理员或店长可以做技师不能自己删除。前端菜单已经隐藏了入口后端接口还要再判断一次这是很多同学容易漏掉的地方。3. 车辆预约、维修工单和配件库存这条主线怎么实现3.1 从预约到完工的状态机设计状态管理是这类系统里最体现业务理解的地方。我把工单状态定义成五个待指派、维修中、待领料、待结算、已完成。每个状态之间不是随意跳转的而是有明确的前置条件。当前状态允许执行的操作下一状态待指派店长或前台指派技师维修中维修中技师申请领料待领料待领料领料完成后技师确认维修中维修中技师填写完工报告待结算待结算店长确认金额客户支付已完成已完成不可再修改终态状态机设计的好处是前端按钮的显示逻辑变得非常简单——根据工单状态渲染对应的按钮即可。后端在状态变更接口里也要做校验不能让人用一个接口把已完成工单直接改回维修中。预约状态同样要设计好。预约有待确认、已确认、已到店、已取消四种状态。只有当预约被确认之后前台才能把它转成维修工单。这个思路其实跟现实中订餐排队是一样的预约不是一定要生成工单客户可能预约了又取消所以要有一个独立的状态流转。3.2 配件出库与库存回滚的并发处理配件库存这块是很多毕设项目最薄弱的地方因为很多人就是做了个简单的增删改查。但稍微有点经验的老师会问如果两个工单同时领同一个配件库存会不会变成负数这个问题不能靠前端判断必须后端处理。我采用的方案是在配件出库时使用乐观锁。配件表里有一个version字段更新库存的SQL语句长这样UPDATE part SET stock stock - #{count}, version version 1 WHERE id #{partId} AND version #{version} AND stock #{count}如果影响行数为0说明要么版本号不对要么库存不足Service层直接抛出库存不足或已被其他工单占用的异常。这样即使两个技师同时点领料也只有一个能成功。这个逻辑不算复杂但放到答辩中讲属于一个很漂亮的亮点说明你考虑到了并发场景。库存回滚的另一种情况是工单因为客户取消而作废时之前已经出库的配件需要退回。我在工单上加了取消时间和取消原因字段。取消时事务里先更新工单状态再根据工单明细把配件库存加回去。这两步必须放在同一个事务里注解用Transactional任何一步失败都要整体回滚。3.3 数据统计接口里的SQL聚合写法统计报表是这个系统的门面。很多同学做统计就是前端把列表拉回来自己数数据量小的时候没事但数据多了性能就很差。应该用SQL聚合函数直接在数据库层面算好接口返回统计结果。我写了一个仪表盘统计接口返回今日预约数、今日进店数、本月收入、待处理工单数。核心SQL大致是这样的SELECT COUNT(DISTINCT CASE WHEN create_time CURDATE() THEN id END) AS todayReservation, COUNT(DISTINCT CASE WHEN arrival_time CURDATE() THEN id END) AS todayArrival FROM appointment维修收入按月统计则用GROUP BY日期SELECT DATE_FORMAT(pay_time, %Y-%m-%d) AS day, SUM(total_amount) AS amount FROM settlement WHERE pay_time DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY DATE_FORMAT(pay_time, %Y-%m-%d) ORDER BY day这里有一个值得注意的点金额字段在MySQL里要用DECIMAL不要用DOUBLE。DOUBLE会有精度问题比如0.1加0.2可能变成0.30000000000000004做金额累计的结果会非常难看。DECIMAL(10,2)可以精确到分完全够用。4. 前端Vue页面与接口对接的实操细节4.1 路由与权限控制前端的页面结构用Vue实现我选的是Vue2 Element UI的组合因为这个组合在网上的资料最多、最成熟遇到问题也好查。Vue3也可以做但Element Plus和很多第三方组件的坑还比较多毕设时间紧的话没必要冒险。路由设计上我把页面分成两个大区登录页和主布局页。主布局页里再按角色动态挂载子路由。这里有个关键点不要把所有菜单都写在路由表里然后隐藏而是登录成功后根据后端返回的菜单权限动态添加路由。虽然实现起来麻烦一点但老师问起你前端是怎么做权限控制的你能答得有理有据。我的做法是后端登录接口返回一个roles数组前端拿这个数组去过滤一份完整菜单配置表只保留该角色能看到的菜单项然后用router.addRoutes动态加入路由。刷新页面时再从localStorage读取角色信息重新生成路由否则一刷新就白屏。4.2 Axios封装与请求拦截axios封装是Vue项目里绝对不能省的环节。如果不统一封装每一个页面都要自己处理token和错误提示代码会重复到让人崩溃。我的request.js大概做了几件事创建axios实例设置baseURL为/api超时时间10秒请求拦截器里从localStorage取token并放到请求头响应拦截器里统一处理HTTP状态码200返回res.data401跳回登录页403提示无权限500提示服务器错误断网时统一提示网络连接异常响应结构我也做了统一格式{ code: 200, message: 操作成功, data: { } }前端判断业务码而不是直接用HTTP状态码。比如登录时密码错误HTTP状态依然是200但业务码是500或自定义4010这样前端处理起来更清晰。4.3 表格、表单和状态标签的常见交互业务管理页面基本就是表格弹窗表单状态标签的组合。Element UI里最常用的是el-table和el-dialog。我踩过的一个坑是编辑弹窗里调用this.$refs.form.resetFields()之前必须保证打开弹窗时this.form对象里的字段都已经初始化。如果你在data里写死了某个字段弹窗又复用了同一个对象编辑时会把上一次的数据带出来。正确的做法是每次打开弹窗做一次深拷贝或者在this.$nextTick里重置表单。状态标签我用el-tag显示不同状态用不同type颜色。比如已完成绿色、维修中蓝色、待结算橙色、已取消灰色。这个细节很加分业务状态一眼就能区分。另一个要注意的是时间字段的格式化。后端返回的时间可能是2024-06-15T21:30:00前端如果直接显示会带一个T很不美观。我封装了一个dayjs格式化方法所有表格里的时间字段都走这个过滤filters: { formatTime: function(value) { if (!value) return -; return dayjs(value).format(YYYY-MM-DD HH:mm); } }5. SQL脚本里那些容易踩的坑外键、字符集与初始化数据5.1 建表顺序与外键依赖既然项目带了SQL脚本那脚本的质量也直接决定老师怎么评价你的数据库功底。我先说一个最常见的翻车点脚本里建表顺序不对外键关联的另一张表还不存在执行直接报错。所以脚本开头先DROP TABLE IF EXISTS从子表开始删再在下面按父表到子表的顺序创建。最稳妥的方案是干脆不用物理外键只保留普通索引。毕设系统里我建议不要用数据库外键约束。为什么因为物理外键在删除和更新数据时会带来很多限制尤其是业务调试时你想删一条主表记录却被子表记录拦住非常浪费时间。逻辑外键完全能满足需求比如在维修工单表里加一个client_id字段代码里通过这个字段关联客户表查询时用JOIN或者查两次接口都行。这样SQL脚本执行起来更顺畅也不会因为外键把数据搞乱。5.2 金额字段到底该用Decimal还是Double这个问题我上面已经提过这里再展开一下。汽车服务会涉及工时费和配件费工时费可能是每小时80元、客户维修时长2.5小时算出来是200.00元配件费可能是43.50元。如果使用DOUBLE浮点数在计算机里用二进制表示容易出现类似199.99999999997这样的精度问题。虽然在显示时可以用round修一下但累计和时就会积累误差。所以金额字段统一用DECIMAL(10,2)Java实体类里对应的字段类型是BigDecimal。前端返回给用户的金额也用这个类型做序列化不能直接当作普通浮点数处理。SQL脚本里还要注意默认值的问题。比如工单状态字段我用TINYINT默认值0对应待指派状态。有些同学喜欢直接用VARCHAR存待指派也不是不行但用数字状态加注释字段的方式更规范化前端再映射成中文显示。脚本里要写上注释status TINYINT(1) DEFAULT 0 COMMENT 工单状态 0待指派 1维修中 2待领料 3待结算 4已完成 5已取消这样老师打开表结构一看就知道你没有乱来。5.3 初始化数据的讲究管理员账号和默认密码SQL脚本里还应该写好初始化数据不然项目拿过去连登录都登不了体验很糟糕。初始化数据至少要包含这几类管理员账号用户名admin密码用MD5加密后存储演示角色账号比如前台reception、技师technician、店长manager几个测试客户和测试车辆几个常用配件比如机油滤芯、刹车片、火花塞几条预约记录和工单记录方便演示统计图表密码加密这块很多同学会直接存储明文然后被老师批评安全意识不够。我建议用MD5加盐的方式虽然MD5本身不算安全但对毕设足够。代码里提供一个PasswordUtil工具类管理员创建时调用这个工具加密。不过要特别提醒如果初始化数据里有测试车辆车牌号不要用真实车牌随便用测试A12345这种格式就好。另外系统初始化数据里的时间字段不要写死因为拿到源码的人过几年再跑统计报表的时间范围会对不上。建议在SQL脚本里使用日期函数动态生成比如DATE_SUB(NOW(), INTERVAL 7 DAY)。这样不管哪一天运行项目都有近一周的演示数据。6. 接口文档的写法与前后端联调经验6.1 接口文档里必须写清楚的字段说明项目标题里提到接口文档说明文档也是交付物的一部分。很多同学以为接口文档就是复制一段curl请求示例其实远远不够。一份让前后端不吵架的文档至少要写清楚每个接口的请求路径、请求方式、请求头要求、请求参数名称、类型、是否必填、说明、返回结果示例、错误码说明。我习惯用Markdown表格来写参数说明而不是贴一大段JSON让人自己猜。比如创建工单的接口参数名类型必填说明appointmentIdLong是预约单IDvehicleIdLong是车辆IDtechnicianIdLong是维修技师用户IDdescriptionString否故障描述estimateAmountBigDecimal否预估金额返回结果也要给示例并且标注每个字段的含义。比如{ code: 200, message: 操作成功, data: { workOrderId: 25, workOrderNo: WO202406150025 } }你如果自己写前端可能觉得描述字段很麻烦但如果你要把项目作为完整源码给老师或其他人没有接口文档别人对接你的项目或者接手你的代码会非常痛苦。这也是为什么源码附带接口文档能成为卖点的原因。6.2 状态码设计不要只用200和500很多初级项目的接口返回只有两种情况成功返回200异常返回500。这会在联调时带来巨大麻烦——前端几乎无法区分没有权限和参数传错和服务器内部错误。我建议定义一个全局的统一返回对象至少包含以下业务码业务码含义使用场景200操作成功正常返回400参数错误缺少必填参数、参数格式不对401未认证或token失效拦截器抛出403无权限角色权限不足404资源不存在查询的对象已在数据库删除409业务冲突库存不足、预约时间被占用500服务端异常未预料的异常在全局异常处理器里用RestControllerAdvice捕获这些异常统一转成上述格式的返回体。这样前端响应拦截器只需要针对code做判断逻辑会非常清爽。我这里提一个容易忽略的小点分页接口的返回数据格式也要统一。不要一会返回List一会返回带total的对象。我统一用PageResult包含total、records、current、size字段。前端el-table配合pagination组件时完全不用再手动改格式。6.3 联调中常见的接口不一致案例这类系统在前后端联调时最常见的矛盾之一是日期格式问题。比如预约时间这个字段前端传来的是2024-06-15 14:30:00后端如果用String接收并能正常解析就还好但如果你用了LocalDateTimeSpringBoot默认的JSON序列化格式是2024-06-15T14:30:00中间是字母T前端直接显示就会很丑。解决方案是在application.yml里配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8另一个坑是枚举值传递。工单状态在数据库里是数字前端状态标签需要中文有时候编辑工单时下拉框需要把中文逆向转成数字。我推荐的做法是后端提供枚举字典接口返回一个数组[{label:待指派, value:0}, ...]前端用字典接口动态渲染下拉框。不要在前端硬编码数字和文字的映射否则后端改一个枚举值前端页面全都要改。联调时还有一个小技巧浏览器F12的Network面板里先查看请求的Payload格式是不是JSON因为axios默认会发送application/json格式的数据但后端如果用RequestParam接收会一直报参数不匹配。这种情况我遇到过无数次。解决办法是后端用POST加RequestBody接收对象前端传对象时不要手动序列化成JSON字符串。只要两边约定好基本不会出问题。7. 部署上线与毕设答辩准备的个人经验7.1 本地打包与服务器部署源码交付给别人时最好带一份详细的部署说明文档。部署方式我推荐两种本地一键启动和服务器部署。我自己的习惯是先在本地把后端打成JAR包前端执行npm run build生成dist目录然后把dist目录里的静态文件放到后端的static目录下或者单独用Nginx提供静态服务。这里最常出的问题是跨域如果前端打包后部署和后端不在同一源会有跨域问题。最省事的方法是让后端直接托管前端静态文件这样部署一个SpringBoot进程就够了。后端打包时还有两个坑。一个是要确认MyBatis的Mapper XML文件能被打进JAR包如果资源文件的filtering配置不对target目录里找不到xml启动时会报Invalid bound statement错误。另一个是Oracle数据库和MySQL的驱动不要同时引入scope如果写错会导致打包时带上多余的依赖启动变慢还可能冲突。服务器上部署的话我用的是Linux JDK17 MySQL8执行命令就三行java -jar carsystem.jar --spring.profiles.activeprod生产环境配置里修改数据库连接和上传文件的路径。前端如果单独部署就在Nginx里配一个location /api反向代理到SpringBoot的8080端口location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; }这件事本身不难但很多人栽在防火墙端口上。服务器安全组如果没放行8080端口外网肯定访问不了。分配环境时先确认好端口策略。7.2 答辩时老师常问的几个技术点答辩前我建议把以下问题提前准备好我基本都遇到过你的项目是怎么做权限控制的——答JWT拦截器角色判断尽量展开说token无状态的好处。为什么选用SpringBoot而不是SSM——答简化配置、内嵌Tomcat、自动化配置、方便前后端分离。数据库表之间的关系有哪些——画一下E-R图重点说客户、车辆、工单、配件之间的关系。遇到并发问题你怎么处理——答配件出库用了乐观锁事务注解保证数据一致性。前端路由是怎么和权限结合的——答登录后动态路由按角色过滤菜单。项目最大的难点是什么——不要说自己没难点我一般说是业务状态机设计和并发库存扣减这两个是真实业务中比较有深度的地方。这些问题的答案其实在项目代码里都有体现关键是你自己要理解自己写的东西。不要背台词而是把代码里某个类的职责、某个SQL的执行过程讲清楚老师一看就知道你亲手做过。7.3 演示环境的准备技巧最后说说演示环节。很多同学答辩时现场打开项目结果数据库没启动、端口被占用、浏览器缓存了旧数据非常狼狈。我建议准备一个专门的演示环境并配置好固定数据。比如提前录入一个叫演示客户的客户车牌号测A00001名下有几条工单记录仪表盘图表就能直接显示出来。演示顺序上不用把所有页面都点一遍那样老师会走神。我的顺序是先登录讲系统首页统计图再进客户管理新建一个客户并登记车辆然后创建一个预约演示时间冲突提示再把预约转成工单指派技师模拟技师领料最后完成结算展示收入统计变化。整个流程5分钟左右但完整覆盖了系统的所有核心业务比零散展示强得多。演示时关闭浏览器插件的自动翻译别用测试环境数据串库。如果现场网络不好建议把前端静态资源打包到本地把数据库密码也提前准备好。这些看起来是小细节但真的会影响最终成绩。