Phaser 游戏内邮件与通知:附件领取、过期和红点幂等要做扎实

讲解 Phaser 游戏内邮箱系统的客户端实现,包括邮件列表、附件领取、过期策略、红点、通知入口、幂等发奖和离线恢复。

邮箱是经济系统,不只是消息列表

游戏内邮箱看起来像 UI 功能:显示标题、正文、附件,玩家点领取。实际它经常承载补偿、活动奖励、排行榜结算、客服发奖、系统公告和回流礼包。只要有附件,邮箱就是经济系统的一部分。最糟糕的实现是客户端本地生成邮件,点击领取就往背包加物品,没有幂等、没有过期、没有领取记录。这样很容易出现重复领取、邮件消失、红点不准、补偿无法追踪。

Phaser 里的邮箱 Scene 或弹窗只负责展示。邮件状态、附件领取和红点计算应由 MailboxService 管理。若有服务端,领取必须由服务端确认;纯单机也要用稳定 id 和本地记录防重复。邮件系统不需要复杂,但必须可靠,因为玩家对“奖励没了”非常敏感。

邮件状态要明确

一封邮件至少有这些状态:未读、已读、可领取、已领取、已过期、已删除。未读和可领取不是一回事;已读但附件未领很常见;无附件公告读完后可以自动归档;过期邮件是否还能读正文要看产品规则。状态最好由字段组合得出:readAt、claimedAt、expiresAt、deletedAt、attachments。不要只用一个字符串状态,否则边界会越来越多。

附件领取要绑定 mailId 和 claimId。服务端或本地存档记录某封邮件是否已领取。即使玩家连续点击、网络重试、页面刷新,领取也只能成功一次。成功后 UI 播放奖励动画,失败时保留邮件状态并显示原因。

flowchart TD
  A["LobbyScene 邮箱入口"] --> B["MailboxService 拉取邮件列表"]
  B --> C["NotificationService 计算红点"]
  B --> D["MailboxView 显示列表、正文、附件"]
  D --> E["玩家点击领取"]
  E --> F["ClaimService 提交 mailId / claimId"]
  F --> G{"领取结果"}
  G -- "成功" --> H["更新 claimedAt、刷新背包、播放动画"]
  G -- "重复" --> I["同步为已领取"]
  G -- "失败" --> J["保留可领取状态并提示"]
  C --> A

红点要可解释

邮箱红点常见问题是一直不消,或者该亮不亮。红点规则要简单:有未读邮件,或有可领取附件。过期邮件不计红点,已领取邮件不计红点,系统公告是否计红点由配置决定。红点计算应该集中在 NotificationService,不要列表、入口、底部菜单各算各的。

红点还需要增量更新。玩家领取附件后,入口红点立刻变化;新邮件到达后,若玩家在大厅,入口提示;若正在战斗,可以延迟到战斗结束显示。不要在关键操作中突然弹邮件。通知是为了引导,不是打断。

附件领取要考虑背包容量

如果背包有容量,邮件附件领取可能失败。失败时不能把邮件标为已领取。可以提供几种策略:背包满则禁止领取;部分可领取但剩余保留;附件进入临时仓库。最简单且最安全的是全部校验通过才领取。UI 要提前显示背包容量不足,而不是提交后才报错。

多附件邮件要么全部领取成功,要么全部不领取。部分成功会让客服和玩家都难受。若服务端支持事务,使用事务;纯本地则先构造新背包状态,全部可放入后一次保存。领取动画播放的是结果,不应决定经济状态。

一个邮件状态解释器

下面的代码展示如何统一计算邮件可见状态和红点。

interface MailAttachment {
  itemId: string;
  count: number;
}

interface Mail {
  id: string;
  title: string;
  body: string;
  receivedAtMs: number;
  expiresAtMs?: number;
  readAtMs?: number;
  claimedAtMs?: number;
  attachments: MailAttachment[];
}

export function explainMail(mail: Mail, nowMs: number) {
  const expired = mail.expiresAtMs !== undefined && nowMs >= mail.expiresAtMs;
  const hasAttachment = mail.attachments.length > 0;
  const claimable = hasAttachment && !mail.claimedAtMs && !expired;
  const unread = !mail.readAtMs && !expired;
  return {
    expired,
    unread,
    claimable,
    showRedDot: unread || claimable,
  };
}

列表、详情页和入口红点都应该使用这个函数或同源逻辑。这样规则改变时只改一处。比如产品决定过期未读邮件仍显示红点,也能集中调整。

邮件列表要控制性能和可读性

邮箱可能积累很多邮件。不要一次创建几百个复杂 DOM 或 Text。列表可以分页或虚拟滚动,默认按未领取、未读、最新排序。过期和已领取邮件可以折叠或自动归档。每封邮件显示标题、来源、时间、附件摘要和状态。重要补偿邮件可以置顶,但要有过期时间。

正文支持富文本时要谨慎。Hugo 站点文章可以写 Markdown,但游戏内邮件不应直接执行 HTML。客户端可以支持有限标记,比如换行、加粗、道具图标占位。外部邮件内容必须转义,避免安全和布局问题。

离线和弱网恢复

玩家领取邮件时可能断网。按钮进入处理中后,如果请求超时,状态应变成“结果未知”或恢复可领取,并允许重试。下次进入邮箱时重新拉取服务器状态。不要在超时后本地直接标记失败,也不要直接发奖。若服务端实际成功,客户端下次同步为已领取并补显示奖励结果。

纯本地游戏也要处理保存失败。领取前先检查存储可用,领取后立即保存。如果保存失败,不能只在内存里加奖励。可以提示玩家存储异常,并保持邮件可领取。经济系统宁可少给一步,也不要制造不可追踪的中间态。

运营和客服视角

邮件是运营工具,必须可追踪。每封系统邮件应有 campaignId 或 reason,比如 maintenance_comp_20260424。领取日志记录 mailId、claimId、玩家 id、奖励列表、时间和结果。客服查问题时,不应该只看到“玩家说没收到”。客户端日志至少能提供 mailId 和 claimId,方便定位。

邮件模板也要版本化。补偿文案、本地化和附件配置分开。客户端未知附件类型时不能崩溃,应显示占位并阻止领取或提示版本过低。活动期间大量邮件发送时,入口红点和弹窗要限频,避免玩家刚登录被消息淹没。

上线前检查清单

确认邮件有稳定 id、过期时间、读取和领取字段;确认附件领取幂等;确认多附件要么全成功要么全失败;确认背包容量不足不会标记已领取;确认红点规则集中且可解释;确认弱网超时不会重复发奖;确认邮件正文使用安全富文本;确认列表支持大量邮件;确认客服日志包含 mailId、claimId 和奖励快照;确认战斗等关键场景不会被邮件弹窗打断。

邮箱是玩家和运营之间的承诺通道。Phaser 可以把邮箱做成漂亮弹窗,但附件领取和红点状态必须像账本一样可靠。只要奖励能追踪、领取能幂等、失败能恢复,邮箱就会成为稳定的运营基础,而不是投诉来源。

邮件来源和优先级

邮件可能来自系统补偿、活动、排行榜、好友、客服、礼包码。不同来源需要不同优先级和展示样式。维护补偿应置顶且醒目,好友赠礼可以归到社交分类,系统公告没有附件时不应抢占奖励邮件位置。列表排序可以先可领取、再未读、再置顶、再时间。排序规则要稳定,否则玩家每次打开邮箱列表跳动,会误以为邮件消失。

来源还决定过期策略。客服补偿可能永不过期,活动奖励可能活动结束后 7 天过期,普通公告 3 天后归档。过期前可以在邮件上显示倒计时,最后一天提高提示权重。不要让带附件邮件悄悄过期,至少在入口红点或邮件列表中给明确提醒。

批量领取的边界

批量领取很受欢迎,但实现风险更高。点击“一键领取”时,系统要先筛选所有可领取且未过期邮件,再逐封或事务式领取。如果某封因为背包容量失败,应该停止并提示,还是跳过继续?规则要明确。对于多货币、无容量限制的奖励,可以批量事务;对于装备、道具容量有限的奖励,最好先预估容量,不够时提示清理背包。

批量领取的动画也要合并。不要同时弹出几十个奖励飞行动画。可以汇总成奖励清单,按道具类型归并数量。日志仍然记录每封邮件的领取结果。玩家看到的是简洁反馈,系统保留的是完整账目。

本地通知和站内通知

邮箱红点是站内通知,本地推送则是系统级能力。Web 游戏里本地通知需要权限,不能默认弹。即使拿到权限,也要克制使用。比如工作台完成、离线收益、重要邮件可以通知;普通广告和小活动不应频繁打扰。通知点击后进入游戏,还要能定位到对应邮件或功能。

权限被拒绝时,游戏内邮箱仍然要完整工作。不要把奖励领取依赖系统通知。通知只是提醒,邮箱才是状态来源。Phaser 客户端可以在大厅显示轻量 toast,避免玩家错过新邮件。

邮件模板和本地化

邮件正文通常需要本地化。不要让服务端直接下发所有语言的长文本给客户端,也不要只下发一种语言导致切换语言后邮件错乱。可以下发 templateId 和参数,客户端按当前语言渲染;对于客服手写邮件,则下发固定文本并标记语言。模板参数要经过转义和格式化,避免道具名、玩家名撑破布局。

附件名称也要跟随本地化。邮件里写“补偿 100 gems”,但背包显示“钻石”,会显得粗糙。邮件模板应引用 itemId,由客户端用道具表渲染名称和图标。这样道具改名时,邮件展示也能一致。

删除、归档和审计

玩家删除邮件时,如果附件未领取,应该二次确认或禁止删除。已领取和无附件邮件可以删除或归档。删除是客户端视图行为,服务端仍可保留审计记录。尤其是补偿邮件,玩家删除后客服仍然需要查到是否发过、是否领过。

归档策略要保护性能。邮箱列表不应无限增长,已读无附件邮件可以 30 天后自动归档,已领取奖励邮件保留一段时间。客户端只拉取最近和未处理邮件,历史记录按需加载。这样大厅打开邮箱不会被几年前的公告拖慢。

与活动和任务系统的关系

邮件经常是活动系统和任务系统的兜底通道。排行榜奖励结算失败,可以补发邮件;任务奖励因为背包满未领取,可以转入邮件;维护补偿通过邮件发放。这种兜底能力很好,但不能滥用。如果所有奖励都通过邮件发,玩家会失去即时反馈。建议只有离线、异步、补偿和异常恢复使用邮件,普通任务奖励仍在任务界面领取。

当奖励从其他系统转入邮件时,要保留来源引用。比如 sourceType: ranked_eventsourceId: spring_arena_04。玩家点开邮件时能看到“春季竞技场排名奖励”,客服也能追到原活动。没有来源的邮件会像凭空出现的账目,后期排查困难。

邮件入口的交互细节

大厅邮箱入口应避免过度抢视觉。强红点表示有附件,弱红点表示未读公告,角标数量只显示未处理邮件,不显示历史总数。点击入口后默认定位到最重要的邮件:可领取优先,其次未读,其次最新。领取后列表不要突然滚动到顶部,保持玩家上下文。

移动端邮箱详情页要避免误触删除。删除按钮不要和领取按钮相邻,未领取附件邮件删除前必须二次确认。附件多时,详情页可以先展示汇总,再展开明细。UI 稳定比装饰动画更重要,因为邮箱承载的是玩家资产。

领取成功后的背包刷新也要即时,否则玩家会误以为附件没有到账。

如果背包页已经打开,应同步刷新数量和排序,避免显示旧缓存。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页