返回博客
Tutorial2 分钟阅读

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'
-- )

准备好开始了吗?

在我们的商店获取脚本,或加入 Discord 获取支持、更新以及新功能预告。