行会仓库 GuildStoreMgr(Lua,和 StoreMgr / BagMgr 容器接口对齐)

定位:行会共享仓库,归属行会 guild_id,不是个人pid;行会成员有分级权限(查看/存入/取出/整理);独立配表扩容;物品存入取出流水、可查日志;容器API和前面背包、个人仓库保持一致,方便复用通用物品工具。 依赖:GuildMgr、ConfigMgr、DbMgr、LogMgr、EventMgr、BagMgr

1. Proto协议

ini 复制代码
// 打开行会仓库
message C2S_OpenGuildStore {}
message S2C_GuildStoreInfo {
    int32 max_grid = 1;
    repeated ItemData item_list = 2;
    int32 perm_mask = 3; // 权限掩码:1查看 2存入 4取出 8整理
}

// 行会仓库存入物品
message C2S_GuildStoreAddItem {
    uint64 item_uid = 1;
    int32 add_num = 2;
}
// 行会仓库取出物品
message C2S_GuildStoreTakeItem {
    uint64 item_uid = 1;
    int32 take_num = 2;
}
// 行会仓库整理
message C2S_GuildStoreSort {}
// 行会仓库扩容(会长/副会长权限)
message C2S_GuildStoreExpand {
    int32 unlock_type = 1; // 2道具 3行会资金
}

message S2C_GuildStoreOpRet {
    int32 ret_code = 1;
    string msg = 2;
    uint64 item_uid = 3;
    int32 left_num = 4;
}

2. guild_store_const.lua

ini 复制代码
local const = {
    RET_OK = 0,
    RET_NO_GUILD = 4001,
    RET_PERM_DENY = 4002,
    RET_ITEM_NOT_EXIST = 4003,
    RET_NOT_ENOUGH_NUM = 4004,
    RET_GRID_FULL = 4005,
    RET_ITEM_LOCK = 4006,
    RET_CANNOT_GUILD_STORE = 4007, -- 配置禁止存入行会仓库
    RET_RESOURCE_NOT_ENOUGH = 4008,
    RET_ALREADY_MAX = 4009,
    RET_PARAM_ERR = 4010,

    -- 权限掩码
    PERM_VIEW = 1,
    PERM_ADD = 2,
    PERM_TAKE = 4,
    PERM_SORT = 8,

    UNLOCK_TYPE_ITEM = 2,
    UNLOCK_TYPE_GUILD_FUND = 3,
}
return const

3. 数据库表

sql 复制代码
-- 行会仓库扩展信息(格子、扩容进度)
CREATE TABLE `guild_store_ext` (
  `guild_id` BIGINT PRIMARY KEY,
  `max_grid` INT DEFAULT 24,
  `last_expand_id` INT DEFAULT 0,
  `dirty` TINYINT DEFAULT 0
) ENGINE=InnoDB;

-- 行会仓库物品
CREATE TABLE `guild_store_item` (
  `id` BIGINT AUTO_INCREMENT PRIMARY KEY,
  `guild_id` BIGINT NOT NULL,
  `item_uid` BIGINT NOT NULL UNIQUE,
  `item_id` INT NOT NULL,
  `num` INT NOT NULL,
  `grid_idx` INT NOT NULL,
  `is_lock` TINYINT DEFAULT 0,
  `expire_time` BIGINT DEFAULT 0,
  `attr_data` TEXT,
  KEY idx_guild (guild_id)
) ENGINE=InnoDB;

-- 行会仓库操作流水
CREATE TABLE `guild_store_flow` (
  `id` BIGINT AUTO_INCREMENT PRIMARY KEY,
  `guild_id` BIGINT NOT NULL,
  `pid` BIGINT NOT NULL,
  `op_type` INT NOT NULL, --1存入 2取出 3整理 4扩容
  `item_uid` BIGINT,
  `item_id` INT,
  `num` INT,
  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
  KEY idx_guild (guild_id)
) ENGINE=InnoDB;

4. 配表 guild_store_expand.csv

id grid_total unlock_type need_item_id need_item_num need_fund remark
1 30 3 0 0 50000 行会资金扩容到30格
2 36 2 10012 2 0 行会扩容券
全局上限:global_guild_store_max = 48

5. GuildStoreMgr 完整代码 guild_store_mgr.lua

ini 复制代码
local const = require("guild_store_const")
local ConfigMgr = require("config_mgr")
local DbMgr = require("db_mgr")
local LogMgr = require("log_mgr")
local EventMgr = require("event_mgr")
local BagMgr = require("bag_mgr")
local GuildMgr = require("guild_mgr")

local M = {}
local guild_store_cache = {} -- guild_id => {container, ext, dirty}

-- 和个人仓库复用相同容器结构
local function NewGuildStoreContainer(guild_id, max_grid)
    local container = {
        guild_id = guild_id,
        max_grid = max_grid,
        item_map = {},
        grid_map = {},
    }

    function container:GetItemByUid(uid)
        return self.item_map[uid]
    end
    function container:GetItemByGrid(grid_idx)
        local uid = self.grid_map[grid_idx]
        if not uid then return nil end
        return self.item_map[uid]
    end
    function container:CheckCanAddItem(item_cfg, add_num)
        if item_cfg.stack_max > 1 then
            for _, item in pairs(self.item_map) do
                if item.item_id == item_cfg.item_id and item.num + add_num <= item_cfg.stack_max then
                    return true
                end
            end
        end
        for i = 1, self.max_grid do
            if not self.grid_map[i] then
                return true
            end
        end
        return false
    end
    function container:AddItem(item_id, num, src_item)
        local cfg = ConfigMgr.GetItemCfg(item_id)
        if not cfg then return false end
        if cfg.stack_max > 1 then
            for _, item in pairs(self.item_map) do
                if item.item_id == item_id and item.num + num <= cfg.stack_max then
                    item.num = item.num + num
                    return true, item.item_uid
                end
            end
        end
        local empty_grid
        for i = 1, self.max_grid do
            if not self.grid_map[i] then
                empty_grid = i
                break
            end
        end
        if not empty_grid then return false end
        local new_item = {
            item_uid = M.GenItemUid(),
            item_id = item_id,
            num = num,
            grid_idx = empty_grid,
            is_lock = false, -- 行会仓库物品统一不支持锁定
            expire_time = src_item and src_item.expire_time or 0,
            attr_data = src_item and src_item.attr_data or "",
        }
        self.item_map[new_item.item_uid] = new_item
        self.grid_map[empty_grid] = new_item.item_uid
        return true, new_item.item_uid
    end
    function container:SubItemNum(item_uid, sub_num)
        local item = self.item_map[item_uid]
        if not item or item.num < sub_num then
            return false, item and item.num or 0
        end
        item.num = item.num - sub_num
        if item.num <= 0 then
            self.item_map[item_uid] = nil
            self.grid_map[item.grid_idx] = nil
        end
        return true, item.num
    end
    function container:AddItemNum(item_uid, add_num)
        local item = self.item_map[item_uid]
        if not item then return false end
        item.num = item.num + add_num
        return true
    end
    function container:Sort()
        local item_list = {}
        for _, it in pairs(self.item_map) do
            table.insert(item_list, it)
        end
        self.grid_map = {}
        self.item_map = {}
        local group = {}
        for _, it in ipairs(item_list) do
            local cfg = ConfigMgr.GetItemCfg(it.item_id)
            if cfg.stack_max > 1 then
                if not group[it.item_id] then
                    group[it.item_id] = {num=0, data=it}
                end
                group[it.item_id].num = group[it.item_id].num + it.num
            else
                table.insert(group, it)
            end
        end
        local new_list = {}
        for _,v in pairs(group) do
            if v.data then
                table.insert(new_list, v)
            end
        end
        local idx = 1
        for _, it in ipairs(new_list) do
            if idx > self.max_grid then break end
            it.grid_idx = idx
            self.item_map[it.item_uid] = it
            self.grid_map[idx] = it.item_uid
            idx = idx + 1
        end
        return true
    end
    -- 推送给单个玩家
    function container:SyncToPlayer(player, perm_mask)
        local list = {}
        for _,it in pairs(self.item_map) do
            table.insert(list, {
                item_uid = it.item_uid,
                item_id = it.item_id,
                num = it.num,
                grid_idx = it.grid_idx,
                is_lock = it.is_lock,
                expire_time = it.expire_time,
            })
        end
        player:SendMsg("S2C_GuildStoreInfo", {
            max_grid = self.max_grid,
            item_list = list,
            perm_mask = perm_mask
        })
    end
    return container
end

-- 加载行会仓库(懒加载,有人打开才加载)
function M.LoadGuildStore(guild_id)
    if guild_store_cache[guild_id] then
        return guild_store_cache[guild_id]
    end
    local ext = DbMgr.QueryOne("SELECT * FROM guild_store_ext WHERE guild_id=?", guild_id)
    if not ext then
        ext = {
            guild_id = guild_id,
            max_grid = 24,
            last_expand_id = 0,
            dirty = 0,
        }
        DbMgr.Insert("guild_store_ext", ext)
    end
    local rows = DbMgr.Query("SELECT * FROM guild_store_item WHERE guild_id=?", guild_id)
    local container = NewGuildStoreContainer(guild_id, ext.max_grid)
    for _,row in ipairs(rows) do
        local item = {
            item_uid = row.item_uid,
            item_id = row.item_id,
            num = row.num,
            grid_idx = row.grid_idx,
            is_lock = row.is_lock == 1,
            expire_time = row.expire_time,
            attr_data = row.attr_data,
        }
        container.item_map[item.item_uid] = item
        container.grid_map[item.grid_idx] = item.item_uid
    end
    local cache_data = {
        container = container,
        ext = ext,
        dirty = false
    }
    guild_store_cache[guild_id] = cache_data
    return cache_data
end

-- 获取成员权限掩码,对接GuildMgr职位权限配置
function M.GetMemberPerm(player)
    local guild = GuildMgr.GetPlayerGuild(player.pid)
    if not guild then return 0 end
    -- GuildMgr返回职位权限:perm_cfg 配置每个职位掩码
    local perm = GuildMgr.GetGuildPosPerm(guild.guild_id, player.guild_pos)
    return perm
end

-- 存入物品:背包 → 行会仓库
function M.AddItem(player, item_uid, add_num)
    local perm = M.GetMemberPerm(player)
    if bit.band(perm, const.PERM_ADD) == 0 then
        return const.RET_PERM_DENY, "没有存入权限", 0
    end
    local guild = GuildMgr.GetPlayerGuild(player.pid)
    if not guild then return const.RET_NO_GUILD, "未加入行会", 0 end
    local store_data = M.LoadGuildStore(guild.guild_id)
    local bag_container = BagMgr.GetBagContainer(player)
    local src_item = bag_container:GetItemByUid(item_uid)
    if not src_item then
        return const.RET_ITEM_NOT_EXIST, "背包物品不存在", 0
    end
    local cfg = ConfigMgr.GetItemCfg(src_item.item_id)
    if not cfg then return const.RET_PARAM_ERR, "物品配置不存在", 0 end
    if cfg.cannot_guild_store == 1 then
        return const.RET_CANNOT_GUILD_STORE, "该物品不能存入行会仓库", src_item.num
    end
    if src_item.is_lock then
        return const.RET_ITEM_LOCK, "锁定物品不能存入行会仓库", src_item.num
    end
    if src_item.num < add_num then
        return const.RET_NOT_ENOUGH_NUM, "物品数量不足", src_item.num
    end
    if not store_data.container:CheckCanAddItem(cfg, add_num) then
        return const.RET_GRID_FULL, "行会仓库格子已满", src_item.num
    end
    -- 扣背包
    local sub_ok, left_bag = bag_container:SubItemNum(item_uid, add_num)
    if not sub_ok then
        return const.RET_NOT_ENOUGH_NUM, "扣除背包物品失败", src_item.num
    end
    -- 加入行会仓库
    local add_ok, new_uid = store_data.container:AddItem(src_item.item_id, add_num, src_item)
    if not add_ok then
        bag_container:AddItemNum(item_uid, add_num)
        return const.RET_GRID_FULL, "行会仓库添加失败", left_bag
    end
    store_data.dirty = true
    -- 流水
    DbMgr.Insert("guild_store_flow", {
        guild_id = guild.guild_id,
        pid = player.pid,
        op_type = 1,
        item_uid = new_uid,
        item_id = src_item.item_id,
        num = add_num,
    })
    EventMgr:Fire("EVT_GUILD_STORE_ADD", player, guild.guild_id, src_item.item_id, add_num)
    -- 同步:存入成功后,只给当前玩家刷新仓库UI;全员广播可按需加
    store_data.container:SyncToPlayer(player, perm)
    bag_container:SyncToClient(player)
    return const.RET_OK, "存入成功", left_bag
end

-- 取出物品:行会仓库 → 背包
function M.TakeItem(player, item_uid, take_num)
    local perm = M.GetMemberPerm(player)
    if bit.band(perm, const.PERM_TAKE) == 0 then
        return const.RET_PERM_DENY, "没有取出权限", 0
    end
    local guild = GuildMgr.GetPlayerGuild(player.pid)
    if not guild then return const.RET_NO_GUILD, "未加入行会", 0 end
    local store_data = M.LoadGuildStore(guild.guild_id)
    local store_item = store_data.container:GetItemByUid(item_uid)
    if not store_item then
        return const.RET_ITEM_NOT_EXIST, "仓库物品不存在", 0
    end
    local cfg = ConfigMgr.GetItemCfg(store_item.item_id)
    if not cfg then return const.RET_PARAM_ERR, "配置错误", 0 end
    if store_item.num < take_num then
        return const.RET_NOT_ENOUGH_NUM, "仓库物品数量不足", store_item.num
    end
    -- 校验背包能否放下 + 负重
    local bag = BagMgr.GetBagContainer(player)
    if not bag:CheckCanAddItem(cfg, take_num) then
        return const.RET_GRID_FULL, "背包空间不足", store_item.num
    end
    if BagMgr.CheckAddWeight(player, cfg.weight * take_num) == false then
        return const.RET_GRID_FULL, "负重不足", store_item.num
    end
    -- 扣仓库
    local sub_ok, left_store = store_data.container:SubItemNum(item_uid, take_num)
    if not sub_ok then
        return const.RET_NOT_ENOUGH_NUM, "扣除仓库物品失败", store_item.num
    end
    -- 加回背包
    local add_ok, _ = bag:AddItem(store_item.item_id, take_num, store_item)
    if not add_ok then
        store_data.container:AddItemNum(item_uid, take_num)
        return const.RET_GRID_FULL, "放入背包失败", left_store
    end
    store_data.dirty = true
    DbMgr.Insert("guild_store_flow", {
        guild_id = guild.guild_id,
        pid = player.pid,
        op_type = 2,
        item_uid = item_uid,
        item_id = store_item.item_id,
        num = take_num,
    })
    EventMgr:Fire("EVT_GUILD_STORE_TAKE", player, guild.guild_id, store_item.item_id, take_num)
    store_data.container:SyncToPlayer(player, perm)
    bag:SyncToClient(player)
    return const.RET_OK, "取出成功", left_store
end

-- 整理
function M.Sort(player)
    local perm = M.GetMemberPerm(player)
    if bit.band(perm, const.PERM_SORT) == 0 then
        return const.RET_PERM_DENY, "没有整理权限"
    end
    local guild = GuildMgr.GetPlayerGuild(player.pid)
    if not guild then return const.RET_NO_GUILD, "未加入行会" end
    local store_data = M.LoadGuildStore(guild.guild_id)
    store_data.container:Sort()
    store_data.dirty = true
    DbMgr.Insert("guild_store_flow", {
        guild_id = guild.guild_id,
        pid = player.pid,
        op_type = 3,
    })
    store_data.container:SyncToPlayer(player, perm)
    return const.RET_OK, "整理成功"
end

-- 行会仓库扩容(会长/副会长权限)
function M.Expand(player, unlock_type)
    local perm = M.GetMemberPerm(player)
    -- 扩容权限单独控制,一般只有会长副会长
    if not GuildMgr.IsGuildLeaderOrVice(player) then
        return const.RET_PERM_DENY, "只有会长、副会长可以扩容"
    end
    local guild = GuildMgr.GetPlayerGuild(player.pid)
    if not guild then return const.RET_NO_GUILD, "未加入行会" end
    local store_data = M.LoadGuildStore(guild.guild_id)
    local ext = store_data.ext
    local all_cfg = ConfigMgr:GetAllGuildStoreExpandCfg()
    local next_cfg = all_cfg[ext.last_expand_id + 1]
    local global_max = ConfigMgr:GetGlobalCfg("global_guild_store_max") or 48
    if not next_cfg or next_cfg.grid_total > global_max then
        return const.RET_ALREADY_MAX, "行会仓库已达上限", ext.max_grid
    end
    if next_cfg.unlock_type ~= unlock_type then
        return const.RET_PARAM_ERR, "解锁类型不匹配", ext.max_grid
    end
    -- 扣费:行会资金 / 消耗会长背包道具
    if unlock_type == const.UNLOCK_TYPE_ITEM then
        local have = BagMgr.GetItemCount(player, next_cfg.need_item_id)
        if have < next_cfg.need_item_num then
            return const.RET_RESOURCE_NOT_ENOUGH, "扩容道具不足", ext.max_grid
        end
        local ok = BagMgr.ConsumeItem(player, next_cfg.need_item_id, next_cfg.need_item_num, "guild_store_expand")
        if not ok then
            return const.RET_RESOURCE_NOT_ENOUGH, "扣除道具失败", ext.max_grid
        end
    elseif unlock_type == const.UNLOCK_TYPE_GUILD_FUND then
        if guild.fund < next_cfg.need_fund then
            return const.RET_RESOURCE_NOT_ENOUGH, "行会资金不足", ext.max_grid
        end
        GuildMgr.CostGuildFund(guild.guild_id, next_cfg.need_fund, "guild_store_expand")
    end
    local old_max = ext.max_grid
    ext.last_expand_id = next_cfg.id
    ext.max_grid = next_cfg.grid_total
    store_data.container.max_grid = next_cfg.grid_total
    store_data.dirty = true
    DbMgr.Insert("guild_store_flow", {
        guild_id = guild.guild_id,
        pid = player.pid,
        op_type = 4,
        num = next_cfg.grid_total
    })
    EventMgr:Fire("EVT_GUILD_STORE_EXPAND", guild.guild_id, old_max, ext.max_grid)
    store_data.container:SyncToPlayer(player, perm)
    return const.RET_OK, "扩容成功", ext.max_grid
end

-- 打开仓库入口
function M.Open(player)
    local perm = M.GetMemberPerm(player)
    if bit.band(perm, const.PERM_VIEW) == 0 then
        player:SendMsg("S2C_GuildStoreInfo", {perm_mask=0})
        return const.RET_PERM_DENY, "没有查看权限"
    end
    local guild = GuildMgr.GetPlayerGuild(player.pid)
    local store_data = M.LoadGuildStore(guild.guild_id)
    store_data.container:SyncToPlayer(player, perm)
    return const.RET_OK, ""
end

-- 保存单个行会仓库
function M.SaveGuildStore(guild_id)
    local store_data = guild_store_cache[guild_id]
    if not store_data or not store_data.dirty then return end
    local ext = store_data.ext
    DbMgr.Update("guild_store_ext", {max_grid=ext.max_grid, last_expand_id=ext.last_expand_id}, "guild_id=?", guild_id)
    DbMgr.Delete("guild_store_item", "guild_id=?", guild_id)
    for _,it in pairs(store_data.container.item_map) do
        DbMgr.Insert("guild_store_item", {
            guild_id = guild_id,
            item_uid = it.item_uid,
            item_id = it.item_id,
            num = it.num,
            grid_idx = it.grid_idx,
            is_lock = it.is_lock and 1 or 0,
            expire_time = it.expire_time,
            attr_data = it.attr_data
        })
    end
    store_data.dirty = false
end

-- 定时落地:定时遍历缓存脏行会仓库保存
function M.TickSave()
    for guild_id, store_data in pairs(guild_store_cache) do
        if store_data.dirty then
            M.SaveGuildStore(guild_id)
        end
    end
end

-- 行会解散回调:清空缓存、清理数据(可选,也可以保留物品)
function M.OnGuildDismiss(guild_id)
    M.SaveGuildStore(guild_id)
    guild_store_cache[guild_id] = nil
    -- 业务选择:行会解散,物品回收/邮件返还给成员,这里只做缓存清理
end

function M.GenItemUid()
    return os.time() * 100000 + math.random(1,99999)
end

return M

6. Handler注册

scss 复制代码
local GuildStoreMgr = require("guild_store_mgr")
local const = require("guild_store_const")

function OnC2S_OpenGuildStore(player)
    GuildStoreMgr.Open(player)
end
function OnC2S_GuildStoreAddItem(player, msg)
    local ret, tips, left = GuildStoreMgr.AddItem(player, msg.item_uid, msg.add_num)
    player:SendMsg("S2C_GuildStoreOpRet", {ret_code=ret, msg=tips, item_uid=msg.item_uid, left_num=left})
end
function OnC2S_GuildStoreTakeItem(player, msg)
    local ret, tips, left = GuildStoreMgr.TakeItem(player, msg.item_uid, msg.take_num)
    player:SendMsg("S2C_GuildStoreOpRet", {ret_code=ret, msg=tips, item_uid=msg.item_uid, left_num=left})
end
function OnC2S_GuildStoreSort(player)
    local ret, tips = GuildStoreMgr.Sort(player)
    player:SendMsg("S2C_GuildStoreOpRet", {ret_code=ret, msg=tips})
end
function OnC2S_GuildStoreExpand(player, msg)
    local ret, tips, new_grid = GuildStoreMgr.Expand(player, msg.unlock_type)
    player:SendMsg("S2C_GuildStoreOpRet", {ret_code=ret, msg=tips})
end

RegisterProtoHandler("C2S_OpenGuildStore", OnC2S_OpenGuildStore)
RegisterProtoHandler("C2S_GuildStoreAddItem", OnC2S_GuildStoreAddItem)
RegisterProtoHandler("C2S_GuildStoreTakeItem", OnC2S_GuildStoreTakeItem)
RegisterProtoHandler("C2S_GuildStoreSort", OnC2S_GuildStoreSort)
RegisterProtoHandler("C2S_GuildStoreExpand", OnC2S_GuildStoreExpand)

-- 订阅行会解散事件
EventMgr:Subscribe("EVT_GUILD_DISMISS", function(guild_id)
    GuildStoreMgr.OnGuildDismiss(guild_id)
end)

7. 关键业务规则

  1. 权限按行会职位掩码控制,会长可在GuildMgr配置每个职位的权限
  2. 行会仓库物品不支持上锁;玩家锁定物品禁止存入
  3. 限时物品存入行会仓库倒计时继续跑,到期扫描逻辑和背包一致,单独写行会仓库过期扫描
  4. 存入来源是玩家背包,取出目标是玩家背包;不能行会仓库直接转给个人仓库
  5. 扩容两种模式:消耗会长背包道具 / 扣除行会资金
  6. 懒加载:行会仓库不是行会创建就加载,第一个成员打开才进内存
  7. 定时tick统一刷脏数据,避免高频DB
  8. 完整流水记录,后台运营可以查谁存、谁取

8. 可选扩展

  • 行会仓库取出冷却(防止恶意刷)
  • 取出审批模式:取出物品需要会长审批
  • 行会仓库物品过期扫描
  • 行会仓库GM指令
相关推荐
站大爷IP1 小时前
Python的文本IO把我坑惨了,原来read和readlines的区别这么大
后端
没事写点啥1 小时前
WeKnora 上下文管理
后端·ai编程
爱勇宝1 小时前
AI 都这么强了,咱们程序员以后还能干点啥?
前端·后端·程序员
用户244159813761 小时前
《深入理解C语言》系列 · 第05篇:内存与程序运行机制
后端
嘿嘿嘿9151 小时前
从跟课到脱稿重写:一个文字格斗游戏里踩过的 9 个坑
后端
旋律4141 小时前
List、Set、Map:三个装数据的容器,到底啥区别?怎么遍历?
java·后端
房西杭1 小时前
StoreMgr 仓库管理器
后端
兔子零10241 小时前
架构师最危险的能力:用正确的技术,做出错误的架构决策
前端·javascript·后端
ytadpole1 小时前
模板下载性能优化记录
后端·性能优化