
我们团队一直在做Java方向的毕业设计和企业实训项目最近刚把一个城市固废清运车辆管理系统完整跑通从前端页面到后端接口从数据库建模到服务器部署整个链路都走了一遍。这个项目看起来只是一个普通的Spring Boot管理后台但真正做进去才发现固废清运这个场景比想象中复杂得多。这篇文章我会把这个项目从0到1的完整思路整理出来包括业务建模、技术选型、核心模块的实现逻辑、部署过程踩过的坑以及一套可以直接拿来用的排查手册。不管你是准备拿它当毕业设计还是公司内部要做类似的车辆调度系统这篇文章应该都能帮你少走很多弯路。1. 项目整体设计与思路拆解1.1 城市固废清运的业务痛点先说业务背景。城市固废清运听起来简单无非就是垃圾车把垃圾桶里的垃圾运到处理站但实际的运营管理远不止这些。环卫公司需要知道每辆车每天跑了哪些点位、清运了多少桶、油耗是多少、司机有没有按时出车、垃圾处理站有没有满仓风险这些信息如果全靠人工登记和电话调度效率非常低。我接手这个项目的时候校方给出的需求核心是把车辆、司机、垃圾桶点位、清运任务、处理站这几个要素统一管理起来做成一个可视化的管理系统。基于Spring Boot这套技术栈前端做一套管理界面后端提供RESTful API数据库存储核心业务数据。项目定位是中小型环卫企业的信息化管理工具不用做到城市级别的智慧环卫那么重但要在业务闭环上说得通。清运任务能派发、车辆能调度、垃圾桶能监测、轨迹能追溯这四个流程走通了项目就立得住。1.2 这类系统适合谁、能学到什么如果你是准备拿这个项目做毕业设计这套系统的业务复杂度刚刚好——它比单纯的CRUD管理系统多了一些业务深度比如任务指派逻辑、满溢状态判断、路径规划接口但又不至于涉及高并发、分布式这些超出课程范围的内容。如果你是做Java开发、想找一个完整的Spring Boot项目练手这个项目也有值得研究的地方MyBatis-Plus的灵活运用、JWT权限校验、定时任务的业务落地、高德地图API的集成方式、还有前后端分离项目打包部署的完整流程。我按企业里做项目的习惯把整体架构拆成了四层Controller层负责接口接收Service层处理业务逻辑Mapper层操作数据库前端页面通过HTTP调用后端接口。整个项目的核心流程是管理员创建清运任务系统根据垃圾满溢状态和车辆位置派发给司机司机完成清运后更新状态管理人员通过可视化页面查看完成情况。2. 技术选型与底层逻辑2.1 为什么是Spring Boot而不是其他框架现在做Java后端项目Spring Boot基本是唯一的主流选择。原因很简单它把Spring那套复杂的XML配置全部自动化了内置Tomcat打jar包就能直接跑生态成熟度也最高。我见过有人用SSHStrutsSpringHibernate组合做校园项目的且不说Struts2的漏洞问题光是那堆配置文件就够折腾几天的。Spring Boot的starter机制让依赖管理变得非常轻量引入什么功能就加什么依赖不用考虑版本兼容问题只要用Spring官方推荐的BOM版本。这个项目我用了Spring Boot 2.7.x原因很实际JDK 8的兼容性最稳毕业设计答辩的时候环境变量不会出幺蛾子。Spring Boot 3.x虽然性能更好但它强制要求JDK 17很多学校的实验环境还是老版本JDK部署阶段容易卡壳。如果企业里用可以根据现有服务器环境来选择。2.2 配套组件的选型考量整个项目的技术栈是这套组合持久层MyBatis-Plus。它是在MyBatis基础上封装的增强工具内置了通用的CRUD方法、分页插件、条件构造器写SQL的频率至少降低60%。核心业务表我都用它的LambdaQueryWrapper来组装查询条件代码可读性比原生MyBatis的XML配置好太多了。数据库MySQL 5.7或8.0均可。实体关系不算特别复杂用关系型数据库最合适。前端Vue 2 Element-UI。这是国内用得最多的管理后台组合组件齐全表格、表单、弹窗、树形控件基本覆盖了所有管理系统的常见需求。权限控制JWT Token。状态无关、无session共享问题后端只需要拦截器校验token不用像传统Session那样做会话保持部署的时候尤其省心。地图服务高德地图Web服务API。车辆定位、轨迹回放、路径规划都用它免费版配额够demo使用了。定时任务Spring Boot自带的Scheduled注解。每天定时检查垃圾桶的满溢状态生成待处理的任务列表。选这些方案的核心逻辑是不求最前沿但求每一环都有成熟案例可以查。毕业设计也好企业实训也罢最怕的是选了冷门框架出了问题连个参考文档都找不到。2.3 数据库设计的关键思路这是整个项目里我花时间最多的地方。系统核心的表有这些vehicle_info车辆信息表车牌号、车辆类型压缩式垃圾车/勾臂车/洒水车、载重、容积、状态空闲/忙/维修、购买时间。driver_info司机信息表姓名、手机号、驾驶证号、所属车队、状态。dump_point垃圾桶点位表点位名称、详细地址、经度、纬度、垃圾桶数量、清运周期。dump_record清运记录表点位ID、车辆ID、司机ID、计划清运时间、实际清运时间、清运桶数。task_order任务表任务编号、车辆ID、司机ID、任务类型清运/维修/巡检、状态待接单/进行中/已完成/超时、优先级。fill_status满溢监测表点位ID、垃圾桶编号、满溢度百分比、上报时间、状态正常/预警/满溢。表结构设计遵循了三个原则。第一业务数据流水单独建表不跟基础信息混在一起这样统计报表好写、历史数据好查。第二所有时间字段统一用datetime类型配合MySQL的时区配置避免查询结果的偏移。第三车辆和司机尽量解耦车辆表不直接存司机ID而是通过任务表建立关联——实际业务中一辆车可能多个司机轮班直接把司机ID挂在车辆表上后期扩展会很痛苦。数据库的初始化脚本加上测试数据一共190多行SQL包含了5辆车、6个司机、15个垃圾桶点位、几十条任务记录。测试数据不要随便乱造得贴合真实业务比如点位的经纬度我用的是真实城区坐标这样高德地图回显的时候看起来才像那么回事。3. 核心功能模块与实现细节3.1 车辆档案与司机管理模块车辆和司机管理是系统的地基功能本身不复杂但有几个细节值得展开。车辆管理这块我除了做常规的增删改查之外还加了一个状态看板。车辆状态我设计成四种空闲、作业中、维修中、停用。判断依据是如果有任务正在进行中task_order表中存在status为进行中且关联了该车辆ID的记录那么车辆状态自动置为作业中如果车辆在维修记录表中存在未完工的记录则显示为维修中。这个逻辑看起来简单但踩了一个坑刚开始我把车辆状态直接做成手动维护的字段后台管理人员经常忘了改状态导致派单的时候把作业中的车又派出去了。后面改成自动推导——车辆状态不直接存储而是通过任务表实时计算。好处是数据永远准确代价是多了一条关联查询但车辆数量就几百台性能完全不是问题。司机管理模块我额外加了一个工作量统计的子页面。每个月末系统根据task_order的实际完成时间统计每个司机的完成单数和总清运桶数管理员可以一键导出Excel报表。这个功能用的方式是Java后端生成Excel用的EasyExcel库代码量很小但放在毕业设计答辩里是个加分项。3.2 清运任务的派发与流转逻辑任务的派发是整个系统的业务核心。最开始的版本做的是管理员手动指定车辆司机来生成任务但实际业务中管理员根本不知道哪辆车离垃圾桶点位最近。后面我改成了一种更贴近真实业务的派单逻辑手动派单 智能推荐。智能推荐的核心逻辑是这样的管理员选择了垃圾桶点位之后系统先查出所有状态为空闲的车辆然后调用高德地图批量测算每个候选车辆到目标点位的行驶距离和预计耗时按距离升序排列默认推荐距离最短的那辆。同时地图上会把所有空闲车辆的实时位置用标记画出来管理员可以自己判断。这里用到了高德地图的批量距离计算APIPOST /batch-calculate一次请求最多可以计算100组起点终点的距离。需要注意的点是高德地图Web服务API的Key要配置正确的域名白名单否则后端请求会被拒绝返回错误码。我在第一次联调的时候就卡在这里控制台一直报错AMAP_SERVICE_NOT_AVAILABLE排查了半天才发现是自己配置Key的时候把白名单加错了。任务状态的流转我用了一个标准的状态机待接单 - 进行中 - 已完成中间有两个例外分支超时未接单和任务取消。司机端的Web页面只显示当前分配给自己的任务点击开始清运按钮后状态变为进行中界面上会播放实时导航路径调高德地图的路径规划接口。这个部分涉及前端比较多的地图交互逻辑后面细说。3.3 垃圾桶满溢监测与预警垃圾桶满溢监测是这个项目里最有智慧环卫感觉的模块。真实业务中垃圾桶里面会装超声波传感器定时上报满溢数据。考虑到毕设项目不是硬件的我直接用了一种更轻量的方式:模拟数据 阈值判断。我在数据库里建了一张fill_status表然后写了一个数据模拟器——每10分钟随机生成一批满溢度数据模拟传感器的上报行为。系统里设置两个阈值当满溢度超过60%时点位状态标记为预警超过85%时标记为满溢系统自动生成一条清运任务待办。这个模块真正的实用点是任务优先级判断。管理员在任务列表页看到的不只是哪个点位该清了而是能按紧急程度排序——满溢度越高任务优先级越高默认排序越靠前。这个设计出来之后整个系统的任务流转逻辑就通顺了点位传感器/模拟器上报满溢数据 - 系统判断是否需要清运 - 生成任务 - 调度员根据推荐派单 - 司机接单清运 - 回写完成标记。定时任务的实现用Spring Boot的Scheduled注解cron表达式配置成每10分钟触发一次。这里要提醒一下Scheduled默认是单线程的如果任务执行时间超过了间隔时间会有任务堆积的问题。实际开发中如果定时逻辑比较复杂建议设置一个ThreadPoolTaskScheduler来保证并发安全。3.4 轨迹回放与可视化看板车辆轨迹回放是答辩时最吸引眼球的功能。实现方式不复杂车辆每次执行任务的中间位置点按时间顺序存一张轨迹表字段包括车牌号、经度、纬度、定位时间。页面上通过高德地图的Polyline组件把轨迹点画出来再加上一个时间轴滑块点击播放按钮就能看到车辆从起点到终点的完整行进过程。这个功能的数据来源在真实项目中是车载GPS设备上报但同类问题一样毕设项目没有硬件我写了一个模拟器每次派单之后按固定间隔生成模拟定位数据时间跨度控制在10-20分钟之间这样轨迹画出来比较自然。可视化看板是另一个加分项用ECharts做的四个核心卡片今日清运任务总数、今日完成率、车辆出勤率、各点位满溢状态分布图。数据通过后端一个统计接口一次性返回前端页面加载完毕之后异步渲染。ECharts是图表库里面上手最简单的配一个饼图加两个柱状图整个看板的视觉效果就出来了。写统计SQL的时候注意一点日期一定不要直接拼字符串跟数据库比对会走不上索引要使用DATE函数或者参数化查询。4. 实操过程与核心环节实现4.1 后端工程结构与编码规范整个后端工程我是按标准的分层结构来组织的src/main/java/com/huanwei/garbage ├── controller // 接口层只负责参数接收和结果封装 ├── service // 业务层所有核心逻辑都在这 │ └── impl // Service实现类 ├── mapper // MyBatis-Plus的Mapper接口 ├── entity // 数据库实体对应的Java类 ├── config // 配置类拦截器、CORS、定时任务、MyBatis-Plus分页插件 ├── common // 统一返回结果、异常定义、工具类 └── task // 定时任务类编码规范上我习惯做几件事。第一统一返回值格式定义了一个Result类Code为200时表示成功否则携带errorMsg。前端拦截器统一判断Code这样错误处理逻辑只写一次。第二所有接口的路径从/admin或/api开始方便后面统一做权限校验。第三数据库字段的命名统一用下划线风格代码里用驼峰MyBatis-Plus默认开启了驼峰映射不配置也能直接映射成功。另外还做了一个全局异常处理器用RestControllerAdvice注解拦截业务异常和参数校验异常统一转成Result返回。这不算什么高深技术但实际开发体验会好很多——不至于数据库字段超长的报错直接裸奔到前端。4.2 核心接口的代码实现我挑一段车辆派送的接口逻辑来说明吧这是整个项目里业务逻辑最完整的一段。管理员的请求体大概是{ pointId: 12, driverId: 3, taskType: CLEAR, priority: 2 }Service层的核心代码如下经过简化public Result createClearTask(CreateTaskRequest req) { // 校验点位是否存在且确实需要清运 DumpPoint point dumpPointMapper.selectById(req.getPointId()); if (point null) { return Result.error(垃圾桶点位不存在); } FillStatus latestFill fillStatusMapper.selectLatestByPointId(req.getPointId()); if (latestFill null || latestFill.getPercentage() 50) { return Result.error(该点位暂未达到清运条件); } // 校验司机是否空闲 Driver driver driverMapper.selectById(req.getDriverId()); if (driver null || 休假.equals(driver.getStatus())) { return Result.error(司机不可用); } // 获取待命车辆优先分配当前空闲车辆中最近的一辆 ListVehicleInfo idleVehicles vehicleMapper .selectList(new LambdaQueryWrapperVehicleInfo() .eq(VehicleInfo::getStatus, 空闲)); VehicleInfo assignedVehicle null; if (CollectionUtils.isNotEmpty(idleVehicles)) { assignedVehicle findNearestVehicle(idleVehicles, point); } if (assignedVehicle null) { return Result.error(暂无空闲车辆可派送); } // 创建任务订单状态为待接单 TaskOrder order new TaskOrder(); order.setTaskNo(generateTaskNo()); order.setPointId(point.getId()); order.setVehicleId(assignedVehicle.getId()); order.setDriverId(driver.getId()); order.setStatus(待接单); order.setPriority(req.getPriority() null ? 1 : req.getPriority()); taskOrderMapper.insert(order); // 将车位状态改为作业中 assignedVehicle.setStatus(作业中); vehicleMapper.updateById(assignedVehicle); return Result.success(order); }这里有几个细节值得说明。findNearestVehicle方法调用了高德地图的批量距离计算接口代码里要注意做空值判断因为高德接口偶尔会返回null值。生成的taskNo用了日期加序号的格式例如CL202501041003好处是按时间可读且不用额外建序列表。这段逻辑在线上的链路是先做业务校验——点位存在吗、满溢度达到阈值了吗、司机空闲吗然后分配车辆最后落库。每一步失败都会返回明确的错误信息前端页面把这些错误直接Toast出来。那种一把梭把所有条件全部往数据库里面硬塞的写法在业务稍复杂的项目里非常难维护。4.3 前端核心页面的实现方式前端工程我用Vue CLI建的目录结构比后端简单得多核心页面有登录页、系统首页看板、车辆管理页、司机管理页、点位管理页、任务管理页、轨迹回放页。任务管理页是最核心的布局是左边点位列表、中间地图区域、右边任务信息面板。管理员在地图上看到带有满溢预警标识的点位单击后右侧面板展示该点位的垃圾桶现状、最近清运记录和距离最近的3辆空闲车辆。然后管理员手动确认指派车辆和司机点击派单按钮完成任务创建。前端的难点在地图API的封装。Vue页面里集成高德地图JS API 2.0需要在index.html里加载JS脚本并且配置安全密钥。踩过一个坑高德地图从2.0版本开始除了Key还要配置安全密钥jscode如果没有配置地图会直接白屏而且控制台只报一个CORS的错很容易被误导到跨域问题上。前端调接口的工具我统一封装了一个request.js基于axios请求拦截器里带上JWT Token响应拦截器里统一处理业务异常码。这样一个接口调用就三行代码createTask(data).then(res { if (res.code 200) { this.$message.success(派单成功); this.refreshTaskList(); } });图表页和看板页相对简单引用了echarts之后配置好option放入容器即可。注意一下ECharts如果是从npm包引进来的Vue里的DOM可能还没完成渲染要在this.$nextTick()里面再初始化图表否则容器宽度是0图表会挤成一团。4.4 部署环境的准备与前端打包项目开发完之后要部署讲解我准备了两种部署方式本地一键启动和服务器部署。本地一键启动的步骤是新建MySQL数据库garbage_manage执行项目根目录下的sql脚本初始化数据。修改application.yml里数据库的用户名密码改成自己环境对应的值。直接运行Application.java启动后端服务。前端工程npm install安装依赖之后npm run serve浏览器访问localhost:8080进入系统。这个流程对毕设环境来说最稳定。需要注意的是Spring Boot内置的Tomcat默认端口是8080如果前端也是8080端口就要改一个一般前端用8081或者后端改到9090看个人习惯。服务器部署我用的是打包成jar的部署方式。后端执行mvn clean package -DskipTests生成的jar包通过rsync上传到服务器然后使用systemd来托管服务进程。systemd配置文件非常简单[Unit] DescriptionGarbage Vehicle Management System Afternetwork.target [Service] Typesimple Userroot WorkingDirectory/opt/garbage-system ExecStart/usr/bin/java -Xms256m -Xmx512m -jar garbage-system.jar Restarton-failure SuccessExitStatus143 [Install] WantedBymulti-user.target这个配置比直接nohup java -jar好得多进程挂了会自动重启服务器重启也会自动拉起服务排查问题的时候systemctl status能看到完整的日志。前端打包执行npm run build生成的dist目录上传到Nginx的html目录同时需要配置Nginx把/api的请求反向代理到后端服务地址。server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html; index index.html; } location /api/ { proxy_pass http://127.0.0.1:9090; 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_pass后面加不加路径反斜杠是有讲究的。如果后端接口是/api/task/list而proxy_pass http://127.0.0.1:9090后面不加路径请求会完整转发为/task/list如果后面加了斜杠/api前缀会被吞掉。这个细节不知道坑了多少人。我的习惯是后端所有接口统一前缀/api然后proxy_pass直接代理到后端根路径这样最不容易混淆。5. 常见问题与排查技巧实录5.1 数据库连接超时与自动断开项目跑了一段时间后用户反馈页面打开很慢报错信息是Communications link failure。原因很简单MySQL默认有一个wait_timeout参数如果连接在8小时内没有活动数据库会自动断开。连接池里的connection还在用实际已经失效了。处理方式有两种。一是在JDBC连接串上加上autoReconnecttruevalidationQuerySELECT 1参数Druid连接池会定期发送探测SQL保持连接活跃。二是在Druid配置里设置testWhileIdletrue每次获取连接前做一次检测。我更推荐第二种因为autoReconnect在MySQL 8.0版本已经标记为失效了。另一个容易踩的坑是时区问题MySQL 8.0默认时区是UTC如果你在配置文件里没有指定serverTimezone可能会在插入时间字段时报错。建议统一配置成serverTimezoneAsia/Shanghai。同时Spring Boot的Jackson序列化时间时默认会转成UTC需要配置spring.jackson.time-zoneGMT8来保证前端拿到的时间跟本地一致。5.2 CORS跨域问题的典型表现前后端分离开发时跨域是绕不开的问题。常见报错是blocked by CORS policy: No Access-Control-Allow-Origin header is present on the requested resource。解决方案是在Spring Boot里配置一个CorsFilter允许前端的来源域名访问。更省事的做法是用CrossOrigin注解加在Controller类上但如果有全局CORS统一配置不需要每个Controller都写一遍。注意一点一旦配置了CORS全局允许安全配置CorsFilter时就要放行OPTIONS预检请求否则前端实际的接口请求还是会被拦截。我调试跨域问题时发现最有效的手段是直接看浏览器Network标签页里的Preflight请求状态码和响应头比看后端日志快得多。5.3 地图API加载失败与定位偏差做地图相关功能时遇到三类高频问题。第一类是API Key不生效。原因是高德地图的JavaScript API 2.0除了Key之外还必须配置安全密钥jscode在页面上初始化时要把两个值一起传给AMapLoader。忘记配置时的表现是地图白屏控制台报script error这个报错信息没有明确的指向性很容易让人误判成网络问题或者跨域浪费很多时间。第二类是经纬度拿不到。原因是浏览器在高版本之后对于获取用户定位有权限限制HTTP环境下navigator.geolocation经常拿不到坐标。解决办法是改成IP定位或者让用户手动在地图上点击选点。第三类是轨迹偏移。高德地图使用的是GCJ-02坐标系如果模拟数据使用的是原始GPS坐标WGS-84轨迹会整体偏移几百米。不要手动去做坐标转换直接用高德提供的坐标转换API同时模拟器在生成定位数据时就要直接使用GCJ-02的坐标避免到处转换。5.4 Maven依赖冲突与下载失败新拉代码之后最常碰到的问题是依赖下载不下来或者版本冲突。常见表现是Idea里一堆ImportError或者编译时提示找不到符号。依赖冲突我遇到最典型的场景是mybatis-plus和mybatis的版本混用。Spring Boot 2.7.x下面正确做法是只引入mybatis-plus-boot-starter依赖不要手动再加mybatis-spring-boot-starter否则会出现两个SqlSessionFactory的冲突。MyBatis-Plus的版本跟Spring Boot版本是有对应关系的我用的是mybatis-plus-boot-starter 3.5.3.2版本在Spring Boot 2.7.x下测试过稳。Maven下载慢的话建议配置阿里云镜像在maven的settings.xml里添加mirror节点。如果依赖还是反复下载失败优先考虑是不是公司网络对Maven中央仓库的访问做了限制换个镜像源一般能解决。5.5 部署时数据库初始化失败部署讲解中最容易出现翻车环节的是数据库初始化。我遇到过三种情况第一种脚本语句执行报错原因是MySQL 8.0对utf8mb4的默认排序规则改成了utf8mb4_0900_ai_ci老项目脚本里常用的utf8mb4_general_ci在8.0上也可以正常兼容但如果用的开发版本是MySQL 5.7反过来就不行。建议写建表语句时直接不指定排序规则用数据库默认值。第二种数据导入中文乱码。解决方法是导入SQL脚本前执行set names utf8mb4同时确保SQL文件本身保存为UTF-8编码。Idea的右下角能看当前文件的编码格式如果是GBK先转成UTF-8再执行。第三种找不到数据库账号。很多服务器默认的MySQL实例里的root账号是只能本地登录的。项目配置里写的localhost连接没问题但如果你打算让其他机器上的前端环境直连数据库就要建立允许远程访问的用户。安全起见建议只创建一个业务专用的账号授予业务库的增删改查权限而不是把root暴露出去。5.6 部署上线后的内存与性能问题Java应用部署到云服务器上最容易遇到的问题是内存不够。默认的无参数jar启动会使用JVM初始堆大小的1/4如果服务器是1G内存光JVM就可能占掉250M再加上MySQL或Nginx内存很容易亮红灯。我上面的systemd配置里显式指定了-Xms256m -Xmx512m256M作为初始堆512M作为最大堆看业务规模决定至少给系统留下500M的空闲。如果出现接口响应慢的情况先看一下是不是SQL层面的性能问题。MyBatis-Plus可以配置日志输出SQL在application.yml里加上mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl开启后控制台会打印每一条执行SQL。慢SQL的大头通常是没有正确使用索引。比如任务表上如果经常要查询某个司机的待接单任务那就要在driver_id和status上建联合索引否则全表扫描数据量一上来性能直接崩。6. 写在最后的一些体会做个项目不是把代码写完就结束了真正花时间的往往是数据库字段设计、状态流转的梳理、异常情况的覆盖面。这个项目我从空手到完整跑通大概用了八天左右真正敲代码的时间其实四天就够剩下的一半时间都在调整业务细节和部署环境。有一点感触很深做管理系统这样的项目不要一开始就埋头写代码先花半天时间想清楚业务流转的每一个环节画出任务状态的流转图设计好数据库表之间的关系代码写起来会顺畅非常多。如果你也准备做类似的项目有一点建议现在网上能下载到的Spring Boot管理系统源码非常多但能真正讲清楚设计逻辑的并不多。拿到一份源码之后先打开数据库的表结构文档再把核心业务逻辑的时序图画出来最后再动手跑代码——这个顺序远比直接看代码有效。有任何业务上的疑问或者开发部署中碰到的问题欢迎在评论区留言我看到都会尽量回复。