ARTICLE DETAIL

资讯详情

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

SSM+微信小程序客运售票系统:从源码到部署的完整实现与避坑指南

SSM+微信小程序客运售票系统:从源码到部署的完整实现与避坑指南 简介一套基于SSM框架的微信小程序客运自助售票系统整站源码包含小程序端、Java后端、SQL脚本及配套论文以完整项目形式呈现售票业务全流程适合有Java基础、希望学习微信小程序全栈开发的读者也可用于课程设计或毕业设计参考。压缩包共1276个文件约14.87MB主要文件类型含小程序前端页面、Vue管理后台、Java业务代码、JSON配置、SQL脚本、bat运行脚本及docx论文并有图片素材与配置文件覆盖界面、逻辑、数据与文档。目前已有77人浏览学习。项目完整实现车次查询、座位选择、在线支付等客运售票核心流程代码注释详细、模块划分清晰数据库设计遵循第三范式并覆盖索引、查询优化与缓存策略论文梳理系统架构、接口定义和前后端交互逻辑配合安装脚本和SQL脚本可快速完成本地部署便于二次开发与功能扩展。1. 客运自助售票小程序一份 SSM 源码包背后真正要交付的东西一份标着「微信小程序 SSM 整站源码」的客运自助售票小程序解压后通常是一套标准三层 Java Web 工程、一份 MySQL 的 sql 脚本和一本讲设计思路的论文。你要交付的不是让查询页面能出几行数据而是把小程序的登录、班次查询、余票锁定、下单、订单状态变更这一整条链路跑通。说句实在话真正卡住新手的往往不是什么高深算法而是域名校验、sql 脚本导入、微信登录 code2session 这三个最不起眼的点。这篇文章就按这条链路往下拆从工程结构、数据库设计、后端接口到小程序端实现最后把最常见的坑一个个排掉适合正在做毕设或课设、需要把 SSM 后端和微信小程序前端完整联调的人。2. SSM 和小程序端的调用链搭建客运售票系统前先看懂代码怎么走2.1 SSM框架每一层在售票业务里负责什么这套系统选 SSM 框架Spring Spring MVC MyBatis而不是 Spring Boot第一反应是“老”但从做项目的角度讲它把每一层都摊开给你看了Spring 管对象创建和事务Spring MVC 管请求路由MyBatis 管 SQL 映射。对需要写论文、答辩讲清楚“用了什么技术、为什么这么分层”的场景SSM 反而比 Spring Boot 好讲——Boot 大量自动配置很多东西是黑匣子SSM 里一个请求从 Controller 到 Mapper 的每一步都能在配置文件和源码里找到对应关系。微信小程序这边核心优势是免安装和自带登录态。用户扫一下码就能进系统查班次、下单不需要走 App 那种审核周期wx.login 能拿到临时 code后端拿 code 换 openid天然识别用户身份不用自己再维护一套账号密码体系。代价是小程序请求必须走 HTTPS 且域名要在后台配白名单本地调试要绕一下这个坑第 5 章专门展开。整条调用链一句话说清小程序 wx.request - 后端 DispatcherServlet - Controller - Service - Mapper - MySQL。出问题按这条链从前往后排先确认请求有没有到后端再看 SQL 有没有执行最后看返回格式前端解析没有。大部分联调翻车都发生在“前端以为后端没收到、后端以为前端没发出去”的中间地带。2.2 整站源码的工程结构先找到配置入口再谈运行解压后典型 Maven 工程结构如下具体包名以你手里那份为准但骨架基本一致ticket-sale/ ├── pom.xml ├── sql/ │ └── ticket.sql ├── src/main/java/com/example/ticket/ │ ├── controller/ │ │ ├── UserController.java │ │ ├── ShiftController.java │ │ └── OrderController.java │ ├── service/ │ │ ├── ShiftService.java │ │ └── OrderService.java │ ├── dao/ │ │ ├── ShiftMapper.java │ │ └── OrderMapper.java │ └── pojo/ │ ├── Shift.java │ ├── Order.java │ └── User.java ├── src/main/resources/ │ ├── jdbc.properties │ ├── spring/ │ │ ├── applicationContext.xml │ │ └── spring-mvc.xml │ └── mapper/ │ ├── ShiftMapper.xml │ └── OrderMapper.xml └── src/main/webapp/WEB-INF/ └── web.xml后端工程旁边一般还有个独立的小程序前端目录通常叫 miniprogram/ 或 pages/里面才是 WXML、WXSS、JS。导入微信开发者工具时选这个目录不要选整个后端工程。环境清单按下面这套配能少踩一半坑组件版本建议说明JDK1.8SSM 老项目在 JDK 11 上容易出现反射和字节码兼容问题Maven3.6.x依赖下载和打包用Tomcat8.5.x支持 Servlet 3.1部署 war 包合适MySQL5.7 优先8.0 能用但要换驱动类见 5.3微信开发者工具稳定版导入前端目录调试MySQL 建议优先 5.7这套源码很多是从 MySQL 5.x 时代传下来的你换 8.0 后驱动和时区会多两个坑。Tomcat 8.5 对老项目兼容性最好Tomcat 9 以上遇到 XML 头解析问题也别意外。2.3 启动前必改的三个文件数据源、事务、请求映射第一步改 jdbc.properties。这是整个数据库连接的入口驱动类、连接串、账号密码都在这jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/ticket?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.password123456如果你用 MySQL 5.7driver 可以写成 com.mysql.jdbc.Driver但既然本地装的是 8.0直接统一用 com.mysql.cj.jdbc.Driver 最省事。url 里两个参数容易漏characterEncodingutf8 不写中文会乱码serverTimezoneAsia/Shanghai 不写MySQL 8 会抛时区异常。useSSLfalse 是本地调试用的正式环境走内网或云数据库再按需开 SSL。第二步看 applicationContext.xml这是 Spring 的根容器数据源、事务、Mapper 扫描都在这context:property-placeholder locationclasspath:jdbc.properties/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.example.ticket.dao/ /bean bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/如果源码里没引入 Druid换成 Spring 自带的 DriverManagerDataSource 也能跑但 Druid 的池化能力和监控页对答辩是加分项我一般会保留。mapperLocations 指向 classpath:mapper/*.xml这决定了 MyBatis 到哪找 SQL 映射文件路径不对会报 Invalid bound statement后面排查时最先查这里。第三步确认 spring-mvc.xml 只扫 controller别把 Service 也扫进 Web 容器。常见写法是mvc:annotation-driven/ context:component-scan base-packagecom.example.ticket.controller/ mvc:resources mapping/static/** location/static//component-scan 只扫 controller 包是 SSM 项目里约定俗成的分层习惯。如果这里 base-package 写成了 com.example.ticket会出现 Service 被 Web 容器和 Spring 容器各创建一份事务注解失效的奇怪问题事务报了错但数据没回滚这就是典型的“配置分层不清导致的黑匣子”。3. 从 sql 脚本到后端接口把余票、订单、用户这三块核心逻辑落地3.1 客运售票的核心表字段设计与 sql 脚本导入客运自助售票的最小业务闭环需要三张表用户表、班次表、订单表。常见做法是把座位信息直接做成订单里的 seat_no 字段而不是单开一张座位表毕设量级下这样逻辑更直白答辩也好解释。表名关键字段作用t_useruser_id, openid, nickname, phone小程序用户身份t_shiftshift_id, shift_no, start_station, end_station, depart_date, depart_time, arrive_time, ticket_price, ticket_count班次与余票t_orderorder_id, order_no, shift_id, user_id, passenger_name, passenger_phone, seat_no, pay_status, create_time订单与支付状态班次表里存一个 ticket_count 余票数字段而不是每天动态算已售多少张是因为查询余票是最高频操作直接读字段比 count 订单表快一个量级。订单表用 pay_status 区分 0 待支付、1 已支付、2 已取消状态流转放后端 Service 层控制不放前端。sql 脚本导入我一般用命令行避开 Navicat 图形界面在字符集上的幺蛾子mysql -uroot -p --default-character-setutf8mb4 set names utf8mb4; source /path/to/ticket.sql导入前先 set names utf8mb4 是为了让客户端、连接、文件三者字符集一致。如果脚本开头有 CREATE DATABASE 语句导入完记得确认当前库是不是切到了 ticket否则后面 JDBC 连的库和导入的库不一致控制台会报 Table doesnt exist。3.2 余票查询与防超卖事务 for update 的两种写法余票扣减必须和订单生成放在同一个事务里否则高并发下会出现两张订单同时读到余票 1、各自扣成 0 的经典超卖。Mapper 里核心是这两条 SQLselect idselectByIdForUpdate resultTypecom.example.ticket.pojo.Shift select shift_id, shift_no, ticket_count, ticket_price from t_shift where shift_id #{shiftId} for update /select update iddecreaseStock update t_shift set ticket_count ticket_count - 1 where shift_id #{shiftId} and ticket_count 0 /updateselect ... for update 是悲观行锁事务提交前其他事务的更新和查询都会被阻塞。这里锁住的是这一行班次记录不是整张表所以不同班次的订单互不影响。decreaseStock 里带上 ticket_count 0 是第二道保险即使锁没生效update 影响行数为 0 也说明库存已经没了。Service 层配合事务注解代码逻辑是这样Service public class OrderService { Transactional(rollbackFor Exception.class) public Order createOrder(String shiftId, String userId, String passengerName, String passengerPhone) { Shift shift shiftMapper.selectByIdForUpdate(shiftId); if (shift.getTicketCount() 1) { throw new RuntimeException(余票不足); } int rows shiftMapper.decreaseStock(shiftId); if (rows 0) { throw new RuntimeException(余票不足请重新选择班次); } Order order new Order(); order.setOrderNo(generateOrderNo()); order.setShiftId(shiftId); order.setUserId(userId); order.setPassengerName(passengerName); order.setPassengerPhone(passengerPhone); order.setPayStatus(0); orderMapper.insert(order); return order; } }rollbackFor Exception.class 很关键默认 Spring 只在 RuntimeException 时回滚如果不写这个参数Service 方法里抛出受检异常时事务不会回滚。这里故意先查后扣是为了在 Java 层给出更友好的“余票不足”提示而不是让 MyBatis 直接抛 SQL 异常。如果你不想用悲观锁也可以改成 update 返回行数 乐观锁版本号但售票场景冲突概率高重试成本不低悲观锁是更直接的做法。3.3 生成订单与支付状态流转模拟支付与微信支付的取舍订单号生成规则我习惯用“T 时间戳 四位随机数”长度刚好且不会暴露当天订单量比自增 id 更适合给用户看。订单落库时 pay_status 是 0支付成功后置为 1取消或超时置为 2后端只认这三种状态。微信支付要不要真接我的看法是如果你没有商户号不要硬接做一个模拟支付按钮就行。前端跳转一个支付确认页点“确认支付”后调后端 /order/pay 接口把订单状态从 0 改成 1。这个方案在答辩里站得住因为支付流程的设计思路和接真支付是一样的差别只在后端没有真的调微信下单接口。有商户号想接真支付就按微信小程序支付开发文档里的流程走前端 wx.requestPayment 唤起收银台后端接收支付回调更新订单状态。回调接口一定要验签不能只信回调参数里的 resultCode微信支付回调是可以被伪造的验签不过直接拒绝。4. 微信小程序端请求封装、登录与下单流程的实现4.1 封装 wx.request一个模块收敛 baseUrl、token 和错误提示自助售票的小程序端页面不止一个如果每个页面都裸写 wx.request后面改接口地址能改到怀疑人生。我一般会在 utils/request.js 里统一封装核心代码如下const BASE_URL http://localhost:8080/ticket const request (url, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL url, method, data, header: { Content-Type: application/json, Authorization: wx.getStorageSync(token) || }, success: (res) { if (res.statusCode 200 res.data.code 200) { resolve(res.data.data) } else { wx.showToast({ title: res.data.msg || 请求失败, icon: none }) reject(res.data) } }, fail: (err) { console.error(request fail, err) reject(err) } }) }) } module.exports { request, BASE_URL }BASE_URL 里的 /ticket 是 Tomcat 部署的上下文路径不是随手写的这个细节会导致第 5.5 节的坑。header 里统一带 Authorization后端根据 token 识别用户不用每个接口单独传 userId。后端返回格式约定为 { code, msg, data }code 200 才算成功这样错误处理不会散落到各个页面。注意 wx.uploadFile 是另一套 API不经过这个封装如果后面要传身份证照片之类得单独处理。4.2 java 后端实现微信小程序登录code2session 换 token登录流程是 java 后端实现微信小程序登录的标准三段式前端 wx.login 拿临时 code后端拿 code 加 appid 和 secret 调微信 jscode2session 接口拿到 openid 后生成自己的业务 token 返回给前端。前端代码const login () { return new Promise((resolve, reject) { wx.login({ success: (res) { if (res.code) { request(/user/login, POST, { code: res.code }) .then((data) { wx.setStorageSync(token, data.token) resolve(data) }) .catch(reject) } else { reject(new Error(wx.login failed)) } } }) }) }后端 Controller 接收 code再调微信接口PostMapping(/user/login) public Result login(RequestBody MapString, String params) { String code params.get(code); // 常见做法是用 hutool 的 HttpUtil 或 Apache HttpClient 请求这个地址 String url https://api.weixin.qq.com/sns/jscode2session?appid appId secret appSecret js_code code grant_typeauthorization_code; String response HttpUtil.get(url); JSONObject json JSONUtil.parseObj(response); String openid json.getStr(openid); if (openid null) { return Result.error(code 换取 openid 失败: json); } String token UUID.randomUUID().toString().replace(-, ); redisUtil.set(token: token, openid, 7200); return Result.ok().put(token, token); }两个敏感点appid 和 secret 不要硬编码在 Controller 里从配置文件读取session_key 不要返回给前端它是微信会话密钥只留在后端解密用户信息时用。code 是一次性的有效期五分钟用过即失效前端千万不要把它存起来重复提交。token 存 Redis 并设置两小时过期比无状态 JWT 好在服务端能主动踢人。4.3 车次查询页与自助下单页从列表点击到订单生成查询页的核心逻辑是拉班次列表、展示余票、点进下单页。关键代码Page({ data: { departDate: , shifts: [] }, onLoad() { const today new Date().toISOString().slice(0, 10) this.setData({ departDate: today }) this.fetchShifts() }, fetchShifts() { const { departDate } this.data request(/shift/search?date${departDate}, GET) .then((data) { this.setData({ shifts: data }) }) }, handleOrder(event) { const shiftId event.currentTarget.dataset.id wx.navigateTo({ url: /pages/order/order?shiftId${shiftId} }) } })handleOrder 里用 dataset 取 shiftId这是小程序页面传参的标准姿势比全局变量干净。跳转后下单页 onLoad 里通过 options.shiftId 拿到参数。下单页提交按钮要防连点常见做法是加一个 submitting 标志位提交中置 true请求完成再放开防止用户双击生成重复订单下单成功返回前把页面栈里的查询页刷新标记带上回列表页时 onShow 重新拉余票保证看到的是扣减之后的数据。前端提交订单调 /order/create把 shiftId、乘客姓名、电话传过去后端返回 orderNo 和 payStatus。接下来判断支付方式模拟支付就展示一个确认弹窗调 /order/pay 改状态真支付就调 wx.requestPayment 走微信收银台。无论哪种支付结果页面都要从后端拉一次最新订单状态不能相信本地缓存的支付结果。5. 部署联调避坑这套 SSM 客运小程序最常见的 5 个卡点被这套项目折磨过的人都知道代码本身不难跑起来才是玄学。下面 5 个卡点是我重装环境过程中彻底摸清的按出现频率排。5.1 开发者工具报 url not in domain list开发期和上线期两种处理现象开发者工具 console 报 request:fail url not in domain list真机上同样失败。原因小程序要求所有请求域名都配置在公众平台的白名单里且必须是 HTTPS本地开发用 http://localhost 肯定不在名单里。解决开发阶段在微信开发者工具右上角“详情 - 本地设置”勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”。上线前再把正式 HTTPS 域名配到小程序后台的 request 合法域名里这个配置修改有审核生效时间提前改不要等到发版当天。5.2 sql 脚本导一半报 1064字符集和文件编码不一致现象source 导入中途报 1064 语法错误或者导入成功但中文全是问号。原因脚本文件是 GBK 编码MySQL 连接默认 utf8mb4两边字符集对不上字符串里的中文被截断导致语法错乱。解决先用编辑器把 sql 文件转成 UTF-8 无 BOM 编码再按 3.1 的命令导入。检查脚本头有没有 SET NAMES utf8mb4 语句没有就手动补上。Navicat 导入失败时别反复重试先看文件编码这是最常见的隐藏坑。5.3 ClassNotFoundException: com.mysql.jdbc.DriverMySQL 8 的老驱动坑现象Tomcat 启动报 java.lang.ClassNotFoundException: com.mysql.jdbc.Driver或者报 SSL 连接警告。原因pom.xml 里 MySQL 驱动是 5.1.x本地数据库是 MySQL 8.0驱动类名从 com.mysql.jdbc.Driver 改成了 com.mysql.cj.jdbc.Driver。解决改 pom.xml 里的依赖坐标dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency改完记得 Maven 重新导入再把 2.3 里 jdbc.properties 的驱动类同步改成 com.mysql.cj.jdbc.Driver。如果你嫌麻烦直接用 docker 跑一个 MySQL 5.7 容器这套项目在 5.7 上兼容性最好。5.4 登录接口返回 invalid codeappid 和 code 一次性问题现象开发者工具打开小程序POST /user/login 返回 errcode 40029 invalid code 或 40013 invalid appid。原因两种情况最常见。一是小程序端用的是测试号后端配置的是正式 appid两端不一致二是 wx.login 返回的 code 被前端缓存第二次请求还拿同一个 code 去换 openid。解决确认开发者工具的账号和后端 appid 对应同一套。code 换 openid 是一次性的后端日志里打印微信返回的完整 JSON先分清是 appid 错还是 code 失效再对症处理。后端拿不到 openid 时不要直接返回失败把微信返回的 errcode 和 errmsg 透传给前端排错效率高很多。5.5 改了 Tomcat 端口后接口全挂baseUrl 没同步现象localhost 后端 curl 能通开发者工具里所有请求全报 404 或连接失败。原因改了 Tomcat 端口但没有同步修改小程序端的 BASE_URL或者应用部署路径是 /ticketBASE_URL 里少写了这一层。解决BASE_URL 统一维护在 utils/request.js 里格式是 http://ip:端口/上下文路径。改了后端端口先 curl 一次完整接口确认通再回小程序看请求。上下文路径在 Tomcat 部署时的 war 包名决定Solr 后端接口文档里写的是 /ticket 开头你本地部署路径也得对应上。6. 从能跑到能用的进阶验证压测防超卖、迁移 Spring Boot 与上线前检查这套系统从“能跑”到“真能用”我一般会做三件事压测防超卖、迁移 Spring Boot、过上线检查清单。防超卖压测用 JMeter 最直接。先跑一次登录接口拿到 token把它放进 HTTP Header Manager再开 50 个线程并发打 /order/create。看结果要抓两个数订单表里成功插入的订单数不能超过库存数库存字段最终不能是负数。如果出现了两个订单共用一个库存的情况说明事务没生效或者锁没加对回 3.2 重新检查。脚本压测比人肉点页面靠谱也是答辩时能拿出来讲的验证手段。迁移 Spring Boot 的核心思路是把 web.xml 和 Spring XML 配置搬进自动配置spring: datasource: url: jdbc:mysql://localhost:3306/ticket?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.ticket.pojo迁移后 Service 和 Mapper 的代码一行不用改把 MapperScan 加到启动类上事务用 EnableTransactionManagement 开启。你会发现 SSM 时期的分层习惯全都能直接沿用。上线前再过一遍检查清单小程序后台的 request 合法域名配好且是 HTTPSappid 换成正式账号secret 不要硬编码在前端用户隐私保护指引里声明手机号、位置的采集项数据库密码从 jdbc.properties 里外置到环境变量。这套项目最值钱的部分不是增删改查而是余票扣减和订单状态流转这两段业务逻辑把它们研究透以后换什么框架都是同一套思路。我习惯把这类源码包当脚手架用先跑通再谈优化跑不通就顺着调用链一层层查查完一遍你就知道 web 系统的水有多深了。希望帮到你。本文还有配套的精品资源点击获取
返回列表