ARTICLE DETAIL

资讯详情

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

校园跑腿外卖一体化完整源码:订单、支付、抢单全链路拆解

校园跑腿外卖一体化完整源码:订单、支付、抢单全链路拆解 大学校园里最真实的痛点大概就是饭点食堂排队、外卖进不了校门、快递驿站排长龙。把这些需求揉在一起做的校园跑腿外卖一体化系统这几年在各高校里几乎成了刚需。我最近把一套完整跑通的源码重新整理了一遍从下单、抢单、支付到结算前端小程序、骑手端、管理后台全都齐活。这篇就把整个项目的架构设计、核心源码逻辑、部署上线流程和踩过的坑完整拆出来给正在做课程设计、毕业设计或者真打算在校园里做点小生意的同学一份可以直接抄作业的参考。这套系统的核心价值在于它不是简单复刻一个美团外卖而是针对校园场景重新设计了配送模型外卖配送、快递代取、超市代买、文件同送全部跑在一条订单链路上。源码层面用Spring Boot做后端小程序端基于UniApp可以同时编译到微信和支付宝管理后台是Vue3全家桶。下面我把整个设计思路和源码实现层层拆开讲。1. 项目定位与技术选型先把场景和需求拿捏住1.1 校园场景拆解这活儿为什么不能直接套用美团模式很多人一开始的想法是照着外卖平台抄一套就行真正动手才发现校园环境有大量边界条件。商业外卖的核心是商家出餐加骑手配送而校园跑腿是人找人的服务需求种类极其碎片化。代取快递需要看驿站营业时间代买零食需要知道超市货架位置帮带食堂饭菜需要精确到窗口。这些需求在传统外卖系统里根本没有对应的数据模型。校园的另一个特征是地理范围小但人流密度高。一个校区可能就两三条主干道但宿舍楼、教学楼、食堂、驿站分散在不同区域配送距离通常不会超过两公里但高峰期订单会集中在极短的午饭和晚饭时段爆发。这种短时高并发对订单系统的压力点和商业外卖完全不同抢单机制、运力调度、配送费计算都要重新设计。还有一层是信任问题。校园跑腿的骑手基本都是本校学生用户和骑手之间存在身份关联。系统必须有一套校园身份认证机制注册时要校验学号、姓名匹配订单完成后能互相评价。这些都决定了源码设计不能照抄通用电商系统得从底层表结构就开始针对校园场景建模。1.2 技术栈选型这套源码用了什么为什么这么选我对这套系统的技术选型核心原则是不追新、只求稳、源码要能看懂能改。后端用Spring Boot 2.7 MyBatis-Plus这个组合的资料多、上手快出了问题随便一搜就有解决方案。数据库用MySQL 8.0订单这类强事务数据必须走关系型数据库。缓存和分布式锁用Redis抢单场景离了它根本扛不住。小程序端用UniApp一套代码编译到微信小程序和支付宝小程序省去双端维护成本。管理后台用Vue3 Element Plus表格表单类页面开发效率极高。这套选型在实际开发中有几个很实际的好处。MyBatis-Plus的单表CRUD几乎不用写SQL能让精力集中在订单流转和并发控制上。UniApp对校园这类小团队项目特别友好不需要维护两套前端代码改一个bug双端同时生效。Spring Boot的自动配置大大减少了配置文件量打包成JAR丢到服务器就能跑部署门槛低。模块技术选型选型理由后端服务Spring Boot 2.7 MyBatis-Plus生态成熟开发效率高数据库MySQL 8.0事务保证订单数据可靠缓存/锁Redis 6.x抢单原子操作热点数据缓存小程序端UniApp Vue3一套代码多端发布管理后台Vue3 Element Plus后台交互快速成型对象存储腾讯云COS用户头像、反馈图片存储支付微信支付V3 支付宝校园用户主流支付方式这里有个经验支付千万要提前申请好商户号我见过好几个项目代码全写完了卡在支付资质申请上拖了两周。学生主体可以申请微信支付但需要学校相关证明建议提前和辅导员沟通。1.3 整体架构一条订单链路把五个端串起来源码整体是经典的前后端分离单体架构加上简单的前端工程。单体架构在校园这种日单量几百到几千的场景下完全够用没必要一上来就搞微服务那只会增加部署和运维负担。系统的核心参与者有五个角色用户C端小程序、骑手端小程序、商家端小程序、管理后台、后端服务。用户下单后订单进入待抢池骑手端实时刷单抢单商家端处理出餐管理后台做审核和运营。这五个端通过后端REST API和WebSocket消息推送协同工作。订单数据流大致是小程序端调POST /api/order/create创建订单后端校验用户认证和基础参数后写库再通过RabbitMQ或Redis发布订阅模式通知骑手端有新人下单。骑手抢单成功后系统推送状态变更给用户。支付环节走微信支付前端发起下单支付后端接收异步回调更新订单状态。整个链路的状态变更都以数据库事务为准缓存和推送都只是辅助加速。2. 数据库设计与订单状态机的源头设计2.1 核心表结构与字段说明这套源码里最值得抄的就是表设计。一共21张表核心的几张我单独说一下。用户表user除了基础账号信息还存了学号、学校ID、校园认证状态骑手额外有骑手审核状态、接单开关、今日单数等字段。学校表college存校区坐标和配送范围边界用经纬度加半径圈定。订单主表orders是整张数据模型的心脏字段我列一下核心的order_no业务订单号、user_id下单人、order_type订单类型(1外卖 2快递 3代买 4同送)、pickup_address取货地址、delivery_address送达地址、goods_desc物品描述、amount商品金额、delivery_fee配送费、payment_amount实付金额、status订单状态、runner_id骑手ID、shop_id关联商家。快递代取还要单独存快递公司、取件码等字段我单独拉了一张express_info表存。商家表和菜单表比较常规值得注意是商家表要加merchant_type区分食堂窗口、校外商户、超市便利店。跑腿服务没有货架所以没有库存表但加了price_rule表管理不同距离区间的配送费阶梯价。钱包表wallet管理用户余额和骑手收入结算单独走settlement_record表不直接改主钱包避免算错账。表名核心字段作用useropenid, student_no, auth_status, role用户与骑手身份collegename, longitude, latitude, radius校区配送范围ordersorder_no, order_type, status, runner_id订单主状态流转express_infopickup_code, express_company, cabinet_no快递代取信息price_rulestart_distance, end_distance, price配送费阶梯计算settlement_recordorder_no, amount, status, type骑手钱包流水这个表结构我踩过一个坑一开始把送达地址只存了一个字符串后来发现配送费计算和订单统计都需要地址的经纬度。建议从第一版就把地址拆两列一列address_text展示用一列address_location存经纬度JSON。2.2 订单状态机的设计与流转控制订单状态是全系统最容易出bug的地方设计阶段就要把状态机定死不能在业务代码里随便改状态。这套源码的状态机如下待支付(0) - 待接单(1) - 已接单(2) - 配送中(3) - 已完成(4)取消则进入已取消(5)。退款单状态下单独有个refund_status字段不混在主状态里。待支付到待接单这一步是支付回调触发不能在前端点完支付就立刻改状态必须等微信异步通知到后端。已接单到配送中需要骑手点击已取货。配送中到已完成需要用户或骑手点击确认送达这里要做距离校验骑手必须进入送达地点的半径范围内才能触发确认按钮防止虚假送达。我还用一张order_status_log表把所有状态变更记下来每改一次状态就插一条日志。这个表在事后排查纠纷、统计订单时长时非常有用。很多跑腿平台后来出了问题对不上账都是因为没做状态日志。这块属于源码里看起来不起眼、但生产环境必不可少的设计。2.3 配送费计算与距离判定配送费不能直接用直线距离算校园里建筑密集用户从宿舍到驿站的实际步行距离和直线距离差很多。源码里用的是腾讯地图的步行路径距离API根据起终点经纬度拿实际步行距离再套用price_rule表中的阶梯规则。配送费规则我设计成了三段0到500米收2元基础配送费500米到1公里收3元超过1公里每500米加1元封顶6元。这个价格在校园场景里用户接受度较高骑手也愿意跑。阶梯规则不要写死在代码里放到数据库表里运营同学随时可以调价改完不用发版。距离判定在取货阶段也用到。用户下单时系统计算骑手当前位置到取货点的距离超过1.5公里的单子不推送给该骑手缩小抢单范围避免骑手接到太远的单配送时间过长。这块距离计算同样走地图API实测下来比用直线距离判断准确得多。3. 关键源码实现从下单到送达的完整链路3.1 下单流程与运力校验下单接口的核心逻辑不只是插入一条orders记录还要做几个前置校验用户是否完成校园认证、当前订单是否在配送范围内、用户有没有未完成订单。这部分用Spring AOP做一个拦截注解统一校验避免每个接口重复写。配送范围内校验用的是圆形范围判断:计算下单经纬度和学校圆心之间的距离超过半径就被拦截。public boolean checkInScope(double lon1, double lat1, College college) { double distance getDistance(lon1, lat1, college.getLongitude(), college.getLatitude()); return distance college.getRadius(); } private double getDistance(double lon1, double lat1, double lon2, double lat2) { double radLat1 lat1 * Math.PI / 180.0; double radLat2 lat2 * Math.PI / 180.0; double a radLat1 - radLat2; double b lon1 * Math.PI / 180.0 - lon2 * Math.PI / 180.0; double s 2 * Math.asin(Math.sqrt( Math.pow(Math.sin(a / 2), 2) Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2))); s s * 6378.137; return s * 1000; }下单完成后要立刻给骑手端发推送源码用的Redis的发布订阅模式。订单创建后把订单号发布到channel:new_order频道骑手端通过WebSocket订阅这个频道实时更新待抢单列表。比轮询数据库高效得多也不会对数据库造成压力。3.2 抢单锁的实现Redis Lua 与数据库状态双保险抢单是整个系统并发压力最大的点也是最容易出事故的环节。两个骑手同时抢一个单如果只靠业务代码里先查再改必然出现超卖。我当时在设计抢单逻辑时直接上了双保险Redis分布式锁加数据库乐观锁。先看数据库这层抢单本质是一个条件更新把订单状态从待接单改成已接单同时锁定抢单骑手ID。用MyBatis-Plus写一个UpdateWrapper条件带上status 1如果影响行数为0说明订单已经被别人抢走。boolean grabbed updateOrderStatus(orderId, runnerId); // SQL: UPDATE orders SET status 2, runner_id ?, // grab_time NOW() // WHERE id ? AND status 1 if (!grabbed) { throw new BizException(手慢了订单已被抢走); }数据库这层已经能保证不超卖但我还加了一层Redis锁做过滤防止大量骑手同时打到数据库造成压力。Redis这里用的不是简单的setnx而是Lua脚本保证原子性避免锁过期和误删问题。-- KEYS[1] order:lock:{orderId} -- ARGV[1] runnerId if redis.call(exists, KEYS[1]) 1 then return 0 end redis.call(set, KEYS[1], ARGV[1], EX, 30) return 1Redis锁拿到后执行数据库更新更新成功才释放锁。锁的过期时间设30秒正常情况下抢单操作几毫秒就完成所以设太短会导致订单还没更新完锁就过期被别的骑手二次获取设太长万一持有锁的线程崩溃订单锁死30秒又会造成体验问题。30秒是我权衡后的经验值实际操作中几乎没出过问题。3.3 支付回调的幂等处理支付这部分是源码里最需要谨慎的地方。微信支付V3的异步回调同一个支付结果可能推送多次回调处理如果不做幂等订单状态会被重复更新对账也会出问题。回调的验签流程一定要完整走拿到回调头部的Wechatpay-Signature、Wechatpay-Timestamp、Wechatpay-Nonce用商户平台下载的微信支付平台证书验签验签通过后再解密报文。这个步骤不能在测试阶段偷懒我见过直接把验签注释掉上线的情况结果被伪造支付回调刷了大量订单。验签通过后真正的业务逻辑只有两步先查订单当前状态如果已经是已支付就直接返回SUCCESS不再走后续逻辑如果是待支付状态才执行更新状态、增加钱包流水、通知骑手端。PayOrder order payOrderMapper.selectByOutTradeNo(outTradeNo); if (PayStatus.PAID.equals(order.getStatus())) { return SUCCESS; // 幂等返回 } // 只有在未支付状态下才更新 order.setStatus(PayStatus.PAID); orderMapper.updateById(order); // 同步订单状态通知骑手端有新单可抢 orderService.afterPaid(order.getOrderNo());回调接口还有一个关键点接口返回给微信的应答必须是纯文本SUCCESS或FAIL不能返回JSON格式的success微信那边不识别会一直重试。这个坑我调试了整整一下午才发现是返回格式的问题。3.4 消息推送与骑手定位的轻量实现消息推送在源码里用的是WebSocket而非第三方推送SDK这样省去额外费用并且校园场景下前后端连接比较稳定。骑手端小程序连接WebSocket后订阅个人通道订单创建、订单取消、用户催单都会实时推给对应骑手。定位功能用微信小程序自带的地图组件骑手APP端每隔10秒上报一次经纬度。用户端能看到骑手动线这是用户体验的重要组成部分。后端接收位置上报的接口只做两件事更新redis里骑手位置缓存把最近位置写入location_log表。骑手接近取货点和送达点时会触发距离判断做到达提醒。真正要注意的是websocket线程安全。一个订单可能同时有用户端、骑手端多个连接在监听推送时必须保证同一个事件按顺序发出否则状态错乱。源码里的做法是每个订单维护一个队列事件按序入队再推送实测下来消息不乱序。4. 部署上线与性能调优源码跑起来只是第一步4.1 本地开发环境与一键启动配置源码拿到手先在本地跑通别急着上服务器。后端环境需要JDK 1.8以上、Maven 3.6、MySQL 8.0、Redis 6.x。数据库脚本在doc/sql目录下直接导入即可。启动前要改application.yml里的数据库账号密码和Redis地址。小程序端需要安装HBuilderX导入uniapp目录后运行到微信开发者工具。需要注意的是微信小程序必须在开发者工具里配置合法域名本地调试可以勾选不校验合法域名但上线前必须换成正式的HTTPS域名。另外appid要换成自己的不能直接依赖源码里的测试号。这里有个小经验本地调试时可以启动一个Nginx把后端接口反向代理到http://localhost:8080同时配好HTTPS证书这样小程序端的请求路径就不用改来改去提交上线时直接切到服务器IP就行。4.2 服务器部署与HTTPS / 小程序合法域名部署方案是数据库、Redis、后端JAR分别部署在云服务器上用Docker Compose管理配合Nginx做反向代理。小程序要求所有请求域名必须是HTTPS证书一个域名就能覆盖API和后端静态资源。Docker Compose编排文件主要包含三个容器MySQL、Redis、后端服务。数据库和Redis用数据卷持久化后端服务镜像在启动时自动拉取最新JAR。services: mysql: image: mysql:8.0 restart: always ports: - 3306:3306 volumes: - ./mysql-data:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: your_password MYSQL_DATABASE: campus_errand redis: image: redis:6.0 restart: always ports: - 6379:6379 backend: build: . restart: always ports: - 8080:8080 depends_on: - mysql - redis environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/campus_errand SPRING_REDIS_HOST: redis部署上线后第一步不要急着做功能测试先把健康检查接口调通。Spring Boot的actuator的/actuator/health返回UP说明数据库、Redis部分连接成功再走一遍微信支付的测试下单流程。4.3 索引优化和缓存策略实战订单表是核心表数据量上来之后查询性能直接决定系统能不能用。orders表上我建了联合索引(user_id, status)和(status, create_time)。骑手抢单页的列表查询走的是(status, create_time)索引用户历史订单走(user_id, status)索引。缓存方面不是所有数据都要缓存。菜品菜单这类读多写少的接口做了Redis缓存过期时间设5分钟保证数据变更能在较短时间内生效。用户信息缓存30分钟登录状态缓存2小时。有个经验一定要分享订单数据千万不要整单缓存订单状态是实时变的数据一缓存就等着出各种脏读问题。我做的是把订单列表页的摘要信息缓存进入详情页时实时查库。这样既减轻数据库压力又保证最关键的状态信息是最新的。4.4 安全与风控防刷单、防参数篡改校园跑腿系统最怕的是刷单。用户端创建订单接口做了频率限制同一个用户10秒内最多创建5次订单超过就拒绝。这里用Redis计数器实现每单唯一校验码绑定用户和订单。支付金额这块必须防篡改前端传过来的paymentAmount只能作为展示参考后端要重新计算一遍——商品金额从数据库商家菜单里取配送费从配送距离实时算两个值相加才是真实应付金额。前后端金额不一致直接报错。这个校验是支付安全的基础防线。骑手结算安全要防的是刷单行为。发的结算规则是订单完成30分钟后跑腿费自动从冻结钱包转成可提现金额30分钟足够取消纠纷处理。同一台设备频繁切换账号也会被风控系统捕捉直接封禁设备。这套逻辑代码量不大但是对平台资金安全特别重要。5. 常见坑点与二次开发避坑指南5.1 高频问题速查表问题现象根因解决方案支付回调一直失败没有返回SUCCESS文本接口返回纯文本SUCCESS别返回JSON骑手抢单提示失败未开启Redis持久化配置appendonly yes重启不丢锁数据订单创建成功但骑手端看不到WebSocket连接断开未感知心跳机制30秒没心跳主动重连用户支付成功但订单状态未变回调验签失败检查平台证书序列号是否配置正确确认送达按钮一直置灰坐标偏差导致距离校验失败送达半径从50米放宽到100米小程序请求被拦截没有配置合法域名在小程序后台配置request合法域名最有意思的坑是确认送达按钮的问题。调测的时候发现测试机定位飘了100多米用户明明到了驿站但一直点不了确认送达。后来在送达判断时加了判断条件用户坐标或骑手坐标有一方在校验半径内就算送达不再要求双方都在半径内问题就解决了。5.2 源码二次开发的三条经验第一改订单状态前先查状态日志。任何针对orders表的update操作写代码前先想想这个状态变更应该由哪个角色、哪个业务动作触发禁止在多处代码里写死状态流转逻辑否则后续查问题会非常难。第二加新功能优先考虑在现有表上扩展字段不要轻易动订单主状态机。比如跑腿代买需要小费功能只用在orders表加一个tip_amount字段不要新搞一套状态。状态机改动牵一发动全身改不好就是连环bug。第三学会看日志定位问题。这套系统几乎所有接口都打了一条info级日志里面带着订单号和关键参数。线上排查问题的时候先tail -f日志通过订单号能搜出完整请求链路。日志一定要打印关键业务参数不要图省事只打印空泛的success。5.3 二次开发的扩展方向这套跑腿外卖一体化源码能扩展的空间非常大。现在单校区跑可以考虑多校区模式——college表已经是独立设计只要扩一个校区管理员角色就能切换管理后台的校区维度。另一个方向是拼单和拼团。同一个宿舍楼的人可以拼单点同一家食堂订单合并配送骑手一趟能送好几个单同时降低用户的配送成本。这个功能需要加一个拼单组表和合并支付逻辑但订单主链路不用大改。再就是反作弊升级。当前的风控逻辑比较简单如果想更稳可以引入设备指纹和位置轨迹分析识别异常频繁的换单、异常短时间的刷单行为。这个方向适合做毕设的同学展现实力能加分不少。这套项目做完调试上线我最大的体会是做校园类项目源码技术难点其实就集中在并发控制和状态管理上真正绕不开的是对场景的理解度。骑手为什么抢单慢、用户为什么催单、商家为什么出餐慢这些运营层面的问题最后都会转化成技术方案——缓存策略、推送时机、审核规则。源码能跑只是及格跑得稳定、跑完还能让运营同学用起来顺手这才是做完一个校园项目的真正标准。最后再提醒一句数据库和Redis的备份策略一定要提前设好我的服务器曾经因为磁盘满了导致MySQL写不进去当时正在做结算好在一键备份脚本派上了用场才没让账目对不上。这个血的教训希望你们不要经历第二次。
返回列表