☰
基于Spring Boot的城市交通公交线路与站点管理系统开发实战
2026/10/21 5:36:31 网站建设 项目流程

城市交通管理系统里的公交线路维护和公交站点管理,表面看就是对两张业务表做增删改查,可真正落地的时候,你会发现里面的关系处理和页面交互细节一点也不轻松。我最近把一个基于Java Spring Boot的城市交通管理系统完整跑了起来,核心功能恰好覆盖公交路线维护、公交站点信息管理,以及线路和站点之间的绑定与调整,我同时整理了项目源码、文档、运行视频和讲解视频,方便后续复盘。整套东西从需求分析、数据库建模、后端接口、前端页面,一直做到最后的字段校验和并发处理,中间踩了不少坑,也沉淀了一套可以直接复用的经验。这篇文章就按我实际操作的顺序来写,不绕弯子,适合正在做毕业设计、自学Spring Boot、或者想用城市交通场景练手的开发者参考。

1. 整体设计:公交管理系统应该先拆成哪几块

1.1 核心需求拆解:公交线路维护与站点管理的真正难点

公交线路和站点管理,如果只做一个单表CRUD,半小时就能写完。但城市交通管理系统的核心难点从来不在于单个表,而在于两个业务对象之间那条扯不清的关系。

简单说,需求是这么几个层面:

  • 公交线路维护:新增线路、修改线路基本信息、停用或删除线路、按编号或名称搜索线路。线路基本信息通常包括线路名称、线路编码、起点站、终点站、首班时间、末班时间、运营状态、备注信息。
  • 公交站点维护:新增站点、修改站点名称、更新站点经纬度和所在区域、标记站点是否启用。站点信息是基础数据,同一个站点可能会被多条线路经过。
  • 线路站点绑定:把站点挂到某条线路下,并且要能设置这个站点在线路中的顺序。这是最核心、最容易出问题的地方。
  • 状态管理:线路和站点都有启用、停用两种状态。停用的站点不能继续被新线路使用,停用的线路在前端列表中要能区分显示。

我在做这个项目前,先花了不少时间梳理需求。你如果只是跟着视频敲代码,很容易忽略一件事:公交线路和站点之间是典型的多对多关系。一条线路经过多个站点,一个站点也会被多条线路使用,所以不能简单地在站点表里加一个线路ID字段。这种设计上的第一直觉,往往是后面改表结构的根源。

实际操作时,我建议把需求拆成“基础数据维护”和“关系维护”两条线。基础数据就是线路表、站点表各自独立的CRUD;关系维护则是针对线路与站点的绑定操作。先分清楚这两类,后面的表结构、Service层方法、前端交互就都清楚了。

1.2 技术选型复盘:为什么用Spring Boot + MyBatis + MySQL

这套系统选型没什么悬念。Spring Boot是目前Java后端项目最主流的基础框架,内嵌Tomcat,简化了大量XML配置,起步快;MyBatis是半自动ORM,SQL仍然由开发者自己控制,很适合需要对复杂关联查询做精细调优的场景;MySQL则免费、通用、部署成本低。这个组合在中小型管理系统里非常成熟,网上资料也多,遇到问题基本搜得到答案。

前端我选的是Thymeleaf模板引擎,配合Bootstrap做页面。可能有人会问,现在不是流行前后端分离吗?为什么不直接上Vue或React?原因很实际:这个项目主要面向管理系统场景,页面交互不算特别重,如果用Vue + Spring Boot,前端还需要单独处理跨域、Token鉴权、打包部署,对刚接触Spring Boot的人来说负担明显变大。Thymeleaf直接服务端渲染,后端返回数据模型后模板自动渲染,页面源码里能直接看到数据,调试起来省心很多。当然,团队开发或需要多端复用时,前后端分离更合适,但作为单人或小团队快速交付,Thymeleaf是性价比最高的选择。

数据库层面我用了MySQL 8.0。字符集统一设为utf8mb4,避免中文乱码和表情符号存储问题。JDBC驱动、连接池这些都用Spring Boot默认配置,不做过度设计。整个技术栈的调性就是:稳定、简单、容易解释。

1.3 功能模块划分与请求数据流

我在设计模块时,没有把功能拆得特别碎,而是按业务域分成了四个大模块:

  1. 线路管理模块:线路的增删改查、分页搜索、启用停用。
  2. 站点管理模块:站点的增删改查、分页搜索、启用停用。
  3. 线路站点绑定模块:在线路详情里维护站点列表,设置站点顺序,保存时先删旧关系再插新关系。
  4. 基础支撑模块:登录拦截、统一的异常处理、统一的返回结果封装。

请求流转过程很简单:浏览器发起HTTP请求,Spring Boot的Controller接收到参数,调用Service层处理业务逻辑,Service层通过Mapper接口操作数据库,结果再返回Controller,最后充实到Thymeleaf模板里返还给浏览器。这个链路是标准的Spring MVC流程,但很多人在做的时候只关注写代码,忽略了每个环节的边界。我个人的习惯是:Controller尽量只做参数接收和结果封装,具体业务判断全部放在Service里,Mapper只负责SQL执行。这样一来,后面做单元测试或替换数据源都会方便很多。

1.4 项目目录结构规划

Maven标准目录结构配Spring Boot,目录分清楚,代码才不容易乱。这是我的一个典型目录结构:

com.city.traffic ├── TrafficApplication.java ├── common │ ├── Result.java │ ├── PageResult.java │ └── exception │ ├── BusinessException.java │ └── GlobalExceptionHandler.java ├── config │ ├── WebMvcConfig.java │ └── MybatisPlusConfig.java ├── controller │ ├── LineController.java │ └── StationController.java ├── service │ ├── LineService.java │ ├── StationService.java │ └── impl │ ├── LineServiceImpl.java │ └── StationServiceImpl.java ├── mapper │ ├── LineMapper.java │ ├── StationMapper.java │ └── LineStationMapper.java ├── entity │ ├── Line.java │ ├── Station.java │ └── LineStationRel.java └── dto ├── LineQueryDTO.java └── StationQueryDTO.java

这里用到了MyBatis Plus的MP配置目录,因为实际开发中BaseMapper能省不少体力的CRUD代码。如果你不想引入MyBatis Plus,直接写纯MyBatis也可以,接口和XML路径对应上就行。目录规划的要点是:entity只放表映射字段,dto放查询参数或前端提交对象,controller只处理路由,service放业务,mapper放SQL。这样在“讲视频”的时候也容易给观众讲清楚每一层干什么。

2. 数据库与核心逻辑:把公交线路和站点的关系彻底捋顺

2.1 三张核心表:线路、站点、线路站点关系表

数据库设计是整个系统最关键的一步。我选择用三张表来表达业务:公交线路表、公交站点表、线路站点关系表。没有把线路和站点直接冗余到一张表里,是因为前面已经分析过,二者是多对多关系。

线路表的核心字段如下:

CREATE TABLE t_bus_line ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', line_code VARCHAR(20) NOT NULL COMMENT '线路编码,如L1001', line_name VARCHAR(50) NOT NULL COMMENT '线路名称,如1路', start_station VARCHAR(50) NOT NULL COMMENT '起点站名称', end_station VARCHAR(50) NOT NULL COMMENT '终点站名称', first_bus_time VARCHAR(10) DEFAULT '06:00' COMMENT '首班车时间', last_bus_time VARCHAR(10) DEFAULT '22:00' COMMENT '末班车时间', line_status TINYINT DEFAULT 1 COMMENT '状态:1启用 0停用', remark VARCHAR(200) COMMENT '备注', update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', UNIQUE KEY uk_line_code (line_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='公交线路表';

站点表核心字段如下:

CREATE TABLE t_bus_station ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', station_code VARCHAR(20) NOT NULL COMMENT '站点编码', station_name VARCHAR(50) NOT NULL COMMENT '站点名称', longitude DECIMAL(10, 6) COMMENT '经度', latitude DECIMAL(10, 6) COMMENT '纬度', station_status TINYINT DEFAULT 1 COMMENT '状态:1启用 0停用', address VARCHAR(200) COMMENT '站点所在地址', UNIQUE KEY uk_station_code (station_code), KEY idx_station_name (station_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='公交站点表';

关系表是整个设计里最需要想清楚的:

CREATE TABLE t_line_station_rel ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', line_id BIGINT NOT NULL COMMENT '线路ID', station_id BIGINT NOT NULL COMMENT '站点ID', station_order INT NOT NULL COMMENT '站点在线路中的顺序,从0开始', direction TINYINT DEFAULT 0 COMMENT '方向:0上行 1下行', UNIQUE KEY uk_line_station_order (line_id, direction, station_order), UNIQUE KEY uk_line_station (line_id, station_id, direction), KEY idx_station_id (station_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='线路站点关系表';

为什么在关系表里要加station_order?你可以想象一下,公交线路是有方向、有顺序的,从起点站到终点站,每个站点都有先后位置。如果关系表里不记录顺序,那前端展示出来的站点列表就会乱套。加direction是因为很多公交线路上行和下行经过的站点不完全一致,即使一致,顺序也可能是相反的。虽然有些简化项目不考虑方向,但真实业务中这个字段能避免掉很多“线路站点顺序不正确”的纠纷。

2.2 多对多关系的本质:用中间表记录顺序和方向

很多新手容易犯的错误是:在站点表里加一个line_id字段,把站点挂到某一条线路上。这样做最直接的问题就是,同一个站点被第二条线路使用时,就得再插一条记录,数据会出现大量冗余;而且一个站点的名称、经纬度、状态重复维护多次,改一次站点信息要同步改好几条记录,非常容易不一致。

正确的做法就是中间表。这和大学选课系统一样:学生和课程是多对多,必须用选课表记录“哪个学生选了哪门课”。公交线路和站点之间的关系表,除了记录“哪个站点属于哪条线路”,还要额外记录“站点在该线路里的位置顺序”。这就是我在表里加station_order的原因。

保存线路站点绑定时,最稳妥方式是事务内先删除旧关系,再批量插入新关系。比如前端的页面提交了一个站点ID和顺序列表,后端拿到lineId和stationIdList,先把这条线路下旧的关系全部删除,再根据顺序逐个插入。这种方式简单直观,不容易出现脏数据。

2.3 业务查询SQL的写法与细节

线路管理中最常用的两个查询:按线路查站点、按站点查线路。

按线路查站点的SQL:

SELECT s.id AS stationId, s.station_name AS stationName, ls.station_order AS stationOrder FROM t_line_station_rel ls INNER JOIN t_bus_station s ON ls.station_id = s.id WHERE ls.line_id = #{lineId} AND ls.direction = #{direction} ORDER BY ls.station_order ASC

按站点查线路的SQL:

SELECT l.id AS lineId, l.line_name AS lineName, l.line_code AS lineCode FROM t_line_station_rel ls INNER JOIN t_bus_line l ON ls.line_id = l.id WHERE ls.station_id = #{stationId} ORDER BY l.line_code ASC

这两个SQL的精髓在于INNER JOIN。中间表t_line_station_rel就像一座桥,把线路表和站点表连接起来。基础数据里只存线路信息、站点信息,不存关联关系;查询时再用关联表动态组合。这样做的好处是:站点表不用存储重复的线路信息,线路表也不用担心站点变化导致的数据冗余。

索引设计上,关系表里的联合索引特别重要。uk_line_station_order (line_id, direction, station_order)既能保证“同一条线路同一个方向,站点顺序不能重复”,也能加速按线路查询。idx_station_id则保证按站点反查线路时不会全表扫描。早期我偷懒没加这个索引,站点多了以后查询直接慢了一倍多,加上以后瞬间恢复。别小看这几分钟的设计,数据量上来的时候效果很明显。

2.4 事务与并发更新:线路站点绑定的安全底线

线路站点绑定这个操作,不是一个简单的单表insert,而是“先删除旧关系,再插入新关系”的组合操作。如果中途出异常,比如插入到第5个站点时数据库报错,那么前4个已经插入成功,旧数据却被删了,最终结果是这条线路的站点数据缺胳膊少腿。为了避免这种情况,方法必须加事务注解。

我习惯在Service实现类上写@Transactional(rollbackFor = Exception.class)。注意,一定要指定rollbackFor,因为Spring默认只对RuntimeException回滚,如果业务代码里抛的是Checked Exception,默认不会触发回滚,数据就处在半成品状态了。

并发方面,如果两个管理员同时编辑同一条线路,最典型的冲突是:A把站点设置成[站1、站2],B把站点设置成[站3、站4],最后提交晚的人会把早先的数据覆盖掉,但业务上可能A的修改已经生效过一段时间了。要处理这种问题,常规做法是加版本号字段。在线路表加一个version INT,每次更新时检查数据库里的version和提交上来的version是否一致,一致则更新并把version加1,不一致则提示“数据已被他人修改,请刷新后重试”。这类并发控制在毕设或中小型项目里不一定会做,但如果在项目文档里写清楚,会是非常加分的点。

3. 实操过程:从零实现公交线路与站点维护功能

3.1 环境准备与项目初始化

我用的环境是JDK 8、Maven 3.6.3、MySQL 8.0、IDEA 2022版。Spring Boot版本选了2.7.x,这个版本对JDK 8支持友好,网上资料也多,不容易出现JDK版本不兼容的怪问题。

创建项目时,我推荐直接在IDEA里用Spring Initializr,勾选Spring Web、Thymeleaf、MyBatis框架依赖。如果只是做纯MyBatis,依赖如下:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>

如果你用MyBatis Plus,再加上:

<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.2</version> </dependency>

配置文件application.yml里,数据库连接信息要按自己本机的MySQL配置来改:

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/city_bus_system?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: yourpassword thymeleaf: cache: false mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.city.traffic.entity configuration: map-underscore-to-camel-case: true

这里有两个小细节。第一,数据库URL里一定要带上serverTimezone=Asia/Shanghai,否则连接MySQL 8.0时大概率会报时区错误。第二,map-underscore-to-camel-case: true可以自动把数据库的station_name映射成实体里的stationName,省去写一堆resultMap的功夫。第一次做项目的人往往忽略了这两点,结果启动就报错,还以为是代码写错了。

3.2 后端编码:实体、Mapper、Service、Controller

后端代码我按“实体类 → Mapper接口 → XML → Service → Controller”的顺序来写。实体类尽量和表字段一一对应,比如Line实体:

public class Line { private Long id; private String lineCode; private String lineName; private String startStation; private String endStation; private String firstBusTime; private String lastBusTime; private Integer lineStatus; private String remark; private Date updateTime; // 省略getter/setter }

Mapper接口如果用MyBatis Plus,可以继承BaseMapper,简单SQL直接调用内置方法:

public interface LineMapper extends BaseMapper<Line> { List<Line> selectPageByQuery(LineQueryDTO dto); }

复杂查询写在XML里。比如带条件的分页查询:

<select id="selectPageByQuery" resultType="com.city.traffic.entity.Line"> SELECT * FROM t_bus_line <where> <if test="lineName != null and lineName != ''"> AND line_name LIKE CONCAT('%', #{lineName}, '%') </if> <if test="lineStatus != null"> AND line_status = #{lineStatus} </if> </where> ORDER BY id DESC </select>

Service层重点写保存线路站点绑定的方法。这是整个系统的业务核心,我把它单独抽出来:

@Service public class LineServiceImpl implements LineService { @Autowired private LineMapper lineMapper; @Autowired private LineStationRelMapper lineStationRelMapper; @Override @Transactional(rollbackFor = Exception.class) public void saveLineStations(Long lineId, Integer direction, List<Long> stationIdList) { // 1. 删除旧的关系 lineStationRelMapper.deleteByLineIdAndDirection(lineId, direction); // 2. 重新插入新的关系,顺序由stationOrder表示 if (stationIdList != null && !stationIdList.isEmpty()) { for (int i = 0; i < stationIdList.size(); i++) { LineStationRel rel = new LineStationRel(); rel.setLineId(lineId); rel.setStationId(stationIdList.get(i)); rel.setDirection(direction); rel.setStationOrder(i); lineStationRelMapper.insert(rel); } } } }

这个方法做的事情并不复杂,但有几个地方需要说明:为什么要先删除再插入?因为前端提交的是整个线路站点的最终完整列表,我们没有办法知道用户到底改了哪个位置、删了哪个站点,最简单可靠的策略就是全量覆盖。第1步删除必须有direction条件,否则上行和下行会互相误删。第2步插入时,第0个就是起点站,最后一个就是终点站,顺序通过循环下标来记录。

Controller层代码如下:

@Controller @RequestMapping("/line") public class LineController { @Autowired private LineService lineService; @GetMapping("/list") public String list(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, LineQueryDTO dto, Model model) { PageResult<Line> pageResult = lineService.pageQuery(pageNum, pageSize, dto); model.addAttribute("page", pageResult); model.addAttribute("query", dto); return "line/list"; } @PostMapping("/save") @ResponseBody public Result<?> save(Line line) { lineService.saveLine(line); return Result.success(); } }

页面请求用普通Spring MVC,接口请求用@ResponseBody返回JSON。Thymeleaf模式下的页面列表走/line/list,表单提交走/line/save。实际项目里,我更喜欢把“页面跳转”和“数据接口”分开,HTML页面通过接口拿到JSON后用JavaScript渲染。这里其实可以灵活调整,但核心思路是一致的:Controller层不要堆业务。

3.3 前端页面:表格、表单、回显与校验

前端用Thymeleaf加Bootstrap,列表页最核心的就是线路表格和新增/编辑弹窗。线路列表的表格核心代码如下:

<table class="table table-bordered table-hover"> <thead> <tr> <th>线路编码</th> <th>线路名称</th> <th>起点站</th> <th>终点站</th> <th>首班时间</th> <th>末班时间</th> <th>状态</th> <th>操作</th> </tr> </thead> <tbody> <tr th:each="line : ${page.records}"> <td th:text="${line.lineCode}"></td> <td th:text="${line.lineName}"></td> <td th:text="${line.startStation}"></td> <td th:text="${line.endStation}"></td> <td th:text="${line.firstBusTime}"></td> <td th:text="${line.lastBusTime}"></td> <td> <span th:if="${line.lineStatus == 1}" class="text-success">启用</span> <span th:if="${line.lineStatus == 0}" class="text-secondary">停用</span> </td> <td> <button class="btn btn-sm btn-primary" th:onclick="'editLine(' + ${line.id} + ')'">编辑</button> <button class="btn btn-sm btn-danger" th:onclick="'deleteLine(' + ${line.id} + ')'">删除</button> </td> </tr> </tbody> </table>

新增编辑就用一个Bootstrap模态框,表单里放线路名称、线路编码、起点站、终点站、首班时间、末班时间、状态等字段。提交前,JavaScript要做一次必填校验。比如线路编码如果不填,直接提示“线路编码不能为空”;线路名称重复时,后端返回错误信息,前端弹窗提示。

站点维护页面结构类似,只是多了经纬度字段。经纬度和地址是站点独有的信息,在公交站点管理中往往用于地图展示和位置定位。实际做的时候可以省略经纬度,只留站点名称和编码,但既然要做城市交通系统,我建议把经纬度加上,为以后对接地图打基础。

3.4 线路站点绑定页面的实现思路

线路站点绑定是这个项目里最能体现业务价值的功能。我的做法是在线路列表的每一行放一个“站点配置”按钮,点击后进入该线路的站点管理页。页面左侧显示线路基本信息,中间显示当前已选站点列表,右侧显示所有可用站点,通过勾选或拖拽来调整顺序。

出于简单考虑,我用的是左侧已选列表加“上移/下移”按钮的方式来维护顺序。站点列表通过Ajax请求传给后端:

function saveStations() { let lineId = $("#lineId").val(); let stationIdList = []; $("#stationList li").each(function () { stationIdList.push($(this).attr("data-id")); }); $.ajax({ url: "/line/saveStations", type: "POST", data: { lineId: lineId, direction: 0, stationIds: stationIdList.join(",") }, success: function (res) { alert("保存成功"); } }); }

后端接收stationIds字符串,按逗号拆分,再传入saveLineStations方法。这个交互虽然看着简单,但业务顺序很清晰:先列线路,再配站点,最后保存关系。整个过程跑通后,再去看标题里的“公交路线维护”和“公交站点”两个关键词,就完全对上了。

4. 踩坑实录与排查技巧

4.1 日期时间字段类型不匹配

公交线路有首班时间和末班时间,这两个字段我用的是VARCHAR(10),直接存"06:00"这样的字符串。一开始有同事建议用TIME类型,但试过之后发现前端表单的输入控件是time类型,提交到后端时格式能对上,但模板回显时总会多出:00秒数,反而不方便。项目里不需要对时间做范围计算,只是展示,所以直接用字符串最简单。

如果你在系统中用了真正的日期字段,比如建线日期,要注意Java端用LocalDate还是Date。Spring MVC接收前端日期字符串时,要加注解:

@DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate buildDate;

不加的话,前端提交2024-05-20很可能报类型转换错误。这类问题表面看是“系统报500”,实际是参数绑定失败,排查时先看控制台是否出现Failed to convert property。

4.2 删除线路或站点时外键约束报错

删除线路时最容易遇到的一个问题:线路下还有站点关系数据,直接删除线路表会报外键约束失败,或者删除成功但关系表里残留大量脏数据。解决思路有两条。

物理删除路线:先删除关系表里所有line_id对应的记录,再删除线路本身。这个操作同样需要放在同一个事务里。删除站点时也一样,先删关系表里station_id对应的记录,再删站点。

更好的方案是逻辑删除:给线路表增加deleted字段,删除时不是真删,而是把状态改成停用,查询时默认过滤掉停用数据。这样做的好处是历史数据可追溯,站点删除后,历史线路的轨迹数据还能保留。对一个城市交通管理系统来说,逻辑删除明显更符合真实业务场景。我最终采用的是“逻辑删除 + 物理删除”结合:线路和站点用状态字段控制显示,关系表用物理删除清理无效关联。

4.3 MyBatis动态更新SQL的坑

使用MyBatis做更新时,如果直接用<set>标签,很容易遇到一个问题:某字段传入值是null,结果MyBatis生成的SQL把原来数据库里的值覆盖成了null。比如前端编辑线路时没有填备注,提交后remark是空字符串,但如果你在XML里写了remark = #{remark},数据库里的备注就会被清空。

解决办法是在更新SQL里加<if>判断:

<update id="updateById" parameterType="com.city.traffic.entity.Line"> UPDATE t_bus_line <set> <if test="lineCode != null and lineCode != ''">line_code = #{lineCode},</if> <if test="lineName != null and lineName != ''">line_name = #{lineName},</if> <if test="startStation != null">start_station = #{startStation},</if> <if test="endStation != null">end_station = #{endStation},</if> <if test="lineStatus != null">line_status = #{lineStatus},</if> </set> WHERE id = #{id} </update>

使用<set>时要注意尾部的逗号。MyBatis会自动去掉最后一个逗号,但前提是SQL结构正确。如果<if>全部不成立,<set>里没有内容,SQL就会变成UPDATE t_bus_line SET WHERE id = ?,这是语法错误。所以至少要保留一个非空判断的字段。我实际写的时候,会把id、update_time固定放在<set>里,确保永远有字段更新。

4.4 端口占用与依赖冲突

Spring Boot项目启动时报Port 8080 was already in use,这是每个初学者绕不开的坑。排查方法:终端执行netstat -ano | findstr 8080,查看到占用进程后,直接结束对应进程,或者改server.port。我在做系统的时候遇到过这个,我随手把端口改成8081,结果前端页面写死的8080请求全部跨端口报错,后来还是老老实实把占用进程结束掉了。这里提醒大家,临时改端口只能解决启动问题,但容易引发联调时前端请求路径不一致,最稳妥还是统一端口。

依赖冲突方面,最典型的是MyBatis和MyBatis Plus版本不兼容。比如同时引入了mybatis-spring-boot-starter和mybatis-plus-boot-starter,启动时经常报Invalid bound statement。解决办法是去掉其中一个,只用MyBatis Plus。排查依赖冲突时可以用mvn dependency:tree看依赖树,找到重复或冲突的依赖再处理。这个命令我每次遇到隐性问题都会跑一遍,比凭空猜快得多。

4.5 前端修改不生效和模板缓存问题

Thymeleaf模板引擎默认有缓存。Spring Boot开发环境下,如果不在配置里关闭缓存,改完页面刷新后看到的一直是旧页面。必须设置:

spring: thymeleaf: cache: false

另外还要注意浏览器缓存。有些时候后台代码已经更新,但浏览器因为缓存展示的还是旧样式。开发时按Ctrl + F5强制刷新,或者打开浏览器开发者工具勾选Disable cache。这个看似很小的问题,我见过有人排查了一下午都没发现,最后只是在配置里加了一行cache: false。

4.6 常见问题速查表

现象可能原因解决办法
启动报时区错误MySQL连接URL缺少serverTimezoneURL加上serverTimezone=Asia/Shanghai
中文乱码数据库字符集不是utf8mb4建库时指定utf8mb4,连接串加characterEncoding=utf8
删除线路报外键错误关系表还有关联数据事务内先删关系表,再删线路
MyBatis更新覆盖nullupdate SQL没做非空判断用<if>动态拼接字段
端口8080被占用其他进程占用端口netstat -ano找进程,或统一改端口
Thymeleaf不更新模板缓存未关闭配置cache:false
线路顺序乱掉未按station_order排序查询加ORDER BY station_order ASC
页面无法跳转Controller返回路径写错检查return的模板路径是否存在

排查问题有一个通用思路:先看控制台日志,再看数据库里实际执行的数据,最后定位到Controller、Service、Mapper的哪个环节。很多时候不用急着改代码,先弄清楚报错在哪一层,解决起来自然就快了。

5. 从项目落地到继续扩展:我的一点实践心得

这个基于Java Spring Boot的城市交通管理系统,做到目前这个程度已经算是一个完整的可运行项目了。但如果你只满足于跑通功能,那还远远不够。我在实际开发过程中,发现这类系统最值得扩展的方向有三个。

第一个是数据可视化。公交线路和站点数据天然适合地图展示。给站点加经纬度,用地图组件把线路和站点画到地图上,站点维护界面会直观很多。第二个是批量导入导出。公交公司日常维护线路时经常要处理Excel表格,如果系统支持Excel导入线路、站点数据,实用性会提升一截。用Apache POI或者EasyExcel都能实现,难度不大。第三个是权限管理。真实的城市交通管理系统肯定不止一个管理员,普通管理员、审核员、超级管理员各自的权限不同。这时可以引入Spring Security或Sa-Token做登录认证和权限控制。

作为一个做过类似项目的人,我还有一个挺深的体会:不要急着写代码,先画一张线路和站点关系的草图。你可以在白纸上画一条公交线路,标出每个站点顺序,再画两条线路交叉经过同一个站点的情况,然后思考数据库应该怎么设计。想明白这个场景后,中间表的必要性自然就清楚了。项目代码里最难做好的不是增删改查,而是“关系”的处理。

最后再说一个小技巧。在实际维护公交线路时,很常见的情况是某条公交线路因为施工临时调整站点顺序,甚至中途取消几个站点。如果你的系统里把原来的站点顺序直接覆盖掉,后面想回溯就很麻烦。建议在关系表里加一个版本号或生效日期字段,每次调整时留历史版本,这样既能看到当前线路走向,又能回溯历史配置。我目前这个版本里还没有完全实现,但下一版一定是优先考虑的功能。

做这类管理系统,说白了就是“需求理解得透彻,表结构设计得合理,事务边界兜住底,前端交互别贪复杂”。只要把这几点撑住,Spring Boot + MyBatis这套组合完全能撑起一个体面的城市交通管理系统项目。如果你正在做相似的系统,希望能从这篇实际操作的记录里找到参考。

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

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

立即咨询