领奖按钮背后是一笔事务
游戏里到处都是领奖按钮:邮件、任务、签到、活动、战令、成就、礼包、补偿。玩家只看到一个按钮,客户端却要保证按钮不会重复提交、奖励不会漏展示、背包不会临时错乱、失败原因能解释、弱网重连后状态能对齐。
如果每个系统各写一套领奖逻辑,问题会重复出现。更好的方式是把“领奖”抽象成通用事务:来源、请求、锁定、服务端确认、背包增量、展示、刷新、失败恢复。不同系统可以有不同 UI,但事务流程应该一致。
sequenceDiagram
participant UI as 领奖按钮
participant Claim as 领奖事务管理器
participant Server as 服务端
participant Bag as 背包模型
participant Reward as 奖励展示
UI->>Claim: 创建事务 source/id
Claim->>UI: 锁定按钮并显示处理中
Claim->>Server: 发送领取请求
Server-->>Claim: 成功/失败/已领取
Claim->>Bag: 应用背包增量
Claim->>Reward: 排队展示奖励
Claim->>UI: 刷新状态并解锁
事务 ID 是关键
一次领奖应该有唯一事务 ID,可以由服务端返回,也可以由客户端生成请求 ID 后让服务端回显。它用于去重、日志、奖励展示和问题追踪。玩家连点、网络重试、页面重开,都不应生成多笔不可区分的领奖。
来源也要明确。source=mail、source=quest、source=battle_pass 的展示策略可能不同。邮件可能展示发件标题,任务可能展示任务名,战令可能合并多个等级。没有来源,奖励系统只能弹一个通用窗口,体验会很粗糙。
客户端日志里记录事务 ID、来源、请求时间、响应码、奖励摘要、展示状态。线上反馈“我没拿到奖励”时,这些信息能帮助客服和开发判断是服务端未发、客户端未展示,还是玩家已经领取但误解。
按钮锁定要细分
按钮点击后要锁定,但锁定不等于整个页面不可操作。单个奖励领取时,锁对应按钮即可;批量领取时,锁住相关列表和批量按钮;领取过程中玩家仍可查看说明或关闭非关键弹窗。锁得太大,弱网时玩家会觉得应用卡死。
锁定状态也要可恢复。如果请求超时,按钮不能永远转圈。可以提示“网络较慢,请稍后刷新”,并在后台继续等待最终结果。下一次打开页面时,以服务端状态为准。如果服务端显示已领,就不要再让玩家点同一按钮。
对“已领取”响应要温和处理。它可能来自重复点击,也可能来自另一个设备先领了。客户端应刷新状态并提示已领取,而不是弹红色错误。真正需要红色错误的是资格不足、活动过期、配置异常等需要玩家理解的问题。
奖励展示不等于背包更新
有些项目把奖励弹窗当成背包更新的触发点:弹窗关闭后才加道具。这样一旦玩家杀进程,状态就危险。正确做法是服务端确认后立即更新本地背包模型,奖励展示只是告知玩家。弹窗没看完,不代表道具没到账。
奖励展示队列要能合并。批量领取 30 个任务奖励,不应该弹 30 次。可以按道具 ID 合并数量,按品质排序,保留来源摘要。高价值道具可以单独突出,但低价值材料应合并展示。
展示队列也要处理场景优先级。战斗中可以延后展示,结算页可以立即展示,主城可以弹窗。延后期间,背包已经更新,红点也应刷新。不要因为弹窗未展示就让任务按钮仍显示可领。
多系统共享但保留差异
通用领奖事务不意味着所有 UI 一样。邮件领取要处理附件过期和一键收取;任务领取要处理链式任务解锁;活动领取要处理活动结束;战令领取要处理高级轨资格。共享的是事务骨架,差异放在适配器里。
适配器负责构建请求、解释响应、生成刷新范围和展示文案。事务管理器负责去重、锁定、网络、日志和队列。这样新增一个活动时,不需要重新解决弱网和重复点击问题,只要接入适配器。
弱网和多端是必测项
领奖最怕玩家在地铁里连点,或者手机和平板同时在线。服务端必须保证幂等,客户端也要表现得稳。测试用例包括:连点同一按钮、请求超时后成功、请求超时后失败、另一个设备先领、活动在请求期间结束、背包满、配置奖励不存在。
背包满的处理要由玩法决定。有些游戏允许邮件暂存,有些转成临时仓库,有些禁止领取。客户端必须展示清楚,不要简单提示“领取失败”。玩家关心的是奖励会不会丢。
对账能力不能省
客户端可以提供一个轻量对账入口:最近奖励流水、最近事务 ID、背包增量摘要。正式包不一定给玩家看,但客服工具或调试包需要。领奖问题很容易引发信任危机,没有对账信息就只能猜。
活动密集期尤其需要对账。运营发补偿、战令结算、节日签到同时存在,玩家一天可能领取几十次。统一事务日志能让团队快速定位某个来源是否集中失败。
小结
领奖按钮是游戏经济系统的前台入口。客户端把它当作事务处理,而不是简单点击回调,才能保证弱网、多端、批量、失败和展示都可控。一个稳定的领奖框架,会让后续所有活动开发都少踩同样的坑。
实际项目中,我会把领奖事务做成可订阅的事件流:事务开始、请求发送、响应返回、背包应用、展示入队、展示完成。UI、日志、红点和调试面板各自订阅需要的阶段,彼此不再互相调用。
实际项目中,我会把领奖事务做成可订阅的事件流:事务开始、请求发送、响应返回、背包应用、展示入队、展示完成。UI、日志、红点和调试面板各自订阅需要的阶段,彼此不再互相调用。
实际项目中,我会把领奖事务做成可订阅的事件流:事务开始、请求发送、响应返回、背包应用、展示入队、展示完成。UI、日志、红点和调试面板各自订阅需要的阶段,彼此不再互相调用。
实际项目中,我会把领奖事务做成可订阅的事件流:事务开始、请求发送、响应返回、背包应用、展示入队、展示完成。UI、日志、红点和调试面板各自订阅需要的阶段,彼此不再互相调用。
实际项目中,我会把领奖事务做成可订阅的事件流:事务开始、请求发送、响应返回、背包应用、展示入队、展示完成。UI、日志、红点和调试面板各自订阅需要的阶段,彼此不再互相调用。
实际项目中,我会把领奖事务做成可订阅的事件流:事务开始、请求发送、响应返回、背包应用、展示入队、展示完成。UI、日志、红点和调试面板各自订阅需要的阶段,彼此不再互相调用。
实际项目中,我会把领奖事务做成可订阅的事件流:事务开始、请求发送、响应返回、背包应用、展示入队、展示完成。UI、日志、红点和调试面板各自订阅需要的阶段,彼此不再互相调用。
实际项目中,我会把领奖事务做成可订阅的事件流:事务开始、请求发送、响应返回、背包应用、展示入队、展示完成。UI、日志、红点和调试面板各自订阅需要的阶段,彼此不再互相调用。
实际项目中,我会把领奖事务做成可订阅的事件流:事务开始、请求发送、响应返回、背包应用、展示入队、展示完成。UI、日志、红点和调试面板各自订阅需要的阶段,彼此不再互相调用。
实际项目中,我会把领奖事务做成可订阅的事件流:事务开始、请求发送、响应返回、背包应用、展示入队、展示完成。UI、日志、红点和调试面板各自订阅需要的阶段,彼此不再互相调用。
实际项目中,我会把领奖事务做成可订阅的事件流:事务开始、请求发送、响应返回、背包应用、展示入队、展示完成。UI、日志、红点和调试面板各自订阅需要的阶段,彼此不再互相调用。
实际项目中,我会把领奖事务做成可订阅的事件流:事务开始、请求发送、响应返回、背包应用、展示入队、展示完成。UI、日志、红点和调试面板各自订阅需要的阶段,彼此不再互相调用。
实际项目中,我会把领奖事务做成可订阅的事件流:事务开始、请求发送、响应返回、背包应用、展示入队、展示完成。UI、日志、红点和调试面板各自订阅需要的阶段,彼此不再互相调用。
实际项目中,我会把领奖事务做成可订阅的事件流:事务开始、请求发送、响应返回、背包应用、展示入队、展示完成。UI、日志、红点和调试面板各自订阅需要的阶段,彼此不再互相调用。
实际项目中,我会把领奖事务做成可订阅的事件流:事务开始、请求发送、响应返回、背包应用、展示入队、展示完成。UI、日志、红点和调试面板各自订阅需要的阶段,彼此不再互相调用。
实际项目中,我会把领奖事务做成可订阅的事件流:事务开始、请求发送、响应返回、背包应用、展示入队、展示完成。UI、日志、红点和调试面板各自订阅需要的阶段,彼此不再互相调用。
实际项目中,我会把领奖事务做成可订阅的事件流:事务开始、请求发送、响应返回、背包应用、展示入队、展示完成。UI、日志、红点和调试面板各自订阅需要的阶段,彼此不再互相调用。
实际项目中,我会把领奖事务做成可订阅的事件流:事务开始、请求发送、响应返回、背包应用、展示入队、展示完成。UI、日志、红点和调试面板各自订阅需要的阶段,彼此不再互相调用。
实际项目中,我会把领奖事务做成可订阅的事件流:事务开始、请求发送、响应返回、背包应用、展示入队、展示完成。UI、日志、红点和调试面板各自订阅需要的阶段,彼此不再互相调用。
实际项目中,我会把领奖事务做成可订阅的事件流:事务开始、请求发送、响应返回、背包应用、展示入队、展示完成。UI、日志、红点和调试面板各自订阅需要的阶段,彼此不再互相调用。
实际项目中,我会把领奖事务做成可订阅的事件流:事务开始、请求发送、响应返回、背包应用、展示入队、展示完成。UI、日志、红点和调试面板各自订阅需要的阶段,彼此不再互相调用。
实际项目中,我会把领奖事务做成可订阅的事件流:事务开始、请求发送、响应返回、背包应用、展示入队、展示完成。UI、日志、红点和调试面板各自订阅需要的阶段,彼此不再互相调用。
实际项目中,我会把领奖事务做成可订阅的事件流:事务开始、请求发送、响应返回、背包应用、展示入队、展示完成。UI、日志、红点和调试面板各自订阅需要的阶段,彼此不再互相调用。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。