平台工程

公屏下线只删了界面 · 删界面不等于关链路

入档:2026-08-18 来源:PB Arena(pb.tiaozhuxiansheng.com)内测反馈 16 条的全量收口会话,当日 13 个提交(8 个功能修复),代码 7 文件 +1359/−414,后端用例 15 → 21,四次部署上线 状态:已上线并线上验证;insight 1 为本次首次发现 ⚠️,其余为既有规律的新变体 前情:零作品的比赛_界面演完不等于链路存在_v1(同项目,2026-07-25)——本篇是那一篇的镜像面同族新变体(2026-08-19):核心律从「删界面不等于关链路」推广为「任何以『用户看不到』为前提的安全属性,只做在渲染层等于没做」。新实例:PB Arena 的「匿名投票」界面显示「作品 A/B/C」,但接口无条件下发 submissions[].userId,同一响应体里的 players[] 又是 id→昵称对照表,打开 DevTools 就能把作品对上人。通用判据:绕过界面直接打接口,看它还给不给。 修法上有个通用形状——前端要那个字段往往只是为了判断「这是不是我自己的」,换成一个布尔位就够,不必暴露全表身份。见 自检46条零驳回_同类中的异类才是缺陷藏身处_v1

事实记录(不可修改区)

一句话总结

下线一个功能要删到哪一层,由「下线动机」决定,不由「用户看不看得见」决定——动机是降噪,删界面就够了;动机是合规或安全,攻击面在 HTTP 层不在 DOM 层,必须 curl 一遍才算下线完。

可复用 insight

1. 下线动机决定下线深度:先写下「我在防谁」 ⚠️首次

拿到「把交流社区去掉」这个指令时,我按产品需求处理它:删导航项、删视图、删相关前端代码、删 CSS,并且刻意保留了服务端接口和历史数据,理由写在提交信息里——「便于回退」。这个判断在 UX 语境下完全正确。

真正的动机是三小时后才补上的:防止有人在公屏发布违规内容。动机一换,同一个「保留接口便于回退」立刻从优点变成漏洞:

而公屏恰恰是站内唯一「谁都能发、所有人都能看」的自由文本入口,且未接内容审核——它是这个站上暴露面最大的一处,第一版却只动了 DOM。

判据:下线前先把动机写成一句「我在防谁」。主语是「不小心的人」→ 删界面;主语是「有意的人」或「监管」→ 必须把这个功能的每一条 HTTP 路由 curl 一遍,未登录、已登录各试一次,看到 404 才算下线完。

这条是 零作品的比赛_界面演完不等于链路存在_v1镜像:那篇是界面全在而链路不存在(演得像有),这篇是界面没了而链路还在(演得像没有)。两篇合起来是同一个提醒——界面和链路是两套独立的事实,任何一方都不能替另一方作证

2. 反馈和代码之间隔着时间,「按字面修」可能把显示错变成静默失败 ⚠️首次变体

反馈写于 08-08,开工是 08-18,中间 20 个提交。不做审计直接按字面修的话:

判据:反馈日期与当前 HEAD 之间隔了几个提交?隔得越多,越要先做一遍「这条现在还成立吗」的审计再排期。审计的产出不是结论,是排期本身——它会改变你先做什么、以及某几条要不要做。

同族但轴不同:内测修复逐项审计闭环_v1 的轴是「清单里哪些已在未提交改动中实现」,本篇的轴是**「反馈与 HEAD 之间的时间漂移」**——前者防重复劳动,后者防按过期描述修出新问题。

3. 序号 / 标号类展示必须眼看一次输出,代码审查看不出来 ⚠️首次

结算页作品排名的每一行都印着「作品 @」,本该是 A/B/C。代码是:

const label = String.fromCharCode(65 + submissions.indexOf(work));

work 来自 submissions.map(s => ({...s, voteCount, authorName})) —— 展开副本不是原对象indexOf 恒返回 −1,charCode(64) 正好是 @

这个 bug 在代码里长得和正确代码一模一样indexOf 是合法调用、参数名对得上、没有任何静态检查会报。它只在渲染出来那一刻现形。它也不在反馈清单里——是我为了验收「并列点名」去截图时顺眼看见的。

判据:凡是「下标 / 序号 / 字母标号 / 排名」类的展示,验收时必须眼看一次真实输出。这类值错了不会抛错、不会变红,只会安静地印错一个字符。

4. 改默认值会把冷门路径变成热门路径,坑要一起填 ⚠️首次

把「冠军积分」新建默认值从 300 改成 1,是一行改动、零风险。但它改变了运营的下一步行为:默认值是 1 之后,想表达「本场不发积分」的第一反应就是把它填成 0。

而填 0 撞上一个存量 bug:Math.max(0, Number(updates.reward ?? current.reward_points) || current.reward_points) —— || 把合法的 0 判成假值,静默落回旧值。改成 0 存不进去,还不报错。这个 bug 在默认值是 300 的年代几乎没人踩(谁会把 300 改成 0),改完默认值它就从冷门路径变成高频路径。

判据:改一个默认值 / 入口 / 排序之前,问一句「这个改动会让人更容易走到哪条以前很少走的路」,然后去看那条路上有没有坑。 一行改动的风险不在这一行,在它诱导出的新行为。

5. 反馈里的「矛盾」和「无主语」是待澄清项,不是待推断项 ⚠️首次

16 条里有三条不可直接执行:

我在「聊天」那条上做了推断并给出推荐:从他当时在主持赛事、界面投在大屏的场景出发,推断是比赛房间聊天,还论证了它与三天前刚做的 BUG-10(专为官方赛事补的 battle 维度聊天通道)冲突。推断错了——正确答案是交流社区,四处里暴露面最大、也是唯一未登录可读的那一处。

如果不问直接做,会撤掉三天前刚交付的功能,同时把真正该关的洞留着。

判据:反馈里出现「矛盾 / 无主语 / 无法定性」时,不要用「最可能」去补全。把全部候选连同各自代价列出来让提出者选——尤其当不同候选的代价差好几个数量级时(删一个聊天面板 vs 删一整个带历史数据的功能区)。

顺手教训

下次改进

关联文档

类型/平台工程