
简介这是一套完整的美团三合一系统含外卖、团购、到店服务商业级PHP源码面向具备Laravel/ThinkPHP开发经验的中高级Web开发者用于快速搭建本地测试环境或二次开发学习。资源包共2011个文件涵盖817个JS交互逻辑、271个HTML页面模板、172个CSS样式文件、48个PHP核心业务脚本及98个PNG/JPG/GIF静态资源整体压缩后83.07MB结构清晰、模块完整包含微信公众号授权、JSAPI支付对接、TP框架伪静态配置等真实商用场景实现。已有158人下载学习配套提供.env数据库配置替换、域名批量替换工具指引、SSL证书部署说明及后台管理入口/admin/login/index并预置了weui、Bootstrap、Summernote、MUI等主流前端组件库便于快速调试与功能扩展。 最近我的私信里关于“美团三合一系统源码下载”的提问一下子多了起来有开餐饮店的朋友想自己做一套门店管理工具也有刚转行开发的读者想找一套结构清楚的项目练手。说实话“三合一”这个说法听起来有点像营销词但把源码下载下来拆开看它其实就是一套非常典型的本地生活门店管理系统。我花了两周时间把一套典型源码从部署到改代码完整跑了一遍这篇文章就把整个项目的设计思路、核心代码逻辑、部署步骤和踩坑清单一次性写清楚。所谓“美团三合一系统”并不是美团官方的软件而是第三方开发者做的轻量级门店管理整合方案把餐饮商家日常最频繁的三个操作合并到一个后台外卖订单的统一接入与提醒、到店团购套餐的验券核销、店内收银与扫码点餐。以前商家处理这三件事至少要切换三个App、登录两个后台忙起来漏单、验错券、对账对不上都很常见。三合一系统的核心价值就是用一个后台把三类业务串起来订单、核销、收银数据汇总到同一张报表对账效率能翻倍。这篇文章适合三类人参考一是想做私域门店系统的小商家或者个体开发者需要一套能跑通全流程的参考实现二是刚入门软件开发、想通过真实项目理解“数据表—接口—前端页面”怎么串联的初学者三是对本地生活平台开放接口对接方式感兴趣的研发同学。我会按真实项目的落地顺序来写从需求设计讲到数据库和代码再讲部署和二次开发最后是问题排查。每个设计决策背后为什么这么做我也会尽量说清楚。1. 整体设计思路与功能拆解1.1 三合一到底合的是哪三块先别急着看代码把业务边界搞清楚更重要。很多初学者拿到源码第一件事就是去翻Controller层结果看了半天不知道每个接口要解决什么问题。正确的打开方式应该是先看业务模块的划分。第一块是外卖订单接入。这个模块的定位是“接单中枢”商家在平台接到外卖订单后系统需要同步订单状态、菜品明细、配送信息并在门店端给出弹窗和语音提醒。数据来源是外卖平台的开放接口核心难点在于数据同步的可靠性既要能主动拉取也要能接收平台推送的回调消息。第二块是团购券核销。顾客在平台上买好团购套餐到店出示券码店员需要快速验证这个券有没有效、有没有被用掉验证通过后完成核销。这块的数据来源同样是对接平台开放的验券接口但业务逻辑和外卖订单完全不同它更像一个“验证—锁定—消耗”的事务过程稍不注意就会出现重复核销、券码被别人截图盗用这类问题。第三块是店内扫码点餐与收银。顾客到店用手机扫桌上的二维码就能看到菜单、下单、提交订单前台确认后出餐最后聚合收款。这一块是纯自研业务不依赖第三方接口数据从头到尾都落在自己的数据库里。把三块放一起三合一的价值就出来了外卖订单进系统团购核销进系统店内订单也进系统每一笔营收都归到同一个账户。月底对账不用再拿三张表格手动匹配了。1.2 源码方案里的三个关键设计取舍我实测的这套源码有四个设计决策让我印象很深对新手理解系统设计很有帮助。第一个取舍是“聚合订单表”。外卖平台的订单、店内扫码点餐的订单、团购核销的记录并没有像很多新手理解的那样分三张表存而是统一落到一张订单主表里再用order_type字段区分来源。这样做的最大好处是统计对账非常方便一条SQL就能算出全店营收。代价是三种订单的字段差异得靠冗余字段和扩展JSON来兜底对表结构设计要求比较高。第二个取舍是“双通道同步策略”。外卖接口既支持平台主动推送也支持系统主动拉取。源码把两个通道都实现了默认以推送回调为主同时用一个定时任务每30秒做一次增量拉取作为兜底。哪怕回调服务挂了轮询任务也能把订单捞回来不容易丢单。第三个取舍是“核销动作本地事务先行”。券码核销这种动作不能只依赖远端接口。源码的做法是先在本地核销记录表里插入一条流水并锁定券码再调远端接口确认最后把远端返回的核销凭证写回本地。这样即使远端接口超时本地也不会出现同一张券被核两次的情况。第四个取舍是“前后端分离”。管理后台用的是Vue Element Plus门店端是H5页面后端统一提供REST接口。这样商家用平板、电脑、手机都能打开后台不需要装客户端部署成本低很多。1.3 系统边界与核心流程从调用关系看这套系统的边界很清楚平台开放接口是上游数据源本地MySQL是业务数据中枢Redis缓存热点数据比如门店配置、券码缓存、登录Token前端页面负责收银员和顾客的交互。顾客到店扫码点餐的完整流程大概是顾客扫桌码前端从后端接口拉菜单列表提交订单后写入订单主表收银台刷新出待确认订单点击确认后状态流转到“制作中”顾客支付成功后状态变成“已完成”。外卖订单的流程则是平台回调打到系统的callback接口系统验签后写入订单表然后通过WebSocket向前端推送一条新订单消息收银端弹出提示音。团购核销的流程是店员输入券码或者扫码枪扫入后端先锁本地再调平台验券接口成功后在核销记录表里写流水整个过程一般要控制在2秒以内。2. 技术栈选型与核心数据库设计2.1 技术栈为什么这么选源码用的技术栈是Spring Boot 2.7 MyBatis-Plus MySQL 8.0 Redis Vue 3这套组合在国内中小型管理系统中非常典型资料多、排查方便、招人也好招。后端用Spring Boot是因为生态成熟定时任务、WebSocket、接口校验这些功能都有现成组件不需要重复造轮子。MyBatis-Plus主要省去了大量单表CRUD的SQL编写业务代码能更集中在订单处理、核销这类复杂逻辑上。Redis在这里承担的职责很明确缓存门店配置、做接口调用频控、存WebSocket会话和分布式锁。至于为什么不用更重的微服务架构原因也很直接——中小门店系统并发量有限单体应用部署简单、运维成本低一台2核4G的云主机就够了给商家省成本比什么都实在。2.2 订单主表这样设计最合理订单主表是整个系统的核心我把简化版的建表SQL列出来你可以对照着理解CREATE TABLE shop_order ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 订单ID, order_sn varchar(64) NOT NULL COMMENT 平台订单号/业务流水号, order_type tinyint(4) NOT NULL DEFAULT 0 COMMENT 订单类型1-外卖 2-店内点餐 3-团购核销, shop_id bigint(20) NOT NULL COMMENT 门店ID, customer_name varchar(50) DEFAULT NULL COMMENT 顾客姓名, customer_phone varchar(20) DEFAULT NULL COMMENT 顾客电话, total_amount decimal(10,2) NOT NULL COMMENT 订单金额, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态0-待处理 1-已确认 2-已完成 3-已取消, source_type tinyint(4) NOT NULL DEFAULT 0 COMMENT 来源1-平台回调 2-轮询拉取 3-本店创建, ext_json text COMMENT 原始报文扩展字段存各渠道特殊数据, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_sn (order_sn), KEY idx_shop_status (shop_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里几个设计细节值得展开说。order_sn加了唯一索引这是防止重复写入的重中之重。平台回调、轮询拉取、手动录入可能同时到达同一笔订单没有唯一索引直接insert必定产生脏数据有了唯一索引service层先用selectByOrderSn判断再决定insert还是update就行。order_type是业务分流的开关所有查询列表和统计报表都会先按它过滤所以这个字段必须建索引。ext_json存放的是一些表结构里不好拆出来的渠道字段比如外卖平台的配送员电话、团购券的券码批次、店内订单的桌号查询出来用JSON解析就行。2.3 核销号与支付流水不可少除了订单主表核销记录表也很关键。它记录了每一次团券核销的完整链路哪个门店核的、哪个店员操作的、券码是什么、平台返回的交易凭证号是什么。这个表的主要作用是给理赔和审计留证据。万一顾客说券被误核销运营人员靠这张表就能查出是哪个门店、哪个账号、什么时间操作的。支付流水表则是收银模块的核心。店内点餐支付一般走聚合支付支付回调回来之后更新订单状态、写支付流水这两步必须在一个事务里完成否则可能出现订单已支付但流水没记录或者反过来。3. 核心功能模块实现详解3.1 外卖订单接入回调与轮询双保险外卖订单同步在源码里是分两条链路实现的。第一条链路是平台回调平台把订单数据POST到系统的callback接口接口第一步做签名校验验签通过才继续处理第二条链路是定时轮询兜底拉取增量订单。回调接口的Controller层代码一般长这样RestController RequestMapping(/api/callback) public class PlatformCallbackController { Autowired private PlatformOrderService platformOrderService; PostMapping(/meituan) public Result handleCallback(RequestBody String rawBody, RequestHeader(X-Sign) String sign) { boolean valid SignatureUtils.verify(rawBody, sign); if (!valid) { return Result.fail(签名校验失败); } platformOrderService.processCallback(rawBody); return Result.success(ok); } }注意这里回调接口的返回必须非常快平台如果长时间没响应会重试重试就会造成重复通知。所以processCallback里的重活一定要异步化常规做法是先把原始报文存一张message_log表然后丢到线程池或者消息队列里慢慢处理回调接口本身只做验签和写日志。轮询兜底任务用Spring自带的Scheduled就够了Component public class OrderPullTask { Scheduled(fixedRate 30000) public void pullNewOrders() { ListString shopIds shopService.getAllShopIds(); for (String shopId : shopIds) { ListPlatformOrderDTO orders platformClient.pullIncrementOrders(shopId); for (PlatformOrderDTO order : orders) { orderService.saveIfAbsent(order); } } } }这里的saveIfAbsent是核心它先按order_sn查一次库没有才插入。把查询和插入放在同一个事务里再加唯一索引双保险就能把重复订单挡在门外。3.2 团购券核销事务先行防止重复核销团购券核销是整套系统里最容易出问题的模块。我把源码里的核销Service简化一下写在下面Transactional(rollbackFor Exception.class) public Result verifyVoucher(Long shopId, String code, Long operatorId) { // 1. Redis原子锁防止并发重复核销 boolean locked redisTemplate.opsForValue() .setIfAbsent(VOUCHER_LOCK: code, 1, 10, TimeUnit.SECONDS); if (!locked) { return Result.fail(当前券码正在核销中请勿重复操作); } try { // 2. 查本地核销记录本地已经用过就直接拒绝 int count voucherRecordMapper.countByCodeAndStatus(code, 1); if (count 0) { return Result.fail(该券已核销请勿重复使用); } // 3. 调平台验券接口 PlatformResult result platformClient.verifyVoucher(code, shopId); if (!result.isSuccess()) { return Result.fail(result.getMessage()); } // 4. 写本地的核销记录 VoucherRecord record new VoucherRecord(); record.setCode(code); record.setShopId(shopId); record.setOperatorId(operatorId); record.setPlatformTradeNo(result.getTradeNo()); voucherRecordMapper.insert(record); return Result.success(result); } finally { redisTemplate.delete(VOUCHER_LOCK: code); } }为什么要先查本地记录再去调平台接口因为本地库在极端情况下可能已经记录了核销但平台接口在某个瞬间还没同步到如果完全不查本地只顾调平台就可能出现“平台说可以核、本地却已经核过”的情况。反过来加Redis锁是为了解决并发问题——两个收银员同时扫同一个券码如果不加锁两边同时通过本地检查最终就会出现一张券被核两次。10秒的过期时间是为了防止进程突然崩溃导致锁永远不释放。3.3 扫码点餐与收银纯自研但流程不简单扫码点餐模块看起来不依赖外部接口但它涉及的流程状态最复杂。前端顾客端H5页面会有一个简单的点餐界面template div classmenu-page div v-foritem in menuList :keyitem.id classmenu-item span{{ item.name }}/span span{{ item.price }}/span el-input-number v-modelcart[item.id] :min0 sizesmall / /div el-button typeprimary clicksubmitOrder提交订单/el-button /div /template script setup import { ref, onMounted } from vue import { getMenuList, submitOrder } from /api/shop const menuList ref([]) const cart ref({}) onMounted(async () { menuList.value await getMenuList() }) async function submitOrder() { await submitOrder({ shopId: route.query.shopId, tableNo: route.query.tableNo, items: Object.entries(cart.value).map(([id, count]) ({ dishId: id, count })) }) } /script收银端则是另一个完全不同的视角它主要展示所有订单的实时状态新订单进来要能自动刷新。这里用WebSocket做实时推送是很自然的选型。订单从顾客提交到收银端展示链路是前端提交订单进后端接口后端落库后通过WebSocket向前端推送一条消息收银页面收到消息后刷新列表并播放提示音。整个链路要求后端接口响应快数据库写入不能有过多冗余操作。3.4 后端订单处理的通用写法不管哪种订单落库之后的处理逻辑是可以统一抽象的。源码里通过Strategy模式把不同订单类型的处理逻辑隔离开order_type字段对应不同的Handler主流程只负责分发这样做的好处是以后加新渠道不用改主流程代码只要新增一个实现类注册进去就行。订单成功落库之后还有一些联动操作值得注意更新门店的今日营收统计缓存、给管理员发送新订单通知、记录操作日志。这些操作如果放在同步逻辑里会让接口响应变慢一般建议用Spring的Async或者丢到消息队列里异步消费。4. 源码获取后的部署与二次开发4.1 拿到源码先做什么安全检查这是整个“源码下载”环节里最重要的一步比部署本身更值得花时间。网上流传的很多管理系统源码下载下来之后不一定干净。我的习惯是拿到任何源码先做四件事。第一全局搜索敏感函数。后端重点搜Runtime.getRuntime().exec、ProcessBuilder、eval、base64_decode这类危险函数看有没有不合理的远程命令执行。前端重点搜document.cookie、eval、atob这类可疑字符串。很多恶意代码打着“业务功能”的旗号藏在工具类里只有搜出来逐个检查才放心。第二检查数据库初始化脚本。花几分钟把SQL文件完整读一遍看有没有奇怪的存储过程、定时事件、外部表。一些恶意脚本会通过触发器或者事件在启动时创建后门账号。第三检查第三方依赖的版本和来源。老项目的依赖里很可能藏着有已知漏洞的组件如果有条件先用Maven的依赖检查插件或者其他依赖扫描工具过一遍再跑。第四看License和版权声明。如果源码是从非官方渠道拿到的或者包含明显闭源的加密逻辑用它做商业项目前一定要谨慎别让版权问题在客户上线之后找上门。4.2 环境准备与配置修改这套源码的部署环境比较标准JDK 1.8或更高版本、MySQL 8.0、Redis 6.x、Node.js 16前后端分开部署。我先说后端配置Spring Boot的application.yml里最需要改的是这几项server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/meituan_allinone?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 换成你自己的密码 redis: host: localhost port: 6379 platform: meituan: app-key: 你在开放平台申请的AppKey secret: 你的密钥 merchant-id: 商家门店ID callback-url: https://你的域名/api/callback/meituan这里有个经常踩的坑MySQL的serverTimezone配置。如果不加serverTimezoneAsia/Shanghai很多环境会出现数据库时间比本地时间早8小时或者直接报错的问题。另外platform开头的配置我自己封装了一个配置类线上如果要支持多门店建议改成配置中心或者数据库表存储别写死在yml里否则每加一家门店都要重新发版。前端部署相对简单Vue项目先执行npm install然后npm run build把dist目录放到Nginx下再配置一个反向代理把/api路径转发到后端8080端口就行。4.3 二次开发最容易扩展的三个点跑通之后大多数人不会满足于原样使用多少都要做点定制。基于我改这套源码的经验三个位置最容易改、也最值得改。第一个是消息通知方式。原版源码的收银端新订单提醒是浏览器弹窗加提示音但实际门店里收银员不一定一直盯着浏览器。很多商家希望订单来了能同步到打印机打小票、或者通过企微/钉钉机器人推送到群里。扩展思路是在OrderSaveHandler里追加一个NotifyHandler列表每种通知方式一个实现类以后加推送渠道就加实现类不用碰核心流程。第二个是营销活动支持。原版几乎只有满减这类基础活动商家经常要求“第二份半价”“会员折扣”“集点兑换”这种更花哨的活动。通用做法是引入规则引擎把优惠计算从订单流程里剥离出来配置表里定义活动规则计算引擎统一执行而不是在每段代码里写死判断。第三个是数据报表的图表化。原版后台的报表比较简陋就是简单表格。要做得像样前端引入ECharts后端提供统计接口按天、按周、按月度汇总营收、订单量、客单价对商家来说非常有价值。这个功能不涉及复杂技术但收益最明显。5. 踩坑记录与常见问题排查5.1 接口对接类问题我把实际操作中遇到的高频问题整理成了一张速查表方便你直接对照排查。问题现象可能原因解决办法平台回调总是验证失败签名算法不对或密钥配置错误用官方SDK做签名别自己造轮子先打印原始报文和待签名字符串对比回调接口偶尔没有收到订单回调地址不是公网可访问或者Nginx拦截了POST请求确认回调URL能公网访问开Nginx的POST日志排查订单重复写入缺少唯一约束或者saveIfAbsent未落在事务里检查order_sn唯一索引确认查询插入在同一事务轮询任务不执行Scheduled默认单线程长时间任务阻塞后续执行给定时任务配置线程池或改用XXL-Job这类分布式调度回调验签的问题我印象最深。有一次商家反馈订单一会有一下没有排查了半天最后发现是签名时排序规则搞错了。平台文档要求把所有参数按字段名ASCII码升序排列后再拼接而代码里有人写成了按请求顺序拼接。这种问题光看代码很难发现必须用原始报文逐步调试比对。5.2 部署运行类问题部署阶段常见的坑有五个。第一是MySQL版本太老SQL里有窗口函数或者utf8mb4相关语法老版本不支持建议直接用8.0以上版本。第二是前端npm install报错大部分情况是Node版本不匹配Vue 3项目建议Node 16以上如果node_modules不干净删掉重新安装往往比一个个解决依赖冲突快得多。第三是跨域问题前端开发环境调用后端接口需要配置代理生产环境则让Nginx统一转发不要前端直接请求8080端口。第四是WebSocket连不上后端WebSocket地址要用ws://协议如果Nginx做了SSL需要配置WebSocket代理的Upgrade头不然前端一直报连接失败。第五是Cron表达式时区问题定时任务执行时间和预期差8小时需要检查JVM默认时区。5.3 数据库与数据一致性陷阱数据库这层最容易出问题的是事务边界。比如核销操作里如果忘了给方法加Transactional本地记录插入和平台调用之间的异常就会导致数据不一致。加了事务之后还要注意事务里不能做耗时太长的外部调用否则数据库连接会长时间占用并发一高就拖垮整个系统。正规做法是本地事务只负责写库外部平台调用放在事务前完成或者用本地消息表加异步确认的方式解耦。另一个容易忽略的是订单状态机。三种订单类型的合法状态流转并不完全一样比如外卖订单可以从“待处理”变“已确认”再变“已完成”但不能从“待处理”直接跳“已完成”。如果在代码里不加校验用户直接调接口篡改状态对账就会乱。建议在状态更新方法里做一个简单的状态机校验不允许的状态流转直接抛异常。5.4 源码来源与安全隐患的预防最后一定要提醒的是源码安全问题。我见过不少开发者在网上找了源码解压之后连看都不看就放到服务器上跑结果服务器被植入了挖矿程序。这里分享几条硬经验。第一尽量从官方渠道或可信的代码托管平台获取源码热门项目看star数和最近更新频率第二部署之前先做依赖扫描尤其是老项目里常见的Log4j、Fastjson等组件优先升级到安全版本第三生产环境数据库账号不要用root单独建一个只有业务库权限的账号第四系统里不要保留默认密码管理员账号、数据库密码、Redis密码全部改掉第五开启操作日志和登录日志万一出事有迹可循。我个人在实际操作中的体会是这类“三合一系统源码”最大的价值不在于代码本身能直接商用而在于它把外卖、团购、店内点餐这三种非常典型的业务场景浓缩到了一个项目里。你只要认真读一遍订单表和核销逻辑再动手把部署和二次开发走一遍对本地生活类信息系统的理解会比看十篇教程都深。如果你拿到的源码版本和这篇文章不完全一样也不用纠结具体类名重点是抓住“聚合订单表、双通道同步、事务先行、状态机校验”这几个设计核心把思路迁移到自己的项目里才是最重要的。后面我还会继续整理门店系统里会员营销和供应链库存的扩展方案到时候再跟你分享。本文还有配套的精品资源点击获取