FiveM邮件系统脚本:游戏内消息与账单
设置 FiveM 邮件系统,用于账单、工作备注和服务器公告。顶级脚本、框架支持及将邮件集成到工作中的思路。
Agency Scripts
Agency Scripts 创始人兼首席开发者
角色扮演中实体邮件的价值
在大多数FiveM服务器完全依赖手机通知和数字消息的时代,实体邮件和信件系统增加了一层独特的沉浸感。玩家可以手写信件给其他角色,发送包含物品的包裹,接收官方政府通知,收到商家的账单,甚至发现神秘匿名便条。这创造了数字通信无法复制的角色扮演机会。留在某人邮箱中的威胁信比短信更具分量。手写情书比电子邮件更具个人感。邮寄的法院传票更显正式。除了角色扮演价值,邮件系统为邮政工作人员提供了自然的工作机会,他们负责收集、分类和投递全城邮件。本教程涵盖从数据库架构到投递机制的完整邮件系统构建。
邮件系统的数据库架构
数据库需处理信件、包裹、邮箱分配和投递状态。每件邮件包含发件人标识、收件人标识、区分信件与包裹的类型、主题行、信件正文、包裹附带物品数据、创建与投递时间戳,以及邮件状态标志(待处理、运输中、已投递、已阅读)。邮箱绑定到房产地址,拥有或租赁住房的玩家拥有个人邮箱。城市内的公共邮箱服务无房玩家,需前往邮局领取邮件。架构还应支持系统生成邮件,用于自动通知如账单、法院文件、工作申请和政府公告,脚本可触发,无需玩家发件人。
-- SQL schema
CREATE TABLE IF NOT EXISTS mail_items (
id INT AUTO_INCREMENT PRIMARY KEY,
sender_id VARCHAR(64),
sender_name VARCHAR(64) DEFAULT 'Unknown',
recipient_id VARCHAR(64) NOT NULL,
mail_type ENUM('letter','package','notice') DEFAULT 'letter',
subject VARCHAR(128) NOT NULL,
body TEXT,
attachments JSON DEFAULT '[]',
postage_paid INT DEFAULT 0,
status ENUM('pending','transit','delivered','read','returned')
DEFAULT 'pending',
mailbox_id INT DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
delivered_at TIMESTAMP NULL,
read_at TIMESTAMP NULL,
INDEX idx_recipient (recipient_id, status),
INDEX idx_status (status)
);
CREATE TABLE IF NOT EXISTS mailboxes (
id INT AUTO_INCREMENT PRIMARY KEY,
owner_id VARCHAR(64),
property_id INT DEFAULT NULL,
location_x FLOAT NOT NULL,
location_y FLOAT NOT NULL,
location_z FLOAT NOT NULL,
box_type ENUM('residential','public','business') DEFAULT 'residential',
capacity INT DEFAULT 20,
UNIQUE KEY unique_owner (owner_id)
);
编写和发送信件
写信界面应如同撰写真实信件。向玩家展示类似纸张的NUI,包含收件人姓名、主题行和自由文本正文字段。NUI中使用手写风格字体,强化实体信件的感觉。收件人字段应通过姓名搜索角色数据库,而非要求玩家知道内部标识符。玩家写完点击发送时,客户端将数据发送至服务器,服务器验证内容,检查发件人是否有足够邮资,扣除费用,并将邮件项以待处理状态插入数据库。对正文长度施加合理限制,标准信件约500字符,高级邮资可达1000字符,防止滥用同时允许有意义的信息。信件随后进入投递流程,等待邮递员处理或自动投递计时器到期。
-- server/mail.lua
local Config = {
postageCost = 50,
premiumPostage = 150,
maxBodyLength = 500,
premiumMaxBody = 1000,
autoDeliverTime = 900, -- 15 minutes if no postal worker
packagePostage = 200,
}
RegisterNetEvent('mail:send', function(data)
local src = source
local sender = GetPlayerIdentifier(src, 0)
-- Validate recipient exists
local recipient = MySQL.single.await([[
SELECT identifier, CONCAT(firstname,' ',lastname) as name
FROM characters WHERE CONCAT(firstname,' ',lastname) LIKE ?
LIMIT 1
]], {'%' .. data.recipientName .. '%'})
if not recipient then
TriggerClientEvent('mail:notify', src, 'Recipient not found.')
return
end
-- Check postage funds
local cost = data.premium and Config.premiumPostage or Config.postageCost
local maxLen = data.premium and Config.premiumMaxBody or Config.maxBodyLength
if #data.body > maxLen then
TriggerClientEvent('mail:notify', src, 'Letter exceeds maximum length.')
return
end
-- Deduct postage (framework-specific money removal)
local paid = exports['framework']:RemoveMoney(src, cost, 'cash')
if not paid then
TriggerClientEvent('mail:notify', src, 'Not enough cash for postage.')
return
end
-- Insert mail
MySQL.insert([[
INSERT INTO mail_items
(sender_id, sender_name, recipient_id, mail_type,
subject, body, postage_paid, status)
VALUES (?, ?, ?, 'letter', ?, ?, ?, 'pending')
]], {
sender, data.senderName, recipient.identifier,
data.subject, data.body, cost
})
TriggerClientEvent('mail:notify', src, 'Letter sent! Postage: $' .. cost)
end)
带物品附件的包裹系统
包裹系统将邮件系统扩展到文本之外,允许玩家相互发送实物物品。玩家访问邮局,从库存中选择要包含在包裹中的物品,写一条可选留言,并支付包裹邮费。服务器从发送者库存中移除物品,并将其作为 JSON 数据块存储在邮件物品的附件列中。当收件人在邮箱或邮局打开包裹时,服务器反序列化附件数据,并将每件物品添加回收件人库存,首先检查是否有足够空间。如果收件人库存已满,包裹将保留在其邮箱中,直到他们腾出空间。这为向离线玩家发送物品提供了合法途径,并为关怀包裹、证据投递和礼物交换等角色扮演打开了新玩法。在服务器端验证所有物品数据以防止复制漏洞,并记录每笔包裹交易以供管理员审核。
-- server/packages.lua
RegisterNetEvent('mail:sendPackage', function(data)
local src = source
local sender = GetPlayerIdentifier(src, 0)
-- Validate items exist in sender inventory
local validItems = {}
for _, item in ipairs(data.items) do
local hasItem = exports['ox_inventory']:Search(src, 'count', item.name)
if hasItem < item.count then
TriggerClientEvent('mail:notify', src,
'You do not have enough ' .. item.name)
return
end
table.insert(validItems, {
name = item.name,
count = item.count,
meta = item.metadata or {}
})
end
-- Remove items from sender
for _, item in ipairs(validItems) do
exports['ox_inventory']:RemoveItem(src, item.name, item.count)
end
-- Deduct postage
local paid = exports['framework']:RemoveMoney(
src, Config.packagePostage, 'cash')
if not paid then
-- Refund items if payment fails
for _, item in ipairs(validItems) do
exports['ox_inventory']:AddItem(src, item.name, item.count)
end
return
end
MySQL.insert([[
INSERT INTO mail_items
(sender_id, sender_name, recipient_id, mail_type,
subject, body, attachments, postage_paid, status)
VALUES (?, ?, ?, 'package', ?, ?, ?, ?, 'pending')
]], {
sender, data.senderName, data.recipientId,
data.subject or 'Package',
data.note or '',
json.encode(validItems),
Config.packagePostage
})
end)
邮政工作人员职位及配送路线
邮递员工作将邮件投递从后台过程转变为主动的角色扮演内容。玩家在邮局打卡后会获得一辆投递货车和一条待投递邮件路线。路线通过查询所有待投递邮件,按投递区域分组,并创建一个最小化行驶距离的有序路径点序列生成。路线上的每个停靠点都会在地图上放置一个标记和目的邮箱的标记。当邮递员到达标记并按下交互键时,邮件状态从待投递变为已投递,并可在收件人邮箱中领取。邮递员获得基本工资加每单奖金,形成稳定收入来源。对于没有活跃邮递员的服务器,实现一个自动投递计时器,在配置的延迟后将待投递邮件状态转为已投递,确保邮件最终送达,即使无人执行邮递路线。
邮箱交互和阅读界面
当玩家接近邮箱并交互时,服务器查询所有寄给他们且状态为已送达或已读的邮件。NUI 显示收件箱样式列表,展示每封邮件的发件人名、主题、类型图标和时间戳。未读信件带有高亮或徽章指示。点击信件以全视图模式打开,样式如纸质信件,顶部显示发件人和日期,正文居中,带关闭按钮。包裹视图显示附带便条和包含物品列表,带“打开包裹”按钮,将物品转移至玩家库存。打开时标记信件为已读,配置保留期后从邮箱移除以防数据库膨胀。添加回复按钮,预填收件人为原发件人,方便往返通信。系统通知采用不同样式,带官方信头以区分玩家邮件。
系统通知和自动邮件
除了玩家间通信,邮件系统成为其他服务器脚本发送沉浸式通知的强大工具。脚本可以发送正式邮件,而非单调的弹出通知。法院系统发送官方传票,带有政府信头。企业发送发票和付款提醒。物业管理脚本发送租金到期通知和驱逐警告。派系系统发送招募信或任务简报。创建一个简单的服务器导出,任何资源都可调用以排队系统邮件项,无需了解邮件系统内部。导出接受收件人标识、主题、正文、邮件类型及可选附件。这使您的邮件系统成为一个通用通知层,感觉融入游戏世界,而非破坏沉浸感的侵入式UI覆盖。
-- server/exports.lua
-- Universal mail export for other resources
function SendSystemMail(recipientId, subject, body, mailType, attachments)
mailType = mailType or 'notice'
attachments = attachments or '[]'
if type(attachments) == 'table' then
attachments = json.encode(attachments)
end
MySQL.insert([[
INSERT INTO mail_items
(sender_id, sender_name, recipient_id, mail_type,
subject, body, attachments, status)
VALUES ('system', 'City of Los Santos', ?, ?, ?, ?, ?, 'pending')
]], {recipientId, mailType, subject, body, attachments})
end
exports('SendSystemMail', SendSystemMail)
-- Usage from another resource:
-- exports['mailsystem']:SendSystemMail(
-- playerId, 'Rent Due',
-- 'Your rent of $2,500 is due in 3 days.',
-- 'notice'
-- )