拓冰建站拓冰建站
首页 / 资讯中心 / 正文

从0到1搭建开源跑腿小程序:智能派单与同城配送系统全解析

简介同城配送作为现代物流末梢依赖高效的调度系统。本文从技术视角拆解一套开源跑腿小程序涵盖用户端、骑手端、管理后台的完整架构。核心围绕智能派单机制通过距离、负载、评分等多维度加权计算最优骑手结合Redis分布式锁与WebSocket实现实时订单流转。同时系统基于Spring Boot与原生微信小程序构建并解决地图选点、坐标偏移、消息推送等工程痛点。这套方案成本低、可控性强已被用于校园快递代取、社区团购等场景为开发者提供可复用的同城配送技术参考。 这事儿我太有发言权了。前年学校封闭管理那阵外面外卖进不来校内快递点又远学生取个件来回得走半小时。当时市面上现成的跑腿SaaS系统要么按单抽成高得离谱要么就是闭源黑盒想改个计费规则都得求着售后。后来我干脆自己撸了一套开源的跑腿小程序用户端、骑手端、管理后台全都有核心是智能派单加同城配送支持预约取件部署到自己服务器上成本几乎为零。这套系统后来被好几个校园团队拿去二次开发也用在了社区团购、同城急送这些场景里。这篇文章就是把整个项目的设计思路、关键代码实现、部署上线时踩过的坑完整记录下来适合想自己搭跑腿平台的技术同学、创业团队也适合正在做小程序毕设、想找个真实项目练手的学生。不管你是刚接触小程序开发还是已经有一定后端经验照着这篇文章的思路走一遍都能把这套系统跑起来而且能看懂每一行代码背后的为什么。1. 项目整体设计与思路拆解1.1 为什么选择自建一套跑腿系统做这个项目之前我对比了市面上几乎所有跑腿平台的实现方式。当时摆在面前的有三条路一是直接用第三方SaaS比如微盟、有赞那种但它们的跑腿模块通常按订单抽成5%到10%而且用户数据、骑手数据全在别人手里二是买一套商业源码价格从几千到几万不等坑的是很多商业源码其实就是开源的换皮还藏着后门三是自己从零开发技术难度有但胜在完全可控。选择完全自研核心考量是可控二字。在这个模式下商家不需要向平台方缴纳任何抽成所有用户手机号、订单数据、骑手接单习惯都存在自己的数据库里后期想加个拼单功能或修改计费规则都是改自己代码的事情。这套系统我采用的是前后端分离架构用户端和骑手端各自独立为小程序服务端统一处理业务逻辑。前端小程序使用原生框架开发后端使用Java Spring Boot数据库用MySQL缓存和派单队列用Redis。1.2 用户端、骑手端、管理后台的三角架构整个系统的核心流程是这样的用户在小程序里填写寄件信息系统预估价格用户支付完成后订单进入派单池。派单引擎根据骑手位置、负载、评分等维度计算出最优骑手通过WebSocket推送派单通知。骑手接单后上门取件、配送、送达确认用户端实时看到骑手位置和订单状态。用户端小程序包含的主要模块我整理成了表格模块核心功能关键实现首页下单填写寄件/收件地址、物品类型、预约时间地图选点、智能计费订单管理订单列表、详情、取消、评价状态机驱动个人中心登录、余额、优惠券、地址簿微信授权登录实时跟踪骑手位置、配送进度WebSocket坐标推送骑手端小程序相对更简单核心是接单工作台和配送流程。骑手打开小程序后切换到听单状态系统才会给他派单。接单后按流程操作到店取件、确认送达全程需要上传商品照片作为凭证。骑手端还有一个关键模块是钱包每完成一单跑腿费加小费打入余额满一定金额后可以提现到微信零钱。1.3 技术选型的关键决策技术选型上前端小程序没选uni-app或者Taro而是直接用原生开发。原因很实在跑腿场景涉及地图选点、实时定位、WebSocket长连接这些能力原生小程序对这些底层API的封装最完善遇到问题排查起来也容易。用跨端框架确实能一套代码多端复用但代价是每端都要做兼容适配遇到地图这种重度依赖系统能力的组件跨端框架往往力不从心。后端选择Spring Boot说实话在这个项目里有点杀鸡用牛刀但考虑到后续可能要接支付、对接物流系统、做数据分析Spring Boot的生态能省下大量时间。数据库表结构设计时我重点考虑了订单表的高频读写做了分表预留Redis主要用于三块骑手实时位置缓存、派单队列、分布式锁。地图服务选的是微信小程序内置的腾讯位置服务一是因为小程序map组件原生支持不用额外引入SDK二是腾讯位置服务的WebService API可以免鉴权直接在前端调用省去了服务端中转的开销。关于地图组件很多新手会问能不能用天地图或者接入高德。微信小程序的map组件底层是腾讯地图这是无法替换的。但如果只是想在页面里嵌入一个完整的地图页面微信在2022年后开放了 半屏小程序 能力可以跳转到腾讯地图小程序或者用web-view承载天地图的网页版。就跑腿场景而言官方map组件加chooseLocation选点已经足够没必要引入额外复杂度。2. 用户端核心功能解析与实现要点2.1 下单流程从地址填写到智能计费下单是整个用户端最复杂的流程也是产品设计的核心命脉。用户打开小程序后首页会默认展示一张地图地图上标记了附近的骑手分布脱敏后的虚拟头像。用户点击我要寄件进入地址填写页寄件地址可以手动输入也可以直接在地图上选点收件地址同理。这里有个体验细节用户每次用地图选点后系统会自动将最近一次使用的地址存入地址簿下次下单时直接一键填充。计费逻辑是跑腿平台最容易产生纠纷的地方我在设计时尽量做到了透明。费用 基础配送费 里程费 超重费 时段加价其中/** * 计算订单配送费用 * 基础价覆盖3公里超出部分按公里计价 */ public BigDecimal calculateFee(OrderDTO order) { // 基础配送价覆盖3公里 BigDecimal baseFee new BigDecimal(5.00); // 超出里程费 double distanceKm order.getDistanceKm(); BigDecimal mileageFee distanceKm 3 ? new BigDecimal(1.5).multiply(BigDecimal.valueOf(distanceKm - 3)) : BigDecimal.ZERO; // 超重费每超出1公斤加1元 BigDecimal weightFee order.getWeightKg() 5 ? new BigDecimal(1).multiply(BigDecimal.valueOf(order.getWeightKg() - 5)) : BigDecimal.ZERO; // 夜间时段加价 LocalTime now LocalTime.now(); boolean isNight now.isAfter(LocalTime.of(21, 0)) || now.isBefore(LocalTime.of(6, 0)); BigDecimal timeFee isNight ? new BigDecimal(3.00) : BigDecimal.ZERO; return baseFee.add(mileageFee).add(weightFee).add(timeFee); }前端在下单页也会根据用户选择的两点距离做一次同样的预计算目的不是省后端请求而是让用户在确认订单前就能看到价格避免支付时才发现金额和预期差太多。这里要注意前端预计算和后端实际计算必须保持同一套规则否则用户截图投诉价格不一致时你得花很大精力去解释。下单时如果用户选择了预约取件订单会先进入待指派状态但不会立即推送给骑手。系统会启动一个定时任务在预约时间前30分钟检查一次订单状态如果还没被用户取消就将订单推入派单池。这个30分钟的提前量是我经过多次实测调整出来的太早了骑手到店后用户还没准备好太晚了派单压力集中骑手接单响应率下降。2.2 微信登录与手机号绑定小程序的用户体系直接用微信登录是最省事的。2022年之后微信收紧了getUserProfile接口现在推荐的做法是静默登录加手机号快捷绑定。流程是这样的前端调用wx.login拿到code传给后端后端用code换openid和session_key之后前端所有请求都带上这个openid作为用户标识。手机号绑定则用微信提供的getPhoneNumber能力用户点一下授权前端拿到加密的手机号数据后端解密后存库。// 前端获取微信登录code wx.login({ success: async (res) { const { code } res; // 将code发送到后端换取登录态 const loginRes await request(/api/auth/login, { method: POST, data: { code } }); if (loginRes.code 0) { wx.setStorageSync(token, loginRes.data.token); } } });这里有个细节容易被忽视getPhoneNumber的返回值在基础库2.21.2之后变成了CloudID格式需要在小程序云开发环境中解密自建后端接口需要配合wx server-sdk使用。如果你是自建服务器且没有接入云开发建议引导用户走微信授权手动输入手机号短信验证的方式真实验证后绑定体验略差一点但稳定可靠。2.3 订单状态机设计与实时跟踪订单状态是整个系统的核心流转逻辑我把它设计成一个完整的状态机每个状态的跳转都对应一个事件而且规定了状态只能按特定路径流转待支付 - 待指派 - 已指派 - 骑手取件中 - 配送中 - 已送达 - 已完成 - 已取消用户主动或超时 已指派 - 骑手到店 - 已取件 - 配送中 - 已送达 配送中 - 异常申报 - 平台介入状态机的设计原则是任何状态之间的跳转必须通过事件触发且事件必须携带操作人ID和备注。这样做的好处是每个订单从诞生到完成的所有操作都有迹可循后续如果出现纠纷直接翻订单日志就能定位是哪一环出了问题。实时跟踪的实现实质上是一个WebSocket技术栈加上定时上报机制的组合方案。骑手端每5秒上报一次经纬度后端将这些坐标写入Redis键名是rider:location:{riderId}值是一个JSON字符串同时通过WebSocket推送给关联订单的用户端。用户端收到坐标后更新地图上的骑手位置标记。这里有个性能优化点全量推送是浪费资源的后端维护一个在线用户地图比如哈希表存储userId和WebSocket会话的映射只推送和骑手有活跃订单关联的用户而不是广播所有坐标。3. 骑手端与智能派单系统深度拆解3.1 骑手工作台与听单模式骑手端工作台的设计有一个关键交互点听单开关。骑手打开小程序后首页是一个大按钮开始接单点击后按钮变为听单中状态系统才会把订单派送给他。为什么要设计这个开关因为骑手不是全天都在线的每天中午和傍晚是两个高峰时段到了深夜反而希望休息一会儿。如果系统不管三七二十一乱派单骑手要么天天拒单导致信用分下降要么直接被惹恼卸载应用。骑手端工作台需要展示的核心数据有四个今日接单量、今日收入、当前订单数、累计好评率。这四个指标支撑了骑手对当天工作的整体判断。尤其是当前订单数这个指标和派单算法的负载评估直接挂钩——系统在计算派单权重时会优先把订单派给当前订单数少的骑手而不是只盯着距离最近的那一个。骑手接单后订单详情页会展示完整的取件和送件信息包括地址、联系人、电话、备注以及关键的时间约束取件时间和送达时限。页面顶部是导航按钮点击后调起地图跳转这里我用的是微信的wx.openLocation接口直接打开腾讯地图导航不需要额外引入地图SDK也不用担心隐私合规问题。3.2 智能派单算法的核心逻辑距离、负载、评分的平衡智能派单是整个系统最有含金量的环节也是最容易翻车的部分。刚开始我做的是就近派单也就是谁离取件点近就派给谁。但上线后很快就发现问题位置最近的骑手可能已经手上压着5单新订单再派给他他根本送不过来而离得稍远一点的骑手可能正在空闲等单。用户端看到的体验就是骑手取件特别慢配送超时严重。后来我把派单改成了综合评分制不再单一参考距离而是设计了多维度的评分模型/** * 骑手综合评分用于智能派单排序 * score越高越优先派单 */ public double calculateDispatchScore(RiderCandidate rider, DispatchContext ctx) { // 距离得分越近越高5公里外衰减为0 double distanceScore Math.max(0, 1 - rider.getDistanceToPickup() / 5000.0); // 负载得分当前订单数越少越高 double loadScore 1 - (double) rider.getCurrentOrders() / rider.getMaxOrders(); // 服务评分历史好评率 double qualityScore rider.getRating() / 5.0; // 顺路得分取件点是否在骑手当前配送路线上 double directionScore rider.getDirectionMatch(ctx.getPickupPoint(), ctx.getDropoffPoint()); // 权重分配可配置 return 0.4 * distanceScore 0.3 * loadScore 0.2 * qualityScore 0.1 * directionScore; }这套评分模型上线后配送准时率提升了差不多15%。核心思路就是四个字综合权衡。距离重要吗重要权重最大。但如果只看距离系统就会陷入局部最优远距离的骑手永远接不到单。加入负载维度后骑手之间的接单量相对均衡不会出现累的累死、闲的闲死的情况。服务评分则是对好骑手的正向激励好评率高的骑手能接到更多单收入自然水涨船高形成良性循环。派单的超时转派逻辑也值得一提。当系统选定最优骑手并通过WebSocket推送派单消息后会启动一个60秒的倒计时。如果骑手在60秒内没有任何操作系统自动将该订单转派给综合评分第二高的骑手同时给第一候选骑手打上一次未响应的标记。连续3次未响应的骑手系统会强制将其转为休息状态避免骑手挂机不接单影响用户体验。3.3 订单并发控制一单不能派给两个人派单系统最常见也是最严重的问题就是同一个订单同时派给了多个骑手造成抢单冲突。这个问题的根源在于并发两个骑手同时点击接单后端同时处理如果不加以控制就会出现都认为订单是自己的的情况。解决并发问题我用Redis分布式锁加数据库唯一索引双重保障// Redis分布式锁保证同一订单只能被一个骑手接取 public boolean tryAssignOrder(Long orderId, Long riderId) { String lockKey order:assign: orderId; // 加锁30秒自动过期防止锁未释放导致死锁 boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, riderId.toString(), 30, TimeUnit.SECONDS); // 如果锁获取失败说明订单已被其他骑手先抢到 if (!locked) { String ownerRiderId (String) redisTemplate.opsForValue().get(lockKey); throw new BizException(订单已被骑手 ownerRiderId 接取); } return true; }光有Redis锁还不够保证100%可靠因为极端情况下Redis服务重启会丢失锁状态。所以订单表里增加了rider_id和status两个字段的联合唯一索引——数据库层面保证同一个订单不可能被两个骑手同时更新成已接单状态。这种缓存锁数据库唯一索引的组合是我在多次压测后总结出来的最稳妥方案既不会因为缓存抖动放行重复接单也不会因为数据库压力过大拖慢接单响应速度。3.4 消息推送与状态同步的工程实践订单状态流转、派单通知、骑手位置更新这些都需要实时推送到小程序端。微信小程序有原生订阅消息能力但它有个很大的限制用户必须主动订阅一次服务端才能给用户发一条模板消息。这在预约取件提醒骑手已接单这些场景下比较鸡肋因为用户很多时候忘了订阅或者订阅次数已用完。所以我的方案是核心场景用WebSocket非核心场景用小程序的订阅消息做兜底。WebSocket在微信小程序端的API是wx.connectSocket需要在app启动时建立连接并且在网络状态变化时自动重连。这里有一个真实的坑小程序切到后台超过一定时间WebSocket连接会被系统主动断开重新切回前台后需要重新wx.connectSocket。我是在App的onShow生命周期里加了一次重连检测如果发现socket状态不是CONNECTING或OPEN就直接重连。WebSocket消息的格式我用的是JSON统一包含type和data两个字段。type表名消息类型比如ORDER_ASSIGN、ORDER_STATUS_CHANGE、RIDER_LOCATION_UPDATEdata是具体的业务数据。前端收到消息后根据type分发到对应的页面处理函数刷新页面数据。这种事件驱动的模式非常适合跑腿这种实时性要求高的场景。4. 实操过程与部署运行记录4.1 环境准备本地跑起来需要什么先列一份环境清单软件版本以我项目中使用到的为准软件版本要求用途JDK8运行Spring Boot后端Maven3.6后端依赖管理MySQL5.7订单、用户、骑手等核心数据Redis6.x位置缓存、派单队列、分布式锁微信开发者工具最新稳定版编译运行小程序Node.js16如果修改前端构建流程后端项目结构是按Maven标准布局的。clone代码后第一步要修改的是配置文件application.yml把数据源、Redis地址、微信小程序AppID/AppSecret、商户号等信息填进去。然后是建库建表项目sql/目录下有一个init.sql文件包含了所有表的建表语句直接在MySQL中执行即可。首次启动时会自动检查表结构缺少的表会自动创建但推荐手动执行init.sql这样能确保表字段版本和代码完全匹配。前端小程序这边用户端和骑手端是两个独立的项目。用微信开发者工具分别导入然后在小程序后台的开发管理-开发设置里配置服务器域名。这里有个要点WebSocket域名要从socket合法域名里配置和普通请求域名分开设置。如果你只是本地开发调试可以在开发者工具中勾选不校验合法域名但真机预览时必须配置真实域名。4.2 配置地图服务和支付地图服务使用腾讯位置服务需要到腾讯位置服务官网注册账号、创建应用拿到一个Key。小程序端需要在app.json里配置permission字段声明用户位置信息用途同时申请scope.userLocation权限。这块如果配置不正确最常见的结果是调用wx.getLocation时报错getLocation:fail the api need to be declared in the requiredPrivateInfos field。这个报错的具体原因是微信在2022年7月之后对隐私接口做了收紧需要在app.json的requiredPrivateInfos字段中显式声明使用哪些隐私接口并且要在隐私协议中说明用途。配置方法如下{ permission: { scope.userLocation: { desc: 您的位置信息将用于骑手接单和配送导航 } }, requiredPrivateInfos: [getLocation, chooseLocation, onLocationChange] }支付方面跑腿小程序的支付流程和普通商城类似核心区别在于跑腿订单是服务类商品微信支付的body字段不能写商品而是写配送服务名称。支付完成后小程序端会收到wx.requestPayment成功回调后端也会在支付回调地址收到异步通知。需要在配置文件里设置好回调地址并且后端在收到回调后要验签、核对金额无误后更新订单状态。这套流程我踩过一个坑支付回调地址必须是HTTPS协议而且域名必须和后台配置完全一致否则微信会静默丢弃回调请求。4.3 部署上线与二次开发建议部署方案我推荐用Docker Compose原因很简单服务少编排方便。项目根目录提供了一个docker-compose.yml里面定义了MySQL、Redis、后端服务三个容器执行docker-compose up -d就能一键启动所有依赖服务。如果你还没有Docker环境也可以直接裸机部署后端用mvn package打成jar包nohup java -jar errand-server.jar 启动即可。数据库初始化时建议导入sql/init.sql后再执行sql/init_data.sql插入一些初始的管理员账号和系统配置。注意有一个sys_config表里面存了计费规则、派单权重、骑手最大接单数等可配置项改这些配置不用重新发版系统每5分钟自动刷新缓存。对想二次开发的同学我建议重点关注三块一是计费规则可以结合自己的业务场景调整起步价、每公里单价二是派单权重修改calculateDispatchScore方法里的四个权重系数三是自定义配送状态比如增加骑手已到楼下正在爬楼这种更细粒度的状态节点。这些都是扩展性最强的部分代码层面已经预留了接口不需要改核心框架。5. 常见问题与排查技巧实录5.1 定位不准确、授权失败怎么办跑腿业务对定位精度的要求远高于普通应用骑手找不到取件点和用户收不到货之间的因果链条定位不准往往是第一环。实际开发中最常见的定位问题有三个没有声明requiredPrivateInfos导致报错使用了网页端的地图API但小程序端GET请求无法携带referer头导致部分API拒绝服务以及wx.getLocation返回的城市和实际所在城市不一致。关于城市不一致的问题我实测下来除了GPS定位小程序还会参考Wi-Fi信息和IP归属地来综合判断位置。在低精度模式下同一个地点可能返回不同的城市字段。解决办法是优先使用wx.chooseLocation让用户手动选点而不是完全依赖自动定位。骑手端的配送导航也建议在取件和送达时强制调用一次wx.getLocation重新校准而不是用之前缓存的位置。5.2 地图坐标偏移与导航偏差这个问题基本上每个做LBS场景的开发者都会遇到通常是GPS原始坐标用的是WGS-84坐标系而国内地图使用的是GCJ-02坐标系两者之间大概有几百米的偏移。微信小程序内置的地图组件和wx.getLocation接口返回的都是GCJ-02加密过的坐标所以小程序端之间直接调用不会有偏移问题。但如果你把坐标传给后端后端再去调其他地图服务商的API就可能出现坐标系不一致导致的偏差。解决方法是强制约定所有坐标在进入后端前统一转为GCJ-02存储后端不做坐标系转换只做透传。如果业务必须调用第三方地图服务比如调高德的路径规划API那就在服务端做一次WGS-84转GCJ-02的转换。这个转换算法网上有很多现成实现核心是一个偏移计算函数大约三十行代码就能搞定。5.3 WebSocket断连、消息丢失的实战调试WebSocket在跑腿系统里是核心中的核心一旦断连消息送不出去用户端就看不到骑手位置骑手端就收不到派单通知。我上线初期经历过一次事故“骑手明明接单了用户端却没有任何反应”。排查了一个下午最后发现是WebSocket的连接生命周期问题用户端小程序切后台超过5分钟后小程序被系统挂起WebSocket连接自动断开。等用户切回前台时onShow触发了但页面没有监听onSocketOpen事件导致前端没有主动重连。解决方案很简单全局监听WebSocket状态切后台不销毁连接只是暂停心跳切回前台时检测连接状态如果处于CLOSED或CLOSING状态立刻重连。同时后端在推送关键消息时做一次兜底——比如订单状态变更这类重要消息WebSocket推送成功后会同时给用户发一条订阅消息作为提醒。这样即使WebSocket有延迟用户也不至于完全感知不到状态变化。5.4 常见错误速查表错误现象可能原因解决办法调用getLocation报错app.json缺少requiredPrivateInfos补充隐私接口声明chooseLocation无法使用基础库版本过低升级基础库到2.9.0以上支付不回调服务器未开放回调端口或域名未备案检查80/443端口、支付回调域名一致性骑手收不到派单消息WebSocket未连接或订阅消息被禁用检查socket连接状态开启订阅消息授权定位偏了几百米坐标系不一致统一为GCJ-02坐标系订单被重复接单未加Redis锁或锁失效保障Redis可用性使用数据库唯一索引兜底价格和前端显示不一致计费规则未同步更新前端预计算和后端计算共用一套规则这套跑腿小程序整体开发加调试前后花了大概两个月。实际运行中我认为最值得投入精力的还是派单算法的调优——它直接决定了用户体验和骑手留存。如果你正准备做类似项目我给的建议是先用最简单的就近派单跑通整个流程等订单量起来了再逐步引入综合评分、转派这些高级特性。所有代码都是开源可改的遇到问题欢迎在项目仓库提issue我看到了都会回复。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门