← 返回博客2026-08-24

为什么公平性证明不该放在手牌复盘页

我们删掉的不只是一个按钮,还纠正了一处容易误导玩家的页面设计:复盘页负责解释手牌,公平性页面负责验证发牌记录。更重要的是,我们用测试防止这项功能再次回到错误的位置。

问题不在按钮本身

表面上看,这次改动很简单:删掉手牌复盘页上的一个导出入口。事实没错,但真正值得讨论的是,这个入口为什么一开始就不该放在那里。

根本问题是两个页面的职责混在了一起。复盘页应该解释一手已经结束的牌;公平性验证页应该核验一份已经导出的记录。前者帮人看懂牌局,后者让人核对证据。把两件事塞进同一个页面,用户很容易把一项范围有限的技术证明,理解成对整手牌更宽泛的保证。

这篇文章对应的合并记录是 4664ff06fix/review-fairness-dup)。它让 client-game/src/views/ReviewView.vue 净减少 336 行,删除了复盘页的公平性导出测试,新增了一项专门检查页面职责的测试,并把完整的证明流程移到 client-game/src/views/tools/FairnessVerifyView.vue。背后的原则很简单:只有能提供证据的页面,才适合承载这项承诺。

别让复盘页替验证器作保证

玩家打开复盘页时,往往刚赢、刚输,或者仍在琢磨某个决定。这时放一个醒目的公平性标签,很容易造成误解:技术上只证明了一件事,玩家却可能以为它证明了一切。

修改前,复盘页的说明还把「公平性证据」列为页面内容。修改后,代码开头明确写着:没有开启高级复盘时,玩家只会看到手牌记录和行动过程(ReviewView.vue:9-10)。公平性证明没有被删掉,只是回到了能把适用范围讲清楚的页面。

这个区别很重要。标准的承诺—揭示(commit-reveal)证明,可以证明系统在发牌前已经对牌序作出承诺,发牌后也没有篡改;但它不能证明服务器从未看到牌。独立验证页在界面代码的开头就写明了这条边界:它只验证导出的 ADR-064 HandRecord JSON,不把标准牌桌描述成「服务器盲发牌」(FairnessVerifyView.vue:3-8)。

验证页要把流程做完整

把入口挪到另一个页面只是第一步。既然用户要专门去验证,那一页就应该提供完整流程,而不是只留一个让人粘贴 JSON 的文本框。

现在的验证页已经补齐了整条路径。登录用户可以从最近的手牌中选择,也可以手动输入手牌 ID;匿名用户则可以上传已经导出的证明文件(FairnessVerifyView.vue:60-68)。选中历史手牌后,页面会读取证明记录;下载的 JSON 文件名与 CLI 命令里的提示一致;用户也可以通过 /api/tools/poker/fairness/verify 直接在浏览器中完成快速验证。

对应的后端接口也保持公开、无状态。代码注释明确说明,它只是对 pf_verify 所用承诺—揭示验证器的一层包装,只接收用户从已结束手牌中导出的 JSON 证明(server/src/handlers/poker_tools.rs:142-153)。这不是文案上的修饰,而是功能本身的边界。

不仅要测「存在」,也要测「不存在」

这次改动里最重要的测试,不是「公平性验证页能不能用」,而是「复盘页会不会再次把自己包装成公平性验证页」。

ReviewView-information-architecture.test.ts 挂载复盘界面后,会检查三项内容确实不存在:export-fairness 区块、通用导出按钮,以及「可验证公平证明」相关文案。测试还会确认教练入口仍然保留,确保清理错误入口时,没有连带删掉原本该留在复盘页的功能(ReviewView-information-architecture.test.ts:104-123)。

验证页则有自己的正向测试:最近手牌可以提供手牌 ID;读取最近手牌的请求返回 401 时,匿名用户仍会留在验证页,并且可以上传已有的证明文件;engine-blind 手牌不会出现在承诺—揭示验证器的最近手牌列表中,因为它使用另一套证明机制;下载文件名也必须与 CLI 提示一致(FairnessVerifyView.test.ts:66-155184-215)。

一套可以直接照用的检查方法

任何涉及信任承诺的产品,都可以用下面四个问题检查:

  1. 这份证据到底是什么?是承诺—揭示记录、审计日志、带签名的记录、可重复验证的回放,还是别的东西?
  2. 它不能证明什么?如果页面没法把这句话直接说清楚,承诺的范围多半已经太大。
  3. 承诺是否紧挨着验证动作?与其在玩家情绪最强烈的页面放一个醒目标记,不如提供一个朴素但完整的验证器:有输入、有输出,也有明确的失败状态。
  4. 有没有测试它不该出现的位置?最常见的回归,不是验证器坏了,而是那句承诺又悄悄回到了错误的页面。

不管是开发者还是编码 agent,都很容易漏掉最后一点。大家通常会为新页面补测试,却忘了锁住旧页面。等下一次整理文案时,一句「看起来很有帮助」的说明就可能把旧问题带回来。针对「不该出现」写测试,才能把产品承诺的边界长期守住。

回顾

  1. 问题:复盘页提供了公平性证明入口,让一项范围有限的证明看起来像对整手牌的全面保证。
  2. 调整:复盘页只负责手牌记录和行动过程;/fairness 负责读取、验证和下载承诺—揭示证明,并提供 CLI 命令。
  3. 防回归:两边都要测试——验证页确实能用,复盘页也确实不再出现那项承诺。
  4. 结论:只在能够出示证据的地方作出承诺,其他页面则用测试守住边界。