赛事状态机到点不切换 · 写入方不等于推进方
入档: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:先问「谁在到点改它」,再问「到点该不该发生这件事」,最后问「这功能跑完数据库该多出什么」。
关联文档
- ⚠️ 零作品的比赛_界面演完不等于链路存在_v1 —— 同项目直接续篇,部分修正本篇:自动推进修对了技术、选错了触发源;并发现整条参赛链路(提交/投票)压根不存在
- ⚠️ 全站文字截断体检_检测工具本身要先被证伪_v1 —— 本篇 insight 3「决定性的一步是量,不是猜」的后半句:量的方法本身也可能是错的——自制判据里一个拍脑袋的常数把 8 个正常元素判成缺陷,还据此改了一处没坏的代码;量之前先用已知正常/已知损坏的样本把工具两端标定
- 内测反馈渠道上线_匿名兜底与422探针验证_v1 —— 同项目上一会话:反馈渠道本身是这次拿到问题的入口;本篇是渠道建起来之后的首轮处理
- ⚠️ reduced-motion本机陷阱_动效降级纯淡入而非跳过_v1 —— 同族「环境差异导致只有部分人复现」:那篇是本机偏好开关让作者自测毫无变化,本篇是用户侧字号/缩放先到临界点;共同解法都是先量再判,别先归因于”他那台机器的问题”
- 站内新版块五件套模式_三次复用与起刊两坑_v1 —— 同族状态坑的另一形态:那篇是 enum 扩容时取反式过滤静默吞掉新状态(有推进方但过滤错),本篇是压根没有推进方
- ⭐ 静态站接账号系统_内容归git状态归库的双源切分_v1 —— 同族状态机的正面样板:四个状态各有明确推进方(发布方定人 / 承接人交付 / 发布方标打款),状态变更走唯一入口且强制同时写一条流转记录——本篇的病根(有人写、有人显示,就是没人推进)在那套设计里没有生长空间
- ⚠️ 云端定时内容生产连环坑复盘_全绿不等于已发_v1 —— 同一验证纪律的时间维度版本:「全绿≠已发」对应本篇「能手动切≠会自动切」,都要求独立核查真实终态
- 交付前实测证伪律_v1 —— 交付前写最小探针证伪;本次「不发任何 HTTP 请求、35 秒后直接读 sqlite」就是给自动推进设的探针
- ⚠️ 美工改稿全站落地_跨稿重复才是规范_v1 —— 同项目次日续篇(本篇修完即是那次会话的时间起点):其 insight 3「派生逻辑只挂在一条取数路径上」与本篇「写入方 ≠ 推进方」同族异形——都是被”你正好在看的那条路径”骗了;并把本篇「改 CSS 要同步改
?v=」扩展成版本号要覆盖会一起变的那一组文件(那次连app.js的 DOM 结构一起改了,而 app.js 压根没有版本号) - ⭐⭐ 成稿链接存进库没人读_写入方不等于展示方_v1 —— 同族第三形态(蛛网之上,2026-07-30):本篇是「有人写、有人显示,没人推进」,那篇是「有人写、有库存、接口也返,没人读」——同一条链上换了一环缺失;那篇也把本篇夸过的「正面样板」(双源切分那套状态机)找出了渲染侧的缺口
- 复盘事实先行原则 —— 本篇顶部的事实冻结区按它执行
- 09_平台工程索引 —— 平台工程区入口