麻将结算全链路语义(SETTLE_SEMANTICS)
第0条(基线)本文件语义以产品源码为权威源;引用只允许以文件与符号(函数、字段、协议名)给出;行号引用前必须复核现码。文件简称:PS=玩法服,GS=游戏服,CC=客户端核心,SC=场景服,MC=匹配中心,DF=麻将定义。进程链路:PS 到 MC 到 SC 到 GC 到 GS 到 CC。
第1章 正常链
第1条(对局结束触发)
Game_End必须以幂等标志防重入;状态必须置对局结束。- 总回合号与当前回合号必须统一递增,并经同步协议发给场景服,供崩溃幂等公式使用;场景服侧接受非严格单调。
- 花猪与流局二次结算必须以独立标志防重。
第2条(倍率与分数)
- 局内禁止直接改总分;分数一律走挂账字段(加分与扣分),局末合并入总分后清零,防下局重复结算。
- 番型与倍数由胡牌检测统一计算;GS 直接消费结果,禁止重复计算;客户端只展示,以服务器为准。
- 全局分数必须平衡:总增量大于 0 时全部增量强制置 0 并记 CRITICAL。
- 已结算玩家(胡牌时已发放)只上报增量:以标记与已结算增量的差值计算。
第3条(结算包)
- 结算包每玩家条目必须含结算序号基线(未推进)与已结算标记。
- 一回合一个 token;直发 key 必须用回合起始基线,否则同回合第二个 token 将导致已胡玩家双付。
第4条(发布通道)
- 通道一(胡牌即时):胡牌时即时自动结算,key 为结算序号加回合号;失败必须回滚已结算标记并入重试队列。
- 通道二(直发):结算发布到 MC,MC 校验房间后转 SC,SC 逐玩家分包、登记 pending 后发出,经 GC 转发到 GS 处理。
- 通道三(增量):花猪、大叫、退税二次结算,带二次结算标记,key 与胡牌同槽。
- 通道四(崩溃保底):崩溃处理与前三通道共用同一线性流水线。
第5条(GS 货币结算顺序)ApplyMahjongSettlement 必须按下列顺序:
- 状态守卫:无房间时按冻结余额分流------有冻结加合法上下文则重建房间;无冻结则幂等成功;有冻结但上下文非法则强制退款。
- key 幂等比较:小于期望跳过并成功返回;等于期望执行;大于期望返回失败并触发重试。
- 冻结 token 校验;跨房二次结算豁免。
- 冻结模型:赢家只发放净收益、冻结镜像不变;输家下调冻结镜像并同步下调入场冻结,防亏损凭空消失。
- 写前日志:必须先持久化已处理 token 再发放;发放失败回退 token。
- 段位分、排行榜、房间状态、可用余额快照;最终余额持久化失败不得回退 token(资金已发,防重复发奖)。
第6条(GS 到客户端展示)
- 结算处理尾部必须下发结算协议,携带错误码(成功为 0)。
- 成功路径必须回确认(含结算 key 与确认 token)供 SC 清理 pending;失败路径禁止回确认,SC 保留重试。
- 客户端结算协议处理顺序:去重防护、存结算结果、发布结算事件。
- 对局流水日志必须写入。
第7条(结算后房间状态)
- SC 房间不得销毁;状态置等待装载,全员未准备。
- PS 局尾必须清理:结算序号按本局实际最大 key 精确推进、杠记录清空、当前回合号清零防 key 溢出。
第2章 失败路径
第8条(错误码产生)错误码仅产生于 GS 结算处理两处:房间不匹配分支且本玩家不在结算包内;正常分支结算失败(未来 token、冻结 token 不匹配、写前日志持久化失败、发放失败、余额快照持久化失败)。错误码 MAHJONG_ERROR_SETTLE_FAIL = 3300025。
第9条(客户端提示)客户端收到非零错误码时必须发布错误事件;UI 映射为结算异常提示文案;客户端只提示,不得主动重发;重试全部由服务端驱动。
第10条(重试层一:PS 本地队列)失败条目必须 5 秒重试、至多 10 次、超限丢弃记错,并以 xpcall 保护。入队点为胡牌失败与结束自动结算失败。
第11条(重试层二:SC 待确认重投)
- 登记必须先于发送,两级表按玩家与房间归档。
- 投递:目标中心无效永久放弃;不可达则计数并排期;成功则发送。
- 定时重试每 tick 驱动;单条上限 4 次、跨会话总上限 12 次;耗尽暂停,重连再触发。
- 上线补投优先,且存在 pending 时不得回上线成功(防冻结本金误退)。
- 确认保护:已有 pending 时只更新中心 ID。
- 确认收敛:确认 token 不小于结算 key 时幂等跳过清理;GS 成功才确认,失败不确认。
第3章 幂等与防重复
第12条(写前日志为 GS 玩家级唯一权威状态)
- 状态定义:已处理结算 token 从 0 起,结算序号跟随该值。
- 公式:结算 key 为结算序号加回合号;期望 key 为已处理 token 加一。
- 三通道(胡牌、直发、崩溃)共用同一线性流水线:小于期望跳过、等于执行、大于重试。
- 结算写前日志必须先持久化 key 再发放;持久化失败必须回退;发放后余额快照失败不回退。
- 退款写前日志必须先推进 token 再发放;发放失败回退。
- 崩溃回退仅在成功后推进 token。
- 入场对齐:进房时必须把已处理 token 重置到冻结 token 基数,使结算序号与冻结 token 对齐。
- 自动结算回放幂等:同 token 同金额重复回调视为幂等成功。
- Guardians 结算通道(同房全量、跨房特殊)均走主结算。
- 二次结算不推进 token,幂等由重试队列保证。
第13条(客户端去重)
- 条件:已有结算结果且待显示时,必须跳过任何新结算。
- 置位:待显示标记置位并深拷贝结果;清除:新局准备时清标记,结果保留供流水恢复,字段受保护不被快照覆盖。
- 前置守卫:陈旧房间消息必须丢弃,EndGame 入口先查房间归属。
第14条(房号不匹配拒做货币操作)
- 结算入口必须强校验:存档房号与结算房号不一致时禁止货币操作。
- 跨房(原房号非零且新房号不同)仅回确认。
- 有冻结但同房号信息缺损时对齐上下文继续。
- 无冻结时保留引用置等待装载、回确认、保留流水。
- 退款必须同样校验房号与冻结 token。
第4章 异常结算
第15条(房间销毁后结算)查房为空时必须记警告并返回失败,使调用方得知结算未发生;结算走主通道加自动结算兜底;场景服房间移除必须清理本房 pending 与孤儿 pending,防永久重试。
第16条(异常回滚与跨局残留)
- 开局核心逻辑必须以 xpcall 包裹;失败时必须清定时器、重置结束毒丸、状态回等待、通知断开错误。
- 每局开始必须重置挂账字段(加分、扣分、局分)。
- 局末结算后挂账必须强制清零,即使自动结算失败;结算由结算包通道独立负责,防下局重复结算。
- 二次结算必须以花猪与流局标志防重。
第17条(杠记录清空与流局退税)
- 流局处理必须先备份再清空杠记录,再遍历退税,防循环异常导致记录残留;退税必须记录退税表与流水。
- 局尾必须统一清空杠记录。
第18条(其他防御)
- 场景服结算处理:房间不存在或匹配中心不匹配必须崩溃上报;不匹配时主动验证防悬挂。
- GS 结算:冻结资金加非法上下文必须强制退款;无房间必须幂等成功并清理冻结 token。
第5章 同步锚定
第19条(同步锚定规则)全量快照同步必须保证多客户端收敛到服务端时间,禁止各客户端按本地时刻分叉:
- 阶段变化才杀换三张与定缺定时器,等待响应阶段每次杀;仅离开进行中才杀回合定时器,防乱序快照误杀行动通知新建的定时器。
- 全量快照替换必须经替换接口。
- 状态码单调防护拒绝阶段回绕;座位号反回归防护;剩余时间单调递减守卫。
- 进行中与等待响应不得由快照重启定时器,只更新回合信息兜底;若重启,各客户端锚定本地时刻产生分叉;回合定时器只由行动通知一次性锚定。
- 换三张与定缺每次同步都必须重启定时器,使所有客户端向服务端时间收敛。
- 全局倒计时锚点仅在非进行中且非零时建立。
- 陈旧房间消息必须经统一入口丢弃。
第6章 缺陷与风险
第20条(S1-S8)
| # | 位置 | 缺陷 | 风险 |
|---|---|---|---|
| S1 | 客户端去重 | 条件不校验房号:结果存在且待显示时,任何新结算(含新房新局)均被跳过;陈旧守卫只挡非空且不同房号的包 | 去重窗口跨房时存在展示丢失窗口 |
| S2 | PS 重试队列加 SC 重投 | 两个通道可产生同族结算;恰好一次依赖结算 key 幂等拦截,先失败后成功与先成功两条路径的终态一致性依赖 token 单调 | 必须测试双通道并发下恰好一次发奖 |
| S3 | GS 余额快照 | 发放后余额快照持久化失败返回失败但不回退 token;客户端提示自动重试,重试因 token 已推进必跳过,资金实际已发 | 提示误导,重试无实际效果 |
| S4 | GS 房间不匹配分支 | 存档房号为空时本玩家判定仍按结算包计算,错误码可能为 0 且不确认,该分支确认覆盖完整性待确认 | pending 可能残留至重试耗尽 |
| S5 | SC 重试上限 | 耗尽仅暂停(等重连),总上限外永久放弃,无死信队列 | 极端场景结算永久丢失,有错误日志可审计 |
| S6 | GS 崩溃 key | 旧协议不传回合号时兜底为已处理 token 加一,与携带回合号的公式可能错位竞争同槽 | 热更新新旧包混合期间崩溃结算与胡牌 key 不匹配 |
| S7 | PS 与 GS 结算包 | 一回合一个 token 依赖包内结算序号基线;旧包无该字段回退到存档值,历史曾导致同回合第二 token 双付 | 热更新灰度期间新旧包混跑必须关注 |
| S8 | 客户端结算缓存 | 终局数据清理定时器 60 秒后才清;待显示标记在新局准备前无超时兜底 | 低风险 |
第7章 测试点
第21条(测试点清单)
- 正常发奖恰好一次:胡牌加结束双通道,赢家增量恰好入账一次。
- 结算 key 单调:多回合下已处理 token 严格递增。
- 重复直发:同包重发触发跳过与确认,SC 删除 pending。
- 失败重试不重复发奖:首次发放失败回退 token,重投成功后发奖一次。
- 写前日志崩溃恢复:token 已持久化但发放未执行,重启重投跳过,不重复发奖。
- key 大于期望:乱序未来 key 返回失败并进重试。
- key 小于期望且同房:历史 key 跳过。
- key 小于期望且跨房二次结算:落到冻结 token 校验并执行。
- 冻结 token 不匹配且同房:返回失败触发重试。
- 输家冻结镜像下调:增量为负时冻结下调且入场冻结同步。
- 输家全损:入场冻结为 0 时仅释放冻结,不发放。
- 房号不匹配:无货币变动。
- 不匹配跨房:仅确认不落账。
- 房间销毁后结算:返回失败且不崩溃。
- 开局异常恢复:房间回等待、毒丸复位、可重开。
- 跨局挂账残留:上局异常后新局挂账字段为 0。
- 流局清空杠记录:退税记录完整且杠记录已空。
- 全局平衡:总增量不大于 0;大于 0 时强制归零。
- 已结算玩家增量:只发增量。
- 客户端去重:待显示期间二次结算被跳过。
- 新局解除去重:准备后新结算正常显示。
- 错误码传播:结算失败码到客户端提示。
- 失败不确认:pending 保留可重投。
- 成功必确认:确认携带 token,SC 清理 pending。
- 重试上限:单条耗尽暂停,重连重触发。
- 中心 ID 非法:永久放弃防无限重试。
- 快照阶段单调:旧状态回绕被拒绝。
- 倒计时收敛:换三张与定缺每次同步重启定时器。
- 进行中定时器不重启:快照不重启回合定时器。
- 崩溃共享流水线:崩溃 key 与胡牌 key 同槽,先到执行后到跳过。
- 双通道恰好一次:自动结算队列与 SC 重投并发,最终仅一次发奖。