ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Boot+Vue农村客运服务系统:从需求拆解到部署实战

Spring Boot+Vue农村客运服务系统:从需求拆解到部署实战 先聊个实在话农村客运服务系统听起来不像电商、不像外卖那么热门但它真做起来复杂度一点不比城市公共交通低。站点分散、班次不固定、有些跑线司机年纪偏大、购票还停留在上车付现金的阶段需求一收上来很容易让人不知道从哪下手。我这次做的这个基于Spring Boot的农村客运服务系统核心思路是把运营侧和乘客侧分开处理运营侧管线路、车次、票务、司机车辆调度乘客侧提供查询班次、在线购票、退票和扫码验票。技术栈没有整花活就是Spring Boot MyBatis-Plus MySQL Redis这套主流组合前端用Vue做了管理端和乘客端。下面我把整个项目从需求拆解、表结构设计、核心接口实现到联调部署踩过的坑逐段写清楚给正在做同类系统或者毕业设计的朋友一份能直接照着干的参考。1. 项目概述与需求拆解1.1 农村客运和城市公交到底差在哪很多人一听农村客运就以为是简化版公交系统这其实是最大的误解。城市公交线路密集、班次间隔短、客流相对稳定系统关注的是车辆到站时间和刷卡数据农村客运正好反过来线路长、站点稀、班次少而且客流有明显的赶集潮和节假日潮。举个例子一条从镇中心到山坳村的线路可能一天只有早上、中午两班车错过一班就要再等半天乘客对这班车今天到底开不开、还有没有票的需求比城市乘客强烈得多。这种业务特点决定了系统不能照搬城市公交的模型。班次管理必须支持每天动态调整遇到恶劣天气或者车辆故障要能快速取消、合并班次售票也不能只靠线上因为很多乘客尤其是老年人并不会用手机下单系统还得兼容司机人工售票和线下补票的场景。所以我在需求梳理阶段就确定了八个字线上预约、线下兜底。1.2 用户角色与核心功能根据实际运营场景我把系统用户划分成了三类角色功能模块围绕这三类角色展开避免一开始就堆功能导致开发量失控。角色核心需求对应的功能模块系统管理员维护线路、站点、车辆、司机等基础资料基础数据管理调度员/运营人员排班、调班、查看实时售票情况、统计分析班次调度管理、运营统计乘客查询班次、购票、退票、验票乘客端服务基础数据管理包含线路信息、站点信息、车辆档案和司机档案这些是系统的地基。调度管理是核心业务模块负责把线路、时间、车辆、司机四个要素组合成具体的班次并且要处理停运、并班、加车这些异常情况。乘客端提供班次查询、余票查看、在线下单、支付、退票以及乘车码核验。运营统计模块我放在了比较靠后的优先级但它是后期最有价值的部分。通过统计每条线路的售票人数、上座率、退票率可以辅助运营方调整班次密度。比如一条线路连续两周上座率不足30%那就该考虑把一天三班改成两班了。2. 技术选型与整体架构方案2.1 Spring Boot版本为什么别一上来就选最高版本技术选型阶段我确实纠结过Spring Boot的版本毕竟网上最新教程都在推3.x。但实际做下来我还是选了Spring Boot 2.7.18。原因很现实首先3.x要求JDK 17起步很多开发机和生产服务器还停留在JDK 8升级JDK本身就有成本其次Spring Boot 3.x把javax包迁移到了jakarta包网上大量旧教程和代码片段不能直接复用遇到问题排查资料的难度明显增加再有就是一些配套组件比如Redis客户端、Swagger相关starter在3.x下的兼容写法变化不小碰一次就要折腾半天。Spring Boot 2.7.18是2.x系列的最后一个版本它在JDK 8、11、17上都能跑生态成熟MyBatis-Plus、Spring Security等主流组件兼容性最稳。对于一个需要稳定交付、快速上线的业务系统成熟稳定永远比版本新更重要。如果项目周期紧或者生产环境不强制要求新特性我认为2.7.x是当前最稳妥的选择。2.2 后端技术栈与前端方案最终确定的技术栈如下技术组件选型说明后端框架Spring Boot 2.7.18核心框架负责接口提供与业务处理ORM框架MyBatis-Plus 3.5.x单表CRUD不用写SQL复杂统计用注解SQL数据库MySQL 8.0存储业务数据缓存Redis缓存班次余票、分布式锁控制并发购票权限认证Spring Security JWT管理端和乘客端统一身份认证接口文档Knife4j基于Swagger增强调试接口很方便前端Vue 3 Element Plus管理端和管理后台乘客端H5移动Web页面适配手机浏览器扫码即可打开前端没有做成独立的Vue项目部署到单独服务器而是用Vite构建后把dist目录下的静态文件直接拷贝到Spring Boot的resources/static目录下。这样后端一个可执行jar包就同时包含接口和页面部署时只需要一个进程在乡镇级别的服务器上运维成本非常低。这个取舍后面部署阶段我会展开讲。2.3 项目整体结构项目采用Maven多模块思路但为了减少复杂度我实际用的是单模块多包结构按业务域分包com.rural.bus ├── config // 配置类Redis、安全配置、跨域、MybatisPlus ├── common // 通用返回体、全局异常、工具类 ├── module.system // 管理员、角色、登录 ├── module.base // 线路、站点、车辆、司机 ├── module.schedule // 班次、余票管理 ├── module.order // 购票、退票、订单 ├── module.passenger// 乘客端接口 └── module.statistic// 运营统计包结构按业务域划分比按技术层controller/service/mapper划分更容易维护。后面加功能不用到处找代码一个业务域里的Controller、Service、Mapper都放在一起。3. 数据库设计与核心表结构3.1 线路、站点与线路站点关系农村客运站点和线路的关系不是一对一的一条线路经过多个站点一个站点也可能在多条线路上。我设计了line、station、line_station三张表。line表保存线路名称比如镇客运站—石桥村、线路类型、里程、预计总耗时station表保存站点名称和区域信息line_station保存线路与站点的关联并且记录站序、距离上一站的间隔时间、票价阶梯起点标记。票价计算在客运里是个容易做复杂的地方。我的做法是给line_station表加一个price_segment字段表示从起始站到当前站累计票价。比如乘客从第二站坐到第四站票价就是站点4的累计票价减去站点2的累计差价。这个逻辑虽然原始但符合农村客运大多数按区间计价的习惯好理解也好维护。3.2 车辆、司机与班次表车辆和司机是调度资源用bus表和driver表维护。bus表记录车牌号、车型、额定座位数、运营状态driver表记录姓名、手机号、从业资格证编号、常驻线路。车辆状态我加了一个枚举字段正常、维修、停运三态调度时只能选择正常状态的车辆避免排班员手动误选。班次表scheduled_trip是核心业务表字段大致如下字段名说明id主键line_id关联线路IDbus_id关联车辆IDdriver_id关联司机IDdepart_date发车日期depart_time发车时间arrive_time预计到达时间sale_status售票状态售票中/已停售/已发车/已取消fixed_price全程票价便于列表展示operator_id排班操作员这里有个设计要点班次表里冗余了line_id和depart_date、depart_time。有人会问为什么不通过联表查询获取线路和时间冗余字段是为了让余票查询、订单列表这些高频查询少做几次关联减轻数据库压力。3.3 订单、乘车人与座位扣减订单表order_info存订单主信息包括订单号、乘客账号、班次ID、购票数量、总金额、支付状态、订单状态待支付/已支付/已退票/已失效。乘车人需要实名信息为了简化操作我允许用户在下单时填写乘车人姓名和身份证号这个信息在验票时核对。座位扣减是购票系统最容易出并发问题的地方。我的方案是引入一张sold_seat表每售出一张票就插入一条记录字段包括订单ID、班次ID、座位号。车上座位号可以重复使用但同一班次内不能重复查询余票时用班次总座位数减去sold_seat表统计的已售数量得到余票数。这张表同时承担了哪些座位被占的实时校验功能锁座位时根据它判断。4. 核心功能模块的实操实现4.1 班次查询接口的实现思路乘客端第一个入口就是查班次。我的接口设计是传入出发站点ID、到达站点ID、出发日期后端先根据两个站点ID找到所有同时包含这两个站点的线路再根据线路ID去trip表查找当天的班次最后带上余票数返回。为了提升性能这个接口没有直接实时统计余票而是先查Redis缓存。缓存键设计成trip:remain:{tripId}值就是实时余票数每次购票成功或退票成功时同步刷新。这样高频的班次列表查询不会压到MySQL上数据库的压力就控制住了。如果Redis里没有缓存再回源到数据库统计并回写缓存缓存过期时间设置为10分钟。4.2 购票锁座一个需要认真对待的并发问题购票是整个系统中并发风险最高的环节。设想一下一辆车只剩最后一张票两个乘客同时下单不加控制就会出现一票多卖。网上很多简单实现是先查余票数、判断大于0、再执行insert这种检查再操作的方式在高并发下必然出问题。我采用的是数据库悲观锁方案。下单时首先开启事务然后执行一条SELECT ... FROM sold_seat WHERE trip_id ? AND seat_no ? FOR UPDATE对目标座位记录加行级锁。如果查不到记录说明座位还没被卖出去执行insert如果查到了说明已经被占直接抛业务异常提示换座。这里关键点是sold_seat表必须为(trip_id, seat_no)建立唯一索引从数据库层面兜底防止重复插入。还需要处理一个边界场景乘客一次买多张票比如买三张。如果逐张插入某一站失败之前插入的已经占用再回滚事务释放即可。由于整个购票操作都在一个事务内任何一步失败都整体回滚不会出现部分成功、部分失败的情况。4.3 未支付订单的定时取消乘客下单后如果不支付座位不能一直被占着。我用了Spring Boot自带的定时任务方案在启动类上加EnableScheduling写一个任务类每两分钟扫描一次超时15分钟未支付的订单将其置为失效状态同时释放已占用的座位记录。这里需要一个小细节扫描条件要加上create_time NOW() - INTERVAL 15 MINUTE AND status pending并且分批处理每次最多处理200条避免大批量数据积压导致性能抖动。定时任务和应用共用一个进程在系统规模不大时完全够用不需要单独引入分布式任务调度框架。4.4 司机端扫码验票验票场景在信号不太好的乡镇地区很常见所以核销逻辑一定要轻量。我给每张票生成一个唯一票据编码其实就是订单号的子串加随机数同时生成一个QR码图片。司机打开乘客端的司机验票入口实际上是一个经过鉴权的H5页面扫乘客手机上的二维码后端接收二维码里的票据编码查询订单状态如果是已支付就改为已核销。考虑到离线场景完整的离线验票可以做成司机端本地缓存白名单但这个复杂度较高当前版本没有做而是把接口做得足够快、足够简单让司机在弱网环境下也基本可以完成扫码操作。同时在开发时我和运营方明确了这个限制让驾驶员提前下载二维码图片到手机相册以备信号盲区时人工核对票据编码。4.5 全局异常与统一返回体后端接口我都统一封装了返回体ResultT包含code、message、data三个字段。controller里不再直接返回裸数据所有业务异常继承一个BizException通过全局RestControllerAdvice捕获转成对应的错误码和提示信息。这个做法的价值在联调阶段体现得很明显。前端同学不用靠HTTP状态码猜错误后端只要按照约定把业务错误码定义清楚比如1001表示班次已取消、1002表示座位已被占、1003表示订单超时未支付前端拿到code直接弹对应提示。排查问题时日志里也有完整的异常链路效率高出不少。5. 开发联调与部署阶段的高频坑5.1 IDEA里改端口没生效是怎么回事Spring Boot项目在IDEA里启动后默认端口是8080你可以在application.yml里写server.port改端口。但有几次我改了配置重启端口还是以前那个查了半天发现IDEA的Run Configuration里单独设置了VM options: -Dserver.port8081这个参数的优先级高于配置文件所以修改配置文件不会生效。解决办法有两个要么改配置里的参数要么直接删除Run Configuration里的JVM参数不设置时以yaml配置为准。还有一个和端口相关的坑本地同时启动了多个项目实例占用了同一端口启动报Port already in use。排查时用netstat -ano | findstr 端口号找到占用进程的PIDWindows下再用taskkill /PID xxx /F结束进程。这问题遇到一次就够了要长记性。5.2 Vue打包之后放进Spring Boot的正确姿势管理端和乘客端都是Vue项目开发阶段前端用Vite代理后端独立跑在8080联调没问题。但部署到测试环境时为了省事我把两个前端都打包后放进了后端的static目录。管理端放在static/admin乘客端放在static/passenger。这里有一个特别容易踩的坑Vue Router默认是history模式刷新页面时路径变成了/admin/dashboard后端没有这个路由映射直接返回404。解决办法有两个第一前端打包时把createWebHistory改成createWebHashHistoryURL会变成/admin/#/dashboard模式刷新不会404第二后端加一个WebMvcConfigurer把/admin/**和/passenger/**的非接口路径转发到对应的index.html。我实际选了第二种因为路径更好看而且加一个配置类也不复杂。5.3 Jackson序列化Long类型精度丢失和日期格式用Vue前端拿后端返回的Long类型ID时如果ID大于JavaScript的最大安全整数前端拿到的尾数会变导致后续操作定位不到正确记录。解决办法是在Jackson配置中为Long类型统一添加ToStringSerializer把所有Long序列化为字符串。这个配置只需要在Jackson2ObjectMapperBuilderCustomizer里加一个自定义序列化器。日期格式也是联调时容易出问题的点。Spring Boot默认序列化LocalDateTime的格式是2025-05-06T10:30:00不是前端想要的yyyy-MM-dd HH:mm:ss。我在application.yml里统一配置了spring.jackson.date-format并配合JsonFormat注解确保所有日期字段按约定格式输出。5.4 跨域问题生产环境几乎没有开发环境总会遇到开发阶段前端跑在5173后端跑在8080前端调用后端必然触发跨域。当时在后端加了一个CorsFilter配置类允许所有来源、所有方法并把前端请求放行。这里要提醒一下生产环境前后端已经打包在一起没有跨域问题默认的关闭跨域是最安全的选择。测试时临时开着允许所有来源没问题上线前一定要把跨域配置收紧否则任何页面都能调用你的接口安全风险很大。5.5 安全认证与接口权限接口权限一开始我想得很细后来实践过程中简化了。管理员和乘客两个体系分别用JWT做认证管理端的接口要求请求头携带Authorization: Bearer token通过Spring Security的过滤器链做校验。乘客端所有查询接口放行但购票、退票、查个人订单的接口必须登录。实际开发中我自定义了一个RequireRole注解在Controller方法上标注需要的角色类型通过AOP切面校验比在Spring Security配置里写十几行权限规则更直观也更容易让新接手的人看懂。安全性上JWT密钥放在配置文件里用环境变量注入不硬编码进代码仓库这点务必要坚持。6. 我的实操总结与后续扩展思考整个项目从头到尾走下来我最深刻的感受是这类业务系统的开发难点往往不在技术而在对业务不确定性的处理。农村客运不像城市公交那样规律班次可能因为赶集临时加车、因为天气临时停运乘客可能提前一天买票也可能上车后才补票。系统设计必须把弹性留出来比如班次状态字段上多加几个状态、订单状态之间允许合理的流转这些看似不起眼的设计才是系统能不能真正用起来的关键。技术层面如果再让我重新做一次我会考虑两点改进。第一是把座位管理做成更灵活的区间锁座模型现在的方式是整张票占用全程座位但实际上有乘客只坐中间两站座位上其他区间还能复用这个优化对提高农村客运上座率帮助很大。第二是引入简单的消息推送能力班次取消和变动时通过公众号模板消息或者短信触达已购票乘客避免乘客白跑一趟。这两个方向扩展起来业务价值很明显代码层面也不算复杂。最后分享一个小技巧这种带前端页面的Spring Boot项目打出来的jar包动辄几十MB部署到乡镇一级的服务器时经常遇到带宽低、网速慢的问题。我的经验是部署前先用strip-nondeterminism或者简单压缩的方式处理一下启动脚本和jar包然后用systemd搭建开机自启服务配合日志轮转基本上一次部署就能稳定跑很久。希望这篇分享能帮你少走一些弯路。
返回列表