微信小程序+SSM客运自助售票系统源码拆解与二开指南
简介一套面向Java Web与小程序开发者的SSM客运自助售票小程序完整源码包内含整站前后端代码、SQL脚本及毕业设计论文。项目采用Spring、SpringMVC、MyBatis经典框架组合前端按微信小程序规范实现车次查询、座位选择、支付等购票流程后端提供稳定API接口数据库脚本遵循第三范式并考虑索引与缓存优化可满足快速迭代与性能需求。资源共1276个文件约14.87MB涵盖vue、js、wxml、wxss等页面逻辑文件java后台服务、json/xml配置、sql数据脚本、docx论文及bat启动脚本结构清晰便于部署学习。目前已有77人学习下载。适合正在做相关课题或希望掌握小程序全栈架构的读者通过源码阅读、脚本导入和文档对照能快速理解从架构设计到前后端协作的完整实现路径并可直接用于二次开发与功能扩展。1. 微信小程序SSM的客运自助售票这套源码真正的价值在哪朋友把一份「客运自助售票小程序」的毕业设计源码丢给我问能不能直接拿去做个小公司的内部售票系统。我花了一晚上把代码和SQL脚本理完结论是这类「微信小程序 SSM」的组合功能闭环完整、代码量小、二开路径清晰但能否落地取决于你先看懂那张订单表而不是先打开Controller。本文以这套整站源码为对象把数据库、SSM后端、小程序前端的实现逻辑与踩坑点逐层拆开帮你判断值不值得买、买回来怎么改。2. 先读SQL脚本再谈功能客运数据表藏着业务边界2.1 为什么第一步是看表而不是看代码我不是先跑代码的人。拿到「整站源码sql脚本论文.zip」第一件事永远是打开SQL脚本看建表语句。原因很简单SSM项目逃不出Controller-Service-Mapper三层业务规则最后都落在表结构和SQL上。这张客运自助售票表怎么设计直接决定了你能不能改出退票、改签、座位选择等功能。常见做法是把表拆成四张核心表用户表account、班次表bus、订单表ticket_order、站点/线路表route或station。注意这里有个关键点班次表里通常不会存座位图而是存「余票数」字段比如left_ticket。这意味着这套系统卖的是「无座票」或者「按数量扣减的票」不是「按座位号选座」。CREATE TABLE bus ( bus_id INT PRIMARY KEY AUTO_INCREMENT, route_id INT NOT NULL COMMENT 关联线路ID, depart_station VARCHAR(50) NOT NULL COMMENT 出发站点, arrive_station VARCHAR(50) NOT NULL COMMENT 到达站点, depart_time DATETIME NOT NULL COMMENT 发车时间, arrive_time DATETIME NOT NULL COMMENT 到达时间, total_ticket INT DEFAULT 50 COMMENT 总票数, left_ticket INT DEFAULT 50 COMMENT 余票数, ticket_price DECIMAL(8,2) NOT NULL COMMENT 票价, status TINYINT DEFAULT 1 COMMENT 1启用 0停用 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段建表语句反映两个信息第一线路信息被拆到了route表bus表通过route_id做关联这是合理的同一条线路可以让多辆车跑第二没有座位维度total_ticket和left_ticket都是整数扣余票就是UPDATE bus SET left_ticket left_ticket - 1。这类设计足够应付毕业设计也足够应付小客运站的卖票需求但如果你想卖「具体座位号」这张表不够用得加一张seat维度表。再看订单表CREATE TABLE ticket_order ( order_id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 业务订单号, user_id INT NOT NULL COMMENT 用户ID, bus_id INT NOT NULL COMMENT 班次ID, ticket_count INT DEFAULT 1 COMMENT 购票张数, total_price DECIMAL(8,2) NOT NULL COMMENT 总价, status TINYINT DEFAULT 0 COMMENT 0待支付 1已支付 2已退票 3已检票, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME NULL COMMENT 支付时间, INDEX idx_user (user_id), INDEX idx_bus (bus_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;order_no加了UNIQUE约束这是好习惯防止并发下单时产生重复订单号。status字段用TINYINT表示状态机0到3四个状态配合pay_time记录支付时间。注意没有refund_time说明退票这个动作被简化成「改状态」如果你要统计退票时效得自己加上。2.2 从账号表看用户体系openid才是真正的钥匙用户表在客运售票系统里比想象中简单因为小程序端不需要用户名密码微信登录拿到openid就够了CREATE TABLE account ( user_id INT PRIMARY KEY AUTO_INCREMENT, nickname VARCHAR(50) DEFAULT , phone VARCHAR(20) DEFAULT , openid VARCHAR(64) NOT NULL UNIQUE, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;你会在表里看到openid字段它是微信用户的唯一标识。后端登录接口拿到微信登录凭证后会换取openid然后查表如果存在就直接登录不存在就insert一条新记录。这套逻辑在毕业设计和中小型系统中非常通用。字段phone留空没问题因为小程序里获取手机号需要企业认证个人开发者用不了。这是客运自助售票小程序最常被忽略的边界乘客要凭手机号接收发车通知那得接短信服务或者让用户在小程序里自己填手机号。设计时要留这个口子别等上线了才发现没地方存手机号。2.3 余票扣减的并发问题表结构解决不了的坑票务系统绕不开并发扣减比如同一个班次只剩最后一张票两个人同时下单。如果你在Service层写成「查询余票→判断0→扣减」在高并发下会超卖。Override Transactional(rollbackFor Exception.class) public boolean createOrder(CreateOrderDTO dto) { Bus bus busMapper.selectById(dto.getBusId()); if (bus.getLeftTicket() dto.getTicketCount()) { throw new BizException(余票不足); } busMapper.decreaseTicket(bus.getBusId(), dto.getTicketCount()); // 插入订单... return true; }这段看起来没问题但两个请求同时读到left_ticket1时你判断两次都通过扣减后变成-1。解决办法有两个层面第一SQL层面加条件WHERE left_ticket #{count}让数据库兜底第二悲观锁SELECT ... FOR UPDATE锁行。毕业设计源码通常只会做到第一层就是decreaseTicket的SQL里带上判断条件UPDATE bus SET left_ticket left_ticket - #{count} WHERE bus_id #{busId} AND left_ticket #{count}用更新行数判断是否扣减成功如果返回0说明余票不够。这是不加锁也能防超卖的最小可行方案也是这类项目里最常见、最推荐的做法。一上线就想用Redis分布式锁的是过度设计四张表的项目扛不住那个复杂度。2.4 SQL脚本导入三个容易被忽略的坑导入sql脚本通常用Navicat或者命令行。命令行导入时要注意编码问题很多毕业设计的SQL脚本是用Windows记事本写的默认GBK编码直接source导入会出现中文乱码mysql -u root -p --default-character-setutf8mb4 bus_system bus_system.sql第二个坑是sql脚本里没写CREATE DATABASE你得手动建库。常见做法是这样CREATE DATABASE IF NOT EXISTS bus_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE bus_system;第三个坑是MySQL版本兼容性。这份源码如果用的是MySQL 5.7脚本里可能会用到datetime默认值CURRENT_TIMESTAMP这个MySQL 5.5以上就支持但如果脚本里出现了utf8mb4_unicode_ci之类排序规则MySQL 5.6以下不认建议直接用MySQL 5.7或8.0。3. SSM后端怎么拆登录态、下单事务与支付模拟3.1 三层架构与包结构先定位再动手SSM是SpringSpringMVCMyBatis的组合源码的包结构基本是固定套路com.example.bus ├── controller // 接口层只做参数接收和返回 ├── service // 业务逻辑层事务边界在这里 ├── dao // MyBatis的Mapper接口 ├── entity // 数据库实体类 ├── dto // 接口出入参对象 ├── interceptor // 登录拦截器 └── common // 统一返回结果、异常处理、工具类改代码时记住一个原则Controller里别写业务逻辑。很多毕业设计为了赶工把余票判断写在Controller里导致事务失效。判断一个SSM项目是否规范就看Controller里有没有直接调用xxxMapper。3.2 小程序登录态token是一张通行证微信小程序的登录流程是wx.login()拿到code → 发给后端 → 后端调用微信接口换取openid → 后端生成token返回小程序 → 小程序后续请求都带token。后端通常用一个拦截器统一验证tokenpublic class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (StringUtils.isEmpty(token)) { throw new BizException(401, 未登录); } // 解析token得到userId放入ThreadLocal或request域 Integer userId JwtUtil.parseToken(token); if (userId null) { throw new BizException(401, 登录已过期); } request.setAttribute(currentUserId, userId); return true; } }两个参数是实际调优重点token有效期设置多长毕业设计里常见做法是7天因为小程序用户不会频繁登录太短会烦人。token里存什么只存userId就够了不要存完整用户信息免得token体积大且数据不一致。在SpringMVC里注册拦截器要排除登录和查询接口mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/api/login/ mvc:exclude-mapping path/api/bus/list/ mvc:exclude-mapping path/api/bus/detail/ /mvc:interceptor /mvc:interceptors登录接口和班次查询接口放行其余接口都要登录态。这里有个细节下单接口要不要登录当然要否则别人知道busId就能帮你买票。但很多源码把下单接口漏在拦截器外面这是安全检查时最容易翻车的地方拿到代码后第一件事就是检查interceptor的exclude清单。3.3 下单事务多张表的一致性怎么保证客运购票的下单动作涉及三张表订单表insert一条记录、班次表update余票、可能还有支付记录表insert。这三步必须在一个事务里否则会出现「订单生成了但余票没扣」或者反过来。Override Transactional(rollbackFor Exception.class) public OrderVO submitOrder(OrderRequest req) { // 1. 校验班次状态 Bus bus busMapper.selectByIdForUpdate(req.getBusId()); // 行锁 if (bus null || bus.getStatus() ! 1) { throw new BizException(404, 班次不存在或停运); } // 2. 扣减余票 int rows busMapper.decreaseTicket(req.getBusId(), req.getTicketCount()); if (rows 0) { throw new BizException(余票不足); } // 3. 生成订单号并插入订单 String orderNo OrderNoGenerator.generate(B, bus.getId()); TicketOrder order new TicketOrder(); order.setOrderNo(orderNo); order.setUserId(req.getUserId()); order.setBusId(req.getBusId()); order.setTicketCount(req.getTicketCount()); order.setTotalPrice(bus.getTicketPrice().multiply(new BigDecimal(req.getTicketCount()))); order.setStatus(0); // 待支付 int orderRows orderMapper.insert(order); if (orderRows ! 1) { throw new BizException(订单创建失败); } // 4. 返回订单信息前端跳转支付页 return orderAdapter.toVO(order); }第2步用selectByIdForUpdate加行锁这是悲观锁方案。加了Transactional之后锁要等事务提交才释放所以submitOrder里不能有耗时操作比如调用外部接口或Thread.sleep。这里有一个关键细节JSON序列化时BigDecimal类型前端会变成number精度可能丢失尤其票价是19.90这种。常见做法是后端把价格转成字符串返回或者前端用parseFloat再显示。我在改造中一般会在DTO里把金额字段定义为String显示时直接用下单时再转回BigDecimal。3.4 支付模拟没有微信支付商户号时怎么办毕业设计源码里「支付」两个字通常是模拟的因为真实微信支付需要商户号个人开发者根本没有。模拟支付的做法是订单创建后状态是0待支付前端点击「确认支付」后调一个接口把状态改成1已支付。PutMapping(/order/pay/{orderNo}) public Result payOrder(PathVariable String orderNo, HttpServletRequest request) { Integer currentUserId (Integer) request.getAttribute(currentUserId); TicketOrder order orderMapper.selectByOrderNo(orderNo); if (order null || !order.getUserId().equals(currentUserId)) { throw new BizException(订单不存在); } if (order.getStatus() ! 0) { throw new BizException(订单状态不允许支付); } order.setStatus(1); order.setPayTime(new Date()); orderMapper.updateById(order); return Result.success(); }这是模拟支付接口的核心逻辑校验订单归属→校验状态→改状态→记录支付时间。如果你真想接入真实微信支付替代方案是在pay接口里先调微信支付统一下单API拿到prepay_id再返回小程序端拉起支付面板收到支付回调后修改订单状态。坑在于支付回调和订单状态更新不是一个事务回调可能延迟或丢失需要加一个定时任务对账——这就超出这篇范围了但你要有这个概念模拟支付改两行代码容易接真实支付是个完整的工程。4. 微信小程序端怎么对接后端从请求封装到页面状态4.1 请求封装统一token注入和错误处理微信小程序的网络请求用wx.request但原生写法每个页面都要重复写header和错误处理所以源码里通常有个request.js工具类const BASE_URL http://localhost:8080/api; const request (url, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL url, method: method, data: data, header: { Content-Type: application/json, Authorization: wx.getStorageSync(token) || }, success: (res) { if (res.statusCode 401) { wx.redirectTo({ url: /pages/login/login }); reject(res); } else if (res.statusCode 200 res.data.code 0) { resolve(res.data.data); } else { wx.showToast({ title: res.data.message || 请求失败, icon: none }); reject(res.data); } }, fail: (err) { wx.showToast({ title: 网络异常请检查后端服务, icon: none }); reject(err); } }); }); };这段代码有几个设计点需要说明第一token从wx.getStorageSync读取在header里用Authorization传递Server端拦截器取的是同一个header名第二后端统一返回{code: 0, message: , data: {...}}这种结构前端只认code0是成功避免HTTP状态码和业务状态码混在一起第三401时跳转登录页这是个兜底行为。4.2 班次查询页面日期不准是因为时区问题客运购票的核心页面是「查询班次」用户选出发地、到达地、日期然后拉取列表。这里的日期参数有一个非常经典的坑new Date() 转出来的字符串是 2026-02-14T08:00:00.000Z后端解析LocalDateTime时差了8小时。正确做法是前端格式化好再传function formatDate(date) { const y date.getFullYear(); const m (date.getMonth() 1).toString().padStart(2, 0); const d date.getDate().toString().padStart(2, 0); return ${y}-${m}-${d}; } // 查询班次列表 const dateStr formatDate(this.data.selectedDate); const res await request(/bus/list, GET, { departStation: this.data.departStation, arriveStation: this.data.arriveStation, departDate: dateStr });后端接口接收departDate2026-02-14之后需要把日期转成当天的起始时间范围再查否则数据库里的depart_time是带时分秒的用等值匹配会查不到select idselectByCondition resultTypecom.example.bus.entity.Bus SELECT * FROM bus WHERE depart_station #{departStation} AND arrive_station #{arriveStation} AND depart_time gt; #{startTime} AND depart_time lt; #{endTime} AND status 1 ORDER BY depart_time ASC /selectstartTime和endTime在Service层拼传入的日期加00:00:00和23:59:59。这个边界处理是班次查询接口最常见的改bug现场很多人查不到当天的车次最先怀疑表数据有问题实际上时间范围没拼对。4.3 下单页与订单列表状态驱动的页面渲染小程序端购票流程有四个页面流转班次列表 → 确认订单 → 支付结果 → 订单列表。不要在多个页面间靠wx.navigateTo传一整对象正确的做法是只传orderNo或busId进下个页面再请求一次详情。// 选择班次后跳转确认订单页 goConfirm(e) { const bus e.currentTarget.dataset.bus; // list-item里绑定的数据 wx.navigateTo({ url: /pages/confirm/confirm?busId${bus.busId}ticketCount1 }); }订单列表页有个使用体验优化点下拉刷新要重新请求数据而不是复用页面栈里的旧数据。微信小程序页面的onShow生命周期在navigateBack回来后会触发所以订单支付成功返回列表时列表要在onShow里重新拉取否则状态停留在「待支付」。onShow() { this.loadOrders(); },这类流量小的小程序不需要考虑数据缓存和虚拟列表但onShow触发请求是个好习惯让数据保持最新。如果需要离线展示就把接口返回storage一下下次onLoad先读缓存再静默刷新这是后面还能升级的方向。5. 部署与排查客运小程序从源码到联调的5个真实踩坑5.1 真机预览时接口不通域名白名单卡住现象模拟器里请求后端接口正常手机预览时所有请求全部失败console报url not in domain list。原因微信小程序真机环境强制校验白名单所有请求域名必须在小程序后台配置为合法域名且必须是HTTPS。本地开发的http://localhost:8080自然不在名单里。解决开发阶段在微信开发者工具右上角「详情」→「本地设置」里勾选「不校验合法域名」手机预览时启用「真机调试」而不是「预览」模式。等到上线前把后端接口域名配成HTTPS并加到小程序后台的request合法域名里。注意这个配置修改要小程序管理员审核生效一般有几分钟延迟别刚提交完就急着测。5.2 登录拦截器失效下单接口裸奔现象用Postman直接调下单接口不传token也能下单成功。原因拦截器配置的exclude-mapping范围写大了/api/**包住了下单接口或者拦截器根本没注册。解决拿到代码先全局搜exclude-mapping和WebMvcConfigurer逐个核对哪些接口该放行。放行清单一般只有登录接口和班次查询接口其余一律拦。验证时可以写个最简单的小程序测试页不发token请求一下能用就是大bug。5.3 数据库里的时间比本地快了8小时现象插入订单的create_time比服务器当前时间快了8小时。原因MySQL连接的useUnicodetruecharacterEncodingutf8没有配serverTimezoneAsia/Shanghai时区被当成了UTC。解决数据库连接串改成下面这样同时确认MySQL服务端时区是东八区执行SHOW VARIABLES LIKE %time_zone%;检查。jdbc.urljdbc:mysql://localhost:3306/bus_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse5.4 小程序端改完代码不生效现象改了request.js里的BASE_URL重新编译后请求还是打到老地址。原因微信开发者工具对JS文件有缓存CtrlS不一定触发全量编译。解决点击工具栏「编译」按钮旁边的小三角选「清缓存并完整编译」或者直接关掉工具重新打开。提交代码前养成习惯最后测一次「清缓存编译」别让队友拉代码时踩缓存坑。5.5 订单表没有索引列表页越用越慢现象订单量到几千条后接口返回明显变慢要800ms以上。原因订单表只建了主键索引查询user_id维度时全表扫描。小项目早期数据量小感知不明显但客运站一天几百单很正常一周就上万条。解决给user_id、bus_id、create_time单独加索引。这类表不需要联合索引因为查询条件通常是user_id status或者bus_id depart_time在user_id和bus_id上建单列索引就能满足建联合索引反而浪费空间ALTER TABLE ticket_order ADD INDEX idx_user_status (user_id, status);6. 二开技巧把固定线路改成可配置顺带学会验证SSM接口的正确姿势毕业设计源码里的线路通常是用SQL脚本写死的比如「北京-上海」每天两班。实际用起来站长要维护发车时间、改票价、增开班次所以二开第一件事就是把班次表管理做成个配置页面。后端加一个管理端接口把班次表设计成按日期生成每日班次而非直接修改基础班次表。常见做法是新增一张bus_plan表存模板每天凌晨或首次查询时根据模板日期生成当天实际班次这样能支持节假日临时加开又不用动基础表CREATE TABLE bus_plan ( plan_id INT PRIMARY KEY AUTO_INCREMENT, route_id INT NOT NULL, bus_no VARCHAR(20) NOT NULL COMMENT 车牌号或车次, depart_time VARCHAR(5) NOT NULL COMMENT HH:mm, arrive_time VARCHAR(5) NOT NULL COMMENT HH:mm, valid_from DATE NOT NULL, valid_to DATE NOT NULL, ticket_price DECIMAL(8,2) NOT NULL, weekday_mask VARCHAR(7) DEFAULT 1111111 COMMENT 周一到周日是否运行1运行 0停运 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;小程序端要响应这个变动班次列表请求带上日期后端把当天的班次从bus_plan里算出来而不是查bus表。验证方法上我习惯在SSM项目里做三件事第一步用Postman跑一遍异常路径比如传个不存在的busId下单看是否会报错而不是返回null第二步看SQL日志通过控制台打印的MyBatis日志确认每个接口只发了预期数量的SQL避免N1查询第三步打开Spring的事务日志logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManagerDEBUG确认下单接口真正开启了事务而查询接口没有。这套源码我改过不止一次最深的一条教训是别迷信整站源码这四个字进入项目的第一天就检查拦截器配了哪些放行、事务注解落在哪一层、SQL脚本的编码对不对——这三处不出问题项目基本就稳了一半。我在交付自己负责的系统时也是这样先把这三处定下来再动业务代码希望帮到你。本文还有配套的精品资源点击获取