定位:行会共享仓库,归属行会 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. 关键业务规则
- 权限按行会职位掩码控制,会长可在GuildMgr配置每个职位的权限
- 行会仓库物品不支持上锁;玩家锁定物品禁止存入
- 限时物品存入行会仓库倒计时继续跑,到期扫描逻辑和背包一致,单独写行会仓库过期扫描
- 存入来源是玩家背包,取出目标是玩家背包;不能行会仓库直接转给个人仓库
- 扩容两种模式:消耗会长背包道具 / 扣除行会资金
- 懒加载:行会仓库不是行会创建就加载,第一个成员打开才进内存
- 定时tick统一刷脏数据,避免高频DB
- 完整流水记录,后台运营可以查谁存、谁取
8. 可选扩展
- 行会仓库取出冷却(防止恶意刷)
- 取出审批模式:取出物品需要会长审批
- 行会仓库物品过期扫描
- 行会仓库GM指令