ARTICLE DETAIL

资讯详情

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

校车购票微信小程序开发实战:从需求到上线全流程解析

校车购票微信小程序开发实战:从需求到上线全流程解析 上个学期末我们学校的校车调度群彻底乱套了——一条“明天下午三点回市区要坐的接龙”的消息发出来底下跟了几十条回复有说要带行李箱的有问中途能不能下车的还有到了发车时间人没出现的。管校车的老师跟我抱怨统计人数全靠手抄收款全靠转账备注月底一对账不是差钱就是多票。我当时就跟他说要不我给你写个小程序吧。于是就有了这个 weixin088 校车购票微信小程序。这个项目说起来不算大但“麻雀虽小五脏俱全”。它覆盖了微信小程序开发的绝大多数基础能力页面导航、列表渲染、表单交互、登录鉴权、微信支付、订阅消息以及真机调试和上线审核的全流程。很多同学想学小程序开发但不知道从哪下手我建议你完整跟一遍这类偏工具型的项目比看十遍文档都有用。这篇文章我会把整个项目的需求设计、前后端实现、支付闭环、上线踩坑全部拆开讲附带能直接抄作业的方案和参数。无论你是刚接触微信小程序还是已经在写业务代码但想了解完整项目结构都应该能从中拿到点实在的东西。1. 需求从哪里来校车购票的四个真实痛点1.1 排班靠吼统计靠手我一开始想的很简单不就是把接龙变成线上投票吗真正聊完需求才发现校车购票远不只是“报名人数统计”。学校现有的班车场景大概是这样的固定线路校区到市区、校区到高铁站、校区到家属院每天有固定班次但节假日、周五下午这种高峰时段会临时加车。过去不管是学生还是调度老师都靠微信群接龙解决问题非常典型人数统计滞后经常超员司机到了出发点才发现座位不够。收款和报名对不上有人报名没付款有人付款没报名。临时退改全靠群里反复吼调度员得手动改名单。每辆车谁坐了哪个位置不知道一旦牵扯到责任问题说不清楚。这四条每一条背后都对应一个功能模块实时余票展示、支付闭环、在线退改、购票记录与座位登记。所以这个项目的第一课其实是需求分析——不是做一个“看起来不错”的小程序而是做一个能替代微信群接龙的可靠工具。1.2 角色拆解学生、司机、管理者各要什么做小程序开发最忌讳的是把所有用户当成一个群体。校车购票实际涉及三方角色每方的痛点都不一样学生乘客最关心还有没有票、几点发车、在哪上车、能不能退。他们需要的是快速查询和一步购票付款方式要支持微信支付退票要能实时到账。司机承运方最关心这趟车到底几个人、谁没来、有没有超员。司机端不需要花哨功能一张验票码就够。调度员/管理者学校老师最关心的是账目清楚、取消班次时能通知到所有人。管理端需要发车管理、订单明细导出、退票审核。你如果只做学生端司机和管理者继续用微信群那这个项目就失败了。我在 MVP最小可用产品设计里把司机验票和管理后台都放进去了只是管理后台用简单的网页实现没有做独立小程序。1.3 MVP功能清单与页面规划结合上面的痛点第一版功能清单我定了五件事车次列表按线路日期展示所有班次显示发车时间、余票数、票价。购票流程选择车次、填写乘车人默认就是本人、选择座位、微信支付。我的车票展示已购票订单支持改签退票展示验票二维码。司机验票扫乘客二维码核对订单状态标记已乘车。管理配置线路维护、班次生成、余票手动调整、订单导出。对应到小程序端就四个页面首页车次列表、购票页、订单列表页、订单详情页。再加一个司机端的扫码页。页面越少逻辑越集中对新手越友好。我见过太多人一上来就搞十几个页面最后一半是空的没必要。2. 页面实现导航栏适配、列表加载与购票表单2.1 顶部导航栏高度为什么是个坑先说一个所有小程序开发者都会遇到的细节顶部导航栏高度。校车购票首页需要在顶部放一个自定义的搜索或者线路筛选条如果用原生导航栏胶囊按钮右上角的“...”和圆圈是固定的但不同手机的胶囊位置不一样你自定义的标题栏稍微偏一点就露怯。我当时用的方案是自定义导航栏拿到状态栏高度和胶囊按钮位置后动态计算。核心代码大概是这样的const { statusBarHeight, platform } wx.getWindowInfo(); const capsule wx.getMenuButtonBoundingClientRect(); const navBarHeight (capsule.top - statusBarHeight) * 2 capsule.height statusBarHeight;这里有个容易忽略的点导航栏总高度并不是胶囊按钮高度加状态栏高度而是“胶囊上方留白 胶囊高度 胶囊下方留白 状态栏高度”。胶囊上下留白在视觉上是相等的所以用(capsule.top - statusBarHeight) * 2 capsule.height算中间区域再加回状态栏高度。不同机型上这个值在 44px 到 50px 之间浮动不能写死。我建议所有涉及自定义导航栏的项目都写一个公共的navBar组件把状态栏高度和导航栏高度算好页面直接调用。不要每个页面单独算一次改起来会疯掉。2.2 车次列表与“加载更多”的正确姿势校车班次按日期拉数据一天可能有二三十个班次如果一次性全渲染页面会明显卡顿。这里要用到“页面列表加载更多”——微信小程序里的标准做法是onReachBottom分页加载。我在车次列表页维护了三个关键变量data: { list: [], page: 1, hasMore: true, loading: false }每次触底时先判断loading和hasMore防止重复请求。请求成功后把返回的新一页数据concat到旧列表后面。这里有两个我踩过的坑不要用setData把整个列表重新赋值数据量大时会有性能问题直接concat后整体赋值反而更稳因为列表页本身数据量可控。onReachBottom触发时机受页面整体高度影响如果首屏已经铺满第一次进入页面会连续触发好几次。用 loading 锁能规避。分页接口的返回结构我统一设计成{ code, message, data: { list, hasMore, page } }前端拿hasMore决定要不要继续加载而不是靠判断列表长度。2.3 购票页的单选框与座位选择逻辑购票页的核心是选择“坐哪个位子”。这个地方我用了微信小程序的radio-group组件渲染一排座位号再用一个二维数组记录哪些座位已经被占了。座位选择逻辑其实有讲究同一趟车上A 座位被下单但还没支付到底算不算占用我第一版做得比较粗暴只要创建了订单就算占用结果发现很多学生下单后不付款二十分钟后座位就被锁死了。后来改成“预占 15 分钟超时自动释放”而这个释放动作由后端定时任务完成。这个设计在校车这种低频场景下完全够用不需要引入真正的分布式锁。单选框还有个体验细节座位号被占用时应该disabled并且整行置灰。提交订单前要校验用户是否选了座位我当时在bindconfirm里加了非空判断提示“请先选择座位”这个校验不写在后端的话前端很容易漏掉空座位订单。2.4 我的车票状态列表与二维码验票“我的车票”页面其实是订单列表按状态分成三个 tab待乘车、已完成、已退票。每个订单卡片上显示车次时间、上车点、座位号和票价。待乘车订单右上角放一个二维码按钮点击后进入验票详情页。二维码验票这块我用的是weapp-qrcode这个库在 Canvas 上绘制二维码。内容不是订单号明文而是订单号加随机签名字段例如orderIdxxxtokenxxx司机端扫码后请求后端验票接口后端二次校验 token 有效性。这样做的好处是即使二维码被人拍照转发离线也用不了因为 token 是一次性的验票成功后立即失效。这里要提醒一句Canvas 绘制的二维码在部分安卓机型上会存在宽高不清晰的问题生成时把尺寸调成 300px 以上导出的图片用wx.canvasToTempFilePath转成本地文件再展示能明显改善模糊问题。3. 后端设计车次、余票与订单状态机3.1 数据模型先把三张表设计好小程序端只是皮真正撑起校车购票的是后端数据模型。我用了简单的 Node.js MySQL三张核心表bus_line线路表线路名称、起点、终点、全程时长、票价。bus_schedule班次表线路 ID、发车日期、发车时间、总座位数、已售座位数、状态正常/取消。这条是核心中的核心余票 总座位数 - 已售座位数。bus_order订单表订单号、用户 openid、班次 ID、座位号、乘车人姓名/手机号、支付金额、状态待支付/已支付/已退票/已验票、支付单号、下单时间。为什么把线路和班次拆开因为同一条线路每天有多趟车如果合并成一张表冗余会非常严重而且改一条线路的票价要连带改所有班次记录。拆开后线路信息随便改班次只存自己的发车日期和余票互不影响。订单一律不直接存用户 ID存的是openid。原因也很简单小程序的wx.login拿到的就是 openid用它做关联可以少一张用户表的复杂度。姓名和手机号我冗余存进订单表避免后面查乘车人还要二次 join 用户表。3.2 余票扣减的并发问题版本号与原子更新校车座位只有几十个但周五下午抢票的高峰期可能同时有几百个人盯着同一个班次下单。如果后端是“先查余票再在内存里减一最后写回”那在并发下一定会超卖。这个问题的经典解法是原子更新加条件判断UPDATE bus_schedule SET sold_seats sold_seats 1 WHERE id ? AND sold_seats total_seats这条 SQL 天然做了两件事把已售座位数加一同时保证加完后不会超过总座位数。affectedRows为 0 就说明没抢到座位直接返回“票已抢完”。我在数据库层面还多加了一个乐观锁版本号字段创建订单和扣减余票在同一个事务里执行任何一步失败都整体回滚。整个事务的伪代码大概是BEGIN; UPDATE bus_schedule SET sold_seats sold_seats 1 WHERE id ? AND sold_seats total_seats; 如果 affectedRows 0ROLLBACK 并返回无票。 INSERT INTO bus_order (order_no, ..., statusPENDING); COMMIT;这套方案不需要引入 Redis 分布式锁对校车项目来说已经足够稳。如果你以后做的是秒杀类高并发项目再去研究 Redis 减库存的方案也不迟。3.3 订单状态机从待支付到已验票订单状态是这类项目最容易写乱的地方。我在代码里用一套整数字典表示状态简单清晰状态码含义可流转到0待支付1 已支付 / 4 已关闭1已支付2 已验票 / 3 已退票2已验票终点态3已退票终点态4已关闭超时未付终点态所有修改状态的操作都走同一个updateOrderStatus(orderNo, fromStatus, toStatus)方法SQL 里带上WHERE status fromStatus这样能防止两个接口同时把同一个订单改到不同状态的情况。比如退票接口和验票接口同时操作同一个订单如果没有状态条件很可能会把已验票的订单改成已退款那票就白坐了钱还退了。3.4 接口鉴权wx.login 与 openid小程序每次调用后端接口后端都必须知道“你是谁”。我的方案是前端调wx.login()拿到临时 code发给后端后端拿 code 去微信接口换 openid 和 session_key然后签发一个自定义 token 返回给前端。之后的请求前端在header里带Authorization: Bearer xxx后端解析出用户身份。这个方案比较传统但可靠。需要注意几点wx.login的 code 有效期只有五分钟而且只能用一次不能缓存。session_key 永远不要下发到前端前端只需要拿着 token 就行。换 openid 的接口地址和参数是固定的但 request 的合法域名必须配置否则开发工具里直接报错。我在这个项目里还做了一个容错如果wx.login失效后端返回 401前端统一跳到登录页面重新静默登录不需要用户手动操作。这块逻辑封装在公共请求模块里每次wx.request都先检查 token失效就自动刷新。4. 支付与消息线上购票的资金闭环与乘车提醒4.1 wx.requestPayment从小程序到微信支付购票流程走到支付这一步对新手来说最容易懵。微信小程序的支付不是前端直接调起扣款而是有一个“后端下单、前端拉起收银台、后端收回调”的三段式流程用户点确认支付前端把订单号和金额发给自己的后端。后端拿着金额调用微信支付的“统一下单”接口得到prepay_id。小程序端拿到prepay_id等参数后调用wx.requestPayment微信弹出支付确认界面。用户输入密码完成支付微信服务器异步通知你的后端后端改订单状态。wx.requestPayment需要的五个参数timeStamp、nonceStr、package、signType、paySign全部由后端生成前端一个都不能自己拼拼了也验不过。这一步我卡过很久后来才知道 paySign 的签名串是把 appId、timeStamp、nonceStr、package 这几个值按字典序拼接后用商户密钥签名顺序错了就是 401 签名错误。4.2 支付回调验签与掉单处理支付成功之后钱进了商户号但你的数据库订单还挂着“待支付”。这个时候靠的不是前端返回结果而是微信服务器的异步回调。回调会带上订单号、交易号、金额和一个签名后端必须做三件事验证签名防止有人伪造回调。核对金额和订单号是否匹配防止“少给钱多改单”。幂等处理同一笔回调可以重复收到但只能成功改一次订单状态。掉单是支付项目里最头疼的问题。用户明明付了钱但回调因为网络原因没收到订单一直卡在待支付。我的处理方式是前端在wx.requestPayment的success回调里再主动调一次后端“查询订单支付结果”接口后端查微信支付订单查询接口用查询结果兜底修正状态。简单说就是双保险——回调没到前端也会主动拉状态。后来又把“未支付订单超过 30 分钟自动关单并调用关单接口”做成了一个定时任务彻底解决死单。4.3 退款后台开放接口与状态回滚校车购票的退票场景很常见发车前两小时学生可以自助退票。退款我用的微信支付“申请退款”接口这个接口和普通支付回调有个关键区别——它需要商户 API 证书而且退款可以不是全额退比如退票手续费。我的退票逻辑分成两步后端先在校车订单表中把状态改成“退款中”。调微信退款接口成功后再把订单状态改成“已退票”并把扣减的余票加回去。这里有个必须注意的坑余票释放必须和退款成功同步。如果先释放余票再退款退款失败就会造成“座位放出去了但原订单还占着钱”的中间态。反向操作更不行退款成功了却不释放座位座位就被白白锁死。我的做法是在退款成功回调里执行UPDATE bus_schedule SET sold_seats sold_seats - 1 WHERE id ?用事务包住“订单状态更新”和“余票回补”两步保证要么都成功要么都失败。4.4 订阅消息乘车提醒一次性订阅校车这种场景特别适合用微信订阅消息做乘车提醒。乘客买完票订阅一次“发车提醒”系统在发车前半小时推送一条“您的班车即将发车请提前到场”的通知。这个功能使用的是wx.requestSubscribeMessage接口模板从微信公众平台的公共模板库选不能自己随便定义。实现上要注意微信订阅消息的机制是“每次订阅只能推送一条”。用户每次都点订阅不太现实所以我只在购票成功后弹一次授权框提醒用户订阅发车提醒。这样虽然只能收到一次提醒但对校车场景来说已经够用了——用户就是为这一趟车买的票提醒一次就够了。我一开始天真地以为订阅消息可以像公众号模板消息一样随便推结果测试时发现授权框经常不弹。排查后确认是模板 ID 配置错了需要在后台申请模板审核通过后拿到模板 ID再填进代码里而且模板内容里的字段比如“发车时间”“乘车地点”要在后台对应填充。5. 真机调试与上线路上踩过的坑5.1 用 charles 抓包看真实流量小程序开发最怕遇到一个问题开发者工具里一切正常真机上就各种接口报错。要排查这类问题charles 这类 HTTP 抓包工具是绕不开的。我之前的习惯是光看开发者工具的 network 面板但真机上的网络环境和工具里完全不一样很可能请求根本没发出去或者被某个加密证书挡住了。用 charles 调试微信小程序的做法其实不复杂在电脑上启动 charles 并设置好监听端口手机把网络请求转发到电脑的监听端口再给手机安装并信任 charles 的根证书这样小程序在手机上的所有 HTTPS 请求都能在 charles 里看到明文。我主要用它确认三件事请求是否真的发出、响应码是什么、返回的数据结构是否和预期一致。这里提醒一下抓包只建议在开发调试环境里做别在公共网络下随意抓取生产环境流量更不要拿抓包工具去做任何越界的事情尤其是涉及用户隐私数据的场景务必谨慎。5.2 请求报错先查域名白名单再查证书我开发校车购票小程序的时候后端服务是部署在学校的一台服务器上的域名还没备案用的还是 IP 加端口的方式访问。结果真机一打开接口全挂报的是 10002 这类请求失败错误。这个问题的根源很简单小程序要求所有wx.request的域名必须在微信公众平台的“服务器域名”白名单里配置而且必须是 HTTPS还必须 ICP 备案。所以上线前一定要把域名和证书准备好开发阶段可以在开发者工具的“详情→本地设置”里勾选“不校验合法域名”但这只是临时挡箭牌真机正式版根本不吃这一套。我建议一开始就把后端对接的域名写成环境变量开发环境用一个测试域名生产环境用正式域名不要在代码里到处硬编码。5.3 分包与主包体积2MB 的紧箍咒如果你用 uni-app 开发小程序很多人会在打正式包时遇到source size 2612kb exceed max limit 2mb这类报错。微信小程序对主包体积的限制是 2MB超过就传不上去。校车购票项目本身不大但引入图表库、地图组件、二维码库后很容易逼近上限。我的处理方案是“能拆就拆”静态图片全部走图床或者 CDN本地只保留启动图和默认头像。二维码生成库放到子包或插件里按需加载购票详情页才用到。页面多时启用分包首屏只放首页和购票页其余页面归入subPackages。这里顺便说一句uni-app 的微信小程序打包如果最终体积还是超了优先检查iconfont、组件库和冗余插件这三个是体积大头。5.4 表单控件在不同机型的偏移问题购票页我用radio-group做座位选择测试时发现部分安卓机型上点击座位号后页面会出现轻微偏移尤其是当页面里有输入手机号的input组件时。这个现象基本可以归因于键盘弹起和滚动锚点的问题。我的解决方案是给页面设置固定的滚动区域而不是让整个page滚动。购票页的核心内容包一层scroll-view并设置scroll-y键盘弹起时用adjust-position控制页面是否自动上推。另外表单控件尽量用微信原生组件不要用input套view的自定义方案原生组件在键盘交互上处理得更成熟。5.5 一些容易被忽略的边界场景苹果手机的防截屏策略在小程序里没有统一开关验票二维码不要依赖截屏传播用一次性 token 保证安全。蓝牙定位、天地图这类能力如果校车项目用不到不要提前集成每多一个插件就多一份审核和兼容性风险。模拟器和真机上的录音、摄像头 API 行为有差异涉及媒体能力的功能一定要早点上真机验证别在模拟器里做完才发现封装不对。6. 项目复盘与扩展方向6.1 技术选型复盘原生还是 uni-app如果回到项目最开始让我重新选一次我还是会选原生微信小程序加简单后端。原因很简单校车购票页面的交互并不复杂原生框架完全扛得住而且原生小程序在调试、性能、审核兼容性上都少一层中间层的麻烦。但如果你团队的成员普遍熟悉 Vue或者你以后打算把同一套代码发布到支付宝小程序、抖音小程序那uni-app会更合适。校车购票这类管理工具型应用关键决策点是“开发速度”而不是“框架先进性”。一个五六个人的校园学生会团队我会推荐所有人先用原生写一遍把wx.request、wx.login、rpx、setData这些基本功打牢再考虑跨端框架。6.2 从校车到校园出行可扩展的功能方向校车购票跑通之后后续可以加的方向其实很多实时位置共享车辆发车后展示司机位置减少等人的焦虑可以用地图组件或者微信的实时定位能力。失物招领在订单上关联“乘车物品登记”下车后发现东西丢了可以通过管理后台回溯班次。消息通知体系从“发车提醒”扩展到晚点通知、停班通知订阅消息的模板可以按场景分开申请。学生认证用学号加姓名做实名认证减少乱占座问题。这个可以考虑对接学校统一身份认证系统但要注意隐私和授权合规。数据看板管理者后台按月导出收入、上座率、热门线路排行方便学校调整班次。这些扩展方向里我觉得最实用的是实时位置因为校车最大的不确定性就是“车到底到哪了”。技术上可以用微信小程序的wx.startLocationUpdate配合腾讯地图或者天地图实现车辆轨迹展示但要注意后台持续定位的耗电和隐私授权说明上线前要写清楚使用场景。6.3 个人心得给想做校园小程序的同学的建议最后说几句实在话。校园类小程序是新手练手非常好的切入点因为需求真实、用户就在身边、反馈快。但我在这个项目里最大的体会不是技术而是“别过度设计”。校车购票小程序本质上就是一个订座位加收钱的小工具。你可以给它加上各种酷炫的动画、复杂的会员体系、智能推荐但用户只关心三件事有没有票、多少钱、怎么退。与其把时间花在锦上添花的功能上不如把支付回调、余票扣减、状态机这三条核心链路靠扎实。这三条链路不出问题项目就成功了百分之八十。有一点我想特别提醒如果你是按课程设计或者毕设来写这个项目尽量在代码里把事务、状态机、接口鉴权这些点做扎实。答辩老师看重的不是你用了多新的框架而是你能不能讲清楚“一个订单从创建到完成中间经过哪些环节、每个环节怎么保证不出错”。我在实际开发中的体会是小程序项目最怕的不是不会写代码而是需求没想清楚就动手。你先把校车购票的用户故事一条一条列出来——比如“学生周五下午三点之前退票两小时内到账”这种具体场景——再根据场景拆页面和接口整个开发会顺畅很多。等哪天学校的老师再不用微信群接龙了你就知道自己这几个月做的事确实解决了真实的问题。
返回列表