ARTICLE DETAIL

资讯详情

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

SSM搭建微信小程序校车购票系统实战指南

SSM搭建微信小程序校车购票系统实战指南 简介这是一套面向计算机专业本科生与Java初学者的课程设计级微信小程序实战项目聚焦校园场景下的校车票务数字化管理需求。资源基于SSMSpringSpring MVCMyBatis后端框架与微信小程序前端技术栈构建完整实现实时校车查询、在线购票含单次票/月票、电子乘车码验票、座位预约、乘车记录统计及紧急联系等核心功能兼顾实用性与教学完整性。压缩包共1031个文件涵盖87个Java后端业务逻辑类、136个JS交互脚本、113个Vue组件、60个WXML页面结构、62个WXSS样式文件及231个PNG/SVG图标资源另有SQL建表语句、BAT一键部署脚本和Eclipse项目配置文件结构清晰、开箱即用。资源包大小为16.33MB目前已有149人学习下载适合用于课程设计参考、SSM与小程序联调实践、前后端分离项目复现及校园信息化系统拓展开发。1. 微信小程序校车购票系统为什么用 SSM 搭建后端比 Spring Boot 更适合校园场景这不是一个“微信小程序 Java 后端”的泛泛而谈项目而是聚焦在高校真实运营场景下校车调度与学生购票强耦合的业务闭环——学生扫码上车前需实时查余票、选班次、支付、生成电子乘车码管理员要按校区/线路/时段动态调班、导出乘车记录、对接一卡通扣费接口财务需对账、退票核销、生成月度报表。这类系统对事务一致性如抢票锁库存、数据库复杂关联学生-班级-院系-校车线路-司机排班-车辆状态、以及与校园已有系统如统一身份认证、教务系统课表做轻量级集成有刚性要求。SSMSpring Spring MVC MyBatis在此类项目中不是“过时选择”而是工程可控性与团队适配性的务实决策高校信息中心或外包团队常以 Java EE 老兵为主熟悉 XML 配置与 MyBatis 手写 SQL 的调试节奏MyBatis 对多表联查、分页统计、动态条件拼接的掌控力远超 JPA 默认行为Spring MVC 的 RequestMapping 粒度恰好匹配小程序每个 API 的明确语义如/api/order/create,/api/bus/seat/lock避免 Spring Boot 自动配置带来的黑盒感。尤其当需要对接老式校园一卡通 HTTP 接口无 OAuth2仅 Basic Auth 时间戳签名或 Oracle 旧库时SSM 的显式控制权反而成了稳定性的“后悔药”。如果你正接手一个已上线但响应慢、退票失败率高、后台导出卡死的校车小程序本方案不是炫技是能让你三天内定位到SeatLockService里那个没加Transactional的方法并修复的路径。2. 搭建可运行的 SSM 后端从pom.xml到web.xml的最小可信配置2.1 依赖版本锁定为什么必须用 Spring 4.3.29.RELEASE 而非 5.x校车小程序前端调用后端 API 时大量使用wx.request({ method: POST, data: { ... } })发送 JSON而早期微信开发者工具v1.05.2108270 及之前对Content-Type: application/json;charsetUTF-8的 header 处理存在兼容性问题——若后端返回Content-Type中含charset部分安卓机型会解析失败。Spring 4.3.29.RELEASE 的MappingJackson2HttpMessageConverter默认不写charset而 Spring 5 默认强制写入。因此pom.xml中必须显式锁定properties spring.version4.3.29.RELEASE/spring.version mybatis.version3.4.6/mybatis.version mybatis-spring.version1.3.2/mybatis-spring.version /properties dependencies !-- Spring Core -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency !-- Spring MVC -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency !-- MyBatis -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version${mybatis-spring.version}/version /dependency !-- MySQL 驱动注意必须用 5.1.478.0 驱动在 Tomcat 7 下易报 ClassCastException -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.47/version /dependency !-- JSON 处理不用 Jackson 2.10用 2.9.10避免 LocalDateTime 序列化 bug -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.9.10/version /dependency /dependencies提示mysql-connector-java 5.1.47是关键。曾有项目升级到 8.0.28 后在 Tomcat 7.0.96 上出现java.lang.ClassCastException: com.mysql.cj.jdbc.Driver cannot be cast to java.sql.Driver根源是 Tomcat 7 的common/lib中旧版驱动未清理干净。务必删除$TOMCAT_HOME/lib下所有mysql-*.jar。2.2web.xml手动注册 DispatcherServlet 与字符编码过滤器SSM 的灵魂在于显式控制请求生命周期。web.xml不是摆设而是解决小程序 POST 数据乱码的第一道防线?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 !-- 字符编码过滤器必须放在所有 filter 之前 -- filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping !-- Spring 容器监听器 -- listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener context-param param-namecontextConfigLocation/param-name param-valueclasspath:spring/applicationContext.xml/param-value /context-param !-- Spring MVC 前端控制器 -- servlet servlet-namedispatcherServlet/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring/spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcherServlet/servlet-name url-pattern//url-pattern /servlet-mapping /web-app逻辑说明CharacterEncodingFilter必须forceEncodingtrue否则request.getParameter()在 GET 请求中仍可能乱码DispatcherServlet的contextConfigLocation指向spring-mvc.xml它只管 Controller 层而ContextLoaderListener加载的applicationContext.xml管理 Service 和 DAO这种分层让事务管理Transactional精准作用于 Service 方法避免 Controller 层误加事务导致连接池耗尽。2.3spring-mvc.xml为小程序定制的注解驱动配置小程序前端发来的请求体是标准 JSON后端 Controller 必须能自动反序列化。spring-mvc.xml的核心是启用注解并配置消息转换器?xml version1.0 encodingUTF-8? beans xmlnshttp://www.springframework.org/schema/beans xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xmlns:mvchttp://www.springframework.org/schema/mvc xmlns:contexthttp://www.springframework.org/schema/context xsi:schemaLocation http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd !-- 启用注解驱动 -- mvc:annotation-driven !-- 配置 JSON 消息转换器 -- mvc:message-converters register-defaultstrue bean classorg.springframework.http.converter.json.MappingJackson2HttpMessageConverter property nameobjectMapper bean classcom.fasterxml.jackson.databind.ObjectMapper !-- 禁用 FAIL_ON_UNKNOWN_PROPERTIES避免小程序多传字段导致 400 -- property namedeserializationFeature map entry keyFAIL_ON_UNKNOWN_PROPERTIES valuefalse/ /map /property !-- LocalDateTime 序列化为 yyyy-MM-dd HH:mm:ss -- property nameserializers map entry keyjava.time.LocalDateTime bean classcom.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer constructor-arg bean classcom.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer/ /constructor-arg /bean /entry /map /property /bean /property /bean /mvc:message-converters /mvc:annotation-driven !-- 扫描 Controller 包 -- context:component-scan base-packagecom.school.bus.controller/ !-- 静态资源放行小程序的图片、字体等 -- mvc:default-servlet-handler/ /beans参数说明FAIL_ON_UNKNOWN_PROPERTIESfalse是血泪经验——小程序前端迭代快某次更新加了source: wechat字段后端 DTO 若没这个属性Spring MVC 默认直接 400 Bad Request用户点击购票按钮毫无反应。设为 false 后未知字段被忽略日志里只 warn业务不中断LocalDateTimeSerializer避免时间字段序列化成时间戳小程序new Date(1623456789000)显示异常统一为可读字符串。3. 校车核心业务落地从班次查询到电子乘车码生成的四步链路3.1 班次与座位状态表设计为什么用bus_schedulebus_seat_status两表而非单表校车业务最易翻车的是“余票计算”。若把所有座位状态塞进bus_schedule表如seat_status VARCHAR(1000)存 JSON看似简单但会导致查询某线路某班次剩余座位数需SELECT COUNT(*) FROM bus_schedule WHERE id? AND seat_status LIKE %0%全表扫描并发抢票时UPDATE bus_schedule SET seat_status... WHERE id?会锁整行高并发下排队严重。正确做法是拆分为表名关键字段说明bus_scheduleid,line_id,departure_time,capacity,status班次基础信息status表示“已发车/停运/正常”bus_seat_statusid,schedule_id,seat_no,status,order_id每个座位独立记录status为0(空闲)/1(已售)/2(预留)order_id关联订单这样查余票只需SELECT COUNT(*) FROM bus_seat_status WHERE schedule_id? AND status0走索引抢票时UPDATE bus_seat_status SET status1, order_id? WHERE schedule_id? AND seat_no? AND status0只锁单行且AND status0是乐观锁避免超卖。3.2 抢票接口实现Transactional的边界与SELECT FOR UPDATE的必要性Controller 层只做参数校验和调用 Service核心在OrderService.createOrder()Service public class OrderService { Autowired private BusSeatStatusMapper seatStatusMapper; Autowired private OrderMapper orderMapper; /** * 创建订单先锁座再生成订单最后更新座位状态 * 注意此方法必须在事务内执行且事务传播行为为 REQUIRED */ Transactional(rollbackFor Exception.class) public Order createOrder(Integer scheduleId, String seatNo, String openid) { // 步骤1查询座位是否可用SELECT FOR UPDATE BusSeatStatusExample example new BusSeatStatusExample(); example.createCriteria() .andScheduleIdEqualTo(scheduleId) .andSeatNoEqualTo(seatNo) .andStatusEqualTo((byte) 0); // 0空闲 ListBusSeatStatus seats seatStatusMapper.selectByExample(example); if (seats.isEmpty()) { throw new BusinessException(座位已被占用请刷新重试); } // 步骤2更新座位为已售此时其他事务无法再 SELECT FOR UPDATE 到该行 BusSeatStatus seat seats.get(0); seat.setStatus((byte) 1); seat.setOrderId(null); // 先清空避免脏数据 seatStatusMapper.updateByPrimaryKey(seat); // 步骤3生成订单此时 seat.id 已知可关联 Order order new Order(); order.setScheduleId(scheduleId); order.setSeatNo(seatNo); order.setOpenid(openid); order.setStatus((byte) 1); // 1已支付校车场景通常免支付扫码即购 order.setCreateTime(new Date()); orderMapper.insert(order); // 步骤4返回完整订单含 seat.id 用于生成乘车码 order.setSeatId(seat.getId()); return order; } }逻辑说明Transactional必须包裹整个方法因为select for update和update必须在同一事务内否则select释放锁后另一线程可能抢先updateseatStatusMapper.selectByExample(...)底层执行SELECT * FROM bus_seat_status WHERE ... FOR UPDATE这是 MySQL InnoDB 行锁不是表锁orderMapper.insert(order)后order.getId()可获取自增主键用于后续生成唯一乘车码。3.3 电子乘车码生成用zxing生成 Base64 图片而非跳转小程序码小程序端不需要跳转学生购票成功后直接显示二维码扫码器扫的就是这个图。后端生成逻辑Controller public class QrCodeController { ResponseBody RequestMapping(value /api/qr/{orderId}, produces image/png) public byte[] generateQrCode(PathVariable Integer orderId) throws Exception { // 1. 查询订单拼接内容BUS-{orderId}-{timestamp} Order order orderMapper.selectByPrimaryKey(orderId); if (order null) throw new BusinessException(订单不存在); String content BUS- orderId - System.currentTimeMillis(); // 2. 用 zxing 生成 QRCode MultiFormatWriter multiFormatWriter new MultiFormatWriter(); MapEncodeHintType, Object hints new HashMap(); hints.put(EncodeHintType.CHARACTER_SET, UTF-8); hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); // 高容错 BitMatrix bitMatrix multiFormatWriter.encode(content, BarcodeFormat.QR_CODE, 300, 300, hints); // 3. 转为 PNG 字节数组 ByteArrayOutputStream outputStream new ByteArrayOutputStream(); MatrixToImageWriter.writeToStream(bitMatrix, PNG, outputStream); return outputStream.toByteArray(); } }参数说明尺寸300x300是实测最优——太小200扫码器识别率低太大400传输慢ErrorCorrectionLevel.H支持 30% 损坏仍可识别应对学生手机屏幕划痕或打印模糊返回byte[]并设置produces image/png小程序端wx.downloadFile直接保存比wx.previewImage渲染更快。3.4 小程序端调用链从wx.login()到wx.request()的完整流程小程序app.js中// app.js App({ onLaunch: function () { wx.login({ success: res { // 1. 获取 code 提交给后端换取 openid wx.request({ url: https://your-domain.com/api/login, method: POST, data: { code: res.code }, success: loginRes { if (loginRes.data.code 200) { wx.setStorageSync(openid, loginRes.data.data.openid); // 2. 跳转首页 wx.switchTab({ url: /pages/index/index }); } } }); } }); } });对应后端LoginControllerController public class LoginController { Autowired private WxAuthService wxAuthService; // 封装微信 auth.code2Session 接口 ResponseBody PostMapping(/api/login) public Result login(RequestBody MapString, String params) { String code params.get(code); if (StringUtils.isBlank(code)) { return Result.fail(code 为空); } // 调用微信接口换取 openid WxAuthResult result wxAuthService.getSessionKey(code); if (result.getOpenid() null) { return Result.fail(获取 openid 失败 result.getErrMsg()); } // 3. 保存 openid 到 session 或 JWT此处简化为直接返回 return Result.success(MapUtil.of(openid, result.getOpenid())); } }关键点WxAuthService必须用RestTemplate调用微信https://api.weixin.qq.com/sns/jscode2session且appid/secret绝不能硬编码应从application.properties读取Result是统一封装类code200表示业务成功避免小程序端res.statusCode 200误判 HTTP 状态。4. 避坑指南SSM 微信小程序校车系统上线前必查的 5 个致命问题4.1 现象小程序调用/api/order/create返回 400但后端日志无任何错误原因web.xml中CharacterEncodingFilter未配置forceEncodingtrue或配置顺序在DispatcherServlet之后导致 POST 请求体未被 UTF-8 解码JSON 解析失败。解决检查web.xml确保filter-mapping在servlet-mapping之前且forceEncodingtrue在 Controller 方法参数前加RequestBody(required false)并捕获HttpMessageNotReadableException打印原始 request body 调试。4.2 现象同一班次多人同时抢票出现超卖卖出 51 张但 capacity50原因createOrder()方法未加Transactional或事务传播行为为SUPPORTS不创建新事务导致SELECT FOR UPDATE与UPDATE不在同一事务内锁失效。解决确认Transactional(rollbackFor Exception.class)注解在 Service 方法上且spring-mvc.xml中mvc:annotation-driven/已启用在BusSeatStatusMapper.xml中selectByExample的 SQL 末尾加FOR UPDATEMyBatis 3.4 支持。4.3 现象后台导出 Excel 报表时Tomcat 内存溢出OutOfMemoryError原因使用POI导出时一次性将全部数据ListOrder加载到内存而校车月度数据可达 10 万行。解决改用SXSSFWorkbookPOI 的流式写入SXSSFWorkbook workbook new SXSSFWorkbook(1000); // 每 1000 行 flush 到磁盘 Sheet sheet workbook.createSheet(乘车记录); // 写入数据时每写 1000 行调用 sheet.flushRows(1000)并在tomcat/bin/catalina.sh中增加-Xmx2g -XX:MaxMetaspaceSize512m。4.4 现象小程序wx.downloadFile下载二维码图片失败控制台报net::ERR_CONNECTION_RESET原因Nginx 反向代理默认proxy_buffer_size过小4k而 PNG 图片经 Base64 编码后体积增大触发缓冲区溢出。解决Nginx 配置中增加location /api/qr/ { proxy_pass http://backend; proxy_buffer_size 128k; proxy_buffers 4 256k; proxy_busy_buffers_size 256k; }4.5 现象管理员后台修改班次时间后已购票学生未收到通知仍按旧时间上车原因未实现“班次变更推送”机制仅更新数据库未调用微信模板消息接口。解决在BusScheduleService.updateSchedule()中新增逻辑// 查询该班次所有未发车订单 ListOrder orders orderMapper.selectByScheduleIdAndStatus(scheduleId, (byte)1); for (Order order : orders) { wxTemplateMsgService.sendChangeNotice(order.getOpenid(), scheduleId, newTime); }模板消息需提前在微信公众平台申请并保存template_id。5. 进阶技巧用 MyBatis 动态 SQL 实现“按校区/日期/线路”三维度实时余票查询校车管理员常需看“明天上午东校区到西校区的余票趋势”前端传参{campus: east, date: 2024-06-15, lineId: 3}后端需一条 SQL 查出该线路当天所有班次的余票数。若用传统WHERE拼接代码冗长且易 SQL 注入MyBatis 的where和foreach是更安全的解法。5.1 Mapper 接口定义public interface BusScheduleMapper { /** * 根据校区、日期、线路ID 查询班次余票 * param queryParam 查询参数对象 * return 班次余票列表 */ ListBusScheduleWithRemain selectScheduleWithRemain(Param(query) ScheduleQuery query); }5.2ScheduleQuery参数对象public class ScheduleQuery { private String campus; // 校区east/west/north private String date; // 日期yyyy-MM-dd private Integer lineId; // 线路ID // getter/setter... }5.3BusScheduleMapper.xml中的动态 SQLselect idselectScheduleWithRemain resultTypecom.school.bus.entity.BusScheduleWithRemain SELECT bs.id, bs.line_id, bs.departure_time, bs.capacity, (bs.capacity - COALESCE(locked.locked_count, 0)) AS remain_count FROM bus_schedule bs LEFT JOIN ( -- 子查询统计每个班次已售座位数 SELECT schedule_id, COUNT(*) AS locked_count FROM bus_seat_status WHERE status 1 GROUP BY schedule_id ) locked ON bs.id locked.schedule_id where !-- 动态添加校区过滤 -- if testquery.campus ! null and query.campus ! AND bs.campus #{query.campus} /if !-- 动态添加日期过滤 -- if testquery.date ! null and query.date ! AND DATE(bs.departure_time) #{query.date} /if !-- 动态添加线路过滤 -- if testquery.lineId ! null AND bs.line_id #{query.lineId} /if !-- 只查未发车班次 -- AND bs.status 1 /where ORDER BY bs.departure_time /select表格各if条件的生效逻辑与安全保证条件触发条件SQL 片段安全机制campusquery.campus非空字符串AND bs.campus ?#{}预编译防注入datequery.date非空AND DATE(bs.departure_time) ?DATE()函数确保索引可用departure_time字段需建索引lineIdquery.lineId非 nullAND bs.line_id ?Integer类型天然防字符串注入注意DATE(bs.departure_time)会阻止departure_time字段的索引使用若查询频次高应另建date_only字段类型DATE并索引或在应用层将date转为时间范围BETWEEN 2024-06-15 00:00:00 AND 2024-06-15 23:59:59。5.4 小程序端调用示例// pages/admin/remain/remain.js Page({ data: { remainList: [] }, onLoad: function() { this.loadRemainData(); }, loadRemainData: function() { wx.request({ url: https://your-domain.com/api/schedule/remain, method: POST, data: { campus: east, date: 2024-06-15, lineId: 3 }, success: res { if (res.data.code 200) { this.setData({ remainList: res.data.data }); } } }); } });我带过的三个校车项目都卡在“余票不准”上——要么缓存没刷新要么 SQL 没加COALESCE导致NULL当0计算。后来固定用这个where 子查询模式配合 Redis 缓存keyremain:${campus}:${date}:${lineId}过期 5 分钟既保证实时性又扛住并发。真正上线那天我盯着后台监控看 1000 请求的remain_count是否恒等于capacity - sold_count连续 2 小时没偏差才敢去吃晚饭。希望帮到你。本文还有配套的精品资源点击获取
返回列表