发布于 更新于 约 8 分钟读完 · 2,984 字

本文目录 11 节
  1. 事实记录(不可修改区)
  2. 一句话总结
  3. 四条可复用 insight
  4. 1. 写入方 ≠ 推进方 ⚠️首次
  5. 2. 并列名词要逐项可达,遍历不是控制 ⚠️首次
  6. 3. 只有一个用户复现的排版问题,通常全员都在临界点内侧 ⚠️首次
  7. 4. 状态机上线会追认存量数据,先查再部署 ⚠️首次
  8. 顺手教训
  9. 下次改进
  10. 后续修正(2026-07-25 同日续篇)
  11. 关联文档

赛事状态机到点不切换 · 写入方不等于推进方

入档:2026-07-25 来源:PB Arena(pb.tiaozhuxiansheng.com)第 1 次内测后的修复会话,3 个功能提交 + 1 篇文档,已部署上线并封版 v2026.07.25(pb-arena 仓库 20c7d0f / c5447fb / 2e9ddba) 状态:已上线并线上验证;四条 insight 均为本次首次发现 ⚠️,尚未跨项目二次验证

事实记录(不可修改区)

  • 项目:PB Arena 官方赛事(报名中 → 进行中 → 出图 / 上传 / 投票 → 完赛)
  • 反馈来源:① 微信,同事两条「后台要加控制比赛进程的功能」「控制出图 上传 投票」;② 线上 feedback 表 3 条(状态显示有点丑 / UI 出图中页面 / 无提示音);③ 用户截图,顶部直播胶囊文字压住主导航
  • 预期目标:赛事到点自动开赛并自动切换阶段;运营能手动干预
  • 实际结果:符合。到点自动开赛、三阶段自动流转、跨轮、自动完赛全部跑通;后端测试 8/8;线上部署后自证生效
  • 关键数据来源:本地便携 Node 跑 node --test;浏览器实测后台面板;线上直接读 sqlite 与 /api/official-events
  • 客观事实(线上,不加解释):部署后一场 20:19 开赛、状态仍是「报名中」的赛事,在无人访问的情况下被自动推进到「已结束」,current_round 从 1 走到 4,4 个轮次全部标记 finished
  • 内测当晚的真实处置:14 人那场赛事是运营手动点「快速结束」收的场——阶段不动只能靠手动兜底

一句话总结

状态字段有人写、有人显示,不等于有人推进它——时间驱动的功能必须存在一个「谁在到点时改这个字段」的答案,验收标准也要写成「不碰任何按钮,等它自己变」。

四条可复用 insight

1. 写入方 ≠ 推进方 ⚠️首次

official_events.phase_ends_at 一直被正确写入(建赛时算好)、被前端正确读取(倒计时一秒一跳),唯独没有任何代码在它到期时推进状态。于是所有表象都齐了:后台看得到「当前阶段」,用户看得到倒计时归零——只是永远不会翻页。

这类缺陷天然抗发现:字段在、UI 在、倒计时在,grep phase_ends_at 也一堆命中;缺的是”消费者”。定位方式不是搜字段名,而是问「谁在到点时改它」并要求指到具体行,答不出来就是没有。

验收口径要跟着改:手动点按钮能切阶段,证明的是”切换逻辑存在”,不是”自动切换存在”。时间驱动功能的验收必须是不碰任何按钮,等它自己变——这次线上那场无人访问却自动完赛的赛事,才是真正的验收证据。

2. 并列名词要逐项可达,遍历不是控制 ⚠️首次

同事的原话是「控制出图 上传 投票」。我第一版给了「跳到下一阶段」——顺序推进,三个阶段确实都能到达,看起来满足了。但比赛已进投票、发现有人没传完想退回上传时,做不到。

需求里并列列举的名词,默认要求的是逐项直达(随机访问),不是按顺序遍历。 遍历只能满足”最终能到”,直达才满足”现在就要到”。第二版补了阶段下拉框 +「切到该阶段」才算真的做完。判据很简单:把用户列举的每一项当作一个入口,逐个问”能不能一步到”。

3. 只有一个用户复现的排版问题,通常全员都在临界点内侧 ⚠️首次

用户截图里胶囊文字换行、压住「封神榜」,其他人都正常。直觉结论是”她浏览器的问题”,实际不是:阶段文字缺 white-space: nowrap,而胶囊是写死的 height: 40px,只要这行字被挤到放不下就换行,换行就溢出。触发条件有四个,任一即可——浏览器设了最小字号(中文用户常设 12px,而这行是 10px)、页面缩放 125%、等宽字体加载失败回退到系统中文字体、窗口更窄。

决定性的一步是量,不是猜:本地默认配置下量到这行字实际宽 56px、单行需要 86px——所有人其实都已经在换行了,只是还没溢出到导航上。她不是例外,她只是先到临界点。

顺带一个反直觉的 CSS 坑:想让文字不换行时,别顺手加 overflow: hidden。它会把 flex / grid 项的自动最小尺寸压成 0,元素反而收缩到被省略号吃掉——本次中途就踩了一次,胶囊从 184px 缩到 104px,文字全没了。正解是只给”必须完整显示”的元素加 nowrap(不加 overflow),把省略让给可牺牲的那个元素(这里是比赛标题)。

4. 状态机上线会追认存量数据,先查再部署 ⚠️首次

新的推进逻辑一旦上线,会立刻按原定时间表处理所有存量记录——不只是以后新建的。线上那场开赛时间已过 56 分钟、4 轮共 48 分钟赛程早已走完的「报名中」赛事,部署后一路被推到「已结束」。

这个行为本身正确(赛程确实过去了),但它是一次静默的批量数据变更,不能由工程侧默认决定。上线前的动作:① 查存量里有多少条会被新逻辑立刻改写、分别会变成什么;② 备份数据库;③ 把处置选项交给运营拍板(本次给了「让它自然结束 / 先延时推到未来 / 删掉」三选一,运营选了第一个)。

顺手教训

  • 带版本号的静态资源,改文件必须同步改版本号:styles.css?v=20260724 这种引用,不动 v= 就等于改动到不了用户浏览器。本次差点漏掉——修完 CSS 在本地反复验证都不生效,才发现是自己在 bust 一个错误的 URL。改 CSS 的同时改版本号应当是同一个动作。
  • 改线上业务数据走后台接口,不走数据库:直连生产库跑 UPDATE 被权限校验拦下,拦得对——绕过接口会丢审计归属(handled_by_user_id 为空、操作流水无记录)。正解是把要写的内容整理好交给运营在后台贴,一分钟的事,记录还干净。
  • 借来的机器什么都没有:这次在朋友的 Windows 机器上协作,没装 Node(测试跑不了)、没有 git 身份(提交被拒)、没有 .ssh(部署连不上服务器,且系统自带 OpenSSH 9.5 与服务器 KEX 协商失败需显式指定 KexAlgorithms)。更要命的是本地已经领先远程 4 个提交却一直没推,封版时才发现——代码只存在于别人的机器和服务器解包目录里。换机器协作时,第一件事是 git status -sb 看 ahead/behind。

下次改进

  • 时间驱动功能的验收标准,一律写成「不碰任何按钮,等它自己变」再交付。
  • 收到并列列举式的需求(「控制 A B C」),先逐项确认”能不能一步到”,再动手。
  • 任何”只有某个用户复现”的表现,先量数值找临界点,再谈是不是环境特例。

后续修正(2026-07-25 同日续篇)

本篇的 insight 1「写入方 ≠ 推进方」成立,但默认了这个状态转移就是时间驱动的,这一点是错的。补完自动推进的当天,下一轮内测就反馈「比赛不应该创建后直接开始计时,应该在管理员发布每个题目后才开始」——到点自动进入出图,等于要求运营在开赛那一秒前就把题目写好,而真实流程是人到齐了现场公布题目。

更要命的是同一次反馈暴露:那场”验收通过”的比赛,从来没有人能提交作品(线上 battles / submissions / votes 三张表 0 行)。本篇的验收标准「不碰任何按钮等它自己变」只证明了阶段会流转,没证明这个阶段里有人能干活。

完整结论见 ⚠️ 零作品的比赛_界面演完不等于链路存在_v1:先问「谁在到点改它」,再问「到点该不该发生这件事」,最后问「这功能跑完数据库该多出什么」。

关联文档

类型/平台工程