1. 外卖系统选型与源码与 SaaS 实践的思考
做外卖系统这些年,从零开始搭建过三套不同架构的平台,也参与过多个SaaS化改造项目。今天想聊聊在选型、源码管理和SaaS实践中的那些坑和经验。无论你是想自建外卖平台,还是准备做二开,这些实战总结应该能帮你少走弯路。
外卖系统本质上是个复杂的多角色协同体系,涉及用户端、商户端、骑手端和管理后台。选型时不仅要考虑技术栈的成熟度,更要关注业务扩展性和运维成本。而源码方案和SaaS模式的抉择,直接决定了后续三年的技术债务水平。
1.1 核心需求拆解
先看典型外卖系统的功能矩阵:
- 用户侧:LBS定位、智能推荐、优惠计算、支付流程
- 商户侧:接单打印、库存同步、营业统计
- 骑手侧:路径规划、订单聚合、电子围栏
- 平台侧:风控体系、结算对账、BI看板
这些模块的技术实现差异很大。比如路径规划需要GIS引擎,而优惠计算涉及规则引擎。在初期选型时,我们团队就犯过"全栈一把梭"的错误——用同一套技术栈实现所有功能,导致后期扩展时掣肘严重。
关键经验:按业务域划分技术栈,核心交易链路用Java+Spring Cloud,实时调度用Go,数据分析用Python。这种混合架构在美团、饿了么等大厂已成标配。
2. 源码方案深度解析
市面上的外卖源码主要分三类:
- 教学演示版(如Github上的校园外卖项目)
- 商业授权版(如某盟、某客的PHP/Java版本)
- 大厂开源组件(如美团的配送算法库)
2.1 教学版源码的陷阱
很多团队刚开始会贪图便宜使用教学版源码,这类代码通常存在:
- 数据库设计违反三范式(比如把订单和支付表合并)
- 硬编码配置(配送费直接写死在Java类里)
- 缺乏事务管理(同时扣减库存和创建订单可能不同步)
- 伪分布式(所有服务打包在同一个war包)
// 典型问题代码示例 - 没有分布式事务的库存操作 public void createOrder(Order order) { orderMapper.insert(order); // 插入订单 itemService.reduceStock(order.getItemId()); // 扣库存 // 如果这里抛出异常,订单已创建但库存未扣减 }2.2 商业源码的二开难题
我们曾采购过某知名商业源码,其架构存在几个致命伤:
- 过度封装:简单的查询接口套了6层抽象
- 数据库耦合:存储过程实现业务逻辑
- 文档缺失:关键表字段无注释
二次开发时最头疼的是支付模块改造。原系统使用支付网关的SDK版本是2018年的,而微信支付API已迭代了5个大版本。最终我们不得不重写整个支付服务,成本比预期高出3倍。
避坑指南:购买商业源码前务必要求提供ER图和API文档,重点检查支付、配送核心模块的代码质量。
3. SaaS化实践的血泪史
将外卖系统改造成SaaS服务时,我们踩过的坑可以写本书。最典型的三个问题:
3.1 多租户隔离方案选型
初期采用共享Schema+tenant_id字段的方案,结果出现:
- 某商户的SQL查询拖垮整个数据库
- 缓存键冲突导致数据错乱
- 分布式事务跨租户污染
后来改造为Schema隔离+物理资源分组,关键配置如下:
# 多租户数据源配置示例 tenants: - id: merchant_a db-url: jdbc:mysql://db-a.cluster:3306/core redis: redis-a:6379 - id: merchant_b db-url: jdbc:mysql://db-b.cluster:3306/core redis: redis-b:63793.2 动态定价的架构设计
不同商户需要配置不同的配送计价规则,我们先后尝试过:
- 硬编码if-else(维护噩梦)
- Drools规则引擎(学习成本高)
- 自定义DSL(最终方案)
// 配送费规则DSL示例 { "base": 5, "extra": { "time": {"19:00-21:00": 2}, "distance": [ {"range": [0,3], "fee": 0}, {"range": [3,10], "fee": "distance*0.8"} ] } }3.3 灰度发布的实践
直接全量上线新功能导致某连锁品牌门店无法接单,后来我们建立了一套完善的灰度机制:
- 商户标签分流(按城市/品类/规模)
- 功能开关控制(Feature Toggle)
- 影子测试(Shadow Testing)
-- 功能开关表示例 CREATE TABLE feature_toggle ( feature_name VARCHAR(50) PRIMARY KEY, is_enabled BOOLEAN DEFAULT false, rollout_percent INT DEFAULT 0, whitelist JSON COMMENT '允许的商户ID列表' );4. 性能优化实战记录
高峰期系统崩溃是最可怕的噩梦。经过多次压测,我们总结出几个关键优化点:
4.1 订单创建性能提升
优化前:平均RT 800ms,QPS 50 优化后:平均RT 120ms,QPS 300+
主要措施:
- 库存扣减改用Redis+Lua脚本
-- 库存扣减Lua脚本 local key = KEYS[1] local num = tonumber(ARGV[1]) local stock = tonumber(redis.call('GET', key)) if stock >= num then return redis.call('DECRBY', key, num) else return -1 end- 订单表按商户ID分片
- 支付流水异步化处理
4.2 配送路径计算优化
原生的Dijkstra算法在百万级路网数据下需要3-5秒计算,改进方案:
- 预计算城市热区网格
- 采用Contraction Hierarchies算法
- 骑手实时位置索引使用GeoHash
# GeoHash网格查询示例 def get_nearby_riders(lat, lng, radius): geohash = encode_geohash(lat, lng, precision=7) neighbors = get_adjacent_geohashes(geohash) return redis_client.georadius( "riders:location", lng, lat, radius, unit="m" )5. 运维监控体系建设
当系统服务上百家商户时,靠人肉运维根本不现实。我们的监控方案:
5.1 指标埋点设计
核心指标包括:
- 订单各阶段耗时(创建→支付→接单→完成)
- 资源利用率(CPU/Memory/DB连接池)
- 业务异常(库存不足、支付失败)
# Prometheus指标示例 order_stage_duration_seconds_bucket{stage="create",le="0.1"} 1234 order_stage_duration_seconds_bucket{stage="create",le="0.5"} 56785.2 日志架构优化
从原始的ELK升级到ClickHouse方案:
- Filebeat收集日志
- Kafka作为缓冲
- ClickHouse存储分析
- Grafana可视化
-- ClickHouse日志分析查询 SELECT merchant_id, count() AS errors, avg(duration) AS avg_time FROM log_table WHERE level = 'ERROR' AND time > now() - INTERVAL 1 HOUR GROUP BY merchant_id ORDER BY errors DESC LIMIT 106. 安全防护经验
外卖系统面临的安全威胁远超想象,我们遇到过的攻击包括:
- 商户账号撞库
- 优惠券批量刷取
- 骑手位置伪造
防御措施:
- 敏感操作二次验证(短信+行为验证码)
- 接口限流(Guava RateLimiter + Redis)
- 参数签名(HMAC-SHA256)
// 接口签名示例 public String generateSign(Map<String,String> params, String secret){ StringJoiner sj = new StringJoiner("&"); params.entrySet().stream() .sorted(Map.Entry.comparingByKey()) .forEach(e -> sj.add(e.getKey()+"="+e.getValue())); return HmacUtils.hmacSha256Hex(secret, sj.toString()); }这些年最大的体会是:外卖系统的复杂度被严重低估。很多团队以为买套源码改改就能上线,实际上每个环节都有深坑。特别是在SaaS化过程中,多租户隔离、个性化配置、性能监控这些隐形需求,往往比核心功能更消耗研发资源。