资讯动态

SpringBoot+WebSocket:智能公交实时位置与换乘规划系统设计解析

发布时间:2026/9/28 8:44:00 来源:尧图企业网站定制
搞了这么多年Java也带过不少做毕设的学生看到“基于SpringBoot的武夷智能公交系统”这类题目我第一反应是这题选得聪明。它既有SpringBoot这个当前最主流的后端框架撑门面又有“线路规划”这种能正经讲算法的功能点还有“实时车次管理”这种能展示推送和并发处理的实战场景。最难得的是整套系统的业务边界非常清晰本地化场景也好讲故事本科生用半年时间从零啃下来不算离谱答辩时又有足够深度的技术点可以聊。这里有个很多人都没想透的问题同样是公交系统为什么有的毕设做完就是个“数据库增删改查展示”而有的能让人眼前一亮差别就在“实时”两个字。用户打开App想知道的从来不是“哪条线路经过哪站”而是“我等的这班车现在在哪儿”“还有几分钟到”“如果想去某个地方怎么换乘最少、最省时间”。把这三个问题做扎实项目就立住了。这篇文章把我从需求拆解、技术选型、表结构设计到换乘算法、WebSocket实时推送、前后端联调的全过程捋了一遍也把踩过的坑和答辩时的话术一起写出来给准备开题或已经开工的同学一份能直接照着做的参考。1. 项目定位与核心需求拆解1.1 传统公交查询的三痛点和系统目标武夷山这个场景我不比各位了解得更多但国内公交出行的问题几乎都一样站牌只写“首末班时间”不告诉你车堵在哪条路、还有几站想从A点到B点不知道哪几趟车能到更不知道换乘哪一站最划算公交公司自己排班全凭经验调整一条线路涉及站点、车辆、时刻表一堆纸质表格改起来特别痛苦。这套系统要做的就是拆掉这三堵墙乘客打开页面能看到线路上每一辆车的实时位置以及到达当前站的预计时间输入起点和终点系统给出多套换乘方案标注全程时间和换乘次数运营人员在管理后台维护线路、站点和车次排班车辆位置和在线状态一目了然。再说得具体一点。传统查询场景里乘客站在站台最焦虑的是“车到底来不来、什么时候来”。这个焦虑的本质是信息不对称而系统把车辆GPS位置实时暴露给乘客就解决了这个问题。再比如说换乘规划武夷山这种旅游城市景点分散游客对公交线路完全不熟一个能给出“景区南门→度假区→高铁北站”多套方案的查询入口价值远高于一张静态线路图。所以我在做需求分析时把用户故事写成了三条主线普通乘客查线路、游客规划出行、运营人员做线路和车次管理。三条主线对应三个角色功能边界从一开始就是清楚的后面开发和写论文都不容易跑偏。1.2 功能模块怎么切分才算合理模块划分直接决定开发工作量。很多同学喜欢把功能拆得很碎今天加一个“意见反馈”明天加一个“天气查询”最后自己把自己累死。我的建议是保住核心链路其余都是加分项。核心链路就是“查询—规划—调度”这三件事。端子模块关键功能核心难点乘客端线路查询按线路号、站点名搜索线路与途经站站点与线路的多对多关系处理乘客端换乘规划输入起终点返回多套换乘方案图算法与换乘次数剪枝乘客端实时位置地图展示车辆位置、预计到站时间WebSocket推送与ETA计算乘客端公告信息线路调整、特殊天气运营通知无难点CRUD即可管理端线路管理维护线路、站点、站点顺序顺序字段设计与地图选点管理端车次管理排班、发车间隔、绑定车辆时刻表自动生成逻辑管理端车辆监控地图看车辆位置、在线/离线状态位置数据刷新与离线判定这个表做完心里就有底了乘客端三个核心功能都是“硬骨头”管理端基本是常规CRUD加一点业务规则。如果时间紧张可以先把车辆监控简化成“列表显示最后上报位置”把地图展示放到第二迭代再做但线路查询和换乘规划无论如何都要保住这两个才是题目的灵魂。2. 架构选型为什么SpringBoot这套组合最合适2.1 初中生都会问的问题为什么不用SSH或SSM我面试的时候常问这个问题做毕设时也建议学生认真想明白。早些年做SSHStrutsSpringHibernate或SSMSpringSpringMVCMyBatis光XML配置文件就能写几屏一个数据源配错就要折腾半天。SpringBoot最大的价值在于“约定大于配置”内嵌Tomcat打成一个jar直接跑starter机制把常用依赖打包好引入即用自动配置根据classpath里的类库自动装配Bean。对你来说这意味着省下大量配置时间把精力集中在业务代码上。当然选SpringBoot还有一个很现实的答辩理由它好讲。面试官或答辩老师问到“自动配置原理”你可以从SpringBootApplication说起讲EnableAutoConfiguration如何通过SpringFactoriesLoader加载AutoConfiguration类再讲条件注解ConditionalOnClass如何按需装配。这套逻辑理清楚了甚至比项目本身还能加分。版本上我建议用SpringBoot 2.7.x配JDK 8网上教程最多踩坑资料最好找如果你非要上3.x配JDK 17那就要做好很多老代码和第三方包不兼容的心理准备不是不行是不值得在毕设里冒险。2.2 技术栈清单与四层架构落地整套系统我用的技术栈很克制没有为了炫技堆东西每个组件都有明确用途组件版本建议用途SpringBoot2.7.x后端核心框架MyBatis-Plus3.5.x数据库ORM单表CRUD免手写SQLMySQL8.0业务数据存储Redis5.x车辆最新位置缓存、热点数据缓存WebSocketSpring自带车辆实时位置推送Spring TaskSpring自带定时任务模拟车辆位置上报、离线检测Vue Element UIVue 2/3管理端前端我用的是Vue 2稳定性优先高德地图JS API最新版地图展示与选点分层还是经典的Controller-Service-Mapper三层没引入复杂设计模式但我在分包上做了文章。包结构是这样com.wuyi.smartbus ├── controller ├── service ├── mapper ├── entity数据库实体 ├── dto请求参数封装 ├── vo响应数据封装 ├── config配置类 ├── common统一返回体、异常处理、常量 └── websocket这样的好处第一是职责清晰答辩画架构图时一页就能讲明白第二是避免entity直接被Controller暴露给前端防止数据库字段泄露。很多初学者图省事直接把Entity返回给前端结果数据库里的deleteFlag、创建时间全被看到了既不好看也不安全。2.3 数据库建模6张核心表的设计思路数据库设计是地基很多同学在这里翻车。我一共设计了6张核心表外加公告等辅助表线路表 t_lineline_no线路编号、name线路名、start_station_name、end_station_name冗余字段、first_time、last_time、status运营状态。冗余首末站名称是为了查询列表不用join站点表换来一点脏数据风险在可接受范围内。站点表 t_stationname、lng、lat、region区域。经纬度必须单独存后面换乘算法和地图展示都要用。注意经纬度字段类型用decimal(10,6)不要用double否则精度会出问题。线路站点关联表 t_line_stationline_id、station_id、station_order、distance_to_next。station_order是灵魂字段站点在一条线路上的先后顺序全看它。distance_to_next表示到下一站的距离用于ETA计算。这里我建了联合唯一索引(line_id, station_order)防止重复数据。车辆表 t_busplate_no车牌号、line_id所属线路、driver、status在线/离线/停运。车辆位置表 t_bus_locationbus_id、lng、lat、speed、direction、run_status行驶/进站/离站、report_time。这个表是实时功能的数据源但要特别注意写入频率后面讲踩坑时细说。车次排班表 t_scheduleline_id、bus_id、start_time发车时间、date_type工作日/周末。排班生成的时刻表就存在这里。设计这6张表最容易犯的错是“一张大表搞定一切”把所有信息塞进线路表里用逗号分隔站点ID。这么做当时觉得省事后面写换乘算法时简直想哭因为把逗号串拆开再逐个查站点SQL写得又臭又慢。按第三范式拆开代价是多写几个join但逻辑清晰太多索引也能正常生效。3. 核心功能实现换乘算法与实时车次3.1 换乘规划用Dijkstra算出“少换乘又省时”的方案换乘规划的本质是图的最短路径问题。把每个站点看作图的节点任意两个相邻站点之间有一条边边的权重可以是距离或行车时间。用户输入起点站和终点站后系统在这个图上跑最短路径算法得出乘车方案。最小生成树用不上这里就是单源最短路径。如果只要求“换乘次数最少”用BFS就够了实现简单十几行代码。但作为毕设我更推荐Dijkstra理由有两个一是它能算“总乘车时间最短”比单纯的换乘次数少更有说服力二是算法有足够的代码量和技术含量答辩时可以重点讲“边的权重怎么设计”。边的权重我分了三种情况相邻站之间的行车时间这是基础权重换乘站额外加上8分钟的换乘缓冲时间因为下车、等车、上车需要时间同一条线路连续乘坐没有额外惩罚。这样算出来的路径天然会避开频繁换乘的绕路方案。核心代码长这样public ListTransferPlan plan(String fromStation, String toStation) { // 1. 查出所有线路站点关系构建邻接表 adjMapstationId, ListEdge // Edge: {toStationId, lineId, durationMin} // 2. Dijkstra优先队列dist数组记录最少时间 // dist[i] 从起点到站点i的最少分钟数 // preLine[i] 到达i站时乘坐的线路用于识别是否需要换乘 PriorityQueueNode queue new PriorityQueue(Comparator.comparingInt(n - n.time)); queue.offer(new Node(fromStation, 0, -1L)); while (!queue.isEmpty()) { Node cur queue.poll(); if (visited.contains(cur.stationId)) continue; visited.add(cur.stationId); for (Edge edge : adjMap.get(cur.stationId)) { int extra (edge.lineId cur.lineId) ? 0 : 8; // 换乘缓冲 int newTime cur.time edge.durationMin extra; if (newTime dist[edge.toStationId]) { dist[edge.toStationId] newTime; pre[edge.toStationId] cur.stationId; preLine[edge.toStationId] edge.lineId; queue.offer(new Node(edge.toStationId, newTime, edge.lineId)); } } } // 3. 回溯pre数组把连续相同lineId的站点合并成一个乘车段 // 得到 ListSegment每段包含“起点站、终点站、乘坐线路号” }实际开发时有个容易出错的点同一个物理位置的站台可能有多条线路停靠但它们在数据库里不是同一条记录。比如“市立医院站”有1路和5路都经过换乘时乘客其实不用走路但如果这是两个站点记录算法会误判为需要换乘并加8分钟惩罚。解决办法是给站点加一个group_id字段物理位置相同的站台共享同一个group_id算法在建图时先按group_id合并节点。这个细节很值钱答辩时能讲出来老师会觉得你真的做过调优。3.2 实时位置模拟与WebSocket推送链路毕业设计拿不到公交公司真实的GPS数据这是客观条件限制但完全可以用模拟器解决。我的方案是写一个定时任务让每辆车沿着所属线路的站点顺序匀速移动每5秒计算一次车辆当前经纬度写入位置表同时推送给前端。模拟器本身的代码不复杂Scheduled(fixedRate 5000) public void simulateBusMovement() { // 1. 查询所有在线车辆 ListBus buses busService.listOnlineBuses(); for (Bus bus : buses) { // 2. 获取该车当前所在线路的站点序列 ListLineStation stations lineStationMapper.listByLineId(bus.getLineId()); // 3. 根据车辆当前进度计算下一个目标站点 Station next stations.get(bus.getCurrentStationIndex() 1); // 4. 按固定速度向next移动一点点更新经纬度 double newLng bus.getLng() (next.getLng() - bus.getLng()) * 0.1; double newLat bus.getLat() (next.getLat() - bus.getLat()) * 0.1; // 5. 写入缓存 推送WebSocket busLocationService.updateAndPush(bus.getId(), newLng, newLat); } }WebSocket推送的链路是车辆位置更新后服务端向同一线路的订阅者群发消息。前端打开线路详情页时先通过HTTP拿到车辆静态信息再建立WebSocket连接之后只靠推送更新位置不需要轮询。这样流量开销小实时性也高5秒刷新一次体感很流畅。推送时要做好两件事。第一Session要按线路分组管理用一个ConcurrentHashMapString, CopyOnWriteArraySet 保存key是lineId第二车辆位置要先写Redis最新位置缓存再通过WebSocket推送不要直接写MySQL因为5秒一次的频率对MySQL压力太大。前端收到消息后更新地图上的车辆marker同时调用ETA接口或直接由后端算好剩余分钟数一起推送减少一次请求。预计到站时间ETA我采用了分段估算法拿车辆当前坐标去匹配线路上最近的站点然后用“当前站到目标站的剩余距离除以平均车速”再乘上1.2的交通系数作为缓冲。公式不复杂但效果比最简单的直线距离除以速度靠谱很多因为走的是站点间的实际道路距离。3.3 管理端排班自动生成全天车次时刻表排班模块是管理端的核心。运营人员选择一条线路、一个日期类型、首班时间、末班时间还要设置高峰和平峰两个时段的不同发车间隔系统自动生成整天的车次时刻表。生成逻辑本质是一个循环从首班车开始依次累加当前时段对应的间隔分钟直到超过末班时间。这里我踩过一个坑如果只用一个固定间隔高峰期的拥挤问题没有针对性所以我把一天切成三个时段——早高峰7:00-9:00间隔6分钟、平峰9:00-17:00间隔12分钟、晚高峰17:00-19:30间隔8分钟。生成时判断当前时间落在哪个区间再取对应间隔。代码大致是public ListLocalTime generateSchedule(String lineId, String dateType) { LocalTime current firstTime; LocalTime end lastTime; ListLocalTime schedule new ArrayList(); while (!current.isAfter(end)) { schedule.add(current); int interval getIntervalByTimeSlot(current); // 查时段表 current current.plusMinutes(interval); } return schedule; }生成的每个时刻点就是一条车次记录。车次要不要绑定具体车辆我建议绑定因为后面车辆监控要展示“某辆车当前跑到哪了”不绑定没法定位。如果车辆数量少于车次数就在生成时给出提示“车辆不足”让运营人员决定是否减少高峰班次。4. 从零搭建到联调关键实操与编码规范4.1 初始化Maven工程与包结构创建项目我用的是Spring Initializr选Java 8、Spring Web、MyBatis-Plus、MySQL Driver、Redis、WebSocket依赖即可。有一点要注意Spring Initializr现在已经默认生成SpringBoot 3.x如果你选了它JDK必须17以上很多培训机构和网课用的还是JDK 8语法老代码直接跑不了。我的建议是手动在pom.xml里把版本改回2.7.18对应的Java版本设成1.8稳字当头。目录结构按前面说的包来建。controller层只做参数接收和结果返回service层写业务逻辑mapper层只写SQL。新手最容易犯的错是在controller里写一大串业务代码看起来也能跑但后面没法测、没法复用答辩老师问两句就露馅。还有一个实用小建议dto和vo不要混接收前端参数的用dto返回给前端的用vo即使字段长得一样也分开这是长见识的好习惯。4.2 MyBatis-Plus配置与分页实战MyBatis-Plus极大减少了单表CRUD样板代码。继承BaseMapper后insert、updateById、selectById、selectPage这些方法直接用不用自己写SQL。但同时要注意多表联查和带复杂条件的统计SQLMP并不擅长这时候就老老实实写XML。比如“查询经过某站点的所有线路”需要join线路表、站点表、关联表我直接手写SQL放在mapper XML里比MP的QueryWrapper更好维护。分页是列表页的刚需MP分页插件配置起来非常快Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }配置完就能用最关键的一步是new Page(pageNum, pageSize)传给selectPage方法。如果查出来total值不对多半是因为分页拦截器没注册成功检查下MybatisPlusConfig有没有被Spring扫描到。另外查询线路列表时我建议默认按首班时间排序并且只查status为1的运营中线路不然把停运线路也展示出来用户体验会很怪。4.3 统一返回体与前后端联调约定前后端分离的项目接口规范不统一联调就是灾难。我定义了一个Result类所有接口统一返回这个结构Data public class ResultT { private Integer code; // 200成功 500业务失败 401未授权 private String msg; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.msg success; r.data data; return r; } public static T ResultT error(String msg) { ResultT r new Result(); r.code 500; r.msg msg; return r; } }前端axios响应拦截器统一判断code不等于200就弹错误提示。这样一来controller里只需关心业务结果不用每个方法写重复的返回逻辑。另一个约定是接口字段名后端Java用驼峰命名前端JavaScript也要求用驼峰禁止一个接口返回createTime另一个返回created_at这种不一致会逼疯人。联调时我发现一个很常见的问题后端返回null的字段前端到处报undefined。为解决这个我在全局配置里加了Jackson的空值处理策略统一把null字符串输出成空字符串集合输出成空数组避免前端判空逻辑重复写。5. 踩坑实录5个高频问题与排查方法5.1 车辆位置写库太频繁数据库卡顿模拟器每5秒更新一次所有在线车辆的位置如果直接写MySQL几十辆车跑起来后数据库CPU直线上升。原因很简单高频update产生大量行锁竞争和redo日志写入。解决办法是分层存储最新位置写Rediskey为bus:loc:busIdvalue存JSON字符串设置过期时间10分钟历史轨迹按30秒一次批量插入MySQL用于离线分析和演示。实际操作时还要注意读接口优先从Redis取取不到再查MySQL。我在测试时发现Redis里出现大量过期键堆积是因为没设置过期时间后来统一加上TTL内存占用立刻降下来了。5.2 换乘算法算出绕路或死循环第一次跑换乘算法时我给的测试数据是“市政府站”到“高铁站”结果方案显示绕了一个大圈。排查下来发现是两个问题建图时没有处理站点分组导致同站换乘被当成普通换乘visited判断写在了循环里但只在出队时标记导致环状线路上重复入队。修复后我用一个手工构造的小拓扑图做了单元测试4条线路、10个站点验证了所有两两组合才算放心。建议你也在开发阶段就建立这个测试习惯。不要等整个系统做完了再测算法不然混着一堆前端问题定位起来非常痛苦。手动构造小图的好处是答案可以手算算法结果对不对一眼就能看出来。5.3 地图上点位偏移车辆跑到马路外面用高德地图展示车辆位置时我发现模拟的GPS经纬度和真实道路对不上点位明显偏移。原因是坐标系不一致GPS原始坐标是WGS-84而高德地图用的是GCJ-02国测局加密坐标百度地图更是有自己的BD-09。不转换点位肯定偏。解决办法有两个方向一是在模拟器生成位置时就按高德坐标生成但这样数据不通用二是写一个坐标转换工具类统一把WGS-84转成GCJ-02再传给前端。毕设场景我推荐第二种因为更贴近真实项目。转换公式网上一搜一大把但要注意边界情况我加上了一个“是否在国内”的简单判断国外坐标不转换防止地图上出现诡异跳点。5.4 WebSocket连接频繁断开实时功能开发完我用Nginx做了反向代理结果发现WebSocket每隔几十秒就自动断开重连。查了半天原因是Nginx默认的HTTP代理不转发Upgrade头。解决方法是加三行配置location /ws/ { proxy_pass http://backend-server; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; }另外浏览器端的WebSocket如果60秒没有消息某些中间件会自动踢掉连接。所以前端要写一个心跳机制每30秒发一个ping后端收到后原样返回pong。把这两个问题解决后连接稳定性立刻提升连续跑两个小时也没有断线。5.5 LocalDateTime序列化格式不对前端向后端传时间后端返回给前端的时间经常变成一串数字或者格式变成“2024-05-01T10:00:00”这种带T的ISO格式。原因在于SpringBoot默认使用Jackson对LocalDateTime的处理走的是JavaTimeModule默认格式不符合业务习惯。统一解决方式是在application.yml里加配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8同时要给LocalDateTime字段加JsonFormat(pattern yyyy-MM-dd HH:mm:ss)双保险。不配置的话前端展示车次时刻表时会非常难看还要自己做字符串截取纯属白费劲。症状根因解决接口响应慢高频写MySQLRedis缓存最新位置批量落库换乘方案绕路未合并同站台节点引入group_id合并节点地图点位偏移坐标系不一致统一转GCJ-02Websocket掉线Nginx未转发Upgrade头补代理配置心跳时间格式带TJackson默认配置全局配置时间格式6. 答辩包装与演示防翻车经验6.1 项目讲演的编排顺序毕设答辩重点不是念PPT而是用最短时间让老师听懂“你做了什么、怎么做的、有什么难点、怎么解决的”。我建议用“四段式”讲项目第一段讲痛点30秒用户不知道车在哪、不知道怎么换乘你的系统解决了这个信息不对称。第二段演示核心功能2分钟先查一条线路看实时位置推送再做一次换乘规划展示两套方案及其时间、换乘次数。第三段讲架构和难点3分钟亮出技术栈和表结构重点讲换乘算法权重设计、WebSocket推送链路、Redis降低数据库压力这三个点。第四段留一个未来展望一句话即可可以接入真实GPS设备、增加语音播报、结合客流做运力调度。第四段不用展开但一定要提因为这是在暗示老师“你还有后续思考”很多高分答辩就是靠这一句话定调。另外“项目的创新点是什么”这种必问题不要答“用了SpringBoot”这种废话要答“把实时位置与换乘算法结合用分组节点解决同站换乘误判”这才是真正的差异化。6.2 演示翻车防护清单现场演示是翻车高发区几乎每年都有学生因为地图API key过期、WebSocket连不上、数据库数据太少而当场尬住。我整理了一份自己用过的防护清单项目打包成jar本地运行不依赖云服务器避免现场网络不通。数据库导入完整演示数据至少10条线路、200个站点、20辆车、一周的排班记录。数据太少的话线路查询页面空空荡荡非常尴尬。给地图配置多个备用key避免一个key超过免费配额导致白屏。WebSocket连不上时准备一张实时位置的静态截图直接切PPT展示。演示前先冷启动一次项目确认Redis、MySQL都正常启动端口没被占用。这些事看起来很琐碎但全都踩过坑之后你会明白答辩当天稳定大于惊艳。一次顺畅的演示给评委的印象比任何华丽的架构图都管用。6.3 代码与论文的一致性经验最后说一个很多人忽略的点论文里的流程图、数据库ER图必须和代码实际逻辑一致。我见过不少同学代码里明明用的是Redis缓存论文里画的技术架构图却没有Redis代码里字段名叫start_time论文表格里写的“发车时间字段startTime”。这种不一致看着是小问题但答辩老师只要仔细看一眼就能抓出来然后怀疑整篇论文的真实性。我在写论文前做了个小整理把数据库每个表的字段清单照抄到论文附录把核心接口的请求和返回示例运行一遍后截图贴进论文把换乘算法的时间复杂度写在设计章节。这样一来论文和代码就是同一个东西。磨刀不误砍柴工花半天时间做这个对齐工作省的是一答辩时被追问到哑火的险。我个人做完这套系统的最大感受是技术栈本身平平无奇真正有价值的是把“实时”和“规划”这两个词落到了具体代码里。很多同学一上来就想用微服务、Elasticsearch其实以这个业务体量来说MySQL加Redis加WebSocket已经绰绰有余把这些基础技术用扎实你的完成度就已经超过90%的同类选题了。这套思路也不只适用于公交凡是涉及“位置加线路加调度”的场景比如景区接驳车、校园摆渡车、企业通勤班车换个表皮就能复用。我后来做校园班车预约模块时直接移植了这套实时位置推送和Dijkstra换乘逻辑省了不少事。如果你正在做类似方向的毕设先把基础CRUD做扎实再往“实时”和“规划”两个点上深挖六个月时间足够你交出一份能打的作品。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑