简介:本资源是一套完整的微信小程序实战项目——SSM架构的客运自助售票系统,面向Java后端与小程序前端开发者,尤其适合希望掌握SSM框架整合、小程序前后端联调及交通类业务系统设计的学习者。资源包含整站源码、可直接执行的SQL建表脚本及配套论文,覆盖车次查询、座位选择、在线支付等核心业务流程,助力快速理解高并发场景下的数据库优化(索引、缓存)与小程序UI规范落地。压缩包共1276个文件,含127个Java后端逻辑文件、140个Vue组件、187个JS交互脚本、94个WXML页面结构及96个WXSS样式文件,辅以SVG图标、PNG素材与配置类JSON,整体14.87MB,结构清晰、注释详尽,便于模块化学习与二次开发。目前已有77人学习下载,提供从环境搭建(含install/run批处理)、数据库初始化到接口调试的完整链路支持,是深入理解Spring+SpringMVC+MyBatis协同机制与小程序工程化实践的优质参考。
1. 这不是又一个“Hello World”小程序:它是一套能直接跑通的客运售票闭环系统,含完整 SSM 后端、微信前端、数据库初始化脚本和可答辩论文
你下载的这个压缩包,名字里带【微信小程序】和 SSM,但别被“小程序”三个字骗了——它根本不是纯前端 demo。整套工程是典型的「前后端分离 + 微信生态嵌入」架构:微信小程序作为用户触点(买票、查班次、支付回调),后端用 Spring + SpringMVC + MyBatis(SSM)搭建 RESTful 接口服务,MySQL 存储车次、座位、订单、用户等全量业务数据,SQL 脚本一键建库建表,论文部分甚至包含 ER 图、用例图、系统部署拓扑和测试用例表。我去年帮交通职院带毕设时,学生拿这套源码改了 3 天就上线了校际定制班线试运行系统。它解决的不是“怎么显示一个按钮”,而是“如何在微信里完成实名购票、座位锁定、超时释放、订单状态机流转、退票资金原路退回”这一整条链路。适合两类人:一是需要快速交付真实业务场景小程序的 Java 开发者(尤其熟悉 Spring Boot 但想补 SSM 底层细节的),二是计算机专业做毕业设计的学生——论文结构、代码注释密度、SQL 建模规范度,都按本科毕设硬指标来打磨过。注意:它不依赖云开发、不走小程序云函数,所有接口调用直连你本地或服务器上的 Tomcat。
2. 搭建前必须厘清的三件事:为什么选 SSM 而非 Spring Boot?微信小程序如何与 Java 后端安全通信?SQL 脚本到底初始化了哪些关键表?
2.1 SSM 选型不是怀旧,而是为教学穿透性和运维可控性让路
很多新手看到“SSM”第一反应是“过时”,但在这套客运系统里,它恰恰是优势。Spring Boot 自动装配虽快,但当学生要理解“拦截器怎么拦登录态”“MyBatis 的 SqlSession 如何被 Spring 管理”“DispatcherServlet 怎么分发请求到 Controller”时,SSM 的 XML 配置(spring-mvc.xml、applicationContext.xml)就像一张透明地图。比如web.xml里明确声明了ContextLoaderListener加载根上下文,DispatcherServlet加载 Web 上下文,这种分层清晰度对调试 session 共享、事务传播边界至关重要。而 Spring Boot 的@SpringBootApplication把这些全藏进自动配置,初学者遇到No bean named 'xxxMapper'时往往无从下手。本项目中,spring-mvc.xml里<mvc:interceptors>显式配置了LoginInterceptor,applicationContext.xml中<tx:annotation-driven />和<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager">组合,确保购票下单时“查库存→扣库存→生成订单→更新状态”四步操作原子性。这不是技术债,是教学友好型设计。
2.2 微信小程序不直接连数据库,所有通信必须经由后端 API 层,且需严格校验 openid 与 session_key
小程序端所有请求(如wx.request({ url: 'https://your-server.com/api/ticket/list' }))都指向你的 SSM 后端域名。关键在于:不能把数据库账号密码写进小程序代码,也不能让小程序直接调 MySQL。本项目采用标准微信鉴权流程:
- 小程序调
wx.login()获取临时 code → 发给后端/api/auth/login接口; - 后端用该 code +
appid+appsecret向微信服务器换取openid和session_key; - 后端生成自定义登录态 token(存 Redis,有效期 2 小时),返回给小程序;
- 后续所有请求携带
Authorization: Bearer <token>,后端LoginInterceptor解析 token 并校验openid是否匹配当前用户。
提示:
session_key仅用于解密微信加密数据(如手机号),绝不能传给前端。本项目WxUtil.java中decryptPhoneNumber()方法严格在服务端完成解密,解密后才存入user_info表的phone字段。
2.3 SQL 脚本不是简单建表,而是按客运业务实体关系建模,含外键约束与索引优化
解压后你会看到db_init.sql,它不是CREATE TABLE user(...)的堆砌,而是按 ER 关系落地:
bus_line表存线路(起点/终点/里程/票价);bus_schedule表存班次(关联bus_line_id,含发车时间、车型、总座位数);seat_layout表存座位图(JSON 格式存储每排座位号及状态"A1":"0","A2":"1",0=空闲,1=已售);order_master主订单表(含schedule_id,user_id,total_amount,status);order_detail明细表(关联order_master_id,记录具体座位号seat_no)。
所有外键均启用ON UPDATE CASCADE ON DELETE RESTRICT,避免误删班次导致订单数据孤儿化。关键字段如bus_schedule.depart_time、order_master.create_time均建 B+Tree 索引,支撑“查今日所有班次”“查某用户历史订单”高频查询。执行脚本前请确认 MySQL 版本 ≥ 5.7(因用到 JSON 类型字段seat_layout.layout_data)。
3. 五步跑通整站:从 JDK 配置到小程序真机扫码,每个环节都有可验证的输出信号
3.1 环境准备:JDK 8 + Tomcat 8.5 + MySQL 5.7 + 微信开发者工具(必须用稳定版)
不要用 JDK 11 或 17 —— SSM 项目中spring-webmvc-4.3.29.RELEASE.jar依赖javax.servlet-api-3.1.0.jar,JDK 9+ 移除了javax.xml.bind包,会导致XmlBeanDefinitionReader初始化失败。Tomcat 必须选 8.5.x(非 9 或 10),因其 Servlet 规范匹配web.xml中<web-app version="3.1">。MySQL 用 5.7.32 以上,确保 JSON 字段支持。微信开发者工具选Stable 1.06.2304060(官网下载页标注“稳定版”),新版工具对wx.request的 HTTPS 证书校验更严,若后端用自签名证书会报net::ERR_CERT_AUTHORITY_INVALID,此时需在开发者工具设置中勾选“不校验合法域名、HTTPS 证书”。
3.2 后端部署:修改三处配置,启动 Tomcat 后访问http://localhost:8080/api/test返回{code:200,msg:"ok"}即成功
- 修改
src/main/resources/jdbc.properties:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/bus_ticket?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true&useSSL=false jdbc.username=root jdbc.password=123456注意:
serverTimezone=GMT%2B8解决时区错误;useSSL=false避免 MySQL 8.0 默认 SSL 强制;allowPublicKeyRetrieval=true兼容新版本认证插件。
- 执行
db_init.sql创建数据库bus_ticket并导入初始数据(含测试班次、座位图、管理员账号); - Eclipse/IDEA 导入项目为 Dynamic Web Project,Target Runtime 选 Tomcat 8.5,Deployment Assembly 中确保
src/main/webapp映射到/; - 启动 Tomcat,在浏览器访问
http://localhost:8080/api/test,返回 JSON 即证明 SpringMVC 容器加载成功; - 访问
http://localhost:8080/api/user/login,用 Postman 发送 POST 请求,Body 选 x-www-form-urlencoded,填username=adminpassword=123456,返回{"code":200,"data":{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}}即登录模块就绪。
3.3 小程序端配置:替换app.js中的baseUrl,真机扫码前必须通过wx.request调通一个接口
打开miniprogram/app.js,找到globalData对象:
globalData: { userInfo: null, baseUrl: 'https://your-domain.com/api/' // ← 修改此处!本地调试用 http://localhost:8080/api/ }注意:真机扫码调试时,
baseUrl必须是 HTTPS 域名(微信强制),不能是http://localhost。本地调试用开发者工具“预览”功能即可,此时baseUrl设为http://localhost:8080/api/。
在miniprogram/pages/index/index.js的onLoad中,添加测试请求:
wx.request({ url: getApp().globalData.baseUrl + 'ticket/list', method: 'GET', success: (res) => { console.log('班次列表获取成功', res.data) that.setData({ scheduleList: res.data.data }) }, fail: (err) => { console.error('请求失败', err) } })在开发者工具控制台看到班次列表获取成功日志,且 Network 面板中ticket/list返回状态码 200、响应体含[{ "id":1, "lineName":"杭州-上海", "departTime":"08:00" }],即前后端联调成功。
3.4 数据库初始化验证:检查bus_schedule与seat_layout的关联是否生效
执行完db_init.sql后,手动验证两个关键点:
- 查
bus_schedule表,确认id=1的班次line_id=1,depart_time='2024-05-20 08:00:00'; - 查
seat_layout表,确认schedule_id=1的记录中layout_data字段为 JSON 字符串,如{"A1":"0","A2":"0","B1":"0","B2":"0"}(表示 4 个座位全空闲)。
若
seat_layout.layout_data是空字符串或 NULL,说明 JSON 字段插入失败,需检查 MySQL 是否启用了local_infile=ON(某些云主机默认关闭),或改用INSERT INTO seat_layout (schedule_id, layout_data) VALUES (1, '{"A1":"0"}');手动插入。
3.5 真机扫码部署:微信开发者工具生成体验版二维码,手机微信扫描后输入管理员账号登录
- 在开发者工具点击「上传」,填写版本号(如
1.0.0)、项目备注(如“客运售票 V1”); - 上传成功后,登录 微信公众平台 → 开发管理 → 开发版本 → 提交审核(学生毕设可跳过审核,直接生成体验版);
- 在开发管理页点击「体验二维码」,用手机微信扫描;
- 首页输入管理员账号
admin密码123456,点击登录; - 成功进入首页后,点击“今日班次”,应看到杭州-上海、杭州-南京等线路列表;点击任一班次,进入选座页,座位图应渲染出 A1/A2/B1/B2 等格子,点击可切换选中状态。
若扫码后白屏,检查手机网络是否能访问你的服务器 IP;若提示“request:fail ssl hand shake error”,说明域名未配 HTTPS 或证书无效,此时需回退到开发者工具预览模式调试。
4. 避坑指南:这五个翻车点,90% 的人在部署第三步就栽了
4.1 现象:Tomcat 启动后访问/api/test返回 404
原因:Eclipse/IDEA 中项目未正确发布到 Tomcat 的webapps目录,或web.xml中servlet-mapping的url-pattern与实际请求路径不匹配。
解决:检查web.xml中<servlet-mapping>的<url-pattern>是否为/api/*(本项目是),再确认项目 Properties → Deployment Assembly 中src/main/webapp是否映射到/,且target runtime已选 Tomcat 8.5。若仍 404,在 Tomcatlogs/catalina.out中搜索Caused by: java.lang.ClassNotFoundException,定位缺失的 JAR 包(常见为mysql-connector-java-5.1.47.jar未放入WEB-INF/lib)。
4.2 现象:小程序调ticket/list返回 500,日志报java.lang.NullPointerExceptionatTicketController.list()
原因:TicketController中@Autowired private TicketService ticketService;注入失败,通常因TicketService实现类未加@Service注解,或applicationContext.xml中未开启<context:component-scan base-package="com.xxx.service" />。
解决:检查TicketServiceImpl.java是否有@Service("ticketService");打开applicationContext.xml,确认<context:component-scan>的base-package值为com.bus.ticket(本项目实际包名),而非com.xxx。若用 IDEA,按Ctrl+Shift+T搜索TicketServiceImpl,看类上方是否有绿色灯泡提示“Add @Service annotation”。
4.3 现象:MySQL 执行db_init.sql报错ERROR 1064 (42000): You have an error in your SQL syntax
原因:SQL 文件编码为 UTF-8 BOM,MySQL 客户端解析时将 BOM 字节EF BB BF当作非法字符;或seat_layout.layout_data字段类型在 MySQL 5.6 下不支持 JSON。
解决:用 Notepad++ 打开db_init.sql→ 编码 → 转为 UTF-8 无 BOM 格式;若 MySQL 版本 < 5.7,将JSON类型字段改为TEXT,并在 Java 实体类SeatLayout.java中private String layoutData;保持 String 类型,@Column(name = "layout_data")注解不变。
4.4 现象:小程序真机扫码后登录页空白,控制台无任何日志
原因:app.js中baseUrl写成http://192.168.1.100:8080/api/,但手机与电脑不在同一局域网,或路由器防火墙阻止了 8080 端口。
解决:手机浏览器直接访问http://192.168.1.100:8080/api/test,若打不开,说明网络不通。此时需:① 电脑关闭防火墙;② 手机连接与电脑相同的 WiFi;③ 在 Tomcatconf/server.xml中<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" address="0.0.0.0"/>,address="0.0.0.0"允许外部访问。
4.5 现象:选座页点击座位无反应,Network 面板中无seat/update请求发出
原因:miniprogram/pages/seat/seat.js中bindtap="selectSeat"对应的selectSeat方法未绑定到 Page 的methods对象,或 WXML 中>@GetMapping("/list") public Result list( @RequestParam(defaultValue = "1") @Min(1) Integer page, @RequestParam(defaultValue = "10") @Max(100) Integer size, @RequestParam(required = false) String keyword) { PageHelper.startPage(page, size); // ← MyBatis-PageHelper 插件启动分页 List<TicketVO> list = ticketService.listByKeyword(keyword); PageInfo<TicketVO> pageInfo = new PageInfo<>(list); return Result.success(pageInfo); }
注意:
@Min(1)和@Max(100)防止page=-1或size=9999导致全表扫描;PageHelper.startPage()必须在ticketService.listByKeyword()调用前执行,否则分页失效。
5.2 Service 层:SQL 改写为动态查询,用<if>标签处理 keyword 为空的情况
TicketMapper.xml中原 SQL:
<select id="list" resultType="TicketVO"> SELECT s.id, s.line_name, s.depart_time, s.arrive_time FROM bus_schedule s </select>改为动态 SQL:
<select id="listByKeyword" resultType="TicketVO"> SELECT s.id, s.line_name, s.depart_time, s.arrive_time FROM bus_schedule s <where> <if test="keyword != null and keyword != ''"> AND (s.line_name LIKE CONCAT('%', #{keyword}, '%') OR s.depart_place LIKE CONCAT('%', #{keyword}, '%') OR s.arrive_place LIKE CONCAT('%', #{keyword}, '%')) </if> </where> ORDER BY s.depart_time ASC </select>关键点:
<where>标签自动处理 AND 前缀,避免WHERE AND ...语法错误;CONCAT('%', #{keyword}, '%')防止 SQL 注入(用#{}而非${});ORDER BY必须显式声明,否则 PageHelper 分页结果顺序不可控。
5.3 前端页面:在index.wxml中加搜索框,index.js中onPullDownRefresh时重置 page=1
<!-- index.wxml --> <view class="search-bar"> <input bindconfirm="onSearch" placeholder="搜索线路、出发地、目的地" /> </view> <scroll-view scroll-y="true" bindscrolltolower="loadMore"> <!-- 班次列表 --> </scroll-view>// index.js Page({ data: { scheduleList: [], page: 1, size: 10, keyword: '' }, onSearch(e) { const keyword = e.detail.value.trim() this.setData({ keyword, page: 1 }) // ← 搜索时重置页码 this.loadScheduleList() }, loadScheduleList() { wx.request({ url: getApp().globalData.baseUrl + 'ticket/list', data: { page: this.data.page, size: this.data.size, keyword: this.data.keyword }, success: (res) => { if (this.data.page === 1) { this.setData({ scheduleList: res.data.data.list }) } else { this.setData({ scheduleList: this.data.scheduleList.concat(res.data.data.list) }) } } }) } })血泪经验:
onPullDownRefresh下拉刷新时,必须调用wx.stopPullDownRefresh()手动停止动画,否则用户会以为卡死。我在loadScheduleList()的success回调末尾加wx.stopPullDownRefresh(),并用try-catch包裹,避免stopPullDownRefresh在非下拉状态报错。
从那以后我每次给学生讲分页,都强制他们先手写一遍PageHelper.startPage()的调用位置,再对比LIMIT #{start}, #{size}的原始写法——不是为了炫技,是让他们看清:框架帮你省的不是代码行数,而是对数据库执行计划的理解成本。希望帮到你。
本文还有配套的精品资源,点击获取