Admin 配置 → 客户端同步:行业调研

Guest App 品牌/功能配置的实时传播方案对比 — 更新于 2026-02-18

问题定义

核心场景

管理员在 Web 后台修改 Guest App 配置(Logo、主题色、功能开关、首页布局等),修改后需要同步到所有正在运行的客户端:

Flutter Guest App (iOS/Android) iPad Kiosk (Flutter iOS) Web 预约页 (Next.js)

关键约束:配置变更是低频事件(一天几次),不是高频实时数据流。

主流平台做法

Firebase Remote Config

机制:客户端 SDK 拉取(Pull-based)

  • 客户端调用 fetchAndActivate() 主动拉取
  • 默认缓存 12 小时(生产),开发环境可设为 0
  • 可结合 FCM 静默推送触发客户端立即 fetch
  • 本地有缓存层,离线时使用上次成功的值
  • 支持条件定向(按版本、平台、用户属性)

结论:不是实时的,靠客户端主动拉取

LaunchDarkly Feature Flags

机制:SSE 流式推送(Server-Sent Events)

  • 服务端/移动端 SDK 默认用 SSE 保持连接
  • 传播速度 毫秒级
  • 移动端后台断开,前台重连获取最新值
  • 本地有 SQLite/SharedPreferences 持久化缓存
  • 提供 Relay Proxy 给高可用场景

最激进的实时方案,但它是专职 Feature Flag 服务

Shopify 主题/品牌配置

机制:Pull-based + ETag

  • 商家修改主题后,店面下次请求时获取新配置
  • 使用 ETag / Last-Modified 减少无变更时的带宽
  • Webhook 通知第三方应用配置变更
  • GraphQL 支持精确查询所需字段
  • Web 端几乎即时(下次页面加载),移动端取决于缓存

最接近我们场景的参考:品牌配置走轮询

Square POS / 零售

机制:API 轮询 + Webhook 事件通知

  • POS 终端定时轮询 + 推送通知触发拉取
  • 传播速度 分钟级
  • POS 有完整离线模式,本地存储完整配置副本
  • 网络恢复后自动同步
  • 版本化配置,支持增量更新

设备端(Kiosk/POS)的最佳参考

Zenoti 沙龙 SaaS

机制:REST API 轮询为主

  • 移动端启动时拉取,定时刷新
  • 传播速度 分钟 ~ 小时级
  • iPad Kiosk 启动时全量加载配置
  • 品牌/主题变更不要求即时生效
  • 业务关键配置(价格、服务)刷新更频繁

同行业标杆:配置同步走简单轮询

Vagaro / Boulevard / Phorest

机制:REST API 轮询

  • 所有沙龙行业竞品都采用轮询模式
  • 没有 WebSocket / SSE 推送品牌配置的先例
  • 配置生效时间 1~5 分钟被视为正常
  • Kiosk 设备"启动加载 + 定时刷新"是行业标配

行业共识:配置同步不需要实时

五种技术方案详细对比
维度 轮询 + ETag SSE WebSocket 推送 (FCM/APNs) 混合 (轮询+推送)
传播延迟 取决于间隔 (30s~5min) 毫秒级 毫秒级 秒级 (不保证) 秒级
服务端复杂度 中 (依赖第三方)
移动端电池 中 (需维持连接) 高 (持久连接) 极低 (OS 优化)
移动端带宽 低 (ETag 304) 低 (仅变更) 极低 (信号量)
离线支持 天然 (缓存) 需额外实现 需额外实现 信号可排队
多租户扩展性 优 (无状态) 中 (每客户端一连接) 差 (连接数爆炸) 优 (FCM 代管)
Flutter 支持 原生 HTTP 需 SSE 库 原生 WebSocket firebase_messaging 组合实现
iPad Kiosk 适用 过度 不必要
实现成本 最低 低~中
谁在用 Shopify, Zenoti, Square, Vagaro LaunchDarkly (配置推送场景无主流采用) Firebase RC (可选) Firebase RC + FCM
什么场景用什么方案
场景特征 推荐方案 典型例子
低频配置变更 + 多客户端 轮询 + ETag 品牌/主题/Logo/功能开关(Celoria 当前场景)
Feature Flag 频繁切换 SSE 推送 LaunchDarkly、A/B 测试框架
实时双向通信 WebSocket 聊天、协作编辑、POS 支付状态
移动端后台唤醒 Push (FCM) + 拉取 重要配置变更需即时通知用户
设备端 (Kiosk/POS) 配置同步 启动加载 + 中等间隔轮询 Square POS, Zenoti Kiosk
为什么 SSE / WebSocket 不适合当前场景

过度工程的典型信号

论点 反驳
"实时推送体验更好" 配置变更一天几次,60 秒轮询 vs 毫秒推送对用户体验差异为 。没有客人会盯着 Logo 等它变。
"SSE 很轻量" 多租户下每个在线客户端维持一个连接。10 租户 x 5 设备 = 50 持久连接。虽不多,但对于日均 <5 次配置变更来说完全不必要。
"已有 settingEventEmitter" 那是服务端内部事件,不等于客户端推送。要到客户端还需要加 SSE endpoint + Flutter SSE 库 + 重连逻辑 + 鉴权。
"WebSocket 已经在用了" 现有 WebSocket 用于 POS 支付状态(高频双向),不适合复用给配置推送(低频单向)。混用会增加连接管理复杂度。
Celoria 推荐方案

Celoria 轮询 + 版本号 + ETag

管理员 修改 Guest App 配置(Logo / 主题 / 功能开关) | v 后端 API 保存到 setting_values |-- 递增 config_version(租户级自增整数) |-- 更新 config_updated_at 时间戳 |-- 清除 SettingCache v 客户端 定时轮询 | GET /api/public/booking/:tenant/app-config If-None-Match: "{etag}" | +--> ETag 匹配 --> 304 Not Modified (0 bytes, ~2ms) | +--> ETag 不匹配 --> 200 + 完整配置 JSON | v 更新本地缓存 --> 触发 UI 刷新

各客户端具体策略

客户端 轮询间隔 刷新触发时机 离线缓存 实现方式
Web 预约页
(Next.js)
120 秒 页面加载 / Tab 重新可见 (visibilitychange) sessionStorage React Query refetchInterval + refetchOnWindowFocus
Flutter Guest App
(iOS/Android)
前台 60 秒
后台不轮询
启动时 / App 从后台恢复 (AppLifecycleState.resumed) Hive / SharedPreferences Timer.periodic + Hive 持久化
iPad Kiosk
(Flutter iOS)
60 秒 启动时全量加载 本地文件缓存 Timer.periodic(常驻前台,始终供电 + WiFi)

API 设计要点

  1. 版本号机制:每个租户维护 config_version(自增整数)。当前 ETag 用 Date.now() 生成 — 每次都不同,等于无效。改为基于 config_version 或内容哈希
  2. ETag 响应:基于 config_version 生成,客户端发送 If-None-Match,无变更时返回 304,零带宽
  3. Cache-Control:private, max-age=30,配合 ETag 实现条件请求。当前用 public, max-age=300(5 分钟强缓存)太激进
  4. 分层配置(可选优化):区分 branding(Logo/主题)/ features(功能开关)/ business(营业时间),允许客户端只请求关心的层
当前代码现状 vs 改进
维度 当前实现 问题 改进
ETag ETag: "${Date.now()}"
(booking.js:443)
每次请求都不同,等于无效 改为 "v{config_version}" 或内容哈希
Cache-Control public, max-age=300 5 分钟强缓存,即使服务端有更新也拿不到 private, max-age=30 + ETag 条件请求
版本号 客户端无法判断是否有变更 租户级 config_version 自增整数
Flutter 端 启动时加载一次,有 isStale 判断但仅在手动拉取时检查 无定时刷新,应用运行期间不会获取更新 加 Timer.periodic 前台定时轮询
事件广播 settingEventEmitter 已存在,支持 SSE/WS 仅服务端内部使用,无客户端订阅端点 可保留用于 Web Admin 实时预览(非 Guest App)
实施路线

分阶段实施

阶段 任务 影响范围 工作量
P0 后端:租户级 config_version + 修复 ETag + 调整 Cache-Control booking.js, setting-service.js ~2h
P0 Web 端:React Query refetchInterval + refetchOnWindowFocus Guest App 预约页(如已有) ~1h
P1 iPad Kiosk:Timer.periodic 轮询 + 本地缓存 ipad_app/ 配置模块 ~2h
P1 Flutter Guest App:前台轮询 + AppLifecycle 触发 + Hive 缓存 guest_app/ 配置模块 ~3h
P2 (可选) FCM 静默推送:管理员保存时触发 FCM → 移动端立即 fetch 后端 FCM 集成 + Flutter firebase_messaging ~4h
行业结论

沙龙/零售 SaaS 行业共识

1. 品牌配置变更(Logo/主题/功能开关)不需要毫秒级实时同步。1~2 分钟生效在所有竞品中被视为正常。

2. 没有任何沙龙行业竞品使用 WebSocket 或 SSE 推送品牌配置。

3. 轮询 + ETag 是行业标配。Shopify/Square/Zenoti/Vagaro 全部采用这个模式。

4. Kiosk 设备采用"启动加载 + 定时刷新"是所有竞品的通用做法。

5. 真正需要实时推送的场景是支付状态协作编辑,不是配置同步。

最终推荐

对 Celoria 而言,轮询 + 版本号 + ETag 是最优方案。理由:

简单可靠 — 无持久连接,无重连逻辑,无第三方依赖

行业验证 — Shopify/Square/Zenoti 全部这么做

零无效带宽 — ETag 304 响应仅 ~200 bytes

天然离线支持 — 本地缓存即离线降级

基础设施已 80% 就绪 — app-config API 已存在,只需修复 ETag + 加轮询

预期效果:管理员保存配置后,所有客户端在 30~120 秒内自动获取并应用更新。无需手动刷新。