先说清这个页面的真实工作量:一家酒店有二十来个房型,运营要维护未来三个月的房态,那是一千八百个格子。旺季调价、周末关房、批量补库存,这些都是每天要做的事。
第一版是标准的后台做法:一个列表,每行一个「房型 + 日期」,点「编辑」弹窗改一条。运营的反馈是「这个功能没法用」。
一是工作量不可接受。改一个月的价格要点开三十次弹窗,每次填表单、点保存、等刷新。运营实际的做法是找我们提数据库脚本,这本身就说明功能失败了。
二是看不到全局。列表是一维的,运营需要的是「这个月哪几天满房、哪几天价格异常」这种二维的直观视图,列表压根表达不了。
三是数据量一大就卡。第二版做成了表格,但把 1800 个格子全部渲染出来,每个格子还是可编辑的输入框,页面直接卡住几秒。
四是多人编辑互相覆盖。两个运营同时改同一个房型的房态,后保存的把前面的改动整片覆盖了,而且谁都不知道发生了什么。
所以四个设计:做成可直接编辑的二维网格、支持框选批量操作、行列双向虚拟渲染、提交时检测并发冲突并展示差异。
本地暂存带来了「未保存状态」这个新的复杂度。页面刷新、意外关闭、切换筛选条件都可能丢掉未保存的改动。处理办法:离开前提示、脏值写入本地存储可恢复、切换筛选条件时先提示保存。这个复杂度是为了「一次改一片」的体验必须付的代价,但要承认它引入了新的出错可能。
为什么不用现成的表格组件。试过几个,问题是都不支持「区域框选 + 批量赋值」这种类 Excel 的交互,而这正是核心需求。而且大部分表格组件的虚拟滚动只做纵向。改造现成组件的成本超过自绘网格。但如果需求只是「大数据量只读展示」,我一定用现成的——判断依据是核心交互是否被覆盖。
冲突处理选「拒绝并展示差异」而不是「合并」。自动合并(比如按字段级合并,我改价格他改库存就都保留)听起来更好,但房态的字段之间是有业务关联的(关房了库存还有意义吗),自动合并可能产生业务上不合理的组合。让人看到差异后决定,比机器猜更安全。代价是运营多一步操作。
踩过的坑:框选时页面卡死。第一版在 mousemove 里直接更新选区并触发整个网格的重渲染,拖过一百个格子就是一百次全量渲染,鼠标拖到一半页面就没响应了。修法两个:选区状态只驱动 class 不动数据;mousemove 用节流并配合 requestAnimationFrame。「高频事件里做重活」是前端性能问题最常见的来源。
踩过的坑二:冻结列和主体的行高对不齐。某些房型名称太长换行了,冻结列那一行变高,而主体那一行没变,整张表从那一行开始全部错位。修法是强制单行显示 + 超出省略 + 悬浮查看全名,从源头消除不定高。这和之前做长列表时的判断一致:用设计约束换实现简单。
没做的部分:没做复制粘贴(选一片格子复制,粘到另一片)。运营提过这个需求,类 Excel 的剪贴板交互实现复杂度不低(要处理系统剪贴板格式、跨浏览器差异),当时优先级排在后面。
网格首屏渲染耗时:用 Performance 面板量「数据到手 → 网格可交互」。20 房型 × 90 天约 180ms。必须说明这里的关键是虚拟渲染后实际只画了多少格子——1800 个逻辑单元格,实际渲染的可能只有 200 个左右(可视区 + 缓冲),这才是 180ms 的原因。报「逻辑规模」和「实际渲染量」两个数字,比只报耗时更能说明设计。另外要报测试机型。
框选流畅度:用 Performance 录制拖拽过程,看有没有长任务和掉帧。改造前拖动百格就无响应,改造后拖动全屏保持流畅——这类交互性能用「有无长任务」描述比给一个帧率数字更实在。
提交数据量:抓包对比。全量提交是 1800 条,差异提交通常几十条。这是个结构性的对比,比耗时数字更能说明问题。
「冲突被拦下」怎么验证:构造场景——两个浏览器窗口同时打开同一房型,A 改价格保存,B 也改同一格保存,验证 B 收到冲突提示并能看到 A 的值。这类并发正确性用确定性场景验证,不要用比率。
不要报「运营效率提升 N 倍」。这个数字算不出来(没有基线、任务量不可比)。可以报的是可核查的事实:改造前运营需要找开发提数据库脚本(这件事本身就是最有力的证据),改造后可自助完成;单次批量能操作的单元格数量。
transform: translate(x, y) 偏移到正确位置。前提是行高列宽固定,所以我强制房型名单行省略显示,从源头消除不定高——不定尺寸的双向虚拟化复杂度会高一个量级(要维护两个方向的尺寸累积表并支持反查)。冻结行列各自独立渲染并同步偏移量,这里的坑是尺寸必须严格对齐,我踩过房型名换行导致整表错位的坑。
mousemove 里更新选区并重渲染整个网格,拖过一百格就无响应了。正确做法:选区只用起点和终点两个坐标表示(不是一个包含所有格子的数组),格子的选中样式由「我的坐标是否落在选区矩形内」计算得出,只驱动 class 变化不改数据;mousemove 做节流并配合 requestAnimationFrame 更新;松开鼠标才把选区展开成具体的单元格集合去做批量操作。更通用的教训是:高频事件(mousemove、scroll、input)里做重活是前端性能问题最常见的来源,处理原则是「事件里只记状态,渲染交给帧回调」。
where version = 加载时的版本);影响行数为 0 说明别人改过了,返回冲突清单。前端拿到清单后展示「你的值 / 他人的值 / 修改人 / 修改时间」,让运营选择用谁的、或者逐格决定。为什么不自动合并:房态的字段有业务关联(关房了库存还有意义吗),按字段自动合并可能产生业务上不合理的组合,让人看到差异后决定更安全。为什么不用悲观锁:锁住整个房型会让另一个运营完全无法工作,而房态维护是长时间的操作,锁的持有时间不可控。
beforeunload 加路由守卫,覆盖关标签页和站内跳转两种情况);脏值实时写入本地存储,重新进入时检测到有未保存改动就提示「上次有 N 处未保存的修改,是否恢复」;切换筛选条件(换酒店、换日期范围)时先提示保存,因为切换会重新加载网格。本地存储的脏值要带上下文(哪家酒店、哪个日期范围、什么时候暂存的),否则恢复时可能把 A 酒店的改动应用到 B 酒店上。还要有过期清理,避免很久以前的脏值被误恢复——那时候的房态版本早就变了,恢复后提交必然全部冲突。
网格框选批量已经解决了「一次改一片」的问题,但运营的真实诉求更结构化:「未来三个月所有周五周六涨 15%,但国庆那一周不动」。用框选来做这件事要框十几次,而且容易漏。
另外两个问题在网格方案里暴露出来。
一是改错了没法撤。框选批量一次改几百格,运营发现框错了区域,只能再框一次改回去,而且改回去的原值已经不知道了。这是真实发生过的事故——某次误把两个月的价格全设成了一个数字,靠数据库备份恢复。
二是看不到会改成什么样。点「应用」之前完全不知道结果,尤其是相对调价(涨 15%)——每一天的原价不同,涨完各是多少心里没数。运营的做法是先改一小片试试看,这本身就说明缺少预览。
所以三个设计:规则式表达调价意图(周期性规则用规则描述,比框选精确)、应用前试算预览改动前后对比、每次批量生成变更批次并可整批回滚。
规则式和框选式并存,不是替代关系。规则式适合结构化的周期性调价(周末涨价、淡季降价),框选式适合零散的个别调整(这三天有活动单独定价)。硬要用一种覆盖全部场景会让那种方式变得难用——规则式表达「这三个不连续的日期」很别扭,框选式表达「未来三个月所有周五」要框十几次。两种都保留,让运营按场景选。
提交具体值而不是规则,代价是数据量大。一次批量可能提交几百条明细。但换来的是「预览即所得」这个确定性,而且服务端逻辑简单(只是批量更新,不需要实现一套规则引擎)。如果规则由服务端执行,还会引入一个新问题:规则是在什么时刻执行的?如果是异步执行,那期间价格被别人改了怎么办?提交具体值 + 版本号条件更新把这些问题都消掉了。
踩过的坑:误改两个月价格,靠数据库备份恢复。运营本想改一周,操作时日期区间选错,把两个月的价格全设成了同一个数字,而当时没有变更记录也没有回滚功能,只能从数据库备份里捞,中间有几个小时线上价格是错的。这个事故直接推动了变更批次和回滚的设计。教训是:任何能一次性影响大量数据的功能,必须配套「预览 + 记录 + 回滚」三件套,缺一个就是事故等着发生。
踩过的坑二:预览和实际结果不一致。早期是提交规则让服务端算,前端预览用的是自己的取整逻辑(四舍五入),服务端用的是向下取整,结果每个价格都差一块钱,运营发现后完全不信任预览功能了。修法就是改成提交试算结果。「预览必须和执行走同一套计算」是所有带预览功能的通用要求——要么共用同一份代码,要么预览的结果直接就是要提交的内容。
没做的部分:没做定时生效的调价(提前配置好,到某个时间自动应用)。运营提过「双十一零点自动涨价」,需要定时任务和冲突处理(到点时价格已被别人改了怎么办),当时没做,用的是运营到点手动点。
试算耗时:在试算函数前后打时间戳,覆盖数百个单元格的规则试算约 40ms。这个数字要报,因为它决定了预览能不能做成「改规则实时刷新预览」——40ms 可以做实时,如果是几百毫秒就只能做成点按钮才算。
单次覆盖量:能说出「一次规则调价可覆盖数百个单元格」,并且对比框选方式需要操作多少次才能达到同样效果(比如「未来三个月所有周五周六」用框选要框 26 次)。这个对比比抽象的效率提升百分比有说服力。
「误改由预览与回滚兜住」怎么验证:这类能力靠演练而不是数字。做法是模拟一次误操作:故意选错日期区间应用,然后走回滚流程,验证价格完全恢复、审计记录完整、回滚本身也留了痕。还要测冲突场景——回滚时某个格子已被他人改过,验证有提示而不是静默覆盖。
不要报「改价效率提升 N 倍」或「零误操作」。前者算不出来,后者是绝对化断言(误操作一定会发生,我们做的是让它可恢复)。正确的表述是「误操作可在分钟级完整回滚,且回滚过程可审计」——这才是这个模块的真实价值。
这个模块的起因不是产品需求,是开发被拖垮了。旅游业务对接了多家供应商,每天都有对接类问题:「这个订单为什么确认失败」「为什么这家酒店查不到房」「对方说他们返回成功了但我们显示失败」。
这些问题只有开发能查,因为要去日志系统里翻报文。流程是:对接人员在群里问 → 开发登录日志平台 → 按订单号搜 → 找到请求和响应 → 截图发群里。一次半小时,一天几次,而且开发的时间全耗在这上面。
更糟的是信息不对称导致的扯皮。对接人员和供应商沟通时说不清细节,只能转述开发的话,来回几轮。而供应商说「我们返回的是成功」,我们说「我们收到的是失败」,没有报文摆在桌面上就无法定论。
第三个问题是问题发现太晚。某家供应商接口挂了几个小时,是运营发现「这家酒店都搜不到了」才报上来。没有健康看板,故障靠人肉发现。
所以三个设计:供应商健康看板让故障可被主动发现、报文留痕可自助查询让对接人员自己定位、结构化差异比对让扯皮有据可依。
报文留痕的存储成本是明确代价。每次供应商调用都存请求和响应,量很大。控制手段:只存关键接口(查价、下单、取消,不存心跳类)、超大报文截断并标注、压缩存储、分级保留(失败的报文保留更久,成功的可以更早清理)。「失败的留久一点」这个分级很实用——排障需要的几乎都是失败的。
把排障能力交给非开发人员,需要接受他们会看到系统内部细节。报文里有我们的请求构造方式、字段映射逻辑的痕迹。这个信息暴露是可接受的(对接人员本来就需要理解对接逻辑),但密钥这类必须脱敏。取舍是「让业务方自助」的价值 > 「内部细节不外露」的洁癖,前提是敏感数据管住。
踩过的坑:报文里的密钥没脱敏,截图发给了供应商。对接人员排障时把三栏对照截图发到了和供应商的沟通群里,截图里包含了我们调用另一家供应商的鉴权头。发现后紧急轮换了密钥。修法是密钥类字段在展示层就替换成掩码(服务端返回时就脱敏,不是前端遮挡),并且「一键复制」默认复制脱敏版。前端遮挡是不够的——打开开发者工具就能看到原值,脱敏必须在服务端做。
踩过的坑二:看板的指标是实时算的,页面一开就把数据库压慘了。成功率、耗时分位这些指标最初是每次打开页面就去聚合原始日志算,几个对接人员同时开着页面自动刷新,聚合查询把库拖慢了。修法是指标预聚合——定时任务按分钟粒度算好存起来,看板只读聚合结果。「看板类页面的指标必须预聚合」是通用经验,实时算在数据量上来后必然出问题。
没做的部分:没做报文重放(用留痕的请求报文重新发一次给供应商,看现在是什么结果)。这对排查「当时失败现在正常」的问题很有用,但重放写接口有风险(可能真的下单成功),需要严格的环境隔离和权限控制,当时没做。
「定位时间从半天到分钟级」怎么来的:改造前的「半天到一天」是从工单记录里估的——从对接人员提问到开发给出报文截图的时间差(包含等待开发排期)。要说清这个时间包含等待,纯查询时间开发只需要几分钟,但要等他有空。改造后是对接人员自己查询的实际操作时长。这个对比的真实价值在于「消除了等待开发这个环节」,而不是查询本身变快了。
报文查询接口 P95:压测按订单号查询,约 300ms。要说明报文数据量级和存储方式(是从数据库查还是从对象存储捞、有没有索引),这决定了这个数字的可信度。
看板指标的时效性:要报出「指标是预聚合的,延迟约 N 分钟」。主动说明延迟比让人以为是实时的更好——如果对接人员以为是实时的,故障刚发生时看板还没反应,他会认为看板不准。
可以补充的实在指标:开发被打断的次数变化(从每天几次降到偶发)。这个数字虽然粗,但它是这个模块真正解决的问题,比查询耗时更能说明价值。
不要报「排障效率提升 N 倍」。没有可比基线。也不要报「问题定位率 100%」——有些问题报文里看不出来(对方系统内部逻辑问题),绝对化断言会被追问穿。
没有匹配的内容,换个关键词试试。
项目拆解 · 供应商房态后台(Vue)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据