
1. 为什么突然想写这么一套计分系统先说个背景。我经常跟几个朋友周末约线下棋牌局玩的是本地规则的长牌和麻将。人凑齐容易难点在于一晚上打四五个小时少说十几局谁赢谁输、赢多少、哪一局是关键转折点靠脑子根本记不过来。以前我们用纸笔记每局打完记个正负数结束以后拿计算器加总经常出现三个人加出三个结果的乌龙。后来用过几款现成的计分App要么规则不匹配要么没法导出场次数据复盘要么广告把计分页面挤得没法看。后来事情升级了。有朋友组织小区里的月度友谊赛十几个人分三桌打打完要把每桌结果汇总、算排名、算积分兑换小奖品。我临时拉了个表格凑合处理被数据校验折磨得够呛。那次以后我就下定决心自己动手写一套适用于室内牌类比赛的局内计分统计和局外结算复盘系统。这篇文章把整个项目的设计思路、数据模型、局内计分逻辑、局外结算规则以及复盘功能的实现过程完整记录下来。重点不是贴一堆代码而是讲清楚每一块为什么这样设计、踩了哪些坑、遇到边界情况怎么处理。如果你也在搞类似的东西——不管是自用计分工具、赛事管理系统还是想给朋友局做一个清爽的结算助手这篇都值得看一看。2. 先想清楚所谓局内和局外到底怎么划分很多人在做这类系统时第一步就搞混了概念。我花了两周时间梳理业务流程最后把整套系统拆成三个核心域牌局、场次、结算复盘。这三个词必须区分清楚。牌局Round指的是单一的一局比如一把麻将从发牌到胡牌的完整过程。场次Session指的一次完整的聚会或比赛可能包含多局。结算复盘Settlement Review是指场次结束后对整体成绩的分析。局内计分统计管的是牌局这一层。它的特点是高频、实时、局部。每打完一局系统要立即算出这一局各家的得分数、是否涉及特殊规则比如自摸加番、杠上开花、封顶等然后立刻汇总到本场次的累计榜上。局外结算复盘管的是场次这一层。它的特点是低频、全局、面向结果。一个晚上或一场比赛全部结束后系统要综合所有牌局数据做最终结算。这套划分看起来简单但它决定了整个系统的数据模型和接口设计。所有程序里的地雷几乎都埋在这两层之间的衔接上。比如局内每一局结束时的当前领先者和整场结算后的最终赢家经常不是同一个人——中间存在翻盘。如果你的数据模型没有把每一局的结果快照保存下来复盘时根本没法讲清楚翻盘是怎么发生的。2.1 业务流程才是真正的起点动手写代码前我先把完整业务流程画了一遍。以一场4人麻将友赛为例赛前建立场次录入选手名单4名或更多选手。赛中每局结束由记录员录入四位选手的本局得分正数代表赢负数代表输。局内统计系统实时更新每位选手的累计分、胜局数、本场排名。局外结算全部局数打完系统自动计算最终得分与名次按照预设的积分规则换算成赛事积分。复盘系统生成本场次的完整牌局流水、排名曲线、单局得失分布供选手和组办方回顾分析。关键点在第2步和第5步。第2步要求录入流程足够流畅否则大家打牌兴致正浓没人愿意在手机上慢慢戳。第5步要求数据结构足够细否则后面做复盘时什么都调不出来。这两点贯穿了整个设计过程。3. 数据模型设计先定四张表再加两个快照数据结构是整个项目的基石。这一节我把最终版本的表结构完整呈现并且说明每个关键字段的设计原因。3.1 场次表与参赛记录场次表存的是一次聚会/比赛的元信息场次ID、场次名称、举办日期、地点、总牌局数、状态进行中/已结束。规则集ID指向一套计分规则配置。备注字段用来记录特殊情况比如有人中途加入或退出。参赛记录单独建表不塞进场次表。为什么因为一场比赛的人数可能中途变化。我们遇到过下午场3人先打晚上又来1人变成4人局。如果参赛信息是场次表的一列这种变动会非常难处理。单独建一张场次-选手关联表就灵活得多每行只记录场次ID、选手ID、加入时间、离开时间、最终排名。这个设计在结算时帮了大忙。算成绩时只需要扫描这个关联表凡是加入时间早于场次结束时间的选手都有资格参与结算——不需要在业务逻辑里写各种补丁处理中途增减人的情况。3.2 牌局明细表每局结果至少要存19个字段牌局明细表是整个系统最核心的表。我设计它的时候坚持了一个原则一条记录只对应一局所有需要维度全部冗余存储不依赖关联查询。为什么冗余因为复盘场景对查表速度要求高而关联查询在百局以上会明显变慢特别是手机端上。最初的表结构是这样的字段名类型说明局ID自增主键全局唯一场次ID外键关联场次表局序号整数本场次内第几局玩家1~4得分整数四家本局得分本局最大赢家ID外键得分最高者本局最大输家ID外键得分最低者特殊规则标记字符串如自摸杠上花封顶录入时间时间戳系统自动填录入人ID外键谁录的这局备注文本手动补充你可能注意到玩家1到4的得分字段是平铺的。这设计合理吗在麻将和长牌场景下每局固定4人参与平铺比竖表读起来快得多导出Excel也更直观。除了这四个得分字段我还存了本局最大赢家和本局最大输家这两个冗余字段。目的是随时可以快速回答一个问题本场次中每局的转折点是什么时候。但后来我意识到——上面这个表结构漏了一个致命的东西牌局结束瞬间各家的累计总分没有存。 | | |这个当时累计分字段是复盘功能的核心数据。没有它画不出排名曲线。没有它没法回答第7局结束时他是领先还是落后。我踩过这个坑初版没有这个字段后来做复盘功能时不得不跑一遍全量重算耗时且容易出错。最终版本在牌局明细表里加上了两个关键字段本局结束后四家累计得分冗余存储本局结束后四家排名快照正是靠这两个字段复盘模块的所有图表才稳稳当当跑了起来。3.3 计分规则集普通用户不用改但你不能不做每个地方的计分规则差异极大。我们玩的长牌规则里有抓分扛分封顶麻将规则里有点炮自摸混一色清一色加成。不同规则组合会导致同一局牌的结果完全不同。一开始我想用一个规则引擎配置闯天下后来想通了大多数普通用户根本不需要改规则他们只想输入几个数字、得到结果。但你的系统如果不支持规则配置一旦有人拿着异规则需求来找你你就得改代码。折中方案是这样系统内置三套常用规则集通用麻将计分、本地长牌计分、自由计分完全手动。可以新建自定义规则集只需要设置最高分上限封顶值、是否允许负分、是否按基数自动翻倍。牌局录入时系统根据规则集自动校验录入结果的合法性。比如通用麻将规则集配置了封顶值100录入员输入某家得分140系统立刻报警提示分值与规则不符。这个校验功能帮我们挡下了很多手误。3.4 一个关键设计取舍为什么不把所有信息塞进一个JSON字段现在流行一把梭把所有信息塞进一个大JSON字段读写都方便。我最初第一版也这样干过——但很快发现两个痛点痛点一要按特殊规则标记做筛选统计JSON字段做不到高效索引。痛点二给复盘页面传数据时前端根本不需要后端的JSON全文它只需要若干标量字段。塞JSON只会让传输体积暴涨。最终的原则是需要筛选的、需要排序的、需要统计的数据一律独立字段纯展示用的、永不参与计算的辅助信息才允许用JSON或文本字段。比如这一局的牌谱描述我会存JSON但本局是否封顶这种参与排名的标记必须独立字段。4. 局内计分录入效率和技术实现的平衡局内计分是这个系统里最影响用户体验的部分。打牌的人在等录入的人不能掉链子。如何让一次录入在10秒内完成是我反复琢磨的核心问题。4.1 录入界面数字键盘和复制上一局的巧思我见过很多计分单据的录入界面毛病往往出在让用户从列表里找人、再手动输入数字、还要点几个下拉框。打了一次牌之后用户根本不想再打字。我做的录入界面是这样设计的默认按上一局的座位顺序排列玩家不用重新挑选。四个得分输入框并排默认值为0负数可以直接输入减号再输数字。最下方是本局特殊标记的快速按钮自摸、杠上开花、封顶等点一下打上标记。一个确认录入按钮支持回车快捷键。这里我要特别推荐一个细节支持复制上一局。如果连续两局的结果一样——比如都是同一家赢、分数相同——录入员只需要点一下复制上局结果再视情况微调。实测下来这个功能平均为每局节省了40%的录入时间。4.2 实时校验规则引擎挡住的那些手滑刚才提过规则集用于校验录入值。这里展开说说校验的完整粒度因为我见过不少系统只做了非空校验等于没做。校验分三层次第一层得分之和必须等于0或等于规则集设定的局分守恒值。四人牌局中如果有人赢了100分其他三人-100、0、0和值不是0说明某个数填错了。这层校验能拦住80%的手误。第二层每位玩家得分不能超过规则集设置的单局上限/下限。如果你设置封顶60任何一人的得分绝对值超过60都会被拦截。第三层逻辑自洽校验。比如标记了自摸那么得分最高者必须和标记所指的玩家是同一人。第三层校验我是在实际使用中发现漏漏的。有一局麻将A玩家点炮给B玩家但录入员手滑选了自摸标记。得分本身没错局分守恒也满足但特殊标记与赢家对不上。如果不校验复盘时数据分析就会被带偏。加了这层之后基本杜绝了低级错误。4.3 局内实时视图当前排名和本局点评录入完一局系统立刻更新场次排名视图。这个视图不仅在结算时需要在现场也很重要——大家打完一局都想知道现在的总排名。实时视图包含的内容当前各家累计得分按总分降序排列。各家的胜局数、负局数、零分局数。最近五局的得分趋势用最简单的空间柱形图展示。本局点评自动生成的简短分析比如第7局后张三从第2名升至第1名主要原因是第7局赢了120分。本局点评这功能是我后来加的。用户反馈说光看数字看不出比赛进程的精彩之处但配上简短点评很多经典翻盘局一眼就能跳出来。这个点评不搞AI生成就是用模板加拼装比如拼上第N局赢家名字得分绝对值排名升降方向几个变量。5. 局外结算看起来简单其实暗坑都在边角录制完最后一张牌局表点一下结算并复盘系统走结算流程。这里没有复杂算法但边界情况的处理决定了系统的稳定性和口碑。5.1 结算标准流程与积分换算结算流程三步走汇总所有牌局记录校验局分守恒。如果存在不守恒的记录返回错误并锁定该局等待修正。按牌局明细里的本局结束后累计得分快照确定最终排名。这里有个关键决定最终排名的依据是最后一张牌局的累计得分快照不是把所有牌局正负分加起来重算一遍。两者在数据上一致但使用快照可以避免因某局被撤销修改导致排名错乱。按预设的积分规则把最终得分换算成赛事积分。常见的换算是分段线性第一名加10分第二名加6分第三名加3分第四名加1分。有的比赛还要乘上参与系数或桌号权重这些都可以在规则集里配。积分换算这一块我从线下跑步赛事那套名次积分表借鉴了思路。你不需要让系统理解比赛语义只需要给它一张名次-积分的映射表再加一个可选权重系数就能覆盖绝大多数比赛场景。5.2 中途有人退出怎么办这是我在实际运营月度赛时真实遇到的场景。比赛进行到第9局选手C临时有急事要走剩下三家没法继续。如果直接结算C的成绩保留已打9局的结果如果直接取消本场次A和B已经到手的领先优势就白打了。我的处理方式是给场次增加一个结算模式字段完整结算全员打到最后一局。提前结算某人中途退出系统按已打局数正常结算退出者成绩照算。作废处理场次作废所有统计数据回滚到本场次起始状态。撑腰这个设计背后的逻辑是计分系统应该尊重现实世界的不完美而不是假定所有比赛都按剧本完美完成。数据模型和结算流程必须支持不完美的比赛。5.3 结算页面的可追溯性结算页面不只是显示一张排名表还要提供每一项排名的追溯依据。看到第2名和第3名只差2分时观赛者肯定会问这2分在哪一局拉开的所以结算页面每个名次后面都有一个查看明细入口点击后展示该选手参与的全部牌局列表、关键翻盘局、和与前后名的分数差距来源。这个做法最初是受电商订单系统的订单追溯功能启发——每一项优惠扣减都能找到来源。放在计分场景里可追溯性就是信任本身。6. 复盘模块数据到洞察差着一个对比的距离结算完成只是第一步。整个项目最有价值的增量在于复盘。复盘让用户不再只是看见谁赢了而是理解为什么赢、怎么赢的。6.1 排名曲线的绘制逻辑排名曲线是最直观的复盘图表每局打完一个点纵轴是排名横轴是局序号四家各一条线。实现上存在一个容易被忽略的坑排名本身是同一时刻的快照不能一条一条叠加画而是每一局扫描全部选手的累计分然后统一排序再落点。如果作为图表服务端的数据是按单条流水推送的前端就会出现第7局的张三排名还没更新但李四排名已更新的错位。解决办法是我的数据模型已经存了快照字段复盘查询时一次性取回整个场次的全部快照再在内存中完成排序逻辑。6.2 逐局得失分布看运气还是看实力用一组堆叠柱状图展示每人的单局得分分布把胜局、平局、负局分别涂成不同颜色。这个图表的最大价值是快速区分运气型领先和实力型领先。举个例子A选手总分第一但他的得分分布很散——三局大胜其余全是小负B选手总分第二但局局小胜只有一局失利。从竞技角度说B的稳定性更值得学习。复盘页面会生成一段文字说明把这个洞察直接点出来而不是让用户自己对着一堆柱状图发愣。6.3 关键转折局自动识别这是复盘模块里最聪明的功能也是花费心思最多的环节。实现思路不复杂遍历每一局的快照字段记录下排名变化量的分值。变化量绝对值最大的那一局就是关键转折局。比如整场12局比赛第9局结束后原本第4名的D直接跳到第1名排名变化量为3这就是绝对转折点。自动生成的分析文字第9局为本场关键转折局D在该局净胜98分排名从第4上升至第1。我没有用任何复杂算法只是抓住排名变化的快照差这一核心指标就达到了很好的复盘效果。6.4 导出与分享复盘结果导出成图片方便发到群聊。同时支持导出完整Excel包含全部牌局流水、排名快照、关键转折局标记。Excel的格式我在细节上打磨了很久表头固定、冻结首行、每列加筛选按钮加粗突出总分列。这些细节让导出文件真正有人用而不是导出来就躺在聊天记录里吃灰。7. 部署与运行环境不折腾的方案做这个小系统我没有上K8s也没有用微服务而是选择了最简单的部署方式因为使用场景决定了没必要复杂化。7.1 技术栈选择与理由技术栈如下后端Python FastAPI数据库SQLite单文件模式生产环境切到PostgreSQL。前端简单响应式页面用原生HTML 轻量JS框架不做重型SPA。部署一台几百块钱的轻量服务器或者直接用内网NAS跑Docker。为什么选SQLite起步因为几十人的比赛读写压力极其有限SQLite单文件模式备份方便——拷走文件就等于备份了全部数据。当数据量真正大起来时再切PostgreSQLSQLAlchemy中间层让切换成本极低。没有一上来就在PostgreSQL上死磕省下了大量运维精力。7.2 权限设计录入员和裁判的区分录入员只能录牌局、修改已录但未结算的牌局裁判管理员可以执行结算、作废场次、管理选手名单、修改规则集。权限机制用最简单的角色字段实现没有上OAuth。但这块有一个值得说的坑录入员修改已录牌局时系统必须同步刷新本场次所有牌局的快照数据。这就引出一个连锁校验方案——修改任何一局都必须跑一遍从该局之后到结算前的快照重算写入日志。否则容易看到总分和排名对不上的诡异现象。7.3 数据备份与容灾我的备份策略很朴素每天自动打包数据库文件上传到NAS每周手动导出一份完整的Excel至网盘。因为比赛数据只有那么几十MB太复杂的备份方案反而是负担。但别忘了一旦系统承接了别人家的月度赛、季度积分赛数据就不仅是自己的了。备份要有恢复演练也要有。我做过一次恢复测试直接在另一台机器上起容器挂载备份文件检查数据完整性20分钟内搞定。这个操作简单但建议每个搭建系统的人都做一遍。8. 实测效果与真实反馈系统做完以后我组织了三次内部测试一次自用长牌局、一次朋友麻将局、一次小区月度赛事。三场下来数据质量非常好局分守恒校验一次没触发误报复盘页面被夸得最多。8.1 局内计分快了多少老朋友用之前录入一局平均耗时在40秒到1分钟——为什么这么慢因为他们一边打一边掏出另一个App算分还要对比纸上的记录算完再手动录进去反复核对中间还可能聊两句。换这套系统后录入一局平均12秒熟练后能压到8秒。省下的时间刚好够大家打完一局之后聊两句闲天节奏舒服很多。8.2 结算环节的意外发现用户使用中有一个谁也没想到的高频操作撤销最后一局。打牌时偶尔会出现这局不算了的情况——比如有人发现发错牌、有人中途接电话、有人想重打一局。如果系统不支持撤销最后一局整个结算就会卡住。我把撤销最后一局做成了场次页面的独立按钮带有二次确认弹窗。这个功能在三次实测中用了不下十次如果没做赛程会非常尴尬。8.3 复盘带来的变化月度赛主办人反馈以前比赛结束大家只知道名次但谁也说不出第几局是转折点。有了复盘页面后赛后讨论明显热闹了。A说我从中场开始连续赢B不服说你看排名曲线第9局那波才是关键。连不会打牌的后勤人员也能从图表里看懂比赛进程。9. 整个过程最值钱的三条经验项目做完后我复盘了整个开发和试运行过程有三条经验值得分享给正准备做同类系统的朋友。第一先划分局内和局外两层再动数据库设计。很多人拿到需求直接建表结果要么表结构太细碎要么一把梭JSON。先明确局内是高频实时的小事务局外是低频全局的大聚合数据模型自然就能设计得清晰。第二冗余快照是复盘功能的命脉。如果当初没有在各局记录后面存下当时累计分和当时排名后期做复盘模块必然痛苦。复盘功能不是锦上添花它是把这个计分工具从记分本升级成比赛分析系统的关键分水岭。设计的时候一定为未来的复盘留好数据。第三边界情况的处理比主流程更能打动用户。我花在处理中途退出撤销最后一局局分不守恒报警上的时间比写主流程多一倍但正是这些细节让用户觉得这个系统是真的被用过的。一个没有任何边界处理逻辑的计分系统更像玩具不像工具。10. 后续还能怎么扩展目前这个系统已经稳定跑了两个月积累了可观的场次数据。我接下来的规划有两个方向第一个方向是多人团队的赛季积分系统。现在的结算只支持单场次赛季概念还没有引入——其实只需要加一张赛季表然后把场次归入赛季同一赛季内做加权累计即可。这会让小区月度赛的年度总冠军评选变得非常顺畅。第二个方向是移动端适配优化。现在的页面在手机上能跑但录入界面在大屏幕下其实更顺手。后续计划做一套微信小程序版专门把录入流程优化到极致——比如扫码进桌、语音输入分数。这个工程量比现在的系统大不少但方向是明确的把录入摩擦降到最低把复盘的洞察做到最透。如果你也在琢磨类似的线下比赛计分工具我的建议很直接别一上来就追求大而全先把手动录入加棋盘记录加基础结算这三点跑通再逐步叠加复盘和赛季功能。小步快跑每加一个功能都真刀真枪拉人测一次比憋一个大版本再发布要靠谱得多。