Guest App 品牌/功能配置的实时传播方案对比 — 更新于 2026-02-18
管理员在 Web 后台修改 Guest App 配置(Logo、主题色、功能开关、首页布局等),修改后需要同步到所有正在运行的客户端:
关键约束:配置变更是低频事件(一天几次),不是高频实时数据流。
机制:客户端 SDK 拉取(Pull-based)
fetchAndActivate() 主动拉取结论:不是实时的,靠客户端主动拉取
机制:SSE 流式推送(Server-Sent Events)
最激进的实时方案,但它是专职 Feature Flag 服务
机制:Pull-based + ETag
最接近我们场景的参考:品牌配置走轮询
机制:API 轮询 + Webhook 事件通知
设备端(Kiosk/POS)的最佳参考
机制:REST API 轮询为主
同行业标杆:配置同步走简单轮询
机制:REST API 轮询
行业共识:配置同步不需要实时
| 维度 | 轮询 + 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 |
| 论点 | 反驳 |
|---|---|
| "实时推送体验更好" | 配置变更一天几次,60 秒轮询 vs 毫秒推送对用户体验差异为 零。没有客人会盯着 Logo 等它变。 |
| "SSE 很轻量" | 多租户下每个在线客户端维持一个连接。10 租户 x 5 设备 = 50 持久连接。虽不多,但对于日均 <5 次配置变更来说完全不必要。 |
| "已有 settingEventEmitter" | 那是服务端内部事件,不等于客户端推送。要到客户端还需要加 SSE endpoint + Flutter SSE 库 + 重连逻辑 + 鉴权。 |
| "WebSocket 已经在用了" | 现有 WebSocket 用于 POS 支付状态(高频双向),不适合复用给配置推送(低频单向)。混用会增加连接管理复杂度。 |
| 客户端 | 轮询间隔 | 刷新触发时机 | 离线缓存 | 实现方式 |
|---|---|---|---|---|
| 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) |
config_version(自增整数)。当前 ETag 用 Date.now() 生成 — 每次都不同,等于无效。改为基于 config_version 或内容哈希config_version 生成,客户端发送 If-None-Match,无变更时返回 304,零带宽private, max-age=30,配合 ETag 实现条件请求。当前用 public, max-age=300(5 分钟强缓存)太激进branding(Logo/主题)/ features(功能开关)/ business(营业时间),允许客户端只请求关心的层| 维度 | 当前实现 | 问题 | 改进 |
|---|---|---|---|
| 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 |
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 秒内自动获取并应用更新。无需手动刷新。