ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

微信外卖小程序系统开发实践:Java后端与订单全流程管理

微信外卖小程序系统开发实践:Java后端与订单全流程管理 简介完整版微信外卖小程序系统源码与数据库基于Java语言开发面向需要搭建或二次开发在线订餐平台的开发者、中小商户及技术学习者。系统覆盖商家管理、骑手管理、用户端小程序、订单处理等完整闭环支持微信支付、多场景适用前后端齐全稳定性和可扩展性较好。压缩包共2825个文件以fr3报表模板、data/rls数据文件、log日志及PDF文档为主辅以dll、exe、配置文件等总大小27.61MB目录结构清晰便于按模块查阅。数据库涵盖用户、商家、商品、订单、骑手等核心数据表外键关联保证数据一致性源码提供了完整的API接口及权限体系开发者可根据实际业务调整商家入驻、配送跟踪、支付对接等逻辑也可作为课程设计或毕业设计的蓝本。目前已有908人学习下载是快速理解和落地微信外卖业务的实用参考资料。1. 微信外卖小程序系统Java 后端与小程序前端都齐了一条链路跑通下单到配送接到餐饮商户的小程序外卖需求时很多人的第一反应是从零开始写一套。我去年也干过这事结果光是梳理“商家怎么改菜单、骑手怎么接单、订单怎么流转”就花了两天更不用说微信支付回调、小程序域名校验这些细节。后来我换了个思路直接拿一套 Java 开发、前后端齐全的微信外卖小程序系统源码做二次开发数据库导进去、后端跑起来再用微信开发者工具加载小程序端当天晚上整个下单链路就通了。这套资源解决的是外卖业务最实际的闭环问题用户在微信里搜商家、下单支付商家后台接单备餐骑手取餐配送并更新状态订单全程可跟踪。后端是 Java 写的多商户并发时比脚本方案稳小程序端、商家端、数据库脚本都带不需要自己造表。资源标题里写的 JAVE 就是 Java 的笔误实际开发语言是 Java前后端分离适合直接拿来改业务。适合三类人想快速上线外卖平台的商户做餐饮、商超信息化项目的开发者以及接小程序外包、需要靠谱模板起手的技术团队。新手照着部署流程一天能跑通熟手可以研究模块拆分和表结构设计把订单模型改成自己的业务。2. 三端模块与订单流转商家、骑手、用户小程序各管哪一段2.1 用户端小程序从搜索商家到支付完成的页面链路用户端是所有流量的入口它的页面链路一般是首页商家列表 → 分类筛选 → 商家详情 → 商品列表含规格选择 → 购物车 → 确认订单 → 支付 → 订单详情。这条链路每一环都对应后端接口接口设计基本跟着页面走/api/shop/list给首页用/api/goods/list给商品列表用/api/order/create给确认订单用/api/order/pay给支付用。这里有一个特别影响体验的点商品列表的加载方式。很多初级开发习惯一次性把全量数据返回商品少没事上百条就开始卡。这套源码里用的是列表加载更多用户滑动到底部小程序端带着page和limit两个参数请求下一页后端分页返回前端把新数据追加到当前数组。// 页面列表加载更多滚动到底部时拉取下一页商品数据 let pageNo 1; const pageSize 10; Page({ data: { goodsList: [], hasMore: true }, loadGoods(shopId) { wx.request({ url: https://api.example.com/api/goods/list, data: { shopId: shopId, page: pageNo, limit: pageSize }, success: (res) { const { code, data } res.data; if (code 0 data.rows.length 0) { this.setData({ goodsList: this.data.goodsList.concat(data.rows) }); pageNo 1; } else { this.setData({ hasMore: false }); } } }); } });这段代码的逻辑是每次请求带上当前页码page和每页条数limit后端返回rows后用concat把新数据追加到goodsList而不是整体替换。hasMore用来标记是否还有下一页列表触底时如果已经返回空数组就把hasMore置为false避免重复请求。这里最容易写错的是pageNo累加时机——必须在success里累加不能放在请求发出前否则失败重试时会跳过一页。还有一个前端细节容易被忽略微信小程序顶部导航栏高度在不同机型上不一样iPhone 刘海屏和安卓状态栏高度差异很大。首页做吸顶搜索框时如果写死一个像素值部分机型会盖住胶囊按钮。我的习惯是用wx.getSystemInfo()拿到statusBarHeight再动态计算导航栏高度而不是写死。2.2 商家管理端菜单、库存、营业时间与订单处理商家模块的权限模型是商家账号登录后只能操作自己的店铺后端靠shop_id字段做数据隔离。商家能做的事主要四类上传菜单、设置价格、管理库存、调整营业时间。对应到表结构分别是商品表、店铺表和分类表操作入口一般是独立的后台页面不走用户端小程序。商家端和用户端不应该混在一个页面里。用户端是逛和买操作频率低、页面要轻商家端是改数据操作频率高、表单要全。我见过有人把商家功能塞进用户小程序里加个隐藏入口结果审核和体验都很别扭。这套源码的做法是商家端单独一套页面菜单管理用列表展示修改商品直接弹表单保存营业时间用一个开关加时间段选择存成两个 time 字段查询时直接判断当前时间是否在区间内。商家端“接单”动作是整个流程的开关。用户支付成功后生成待接单订单商家端订单列表里出现这笔订单商家点击“接单”订单状态从“已支付”变成“已接单”之后骑手端才能看到这笔订单。这个状态更新后端一般是一个通用接口// 订单状态更新接口校验状态转移合法性 RequestMapping(/api/order/status) public Result updateOrderStatus(RequestBody StatusUpdateDTO dto) { // 1. 查出当前订单校验 operator 是否有权限操作该订单 Order order orderMapper.selectById(dto.getOrderId()); if (order null) { return Result.error(订单不存在); } // 2. 状态机校验只有 fromStatus 等于当前 status 才允许流转 boolean allowed stateMachine.canTransit(order.getStatus(), dto.getTargetStatus(), dto.getOperatorType()); if (!allowed) { return Result.error(非法状态转移); } // 3. 更新状态并记录状态变更日志 orderMapper.updateStatus(dto.getOrderId(), dto.getTargetStatus()); orderLogMapper.insert(new OrderLog(order.getId(), order.getStatus(), dto.getTargetStatus())); return Result.ok(); }这段代码的核心是状态机校验。canTransit接收当前状态、目标状态和操作者类型在允许列表里判断比如商家只能把“已支付”改成“已接单”不能把“已完成”改成“配送中”。校验放在 Service 层而不是 Controller 层是为了防止小程序端绕过接口直接拼参数改状态。状态变更日志也要记录后面排查问题全靠它。2.3 骑手端注册、登录、接单与配送状态更新骑手端核心是“附近订单”和“配送状态”。骑手登录后能看到配送范围内的待配送订单点击接单后订单绑定到该骑手其他骑手不能再接。这个绑定在后端就是订单表里rider_id字段从空值变成当前骑手 id同时状态从“已接单”变成“配送中”。这里有一个并发问题值得注意两个骑手同时点击接单怎么办常见做法是后端用带条件的更新语句比如update orders set rider_id ? where order_id ? and rider_id is null这个where条件保证只有一个骑手能更新成功另一个更新的影响行数是 0接口返回“已被接单”。这个写法比“先查再改”更稳二次开发时值得保留。骑手更新配送状态一般用几个按钮已取餐、配送中、已送达。每次点击都调同一个状态流转接口后端根据当前状态决定能否跳到目标状态。这个设计比写三个独立接口好维护新增状态只需要改配置不用动前端按钮。配送状态对用户端来说就是订单进度条的数据来源。用户端每隔几秒轮询订单详情接口把最新状态显示出来。轮询间隔不要设太短5 到 10 秒一次足够高峰期请求量会很可观。如果后续订单量大可以改成 WebSocket 推送但这套源码的轮询方案在中小业务量下完全够用。2.4 订单处理模块从创建到完成的全流程自动化把三个角色串起来看订单的完整生命周期创建待支付→ 支付确认 → 商家接单 → 骑手接单 → 配送中 → 已送达/完成中间可能进入取消、退款等异常状态。每一步状态转移都由后端校验和控制不是靠人工在不同系统里重复录入这就是“自动化处理降低出错概率”的含义。数据库订单状态表是整个系统的信息中枢商家端看到的是同一个表骑手端看到的也是同一个表用户端进度条的数据还是同一个状态字段。状态转移规则建议直接放在枚举或配置表里后期要改流程只改配置。下面这张表是各角色的职责和支撑数据角色主要负责关键操作支撑数据表用户下单、支付、查看进度创建订单、支付、订单详情user、orders商家菜品维护、接单接单、完成出餐shop、goods、orders骑手配送抢单、取餐、送达rider、orders管理员平台运营商家审核、骑手审核、数据统计admin、shop、rider还有一个防重复提交的细节小程序网络差时用户可能连续点两次支付按钮导致创建两笔订单。后端一般做法是创建订单前做幂等校验同一个用户对同一家店、同一个购物车内容短时间内只能创建一笔有效订单或者让前端生成一个临时订单号后端根据这个号去重。这个机制在二次开发时千万不要省略线上真出过这种问题。3. 数据库设计拆解五类核心表如何支撑订单全生命周期3.1 核心表清单与外键关系这套系统的数据量级属于中小型电商表结构按用户、商家、商品、订单、骑手划分。主要表之间的关系很清晰订单表同时关联用户、商家和骑手商品表关联商家。通过外键保证了数据一致性不会出现“订单属于一个不存在的商家”这种脏数据。表名核心字段外键关联作用userid, nickname, openid, mobile无用户登录和身份标识shopid, name, address, business_hours, status无商家入驻与营业状态goodsid, shop_id, name, price, stock, category_idshop_id → shop.id菜单与库存ordersid, order_no, user_id, shop_id, rider_id, status, total_amountuser_id → user.id, shop_id → shop.id, rider_id → rider.id订单流转riderid, name, mobile, status, current_lat, current_lng无骑手接单与位置用户表的openid字段是微信生态的独特标识同一个用户在小程序里不管换什么设备openid都不变用它做用户唯一索引比用自增 id 更可靠。这是小程序开发里最容易忽略的一点。订单表里要把对外展示的订单号和数据库主键分开。主键用自增 id对外展示用order_no生成规则一般是时间戳加随机数。这样用户不会通过单号猜到平台的订单量也方便后续做订单异步对账。我看到很多二次开发把order_no直接当主键用订单量大后写入性能会下降。3.2 订单表的状态字段设计订单表最核心的是status字段它直接决定订单当前阶段各端页面按status显示不同的按钮和文案。典型设计如下status 值含义可操作角色合法下一状态0待支付用户1已支付或 5已取消1已支付 / 待商家接单商家2已接单或 6退款2已接单商家 / 骑手3配送中3配送中骑手4已送达/完成4已完成--5已取消--6已退款--状态转移规则在代码里实现成状态机不允许非法跳转。比如从“待支付”直接跳到“已完成”业务上不可能但如果状态校验不严格调用接口的人就能破坏订单流程。不少改造项目后期出问题都是因为新增了绕过状态机直接改数据库的脚本结果三端显示不一致。3.3 常用查询 SQL统计订单量、查找附近骑手、分页列表商家端最常见的需求是统计今日订单数和营业额。下面这条 SQL 直接查订单表聚合-- 统计指定商家的今日订单数和营业额 SELECT COUNT(*) AS order_count, COALESCE(SUM(total_amount), 0) AS total_amount FROM orders WHERE shop_id 1 AND status 4 AND pay_time CURDATE() AND pay_time DATE_ADD(CURDATE(), INTERVAL 1 DAY);这里有两个容易被忽略的条件status 4表示只统计已完成的订单退款的不计入营业额pay_time限定当天范围不加的话历史订单也会被统计进去。COALESCE是为了防止当天没有订单时SUM返回空值。骑手端“抢单列表”的基础是附近空闲骑手查询。MySQL 5.7 及以上版本提供ST_Distance_Sphere球面距离函数可以直接按经纬度算距离-- 查找当前坐标 3 公里内的空闲骑手按距离排序 SELECT id, name, mobile, ROUND(ST_Distance_Sphere( POINT(current_lng, current_lat), POINT(116.397, 39.909) ) / 1000, 2) AS distance_km FROM rider WHERE status 0 AND ST_Distance_Sphere( POINT(current_lng, current_lat), POINT(116.397, 39.909) ) 3000 ORDER BY distance_km ASC;ST_Distance_Sphere返回单位是米/ 1000转成公里。经纬度数据类型要用decimal(10,6)精度不够时距离计算偏差很大。这个查询在骑手数量过万后性能会变差因为没法走索引后续可以引入空间索引或网格分桶不过中小业务量直接跑没问题。订单列表分页查询是商家端订单页的基础按创建时间倒序分页取数据-- 按 shop_id 分页查询订单按创建时间倒序 SELECT id, order_no, status, total_amount, created_at FROM orders WHERE shop_id 1 ORDER BY created_at DESC LIMIT 10 OFFSET 0;LIMIT 10 OFFSET 0是第一页LIMIT 10 OFFSET 10是第二页。OFFSET越大查询越慢数据量过万后建议改用游标分页用created_at 上一页最后一条记录的 created_at代替OFFSET效率会好很多。数据库增删改查本身不难难的是在数据量上来后仍然保持稳定。3.4 索引设计与字符集注意事项订单表的查询条件基本都带shop_id和status建索引时建议建联合索引(shop_id, status, created_at)这样上面三条 SQL 都能走索引。如果单独给status建索引效果很差因为状态字段区分度太低优化器很可能放弃索引做全表扫描。这也是数据库设计里最常被问到的问题之一。字符集方面数据库、表、连接串三层都要保持utf8mb4。微信用户昵称经常带 emoji 表情utf8存不了四字节字符只有utf8mb4能存。如果发现某个用户的昵称插入报错或者导入数据后中文乱码先检查字符集配置。连接串里加上characterEncodingutf8mb4导入 SQL 时命令行加--default-character-setutf8mb4才能保证全链路一致。4. 从源码到可运行本地部署与微信开发者工具接入4.1 环境准备与项目结构部署这套系统前先确认机器上有什么JDK 8 或 11、Maven 3.x、MySQL 5.7 或 8.0、微信开发者工具稳定版以及一个微信小程序 AppID测试号也可以。如果本地没装 Maven直接用 IDE 里集成的 Maven 也行但命令行方式最通用。项目目录一般分两块后端是 Java Maven 工程标准结构是src/main/java放代码、src/main/resources放配置数据库脚本通常在sql目录前端是微信小程序工程页面在pages目录公共方法在utils目录全局配置在app.js和project.config.json。打开项目后先别急着跑先找到sql目录里的 .sql 文件这是建库脚本。资源包里除了代码和脚本还带了左侧图标.bmp、顶端图标.bmp、fpdfcjk.bin、license.dat以及一组 .rls.data 和 .fr3.data 文件。这些是原工程遗留的资源文件用途是图标显示、PDF 中文字体映射和报表模板Java 后端跑基础下单流程不强制依赖但部署时建议原样保留后面做小票打印或报表导出时会用到删了再找就麻烦。4.2 数据库初始化建库导入脚本常见做法是先创建数据库再导入 SQL 脚本。命令行操作如下mysql -uroot -p -e CREATE DATABASE waimai DEFAULT CHARACTER SET utf8mb4; mysql -uroot -p waimai sql/waimai.sqlDEFAULT CHARACTER SET utf8mb4一定要写不然建表默认 latin1导入中文全是乱码。这是这条链路里最常见的翻车点。导入完成后可以用SHOW TABLES;检查表是否齐全用SELECT COUNT(*) FROM user;确认数据至少能读。4.3 配置修改数据库连接、端口与微信参数后端配置集中在src/main/resources/application.yml重点看数据源和微信支付参数server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/waimai?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver wx: appid: wx1234567890abcdef secret: your_app_secret mch-id: 1500000000 pay-key: your_pay_key这是标准的 Spring Boot 数据源配置。url里的characterEncodingutf8mb4保证读写中文不乱码serverTimezoneAsia/Shanghai不加的话 MySQL 8 连接常报时区错误。微信相关参数换成自己申请小程序时拿到的真实 AppID、AppSecret支付参数换成微信支付商户号对应的mch-id和 API 密钥。提示如果打开项目发现没有sql目录先检查database或docs目录脚本一般在这几个位置。整个数据库的建表脚本和初始数据通常是一个文件也可能按模块拆成多个文件逐个按顺序导入即可。4.4 启动后端与服务自测后端启动方式很直接mvn clean package -DskipTests java -jar target/waimai-backend.jarmvn clean package是打包-DskipTests跳过测试加快速度java -jar启动可执行 jar。启动日志里看到Started Application in x seconds和Tomcat started on port 8080说明启动成功。启动后用 curl 验证接口curl http://localhost:8080/api/health如果返回 ok 或者 JSON 数据说明后端和数据库已经连通。如果启动失败先看日志里是不是数据库连不上最常见原因是密码错误或时区配置缺失。端口被占用就把server.port改成 8081但小程序端的baseUrl也要跟着改。4.5 小程序端配置AppID、合法域名与真机调试小程序端用微信开发者工具打开前端工程目录修改app.js或config.js里的接口地址指向本地后端再把 AppID 换成自己的。开发者工具默认会校验 request 合法域名本地开发时会把http://localhost拦截下来这一步有两种处理方式。开发阶段在微信开发者工具的“详情 → 本地设置”里勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”这样本地 http 接口能正常调。上线阶段必须把后端部署到公网 HTTPS 域名在微信公众平台的小程序后台把域名加进 request 合法域名列表然后关掉开发者工具的“不校验”开关否则正式发布后用户端请求全部失败。真机调试时手机访问不到电脑的localhost需要把接口地址改成电脑的局域网 IP比如http://192.168.1.100:8080同时保证手机和电脑在同一 WiFi 下。还有一种先官方再本地的省心做法后端已经部署到公网时真机直接连公网地址调试跳过了局域网这一步。5. 避坑指南部署与二次开发中的 5 个常见问题5.1 小程序请求后端失败开发者工具正常真机全挂现象开发者工具里接口一切正常一用真机预览页面数据全空白请求全部失败。原因开发者工具勾选了“不校验合法域名”真机环境不认这个开关同时接口地址写的是localhost或 127.0.0.1手机访问不到电脑本机。解决开发阶段把后端接口地址改成电脑的局域网 IP手机和电脑连同一个 WiFi 再试上线前把后端部署到公网 HTTPS 地址在微信公众平台配置好 request 合法域名。我一般上线前强制走一遍真机调试不能只在开发者工具里点“预览”就完事。5.2 微信支付回调不触发订单一直停在待支付现象用户微信里已经扣款成功但订单状态没变一直显示“待支付”商家端接不到单。原因微信支付回调 URL 必须是公网可访问的地址本地 localhost 收不到微信服务器的通知另一种情况是回调地址配置和实际控制器路径不一致。解决开发时将本地服务映射出一个临时公网地址让微信回调能打进来。这类工具按“本地公网地址映射”搜索就有现成的选稳定一点的即可。同时在后端回调接口打印日志看微信服务器是否真的请求到了本地。排查时也可以用抓包工具观察回调请求是否到达。5.3 导入 SQL 后中文乱码商品名全变成问号现象数据库里的中文显示为乱码或问号小程序端商品名、商家名全都不可读。原因建库时没指定utf8mb4MySQL 默认字符集不支持中文存储或者导入脚本时命令行没带字符集参数。解决建库用CREATE DATABASE ... DEFAULT CHARACTER SET utf8mb4导入时加--default-character-setutf8mb4。已经乱码的库别直接改字符集大概率改不干净最省事的是把数据导出再重建库导入。从那以后我每次建库都会先查一遍字符集而不是等数据进去再返工。5.4 加载更多列表重复数据翻页时同一批商品反复出现现象用户滑动到底部触发加载更多商家列表或商品列表和前面内容重复或者翻页后出现大段空白。原因pageNo没有在状态里正确记录下一页请求时又把第一页数据返回前端追加数据时用了整体赋值把原有列表替换成了新的一页。解决前端用this.data.page记录当前页码每次请求成功后page 1再更新setData时用concat追加而不是直接赋值。后端确认分页参数是page limit还是pageNo pageSize两边的字段名必须对齐这是联调最容易错的地方。5.5 资源包里的 license.dat 和报表文件能删吗现象把资源包里的license.dat、AAAemployee.rls.data、test.fr3.data等文件删掉后启动或用到报表导出功能时报授权缺失或文件无效的错误。原因这些文件不是垃圾文件。fpdfcjk.bin是 PDF 中文字体映射两个.bmp是界面图标license.dat是授权文件.rls.data和.fr3.data是原工程的报表模板数据。Java 后端跑基础下单流程不一定用得上它们但扩展功能比如打印小票、导出对账单会按路径读取这些文件删了程序找不到资源就报错。解决部署时原样保留整个资源目录不要改名、不要挪位置。自己重新打包时也记得把这些文件复制过去。最稳妥的做法是目录结构和源码包保持完全一致只改配置不动资源。注意如果你二次开发后准备把系统交付给客户license.dat涉及原工程的授权机制商用前建议确认授权范围避免交付后出现授权争议。6. 二次开发进阶订单状态机扩展与多场景改造订单状态机是这套源码里最值得改造的部分。看懂状态机就能把外卖系统扩展成商超配送、生鲜电商、自提点模式。先看这个典型的状态枚举public enum OrderStatus { CREATED(0, 待支付), PAID(1, 已支付), ACCEPTED(2, 已接单), DELIVERING(3, 配送中), COMPLETED(4, 已完成), CANCELED(5, 已取消), REFUNDED(6, 已退款); public final int code; public final String desc; OrderStatus(int code, String desc) { this.code code; this.desc desc; } }枚举的每个值对应数据库status字段的数字。新增状态时比如加一个“商家拒单”需要在枚举类、数据库字典表、状态机校验逻辑三处同步修改缺一处就会状态不同步。我看这套源码的习惯是先画一张状态转移表把每个旧状态合法跳转到的新状态列出来然后在代码里用允许列表校验而不是堆一堆 if-else。多场景扩展有个取巧的设计订单表加一个delivery_type字段0 表示外卖配送、1 表示到店自提、2 表示堂食。自提和堂食订单不分配骑手用户端进度条显示“到店取餐”而不是骑手位置商家端接单后直接标记“备餐完成”用户凭取餐码到店取餐。这个改法只动了前端渲染和后端查询过滤逻辑订单状态机完全不用动。还有一种常见扩展是商超外卖商品表加spec规格字段比如“500ml/瓶”“1L/桶”购物车按规格维度计算。用户端选规格时从商品详情的规格列表里选其实就是把原来的单一价格改成多规格价格组。数据库层面加一张goods_sku表订单明细表关联sku_id而不是goods_id价格快照存在订单明细里这样商品改价不影响历史订单。我第一次跑这套系统时被“商家接单后骑手才能看见订单”这条规则卡了半天后来发现后端查询接口里用status做了过滤不是接单动作去通知骑手端。从那以后我每次改订单流程都强制先画一遍状态转移表再动代码这套习惯帮我少踩了不少坑。希望这个做法对你也同样有用。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进