这个页面有一个前面所有后台都没有的前提:它开一整天不刷新。早上开店打开,晚上关店才关,中间十几个小时。这个前提让一批平时不会暴露的问题必然暴露。
一是内存持续增长最后卡死。订单一天下来几百上千条,全都堆在列表里;每个订单卡片有定时器(显示已等待时长);轮询的响应数据不断累积。到了晚上高峰期页面已经卡得点不动了,店员只能刷新页面——而刷新的瞬间可能漏掉一单。
二是长连接假死没人发现。网络抖动后长连接在浏览器看来还是 open,但实际收不到消息。店员以为没单,其实单一直在进来,直到用户打电话来催或者平台超时罚款。这是商家最不能接受的问题——漏一单就是一次差评加一次罚款。
三是提醒响一声不够。店员在后厨忙,一声「叮」听不到。等他回到电脑前,单已经等了十分钟。
四是打印机是实体设备,会各种坏。缺纸、离线、卡纸、被拔了电源。第一版打印失败只在控制台报了个错,店员完全不知道有单没打出来,后厨没收到小票就不做,用户等不到餐。
所以四个设计:长连接加轮询双通道、未处理单持续提醒且置顶计时、打印双通道加失败重试与补打、长时运行的内存与连接治理。
轮询兜底的代价是额外请求,但这个代价必须付。30 秒一次轮询,一天两千多次请求,乘以商家数量不算小。但它换来的是「长连接假死能被发现」,而漏单对商家意味着差评加罚款,对平台意味着投诉。取舍方向很明确:宁可多花点服务器资源,不可漏单。优化手段是轮询接口做得极轻(只返回「上次位点之后有没有新单」这一个判断)。
云打印和本地打印都支持,代价是两套代码两套排障路径。但商家的条件差异是客观的——连锁品牌愿意买云打印机,夫妻小店只有一台旧的热敏打印机。强推一种会直接损失一批商家。缓解手段是把打印能力抽象成统一接口,业务层不关心走哪个通道。
踩过的坑:长连接假死导致某商家漏了十几单。网络抖动后连接状态还是 open,服务端也认为连接活着(因为 TCP 层没断),但消息推不过去。店员一上午没看到新单,以为生意不好,直到用户打电话来问才发现。这一次事故直接推动了轮询兜底和心跳双向检测。教训是:连接状态不等于消息可达,必须用「有没有收到应该收到的消息」来判断链路是否正常。
踩过的坑二:浏览器静音策略导致提醒音一直没响。店员早上打开页面后没有任何点击就去忙了,浏览器因为「没有用户交互」拒绝播放音频,整个上午的提醒音都是静音的,而页面上没有任何异常提示。修法是进入工作台时强制一次「开启提醒音」的交互,并且实际尝试播放一次来检测是否真的可播,不可播时在页面顶部显示醒目的红色警告。教训是:依赖浏览器策略的能力必须做「实际可用性检测」,不能假设 API 调用成功就等于功能生效。
没做的部分:没做桌面通知(系统级弹窗)。它能在浏览器最小化时也提醒,但需要用户授权,而且商家电脑上往往还开着其他收银软件,通知会被淹没。当时靠「持续响铃 + 标签标题闪烁」覆盖,后来商家更倾向于用云打印机来兜底(打印机响了就知道有单)。
12 小时内存稳定:用浏览器的性能面板做长时间录制,或者定时采样 performance.memory。关键是要模拟真实的订单流入——不能空跑 12 小时,要用脚本按真实节奏(含高峰)注入订单。观察内存曲线是否呈持续上升趋势,这比绝对值更重要。要说明测试的订单总量(比如 12 小时注入 800 单),否则「300MB 以内」没有参考意义。
推送到响铃:服务端推送时刻与客户端音频开始播放时刻各打点,约 1.5 秒。要说明这是长连接通道的数字,轮询兜底通道的延迟是一个轮询周期(最坏 30 秒),两个要分别报。
「长连接假死能被发现」怎么验证:这是这个模块最该演练的。做法是用代理工具让 WebSocket 连接保持但丢弃消息(模拟假死),验证轮询在一个周期内发现有未推送的单、触发告警、重建连接、并补齐那段时间的单。不能只测「断线重连」——断线是显式的容易处理,假死才是真正的坑。
打印失败的处理:物理演练——把打印机纸抽掉、拔掉网线、制造卡纸,验证每种情况下都能收到失败状态、订单标红、店员收到提示、补打可用。这类和实体设备相关的验证只能物理演练,没法用代码模拟全部情况。
不要报「漏单率降为 0」。绝对化断言,而且漏单可能由商家自己的原因造成(电脑关机、断网)。正确表述是「因客户端链路问题导致的漏单,在双通道加持续提醒后未再出现」,并且能说出双通道的检测延迟上界。
菜品管理看起来是最普通的增删改查,但真做起来有三个问题让第一版几乎不可用。
一是规格组合爆炸。一个奶茶有「大杯中杯小杯」三个规格、「常温少冰去冰全冰」四个做法,组合起来 12 个 SKU,每个可能有不同价格。第一版是让商家逐个添加 SKU,填 12 次表单,而且改价要再改 12 次。商家的做法是干脆不用规格,把「大杯」「中杯」建成两个独立菜品——数据就乱了。
二是估清(售罄)操作太慢,而它是最高频的操作。某个菜卖完了,店员要点进菜品编辑页、改库存或改状态、保存、等刷新。而估清在饭点是每几分钟就要做一次的操作,而且要求立刻生效——晚一分钟就可能有用户下了一单卖不了的菜,最后是退单加差评。第一版把估清做成了「编辑菜品的一个字段」,路径长且和其他字段一起保存,慢且容易误改。
三是连锁商家的批量改价把门店特价冲掉了。总部要给所有门店涨价,一键下发,结果把某几家店针对周边竞争做的本地特价全覆盖了,门店店长来投诉。第一版的批量下发是无脑覆盖,没有任何差异提示。
所以三个设计:规格组合做成矩阵编辑、估清独立成轻操作并即时生效、批量下发前展示逐店差异并允许选择性下发。
矩阵编辑在维度多时会退化。两个维度是表格,三个维度(杯型 × 做法 × 糖度)就是三维,没法用平面表格表达。我们的处理是限制规格组数量(最多两组进矩阵,第三组以上退回列表式编辑),并且在界面上说明原因。承认「这个交互有适用范围」比硬做一个三维编辑器好——后者的可用性一定很差。
估清即时生效的代价是缓存失效的复杂度。菜单缓存在多层(CDN、服务端缓存、客户端缓存),估清要让每一层都及时失效。做法是菜单缓存的 key 带一个店铺级的版本号,估清时递增版本号,所有层的旧缓存自然失效。代价是估清会导致整个店的菜单缓存全部失效(而不只是那一个菜),高频估清时会有一波回源。这个代价可以接受——估清的频率虽然高,但相对于菜单查询量还是低得多。
踩过的坑:批量下发覆盖了门店本地特价,门店店长集体投诉。总部统一涨价,一键下发到 40 家店,其中 6 家店针对周边竞争做的本地特价被全部覆盖,这几家店的销量当天就掉了,店长投诉到总部。修法就是差异预览加选择性下发,并且把默认选项改成「排除有本地自定义的门店」。教训是:批量操作的默认行为必须是最保守的那个,因为操作者往往一路点确定。
踩过的坑二:估清走了菜品完整保存接口,把店员之前的误改一起提交了。店员在编辑页误改了菜品描述但没保存就离开了(表单有本地暂存),后来他在列表上点估清,而估清复用的是完整保存接口,把暂存的错误描述一起提交上去了。修法是估清用独立的轻接口,只改可售状态这一个字段。教训是:高频的单字段操作必须有专用接口,复用完整保存接口会带来意料之外的字段变更。
没做的部分:没做菜品的定时上下架(比如午市菜品到点自动上架)。商家提过,需要定时任务和时区处理,而且和「今日估清次日恢复」的逻辑有交叉,当时怕把状态模型搞复杂,没做。
规格组合的编辑成本:报「12 个组合由逐个填写变为矩阵批量,实际需要改动的格子数」。这个对比比「效率提升 N 倍」诚实——实际改动格子数取决于定价规律(如果 12 个价格全不同,矩阵也帮不上忙;而真实情况通常是按维度加价,只需改 2 到 3 处)。要说明这个前提。
估清生效延迟:从店员点击到用户端菜单不再展示该菜品,约 2 秒。要说清链路(改状态、递增菜单版本号、各层缓存失效、用户端推送或下次请求),以及「已打开菜单页的用户」和「新进入的用户」两种情况分别是多久——前者靠推送,后者靠缓存版本号,延迟不同。
批量下发的差异预览:报「差异预览拦下多少次会覆盖本地特价的下发」。这是个绝对数、口径清晰,而且直接证明这个功能的价值。比说「我做了差异预览」有说服力。
不要报「菜品管理效率提升」或「估清准确率」。前者算不出基线;后者「准确」没有明确定义。可以报的是「因未及时估清导致的退单量」变化——但要注意这个指标受商家自身勤快程度影响,我们只能说降低了操作成本。
这两个配置看起来是小功能,但它们直接决定商家能不能接到单,配错了商家会认为是平台的问题。
第一版的配送范围是「填一个半径」,营业时间是「填开始和结束时间」。都不够用。
一是圆形范围完全不符合实际。门店旁边隔着一条河或者一条高速,直线距离一公里但实际要绕十公里;反过来沿着主干道三公里内很好送。用半径的结果是「近的送不了、远的送不到」,商家只能把半径调小,损失了本该能做的生意。
二是一天只有一个营业时段不够。很多店是「午市 11 到 14 点、晚市 17 到 21 点」,中间休息。第一版只能填一个时段,商家只好填 11 到 21 点,结果下午三点有人下单,店里没人做,只能拒单,而拒单会影响商家评分。
三是跨零点的时段表达不了。夜宵店是 18 点到次日 2 点。第一版的「开始时间小于结束时间」校验直接把这种情况判为非法。
四是配错了没有任何提示。商家把多边形画歪了、门店压根不在自己画的范围里、或者把范围画成了自相交的形状,系统照样保存,然后就没单了,商家来投诉「你们不给我派单」。
所以四个设计:范围用多边形而不是半径、营业时间支持一天多时段与按周差异、时段模型要能表达跨零点、保存前做几何与逻辑校验并给影响预览。
多边形比半径灵活,代价是商家要会画、我们要做校验。部分商家(尤其是年纪大的店主)不会用地图绘制。缓解手段是保留「按半径快速生成」作为起点——先生成一个圆形近似的多边形,商家再拖顶点微调。这比「二选一」好:既降低了上手门槛,又保留了精细调整的能力。
点在多边形内的判定要做性能优化,因为它在下单链路上。每次下单都要判定用户地址是否在范围内,而多边形可能有几十个顶点。做法是先用外接矩形快速排除(绝大多数不在范围的地址会在这一步被排除,成本是几次数值比较),只有通过矩形判定的才做精确的多边形判定。这是典型的「便宜的粗筛 + 昂贵的精判」两段结构,和搜索里的网格预筛是同一个思路。
踩过的坑:自相交多边形导致「同一个地址有时能下单有时不能」。商家画的范围自相交了,我们的判定用的是射线法,对自相交图形,射线法的结果取决于射线方向,而不同请求的计算路径略有差异,结果同一个地址的判定不稳定。用户投诉「刚才还能下单现在不能了」,我们查了很久才定位到是多边形本身非法。修法是保存时做自相交校验并拒绝保存。教训是:几何数据必须在入口做合法性校验,因为下游算法对非法输入的行为是未定义的。
踩过的坑二:跨零点时段被校验规则禁掉,夜宵商家没法配。第一版有个「结束时间必须晚于开始时间」的校验,看起来合理,但把所有夜宵店都挡住了,商家只能配成「18:00-23:59」然后次日的部分完全接不到单。修法是加「跨天」标记,判定时把跨天时段拆开算。教训是:看起来「显然合理」的校验规则往往隐含了对业务的错误假设,加校验前要想清楚有没有正当的反例。
没做的部分:没做基于实际路网的可达范围生成(输入配送时长上限,自动生成「15 分钟骑行可达」的等时圈)。这比手画准确得多,但需要路网数据和等时圈计算服务,成本较高,当时是让商家自己画。
「配置错误在保存前被拦下」怎么量化:报各类校验的触发次数分布(自相交、门店不在范围内、时段重叠、面积超限)。这个分布很有信息量——如果「门店不在范围内」占大头,说明绘制交互有问题(可能是地图初始位置没定位到门店),要改交互而不只是加校验。
点在多边形内判定的性能:压测下单接口,对比「有外接矩形预筛」和「直接精确判定」的耗时差。要说明多边形的顶点数,因为耗时和顶点数直接相关。
「跨零点时段正确」怎么验证:写单测覆盖边界——18:00 到次日 02:00 的配置下,判定 19:00、23:59、00:01、01:59、03:00 各时刻的营业状态。这类逻辑用单测覆盖边界比用线上数据说明更有力,而且要覆盖跨月、跨年、夏令时切换(如果业务涉及有夏令时的地区)。
影响预览的价值:可以报「保存前预览显示与商家预期不符、商家因此修改了配置的次数」——如果有埋点的话。这个数字直接证明预览拦下了错误配置。
不要报「商家配置错误率下降 N%」。没有可靠基线(改造前的错误配置压根没被记录,因为没有校验)。正确表述是「哪些错误现在会被拦下、各拦下多少次」。
没有匹配的内容,换个关键词试试。
项目拆解 · 商家管理后台(Vue)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据