在餐饮 SaaS 领域,“点餐小程序”几乎是标配。但在实际运营中,很多团队会遇到几个典型问题:

  • 高峰期下单卡顿:午市、晚市并发上来后,接口超时、下单失败。
  • 首屏加载慢:用户打开点餐页要等 2~3 秒才能看到菜品。
  • 包体积大、低端机掉帧:Flutter 页面在低配安卓机上滑动不流畅。
  • 后端耦合严重:订单、菜品等逻辑揉在一起,难以扩展。
  • 源码及演示:s.ymzan.top

为了彻底解决这些问题,我们对整套点餐系统进行了一次从端到云的全链路性能优化,技术选型如下:

层级 技术选型
前端 Flutter(小程序容器:Taro/uni-app 混合方案)
网关 Go + Gin
服务治理 gRPC + Consul
数据层 MySQL + Redis + Elasticsearch
部署 Docker + Kubernetes

本文不会讲太多概念,而是围绕真实业务场景,拆解我们做过的具体优化动作,并给出可直接复用的代码示例
在这里插入图片描述

整体架构概览

我们先给一张简化版架构图(文字版):

[Flutter 小程序]
        ↓ HTTPS / WebSocket
[Gin API Gateway]
  ├─ 鉴权 / 限流 / 熔断
  ├─ 请求聚合(BFF)
        ↓ gRPC
[Order Service] [Product Service] [User Service] [Payment Service]
        ↓
[MySQL / Redis / ES]

核心思想只有一句话:端侧重渲染与缓存,网关重聚合与保护,服务侧重拆分与异步。

Flutter 端:首屏与交互性能优化

1. 首屏加载:从 2.5s 到 600ms

点餐首页的核心数据包括:

  • 店铺信息
  • 分类列表
  • 商品列表(含规格、库存)
  • 活动标签
优化前的问题
  • 页面 initState 里串行请求 4 个接口
  • 每个接口都返回大量冗余字段
  • 没有本地缓存策略
优化方案

① 接口聚合(BFF)

由网关统一提供一个 /recommend/home 接口,一次性返回首页所需全部数据:

// HomeResp 首页聚合响应
type HomeResp struct {
  ShopInfo    *ShopInfo    `json:"shop_info"`
  Categories  []Category   `json:"categories"`
  Products    []Product    `json:"products"`
  Activities  []Activity   `json:"activities"`
}

Flutter 侧只需要一次请求:

Future<HomeData> loadHomeData() async {
  final resp = await dio.get('/recommend/home');
  return HomeData.fromJson(resp.data);
}

② 字段裁剪 + Protobuf

  • 只返回前端真正使用的字段
  • 网关到内部服务使用 gRPC + Protobuf,减少序列化开销

③ 本地缓存 + 版本号

class HomeCache {
  static const _key = 'home_data_v1';

  static Future<void> save(HomeData data) async {
    final prefs = await SharedPreferences.getInstance();
    prefs.setString(_key, jsonEncode(data.toJson()));
  }

  static Future<HomeData?> get() async {
    final prefs = await SharedPreferences.getInstance();
    final str = prefs.getString(_key);
    if (str == null) return null;
    return HomeData.fromJson(jsonDecode(str));
  }
}

配合后端返回的 data_version,版本一致直接用缓存,不一致再更新。

📌 效果:首屏接口耗时从 2.5s → 600ms,弱网环境提升尤为明显。

2. 列表渲染:长列表不卡顿

点餐系统的商品列表往往有上百条,Flutter 常见坑是:

  • 使用 ListView 直接渲染全部 Widget
  • 每次 setState 重建大量节点
优化要点

① 使用 ListView.builder + const Widget

ListView.builder(
  itemCount: products.length,
  itemBuilder: (context, index) {
    final p = products[index];
    return ProductItem(
      key: ValueKey(p.id),
      product: p,
    );
  },
)
class ProductItem extends StatelessWidget {
  const ProductItem({required this.product, Key? key}) : super(key: key);
  final Product product;

  
  Widget build(BuildContext context) {
    return // 精简布局,避免深层嵌套
  }
}

② 图片优化

  • 使用 CDN + WebP
  • 缩略图尺寸控制在 200×200 以内
  • 懒加载:cached_network_image
CachedNetworkImage(
  imageUrl: product.coverUrl + '!thumb',
  width: 80,
  height: 80,
  fit: BoxFit.cover,
)

📌 效果:低端安卓机滑动帧率稳定在 55fps 以上。

3. 状态管理:减少无效刷新

点餐过程中频繁操作:

  • 加菜 / 减菜
  • 切换规格
  • 选择优惠券

如果全局 setState,性能会非常糟糕。

我们采用 Riverpod + 局部刷新

final cartProvider =
    StateNotifierProvider<CartNotifier, CartState>((ref) {
  return CartNotifier();
});

UI 层只监听需要的数据:

final totalPrice = ref.watch(cartProvider.select((c) => c.totalPrice));

📌 收益:UI 刷新次数减少 60% 以上。

Go 微服务:高并发下的稳定性优化

1. 服务拆分边界

我们按业务能力拆分服务,而不是按技术层:

服务 职责
Order Service 下单、订单状态流转
Product Service 商品、分类、库存
User Service 用户、会员、地址
Payment Service 支付、退款
Marketing Service 优惠券、满减

服务间通信统一使用 gRPC,避免 HTTP JSON 的重复解析。

2. 下单链路性能优化

下单是点餐系统中最关键的链路,我们做了几件事:

① 库存扣减:Redis Lua 脚本

避免“超卖”同时保证性能:

-- stock.lua
local stock = tonumber(redis.call("GET", KEYS[1]))
if stock < tonumber(ARGV[1]) then
  return -1
end
redis.call("DECRBY", KEYS[1], ARGV[1])
return stock - tonumber(ARGV[1])

Go 调用示例:

script := redis.NewScript(stockLua)
res, err := script.Run(ctx, rdb,
  []string{"stock:product:" + productID},
  qty,
).Int()
② 订单创建异步化

核心流程只做:

  • 参数校验
  • 库存预扣
  • 订单写入(MySQL)
  • 返回订单号

后续操作(推送厨房、通知商家、积分计算)通过 Kafka 异步处理:

kafka.Producer.Send(&sarama.ProducerMessage{
  Topic: "order.created",
  Value: sarama.StringEncoder(orderJSON),
})

📌 效果:下单接口 P99 从 800ms 降到 120ms。

3. 缓存设计:减少 DB 压力

热点数据全部进 Redis:

数据 缓存 Key TTL
商品详情 product:{id} 5 min
店铺信息 shop:{id} 10 min
活动配置 activity:list 1 min
库存 stock:product:{id} 实时

并统一封装缓存模板:

func CacheGetctx context.Context, key string, loader func( (*T, error)) (*T, error) {
  var val T
  if err := cache.Get(ctx, key, &val); err == nil {
    return &val, nil
  }
  v, err := loader()
  if err != nil {
    return nil, err
  }
  cache.Set(ctx, key, v, time.Minute*5)
  return v, nil
}

网关层:BFF + 限流 + 熔断

1. BFF(Backend For Frontend)

网关负责:

  • 接口聚合
  • 字段裁剪
  • 协议转换(gRPC ↔ HTTP)
func HomeHandler(c *gin.Context) {
  ctx := c.Request.Context()

  orderClient := orderpb.NewOrderClient(conn)
  productClient := productpb.NewProductClient(conn)

  // 并发调用
  g, _ := errgroup.WithContext(ctx)
  var products *productpb.ProductListResp
  var orders *orderpb.OrderCountResp

  g.Go(func() error {
    products, _ = productClient.List(ctx, req)
    return nil
  })
  g.Go(func() error {
    orders, _ = orderClient.Count(ctx, req)
    return nil
  })

  g.Wait()
  // 组装返回
}

2. 限流与熔断

  • 限流:令牌桶算法(uber/ratelimit)
  • 熔断:hystrix-go
hystrix.ConfigureCommand("order_service", hystrix.CommandConfig{
  Timeout:                1000,
  MaxConcurrentRequests:  100,
  ErrorPercentThreshold: 50,
})

📌 作用:高峰期某个服务挂掉,不会导致整个点餐系统雪崩。


六、数据库与索引优化(简要)

  • 订单表按 shop_id + create_time 建联合索引
  • 商品表避免 SELECT *
  • 大文本字段(描述、富文本)单独拆表
  • 报表类查询走 ES,不直接打 MySQL

总结

回顾这次点餐系统的重构之旅,与其说是技术的堆砌,不如称之为一次对“用户体验”的极致致敬。我们用 Flutter 的灵活抹平了端的差异,用 Go 的简洁与高效撑起了高并发的底盘。但真正的挑战不在于写出多少行代码,而在于如何在毫秒级的响应中,找到架构稳定与业务敏捷之间的平衡。从 Redis Lua 的原子锁到 gRPC 的流式通信,每一个技术决策背后,都是对系统瓶颈的精准打击。技术永远在迭代,微服务与跨端开发也只是当下的答案,而非终点。希望这篇万字复盘,不仅能为你提供一套可复用的点餐源码优化方案,更能成为你架构设计中应对复杂业务时的那盏引路灯。

Logo

一站式 AI 云服务平台

更多推荐