公开 API
通过 Java 或 Kotlin 发送、查询和领取系统邮件
公开 API
包名:
SystemMail API 面向其他服务端插件。所有数据库操作都通过 CompletionStage<T> 异步返回,不占用 Bukkit 主线程。
获取服务
Kotlin:
Java:
也可以通过 Bukkit ServicesManager 获取:
MailId 与 AttachmentId 是公开数据类。Java 可直接使用
服务方法
| 方法 | 业务结果 | 说明 |
|---|---|---|
send(request) | DeliveryReceipt | 发送一封系统邮件 |
sendDurable(request) | DeliveryReceipt | 恢复持久化的邮件发送意图 |
getMail(playerId, mailId) | MailView? | 查询玩家自己的单封邮件 |
getInbox(playerId, query) | List<MailView> | 查询玩家收件箱 |
getUnreadCount(playerId) | Int | 查询未读数量 |
markRead(playerId, mailId) | MailView? | 标记已读并返回详情 |
claim(playerId, mailId) | ClaimResult | 领取邮件的全部可领附件 |
delete(playerId, mailId) | DeleteResult | 删除没有受保护附件的邮件 |
表中的业务结果均包在 CompletionStage<T> 中。
异步发送与线程边界
send 和 sendDurable 返回后,收件人计划、收件关系、附件状态与通知事件会由
SystemMail 工作线程分批写入数据库。大型群发的完成耗时表示后台持久化完成所需时间
调用方不得在 Bukkit 主线程对返回值调用 join()、get() 等阻塞等待
方法,应使用 thenAccept、whenComplete 等回调继续处理结果。否则调用方会主动把整个
异步发送耗时变成主线程停顿。
运行时空结果
SystemMail 只会在启动验证和业务初始化完成后注册公开服务。如果已经取得的服务实例后来
遇到运行时业务门关闭,调用会静默返回中性结果,不写数据库、不发包也不发放奖励:查询
返回 null、空列表或 0,删除返回 NOT_FOUND,领取返回空的
NOTHING_CLAIMABLE。
send 与 sendDurable 的中性结果是保留空回执:mailId 为全零 UUID、
recipientCount=0、duplicate=true。它不代表投递成功;调用方必须保留自己的待投递
记录,稍后使用原 idempotencyKey 重试:
发送邮件
Kotlin
Java
请求字段
| Builder 方法 | 是否必填 | 说明 |
|---|---|---|
idempotencyKey(value) | 是 | 同一业务动作保持不变的去重标识 |
recipient(uuid) / recipients(uuids) | 是 | 一个或多个收件人 |
title(value) | 是 | 邮件标题 |
content(value) | 否 | 正文,默认空字符串;多行使用 \n |
senderName(value) | 否 | 发件人,默认“系统邮件” |
senderAvatar(value) | 否 | AXUI 头像路径,默认 "" |
item(value) / items(values) | 否 | Bukkit ItemStack 附件 |
currency(...) | 否 | 货币附件 |
command(...) | 否 | 服务端命令附件 |
sourcePlugin(value) | 是 | 调用方的稳定插件标识 |
deliverAt(value) | 否 | 投递时间,默认当前时间 |
expireAt(value) | 否 | 过期时间,默认永不过期;必须晚于投递时间 |
API 中 SystemMailRequest.content 和 MailView.content 始终是包含换行符的 String。只有 AXUI 的 detail.texts 会转换为 List<String>。
从持久待发送记录恢复
普通业务使用 send(request)。只有调用方已经把完整邮件内容保存到自己的数据库,并需要在宕机或依赖故障后恢复发送时,才使用 sendDurable(request)。
两者使用相同的收件人、附件、命令白名单、过期时间和重复发送校验。区别只有一个:普通 send 不接受早于当前时间 5 分钟以上的 deliverAt,sendDurable 则允许按调用方原本保存的历史投递时间恢复。
调用方必须在第一次发送前保存并在重放时原样还原:
sourcePlugin与idempotencyKey;- 收件人、标题、正文、发件人和头像;
- 所有物品、货币与命令附件;
deliverAt与expireAt。
以下是调用流程伪代码,intent 与 pendingStore 由调用插件自行实现:
不要更换去重标识重试,也不要用 sendDurable 随意创建历史邮件。只有取得成功回执后,才能把调用方自己的待发送记录标记为完成。原 expireAt 不会自动顺延;邮件已经过期时,玩家仍然无法查看或领取。
重复发送保护
idempotencyKey 用于防止奖励重复投递,不是随手填写的备注。对于同一个业务动作,它必须保持稳定,例如订单号、任务实例 ID,或“活动 + 日期 + 玩家 UUID”。
- 唯一范围是区分大小写的
(sourcePlugin, idempotencyKey); - 来源、key 和规范化请求完全相同:返回原
mailId,duplicate=true; - 如果另一台子服仍在分批构建原邮件,重试会立即返回已经保存的
mailId;原任务或后续恢复任务会继续完成构建; - 相同来源与 key 对应不同请求:直接失败,提醒调用方误用了 key;
- 超时或结果不明时,必须使用原 key 重试,不要生成新 key。
请求比较包含收件人、标题、正文、发件人、头像、物品、货币、命令、附件 icon、来源以及投递/过期时间。
构建期间邮件处于 BUILDING,不会出现在收件箱、未读数或通知检查中。全部批次和数量核对完成后,才会切换为 ACTIVE。进程中断后,SystemMail 会在启动维护或使用相同去重标识重试时继续构建,不会产生只有部分玩家可见的邮件,也不会重复创建附件状态。
货币附件
provider 必须与 ArcartXLinkManager.getEconomyProvider(provider) 的注册标识一致,这个id可以通过AX的插件源码得到,详情可自行查看AX代码仓库。
displayName:玩家看到的货币名称;未填写时读取AX配置中的显示名称;icon:AXUI 贴图路径,默认"",只影响显示。
命令附件
命令只会在同一封邮件的全部物品和货币附件完成后执行。displayTitle 与 icon 用于玩家界面,真实命令不会下发客户端。
支持:
<player>:玩家名;<uuid>:玩家 UUID;<mail_id>:邮件 UUID。
命令必须命中 mail.yml -> commands.allowed-prefixes。默认空列表会拒绝所有命令附件。一般使用 CONSOLE;只有确实需要玩家权限和上下文时才使用 PLAYER。
查询与领取
hasAvailableAttachments=true 只匹配至少还有一个 AVAILABLE 附件的邮件。
领取 API 玩家物品空槽不足时,ClaimResult.message 为 INVENTORY_FULL,货币和命令也不会先执行。
delete 成功后,该玩家再使用原 mailId 调用 getMail 或 markRead 会得到 null。
还有未领取、处理中或待管理员确认附件时返回 PROTECTED_ATTACHMENTS,不会删除邮件。
公开 API 严格使用邮件原始过期时间。mail.yml -> limits.ui-claim-expiry-grace-minutes 只保护玩家当前 UI 会话,不适用于 SystemMailService.claim。
AttachmentView.state 可能为:
事件
| 事件 | 时机 |
|---|---|
SystemMailPreSendEvent | 发送前,可取消;可能在异步线程触发 |
SystemMailDeliveredEvent | 在线玩家的投递事件被处理时 |
SystemMailAttachmentClaimedEvent | 附件状态已提交为 CLAIMED 后 |
SystemMailAttachmentQuarantinedEvent | 外部副作用结果无法确认并进入隔离后 |
事件只适合做扩展和观察,不应作为资产事实来源。监听 SystemMailPreSendEvent 时先检查线程,不要无条件调用仅限 Bukkit 主线程的 API。
异常处理
CompletionStage 可能异常完成。调用方应记录完整异常,并根据业务决定何时重试。
对于 send,超时不能直接理解为“邮件没有创建”。使用原 idempotencyKey 重试,SystemMail 会返回已经创建的邮件,或只创建一次新邮件。
