☰
SpringBoot2+Vue3+MyBatis-Plus公交线路查询系统设计与实现全解析
2026/10/13 12:04:00 网站建设 项目流程

做Java方向的人大概率都接触过这类需求:城市公交线路查询,业务看着简单,Server要处理的东西却一点都不少。最近我把一套完整的公交线路查询系统源码重新整理了一遍,技术栈锁定在SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0,并且配齐全套项目文档,正好把整个设计思路和实现过程从头到尾捋一遍。这篇就围绕这个项目,把技术选型、数据库建模、后端接口设计、前端页面落地,以及我实际开发中踩过的坑一次讲清楚,适合正在做Java Web毕设、课设,或者想完整走一遍前后端分离流程的朋友直接参考。

这套系统能做的事情很直观:用户输入线路号,能看到这条线所有经过的站点和首末班时间;输入站点名,能反查哪些线路会经过这里;还可以给定起点和终点,给出直达或者一次换乘的建议。从技术角度看,它是一个标准的前后端分离Web应用,麻雀虽小五脏俱全,几乎覆盖了Java Web开发的核心环节。

1. 技术栈为什么这么选:每一环都有明确理由

很多刚入门的同学选技术栈是看哪个名字火选哪个,但真正动手做项目,每个选择背后的约束条件才是关键。这套系统当初定技术栈时,我优先考虑了三个问题:资料全不全、上手快不快、答辩或讲解的时候能不能说清楚。

1.1 后端框架:为什么不选SpringBoot3

现在SpringBoot3已经发布很久了,很多人会问为什么还选SpringBoot2。实际原因很简单:图稳。

SpringBoot3强制要求JDK17以上,而目前高校机房、多数公司存量项目、大量教程和文档生态,还是以JDK8和JDK11为主。SpringBoot2.7.x配合JDK8或者JDK11,是一切兼容性的“最大公约数”。你对SpringBoot2的使用经验完全可以平滑迁移到3.x,但反过来,如果你的电脑还是JDK8环境,硬上SpringBoot3会先被环境问题劝退。

另外,这个项目的核心需求是“查询型业务”,单机部署,没有复杂的分布式诉求,SpringBoot2的自动配置、内嵌Tomcat、Starter体系已经足够高效。尤其是配合MyBatis-Plus,开发效率并不比SprngBoot3慢。真心建议:如果你不是冲着新版本特性去的,选SpringBoot2.7.x,你会少踩很多环境层面的坑。

1.2 持久层:MyBatis-Plus为什么比JPA更合适

这个系统涉及大量“线路表、站点表、关系表”的多表联查。用JPA虽然实体映射方便,但遇到复杂查询时往往要写JPQL或者拼接Specification,调试起来并不直观。原生MyBatis写SQL很自由,但又要把单表的增删改查全部手动写一遍,体感太重复。

MyBatis-Plus正好卡在中间:BaseMapper内置了大部分单表CRUD,用LambdaQueryWrapper能把查询条件写得像自然语言;同时完全保留XML自定义SQL的能力,多表联查时不受任何限制。我在这个项目里大量使用了selectList、selectPage这类内置方法,以及LambdaQueryWrapper.eq模糊匹配,代码量差不多是原生MyBatis的一半。

还有一个细节:MyBatis-Plus提供分页插件,分页查询直接Page对象传入Mapper方法就能搞定,不用手写LIMIT和COUNT语句。这个功能每个查询系统都用得上。

1.3 数据库:MySQL8.0带来的不只是版本号变化

做这套系统,我一直推荐MySQL8.0而不是5.7,理由有三。

第一,窗口函数。比如查询一条线路的所有站点时,我们需要按seq_no排序并且可能对站点进行分组统计,窗口函数非常方便。5.7没有窗口函数,只能用临时变量模拟,代码又丑又难维护。

第二,CTE(公共表表达式,即WITH语句)。换乘查询的核心逻辑,比如“经过站点A的线路集合”这种中间结果,用CTE来表达会比子查询嵌套可读性好很多。虽然MyBatis-Plus大多数时候用Wrapper构建SQL,但是为了复杂换乘逻辑,我直接在XML里写了WITH语句,8.0支持得很干脆。

第三,MySQL8.0默认字符集是utf8mb4,不用担心emoji或生僻字存入报错,这也简化了建库配置。

开发环境我用Docker起MySQL8.0容器,直接映射3306端口,省去本机安装的麻烦。你要是不会Docker,也可以直接用本机安装包,但要注意时区参数和认证插件问题,这个我放到第5节专门讲。

1.4 前端:Vue3组合式API的实战优势

Vue3相比Vue2,最大的变化是组合式API(Composition API)。用<script setup>语法写组件,代码组织方式从“按选项分类”变成“按业务逻辑分类”。以这个公交查询系统为例,线路列表、站点查询、换乘方案三个功能,对应的请求逻辑和状态管理可以分别封装成独立的组合式函数,互不干扰。

配合Vite构建工具,开发环境下热更新快到几乎无感,Element Plus提供现成的表格、表单、卡片、自动补全组件,做管理界面和查询界面都很省力。整套前端方案最终落地的体验,比Vue2 + Webpack时代要舒服太多。

2. 数据库设计:公交线路模型的关键在“中间表”

绝大部分查询系统的业务复杂度,都藏在数据模型里。公交线路看起来就是“线路到站点”的对应关系,实际操作就会发现,这里是一个典型的多对多关系,并且还带方向、顺序两个隐藏维度。

2.1 线路表与方向字段:新手最容易忽略的东西

先看看线路表的设计,我用的是这张表:

CREATE TABLE `bus_line` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', `line_no` VARCHAR(20) NOT NULL COMMENT '线路编号,如601', `line_name` VARCHAR(100) COMMENT '线路名称,如601路', `direction` TINYINT NOT NULL DEFAULT 0 COMMENT '方向:0-上行,1-下行', `start_station` VARCHAR(50) COMMENT '起点站', `end_station` VARCHAR(50) COMMENT '终点站', `first_time` VARCHAR(10) COMMENT '首班时间,如06:00', `last_time` VARCHAR(10) COMMENT '末班时间,如21:30', `fare` DECIMAL(4,2) DEFAULT 2.00 COMMENT '票价', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY `uk_line_direction` (`line_no`, `direction`) ) COMMENT='公交线路表';

为什么要有direction字段?我见过太多同学在做公交系统时只建一张线路表,把所有站点存成一个字符串,或者只记录一个方向的站点。真实的公交线路是分上下行的,也就是“A到B”和“B到A”,这往往不是简单的反向,因为中途部分站点可能是单方向的,两条方向的站点数量和顺序都可能不一样。

查询的时候如果忽略direction,就会出现“这趟车明明经过了人民医院,却查不到”的怪异bug。把线路方向拆出来,每条线路按方向存两条记录,查询时再配合前端页面标签展示,逻辑就清楚多了。

2.2 站点表与线路站点关系表:多对多关系的标准处理

站点本身独立成一张表,这个大家都理解,关键是中间关系表。

CREATE TABLE `bus_station` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', `name` VARCHAR(50) NOT NULL COMMENT '站点名称', `lng` DECIMAL(10,6) COMMENT '经度', `lat` DECIMAL(10,6) COMMENT '纬度', `region` VARCHAR(50) COMMENT '所属区域', UNIQUE KEY `uk_station_name` (`name`) ) COMMENT='公交站点表'; CREATE TABLE `bus_line_station` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', `line_id` BIGINT NOT NULL COMMENT '线路ID', `station_id` BIGINT NOT NULL COMMENT '站点ID', `seq_no` INT NOT NULL COMMENT '站点顺序号,从1开始', KEY `idx_line_id` (`line_id`), KEY `idx_station_id` (`station_id`) ) COMMENT='线路站点关系表';

这里用生活化一点的说法:线路和站点之间的关系,就像“学生选课”。一个学生可以选多门课,一门课也可以被多个学生选,中间那张成绩表就是关系表。line_station表里的seq_no字段,就是这条线上这辆车先到哪站后到哪站的顺序标记,查询线路详情时按它排序,站点顺序就出来了。

这张关系表是整个换乘查询的数据底座,没有它,换乘推荐就是空中楼阁。

设计基础数据时,建议多录入几条真实感的线路,比如设一条601路从“城东站”走“人民广场”到“火车站”,再设一条602路从“动物园”走“人民广场”到“软件园”,这样“人民广场”就是天然的换乘站,演示效果会很有说服力。

2.3 业务查询如何落在表结构上

线路查询的本质SQL很直接。查某条线路的所有站点,就是三表联查:

SELECT s.name, s.lng, s.lat, ls.seq_no FROM bus_line_station ls JOIN bus_station s ON ls.station_id = s.id WHERE ls.line_id = #{lineId} ORDER BY ls.seq_no

按站点查线路,反过来再联一次就可以了。这些查询用MyBatis-Plus的注解或XML写都行,中间表的索引一定要建,不然数据量上来之后查询会变慢。公交数据量虽然不大,但从一开始保留好索引习惯,毕业后进企业写数据库设计文档时不会露怯。

3. 后端核心实现:把MyBatis-Plus用到顺手

后端开发追求的不只是功能跑通,更要讲结构清晰、接口规范、异常可控。这一节我把项目里的分层、配置、核心接口逐一说明。

3.1 项目分层与常用配置

这是一个标准的四层结构:

controller - 接收参数,返回结果 service - 业务逻辑 mapper - 数据访问 entity - 实体类映射

统一返回结果我用了Result<T>类,包含code、msg、data三个字段。这样前后端约定好格式后,前端Axios拦截器一次性处理,不用每个接口单独判断成功失败。

MyBatis-Plus的分页插件配置是刚需,不配的话selectPage不会生效:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

这个配置几乎可以无脑加到任何SpringBoot + MyBatis-Plus项目里,属于写了不会错、不写会出事的经典配置。

3.2 三个核心查询接口的设计

第一个接口:按线路查询。后端接收一个keyword参数,比如“601”或者“6”,用LambdaQueryWrapper匹配line_no或line_name,返回匹配的线路列表。

public List<BusLineVO> queryLine(String keyword) { LambdaQueryWrapper<BusLine> wrapper = new LambdaQueryWrapper<>(); wrapper.like(BusLine::getLineNo, keyword) .or().like(BusLine::getLineName, keyword); return busLineMapper.selectList(wrapper).stream().map(this::toVO).toList(); }

第二个接口:按站点查询。入参是站点名称,从line_station表和station表联查,返回经过该站的所有线路。接口代码本身不难,关键是记得用DISTINCT或者group by line_id,因为一条线路的多个方向都经过同一个站点时会查出重复记录。

第三个接口是重中之重:换乘查询。入参是起点站名和终点站名。我的实现分为两步。

直达线路计算:查所有包含起点站的线路ID集合A,查所有包含终点站的线路ID集合B,取交集。

一次换乘计算:遍历集合A中的每条线路,取出它经过的所有站点;再遍历集合B中的每条线路,看是否存在某个站点同时出现在两条线路上。如果有,这个站点就是换乘站,返回“起点站—换乘站—终点站”的结构化方案。

这一步的业务逻辑如果用SQL写会比较绕,我更推荐在Service层把数据查出来之后用Java集合计算。数据量小,性能完全够,而且逻辑写在代码里,答辩时一句话就能讲清楚。

3.3 参数校验与统一异常处理

接口写多了,参数校验不能省。用Spring的@Validated配合@NotBlank注解,比如查询参数只要是空的,直接返回提示而不会打到数据库层。再配一个@RestControllerAdvice全局异常处理器,把业务异常和参数异常统一转换成语义明确的JSON返回,前端处理报错就非常省事。这套组合在任何Java Web项目里都是通用套路,值得养成习惯。

项目文档方面,除了代码,我还整理了一份数据库设计说明和接口文档。接口文档用表格列清楚路径、参数、返回值示例,对答辩答辩和团队协作帮助很大。建议README里也放好启动步骤、数据库初始化脚本和演示账号,拿到源码的人能快速跑起来,体验感完全不一样。

4. 前端Vue3落地:从零搭出一个可用的查询页面

后端接口就绪后,前端配合起来才完整。下面按实际开发顺序,把Vue3项目从创建到页面实现走一遍。

4.1 用Vite初始化项目并引入依赖

创建项目用Vite官方脚手架命令:

npm create vite@latest bus-query -- --template vue

装完基础依赖后,再安装项目必需的三件套:

npm install element-plus axios vue-router

main.js里做三件事:挂载Element Plus、注册路由、把全局样式引进来。Element Plus按需引入虽然能减小体积,但初期开发为了省事,直接全量引入完全够用,这个体量的前端项目不需要纠结性能到那个程度。

联调之前,先封装一个Axios实例,基础路径指向开发服务器代理地址。这里有一个关键点:前端页面一旦有多个组件都要发请求,直接在组件里写axios.get会让代码迅速失控,封装一个带拦截器的统一请求模块,越早做越省心。

4.2 搜索联想与线路列表页

首页的核心组件是搜索框。Element Plus的el-autocomplete组件写起来很舒服:用户输入“6”的时候,触发fetch-suggestions回调,请求后端的/api/line/suggest接口,返回匹配到的线路号列表。为了不让每次击键都发请求,我在回调里加了个简单的防抖函数,一个setTimeout搞定,效果立刻顺滑很多。

线路列表用卡片或表格展示,取决于个人审美。卡片展示的好处是每个线路能单独展示方向、首末班时间、票价,点击进入详情也更自然。列表数据从后端分页返回,配合el-pagination组件做分页控制,接口轮询那一套先不用上,查询系统做基础分页就够了。

前端调用链路的组织,我用src/api目录统一管理所有请求函数,每个页面组件只关心数据展示,逻辑复用性明显变好。

4.3 线路详情页与动态路由

点击线路卡片后跳转到详情页,路径是/line/:id,用动态路由实现。详情页需要展示两个核心信息:线路基础信息和按顺序排列的站点列表。站点列表可以用一个横向滚动的时间线样式,每个站点一个节点,看起来很像地图上的线路走向。

注意一个细节:同一个页面组件在参数变化时不会自动重新请求数据,要监听route.params.id的变化。这个在Vue2时代经常被大家忽略,Vue3里用watch(() => route.params.id, fetchDetail)可以轻松解决,但前提是你要记得写。

如果想让系统更有“卖相”,可以接入高德地图JS API,把站点经纬度渲染成地图上的Marker,再把线路串联成一条Polyline轨迹。这一步完全是加分类功能,核心查询业务不支持地图也能正常跑,但有了地图展示之后,演示效果会有很大提升。

4.4 站点详情与换乘结果展示

站点详情页展示的是“经过这个站点的所有线路清单”,这正好和线路详情互为反查关系,后端接口也对应实现。页面用一个简单表格就够了,字段包括线路号、方向、起点终点站。

换乘结果展示相对复杂一点。直达时显示一条线路;一次换乘时,展示两段行程以及换乘站名称。我用的方案是把结果渲染成一个分步的“步骤条”,第一步显示从起点站乘坐哪路车,第二步显示在换乘站下车,第三步显示换乘哪路车到达终点。用户在手机上扫一眼就能看懂,比单纯返回一段JSON文本灵多了。

5. 常见问题排查实录:几乎每个新手都会踩的坑

做这套项目时遇到的问题,我逐个记录了一下,很多都是搜索高频问题,这里直接给结论,方便后来人避坑。

5.1 MySQL8.0连接报错与时区问题

第一个高频报错是启动后连数据库直接抛Server returns invalid timezone。因为MySQL8.0的驱动对时区是敏感校验的,连接串上必须带上serverTimezone=Asia/Shanghai。另一个报错是Public Key Retrieval is not allowed,这跟8.0默认的认证插件有关,连接参数里加上allowPublicKeyRetrieval=true即可。

实践下来最省心的JDBC URL配置是:

jdbc:mysql://localhost:3306/bus_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true

同时驱动包一定要用com.mysql.cj.jdbc.Driver,老版本的com.mysql.jdbc.Driver已经不建议用了。

5.2 MyBatis-Plus字段映射与驼峰命名

数据库字段用line_no这种下划线命名,Java实体用lineNo驼峰命名,MyBatis-Plus默认是开了驼峰映射的,正常情况下能自动对应。但如果你在XML里手写SQL,返回结果的映射有时候会翻车,稳妥做法是把映射开关显式配置在application.yml里:

mybatis-plus: configuration: map-underscore-to-camel-case: true

这个配置写出来,基本就告别字段对不上的玄学问题。

5.3 前端跨域:Vite代理才是正解

前后端分离项目,前端页面域名跟后端接口域名不一致时必出跨域问题。开发阶段最推荐的方式不是在后端写@CrossOrigin注解,而是直接在Vite配置文件里做代理:

server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }

这样前端所有/api开头的请求都会自动转发到后端服务,浏览器层面完全感知不到跨域。生产部署时则用Nginx再配一次反向代理,方式和Vite代理类似,一套思路走到底。

5.4 Vue3组合式API的响应式陷阱

用reactive创建响应式对象后,如果直接解构,比如const { list } = reactive({ list: [] }),解构出来的list就失去了响应式,页面不会自动更新。这是Vue3初学者最容易踩的坑。解决方案有两种:用toRefs包一层再解构,或者干脆用ref定义独立变量。我在这个项目里统一用了ref,代码更直白,心智负担小。

5.5 部署与演示环节的常见问题

部署时最常遇到两类问题。一类是MySQL8.0没有设置字符集或排序规则,导致中文乱码,建库时指定utf8mb4即可。另一类是后端jar包启动后前端页面白屏,通常是因为前端构建后的dist目录没有正确配到Nginx的站点根目录,或者接口代理没配上。演示之前,一定要把前后端完整跑一遍全流程,尤其换乘逻辑多测几组数据,不要只测理想数据。

项目里我维护了一份部署检查清单,从数据库导入、后端启动、前端构建、Nginx配置到防火墙端口放行,一步步打勾。这类操作细节虽然不写进技术教程里,但实际项目交付时比任何花哨功能都实用。

做完这套系统,我个人最大的体会是:业务模型一旦拆清楚了,写代码只是时间问题。公交线路查询的难点从来不是SpringBoot的注解或者Vue3的语法,而是你有没有意识到“线路有方向”“站点有顺序”“换乘要找交集”这些业务细节。把业务翻译成表结构和接口设计,项目就已经完成了一大半。要是再想往上走,可以考虑把数据换成真实城市公交线路,加入实时到站预测、周边站点推荐,甚至做一个面向特殊人群的语音查询版本。先把这套基础版本跑通,后面每一步扩展都会有清晰的方向。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询