TT Lab
博客

한국어 English 日本語 中文

开始
博客

博客

比赛记录与个人隐私:结束的比赛留在了哪里,什么没有留下

状态:草案(2026-10-26),代码依据。 动作基于 2026-10-25 的仓库代码与设计文档读取。生产数据库中的「修正记录」条数读取自 2026-10-18。未登载用户数等运营统计。 编写规则:实测数据为基础,无法测量的标为「未实测」,未找到依据的标为「无法确认」。

目录

1. 比赛结束后

一场五局三胜制的比赛以 3:1 结束。相机记分牌上显示「比赛结束」界面,局分(11:7, 9:11, 11:5, 13:11)并排显示。这场比赛现在在哪里?

只在手机里。 正如在第 2 篇中所见,相机记分牌将整个比赛历史保存在浏览器的 localStorage(ttplay:cam)中。历史记录的每个条目都有对应的来源:点击屏幕的分数(tap)、遥控器按键(key)、分数调整(adj)、接受自动判定的分数(auto)。服务器并不知道这场比赛。

在这种状态下,向服务器发送数据的途径只有用户选择的三种:

途径 发送到服务器的内容 保留位置
记录这场比赛 局分、比赛时长、对手 比赛记录、评级
直播分享(比赛中) 得分·撤销·结束事件 直播比赛与事件列
自动分数(比赛中) 帧(不保存)、人工对芯片的回答记录 修正记录(无视频)
경기 끝 이력 전체는 휴대폰 localStorage 에만 「이 경기 기록하기」 게임 점수만 넘김 · 손으로 올린 게임은 뺌 Google 로그인 · 표시 이름 구글 실명은 어디에도 안 나감 상대 고르고 「기록하기」 하루 30경기 · 같은 상대 10경기 계정 있는 상대: 확인 대기 상대가 「확인」을 눌러야 함 계정 없는 상대 개인 기록만 7일 안 확인 이의 7일 지남 확인됨 Elo 반영 K 40 · 24 · 16 이의 레이팅 안 움직임 개인 기록 레이팅에 안 들어감 상대 이름은 적은 사람만 순위표: 공개를 켠 사람 중 확인된 경기 3개 이상 프로필은 비공개가 기본 · 레이팅은 1500 에서 시작
图:已结束的比赛如何成为记录与评级

2. 「记录这场比赛」

什么被传输了

点击「比赛结束」弹窗中的「记录这场比赛」(backend/static/tt/play/lib/cam-sheets.mjs)时,界面会将比赛摘要 {best_of, games, source:'scorekeeper'} 暂存于 sessionStorage 中,并跳转至「我的记录」界面(/tt/me#match)。如果尚未登录,则会先进行登录。在记录界面选择对手并点击「记录」后,数据才会发送到服务器(POST /api/tt/matches).

传输的仅为 局分。不包含每球历史、谁在何时如何得分、以及自动判定的痕迹。某些情况下无法进行记录:

登录与显示名称

编写记录需要 Google 登录。访客仅具阅读权限。写入权限如同站点社区的门槛。

这里有一个重要的设计: 谷歌账号的名称绝不会被导出。 谷歌姓名通常为真实姓名,如果直接显示在比赛对手或排行榜上,乒乓球馆里遇到的人就会知道我的真名。因此,乒乓球区域的任何响应或界面都不会使用谷歌姓名(backend/app/tt_records.py 导言「谷歌姓名绝不导出」)。在设定显示名称之前,选手被称为「乒乓球手 #编号」,若要记录比赛,必须先设定显示名称(2–20 个字符)。因为如果对手界面上只显示编号,将无法认出是谁记录的。

相机记分牌的昵称栏(最多 12 个字符)也附有「用昵称代替真名」的指引。

对手确认后评级才会变动

如果仅凭自己记录的胜场就能提升评级,排行榜就会变成比拼谁记录得更多的表格。因此,与拥有账号的对手之间的比赛会进入 等待确认状态,只有当对手点击「确认」后才会变为 已确认,此时评级才会发生一次变动。对手也可以发起「异议」。 如果在 7 天内未获确认 ,则不会计入评级,仅保留为个人记录。过期判定并非通过批处理作业修改行数据,而是在读取时根据生成的时间来判定(由单个数据库时钟衡量)。

也可以记录与无账号对手(社区朋友)的比赛。此时对手的姓名 仅对记录该比赛的人可见 ,且评级不会变动。

Elo 评级

评级采用源自国际象棋的 Elo 体系。

# backend/app/tt_records.py 에서 줄임
def k_factor(rating, rated_games):
    if rated_games < 10:   return 40      # 처음 10경기: 제자리를 빨리 찾게
    if rating >= 2000:     return 16
    return 24

def expected(ra, rb):                     # A 가 이길 기대값
    return 1 / (1 + 10 ** ((rb - ra) / 400))

# 변화량 = K × (실제 결과 − 기대값). 두 사람이 각자 자기 K 를 쓴다.

例如,若两人均为 1500 且在前 10 场比赛内,则期望值为 0.5,获胜者为 40 × 0.5 = +20,失败者为 −20。在相同的 K 40 下,若 1600 战胜 1500,由于期望值约为 0.64,因此仅上升约 +14.4,若落败则下降约 −25.6。

每日上限与并发

上限 数值 原因
每日比赛记录 30 防止集中录入
与同一对手每日 10 延迟相互刷分提升评级的行为
每日练习记录 20 —
比赛日期 一年以内 缩小集中补录旧记录以刷评级的途径
单局分数上限 50 过滤输入错误(如 110:9)

上限在数据库中进行统计。为防止同一人的两个请求被同时计算为「第 29 个」并通过,会针对每个人使用数据库锁(advisory lock)进行排队。确认时,会先锁定比赛行,再按照 编号较小的一方 开始锁定选手行。因为如果爱丽丝和鲍勃同时确认彼此的不同比赛,且都「从自己的行开始」锁定,就会导致双方互相等待对方的行而陷入死锁。

3. 记录界面

界面 显示内容 谁可见
我的记录 /tt/me 我的比赛、等待确认·异议、评级、练习日志 本人
排行榜 /tt/rank 公开选手排名、俱乐部·本月筛选、本周飙升榜 所有人
历史 /tt/me/history 汇集申请·比赛·分数的变动,CSV 下载 本人
比赛分享 /tt/m/{코드} 开启分享的单场比赛 收到链接的人

历史不会积累新记录,而是读取并汇集各功能已经留下的内容(backend/app/tt_history.py)。比赛记录、直播记分牌事件(由服务器重新计算)、录像分析结果、自动分数修正记录以及评级变动会混合显示在同一行中。由于直播记分牌和比赛记录彼此没有通过密钥相连,因此会通过「结束分数相同且时间相近」的条件进行 推测性 连接。由于存在连错的可能,界面通常会同时显示流的来源(直播·视频)。地址中没有人员编号,且所有查询仅绑定至登录用户,因此无法查看其他人的历史记录。CSV 导出时会防止 Excel 公式注入。

比赛分享不使用比赛编号,而是使用单独生成的 11 位代码。因为比赛编号是顺序递增的,如果在地址中使用,别人就可以逐个浏览所有的比赛。已确认的比赛双方选手均可开启分享,个人记录仅限记录者开启分享。处于等待确认或异议状态的比赛,由于连「是否存在这场比赛」尚未确定,因此无法分享。在分享界面中,未公开的选手显示为「乒乓球手」,无账号的对手姓名则始终不会显示。

4. 直播分享:服务器重新计分

如果在比赛中开启「直播分享」,收到观赛链接的人就可以实时查看比分(backend/app/tt_watch.py).

名称规则同样如此。链接由记录者亲自发放,因此会显示为该场比赛设定的昵称,但在任何人可见的 主页动态中,未开启公开的主播会显示为「乒乓球手」,对手则显示为「对方选手」。

5. 修正记录:仅限同意者,且目前为 0 条

在第 3 篇中提到,当人工对自动判定的芯片做出回答(「正确」、修改为其他选手、「无效」)时,该回答会成为 修正记录。我们来追踪这些数据流向了何处。

包含什么

自动分数界面会为人工回答的每个回合创建一个行数据并发送至 POST /api/tt/corrections 。行数据包含以下内容(backend/app/tt_corrections.py).

不包含视频。 服务器不会原封不动地存储浏览器发送的内容,而是会重新构建已知的结构。几乎没有可输入文本的地方,因此不会积聚脏话、地址或脚本。行记录的键中包含登录人员的编号,因此无法覆盖他人的行。限制为每人每分钟 30 个请求、每天 2,000 行。

随着时间流逝而自动写入的判定不会留下修改记录。因为这不是由人工确认的。

谁拿走了什么

至今从未用于训练

在 2026-10-18 读取运营数据库时,修改记录为 0 条。因此, 至今还从未将修改记录用于学习或评估。 第 5 篇中的训练模型全部通过公开数据和合成图像进行学习。

我们确实有这个计划。正如第 5 篇所述,当使用可商业化使用的资料重新训练学习型模块时,同意将数据用于学习的人所修改的判定可以成为诸如“需要确认”等校准模块的素材。因为“人工修改的判定”即是“引擎出错位置”的标准答案。不过,这需要积累足够的修改记录,并且首先需要重新检查作为学习素材时的保存期限以及同意说明。

6. 视频不出设备,以及例外情况

摄像头计分板的首要原则是 摄像头视频不会离开设备(backend/static/tt/play/cam.js 前言)。录制也是在手机内部进行合成并保存在手机上。读出比分则使用浏览器的语音合成功能。

例外情况是用户开启的两项功能。

自动计分。 由于寻找球的 GPU 位于服务器上,因此会发送帧。不过我们缩小了范围(第 1 篇)。

上传录像进行分析。 这是用户自行选择并上传视频文件的独立功能。浏览器会先将其压缩至 360p60 500kbps 后上传,原件将在 30 天后删除。分析操作员在没有数据库和存储密钥的情况下,仅通过网页内部 API 进行工作。

7. 以昵称代替真名

如果将个人信息设计精简为一句话,那就是 “不让乒乓球馆遇到的人知道我的真名” 。

位置 显示名称
摄像头计分板界面 该比赛设定的昵称(12个字符,“以昵称代替真名”说明)
比赛记录·对手界面 显示名称(2–20个字符)。设定前为“乒乓球爱好者 #编号”
排行榜·公开个人资料 仅限开启公开的用户。默认不公开
无账号的对手 仅对记录该场比赛的人可见
直播主页动态 未公开的主办方显示为“乒乓球爱好者”,对手显示为“对方选手”
修改记录导出 无名,每次导出时都会改变的掩码值
谷歌账号名称 不会输出到任何地方

演示视频中也遵循相同的原则。虽然保留了原画面中嵌入的名称(直播字幕),但我们后绘制的文字仅统一使用“选手 A·B”。

8. 服务器上有哪些表

将与记录相关的服务器端表汇总在一起,结构如下。每个表都附带了数据库迁移(migration) 编号,每一次迁移都引入了一项功能。

迁移 表 包含内容 不包含内容
0036 tt_players 显示名称(2–20 个字符,为空时显示“乒乓球爱好者 #编号”)、Rating、计入 Rating 的比赛场数、公开状态(默认不公开) 谷歌名称
0036 tt_matches 比赛时长、局比分、来源(直接输入·计分助手·录像分析)、状态(等待确认·已确认·异议·超期·个人记录)、双方的 Rating 变化量 各相持(rally)历史
0036 tt_practice 练习日志(仅本人可见) —
0039 tt_live_matches·tt_live_events 直播比赛与事件列(带编号的得分·撤销·结束)。在同一迁移中,将用于比赛分享的 11 位代码 tt_matches 添加到 中 比分本身(服务器从事件中重新计算)、视频
0040 tt_corrections 原始判定·修改判定·事件摘要·依据·是否同意 视频
0048 tt_videos 用户上传视频的分析队列与结果 —(原件在 30 天后删除)

这些表中值得注意的一点是, “不包含什么”占了设计的一半。比赛记录不包含相持,直播不保存比分而只保存事件,修改记录不包含视频。各个表只包含其功能绝对必要的内容,历史记录界面不创建新表,而是读取并汇总这些表(第 3 节)。没有包含的内容自然也就无法泄露。

9. 总结

乒乓球寄语。 今天的比赛由手机优先记住。只需告诉服务器“比分是多少”,名字也只需使用昵称。因为需要对手点击“没错”才能增加积分,所以赢球的那天请务必请求确认。

参考文献

  1. A. E. Elo. The Rating of Chessplayers, Past and Present. Arco, 1978. (Elo Rating 的原始文献。本次未公开原文,我们的计算方式可在代码 backend/app/tt_records.py 中确认)
  2. International Table Tennis Federation. ITTF Statutes 2026, Chapter 2: The Laws of Table Tennis, §2.12(比赛与局数)。 https://db.ittf.com/document
  3. 代码与设计:仓库 backend/app/tt_records.py·tt_history.py·tt_watch.py·tt_corrections.py·tt_live.py 前言, backend/static/tt/play/cam.js·lib/cam-sheets.mjs·lib/camauto.mjs.

延伸阅读

登录后即可点赞

评论

还没有评论。

登录后即可发表评论