个人邮件和全局邮件实现方案

在游戏服务端开发中,个人邮件和全服邮件最大的区别是发送范围不同,因此存储和投递方式也不同。

一般来说:

  • 个人邮件:直接写入玩家的个人邮箱,通常持久化到数据库。
  • 全服邮件:通常采用 全局存储 + 玩家登录或查询时懒加载 的方式,避免一次性给所有玩家插入邮件。

下面从实际服务端开发的角度介绍。

个人邮件怎么实现?

个人邮件比较简单,例如:

  • 玩家完成任务后收到奖励邮件。
  • 系统向某个玩家发送补偿邮件。
  • 玩家之间发送邮件。
  • 排行榜结算时向指定玩家发放奖励。

1. 数据库设计

假设使用 MySQL,可以设计一张个人邮件表:

CREATE TABLE player_mail (
    id BIGINT PRIMARY KEY,
    player_id BIGINT NOT NULL,
    title VARCHAR(255),
    content TEXT,
    attachments JSON,
    is_read BOOLEAN DEFAULT FALSE,
    is_claimed BOOLEAN DEFAULT FALSE,
    create_time DATETIME,
    expire_time DATETIME,
    INDEX idx_player_id (player_id)
);

其中:

  • id:邮件唯一 ID。
  • player_id:接收邮件的玩家。
  • attachments:邮件附件,例如金币、钻石、道具。
  • is_read:是否阅读。
  • is_claimed:附件是否领取。

2. 发送流程

例如,给玩家 10001 发送一封邮件。

游戏业务模块
    |
    | 发送邮件请求
    v
邮件服务 MailService
    |
    | 构造邮件数据
    v
MySQL 持久化
    |
    | 玩家在线?
    |
    +---- 是 ---> 通知玩家有新邮件
    |
    +---- 否 ---> 不需要立即通知

玩家上线后,可以通过数据库查询:

SELECT *
FROM player_mail
WHERE player_id = 10001
  AND expire_time > NOW()
ORDER BY create_time DESC;

这样就可以获取玩家的邮件列表。

需要注意:玩家不在线,也不影响邮件投递。

因为邮件已经持久化到了数据库中。


全服邮件怎么实现?

全服邮件才是值得重点讨论的地方。

例如:

游戏有 100 万玩家,运营需要给所有玩家发送一封补偿邮件。

最直观的方式就是:

for (auto& player : allPlayers) {
    SendMail(player.id, mail);
}

这样做虽然简单,但是存在明显问题。

如果游戏有 100 万玩家,就需要:

  • 查询 100 万个玩家。
  • 生成 100 万条邮件数据。
  • 向数据库插入 100 万条记录。
  • 可能还需要触发大量通知。

对于大型游戏,这样做会产生较大的数据库写入压力。

因此,全服邮件通常会采用另一种设计。

1. 全局邮件表

单独建立一张全服邮件表:

CREATE TABLE global_mail (
    id BIGINT PRIMARY KEY,
    title VARCHAR(255),
    content TEXT,
    attachments JSON,
    create_time DATETIME,
    expire_time DATETIME
);

当运营发送全服邮件时,只需要向这张表插入一条记录。

例如:

id title content attachments
1001 停服补偿 感谢大家的支持 100钻石
1002 版本更新 新版本已经上线 200金币

这样,不管游戏有 10 万玩家还是 100 万玩家,发送一封全服邮件都只需要保存一份邮件内容。

2. 玩家怎么收到全服邮件?

采用懒加载机制。

当玩家登录游戏,或者打开邮箱时:

玩家登录
    |
    v
加载个人邮件
    |
    v
查询当前有效的全服邮件
    |
    v
筛选玩家有资格接收的邮件
    |
    v
判断哪些邮件尚未领取或处理
    |
    v
合并个人邮件与全服邮件
    |
    v
返回客户端

例如:

vector<Mail> GetPlayerMails(int64_t playerId) {
    // 1. 获取个人邮件
    auto personalMails = GetPersonalMails(playerId);

    // 2. 获取有效的全服邮件
    auto globalMails = GetValidGlobalMails();

    // 3. 获取玩家对全服邮件的处理状态
    auto states = GetGlobalMailStates(playerId);

    // 4. 合并玩家有资格查看的邮件
    return MergeMails(personalMails, globalMails, states);
}

这里需要特别注意:

懒加载不一定意味着真的把全服邮件插入个人邮件表。

实际上有两种方案。

  • 延迟投递:发送时只保存模板,玩家登录后再创建个人邮件。减少发送时的集中写入,但仍然需要为登录玩家创建邮件记录。
  • 查询时合并:发送时只保存全服邮件,玩家查询时动态合并,只有状态发生变化时才保存个人状态。通常存储开销最低,但查询和状态管理逻辑更复杂。

怎么判断全服邮件是否已经领取?

这里是邮件系统的一个关键问题。

假设全服邮件 ID 为 1001。

玩家 A 领取了附件,玩家 B 没有领取。

由于全服邮件只有一份,因此不能直接修改 global_mail 中的领取状态。

领取状态必须与玩家关联。

可以设计一张状态表:

CREATE TABLE player_global_mail_state (
    player_id BIGINT NOT NULL,
    mail_id BIGINT NOT NULL,
    is_read BOOLEAN DEFAULT FALSE,
    is_claimed BOOLEAN DEFAULT FALSE,
    PRIMARY KEY (player_id, mail_id)
);

例如:

player_id mail_id is_read is_claimed
10001 1001 1 1
10002 1001 1 0
10003 1001 0 0

这样就能够知道:

  • 玩家 10001 已经领取奖励。
  • 玩家 10002 已经阅读,但没有领取。
  • 玩家 10003 还没有阅读。

而全服邮件的内容仍然只保存一份。

通常只需要为玩家已经产生状态变化的邮件保存记录;没有状态记录的邮件可以视为未读、未领取。


邮件附件怎么避免重复领取?

这也是实际开发中非常重要的一点。

假设玩家点击领取 100 钻石。

如果客户端因为网络超时重复发送领取请求:

第一次请求:领取100钻石
第二次请求:领取100钻石
第三次请求:领取100钻石

如果服务端没有做好处理,就可能出现重复发奖。

因此,邮件附件领取必须保证幂等性。

一种实现方式是:

玩家请求领取附件
      |
      v
校验邮件是否存在、是否过期
      |
      v
检查附件是否已领取
      |
      +---- 已领取 ---> 返回已领取
      |
      v
执行原子领取流程
      |
      +---- 发放道具
      |
      +---- 标记邮件已领取
      |
      v
返回成功

这里必须强调:

不能简单地先判断 is_claimed == false,然后发放奖励。

因为两个并发请求可能同时通过检查。

如果玩家背包和邮件状态位于同一个数据库,可以通过事务、条件更新和唯一约束实现原子领取。

如果背包系统与邮件系统是两个独立服务,则需要通过唯一的奖励发放流水号实现幂等,并结合可靠消息或补偿机制处理跨服务失败。


实际开发中还需要考虑什么?

除了个人邮件和全服邮件的基本实现,还需要考虑:

  1. 邮件过期:定时清理过期邮件,或者查询时直接过滤。
  2. 新玩家是否接收历史邮件:通常需要根据角色创建时间、邮件发放时间和运营规则判断。
  3. 分区分服:全服邮件究竟是当前区服所有玩家,还是整个游戏所有玩家。
  4. 邮件数量限制:例如个人邮箱最多保存 100 封邮件。
  5. 离线邮件:玩家离线期间依然能够正常接收邮件。
  6. 全服邮件缓存:可以将有效全服邮件缓存在 Redis 或服务端内存中,降低数据库查询压力。
  7. 批量领取:需要保证每封邮件独立幂等,避免重复领取。
  8. 运营邮件撤回:撤回时需要考虑附件是否已经被玩家领取。
posted @ 2026-11-05 12:28  光風霽月  阅读(8)  评论(0)    收藏  举报