资讯动态

SpringBoot2+Vue3公交线路查询系统:从数据库设计到部署全解析

发布时间:2026/9/29 16:51:19 来源:尧图企业网站定制
说实话看到“Java Web 公交线路查询系统”这个标题我第一反应是这又是一套典型的技术栈全家桶练手项目。SpringBoot2 Vue3 MyBatis-Plus MySQL8.0几乎是目前前后端分离项目最主流的组合了。但真正拿到源码和文档看过之后我发现公交线路查询这个业务场景其实被大大低估了——它涉及的多条件模糊检索、站点顺序维护、线路与站点的一对多关联、换乘路线组合都是真实企业级项目中天天要写的东西。尤其适合拿来做毕业设计或者简历里的项目经历。这篇文章我打算从技术选型的“为什么这么做”到数据库表的“怎么设计才不返工”再到前后端核心代码的“实现思路”最后完整走一遍从环境搭建到部署上线的全流程。顺手把我在这个项目里踩过的坑、排查过的诡异问题也一并整理出来。无论你是准备用这套源码做课设、毕设还是想自己从头写一个类似的查询系统这篇应该都能帮你少走不少弯路。1. 项目整体设计与技术选型拆解1.1 公交查询系统的核心需求到底有哪些很多同学拿到这种项目第一反应是“不就是查个公交车嘛”但真让你把需求列全你会发现事情没那么简单。一个完整的公交线路查询系统至少要有这么几块一是基础数据管理。公交线路不是只有个名字就行每条线路要关联起点站、终点站、首末班时间、票价、全程里程还要挂一串有序经过的站点列表。站点本身也不是简单字符串得有站点名称、区域位置、经纬度坐标甚至到站说明。二是多维度的查询场景。用户最常见的操作是我知道线路名想查这条线经过哪些站——这是“线路查询”。我在某个路口想知道这里有哪些公交——这是“站点反查线路”。更进一步的我想从A地去B地中间得换乘两趟车——这是“换乘方案查询”。三是数据展示的友好性。查出来的结果不能是一张平铺的二维表线路详情页要有站点顺序、运营时间、票价信息最好还能标出换乘站。这对后端返回的数据结构设计是有要求的前端也能体现出Vue3在渲染复杂列表和动态交互上的优势。这套源码里基础功能覆盖得挺全主打的就是线路查询、站点查询和线路详情查看。换乘算法不是所有版本都带但数据表结构是支撑得住的后面想加也容易。1.2 技术栈选型背后每一层都有它的理由先看后端SpringBoot2。可能有人会问现在SpringBoot3都出来了为什么还要用2原因很现实毕业设计和绝大多数企业老项目SpringBoot2的生态最成熟资料最多踩坑的答案一搜一大把。而且2.7.x版本对MyBatis-Plus、各类生成器和老版JDK的兼容性都非常稳。在“求稳”这件事上SpringBoot2夜熬得住。持久层选MyBatis-Plus而不是原生MyBatis或者Spring Data JPA核心原因就一个开发效率。JPA虽然Hibernate自动建表很爽但遇到复杂查询、动态条件拼接、关联结果集映射的时候写起来反而别扭。原生MyBatis灵活度高但单表CRUD都得手写XML一个公交线路系统光增删改查的样板代码就能占掉三分之一的开发量。MyBatis-Plus正好卡在中间单表CRUD零SQL复杂查询用LambdaQueryWrapper拼条件自动填充、逻辑删除、分页插件一应俱全。它在代码生成器里跟SpringBoot2是黄金搭档配好后从建表到能跑一个查询接口也就一小时的事。前端Vue3准确的说是Vue3 Vite Composition API的写法。很多老教程还在用Vue2的Options API但Vue3才是当前整个前端生态的主流方向。Vite启动和热更新的速度比Webpack快得不是一个量级开发体验好太多。而且Vue3的setup语法糖写业务逻辑更聚焦像查询表单的搜索条件、结果列表的加载状态、错误提示用ref和reactive几行就理清楚了。数据库选MySQL8.0不只是版本新。8.0的窗口函数、公共表表达式CTE、UTF8MB4默认字符集、更好用的JSON类型对这类业务系统来说都是实打实的提升。我自己在项目里做“经过某站点的所有线路”这种反查就用了JSON数组和IN查询配合一句话就查出来了。换乘查询的线路拼接用窗口函数也能简化不少功夫。1.3 这套技术组合谁能直接上手抄作业简单总结一下适合哪些人第一计算机相关专业做毕业设计、课程设计的同学这套代码的目录结构、表设计、接口文档齐全非常适合二次开发和写论文支撑材料。第二准备找Java后端或全栈开发工作的人可以在读懂源码的基础上把换乘算法、Redis缓存、权限登录这些功能自己加上这就是一个能写进简历的1-2年经验级别的项目。第三想学习SpringBoot2 Vue3前后端分离思想的初学者这是最典型的参考样本。2. 数据库设计一张线路表远远撑不起这个业务2.1 核心表结构逐表拆解我见过很多人做这类项目图省事一张表把线路id、站点字符串如“人民广场站-南京路站-火车站”、首末班时间、票价全塞进去。看起来也能实现但一旦要查某个站点经过哪些线路逻辑就变成在字符串里LIKE匹配数据一多性能拉胯而且站点顺序完全没法维护。这套源码的表设计是标准的范式化建模起码拆成三张主表加一张关联表。第一张是线路表字段大致包含线路ID主键自增、线路编码比如“K1”、线路名称比如“1路公交”、起点站、终点站、全程票价、起点首班时间、起点末班时间、终点首班时间、终点末班时间、线路总里程、是否运营中、备注信息、创建时间、更新时间。需要注意的是起点站和终点站虽然看起来是“站”但在线路表里存的是字符串名称因为它是线路的属性描述真正的站点实体在站点表里维护。线路表的所有时间字段都建议用TIME类型不要用DATETIME因为末班车没有日期概念用TIME存既直观又不占空间。第二张是站点表这是很多人会忽略重点的地方。站点表字段要包含站点ID、站点名称、所属区域如“浦东新区”、站点描述标志性建筑、经度、纬度、创建时间、更新时间。经纬度字段用DECIMAL(10,6)存不要用FLOAT因为FLOAT的精度不够容易出现坐标偏移。以后要扩展“附近站点查询”直接拿经纬度算距离就行。第三和第四张是线路-站点关联表和线路站点顺序表。这里要说明一下顺序本身是关联关系的一部分可以放在一张关联表里ID、线路ID、站点ID、站点顺序号、距下一站距离米、是否换乘站。顺序号必须单独拿出来因为线路上的站点顺序是业务核心逻辑有了它前端才能画出“先到哪儿后到哪儿”的路线时间线。2.2 为什么要用关联表而不是直接在站点表里加线路ID这是一个设计上的关键决策。你可以选择在station表里加一个line_id字段表示“这个站点属于哪条线路”。对单线路站点来说确实简单。但公交的典型场景是一个站点可能经过十几条线路一个线路又挂在几十个站点上。这是个多对多关系。如果在站点表加line_id你存不下如果存JSON数组形式的多个线路ID查询“经过A站的线路”都得全表扫一遍然后JSON_CONTAINS索引全废。用独立关联表的好处非常明显一对多和多对多的查询都变得优雅。查一条线路经过哪些站点就是SELECT station_id FROM line_station WHERE line_id ? ORDER BY station_order。查一个站点被哪些线路经过就是SELECT line_id FROM line_station WHERE station_id ?。想更新某条线路的站点顺序先删除再批量插入关联记录事务一包简单可靠。这套源码的查询核心性能就是靠这种设计撑住的。2.3 索引和SQL优化上必须做对的两件事第一件所有外键关联字段都要建索引。line_station表的line_id、station_id就是最高频的查询入口不加索引的话数据量到几万条时查询就开始明显变慢。在Navicat里用EXPLAIN看一眼会发现type列直接是ALL全表扫描这在大一点的数据量下就是事故。第二件查询SQL里不要写SELECT *只查需要的字段。这个项目里线路列表页只需要id、线路名称、首末班时间、票价那就只查这几列。MyBatis-Plus虽然有select方法但很多人在service层图省事直接list()一把梭等到了联调阶段接口响应500ms才开始后悔。配合分页插件PageHelper或MyBatis-Plus自带的分页拦截器把大数据量的列表页优化到30ms内不是问题。这张表设计的分量在做“站点反查线路”和后续扩展“换乘方案”时才会真正体现。数据模型对一次后面至少省一半的返工成本。3. 后端接口设计查询逻辑的层层拆解3.1 项目初始化和基础依赖配置拿到这份源码你先别急着跑把目录结构看懂比什么都重要。标准的SpringBoot多模块或单模块结构一般包含controller、service、mapper、entity、dto、config这几个核心包。controller层只负责接收参数和返回结果service层写业务逻辑mapper层就放MyBatis-Plus的BaseMapper接口所有能看懂的分层都在这里。pom.xml这边的关键依赖有spring-boot-starter-web、mybatis-plus-boot-starter注意版本要对上SpringBoot2的版本、mysql-connector-java8.x版本注意groupId是com.mysql不是老旧的mysql:mysql-connector-java、lombok省略实体类getter/setter、hutool或commons-lang3工具类、druid-spring-boot-starter连接池。配置application.yml时有个小细节MySQL8.0的驱动类名是com.mysql.cj.jdbc.Driverurl里要带serverTimezoneAsia/Shanghai和useUnicodetruecharacterEncodingutf8。少一个时区参数连接数据库就报“The server time zone value ... is unrecognized”。3.2 线路多条件查询接口核心参数与LambdaQueryWrapper公交线路查询系统最核心的接口就是线路列表查询参数大概有三类按线路名称模糊搜索、按途经站点搜索、按运营状态过滤。我不会让你写一堆if else去拼SQLMyBatis-Plus的LambdaQueryWrapper就是为这个场景准备的。举个例子按线路名称模糊查询同时要求只返回运营中的线路LambdaQueryWrapperBusLine wrapper Wrappers.lambdaQuery(); wrapper.like(StringUtils.hasText(lineName), BusLine::getLineName, lineName) .eq(BusLine::getStatus, 1) .orderByDesc(BusLine::getCreateTime); PageBusLine page busLineMapper.selectPage(new Page(pageNum, pageSize), wrapper);用这个API的精髓在于第一个参数是boolean类型只有条件成立时才拼这个片段。这样不管前端传不传lineNameSQL都能稳定生成。再配合Page分页对象返回结构里直接就有总数、页码、列表数据前端分页组件无缝对接。3.3 站点反查线路服务层千万别硬写嵌套循环如果说查询线路是基础题那“通过站点查所有经过的线路”就是加分题。正确的SQL逻辑是这样先在line_station关联表中查出该站点对应的所有line_id再用这些line_id去bus_line表查出线路完整信息。写成代码最土的是先查关联表得到一个List然后foreach用stream拆成id集合一次IN查询搞定。ListLineStation lsList lineStationMapper.selectList( Wrappers.lambdaQuery().eq(LineStation::getStationId, stationId)); if (lsList.isEmpty()) return new ArrayList(); ListLong lineIds lsList.stream().map(LineStation::getLineId).collect(Collectors.toList()); return busLineMapper.selectBatchIds(lineIds);但记住一个原则不要在for循环里发单条SQL哪怕列表只有10条也可能触发数据库10次往返。能合并成一次IN查询就一定要合并。这就是我常说的“N1查询问题”初学者最容易栽在这里。3.4 自定义VO把站点顺序和线路信息拼成一个完整对象很多刚写后端的人会直接返回实体类给前端。但实体类里有一堆敏感字段、关联字段、非展示字段直接暴露既不安全也不灵活。推荐的方案是定义VO对象比如BusLineDetailVO里面包含线路基本信息字段、List 有序站点列表、首末班时间格式化后的字符串、全程站点数。Service层负责把mapper查到的原始数据转成VO常见写法是BusLine line busLineMapper.selectById(lineId); ListLineStation lsList lineStationMapper.selectList( Wrappers.lambdaQuery().eq(LineStation::getLineId, lineId) .orderByAsc(LineStation::getStationOrder)); ListStation stations stationMapper.selectBatchIds( lsList.stream().map(LineStation::getStationId).collect(Collectors.toList()));然后把list里的station顺序按lsList里的station_order值排一遍。这里有个坑selectBatchIds返回的列表顺序是按照数据库底层存储的顺序不是你传入的id顺序所以一定要手动按order排。排完后组装进VO接口就大功告成。实体类字段映射这里我再多说一句数据库字段如果使用下划线命名法比如line_name、first_bus_timeMyBatis-Plus默认开启了驼峰映射Java属性写成lineName、firstBusTime即可自动对应。如果有一天你发现查出来的字段是null先检查配置里的map-underscore-to-camel-case是否为true。4. 前端页面开发Vue3 Vite 的查询交互实现4.1 前端工程结构和页面模块划分前端Vue3项目的工程结构相对固定core包基本都是src/api、src/router、src/views、src/components、src/store、src/utils。用Vite初始化项目后src/api里放所有后端接口请求的封装每个页面模块就一个js文件比如line.js里封装getLineList、getLineDetail、getStationList四个方法。统一走axios实例baseURL由环境变量控制开发环境代理到localhost:8080生产环境用相对路径。页面模块主要就三个线路列表页、站点列表页、线路详情页再加一个首页仪表盘放统计数据和快捷入口。线路列表页用Element Plus的el-table渲染数据顶部用el-form做搜索栏输入线路名称、选择运营状态点查询触发列表刷新。支持分页的话再接一个el-paginationpageNum和pageSize用reactive管理。4.2 用组合式API组织业务逻辑比Options API清楚十倍Vue3最大的变化就是用Composition API script setup语法组织逻辑。以前Vue2写一个查询功能data里放变量、methods里放函数、computed里放计算属性逻辑一多一个组件几百行来回跳。Vue3直接把“查询”相关的所有状态和动作集中在一起写比如const searchForm reactive({ lineName: , status: null }); const tableData ref([]); const loading ref(false); async function fetchList() { loading.value true; try { const res await getLineList({ ...searchForm, pageNum: 1, pageSize: 10 }); tableData.value res.data.records; } finally { loading.value false; } } onMounted(fetchList);我特别建议初学者养成用async/await和try/finally控制loading的习惯。加载状态虽然只是个小细节但直接影响用户体验不发请求时表格一片空白用户只会觉得系统“卡”加上loading转圈和Error提示体验立刻专业起来。4.3 axios封装和跨域代理前端调不通接口的第一大坑前端独立开发时默认端口5173后端SpringBoot默认端口8080前后端直接调接口必然报跨域。解决方式有几种后端的CorsFilter全局配置和前端Vite代理是最常用的组合。Vite代理只需要在vite.config.js里加一行配置server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端发request到/api/line/listVite开发服务器会转发给http://localhost:8080/api/line/list。注意前端的每个请求都要带/api前缀后端Controller的RequestMapping类级里也要有/api这个路径或者在后端用server.servlet.context-path统一配置。axios实例封装时响应拦截器要统一解包。比如后端统一返回ResultVOcode、message、data拦截器里直接判断code是否为200不是就弹出ElMessage.error。这样业务代码里就不用每个方法都重复判断了。4.4 线路详情页站点时间线和动态渲染的算法线路详情页是展示Vue3渲染能力的好场景。页面顶部展示线路基本信息卡片下面是一个垂直的站点时间线每个站点按顺序渲染成一个节点如果是换乘站再加一个特殊图标标记。El-Timeline组件可以做这个效果数据源就是后端返回的stationList数组。需要注意的一个交互是首次进入详情页是通过路由跳转传lineId你要在onMounted里拿route.query.lineId调后端接口这中间会有短暂的空数据期。页面要加v-if判断没数据不渲染数据拿到后再渲染时间线。这里也是新手容易踩的坑——route参数还没拿到就去调接口结果请求的是undefined。另一个提升体验的小细节是“站数”和“里程”的自动计算。后端接口返回了站点数量和总里程前端详情页头部就能显示“共25站 · 全程12公里 · 票价2元”。这个展示密度很符合用户对查询结果的心理预期不用点开时间线就对上号了。5. 从零到部署环境搭建与项目运行完整笔记5.1 后端启动三步走JDK、Maven、配置数据库第一步检查JDK版本。SpringBoot2.7最低要求JDK8推荐JDK8或JDK11。命令行里java -version看一眼如果是OpenJDK 11就没问题。如果电脑装了多个JDK记得在IDE里把Project SDK指定到对应的版本不然编译会报“invalid source release”。第二步用Maven拉依赖。在项目根目录执行mvn clean install -DskipTests第一次运行会下载依赖网速慢的可能要等好一会儿。如果下载卡住先检查maven的镜像源。国内环境建议在settings.xml里配置阿里云镜像速度能快十倍不止。第三步配置数据库。先用Navicat或命令行创建数据库字符集选utf8mb4排序规则选utf8mb4_general_ci。然后导入项目自带的SQL脚本——比如init.sql。最后修改application.yml里的数据库账号密码和连接地址。到这里后端就可以启动了启动成功的标志是控制台出现“Started Application in x.xxx seconds”和Tomcat started on port(s) 8080。5.2 MySQL8.0安装与配置的完整注意事项这个项目的数据库依赖MySQL8.0如果你电脑上之前装了5.7版本会有个麻烦事MyBatis-Plus生成的SQL语法和驱动连接方式有小差异强烈建议直接按项目要求装8.0。装MySQL8.0时Windows用户我推荐用ZIP方式免安装比MSI安装包更容易控制。装完记得做三件事一是把MySQL服务注册成Windows服务并设为自动启动二是在my.ini配置文件里加character-set-serverutf8mb4和default-time-zone08:00这样后端连接串里的serverTimezone也可以精简掉三是用ALTER USER语句设置root密码加密方式为mysql_native_password否则新版MySQL默认的caching_sha2_password会导致老驱动连不上。如果你熟悉Docker其实更推荐一条命令跑起来docker run -d --name mysql8 -p 3306:3306 -e MYSQL_ROOT_PASSWORDadmin123 -e MYSQL_DATABASEbus_system -e TZAsia/Shanghai mysql:8.0好处是物理机干净卸载也彻底对连开好几个数据库版本的人来说最友好。5.3 前端环境准备与Node版本陷阱Vue3 Vite要求Node.js版本至少16以上强烈建议直接装Node 18 LTS。我之前在Node 14上跑Vite 4启动直接报“requires Node 16”。版本不对就先补环境不要硬跑。装完用node -v和npm -v确认。前端依赖安装命令常规是npm install但如果你之前装过yarn或pnpm个人推荐pnpm磁盘占用小安装速度快而且对Vite项目的依赖解析更友好。装完依赖执行npm run dev默认会启动在5173端口浏览器访问就进入前端页面。如果端口被占用Vite会自动换成下一个端口但代理配置里写死的target和你实际访问的页面端口没关系改成前端实际端口即可。5.4 前后端联调和项目打包联调阶段最常见的现象是前端页面打开了但表格一直转圈。打开浏览器F12的Network面板看具体的请求状态404说明路径没对上500说明后端内部异常去后端控制台看堆栈CORS error说明代理没生效或后端没放开跨域。按这几个方向排查基本都能迅速定位。开发验证完成后打包后端执行mvn clean package -DskipTests在target目录下生成一个jar包。启动方式不再依赖IDE直接用命令行java -jar bus-system-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod前端执行npm run build生成的dist目录就是纯静态文件。最省事的部署方案是把dist目录直接复制到SpringBoot的resources/static下这样整个系统合并成一个jar包一条命令跑起来整个项目适合没有专门Nginx服务器的学习环境。如果要求前后端彻底分离就把dist放到Nginx的html目录配一个反向代理转发/api到后端服务。6. 常见问题排查与避坑技巧实录6.1 数据库连接失败时区与驱动类名是第一凶手后端启动时报错“Cannot create PoolableConnectionFactory”或者“The server time zone value Öйú±ê׼ʱ¼ä is unrecognized”十次有八次是因为url参数没带serverTimezone或者驱动类名写错了。MySQL8.0必须用com.mysql.cj.jdbc.DriverMySQL5.7的老项目用的com.mysql.jdbc.Driver已经废弃。另外注意MySQL8.0默认认证插件的坑。如果你用较老的mysql-connector-java版本连接高版本MySQL会报“Unable to load authentication plugin caching_sha2_password”。对应两种解法升级connector版本到8.x或者执行SQL把root账号改成mysql_native_password。我个人更推荐升级驱动一劳永逸。6.2 前端页面一直404检查代理和路径前缀前端代理配好了但请求仍然报404核心检查两处一是代理配置里target地址的端口对不对二是后端Controller类定义的RequestMapping路径跟前端请求路径是不是完全一致。后端路径大写的要注意Windows开发环境下Tomcat和IDE大小写不敏感但部署到Linux的Tomcat后大小写敏感尽量全小写。6.3 MyBatis-Plus的pagination插件必须显式配置很多人抄了别人的代码但没有配置MybatisPlusInterceptor的PaginationInnerInterceptor然后发现分页查询返回的总数一直不对或者分页参数不生效。新版MyBatis-Plus配置分页拦截器不是自动开启的要在config类里显式声明Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }缺失这个BeanselectPage查出来的records确实是全部数据只不过帮你截断了一下但count(*)总数永远等于全部记录数这就是典型的“翻页到最后一页发现数据不对”。6.4 换乘查询扩展架构能不能撑住才是关键在源码基础上我最建议扩展的功能就是换乘查询。常见算法有两种一是直接查“从A站出发可用线路”和“经过B站的线路”之间的交集如果两条线有共同站点就构成一次换乘方案二是用Dijkstra或BFS在图结构上求最短换乘路径。这个系统的表结构完全支持第一种方案关联表加一个IS_TRANSFER字段就够了SQL写起来也不复杂。考虑到公交站点本身具备图的节点属性后端后续改进余地非常大。我在修改这个功能时的做法是写一个TransferService先查A站线路列表再查B站线路列表两层循环找交集站点组合成换乘方案VO。实测下来在500条线路、2000个站点的数据量下响应时间在100ms以内不依赖任何缓存就能满足课设性能要求。这就是数据库设计得好的功劳换乘这个看似高级的功能其实只是个普通的关联查询。6.5 文档与源码配套写论文和答辩的利器这套源码“含文档”是它最大的附加值之一。文档里一般包括系统需求分析、数据库设计说明书ER图、表结构说明、接口文档、页面原型说明。这对应届生来说太重要了因为毕业设计的论文工作量主要就在需求分析、总体设计、数据库设计这几章。直接用这套文档修改补充比从零开始画ER图和写用例表省太多时间。我建议拿到文档后做的第一件事不是写代码而是通读需求分析核对自己准备实现的功能列表跟文档是否一致。答辩时老师经常会问“你这个系统怎么保证数据一致性”“数据库为什么这样设计”“接口超时怎么处理”——这些在数据库设计和接口文档里都有对应逻辑趁早把思路理顺答辩就能对答如流。写在最后的一点个人体会把整个项目从源码到部署过了一遍我最想说的是公交线路查询这个题目看起来朴实无华实际上把全栈开发里最核心的几件事全练到了——数据模型设计、复杂查询SQL、服务端接口分层、前端交互与列表渲染、环境部署与问题排查。这一整套流程走下来你对SpringBoot2 Vue3这个技术组合的理解绝对比看一百个教程都管用。最后再分享一个小技巧改这套源码的时候先用git init提交一个初始版本之后每完成一个功能比如加了换乘查询就提交一次。这样即使改坏了也能随时回滚而且答辩时还能截图展示你的开发迭代记录这在老师眼里是实实在在的工程素养比空口说“我做了这个功能”可信得多。

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

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

免费获取报价 →
↑