☰
Spring Boot农村客运系统实战:从设计到部署的完整总结
2026/10/7 5:20:11 网站建设 项目流程

做农村客运服务系统这个项目,是我过去几个月投入精力最多的一件事。说直白点,这个基于Spring Boot的农村客运服务系统,就是要把农村班线的班次管理、售票订票、车辆调度和站点信息从纸质台账和微信群聊里搬到一个正经的后台系统里来。它解决的痛点是:村民不知道车几点来、客运公司不知道车上坐了多少人、调度员只能靠电话一个个确认。如果你是正在做毕业设计的学生,或者乡镇客运公司的技术负责人,又或者单纯想看看一个Spring Boot项目从设计到上线要踩多少坑,那这篇总结应该对你有用。

我把从需求梳理、技术选型、数据库设计,到接口开发、前端打包部署,再到上线后遇到的各类问题的过程完整写下来。里面没有那种照着官方文档念的废话,全是我实际敲过代码、跑过服务的记录,顺手能复制的代码和配置我也会直接贴出来。

1. 项目整体设计与核心业务拆解

1.1 农村客运系统到底要解决什么问题

一开始我就没打算做一个大而全的“智慧交通平台”,而是先围着农村客运的实际场景画了一张业务流程图。农村客运和城市公交差别非常大:线路固定但班次不固定,一辆车可能一天只跑两趟,乘客数量波动极大,乡镇站点之间距离又远。过去客运公司靠调度员拿着一张纸,上面写着每个车次几点发车,司机发车之前给调度打个电话汇报人数,调度再决定要不要加开班车。乘客呢,只能提前去站台守着,或者给司机打电话问。

系统要解决的第一件事是信息透明。把每天哪些班次、几点发车、途经哪些站点、余票多少,全部变成可查询的数据。第二件事是调度效率。以往加开一班车,要从填单子、盖章、通知司机三条线走下来,现在直接在系统里处理,核验客流后自动生成建议班次,调度员确认一下就生效。第三件事是财务对账。农村客运很多车票是车上现金交易,票款、补贴、油费混在一起,系统里每笔订单都有记录,月底对账就不用靠司机口述了。

所以这系统不是给城里人坐地铁用的那种高并发平台,而是一个贴合乡镇场景、业务规则相对复杂但并发量可控的管理系统。这也决定了我后来的技术选型思路——稳定优先,别炫技。

1.2 技术选型:为什么是Spring Boot + MyBatis + Vue

技术栈我基本没有犹豫:后端用Spring Boot,持久层用MyBatis,前端用Vue。这套组合看起来普通,但在农村客运这种业务逻辑偏重、并发量不高的场景中非常合适。

先解释为什么不用微服务。一个乡镇客运公司,全部业务跑在一台4核8G的服务器上都没问题,拆微服务只会增加运维成本。Spring Boot内置Tomcat,打成jar包直接跑,对一线运维人员就是双击启动的事。版本我最后选了Spring Boot 2.7.18,而不是最新的3.x,理由在后面的排查记录里会详细说到。持久层没有用Spring Data JPA,而是用MyBatis,因为班次查询、订单报表这种场景要写大量自定义统计SQL,MyBatis的Xml映射文件可以让我把SQL写得更直观,而且动态SQL处理多条件筛选特别灵活。

前端用Vue不是因为流行,而是因为它可以用来快速做一个单页管理后台。Vue 3 + Element Plus的组合,表格、表单、弹窗都是现成的。我的项目分了两端:管理端给客运公司内部人员用,查询端给乘客用。乘客端我用的是Vue Router的hash模式,后面嵌入Spring Boot的静态资源目录非常省事,不需要配置服务端路由转发。

1.3 模块划分与数据库设计要点

整个系统按功能拆了六个模块:线路站点管理、班次管理、售票订单管理、车辆与驾驶员管理、调度管理、统计报表。数据库表跟着模块走,核心表给了很大的设计自由度。

线路表、班次表、站点表这三张是基础,但真正让查询变复杂的是班次站点关系表。一辆农村班车从镇里出发,途中经过8个村站,同样的线路可能因为停靠点不同分成A、B两个班次。我在schedule_station表里加了station_order字段,用来表示某个班次经过站点的顺序,同时记录预计到达时间和发车时间。这样一来,查“上午10点以后从李庄去县城”的车,就变成了查班次站点关系表,把起点站点、终点站点、时间范围三个条件拼在一起,再用子查询过滤出符合条件的班次。

订单表是另一个设计重点。农村客运的订单有两种形态:线上预订和车上补票。线上预订在系统里直接生成订单,车上补票则是由司机在移动端录入。订单表里用order_type字段做了区分,同时用status字段保存状态机:待支付、已支付、已出票、已检票、已退票、已取消。每个状态变更都写进订单流水表,这条流水不仅方便排查问题,月底和客运公司财务对账时也一目了然。

还有一张容易被忽略的是基础参数表。比如每单默认的取票过期时间、车辆座位数、里程补贴单价,这些参数会频繁调整,如果硬编码在业务代码里,每次修改都要重新发版。我用一张sys_config表,配合后端的缓存注解,改动之后五秒钟生效,这个设计后续给我省了不少事。

2. 核心功能实现与关键细节

2.1 班次查询与预订接口怎么设计

班次查询接口是整个系统最核心的入口,乘客端、管理端、驾驶员端都会调用。接口设计没有走复杂的CQRS模式,就是普通的RESTful接口,但对于查询条件做了严格拆分。

请求参数如下:线路ID或班次ID、出发站点ID、到达站点ID、出发日期、出发时间区间。用GET请求,参数通过Spring MVC的ModelAttribute绑定到查询对象。之所以不把出发站点和到达站点简单合并成“站点ID”,是因为同一辆班车沿途要停好几个站点,乘客可能从第2站上车、第6站下车,系统必须根据station_order来判断乘坐区间是否合法。

下单时的余票判断是个关键点。最初我直接在schedule表上计算“总座位数 - 已售订单数”,后来发现完全不靠谱——一个人从第2站坐到第6站,另一个人从第4站坐到第8站,同一条路线不同区间的座位会有重叠。我改成按区间来统计:先从schedule_station表查出该班次所有站点顺序,拿到要买的起终点索引,然后统计所有已出票订单里,与当前区间有重叠的座位数之和,再用总座位数减去这个重叠数。计算逻辑不复杂,但用SQL表示出来就比较绕,这地方既考验业务理解,也考验SQL能力。

核心代码片段:

@Service public class ScheduleService { @Autowired private ScheduleMapper scheduleMapper; public PageResult<ScheduleDTO> querySchedules(ScheduleQuery query) { if (query.getDepartDate() == null) { query.setDepartDate(LocalDate.now()); } return scheduleMapper.selectScheduleList(query); } public boolean isSeatAvailable(Long scheduleId, Long fromStationId, Long toStationId, int seatCount) { // 查出起点和终点在班次站点中的顺序 int fromOrder = scheduleStationMapper.getStationOrder(scheduleId, fromStationId); int toOrder = scheduleStationMapper.getStationOrder(scheduleId, toStationId); // 统计重叠区间的已售座位数 int soldSeats = orderMapper.countOverlapOrders(scheduleId, fromOrder, toOrder); int totalSeats = scheduleMapper.selectTotalSeats(scheduleId); return (totalSeats - soldSeats) >= seatCount; } }

这里有一个容易出错的地方:订单状态必须过滤,只统计已支付、已出票和已检票的记录。待支付订单超时未付款,如果不释放座位,会导致虚占库存。我用了Spring Boot自带的@Scheduled做一个清理任务,每五分钟扫一遍超过十五分钟的待支付订单,把它改成已取消,同时释放座位。这个方案对于农村客运的并发量完全够用,不需要引入Redis分布式锁。

2.2 定时任务:自动生成班次计划和清理过期订单

农村客运的班次不是乘客今天买票就今天随意发车,而是客运公司每天晚上得确定第二天的发车安排。系统里我加了一个“智能排班”的任务,每天凌晨两点扫描未来三天的季节性需求,根据历史同期的售票数据生成建议班次,调度员在管理端确认后自动发布。

实现上用的是Spring Boot的@Scheduled注解,没啥高深的。但这里有个问题是任务执行时机不固定,如果某天系统重启,重启时的初始化阶段可能不会触发当天凌晨的排班。所以我把生成排班的逻辑单独抽成接口,在应用启动完成后调用一次:

@Component public class ScheduleGenerateTask { @PostConstruct public void init() { generateNextThreeDays(); } @Scheduled(cron = "0 0 2 * * ?") public void generateNextThreeDays() { // 每年节假日日期存在基础表里,这里只处理普通日期 List<Date> dates = new ArrayList<>(); dates.add(LocalDate.now().plusDays(1).toDate()); dates.add(LocalDate.now().plusDays(2).toDate()); dates.add(LocalDate.now().plusDays(3).toDate()); for (Date date : dates) { buildScheduleForDate(date); } } }

@PostConstruct和@Scheduled用了同一个方法,工具类里我实际写的是generateNextThreeDays()里循环调用buildScheduleForDate,这里为了示例简化了。要注意的是定时任务默认在单线程执行器里串行跑,如果多个任务相互依赖,最好直接用@Scheduled加固定延迟,而不是cron表达式。我这里的排班任务涉及查历史表、生成多条班次记录,执行时间可能超过几秒,但如果任务被上一次执行卡住,Spring Boot默认会等上一次跑完再执行下一次,目前没出过问题。

2.3 车辆进出站状态变更与数据闭环

班车从始发站发车、沿途停靠、最后到底站,这个状态流转如果只靠人工修改,极容易出现漏改错改。我设计了一个“到站打卡”机制,驾驶员端App上有一个简单的打卡按钮,车辆到达站点后点击,后端就把schedule_station的status更新为已到达,并记录实际到达时间。

这个模块涉及两个技术点:一个是怎么保证同一班次不会被重复打卡,另一个是怎么计算前后站点顺序。我在schedule_station表里加了status字段和实际到达时间字段,打卡接口传入scheduleId和stationId,后端先检查这条站点记录的station_order,再和当前最新进度比较。只有当天处于“待发车”或“运行中”状态,且当前打卡站点的order大于上一打卡站点的order,才允许更新。

为了减少司机误操作,我做了接口幂等处理。同一个站点重复提交,如果发现该站点已经是已到达,就直接返回成功,不重复写时间。同时用数据库的唯一索引兜底,防止极端情况下的重复插入。状态枚举我放在Java里,用常量类管理,不直接写魔法值:

public class ScheduleStatus { public static final int PENDING = 0; public static final int RUNNING = 1; public static final int ARRIVED = 2; public static final int CANCELED = 3; }

为什么不用数据库字典表?因为状态只会在代码里判断,不参与复杂查询,建表反而多一次关联查询,得不偿失。但订单的支付状态和班次状态不同,支付状态涉及对账和退款流程,我特意在数据库表里加了comment注释,并且用tinyint存储,因为状态枚举值就几个,用字符串反而浪费空间。

2.4 自动装配原理与自定义系统参数配置

Spring Boot最核心的能力就是自动装配。稍微懂点原理的人都知道,主类上的@SpringBootApplication包含@EnableAutoConfiguration,它会去读spring.factories或AutoConfiguration.imports文件里注册的配置类。我在这部分没有做深度的二次开发,但利用Spring Boot的@ConfigurationProperties做了一套自定义参数绑定,效果很好。

前面提到的基础参数表sys_config,我写了一个配置类:

@Component @ConfigurationProperties(prefix = "bus.config") public class BusConfig { private int expireMinutes = 15; private String servicePhone = "12328"; private int maxAdvanceDays = 3; // getter/setter 省略 }

然后在service里注入这个Bean,读取参数就直接用。参数需要动态修改时,我在管理端提供一个编辑接口,把新值写入sys_config表,同时更新这个Bean的内存值。

这里我要吐槽一个Spring Boot初学者容易犯的错:把@ConfigurationProperties用在Controller上,这种做法虽然也能跑,但会把配置逻辑和接口业务混在一起,后面根本没法维护。配置类就应该单独一层,业务代码只通过getter拿值,要改数据源就改配置类,要改业务规则就改service,互不影响。

再往下说,自动装配的原理其实不复杂,但很多人面试被问“为什么Spring Boot能自动配置”就卡住。我自己的理解是:Spring Boot默认把Tomcat、数据源、MyBatis这类组件都整理成一个个AutoConfiguration类,每个类上都有@ConditionalOnMissingBean这类条件注解,你只要往容器里手动放一个自定义Bean,自动配置的那个就自动退让。这也是为什么我们在pom里引了spring-boot-starter-data-redis,但没写任何Bean配置,RedisTemplate就自动能用。

3. 实操过程:从0到1搭起来的完整流程

3.1 项目初始化与依赖版本选择

创建项目我用的是Spring Initializr,但专门没在IDEA里用内置的Spring Boot 3.2模板,而是直接选Spring Boot 2.7.18。原因很实际:公司技术栈其他老项目都是JDK 8和Spring Boot 2.x,如果我一上来用JDK 17和Spring Boot 3.x,会导致打包部署环境和现有运维脚本全部不兼容。另外,MyBatis官方starter在2.x版本下的适配最稳。

我在pom.xml里放的依赖是这样的:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>mysql-connector-j</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

这里有个特别需要注意的点:Spring Boot 2.7.18默认连接的MySQL驱动是mysql-connector-j,但一些教程还在让你引入mysql-connector-java,这两个其实是指向同一份代码,只是坐标不同。引入老坐标虽然也能跑,但IDEA会提示版本过低,而且部分新功能不可用。为了避免新手抄错,我统一用新坐标。

Java版本我选的JDK 8。虽然JDK 17已经出来好多年,但很多乡镇客运公司的服务器还是CentOS 7,自带OpenJDK 8,我用JDK 17编译的jar包放上去直接跑不起来。如果项目是从零开始,并且能完全掌控服务器环境,那用JDK 17当然更好;但在系统集成场景里,兼容旧环境的价值远大于新语法特性带来的那点便利。

3.2 核心数据库表与实体类设计

我直接给出几张核心表的建表SQL,你在自己项目里可以直接改表名复用。

CREATE TABLE `bus_line` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `line_name` varchar(100) NOT NULL COMMENT '线路名称', `start_station_name` varchar(100) NOT NULL, `end_station_name` varchar(100) NOT NULL, `base_fare` decimal(8,2) DEFAULT '0.00', `status` tinyint(4) NOT NULL DEFAULT '1', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='农村客运线路表';
CREATE TABLE `bus_schedule` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `line_id` bigint(20) NOT NULL, `depart_date` date NOT NULL, `depart_time` time NOT NULL, `arrive_time` time DEFAULT NULL, `vehicle_id` bigint(20) DEFAULT NULL, `driver_name` varchar(50) DEFAULT NULL, `driver_phone` varchar(20) DEFAULT NULL, `total_seats` int(11) NOT NULL DEFAULT '19', `status` tinyint(4) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `idx_line_date` (`line_id`, `depart_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='班次表';
CREATE TABLE `bus_schedule_station` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `schedule_id` bigint(20) NOT NULL, `station_id` bigint(20) NOT NULL, `station_order` int(11) NOT NULL COMMENT '停靠顺序,从1开始', `plan_arrive_time` time DEFAULT NULL, `plan_depart_time` time DEFAULT NULL, `actual_arrive_time` time DEFAULT NULL, `status` tinyint(4) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `uk_schedule_station` (`schedule_id`, `station_order`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='班次停靠站点明细';

实体类我偷懒用了Lombok的@Data,字段和表字段保持一致。这里有个小坑是MySQL里bus_schedule表的status字段命名,和MyBatis的自动驼峰转换没有任何冲突,但你如果在代码里写Status这种不规范命名,MyBatis的驼峰映射就会失效。我统一把小写字母和下划线作为数据库字段标准,实体字段用驼峰,二者能自动匹配。

3.3 业务层与接口层实现示例

以乘客端查询班次列表为例,Controller返回统一结构Result<T>,这个类很简单,就是code、msg、data三个字段。

@RestController @RequestMapping("/api/schedule") public class ScheduleController { @Autowired private ScheduleService scheduleService; @GetMapping("/list") public Result<PageResult<ScheduleDTO>> list(ScheduleQuery query) { return Result.success(scheduleService.querySchedules(query)); } }

查询条件接收类我觉得用普通POJO就行,不需要在Controller参数上加奇怪注解。

@Data public class ScheduleQuery { private Long lineId; private Long fromStationId; private Long toStationId; private LocalDate departDate; private String timeRange; // morning/afternoon/evening private int pageNum = 1; private int pageSize = 10; }

MyBatis的Xml里写的是动态SQL,三表关联查询。为了不让SQL查询结果映射到DTO时出现多层嵌套,我直接用Map放中间结果,然后在service层组装成DTO。农村客运数据量不大,这种麻烦写法性能完全够,但代码可读性比纯SQL映射好很多。

public PageResult<ScheduleDTO> querySchedules(ScheduleQuery query) { if (query.getFromStationId() == null || query.getToStationId() == null) { throw new BizException("请选择起终点站点"); } List<ScheduleDTO> list = scheduleMapper.selectScheduleList(query); return PageResult.of(list, query.getPageNum(), query.getPageSize()); }

分页我用了PageHelper,用法就一句话。我不推荐在Mapper里手写limit #{offset}, #{pageSize},因为那样统计总数和列表查询要写两遍SQL,很容易出现总数和列表参数不一致的Bug。

3.4 前端Vue打包后放进Spring Boot的实际操作

Vue前端我用的是Vue 3 + Element Plus,开发时通过代理转发请求到后端,上线时我没有单独部署Nginx,而是把Vue打包后的静态文件直接放进Spring Boot的src/main/resources/static目录。这样做的好处是运维少维护一个服务,整个系统一个jar包就能跑。

具体操作步骤:

npm run build

构建完的dist目录里会有index.html、static文件夹等,把dist里的所有东西复制到src/main/resources/static根目录下。这时再去访问http://localhost:8080/,就能看到前端页面。

第一次这样做时会遇到两个问题。第一个是刷新页面404,因为Vue Router如果开了history模式,后端没有配置转发规则。解决办法有两个:一是前端改用hash模式,URL路径里带个#,不做服务端路由处理;二是在后端写一个转发Controller,把非/api开头的路径都转发到forward:/index.html。我图省事直接用了hash模式,上线后没人会在意地址栏好不好看。

第二个问题是静态资源缓存。前端每次发布后,浏览器里旧的JS文件会被缓存住,导致页面更新不及时。我在application.yml里加了静态资源缓存控制,发布时手动清理浏览器缓存。如果是正式环境,我建议引入一个版本号参数,每次发布后改一下请求地址的参数,简单粗暴但有效。

我贴一下application.yml的关键配置:

server: port: 8080 spring: mvc: static-path-pattern: /** resources: static-locations: classpath:/static/

因为默认就是classpath:/static/,所以这段配置其实可以不加,但显式写出来能让团队里不熟悉的人一眼看懂前端文件放哪。这也是我在项目文档里坚持留下来的原因。

4. 常见问题与排查经验实录

4.1 Spring Boot版本太高引起的javax/jakarta包名坑

这个坑我猜遇到的人不少。我用IDEA新建项目时,默认选了Spring Boot 3.2.0,结果项目里引入的很多老依赖全部报错,最典型的是javax.servlet包找不到,变成了jakarta.servlet。这是因为Spring Boot 3.x把Java EE的命名空间从javax改成了jakarta,很多第三方库如果还在用javax,编译时根本过不了。

我当时的做法是直接把Spring Boot版本降到2.7.18。如果项目是非用3.x不可的,那你必须把所有依赖一起升级到适配jakarta的版本,比如MyBatis Starter要用3.0.3以上。这个改动看似简单,但会牵连到shiro、redis连接池、swagger等多个依赖,成本反而高。

对于毕业设计或者企业内部小型系统,我的建议是:用Spring Boot 2.7.x,不要盲目追求新版本。新版本带来的特性你大概率用不上,但兼容性问题会实实在在消耗你的时间。

4.2 MyBatis SQL多表查询与分页翻车

我在查询班次列表时写了三表关联SQL,用了PageHelper分页。第一次跑测试时发现数据总数对不上,仔细排查后才发现原因:PageHelper会在执行查询前自动拦截并生成count语句,但如果SQL里包含了group by或子查询,生成的count语句很可能不准确。

我当时的SQL是:

SELECT s.id, s.depart_time, ss1.station_name AS from_station, ss2.station_name AS to_station, COUNT(o.id) AS sold_count FROM bus_schedule s LEFT JOIN schedule_station ss1 ON ... LEFT JOIN schedule_station ss2 ON ... LEFT JOIN bus_order o ON ... GROUP BY s.id, ss1.station_name, ss2.station_name

PageHelper生成的count是SELECT count(0) FROM (原来的SQL),但原来的SQL里有GROUP BY,count出来的却是分组后的行数,而不是原始记录数。这个逻辑在MySQL里有时能蒙对,但数据一多就乱了。

后来我改用MyBatis的PageInterceptor自定义实现,或者干脆在SQL外套一层子查询手动分页。最稳妥的办法是不要在复杂SQL上依赖PageHelper自动count,而是用两个独立方法:queryList和queryCount,自己控制参数。

4.3 跨域、Session与基于Token的登录鉴权

前端开发时我用Vue的devServer.proxy解决跨域,但打包放进Spring Boot后就不再跨域了,因为前后端同源。可如果管理端和乘客端部署在不同域名下,跨域还是躲不掉。

我最早用Session做了登录,结果发现跨域请求时Cookie带不上。后来改成JWT Token方案,登录接口返回一个Token字符串,客户端存到localStorage,每次请求在Header里加Authorization: Bearer xxx。后端写一个拦截器,解析Token并校验有效期,同时把用户信息放到ThreadLocal里,方便业务层获取当前操作人。

写拦截器时有个细节:

@Component public class JwtTokenInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } String token = request.getHeader("Authorization"); // 校验逻辑 return true; } }

静态资源请求也要过滤掉,不然访问前端页面会被拦住。我通过在addInterceptors里配置excludePathPatterns("/static/**", "/index.html")解决。

4.4 定时任务重复执行与并发问题

前面讲了用@Scheduled做自动排班,但单机运行没问题,一旦部署两个实例做负载均衡,同一条凌晨2点的任务会在两个机器上同时跑,产生重复数据。我在正式环境用了定时任务加分布式锁的方案:执行任务前先往Redis里写一个带过期时间的key,只有写入成功的实例才能继续执行,另一个实例直接跳过。

代码大概是:

@Scheduled(cron = "0 0 2 * * ?") public void generateTask() { boolean locked = redisTemplate.opsForValue() .setIfAbsent("task:schedule:gen", "1", 5, TimeUnit.MINUTES); if (!locked) { return; } try { doGenerate(); } finally { redisTemplate.delete("task:schedule:gen"); } }

这里要特别注意锁的过期时间必须大于任务最大执行时间。我前面第一次设了10秒,结果任务跑了30秒,第二个实例在锁过期后又进来跑了一遍,数据库立刻出现重复班次。后来改成5分钟,问题解决。

4.5 宝塔部署Spring Boot服务的推荐流程

客户现场服务器装的是宝塔面板,我一开始直接上传jar包,在宝塔的“Java项目”管理里设置启动命令,效果可以但手动操作步骤多。后来我改用Docker部署,把构建和分发都简化了。

在项目根目录写一个Dockerfile:

FROM openjdk:8-jre-slim COPY target/rural-bus-system.jar /app/app.jar WORKDIR /app EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]

构建镜像:

docker build -t rural-bus-system .

宝塔装好Docker后,在Docker管理器里创建容器,映射80端口到8080,再把宿主机的/data/bus挂载到容器里,用来存日志和上传的图片。这样后期升级只重新构建镜像,容器配置完全不变。

用Docker部署要注意时间问题,容器默认UTC时区,定时任务执行时间和本地差8小时。我在Dockerfile里加上:

ENV TZ=Asia/Shanghai

这个不写的话,凌晨2点排班任务会变成北京时间上午10点才跑,轻则延后,重则影响当天的数据生效。

从设计到上线这套系统,整体过程走了大概一个月,其中有三天时间专门花在处理MyBatis分页和Spring Boot版本兼容上。现在我仍然觉得,农村客运这种传统业务场景,技术本身不是瓶颈,真正难的是把模糊的业务规则翻译成数据库表和接口逻辑。比如“区间余票”这个概念,不跟司机聊上两小时,永远想不到座位重叠的问题。

如果后面要扩展这个项目,我会先把第三方支付接进来,把线上售票和线下补票的账目完全打通。另外可以考虑给乘客端加一个实时位置跟踪功能,但这需要车辆GPS设备配合,属于硬件投入的问题了。至少在当前这个版本里,稳定运行了几个月的服务已经说明,Spring Boot认认真真做业务管理系统,是能扛住事的。

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

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

立即咨询