
简介这是一套完整的外卖平台全端源码涵盖用户端、商户端、配送端以及小程序与APP面向需要搭建或二次开发外卖业务的开发者、创业团队与技术运维人员。系统采用前后端分离与混合开发模式开源程度高可自由修改代码以满足区域化运营、骑手调度、商家管理等场景需求。压缩包共2000个文件以1490个JavaScript、419个HTML、84个CSS及少量XML配置为主覆盖后端逻辑、管理后台页面、移动端界面与项目配置整体约137MB目录结构清晰包含多套前端构建产物与小程序工程目录便于直接部署或对照学习。已有933人学习下载适合具备一定Web开发基础的中高级开发者深入研究。通过源码可完整了解外卖平台从商品管理、订单流转到配送履约的链路设计掌握混合APP与小程序联调方法并基于开源代码快速构建属于自己的外卖系统节省从零开发的成本与时间。1. 整站源码食刻外卖系统它为什么值得你花时间跑一遍整站源码食刻外卖系统说白了是一套把用户端、商户端、配送端全塞进同一个开源仓库的外卖系统源码附带微信小程序和APP两端。它想解决的问题很直接你不用从零设计订单状态机、商户结算、骑手调度这些外卖业务的骨架拿到就能跑、改改就能运营。适合三类人看要做同城配送或外卖业务的创业团队、需要给学生或新人做项目练手的导师、以及想在开源外卖系统基础上做二次开发的个人开发者。下面按“先看清结构、再跑通、再联调、最后避坑”的顺序展开全部用最常见的 Java Spring Boot MyBatis MySQL 技术栈来举例。2. 拆开整站源码目录四个端各管什么为什么订单状态机是命根子拿到任何一套整站源码第一件事不是急着启动而是先把目录结构和模块边界摸清楚。食刻外卖这类系统目录里通常会有四个明显的大块后端服务、商户端、配送端、用户端小程序和APP。如果一上来就npm install加mvn spring-boot:run四端同时启动最后大概率是被各种联调错误搞得一头雾水。2.1 后端服务订单、商品、结算的包结构长什么样后端是整个系统的“业务命根子”订单、商品、门店、骑手、结算这些数据模型都在这。常见做法是 Spring Boot MyBatis MySQL Redis 的组合包结构一般按业务域拆而不是按三层架构拆。也就是说你看到的不是 controller/service/mapper 三个大包而是 order、shop、product、user、rider、pay 这样的业务包每个包内再自己消化 controller、service、mapper。这种拆法在开源系统里很常见和市面上那些 Java 开源多商户商城源码的思路同源目的都是让每一块业务能独立维护想砍掉某个模块时不用牵一发动全身。典型的包结构长这样com.shike ├── order │ ├── controller/OrderController.java // 下单、订单列表、订单详情 │ ├── service/OrderService.java // 订单状态流转的核心逻辑 │ ├── entity/Order.java // 订单主表 │ └── mapper/OrderMapper.java ├── shop │ ├── entity/Shop.java // 门店、营业状态 │ └── service/ShopService.java // 开关店、审核 ├── rider │ ├── entity/Rider.java │ └── service/RiderService.java // 骑手位置上报、接单、送达 ├── pay │ ├── controller/PayNotifyController.java // 支付回调入口 │ └── service/WxPayService.java // 微信支付下单与验签 └── user ├── entity/User.java └── service/UserService.java这个结构的核心是 order/service/OrderService.java 里的状态流转。外卖订单不是简单的新增和删除它有一个明确的生命周期待支付 → 已支付/待接单 → 已接单 → 配送中 → 已完成中间还穿插着用户取消、商户拒单、超时自动取消这些分支。如果你的业务方改一下状态定义比如要加一个“备餐中”状态你需要同时改状态枚举、状态机流转逻辑和所有端的状态展示这三个地方如果不同步订单就会卡死在某个环节。所以我建议拿到源码后第一步就全局搜订单状态枚举先把状态列表打印出来贴在显示器边上。2.2 商户端菜品上下架与营业状态不是简单的前端页面商户端面向的是餐厅老板或店员常见形态是 Vue 写的管理后台也有人叫商家版 Web。它的核心职责有三个菜品管理、订单处理、营业状态控制。菜品管理包括新增、改价、上下架、库存同步订单处理是查看新订单、接单、拒单、打印小票营业状态控制则决定了一个门店今天是否对外营业、高峰期是否临时休市。很多从用户端入手看源码的人会低估商户端的复杂度。订单列表要实时刷新不能靠用户手动刷新页面菜品上下架要和用户端的小程序菜单实时一致不然用户在小程序里能点到商户已经下架的菜营业状态的切换要影响新订单的进入但又不能影响已经在配送中的订单。这里的重头戏是“菜品上下架状态与用户端缓存的一致性”。常见实现是商户端改状态后写一条 Redis 消息或直接更新缓存用户端读菜单时先走缓存、缓存没有再查库。调试时最容易翻车的点是缓存里头的菜品数据没刷掉用户端还能看到下架的菜下单后商户端又显示不出来。遇到这种情况先看 Redis 里对应的门店菜单 key 是否过期别急着改代码。2.3 配送端骑手位置上报与抢单/派单的数据流配送端是外卖系统里最容易和网约车app开发里的派单逻辑做类比的部分。骑手端要处理四件事位置上报、接单/抢单、取货确认、送达确认。位置上报是高频的一般每 3 到 5 秒上报一次经纬度后端收到后更新骑手位置并计算骑手与门店、骑手与用户的距离用来做派单优先级和配送里程展示。这里的核心数据结构是“骑手会话 轨迹点”。如果源码里骑手位置只存在内存里那骑手一断线重连位置就丢了。常见的做法是维护一张 rider_location 表或者用 Redis 的 GEO 结构存最新位置历史轨迹另建一张表。距离计算的坑在于坐标系这个我会在第4章专门讲。抢单/派单的逻辑一般有两种商家自主发单到骑手池骑手抢或系统根据距离、评分、当前订单量自动派。前者实现简单后者要一套权重计算。开源系统大多数先做抢单模式因为它不需要复杂的调度策略只要保证同一订单不能被两个骑手抢到就行通常是先到先得加乐观锁。配送端的状态变更要和订单状态联动。骑手点“送达”订单状态要同步从“配送中”变成“已完成”同时要触发用户端小程序的订单状态刷新和可能的评价提醒。这里我建议重点看骑手确认送达的接口有没有做幂等因为骑手在弱网环境可能连点两次请求重复提交会导致订单状态被覆盖或结算记录多算一次。2.4 小程序与APP一套用户逻辑在双端的复用与差异用户端的微信小程序和APP在功能上是同构的浏览门店、点餐、下单、支付、跟踪订单、售后。很多开源系统为了省成本直接基于 uni-app 写一套代码同时打包成微信小程序和安卓/iOS APP。这确实能省掉业务逻辑的重复开发但要清楚“一套代码打两端”不等于“两端的配置也一套搞定”。小程序和APP的差异点主要在三个地方。第一是登录体系微信小程序走 wx.login 换 openidAPP 走手机号验证码或第三方授权两套登录态不能混用源码里的 user 表通常会有 openid、mobile、unionid 多个字段来兼容。第二是支付小程序用微信支付的小程序支付APP 内如果是微信支付要申请开放平台的移动应用商户号和 appid 的绑定关系不同。第三是发布更新小程序要过微信审核APP 要上架应用商店或用企业签名分发这决定了你改一版代码后两端用户拿到新版本的时间差。我见过不少团队在小程序上改个文案当天就能发APP 那边要等一周就是这个原因。还有一个小细节值得顺手做小程序里订单列表这种长列表页面默认要处理“加载更多”的分页交互不要一次把所有历史订单塞给前端列表接口要带好 page 和 pageSize并约定返回值里的 hasMore 字段。很多刚从网页转小程序的人会忽略这个接口直接返回全量数据订单一多小程序直接卡。3. 本地跑通整站源码从环境检查到四端启动的最小步骤目录结构看明白之后就可以开始跑了。这里我按“先起后端再起前端最后接小程序和APP”的顺序来讲。启动顺序之所以重要是因为商户端和配送端页面一打开就要拉接口后端没起来前端界面全是报错小程序和APP同理它们的请求都是指向后端服务的而且需要额外的登录和域名配置。3.1 环境版本匹配JDK、Node、MySQL、Redis 的“能跑”组合这一步我建议你先把本机环境和服务端的版本要求对上不要凭感觉装最新版。后端如果是 Java 技术栈JDK 8 和 JDK 11 都常见Spring Boot 2.x 对应 JDK 8Spring Boot 3.x 就要 JDK 17。前端商户端一般是 Vue 2 或 Vue 3Node 14 或 16 比较稳Node 超过 20 之后跑老项目容易出现 node-sass 或 OpenSSL 兼容问题。数据库 MySQL 5.7 和 8.0 都行但要注意 MySQL 8 的默认认证插件是 caching_sha2_password老驱动或老客户端连的时候可能报认证失败。Redis 用来存会话、缓存菜单和骑手位置版本 5 以上基本够用。先花两分钟检查本机java -version node -v npm -v mysql --version redis-cli ping这个检查的目的不是看版本数字本身而是确认你机器上有没有这些运行时以及它们之间的兼容性。如果redis-cli ping返回 PONG说明 Redis 服务活着如果返回 Could not connect先systemctl start redis或redis-server把它拉起来再说。Java 版本如果显示的是 1.8那大概率是 JDK 8和 Spring Boot 2.x 匹配如果显示 17就优先找源码里 pom.xml 的java.version有没有明确写版本。我一般会顺手把版本号记在项目 README 的旁边方便后面排查问题。3.2 数据库初始化建库、导入SQL、造一个能登录的测试商户数据库初始化是源码跑通前最容易被卡住的一步。开源系统通常会附带一份 sql/ 目录里面有建库脚本或全量备份里面可能包含初始化商户、菜品、管理员账号等数据。如果你直接拿 Navicat 导入注意先看 SQL 文件的编码和表前缀很多系统会带 shike_ 这样的表前缀前后端配置里也会写。导完数据后再单独建一个专用的 MySQL 账号不要用 root 跑业务不然后面排查问题的时候分不清是权限问题还是业务问题。CREATE DATABASE shike_order DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER shikelocalhost IDENTIFIED BY Shike123; GRANT ALL PRIVILEGES ON shike_order.* TO shikelocalhost; FLUSH PRIVILEGES;utf8mb4 是必选项因为门店名、菜品备注里可能带 emoji用老 utf8 存 emoji 会报 Incorrect string value 错误。建好库之后把源码里的 sql 文件导入mysql -ushike -pShike123 shike_order shike_order_init.sql导入完成后去 sys_user 或 admin_user 表里确认管理员和测试商户的初始账号。很多开源系统的默认密码是 123456 或者是 MD5 加密过的常量搜索 INSERT INTO 语句就能看到。强烈建议第一次登录后立刻改掉默认密码尤其当你把服务映射到公网测试的时候。3.3 按依赖顺序启动先后端再商户端最后小程序和APP后端启动有两种方式一种是用 Maven 直接跑方便看日志另一种是先打成 jar 再跑更接近生产。本地调试我一般用第一种因为 mvn spring-boot:run 会在控制台实时输出 SQL 和报错排查问题更直接。前提是 application-dev.yml 里的数据库地址、账号、密码和 Redis 地址都改成本机的。cd server mvn clean package -DskipTests java -jar target/shike-server.jar --spring.profiles.activedev--spring.profiles.activedev 用来指定加载 application-dev.yml 这组配置而不是默认的 application.yml。如果启动日志里有 Started ShikeServerApplication 字样说明后端已经起来。如果卡在 HikariPool ... is closing 或者 Access denied for user基本就是数据库连接问题回头检查数据库账号的 host 权限和密码。如果报端口被占用先用netstat -tlnp | grep 8080看是哪个进程占用了 8080别盲目换端口先确认是不是之前启动的后端实例没杀掉。后端起来之后再启动商户端和配送端。这两个一般是独立的 Vue 工程它们开发模式下的接口请求都走 Vite 或 Webpack 的转发配置把 /api 转发到后端的 localhost:8080。先安装依赖再启动cd merchant-web npm install npm run devnpm install 如果在默认源下特别慢可以给 registry 参数临时指到国内镜像源速度会快很多。如果 npm install 报权限错误别用 sudo 硬刚先确认是不是 Node 版本太高导致的 node-gyp 编译失败常见处理是切换 Node 版本到 16 再装。启动成功后Vite 会在终端输出一个本地地址比如 http://localhost:5173浏览器打开能看到登录页。配送端的启动方式和商户端基本一致换到 rider-web 目录执行同样的命令。如果两个前端同时跑注意它们的默认端口不能冲突Vite 配置在 vite.config.js 或 vue.config.js 里遇到项目 A 能开、项目 B 一片白的情况先看控制台是不是端口被占。小程序和APP的启动方式不太一样。小程序工程要用微信开发者工具导入不能像普通网页那样在浏览器里直接打开导入时选择项目目录填好 AppID。如果支付功能还没准备好可以先使用测试号。APP工程如果用 uni-app 写的一般通过npm run dev:app生成运行包再用 HBuilderX 或 Android Studio 跑模拟器。想验证小程序和 APP 在代码层面的同一套逻辑可以留意 src/pages/order 这类目录两边引用的是同一份业务代码。uniapp 打包小程序的常用命令是npm run build:mp-weixin产物在 dist/build/mp-weixin 下微信开发者工具直接导入这个目录即可。3.4 启动自检看日志、探活接口、确认第一个页面能打开四端都启动后不能只看界面就认为成功了要按顺序做一轮自检。后端有探活接口的话直接访问 http://localhost:8080/actuator/health 或项目自定义的 /api/health返回 UP 才说明服务真的活着。商户端登录后随便点一个菜单页面打开浏览器开发者工具看接口返回码重点关注 401 和 500。401 说明登录态没带全500 可能是后端某个表没初始化或配置缺失。小程序端自检时建议先打开调试模式不校验合法域名确认接口通、数据能渲染再关掉调试模式测正式流程。第一次运行小程序如果白屏不用慌最常见的原因是 app.js 里的 BASE_URL 还是后端的局域网地址而手机和电脑不在同一网络或者没开启“不校验合法域名”。这时候先用微信开发者工具的本地调试带过去后续再按第4章配好正式域名。4. 让四端联调起来小程序、定位、支付、推送的参数配置决定成败源码能跑通和能运营之间隔着一整层配置。我见过太多项目本地跑得很顺一到真机联调就崩问题往往不是代码逻辑而是小程序合法域名没配、地图坐标系没统一、支付回调地址被防火墙挡住、推送 key 填错。这一章把配置点逐个说透。4.1 微信小程序接入appid、request合法域名和动态标题设置小程序端联调第一步是确认 appid。项目里如果有多个小程序用户端、商户端可能都有小程序版要确认你导入的是正确的那一个。在微信开发者工具里改 appid 很简单但后端 application.yml 里的 wx.appid、wx.secret 也要同步改否则前端登录拿不到 openid接口会一直 401。const BASE_URL https://api.yourdomain.com/api; const request (url, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: ${BASE_URL}${url}, method, data, header: { Authorization: wx.getStorageSync(token) }, success(res) { if (res.statusCode 200) resolve(res.data); else if (res.statusCode 401) { wx.navigateTo({ url: /pages/login/index }); reject(res); } else reject(res); }, fail: reject }); }); };这段封装解决两个问题一个是把 BASE_URL 统一收敛到一个常量换环境只改一行另一个是把 401 统一处理成跳登录页。源码里如果每个页面都自己写 wx.request建议改成这种统一封装再往后走。参数说明header.Authorization 是后端认的登录凭证字段有些系统叫 token 或 X-Token要以源码实际的拦截器为准。接下来是 request 合法域名。小程序生产环境要求所有请求域名必须是 HTTPS 且在小程序后台配过 request 合法域名。本地联调时可以在开发者工具里勾选“不校验合法域名”但真机预览时这个选项不生效手机上的请求会被拦。我建议联调阶段直接把后端挂到一台有公网 HTTPS 的服务器上域名配好 SSL 证书再在小程序后台把域名加进 request 合法域名列表。这样做还有一个好处支付回调也需要公网可达的地址反正都要配早配早省事。另外小程序里订单进度页这类需要随状态变化的页面可以用 wx.setNavigationBarTitle 动态设置标题。比如订单从“待接单”变成“配送中”把标题从“订单详情”改成“商家备餐中”或“骑手配送中”这个小改动对用户体验提升很明显。实现也不复杂wx.setNavigationBarTitle({ title: 骑手配送中 });调用时机放在订单详情接口返回后根据 order.status 映射对应文案即可。记住 wx.setNavigationBarTitle 要在页面 onLoad 或状态变更后调用且页面标题不能超过 32 个字节。4.2 地图与定位GCJ-02 和 WGS-84 坐标系配送距离怎么算才不偏外卖系统只要涉及骑手定位和配送距离必然要面对坐标系问题。国内的高德、腾讯地图用的是 GCJ-02 坐标也就是火星坐标系微信小程序 wx.getLocation 返回的也是 GCJ-02。但 GPS 原始信号是 WGS-84也就是地球坐标系。如果源码里混用了两套坐标最直接的表现是骑手定位偏移几百米配送距离算出来不对骑手端导航又指向另一个位置。常见做法是统一在后端用 GCJ-02 作为存储和计算坐标系前端上报时如果是 GPS 原始坐标就先转换。一个通用的 GCJ-02 转 WGS-84 的简化实现长这样public double[] gcj02ToWgs84(double lng, double lat) { double dlat transformLat(lng - 105.0, lat - 35.0); double dlng transformLng(lng - 105.0, lat - 35.0); double radlat lat / 180.0 * Math.PI; double magic Math.sin(radlat); magic 1 - 0.00669342162296594323 * magic * magic; double sqrtMagic Math.sqrt(magic); dlat (dlat * 180.0) / ((6378245.0 * (1 - 0.00669342162296594323)) / (magic * sqrtMagic) * Math.PI); dlng (dlng * 180.0) / (6378245.0 / sqrtMagic * Math.cos(radlat) * Math.PI); double mgLng lng dlng; double mgLat lat dlat; return new double[]{ lng * 2 - mgLng, lat * 2 - mgLat }; }这段代码做了两件事先把经纬度投影到墨卡托相关的偏移量上再把偏移量回减得到真实坐标。使用时要补全 transformLat 和 transformLng 两个辅助函数。实际业务里这套算法主要用于国内配送范围内的坐标纠偏海外区域坐标精度不在覆盖范围内。更省事的方案是使用高德或腾讯地图服务端的坐标转换 API把转换逻辑外包给地图服务商缺点是会多一次网络请求骑手位置高频上报时成本需要评估。距离计算上我一般不建议用数据库的 ST_Distance_Sphere 在 SQL 里算距离虽然 MySQL 5.7 及以上支持但骑手位置高频更新每次都触发空间索引查询压力不小。更常见的做法是在 Redis 里用 GEO 结构维护骑手位置算距离时用 GEODIST或者直接在业务代码里用 Haversine 公式算球面距离。优先级从高到低是Redis GEO → 业务代码 Haversine → SQL 空间函数。选型时结合订单量和骑手数量来定骑手少于 100 人业务代码算就够。4.3 支付与结算商户号、回调地址、验签顺序一个不能错支付是整站源码里最像黑匣子的一环你在本地怎么测都通一上正式环境就一堆问题。配置的核心有三块商户号、回调地址、API 密钥。商户号要和小程序 appid 做绑定APP 支付则要在微信开放平台建移动应用并关联同一个商户号。回调地址必须是公网 HTTPS微信服务器会主动 POST 通知到这个地址本地 localhost 肯定收不到。PostMapping(/api/pay/notify) public String notify(RequestBody String xml, HttpServletRequest request) { // 1. 先验签验签通过再处理业务避免伪造通知 boolean signOk WxPayUtil.verifySign(xml, payConfig.getApiV3Key()); if (!signOk) { return xmlreturn_codeFAIL/return_codereturn_msgBAD_SIGN/return_msg/xml; } // 2. 取订单号更新支付状态并确保幂等 String outTradeNo WxPayUtil.parseOutTradeNo(xml); String transactionId WxPayUtil.parseTransactionId(xml); orderService.handlePaySuccess(outTradeNo, transactionId); // 3. 返回 SUCCESS微信收到后不再重试 return xmlreturn_codeSUCCESS/return_code/xml; }这段代码的顺序很重要先验签再查订单最后更新状态。如果先更新状态再验签等于把支付入口敞开了任何人都能伪造一个通知把你的订单改成已支付。apiV3Key 是商户平台设置的 API v3 密钥注意不要和商户号、AppSecret 混在一起更不要提交到 Git 仓库。微信支付通知有重试机制如果业务代码处理失败返回了 FAIL微信会每隔一段时间重新 POST 一次。所以成功回调处理一定要做幂等同一个 out_trade_no 重复通知时第二次直接返回成功而不是再给用户加一次余额或再结算一次给商户。实际联调时微信支付有沙箱环境可以测但沙箱环境用的密钥和正式环境不一样很多源码的支付工具类要支持切换。你拿到源码后先看 WxPayService 里读配置的方式确认 wx.pay.sandbox 开关是 true 还是 false。我见过的翻车现场大多数是把沙箱的密钥当正式密钥用或者反过来导致正式支付下单时报 Invalid sign。支付回调的日志要单独打一个文件别和业务日志混在一起。微信什么时候通知的、通知了几次、你处理的结果是什么这三件事在事后排查里缺一不可。4.4 消息触达从接单到送达状态变更靠什么推给各端订单状态一变要马上让商户端、配送端、用户端都感知到。轮询当然也能实现但外卖场景下轮询体验太差骑手端 5 秒轮询一次不仅耗电还容易漏单。常见方案是 WebSocket 或第三方推送商户端和配送端用 WebSocket 保持长连接用户端小程序用订阅消息APP 用厂商推送或 uni-push。WebSocket 连接的鉴权是个容易漏的点。连接地址上带 token 的话token 过期链接就断了不带 token 的话匿名连接能订阅任意订单等于把订单状态裸奔。常见的做法是连接建立后先发一个鉴权消息服务端校验通过后才允许订阅频道。频道的粒度一般到订单号比如 order_{order_no}不按用户维度发全量广播否则订单一多每个连接都要处理大量无关消息。订阅消息是微信小程序触达用户的主要方式用户下单时弹窗授权订阅订单状态变化时后端调用 subscribeMessage.send 推送。注意订阅消息是一次性授权用户拒绝一次之后你再想推就推不出去了所以下单页的授权时机和话术值得调一下。APP 端的推送要按平台分别配置。Android 走厂商通道还是 uni-push 看源码集成了什么iOS 的推送证书和描述文件要放到推送服务后台。如果你在本地调试时发现 iOS 收不到推送先检查证书环境是开发还是生产这一步错位会导致真机永远静默。另外iOS 用户在浏览器里直接下 APP 安装包会被系统拦住企业签名分发要用 itms-services 协议生成安装页这属于发布环节的配置属于典型的“不上线不知道、一上线就翻车”的类型提前在配置清单里占个位置。5. 运营前必看的避坑清单整站源码跑起来的五个典型翻车现场这一章列的全是我认为最值得提前知道的坑每条按“现象→原因→解决”写。你没踩过算运气好踩过就知道这些坑有多耗时间。5.1 小程序真机白屏开发者工具里却一切正常现象微信开发者工具里小程序加载正常数据也能显示但拿手机预览或真机调试时页面空白只有导航栏。原因通常是两类一类是手机和开发机不在同一网络请求的 BASE_URL 指向 http://192.168.x.x:8080手机访问不到另一类是生产环境下的 request 合法域名校验真机上开发者工具的“不校验合法域名”选项不生效所有非 HTTPS 域名外的请求都被拦。解决方法是把后端部署到公网配置好 HTTPS 域名在小程序后台把域名加到 request 合法域名里。如果你想快速确认到底是网络还是域名问题打开 Charles 抓包看小程序的请求列表能看到 ERR_NAME_NOT_RESOLVED 这种 DNS 级别的报错也能看到 url not in domain list 这种域名拦截报错两种报错对应两个完全不同的排查方向。小程序抓包这一步在联调阶段几乎是必做的越早学会越省时间。5.2 骑手定位偏移几百米坐标系混用和缓存定位现象骑手端地图上显示的位置和实际位置差了四五百米配送距离也跟着偏客户投诉骑手找不到地方。原因坐标系混用是最常见的前端上报的是 GCJ-02后端按 WGS-84 存储或计算还有一种情况是骑手端用了缓存的最后一次定位没等 GPS 定位成功就上报了旧位置。解决统一坐标系是第一步所有端和后端约定都用 GCJ-02GPS 原始坐标先转换再上报。缓存定位的问题靠业务逻辑处理定位模块启动后强制等待 GPS 信号拿到新位置前不允许上报。可以在后端日志里给每条位置上报打上时间戳和坐标来源标记排查偏移问题时对比同一时间点上报坐标和实际坐标的差值能快速判断是转换问题还是缓存问题。5.3 订单状态卡在“待接单”状态机缺了超时和取消的转移现象用户下单支付后商户端一直不接单订单永远停在“待接单”既没有超时提醒也不能自动取消。原因源码里的状态机只写了正向流转待支付→待接单→已接单→配送中→已完成但没有实现超时未接单自动取消或自动转派。外卖场景里商户 5 分钟不接单系统最好能自动催单或取消。解决先看订单状态枚举和状态流转方法补上超时任务。常见做法是下单时往 Redis 里写一个带过期时间的 key过期后触发回调或者用定时任务扫订单表把超过 N 分钟仍未接单的订单捞出来处理。注意定时任务的扫描粒度不要太密每分钟扫一次就行太密会给数据库加没必要的压力。补状态机的顺序是先加状态定义再加流转方法最后加超时触发逻辑三步缺一步都会让订单卡住。5.4 支付回调一直收不到公网地址、防火墙和验签的顺序问题现象用户支付成功了钱也扣了但系统里订单还是“待支付”找遍日志都没看到支付回调的记录。原因回调地址是 localhost 或局域网地址微信服务器访问不到或者云服务器安全组没放通 443 端口或者回调地址指向的路径和后端 controller 实际路径不一致。解决第一步先把回调地址换成公网 HTTPS 域名本地调试可以先用临时公网映射工具把 localhost:8080 映射到一个公网临时域名但如果做支付回调的联调建议直接用测试号加正式域名这种临时映射工具有时候会断连等订单多起来再换域名又要重新配置一遍。第二步确认云服务器安全组和防火墙都放行了 443用curl -I https://api.yourdomain.com/api/pay/notify从外部访问一下看看返回码是不是 403 或 405。如果返回 405说明地址通了但方法不对检查 controller 是 POST 还是 GET。最后再检查验签逻辑有时回调其实已经到了但因为验签失败返回了 FAIL日志又被其他日志刷掉了所以支付日志一定要单独隔离出来。验签失败最常见的原因是商户号、appid 或 API v3 密钥配置不一致按 4.3 的顺序重对一遍。5.5 一上线数据库连接池就被打满默认参数和慢查询现象系统上线第一天订单量稍微上来一点后端日志开始报 Connection is not available, request timed out数据库连接池被打满。原因源码里的数据库连接池参数是开发环境的默认值比如 HikariCP 的 maximum-pool-size 只有 10而每个请求要占用一个连接直到事务结束再加上某些列表查询没有分页或缺少索引一条慢查询把连接攥在手里几十秒连接池自然就满了。解决先调连接池参数给一个适合业务起步的配置spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000maximum-pool-size 不要盲目调大连接数越大数据库压力越大通常 20 到 50 之间起步够用。connection-timeout 是等待连接的超时时间超过 30 秒拿不到连接就报错这个值不要设太长否则用户请求会挂起很久。调完参数后再查慢查询开启 MySQL 慢查询日志把执行时间超过 1 秒的 SQL 捞出来看是不是少了索引。外卖系统里最容易缺索引的三张表是订单表的 order_no、订单状态和创建时间组合、骑手位置表的时间戳。加上索引之后再看连接池的活跃连接数确认峰值时不会再顶满。除了这五个还有一类坑容易被忽略源码里可能存在硬编码的第三方密钥或测试账号比如短信平台的 access key、地图服务的 SK。拿到源码后第一件事全局搜 yml、properties 和前端配置文件里的 key、secret、token把所有能看到的密钥都换成自己的。这个动作一定要做很多开源项目被扫号就是因为默认密钥还在跑。6. 从源码到“完美运营”上线前的验证清单和一个状态机埋点技巧最后一章不展开新系统了就讲两件事上线前按什么清单做验收以及我每次都会加的一个状态机埋点技巧。先给一份我常用的验证清单。认证与权限商户端不同角色的账号能看到的菜单和数据是否隔离用户端登录态过期后能不能自动续期或准确跳转。核心交易链路下单→支付→商户接单→骑手取货→送达→结算这条链路必须在公网环境下完整走一遍中途任何一步断了都要能从上一步恢复。异常分支用户支付成功后取消订单钱怎么退商户拒单后用户是否收到通知骑手长时间不接单有没有自动派给下一个人。数据一致性商户端营业额和用户端实付金额是否对得上结算记录和订单状态是否一一对应。安全项默认密码是否全部改掉后台管理页是否对公网开放HTTPS 是否全覆盖所有外部接口的密钥是否已经换成新的。性能项用压测工具对下单接口和订单列表接口各打一轮看响应时间的 P95 是不是在可接受范围。这份清单每上线一个新版本都要过一遍不要只在上线第一天做。再讲状态机埋点。这个技巧成本很低但排查线上问题的时候特别管用。很多订单问题之所以难查是因为你不知道它从“待接单”变到“已接单”中间经过了哪些操作、是谁操作的、什么时候操作的。给订单状态流转加一个埋点日志每次状态变更都记录订单号、变更前状态、变更后状态、操作人、来源端和时间戳问题出现时拉一条日志就能还原现场。Aspect Component public class OrderStatusLogAspect { Around(annotation(orderStatusChange)) public Object logStatusChange(ProceedingJoinPoint joinPoint, OrderStatusChange orderStatusChange) throws Throwable { Object[] args joinPoint.getArgs(); String orderNo extractOrderNo(args); int beforeStatus extractStatus(args); Object result joinPoint.proceed(); int afterStatus extractStatus(args); // 记录订单号、变更前状态、变更后状态落库或单独日志文件 orderStatusLogService.record(orderNo, beforeStatus, afterStatus); return result; } }这段 AOP 的意图是在标注了 OrderStatusChange 注解的方法前后自动记录状态变化。使用时要给订单状态流转方法加上注解并在 extractOrderNo 和 extractStatus 里按实际入参结构取订单号和状态。埋点日志建议单独落一张表或一个独立的日志文件不跟业务日志混在一起这样事后排查时 grep order_no 一下就能拉出完整流转轨迹。我自己每次接手外卖或配送类系统第一件事就是补这个埋点它的价值不亚于给数据库做备份。状态机本身的逻辑要看埋点则是给状态机一个可追溯的“行车记录仪”两件事合起来订单问题基本都能在五分钟内定位到具体环节。这套源码能不能直接运营取决于你愿不愿意花时间把配置和安全项过一遍。按上面的顺序做完它就是你自己的系统了。希望帮到你。本文还有配套的精品资源点击获取