3
0

5 Commits 6459c48ca5 ... 040fc27a0e

Autor SHA1 Mensagem Data
  gitxsm 040fc27a0e 七日盛典-定制活动不再清除前一次选择的道具 há 5 dias atrás
  gitxsm c6231b2fee 商店数据改为分段发送 há 6 dias atrás
  gitxsm 6e3784a018 部分活动礼包改为可批量购买 há 6 dias atrás
  pigflower 96c8ceabae Merge branch '360test' of http://43.226.57.217:3000/yishanyou/GongFuServer into 360test há 1 semana atrás
  pigflower 188faff41e web功能优化 há 1 semana atrás

+ 3 - 0
script/common/Lang.lua

@@ -481,6 +481,7 @@ TOMORROW_OPEN = [[暂未开启]]
 
 ABS_ITEM_ERR = [[道具不足]]
 ABS_SNOWBALL_ERR = [[所有礼包已激活]]
+ABS_GIFT_BUY_LIMIT = [[礼包已达可购买上限]]
 ABS_SNOWDRESS_NEED_ERR = [[需要先装扮前面的]]
 
 CHAT_UNION_YQSUCESS = [[邀请成功]]
@@ -816,6 +817,8 @@ MUST_SELECT_ONE = [[请至少选择一个材料]]
 NUM_EXCEED = [[数量超出]]
 
 
+VOUCHER_NOT_ENOUGH = [[代金券不足]]
+
 ---------------诸神圣域---------------
 DATA_ERR = [[数据异常]]
 DATA_OLD = [[请获取最新数据后再操作]]

+ 2 - 0
script/common/LogDefine.lua

@@ -324,6 +324,8 @@ DEFINE = {
 	Artifacts				= 784,			-- 英雄神威灵装
 	BattleGift				= 785,			-- 闯关礼金
 	RecycleItem				= 786,			-- 收纳箱
+	AnniversaryVoucherWheel = 787,			-- 周年庆活动-代金券转盘
+	AnniversaryActiveWheel  = 788,			-- 周年庆活动-活跃度转盘
 
 	errHandle				= 99999,		-- 异常处理
 }

+ 31 - 28
script/module/absAct/BusOneActivityBuy.lua

@@ -96,25 +96,25 @@ end
 function BusOneBuy_Buy(human, nID)
     local tConf = tBuyConf[nID]
     if not tConf then
-        print("[BusOneBuy_Buy] 不存在对应的配置 返回 nID = "..nID)
+        -- print("[BusOneBuy_Buy] 不存在对应的配置 返回 nID = "..nID)
         return
     end
 
     if tConf.nType == BUSONEBUY_TYPE_MONEY then
-        print("[BusOneBuy_Buy] 配置对应的类型不正确 返回 nID = "..nID.." nType = "..tConf.nType)
+        -- print("[BusOneBuy_Buy] 配置对应的类型不正确 返回 nID = "..nID.." nType = "..tConf.nType)
         return
     end
 
     local nNowBuyNum = BusOneBuy_GetBuyNum(human, nID)
     if nNowBuyNum >= tConf.nBuyNum then
-        print("[BusOneBuy_Buy] 当前购买的次数大于可购买的次数 返回 nID = "..nID.." nType = "
-                ..tConf.nType.." nNowBuyNum = "..nNowBuyNum)
+        -- print("[BusOneBuy_Buy] 当前购买的次数大于可购买的次数 返回 nID = "..nID.." nType = "
+        --         ..tConf.nType.." nNowBuyNum = "..nNowBuyNum)
         return
     end
 
     if not ObjHuman.checkRMB(human, tConf.nPrize) then
-        print("[BusOneBuy_Buy] 当前购买的金币不足 返回 nID = "..nID.." nType = "
-            ..tConf.nType.." nNowBuyNum = "..nNowBuyNum)
+        -- print("[BusOneBuy_Buy] 当前购买的金币不足 返回 nID = "..nID.." nType = "
+        --     ..tConf.nType.." nNowBuyNum = "..nNowBuyNum)
 		return
 	end
 	
@@ -128,7 +128,8 @@ function BusOneBuy_Buy(human, nID)
     BusOneBuy_Query(human)
 end
 
-function BusOneBuy_Get(human)
+function BusOneBuy_Get(human, itemMul)
+    itemMul = itemMul or 1
     local tItem = {}
     for nID, v in pairs(tBuyConf) do
         local nStatus = BusOneBuy_GetBuyStatus(human, nID)
@@ -138,7 +139,7 @@ function BusOneBuy_Get(human)
                     tItem[tData[1]] = 0
                 end
 
-                tItem[tData[1]] = tItem[tData[1]] + tData[2]
+                tItem[tData[1]] = tItem[tData[1]] + tData[2] * itemMul
             end
 
             local nNowBuyNum = BusOneBuy_GetBuyNum(human, nID)
@@ -164,14 +165,14 @@ end
 
 -----------------------------------外部调用-----------------------------
 function isOpen(human, YYInfo, funcConfig)
-    print("[BusOneActivityBuy_isOpen] 进入判断")
+    -- print("[BusOneActivityBuy_isOpen] 进入判断")
     local state, endTime, startTime = AbsActLogic.isStarted(human, funcConfig and funcConfig.funcID or BUSONEBUYABSID)
     if not state then 
         print("[BusOneActivityBuy_isOpen] 当前活动未开启")
         return 
     end
 
-    print("[BusOneActivityBuy_isOpen] 进入判断 endTime = "..endTime.." startTime = "..startTime)
+    -- print("[BusOneActivityBuy_isOpen] 进入判断 endTime = "..endTime.." startTime = "..startTime)
     return true, endTime, startTime
 end
 
@@ -193,38 +194,40 @@ function isRed(human, YYInfo, absActConfig)
 end
 
 function onCharge(human, price, funcID, buyID, buyNum)
-    print("[BusOneActivityBuy_onCharge] 进入购买 buyID = "..buyID)
+    -- print("[BusOneActivityBuy_onCharge] 进入购买 buyID = "..buyID)
     local bRed = false
     for nID, v in pairs(tBuyConf) do
         if buyID == v.nBuyID then
             local nStatus = BusOneBuy_GetBuyStatus(human, nID)
             if nStatus == CommonDefine.COMMON_PRIZE_STATE_NOGET then
                 BusOneBuy_SetBuyStatus(human, nID, CommonDefine.COMMON_PRIZE_STATE_CANGET)
-                BusOneBuy_SetBuyNum(human, nID, 1)
+                -- BusOneBuy_SetBuyNum(human, nID, 1)
+                BusOneBuy_SetBuyNum(human, nID, buyNum)
                 bRed = true
             end
         end
     end
 
     if true == bRed then
-        print("[BusOneActivityBuy_onCharge] 进入发送数据 buyID = "..buyID)
-        BusOneBuy_Get(human)
+        -- print("[BusOneActivityBuy_onCharge] 进入发送数据 buyID = "..buyID)
+        BusOneBuy_Get(human, buyNum)
         YunYingLogic.sendBanner(human)
         BusOneActivityTask.BusOneTask_SendRed(human)
     end
 end
 
--- function GetRemainNum(human, nBuyID)
---     for nID, v in pairs(tBuyConf) do
---         if nBuyID == v.nBuyID then
---             local nBuyNum = BusOneBuy_GetBuyNum(human, nID)
---             if v.nBuyNum > nBuyNum then
---                 return 1
---             else
---                 return 0
---             end
---         end
---     end
-
---     return 0
--- end
+function GetRemainNum(human, nBuyID)
+    for nID, v in pairs(tBuyConf) do
+        if nBuyID == v.nBuyID then
+            local nBuyNum = BusOneBuy_GetBuyNum(human, nID)
+            if v.nBuyNum > nBuyNum then
+                -- return 1
+                return v.nBuyNum - nBuyNum
+            else
+                return 0
+            end
+        end
+    end
+
+    return 0
+end

+ 35 - 35
script/module/absAct/PremiumGiftLogic.lua

@@ -100,15 +100,15 @@ function premiumGift(human, id, buyConf, isFirst, cnt, buyNum)
         return
     end
 
-    -- if nowBuyCnt + buyNum > config.cnt then
-    --     Broadcast.sendErr(human, Lang.HERO_BAG_BUY_CAP_NO_CNT)
-    --     return
-    -- end
+    if nowBuyCnt + buyNum > config.cnt then
+        Broadcast.sendErr(human, Lang.HERO_BAG_BUY_CAP_NO_CNT)
+        return
+    end
 
     -- 当金币购买之后超过最大金币时,不允许购买
     for j = 1, #config.reward do
         local itemID = config.reward[j][1]
-        local itemCnt = config.reward[j][2]
+        local itemCnt = config.reward[j][2] * buyNum
         if itemID == ItemDefine.ITEM_JINBI_ID then
             if not ObjHuman.canAddJinbi(human, itemCnt) then
                 return
@@ -117,19 +117,19 @@ function premiumGift(human, id, buyConf, isFirst, cnt, buyNum)
     end
 
     -- -- 增加已购买次数
-    -- human.db.absAct[id].premiumCnt[premiumID] = nowBuyCnt + buyNum
-    -- -- 发放物品
-    -- local items = { }
-    -- for _, v in ipairs(AbsActExcel.premiumGift[premiumID].reward) do
-    --     table.insert(items, {v[1], v[2]*buyNum})
-    -- end
-    -- BagLogic.addItemList(human, items, "premium_reward")
+    human.db.absAct[id].premiumCnt[premiumID] = nowBuyCnt + buyNum
+    -- 发放物品
+    local items = { }
+    for _, v in ipairs(AbsActExcel.premiumGift[premiumID].reward) do
+        table.insert(items, {v[1], v[2]*buyNum})
+    end
+    BagLogic.addItemList(human, items, "premium_reward")
 
     -- 增加已购买次数
-    human.db.absAct[id].premiumCnt[premiumID] = nowBuyCnt + 1
-    -- 发放物品
-    -- local items = { }
-    BagLogic.addItemList(human, AbsActExcel.premiumGift[premiumID].reward, "premium_reward")
+    -- human.db.absAct[id].premiumCnt[premiumID] = nowBuyCnt + 1
+    -- -- 发放物品
+    -- -- local items = { }
+    -- BagLogic.addItemList(human, AbsActExcel.premiumGift[premiumID].reward, "premium_reward")
 
     Broadcast.sendErr(human, Lang.ITEM_BUY_SUCCESS)
     AbsActLogic.actDetailQuery(human, id)
@@ -248,22 +248,22 @@ function giftBuy(human, id, premiumID)
     AbsActLogic.actDetailQuery(human, id)
 end
 
--- function GetRemainNum(human, nBuyID)
---     local premiumID = ABS_PREMIUM_GIFT_LIST_BUYID[nBuyID]
---     if not premiumID then
---         return 0
---     end
-
---     local config = AbsActExcel.premiumGift[premiumID]
---     if not config then return 0 end
-
---     if not human.db.absAct[ABC_PREMIUM_GIFT_ACT_ID].premiumCnt then
---         return 0
---     end
-
---     if not human.db.absAct[ABC_PREMIUM_GIFT_ACT_ID].premiumCnt[premiumID] then
---         return config.cnt
---     else
---         return config.cnt - human.db.absAct[ABC_PREMIUM_GIFT_ACT_ID].premiumCnt[premiumID]
---     end
--- end
+function GetRemainNum(human, nBuyID)
+    local premiumID = ABS_PREMIUM_GIFT_LIST_BUYID[nBuyID]
+    if not premiumID then
+        return 0
+    end
+
+    local config = AbsActExcel.premiumGift[premiumID]
+    if not config then return 0 end
+
+    if not human.db.absAct[ABC_PREMIUM_GIFT_ACT_ID].premiumCnt then
+        return config.cnt
+    end
+
+    if not human.db.absAct[ABC_PREMIUM_GIFT_ACT_ID].premiumCnt[premiumID] then
+        return config.cnt
+    else
+        return config.cnt - human.db.absAct[ABC_PREMIUM_GIFT_ACT_ID].premiumCnt[premiumID]
+    end
+end

+ 40 - 39
script/module/absAct/SpecialCustomLogic.lua

@@ -238,7 +238,7 @@ function customBuy(human,args,id, bFromPay, buyNum)
         return
     end
 
-    -- buyNum = buyNum or 1
+    buyNum = buyNum or 1
 
     -- 检查来源
     local tConfig = AbsActExcel.custom[args]
@@ -268,14 +268,14 @@ function customBuy(human,args,id, bFromPay, buyNum)
 
     -- 已达到限购次数,返回
     local config = AbsActExcel.custom[args]
-    if absAct.custom[args].cnt >= config.limitCnt then
-        return
-    end
-
-    -- if buyNum + absAct.custom[args].cnt > config.limitCnt then
+    -- if absAct.custom[args].cnt >= config.limitCnt then
     --     return
     -- end
 
+    if buyNum + absAct.custom[args].cnt > config.limitCnt then
+        return
+    end
+
     -- 钻石价格不为0时,表示钻石购买,则需判断钻石是否足够
     if config.price ~= 0 then
 	    if not ObjHuman.checkRMB(human, config.price) then
@@ -285,8 +285,8 @@ function customBuy(human,args,id, bFromPay, buyNum)
     end
 
     -- 购买次数增加一次
-    absAct.custom[args].cnt = absAct.custom[args].cnt + 1
-    -- absAct.custom[args].cnt = absAct.custom[args].cnt + buyNum
+    -- absAct.custom[args].cnt = absAct.custom[args].cnt + 1
+    absAct.custom[args].cnt = absAct.custom[args].cnt + buyNum
 
      -- 状态设置为已卖完,此时,为防止后续报错,导致可无限购买,故先设置为无法继续购买
     absAct.custom[args].state = 2
@@ -294,19 +294,20 @@ function customBuy(human,args,id, bFromPay, buyNum)
      -- 统计物品,并发放
     local item = {}
     for i = 1, #config.fixed do
-        item[#item+1] = {config.fixed[i][1],config.fixed[i][2]}
-        -- item[#item+1] = {config.fixed[i][1],config.fixed[i][2] * buyNum}
+        -- item[#item+1] = {config.fixed[i][1],config.fixed[i][2]}
+        item[#item+1] = {config.fixed[i][1],config.fixed[i][2] * buyNum}
     end
     for i = 1,#absAct.custom[args].selectItem do
-        item[#item+1] = {absAct.custom[args].selectItem[i][1],absAct.custom[args].selectItem[i][2]}
-        -- item[#item+1] = {absAct.custom[args].selectItem[i][1],absAct.custom[args].selectItem[i][2]*buyNum}
+        -- item[#item+1] = {absAct.custom[args].selectItem[i][1],absAct.custom[args].selectItem[i][2]}
+        item[#item+1] = {absAct.custom[args].selectItem[i][1],absAct.custom[args].selectItem[i][2]*buyNum}
     end
     BagLogic.addItemList(human, item, "abs_custom")   
 
      -- 判断是否达到限购次数,未达到则将购买状态设为选择物品
     if absAct.custom[args].cnt < config.limitCnt then 
         --absAct.custom[args].selectItem = {}
-        absAct.custom[args].state = 0
+        -- absAct.custom[args].state = 0
+        absAct.custom[args].state = 1
     else
         absAct.custom[args].state = 2
     end
@@ -348,29 +349,29 @@ function isRed(human, YYInfo, funcConfig)
     return bRed
 end
 
--- function GetRemainNum(human, nBuyID)
---     local tConfig = AbsActExcel.custom
---     local nChoseID, nLimitNum = nil, nil
---     for nID, v in pairs(tConfig) do
---         if v.buyID == nBuyID and nID < SPECIALCISTOMNOBUYID then
---             nChoseID = nID
---             nLimitNum = v.limitCnt
---             break
---         end
---     end
-
---     local tActDBData = human.db.absAct[SPECIALCISTOMACTID]
---     if not tActDBData then
---         return 0
---     end
-
---     if not nChoseID then
---         return 0
---     end
-
---     if tActDBData.custom and tActDBData.custom[nChoseID] and tActDBData.custom[nChoseID].cnt then
---         return tActDBData.custom[nChoseID].cnt >= nLimitNum and 0 or (nLimitNum - tActDBData.custom[nChoseID].cnt)
---     else
---         return nLimitNum
---     end
--- end
+function GetRemainNum(human, nBuyID)
+    local tConfig = AbsActExcel.custom
+    local nChoseID, nLimitNum = nil, nil
+    for nID, v in pairs(tConfig) do
+        if v.buyID == nBuyID and nID < SPECIALCISTOMNOBUYID then
+            nChoseID = nID
+            nLimitNum = v.limitCnt
+            break
+        end
+    end
+
+    local tActDBData = human.db.absAct[SPECIALCISTOMACTID]
+    if not tActDBData then
+        return 0
+    end
+
+    if not nChoseID then
+        return 0
+    end
+
+    if tActDBData.custom and tActDBData.custom[nChoseID] and tActDBData.custom[nChoseID].cnt then
+        return tActDBData.custom[nChoseID].cnt >= nLimitNum and 0 or (nLimitNum - tActDBData.custom[nChoseID].cnt)
+    else
+        return nLimitNum
+    end
+end

+ 24 - 24
script/module/present/OpenServerGiftPackage.lua

@@ -75,14 +75,14 @@ function onCharge(human, nBuyID, nBuNum)
         -- print("[OpenServerGiftPackage_onCharge] 开始发送奖励")
         local nNowCnt = OpenServerGiftPackage_GetNum(human, nChoseID)
         if nNowCnt > 0 then
-            OpenServerGiftPackage_SetNum(human, nChoseID, -1)
-            BagLogic.addItemList(human, tItem, "OpenServerGiftPackage")
-            -- OpenServerGiftPackage_SetNum(human, nChoseID, -nBuNum)
-            -- local tItemList = {}
-            -- for _, v in ipairs(tItem) do
-            --     table.insert(tItemList, {v[1], v[2]*nBuNum})
-            -- end
-            -- BagLogic.addItemList(human, tItemList, "OpenServerGiftPackage")
+            -- OpenServerGiftPackage_SetNum(human, nChoseID, -1)
+            -- BagLogic.addItemList(human, tItem, "OpenServerGiftPackage")
+            OpenServerGiftPackage_SetNum(human, nChoseID, -nBuNum)
+            local tItemList = {}
+            for _, v in ipairs(tItem) do
+                table.insert(tItemList, {v[1], v[2]*nBuNum})
+            end
+            BagLogic.addItemList(human, tItemList, "OpenServerGiftPackage")
 
             OpenServerGiftPackage_Query(human)
         end
@@ -115,19 +115,19 @@ function isOpen(human, YYInfo, funcConfig)
 	if isBaseOpen(human, YYInfo, funcConfig, true) then return true end
 end
 
--- function GetRemainNum(human, nBuyID)
---     local nChoseID = nil
---     for nID, v in ipairs(OpenActExcel.OpenGiftPackage) do
---         if v.buyID == nBuyID then
---             nChoseID = nID
---             break
---         end
---     end
-
---     if not nChoseID then
---         return 0
---     end
-
---     local nNowCnt = OpenServerGiftPackage_GetNum(human, nChoseID)
---     return nNowCnt
--- end
+function GetRemainNum(human, nBuyID)
+    local nChoseID = nil
+    for nID, v in ipairs(OpenActExcel.OpenGiftPackage) do
+        if v.buyID == nBuyID then
+            nChoseID = nID
+            break
+        end
+    end
+
+    if not nChoseID then
+        return 0
+    end
+
+    local nNowCnt = OpenServerGiftPackage_GetNum(human, nChoseID)
+    return nNowCnt
+end

+ 3 - 1
script/module/shop/Proto.lua

@@ -31,13 +31,15 @@ CG_SHOP_QUERY = {
 GC_SHOP_QUERY = {
     {"shopList",        20,    ShopInfo},   -- 商店列表
     {"shopInfo",        1,     ShopInfo},   -- 当前商店信息
-	{"list",            100,   GOODS},
+	{"list",            15,   GOODS},
 	{"refreshItem",     1,     ItemData}, -- 刷新道具ID
 	{"nextRefreshTime",	1,	   "int"}, -- 下次刷新时间
 	{"freeCnt",         1,     "int"}, -- 免费刷新次数
 	{"freeMax",         1,     "int"}, -- 免费刷新上限
 	{"costCnt",         1,     "int"}, -- 钻石刷新次数
 	{"costMax",         1,     "int"}, -- 钻石刷新次数上限
+	{"isStart",      	1,     "byte"}, -- 0-非第一段数据,1- 第一段数据
+	{"isEnd",      		1,     "byte"}, -- 0-非最后一段数据,1- 最后一段数据
 }
 
 -- 刷新物品

+ 83 - 39
script/module/shop/ShopLogic.lua

@@ -166,15 +166,15 @@ function query(human, shopType)
         end
     end
 
-	Grid.makeItem(msgRet.refreshItem,shopTypeConfig.refreshItemID,shopTypeConfig.refreshItemCnt)
+	Grid.makeItem(msgRet.refreshItem, shopTypeConfig.refreshItemID, shopTypeConfig.refreshItemCnt)
 
 	local shopConfig = ShopExcel[shopType]
 	if not shopConfig then return end
 
 	local now = os.time()
 
-    initHumanShopDB(human,shopType)
-    checkFreeCnt(human,shopType)
+    initHumanShopDB(human, shopType)
+    checkFreeCnt(human, shopType)
 
 	msgRet.nextRefreshTime = 0
 
@@ -192,48 +192,92 @@ function query(human, shopType)
 	msgRet.costCnt = getLeftCostCnt(human, shopType)
 	msgRet.costMax = getCostCntMax(human, shopType)
 
+    -- 先把所有商品数据收集到临时表
+    local allGoods = {}
+
     -- 不刷新的没有记录的 走配置
-    local cnt = 0
     if shopTypeConfig.refreshType == ShopDefine.SHOP_REFRESH_TYPE0 then
-       for index, v in pairs(shopConfig) do
-          if not human.db.shop[shopType].goods[index] then
-             local tempConfig = v
-             cnt = cnt + 1
-             Grid.makeItem(msgRet.list[cnt].itemData, tempConfig.itemID, tempConfig.cnt)
-             msgRet.list[cnt].itemIndex = index
-             Grid.makeItem(msgRet.list[cnt].needItem, tempConfig.needItemID, tempConfig.price)
-		     msgRet.list[cnt].maxCanBuy = tempConfig.limitBuyCnt
-		     msgRet.list[cnt].nowBuy = 0
-		     msgRet.list[cnt].zhekou  = tempConfig.zhekou
-		     msgRet.list[cnt].order  = tempConfig.order
-             msgRet.list[cnt].needVipLv = tempConfig.needVipLv
-             msgRet.list[cnt].rare = tempConfig.rare
-             msgRet.list[cnt].limitType = tempConfig.limitType
-             msgRet.list[cnt].bCanChose = tempConfig.isquickbuy and tempConfig.isquickbuy or 0
-             msgRet.list[cnt].bChose = 0
-          end
-       end
+        for index, v in pairs(shopConfig) do
+            if not human.db.shop[shopType].goods[index] then
+                local tempConfig = v
+                allGoods[#allGoods + 1] = {
+                    itemID      = tempConfig.itemID,
+                    itemCnt     = tempConfig.cnt,
+                    itemIndex   = index,
+                    needItemID  = tempConfig.needItemID,
+                    needItemCnt = tempConfig.price,
+                    maxCanBuy   = tempConfig.limitBuyCnt,
+                    nowBuy      = 0,
+                    zhekou      = tempConfig.zhekou,
+                    order       = tempConfig.order,
+                    needVipLv   = tempConfig.needVipLv,
+                    rare        = tempConfig.rare,
+                    limitType   = tempConfig.limitType,
+                    bCanChose   = tempConfig.isquickbuy and tempConfig.isquickbuy or 0,
+                    bChose      = 0,
+                }
+            end
+        end
     end
 
     for _, v in pairs(human.db.shop[shopType].goods) do
         local tempConfig = shopConfig[v.index]
-        cnt = cnt + 1
-        Grid.makeItem(msgRet.list[cnt].itemData, tempConfig.itemID, tempConfig.cnt)
-        msgRet.list[cnt].itemIndex = v.index
-        Grid.makeItem(msgRet.list[cnt].needItem, tempConfig.needItemID, tempConfig.price)
-		msgRet.list[cnt].maxCanBuy = tempConfig.limitBuyCnt
-		msgRet.list[cnt].nowBuy = v.nowBuy
-		msgRet.list[cnt].zhekou  = tempConfig.zhekou
-		msgRet.list[cnt].order  = tempConfig.order
-        msgRet.list[cnt].needVipLv = tempConfig.needVipLv
-        msgRet.list[cnt].rare = tempConfig.rare
-        msgRet.list[cnt].limitType = tempConfig.limitType
-        msgRet.list[cnt].bCanChose = tempConfig.isquickbuy and tempConfig.isquickbuy or 0
-        msgRet.list[cnt].bChose = ShopLogic_GetChose(human, shopType, v.index)
-    end
-
-	msgRet.list[0] = cnt
-	Msg.send(msgRet, human.fd)
+        allGoods[#allGoods + 1] = {
+            itemID      = tempConfig.itemID,
+            itemCnt     = tempConfig.cnt,
+            itemIndex   = v.index,
+            needItemID  = tempConfig.needItemID,
+            needItemCnt = tempConfig.price,
+            maxCanBuy   = tempConfig.limitBuyCnt,
+            nowBuy      = v.nowBuy,
+            zhekou      = tempConfig.zhekou,
+            order       = tempConfig.order,
+            needVipLv   = tempConfig.needVipLv,
+            rare        = tempConfig.rare,
+            limitType   = tempConfig.limitType,
+            bCanChose   = tempConfig.isquickbuy and tempConfig.isquickbuy or 0,
+            bChose      = ShopLogic_GetChose(human, shopType, v.index),
+        }
+    end
+
+    -- 分批发送,每批最多 15 条
+    local BATCH_SIZE = 15
+    local total = #allGoods
+    local totalBatches = math.max(math.ceil(total / BATCH_SIZE), 1)
+
+    for batchIdx = 1, totalBatches do
+        local startIdx = (batchIdx - 1) * BATCH_SIZE + 1
+        local endIdx   = math.min(batchIdx * BATCH_SIZE, total)
+
+        msgRet.isStart = (batchIdx == 1) and 1 or 0
+        msgRet.isEnd   = (batchIdx == totalBatches) and 1 or 0
+
+        -- 非第一批不重复发送商店列表等头部信息
+        if batchIdx > 1 then
+            msgRet.shopList[0] = 0
+        end
+
+        local cnt = 0
+        for i = startIdx, endIdx do
+            cnt = cnt + 1
+            local item = allGoods[i]
+            Grid.makeItem(msgRet.list[cnt].itemData, item.itemID, item.itemCnt)
+            msgRet.list[cnt].itemIndex  = item.itemIndex
+            Grid.makeItem(msgRet.list[cnt].needItem, item.needItemID, item.needItemCnt)
+            msgRet.list[cnt].maxCanBuy  = item.maxCanBuy
+            msgRet.list[cnt].nowBuy     = item.nowBuy
+            msgRet.list[cnt].zhekou     = item.zhekou
+            msgRet.list[cnt].order      = item.order
+            msgRet.list[cnt].needVipLv  = item.needVipLv
+            msgRet.list[cnt].rare       = item.rare
+            msgRet.list[cnt].limitType  = item.limitType
+            msgRet.list[cnt].bCanChose  = item.bCanChose
+            msgRet.list[cnt].bChose     = item.bChose
+        end
+        msgRet.list[0] = cnt
+
+        Msg.send(msgRet, human.fd)
+    end
 end
 
 -- 刷新物品

+ 11 - 11
script/module/voucher/VoucherShopLogic.lua

@@ -375,8 +375,8 @@ end
 
 -- 请求代金券购买商品
 function VoucherShop_BuyItem(human, nBuyID, nBuyNum)
-    if not human or 0 >= nBuyID or nBuyNum <= 0 or nBuyNum >= 100 then
-        return
+    if not human or 0 >= nBuyID or nBuyNum <= 0 or nBuyNum >= 200 then
+        return Broadcast.sendErr(human, Lang.COMMON_ARGUMENT_ERROR)
     end
 
     local tBuyConfig = VoucherShop_GetShopConfig()
@@ -386,27 +386,27 @@ function VoucherShop_BuyItem(human, nBuyID, nBuyNum)
     end
 
     if tBuyConfig[nBuyID] or tInflateConfig[nBuyID] then
-        print("[VoucherShop_BuyItem] 玩家使用代金券购买代金券礼品,直接返回")
+        -- print("[VoucherShop_BuyItem] 玩家使用代金券购买代金券礼品,直接返回")
         VoucherShop_WriteLog(human, "[VoucherShop_BuyItem]", "玩家使用代金券购买代金券礼品,直接返回")
         return
     end
     
     local tTrueBuyConfig = BuyExcel.buy[nBuyID]
     if not tTrueBuyConfig then
-        print("[VoucherShop_BuyItem] 不存在对应的商品信息 nBuyID = "..nBuyID)
-        return
+        -- print("[VoucherShop_BuyItem] 不存在对应的商品信息 nBuyID = "..nBuyID)
+        return Broadcast.sendErr(human, Lang.COMMON_ARGUMENT_ERROR)
     end
 
     local nDelVoucherNum = tTrueBuyConfig.Voucher * nBuyNum
     if 0 >= nDelVoucherNum then
-        print("[VoucherShop_BuyItem] 该商品无法用代金券购买,配置的数量为空 nBuyID = "..nBuyID.." nDelVoucherNum = "..nDelVoucherNum)
-        return
+        -- print("[VoucherShop_BuyItem] 该商品无法用代金券购买,配置的数量为空 nBuyID = "..nBuyID.." nDelVoucherNum = "..nDelVoucherNum)
+        return Broadcast.sendErr(human, Lang.COMMON_COMFIG_ERROR)
     end
 
     local nNowNum = BagLogic.getItemCnt(human, VoucherShopDefine.VOUCHERITME_ID)
     if nNowNum < nDelVoucherNum then
-        print("[VoucherShop_BuyItem] 对应购买代金券不足直接返回 nBuyID = "..nBuyID.." nDelVoucherNum = "..nDelVoucherNum.." nNowNum = "..nNowNum)
-        return
+        -- print("[VoucherShop_BuyItem] 对应购买代金券不足直接返回 nBuyID = "..nBuyID.." nDelVoucherNum = "..nDelVoucherNum.." nNowNum = "..nNowNum)
+        return Broadcast.sendErr(human, Lang.VOUCHER_NOT_ENOUGH)
     end
 
     if tTrueBuyConfig.module and tTrueBuyConfig.module ~= "" then
@@ -415,8 +415,8 @@ function VoucherShop_BuyItem(human, nBuyID, nBuyNum)
             local nTrueNum = tModule.GetRemainNum(human, nBuyID)
 
             if not nTrueNum or nTrueNum < nBuyNum then
-                print("[VoucherShop_BuyItem] 获取不到对应的配置nBuyID = "..nBuyID.." nDelVoucherNum = "..nDelVoucherNum.." nNowNum = "..nNowNum.." nBuyNum = "..nBuyNum)
-                return
+                -- print("[VoucherShop_BuyItem] 获取不到对应的配置nBuyID = "..nBuyID.." nDelVoucherNum = "..nDelVoucherNum.." nNowNum = "..nNowNum.." nBuyNum = "..nBuyNum)
+                return Broadcast.sendErr(human, Lang.ABS_GIFT_BUY_LIMIT)
             end
         end
     end

+ 25 - 19
script/module/zhuanpan/ZhuanpanGift.lua

@@ -6,6 +6,8 @@ local BagLogic = require("bag.BagLogic")
 local Grid = require("bag.Grid")
 local BuyLogic = require("topup.BuyLogic")
 local ZhuanpanGiftConfig = require("excel.zhuanpan").gift
+local Lang = require("common.Lang")
+local Broadcast = require("broadcast.Broadcast")
 
 local LOGTAG = "zhuanpanGift"
 
@@ -53,44 +55,48 @@ function onCharge(human, buyId, buyNum)
 
     local giftCfg = getCfg(buyId)
     if not giftCfg then
-        return
+        return Broadcast.sendErr(human, Lang.COMMON_COMFIG_ERROR)
     end
 
-    if zhuanpanGiftData and (zhuanpanGiftData[buyId] or 0) >= giftCfg.amount then
-        return
-    end
+    -- if zhuanpanGiftData and (zhuanpanGiftData[buyId] or 0) >= giftCfg.amount then
+    --     return
+    -- end
 
     -- if buyNum + (zhuanpanGiftData[buyId] or 0) >= giftCfg.amount then
     --     return
     -- end
 
+    if buyNum + (zhuanpanGiftData and zhuanpanGiftData[buyId] or 0) > giftCfg.amount then
+        return Broadcast.sendErr(human, Lang.ABS_GIFT_BUY_LIMIT)
+    end
+
     local itemArr = {}
     for idx, itemCfg in ipairs(giftCfg.rewards) do
-        itemArr[idx] = {itemCfg[1], itemCfg[2]}
-        -- itemArr[idx] = {itemCfg[1], itemCfg[2] * buyNum}
+        -- itemArr[idx] = {itemCfg[1], itemCfg[2]}
+        itemArr[idx] = {itemCfg[1], itemCfg[2] * buyNum}
     end
 
     BagLogic.addItemList(human, itemArr, LOGTAG)
 
     zhuanpanGiftData = zhuanpanGiftData or {}
-    zhuanpanGiftData[buyId] = (zhuanpanGiftData[buyId] or 0) + 1
-    -- zhuanpanGiftData[buyId] = (zhuanpanGiftData[buyId] or 0) + buyNum
+    -- zhuanpanGiftData[buyId] = (zhuanpanGiftData[buyId] or 0) + 1
+    zhuanpanGiftData[buyId] = (zhuanpanGiftData[buyId] or 0) + buyNum
     human.db.zhuanpanGiftData = zhuanpanGiftData
 
     ZhuanpanGift_Query(human)
 end
 
--- function GetRemainNum(human, nBuyID)
---     local giftCfg = getCfg(nBuyID)
+function GetRemainNum(human, nBuyID)
+    local giftCfg = getCfg(nBuyID)
 
---     if not giftCfg then
---         return 0
---     end
+    if not giftCfg then
+        return 0
+    end
 
---     local zhuanpanGiftData = human.db.zhuanpanGiftData
---     if not zhuanpanGiftData or not zhuanpanGiftData[nBuyID] then
---         return giftCfg.amount
---     end
+    local zhuanpanGiftData = human.db.zhuanpanGiftData
+    if not zhuanpanGiftData or not zhuanpanGiftData[nBuyID] then
+        return giftCfg.amount
+    end
 
---     return (giftCfg.amount > zhuanpanGiftData[nBuyID]) and (giftCfg.amount - zhuanpanGiftData[nBuyID]) or 0
--- end
+    return (giftCfg.amount > zhuanpanGiftData[nBuyID]) and (giftCfg.amount - zhuanpanGiftData[nBuyID]) or 0
+end

+ 166 - 0
webServer/docs/webgame-api.md

@@ -0,0 +1,166 @@
+# WebGame 前端接入接口文档
+
+## 服务器地址
+- **Base URL**: `https://serverkfhero.3ligame.com/api`
+
+---
+
+## 1) 角色列表(通用)
+
+### 请求
+- **GET** `/webGame/getUserRoleList`
+
+### 参数(Query)
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| uid | string | 是 | 用户ID |
+| channel_id | number/string | 否 | 渠道ID(WebGame 传 `25`) |
+
+### 返回示例
+```json
+{
+  "code": 1,
+  "msg": "请求成功",
+  "data": [
+    {
+      "roleId": "10001",
+      "roleName": "张三",
+      "zhandouli": 123456,
+      "serverName": "S1",
+      "serverId": 1,
+      "createTime": "2026-04-20 12:00:00"
+    }
+  ]
+}
+```
+
+---
+
+## 2) 登录跳转 URL(WebGame)
+
+### 请求
+- **POST** `/webgame/loginUrl`
+
+### 参数(JSON Body)
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| user_id | string | 是 | 用户ID |
+| server_id | number/string | 是 | 区服ID |
+| pid | string | 是 | 平台ID |
+| time | number/string | 是 | 时间戳(秒) |
+| sign | string | 是 | 签名 |
+| ext | string | 否 | 透传参数 |
+| client | string | 否 | 客户端类型 |
+| isAdult | number/string | 否 | 防沉迷标识 |
+
+### 返回示例
+```json
+{
+  "code": 1,
+  "msg": "success",
+  "data": {
+    "gameUrl": "https://xxx.xxx.com/webgame/index.html?user_id=...&server_id=...&pid=...&time=...&sign=..."
+  }
+}
+```
+
+---
+
+## 3) 角色查询(WebGame)
+
+### 请求
+- **GET** `/webgame/roleList`
+
+### 参数(Query)
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| user_id | string | 是 | 用户ID |
+| pid | string | 是 | 平台ID |
+| server_id | number/string | 是 | 区服ID |
+| time | number/string | 是 | 时间戳(秒) |
+| sign | string | 是 | 签名 |
+| role_id | string | 否 | 角色ID(不传则返回该服下该账号所有角色) |
+
+### 返回示例
+```json
+{
+  "code": 1,
+  "message": "ok",
+  "data": [
+    {
+      "role_id": "81075519771",
+      "name": "encoded_name",
+      "lv": 7,
+      "sex": "m",
+      "vocation": 0,
+      "createTime": "2026-04-20 12:00:00",
+      "power": 123456
+    }
+  ]
+}
+```
+
+---
+
+## 4) 充值下单(WebGame)
+
+### 请求
+- **POST** `/webgame/createOrder`
+
+### 参数(JSON Body)
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| user_id | string | 是 | 用户ID |
+| server_id | number/string | 是 | 区服ID |
+| role_id | string | 是 | 角色ID |
+| goods_id | string/number | 是 | 商品ID |
+| goods_name | string | 否 | 商品名 |
+| money | string/number | 是 | 金额(元) |
+| extra_info | string | 否 | 透传参数(通常放内部订单号) |
+| time | number/string | 是 | 时间戳(秒) |
+| sign | string | 是 | 签名 |
+
+### 返回示例
+```json
+{
+  "code": 1,
+  "message": "ok"
+}
+```
+
+---
+
+## 5) 支付回调(WebGame)
+
+### 请求
+- **POST** `/webgame/payCallback`
+
+### 参数(JSON Body)
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| user_id | string | 是 | 用户ID |
+| pid | string | 否 | 平台ID |
+| order_id | string | 是 | 平台订单号 |
+| money | string/number | 是 | 金额(元) |
+| time | number/string | 是 | 时间戳(秒) |
+| server_id | number/string | 是 | 区服ID |
+| role_id | string | 是 | 角色ID |
+| extra_info | string | 否 | 透传参数(内部订单号) |
+| sign | string | 是 | 签名 |
+
+### 返回示例(成功)
+```json
+{
+  "code": 1,
+  "message": "ok"
+}
+```
+
+### 返回示例(失败)
+```json
+{
+  "code": 4,
+  "message": "充值失败,请联系客服"
+}
+```
+

+ 2 - 0
webServer/src/channels/factory/ChannelFactory.ts

@@ -18,6 +18,7 @@ import { HuaweiChannelHandler } from "../handlers/HuaweiChannelHandler";
 import { HongKongTaiwanChannelHandler } from "../handlers/HongKongTaiwanChannelHandler";
 import { ZeroOneChannelHandler } from "../handlers/ZeroOneChannelHandler";
 import { QingtianChannelHandler } from "../handlers/QingtianChannelHandler";
+import { WebGameChannelHandler } from "../handlers/WebGameChannelHandler";
 
 
 const logger = require("../../utils/log");
@@ -62,6 +63,7 @@ class ChannelFactory {
     this.registerHandler(22, new ZeroOneChannelHandler()); // 1折渠道
     this.registerHandler(23, new ZeroOneChannelHandler()); // 逍遥浪人
     this.registerHandler(24, new ZeroOneChannelHandler()); // 逍遥浪人50倍返利
+    this.registerHandler(25, new WebGameChannelHandler()); // WebGame
   }
 
   /**

+ 1 - 1
webServer/src/channels/handlers/HongKongTaiwanChannelHandler.ts

@@ -157,7 +157,7 @@ export class HongKongTaiwanChannelHandler implements ChannelHandler {
 
         // 从请求中获取platform,如果没有则尝试从数据中推断
         // 默认使用android
-        const platformType = platform || 'android';
+        const platformType = extrasParams == "" ? "ios" : 'android';
         const platformConfig = this.getPlatformConfig(platformType, config);
 
         const callbackKey = platformConfig.callbackKey;

+ 723 - 0
webServer/src/channels/handlers/WebGameChannelHandler.ts

@@ -0,0 +1,723 @@
+import {Context} from "koa";
+import * as crypto from 'crypto';
+
+import {ChannelHandler, LoginResult, PaymentResult} from "../interfaces/ChannelHandler";
+import {ChannelConfig} from "../../config/channelConfig";
+import {PaymentHelper} from "../../utils/PaymentHelper";
+import {setWebgameAuthInfo} from "../../utils/webgameAuthCache";
+import {formatDate, getClientIp} from "../../utils/common";
+import {WEBGAME_APP_ID, WEBGAME_REPORT_URL} from "../../config/thirdParams";
+import {getRoleInfoByUidAndServerId} from "../../mongo/mongodb";
+import {query} from "../../sql/query";
+
+const logger = require("../../utils/log");
+const User = require("../../model/UserModel");
+const axios = require("axios");
+
+const DEFAULT_GATE_DOMAIN = "https://mind.yishanyou.com/webgame/index.html";
+
+export class WebGameChannelHandler implements ChannelHandler {
+
+    private toIntOrZero(v: any): number {
+        if (v === undefined || v === null || v === '') return 0;
+        const n = Number(v);
+        return Number.isFinite(n) ? Math.trunc(n) : 0;
+    }
+
+    // 上报登录/支付事件(fire-and-forget,不阻塞主流程)
+    // 上报地址由 WEBGAME_REPORT_URL 配置,格式:POST /api/v2/webgame/log (application/x-www-form-urlencoded)
+    private reportEvent(logType: 'login' | 'payment', params: Record<string, any>, config: ChannelConfig): void {
+        const reportUrl = WEBGAME_REPORT_URL;
+        if (!reportUrl) return;
+
+        const loginKey = config.loginConfig?.signKey ?? '';
+        const timestamp = String(Math.floor(Date.now() / 1000));
+        const userId = String(params.user_id ?? '');
+        const gameId = String(WEBGAME_APP_ID);
+        const sign = crypto.createHash('md5').update(userId + gameId + timestamp + loginKey).digest('hex').toLowerCase();
+
+        const payload: Record<string, string> = {
+            user_id: userId,
+            game_id: gameId,
+            sign,
+            timestamp,
+            server_id: String(params.server_id ?? ''),
+            log_type: logType,
+        };
+        if (params.log_data) payload.log_data = params.log_data;
+        if (params.ip_addr)  payload.ip_addr  = String(params.ip_addr);
+
+        axios.post(reportUrl + '/api/v2/webgame/log', new URLSearchParams(payload).toString(), {
+            headers: {'Content-Type': 'application/x-www-form-urlencoded'},
+            timeout: 5000,
+        }).then((res: any) => {
+            logger.info('WebGame 事件上报成功', {logType, result: res.data});
+        }).catch((err: any) => {
+            logger.warn('WebGame 事件上报失败', {logType, error: err?.message});
+        });
+    }
+
+    async handleLogin(ctx: Context,config:ChannelConfig): Promise<LoginResult> {
+        const data = ctx.request.body as Record<string, unknown>;
+        const uid = data.uid;
+        // token 仅透传,不做校验
+        if (uid === undefined || uid === null) {
+            return {code: 0, msg: "缺少必要参数: uid"};
+        }
+
+        const uidStr = String(uid);
+        let uidDecoded: string;
+        try {
+            uidDecoded = decodeURIComponent(uidStr.replace(/\+/g, "%20"));
+        } catch {
+            uidDecoded = uidStr;
+        }
+
+        const platformStr = data.platform == null ? "" : String(data.platform);
+
+        // 登录成功后写入 accounts / account_login_logs(参考 checkUserToken)
+        try {
+            const ip = getClientIp(ctx);
+            const create_time = formatDate(new Date());
+
+            const channel_id = config.channelId;
+            const device_no = (data as any).device_no ?? "";
+            const reg_device = (data as any).reg_device ?? "";
+            const device_type = this.toIntOrZero((data as any).device_type);
+            const device_model = (data as any).device_model ?? "";
+            const device_version = (data as any).device_version ?? "";
+            const system_version = (data as any).system_version ?? "";
+
+            const accountInfo = (await User.checkAccountIsExist(uidDecoded, channel_id))[0];
+            if (!accountInfo) {
+                const accountRes = await User.createAccount(
+                    uidDecoded,
+                    channel_id,
+                    ip,
+                    device_no,
+                    reg_device,
+                    create_time,
+                    platformStr
+                );
+
+                if (!accountRes || accountRes.affectedRows <= 0) {
+                    logger.error("WebGame 登录落库失败: 添加账户失败", {uid: uidDecoded, channel_id});
+                    return {code: 0, msg: "添加账户失败"};
+                }
+            }
+
+            const logRes = await User.logAccountLogin(
+                uidDecoded,
+                ip,
+                device_type,
+                device_no,
+                device_model,
+                device_version,
+                system_version,
+                create_time,
+                channel_id,
+                platformStr
+            );
+
+            if (!logRes || logRes.affectedRows <= 0) {
+                logger.error("WebGame 登录落库失败: 添加登录日志失败", {uid: uidDecoded, channel_id});
+                return {code: 0, msg: "添加日志失败"};
+            }
+        } catch (e: any) {
+            logger.error("WebGame 登录落库异常", {msg: e?.message, stack: e?.stack});
+            return {code: 0, msg: "登录落库异常"};
+        }
+
+        // 登录成功后打点上报
+        this.reportEvent('login', {
+            user_id: uidDecoded,
+            game_id: WEBGAME_APP_ID,
+            server_id: (data as any).server_id ?? '',
+            ip_addr: getClientIp(ctx),
+        }, config);
+
+        return {
+            code: 1,
+            msg: "success",
+            data: {
+                uid: uidDecoded,
+            },
+        };
+    }
+
+    /**
+     * 联运 Web 游戏登录:生成带签名的游戏入口 URL。
+     * 签名 md5(uid+platform+gkey+skey+time+is_adult[+exts]+'#'+lkey),缺省字段在签名中按空串拼接;uid 为 urldecode 后的值参与签名。
+     * 必填:uid、platform、time、back_url、type;gkey、skey、is_adult、exts 可选(不传则不出现在 URL 查询串中)。
+     */
+    async getGameUrl(ctx: Context, config: ChannelConfig): Promise<LoginResult> {
+        const data = ctx.request.body as Record<string, unknown>;
+        const lkey = config.loginConfig?.signKey;
+        if (!lkey) {
+            logger.error("WebGame 获取游戏 URL 失败: 未配置登录密钥 loginConfig.signKey");
+            return {code: 0, msg: "服务器未配置登录密钥"};
+        }
+
+        const gateDomain = DEFAULT_GATE_DOMAIN;
+
+        const uid = data.uid;
+        const platform = data.platform;
+        const gkey = data.gkey;
+        const skey = data.skey;
+        const time = data.time;
+        const is_adult = data.is_adult;
+        const back_url = data.back_url;
+        const type = data.type;
+
+        if (
+            uid === undefined ||
+            uid === null ||
+            platform === undefined ||
+            platform === null ||
+            time === undefined ||
+            time === null ||
+            back_url === undefined ||
+            back_url === null ||
+            type === undefined ||
+            type === null
+        ) {
+            return {
+                code: 0,
+                msg: "缺少必要参数: uid, platform, time, back_url, type",
+            };
+        }
+
+        const uidStr = String(uid);
+        let uidDecoded: string;
+        try {
+            uidDecoded = decodeURIComponent(uidStr.replace(/\+/g, "%20"));
+        } catch {
+            uidDecoded = uidStr;
+        }
+
+        const platformStr = String(platform);
+        const gkeyStr = gkey === undefined || gkey === null ? "" : String(gkey);
+        const skeyStr = skey === undefined || skey === null ? "" : String(skey);
+        const timeStr = String(time);
+        const isAdultStr =
+            is_adult === undefined || is_adult === null ? "" : String(is_adult);
+
+        const extsVal = data.exts;
+        const hasExts =
+            extsVal !== undefined &&
+            extsVal !== null &&
+            String(extsVal) !== "";
+
+        let signRaw = `${uidDecoded}${platformStr}${gkeyStr}${skeyStr}${timeStr}${isAdultStr}`;
+        if (hasExts) {
+            signRaw += String(extsVal);
+        }
+        signRaw += `#${lkey}`;
+
+        const sign = crypto.createHash("md5").update(signRaw).digest("hex").toLowerCase();
+
+        const base = gateDomain.replace(/\/+$/, "");
+        const params = new URLSearchParams();
+        params.set("uid", uidDecoded);
+        params.set("platform", platformStr);
+        if (gkeyStr !== "") {
+            params.set("gkey", gkeyStr);
+        }
+        if (skeyStr !== "") {
+            params.set("skey", skeyStr);
+        }
+        params.set("time", timeStr);
+        if (isAdultStr !== "") {
+            params.set("is_adult", isAdultStr);
+        }
+        params.set("back_url", String(back_url));
+        params.set("type", String(type));
+        if (hasExts) {
+            params.set("exts", String(extsVal));
+        }
+        params.set("sign", sign);
+
+        const gameUrl = `${base}/login.html?${params.toString()}`;
+
+        // 供客户端只用 sign 调用 /webgame/auth 做权限判断
+        setWebgameAuthInfo(sign, {
+            uid: uidDecoded,
+            platform: platformStr,
+            time: timeStr,
+            gkey: gkeyStr || undefined,
+            skey: skeyStr || undefined,
+            is_adult: isAdultStr || undefined,
+            exts: hasExts ? String(extsVal) : undefined,
+        });
+
+        // 登录成功后写入 accounts / account_login_logs(参考 checkUserToken)
+        try {
+            const ip = getClientIp(ctx);
+            const create_time = formatDate(new Date());
+
+            const channel_id = config.channelId;
+            const device_no = (data as any).device_no ?? "";
+            const reg_device = (data as any).reg_device ?? "";
+            const device_type = this.toIntOrZero((data as any).device_type);
+            const device_model = (data as any).device_model ?? "";
+            const device_version = (data as any).device_version ?? "";
+            const system_version = (data as any).system_version ?? "";
+
+            const accountInfo = (await User.checkAccountIsExist(uidDecoded, channel_id))[0];
+            if (!accountInfo) {
+                const accountRes = await User.createAccount(
+                    uidDecoded,
+                    channel_id,
+                    ip,
+                    device_no,
+                    reg_device,
+                    create_time,
+                    platformStr
+                );
+
+                if (!accountRes || accountRes.affectedRows <= 0) {
+                    logger.error("WebGame 登录落库失败: 添加账户失败", {uid: uidDecoded, channel_id});
+                    return {code: 0, msg: "添加账户失败"};
+                }
+            }
+
+            const logRes = await User.logAccountLogin(
+                uidDecoded,
+                ip,
+                device_type,
+                device_no,
+                device_model,
+                device_version,
+                system_version,
+                create_time,
+                channel_id,
+                platformStr
+            );
+
+            if (!logRes || logRes.affectedRows <= 0) {
+                logger.error("WebGame 登录落库失败: 添加登录日志失败", {uid: uidDecoded, channel_id});
+                return {code: 0, msg: "添加日志失败"};
+            }
+        } catch (e: any) {
+            logger.error("WebGame 登录落库异常", {msg: e?.message, stack: e?.stack});
+            return {code: 0, msg: "登录落库异常"};
+        }
+        logger.info("WebGame 生成游戏登录 URL", {gateDomain: base, signLen: signRaw.length});
+        return {
+            code: 1,
+            msg: "success",
+            data: {
+                gameUrl,
+            },
+        };
+    }
+
+    async handlePayment(ctx: Context, config: ChannelConfig): Promise<PaymentResult> {
+        const data = ctx.request.body as any;
+        logger.info("WebGame渠道支付回调参数", {url: ctx.href, params: data});
+
+        // 新回调字段(不验 sign)
+        const status = data?.status;
+        const amount = data?.amount;
+        const channelOrderId = data?.channel_order_id ?? data?.game_order ?? data?.id;
+        const cpOrderId = data?.channel_exts ?? data?.extras_params ?? data?.game_order;
+
+        // 兼容旧字段(历史上有 id/user_id/sign 等)
+        const user_id = data?.user_id ?? data?.userId ?? data?.channel_uid;
+
+        if (!cpOrderId || !status || amount === undefined || amount === null) {
+            logger.warn("WebGame渠道支付回调失败: 缺少必要参数", {cpOrderId, status, amount});
+            return {code: 0, msg: "缺少必要参数"};
+        }
+
+        if (status !== "completed") {
+            logger.warn("WebGame渠道支付状态非completed", {channelOrderId, status});
+            return {code: 0, msg: `支付状态非completed: ${status}`};
+        }
+
+        try {
+            const validation = await PaymentHelper.validateOrder(String(cpOrderId));
+            if (!validation.valid) {
+                return {
+                    code: validation.message?.includes("重复发货") ? 1 : 0,
+                    msg: validation.message || "订单验证失败"
+                };
+            }
+
+            const orderInfo = validation.orderInfo;
+
+            const payAmount = parseFloat(String(amount));
+            if (!Number.isFinite(payAmount)) {
+                return {code: 0, msg: "支付金额格式错误"};
+            }
+            if (Math.abs(Number(orderInfo.amount) - payAmount) > 0.01) {
+                logger.warn("WebGame渠道支付金额不匹配", {
+                    channelOrderId,
+                    requestAmount: payAmount,
+                    orderAmount: orderInfo.amount
+                });
+                return {code: 0, msg: `订单金额不一致: 订单金额${orderInfo.amount}元,支付金额${payAmount}元`};
+            }
+
+            logger.info(`WebGame渠道支付订单开始发货`, {
+                channelOrderId,
+                cpOrderId,
+                user_id,
+                amount: payAmount
+            });
+
+            const result = await PaymentHelper.deliverOrder(
+                orderInfo,
+                ctx.request.ip,
+                validation.url,
+                String(channelOrderId ?? ''),
+            );
+
+            logger.info(`WebGame渠道支付订单发货完成`, {channelOrderId, cpOrderId, result});
+
+            // 支付成功后打点上报
+            if (result.code === 1) {
+                this.reportEvent('payment', {
+                    user_id: user_id ?? orderInfo.uid,
+                    game_id: WEBGAME_APP_ID,
+                    server_id: String(orderInfo.server_id ?? ''),
+                    ip_addr: ctx.request.ip,
+                    log_data: JSON.stringify({
+                        order_id: cpOrderId,
+                        channel_order_id: channelOrderId,
+                        amount: payAmount,
+                    }),
+                }, config);
+            }
+
+            return result;
+        } catch (error: any) {
+            logger.error("WebGame渠道支付回调处理异常", {error: error.message, stack: error.stack});
+            return {code: 0, msg: "回调处理异常"};
+        }
+    }
+
+    // 角色查询接口(GET),平台查询指定区服下指定账号的角色信息
+    // sign = md5(user_id + server_id + time + login_key)
+    async handleRoleList(ctx: Context, config: ChannelConfig): Promise<any> {
+        const q = {...ctx.query, ...ctx.request.body} as any;
+        logger.info('WebGame 角色查询请求', {q});
+
+        const {user_id, pid, server_id, time, sign} = q;
+
+        if (!user_id || !pid || !server_id || !time || !sign) {
+            return {code: 2, message: '参数不全', data: []};
+        }
+
+        const loginKey = config.loginConfig?.signKey ?? '';
+        const expectedSign = crypto.createHash('md5')
+            .update(String(user_id) + String(server_id) + String(time) + loginKey)
+            .digest('hex').toLowerCase();
+
+        if (expectedSign !== String(sign).toLowerCase()) {
+            logger.warn('WebGame 角色查询签名错误', {user_id, server_id, expectedSign, sign});
+            return {code: 3, message: '签名错误', data: []};
+        }
+
+        try {
+            // 查 db_name
+            const rows = await query(
+                `SELECT db_name FROM game_server WHERE tag = ? AND sid = ? LIMIT 1`,
+                [config.channelId, String(server_id)]
+            ) as any[];
+
+            if (!rows || rows.length === 0 || !rows[0].db_name) {
+                logger.warn('WebGame 角色查询找不到区服', {server_id, channelId: config.channelId});
+                return {code: 5, message: '服务器错误', data: []};
+            }
+
+            const dbName = rows[0].db_name;
+            const newUniqueTag = `${config.channelId}|${server_id}|${user_id}`;
+
+            // 查 MongoDB 角色信息
+            const {getDb} = require("../../mongo/mongodb");
+            const db = getDb(dbName);
+            const collection = db.collection('char');
+
+            const filter: any = {newUniqueTag};
+            if (q.role_id) filter.roleId = String(q.role_id);
+
+            const docs = await collection.find(filter).toArray();
+
+            if (!docs || docs.length === 0) {
+                return {code: 4, message: '未创建角色', data: []};
+            }
+
+            const data = docs.map((doc: any) => ({
+                role_id:    doc.roleId    ?? doc._id.toString(),
+                name:       encodeURIComponent(doc.name ?? ''),
+                lv:         doc.lv        ?? 0,
+                sex:        doc.sex       ?? 'm',
+                vocation:   doc.vocation  ?? 0,
+                createTime: doc.createTime
+                    ? new Date(doc.createTime).toISOString().replace('T', ' ').slice(0, 19)
+                    : '',
+                power:      doc.zhandouli ?? doc.power ?? 0,
+            }));
+
+            return {code: 1, message: 'ok', data};
+        } catch (e: any) {
+            logger.error('WebGame 角色查询异常', {msg: e?.message, stack: e?.stack});
+            return {code: 5, message: '服务器错误', data: []};
+        }
+    }
+
+    // 登录跳转接口:验签 + 落库,返回拼接了平台参数的游戏 URL
+    // sign = md5(user_id + server_id + pid + time + login_key)
+    // 时间戳有效期 10 分钟
+    async handleLoginUrl(ctx: Context, config: ChannelConfig): Promise<any> {
+        const q = {...ctx.query, ...ctx.request.body} as any;
+        logger.info('WebGame 登录跳转请求', {q});
+
+        const {user_id, server_id, pid, time, sign} = q;
+
+        if (!user_id || !server_id || !pid || !time || !sign) {
+            return {code: 0, msg: '缺少必要参数'};
+        }
+
+        const loginKey = config.loginConfig?.signKey ?? '';
+        const expectedSign = crypto.createHash('md5')
+            .update(String(user_id) + String(server_id) + String(pid) + String(time) + loginKey)
+            .digest('hex').toLowerCase();
+
+        if (expectedSign !== String(sign).toLowerCase()) {
+            logger.warn('WebGame 登录跳转签名错误', {user_id, expectedSign, sign});
+            return {code: 0, msg: '签名错误'};
+        }
+
+        const ts = parseInt(String(time));
+        if (Math.abs(Math.floor(Date.now() / 1000) - ts) > 600) {
+            return {code: 0, msg: '登录链接已过期'};
+        }
+
+        // 落库
+        try {
+            const ip = getClientIp(ctx);
+            const create_time = formatDate(new Date());
+            const channel_id = config.channelId;
+            const uidStr = String(user_id);
+
+            const accountInfo = (await User.checkAccountIsExist(uidStr, channel_id))[0];
+            if (!accountInfo) {
+                await User.createAccount(uidStr, channel_id, ip, '', '', create_time, String(pid));
+            }
+            await User.logAccountLogin(uidStr, ip, 0, '', '', '', '', create_time, channel_id, String(pid));
+        } catch (e: any) {
+            logger.error('WebGame 登录跳转落库异常', {msg: e?.message});
+        }
+
+        // 拼接游戏 URL
+        const base = DEFAULT_GATE_DOMAIN.replace(/\/+$/, '');
+        const params = new URLSearchParams();
+        params.set('user_id', String(user_id));
+        params.set('server_id', String(server_id));
+        params.set('pid', String(pid));
+        params.set('time', String(time));
+        params.set('sign', String(sign));
+        if (q.ext   != null) params.set('ext',     String(q.ext));
+        if (q.client != null) params.set('client',  String(q.client));
+        if (q.isAdult != null) params.set('isAdult', String(q.isAdult));
+
+        const gameUrl = `${base}?${params.toString()}`;
+        logger.info('WebGame 登录跳转生成游戏URL', {user_id, server_id, gameUrl});
+
+        // 登录打点
+        this.reportEvent('login', {
+            user_id,
+            game_id: WEBGAME_APP_ID,
+            server_id,
+            ip_addr: getClientIp(ctx),
+        }, config);
+
+        return {code: 1, msg: 'success', data: {gameUrl}};
+    }
+
+    // 充值下单接口(GET),平台调用我们,验证签名后返回确认
+    // sign = md5(user_id + server_id + role_id + time + pay_key)
+    // extra_info 由客户端传入我们系统的 orderId,透传给回调
+    async handleCreateOrder(ctx: Context, config: ChannelConfig): Promise<any> {
+        const q = {...ctx.query, ...ctx.request.body} as any;
+        logger.info('WebGame 充值下单请求', {q});
+
+        const {user_id, server_id, role_id, goods_id, goods_name, money, extra_info, sign, time} = q;
+
+        if (!user_id || !server_id || !role_id || !goods_id || !money || !sign || !time) {
+            return {code: 0, message: '缺少必要参数'};
+        }
+
+        const payKey = config.paymentConfig?.callbackKey ?? '';
+        const expectedSign = crypto.createHash('md5')
+            .update(String(user_id) + String(server_id) + String(role_id) + String(time) + payKey)
+            .digest('hex').toLowerCase();
+
+        if (expectedSign !== String(sign).toLowerCase()) {
+            logger.warn('WebGame 充值下单签名错误', {user_id, expectedSign, sign});
+            return {code: 2, message: '签名错误'};
+        }
+
+        logger.info('WebGame 充值下单验证通过', {user_id, server_id, role_id, goods_id, money, extra_info});
+        return {code: 1, message: 'ok'};
+    }
+
+    // 充值回调接口(GET),平台支付成功后调用,发货
+    // sign = md5(user_id + order_id + money + time + server_id + role_id + extra_info + pay_key)
+    // extra_info 为客户端传入的内部 orderId,用于发货查询
+    async handlePayCallback(ctx: Context, config: ChannelConfig): Promise<any> {
+        const q = {...ctx.query, ...ctx.request.body} as any;
+        logger.info('WebGame 充值回调请求', {q});
+
+        const {user_id, pid, order_id, money, time, server_id, role_id, extra_info, sign} = q;
+
+        if (!user_id || !order_id || !money || !time || !server_id || !role_id || !sign) {
+            return {code: 4, message: '缺少必要参数'};
+        }
+
+        const payKey = config.paymentConfig?.callbackKey ?? '';
+        const extraStr = extra_info == null ? '' : String(extra_info);
+        const expectedSign = crypto.createHash('md5')
+            .update(String(user_id) + String(order_id) + String(money) + String(time) + String(server_id) + String(role_id) + extraStr + payKey)
+            .digest('hex').toLowerCase();
+
+        if (expectedSign !== String(sign).toLowerCase()) {
+            logger.warn('WebGame 充值回调签名错误', {user_id, order_id, expectedSign, sign});
+            return {code: 2, message: '签名错误'};
+        }
+
+        // extra_info 为内部 orderId
+        if (!extraStr) {
+            logger.warn('WebGame 充值回调 extra_info 为空,无法定位内部订单');
+            return {code: 4, message: '充值失败,请联系客服'};
+        }
+
+        const validation = await PaymentHelper.validateOrder(extraStr);
+        if (!validation.valid) {
+            if (validation.message?.includes('重复发货')) {
+                return {code: 3, message: '订单号重复'};
+            }
+            logger.warn('WebGame 充值回调订单验证失败', {extra_info: extraStr, msg: validation.message});
+            return {code: 4, message: '充值失败,请联系客服'};
+        }
+
+        const orderInfo = validation.orderInfo;
+        const payAmount = parseFloat(String(money));
+        if (!Number.isFinite(payAmount) || Math.abs(Number(orderInfo.amount) - payAmount) > 0.01) {
+            logger.warn('WebGame 充值回调金额不匹配', {order_id, money, orderAmount: orderInfo.amount});
+            return {code: 4, message: '充值失败,请联系客服'};
+        }
+
+        logger.info('WebGame 充值回调开始发货', {order_id, extra_info: extraStr, user_id, money: payAmount});
+        const result = await PaymentHelper.deliverOrder(orderInfo, ctx.request.ip, validation.url, String(order_id));
+        logger.info('WebGame 充值回调发货完成', {order_id, result});
+
+        if (result.code === 1) {
+            // 支付成功打点上报
+            this.reportEvent('payment', {
+                user_id,
+                game_id: WEBGAME_APP_ID,
+                server_id,
+                ip_addr: ctx.request.ip,
+                log_data: JSON.stringify({order_id, platform_order_id: order_id, amount: payAmount}),
+            }, config);
+            return {code: 1, message: 'ok'};
+        }
+
+        return {code: 4, message: '充值失败,请联系客服'};
+    }
+
+    // 聊天监控接口:客户端传业务参数,服务端生成 game_id/timestamp/sign 后转发 SDK
+    async handleChatMonitor(ctx: Context, config: ChannelConfig): Promise<any> {
+        const q = ctx.request.body as any;
+        logger.info('WebGame 聊天监控请求', {q});
+
+        const {user_id, chat_content, channel, role_id, role_name, server_id, account_name} = q;
+
+        if (!user_id || !chat_content || !channel || !role_id || !role_name || !server_id || !account_name) {
+            return {code: 0, data: null, msg: '缺少必要参数'};
+        }
+
+        const reportUrl = WEBGAME_REPORT_URL;
+        if (!reportUrl) {
+            logger.error('WebGame 聊天监控未配置 WEBGAME_REPORT_URL');
+            return {code: 0, data: null, msg: '服务器配置错误'};
+        }
+
+        const loginKey = config.loginConfig?.signKey ?? '';
+        const gameId = String(WEBGAME_APP_ID);
+        const timestamp = String(Math.floor(Date.now() / 1000));
+        const sign = crypto.createHash('md5')
+            .update(String(user_id) + gameId + timestamp + loginKey)
+            .digest('hex').toLowerCase();
+
+        const payload: Record<string, string> = {
+            user_id: String(user_id),
+            game_id: gameId,
+            sign,
+            timestamp,
+            chat_content: String(chat_content),
+            channel: "世界",
+            role_id: String(role_id),
+            role_name: String(role_name),
+            server_id: String(server_id),
+            account_name: String(account_name),
+
+        };
+
+        // 可选字段透传
+        const optionalFields = ['level', 'gold', 'chat_time', 'ip_addr',
+            'sec_chat_user_id', 'sec_chat_role_id', 'sec_chat_nickname', 'sec_chat_role_level'];
+        for (const field of optionalFields) {
+            if (q[field] !== undefined && q[field] !== null && q[field] !== '') {
+                payload[field] = String(q[field]);
+            }
+        }
+
+        try {
+            const res = await axios.post(
+                reportUrl + '/api/v2/webgame/chat-monitor',
+                payload,
+                {headers: {'Content-Type': 'application/json'}, timeout: 5000}
+            );
+            console.log("请求地址,",reportUrl + '/api/v2/webgame/chat-monitor',"参数",payload);
+            logger.info('WebGame 聊天监控 SDK 响应', {result: res.data});
+            return res.data;
+        } catch (err: any) {
+            logger.error('WebGame 聊天监控请求 SDK 失败', {error: err?.message});
+            return {code: 0, data: null, msg: '聊天监控请求失败'};
+        }
+    }
+
+    // 签名规则:所有参数(除sign)按字典序排序,拼接key=value&...&key={game_secret},MD5
+    private verifySign(data: any, gameSecret: string): boolean {
+        try {
+            const receivedSign = data.sign;
+            if (!receivedSign) return false;
+
+            const params = {...data};
+            delete params.sign;
+
+            const sortedKeys = Object.keys(params).sort();
+            const signStr = sortedKeys
+                .map(key => {
+                    const v = params[key];
+                    if (v === null || v === undefined) return `${key}=`;
+                    return `${key}=${v}`;
+                })
+                .join('&');
+
+            const stringToSign = `${signStr}&key=${gameSecret}`;
+            const calculatedSign = crypto.createHash('md5').update(stringToSign).digest('hex').toLowerCase();
+
+            logger.info("WebGame渠道签名验证", {calculatedSign, receivedSign: receivedSign.toLowerCase()});
+
+            return calculatedSign === receivedSign.toLowerCase();
+        } catch (error: any) {
+            logger.error("WebGame渠道签名验证出错", {error: error.message});
+            return false;
+        }
+    }
+}

+ 14 - 1
webServer/src/config/channelConfig.ts

@@ -51,7 +51,8 @@ import {
     ZERO_ONE_QUICK_MD5_KEY, ZERO_ONE_QUICK_CALLBACK_KEY, ZERO_ONE_QUICK_PRODUCT_CODE,
     QINGTIAN_MD5_KEY, QINGTIAN_CALLBACK_KEY, QINGTIAN_PRODUCT_CODE,
     XIAOYAO_QUICK_MD5_KEY, XIAOYAO_QUICK_CALLBACK_KEY, XIAOYAO_QUICK_PRODUCT_CODE,
-    XIAOYAO50_QUICK_MD5_KEY, XIAOYAO50_QUICK_CALLBACK_KEY, XIAOYAO50_QUICK_PRODUCT_CODE
+    XIAOYAO50_QUICK_MD5_KEY, XIAOYAO50_QUICK_CALLBACK_KEY, XIAOYAO50_QUICK_PRODUCT_CODE, WEBGAME_LOGIN_KEY,
+    WEBGAME_PAY_KEY, WEBGAME_APP_ID
 } from "./thirdParams";
 
 // 渠道配置接口定义
@@ -403,4 +404,16 @@ export const channelConfigs: Record<number, ChannelConfig> = {
             productCode: XIAOYAO50_QUICK_PRODUCT_CODE,
         },
     },
+    25: {
+        channelId: 25,
+        name: "WebGame",
+        platform: "",
+        paymentConfig: {
+            callbackKey: WEBGAME_PAY_KEY,
+        },
+        loginConfig: {
+            appId: WEBGAME_APP_ID,
+            signKey: WEBGAME_LOGIN_KEY,
+        },
+    }
 };

+ 6 - 0
webServer/src/config/thirdParams.ts

@@ -179,3 +179,9 @@ export const HKT_DOMAIN = "https://mia.hkhappygame.com";
 export const HKT_FB_ID = "176358215454264";
 export const HKT_FB_SECRET = "f437717865e8f40d988deedcc577f833";
 export const HKT_FB_CLIENT_TOKEN = "8df8bed44f30bea52a89a77e5bfc69b7";
+
+//WebGame Sdk
+export const WEBGAME_APP_ID = "1007";
+export const WEBGAME_LOGIN_KEY = "2e8afaa726a54a467cd47f1b5852cc7b";
+export const WEBGAME_PAY_KEY = "0f89756bd324850648f744353bcf29fd";
+export const WEBGAME_REPORT_URL = "https://dev-gdk.qkyx.online"; // 日志上报地址,待填写

+ 205 - 0
webServer/src/controller/ApiController.ts

@@ -49,6 +49,7 @@ const logger = require("../utils/log");
 const axios = require("axios");
 const {channelFactory} = require("../channels/factory/ChannelFactory");
 const {MianyouChannelHandler} = require("../channels/handlers/MianyouChannelHandler");
+import {getWebgameAuthInfo} from "../utils/webgameAuthCache";
 
 /**
  * Google支付回调处理
@@ -523,6 +524,94 @@ class ApiController {
         ctx.body = result;
     }
 
+    /**
+     * WebGame:校验登录 URL 的签名,用于客户端判断是否有权限进入。
+     * 支持 GET/POST:客户端只需传 gameUrl 上的 sign 即可。
+     * 必填:sign
+     */
+    async webgameAuth(ctx) {
+        const body = (ctx.request && ctx.request.body) ? ctx.request.body : {};
+        const query = ctx.query || {};
+        const data = {...query, ...body} as any;
+
+        const sign = data.sign;
+
+        if (sign == null || String(sign) === "") {
+            ctx.body = ApiController.fail("缺少必要参数: sign");
+            return;
+        }
+
+        const info = getWebgameAuthInfo(String(sign));
+        if (!info) {
+            ctx.body = ApiController.fail("签名无效或已过期");
+            return;
+        }
+
+        ctx.body = ApiController.success("success", 1, false, info);
+    }
+
+    /**
+     * WebGame:生成带签名的 gameUrl(对外独立接口,直接调用 WebGameChannelHandler.getGameUrl)。
+     * POST JSON Body:参考 webGameApi.md
+     */
+    async webgameGameUrl(ctx) {
+        const channelId = 25;
+        const channelConfig = ChannelConfigManager.getConfig(channelId);
+        if (!channelConfig) {
+            ctx.body = ApiController.fail("未知渠道id");
+            return;
+        }
+
+        const handler = channelFactory.getHandler(channelId);
+        if (!handler || typeof (handler as any).getGameUrl !== "function") {
+            ctx.body = ApiController.fail("渠道处理器不支持该接口");
+            return;
+        }
+
+        const result = await (handler as any).getGameUrl(ctx, channelConfig);
+        ctx.body = result;
+    }
+
+    async webgameCreateOrder(ctx) {
+        const channelId = 25;
+        const channelConfig = ChannelConfigManager.getConfig(channelId);
+        if (!channelConfig) { ctx.body = {code: 0, message: '未知渠道id'}; return; }
+        const handler = channelFactory.getHandler(channelId);
+        ctx.body = await (handler as any).handleCreateOrder(ctx, channelConfig);
+    }
+
+    async webgameLoginUrl(ctx) {
+        const channelId = 25;
+        const channelConfig = ChannelConfigManager.getConfig(channelId);
+        if (!channelConfig) { ctx.body = {code: 0, msg: '未知渠道id'}; return; }
+        const handler = channelFactory.getHandler(channelId);
+        ctx.body = await (handler as any).handleLoginUrl(ctx, channelConfig);
+    }
+
+    async webgameRoleList(ctx) {
+        const channelId = 25;
+        const channelConfig = ChannelConfigManager.getConfig(channelId);
+        if (!channelConfig) { ctx.body = {code: 5, message: '服务器错误', data: []}; return; }
+        const handler = channelFactory.getHandler(channelId);
+        ctx.body = await (handler as any).handleRoleList(ctx, channelConfig);
+    }
+
+    async webgamePayCallback(ctx) {
+        const channelId = 25;
+        const channelConfig = ChannelConfigManager.getConfig(channelId);
+        if (!channelConfig) { ctx.body = {code: 4, message: '充值失败,请联系客服'}; return; }
+        const handler = channelFactory.getHandler(channelId);
+        ctx.body = await (handler as any).handlePayCallback(ctx, channelConfig);
+    }
+
+    async webgameChatMonitor(ctx) {
+        const channelId = 25;
+        const channelConfig = ChannelConfigManager.getConfig(channelId);
+        if (!channelConfig) { ctx.body = {code: 0, data: null, msg: '未知渠道id'}; return; }
+        const handler = channelFactory.getHandler(channelId);
+        ctx.body = await (handler as any).handleChatMonitor(ctx, channelConfig);
+    }
+
     async callPay(ctx) {
         const data = ctx.request.body;
         let channelId = 1; // 默认渠道ID
@@ -627,6 +716,9 @@ class ApiController {
             case 24:
                 ctx.body = result.code === 1 ? "SUCCESS" : "Fail";
                 break;
+            case 25:
+                ctx.body = result.code === 1 ? "SUCCESS" : "Fail";
+                break;
             default:
                 ctx.body = result;
         }
@@ -1634,6 +1726,119 @@ class ApiController {
         }
     }
 
+    /**
+     * WebGame 渠道封禁接口:接收 SDK 平台发送的禁言/封号/解封请求。
+     * 支持 GET/POST,签名验证:md5(account_name + role_id + timestamp + login_key)
+     */
+    async webgamePunish(ctx) {
+        const body = (ctx.request && ctx.request.body) ? ctx.request.body : {};
+        const query = ctx.query || {};
+        const data = {...query, ...body} as any;
+
+        logger.info("webgamePunish 接口请求:", {data});
+
+        const {
+            server_id, account_name, role_id,
+            punish_type, punish_time, timestamp, platform_sign
+        } = data;
+
+        if (!account_name || !role_id || !timestamp || !platform_sign || !server_id || punish_type === undefined) {
+            ctx.body = {code: 0, data: null, msg: "缺少必要参数"};
+            return;
+        }
+
+        const channelConfig = ChannelConfigManager.getConfig(25);
+        if (!channelConfig) {
+            ctx.body = {code: 0, data: null, msg: "渠道配置不存在"};
+            return;
+        }
+
+        const loginKey = channelConfig.loginConfig?.signKey;
+        if (!loginKey) {
+            ctx.body = {code: 0, data: null, msg: "渠道密钥未配置"};
+            return;
+        }
+
+        // 签名验证: md5(account_name + role_id + timestamp + login_key)
+        const signStr = String(account_name) + String(role_id) + String(timestamp) + loginKey;
+        const expectedSign = CryptoJS.MD5(signStr).toString().toLowerCase();
+        if (expectedSign !== String(platform_sign).toLowerCase()) {
+            logger.warn("webgamePunish 签名验证失败", {expectedSign, platform_sign});
+            ctx.body = {code: 0, data: null, msg: "签名验证失败"};
+            return;
+        }
+
+        // 时间戳有效期验证(5分钟)
+        const now = Math.floor(Date.now() / 1000);
+        const ts = parseInt(timestamp);
+        if (Math.abs(now - ts) > 300) {
+            logger.warn("webgamePunish 时间戳过期", {timestamp, now});
+            ctx.body = {code: 0, data: null, msg: "请求已过期"};
+            return;
+        }
+
+        const channelTag = data.channel_id || 25;
+        const url = await getServerList(server_id, channelTag);
+        if (!url) {
+            logger.warn("webgamePunish 区服不存在", {server_id, channelTag});
+            ctx.body = {code: 0, data: null, msg: `区服id错误: serverId ${server_id}`};
+            return;
+        }
+
+        const banTime = parseInt(punish_time) || 0;
+        let banInfo: any;
+
+        switch (parseInt(punish_type)) {
+            case 1: // 禁言
+                banInfo = {
+                    type: "setBan",
+                    roleBanInfo: {
+                        channelTag: channelTag,
+                        serverTag: server_id,
+                        roleTag: role_id,
+                        banTime: banTime,
+                    },
+                };
+                break;
+            case 2: // 封号
+                banInfo = {
+                    type: "setBan",
+                    accountBanInfo: {
+                        channelTag: channelTag,
+                        accountTag: account_name,
+                        banTime: banTime,
+                    },
+                };
+                break;
+            case 3: // 解封
+                banInfo = {
+                    type: "setBan",
+                    accountBanInfo: {
+                        channelTag: channelTag,
+                        accountTag: account_name,
+                        banTime: 0,
+                    },
+                };
+                break;
+            default:
+                ctx.body = {code: 0, data: null, msg: `不支持的处罚类型: ${punish_type}`};
+                return;
+        }
+
+        logger.info("webgamePunish 发送服务器:", {banInfo});
+
+        const param = JSON.stringify(banInfo);
+        const sendMsg = new Msg();
+        sendMsg.connect(url, Account);
+        new Promise((resolve) => {
+            setTimeout(async () => {
+                sendMsg.CG_TEST_PROTO("test", param, server_id);
+            }, 1000);
+        });
+
+        ctx.body = {code: 1, data: null, msg: "封禁操作成功"};
+    }
+
     /**
      * 通过 userId 查询该账号在各区服的角色列表
      * 请求参数(GET/POST 均可):uid、channel_id

+ 17 - 2
webServer/src/router/index.ts

@@ -21,6 +21,13 @@ router.post("/checkUserToken", ApiController.checkUserToken);
 //第三方登陆
 router.post("/thirdLogin", ApiController.thirdLogin);
 
+// WebGame:校验登录URL签名(客户端判断权限)
+router.post("/webgame/auth", ApiController.webgameAuth);
+router.get("/webgame/auth", ApiController.webgameAuth);
+
+// WebGame:获取带签名的 gameUrl
+router.post("/webgame/gameUrl", ApiController.webgameGameUrl);
+
 //获取区服列表
 router.get("/serverList", ApiController.getServerList);
 router.post("/serverList", ApiController.getServerList);
@@ -52,6 +59,9 @@ router.post("/sendAllMail", ApiController.sendAllMail);
 
 router.post("/banUser", ApiController.banUser);
 
+// WebGame:封禁/解封/禁言接口(SDK平台回调)
+router.post("/webgame/punish", ApiController.webgamePunish);
+
 router.post("/getQuickSign", ApiController.getQuickSign);
 
 router.post("/uicFilter4399", ApiController.uicFilter4399);
@@ -92,7 +102,12 @@ router.post("/qqReport", TencentController.qqReport);
 // mianyou开服同步
 router.post("/mianyou/syncServer", ApiController.mianyouSyncServer);
 
-// 通过 userId 查询角色列表(战力、角色名、角色id、区服、创建时间)
-router.get("/getUserRoleList", ApiController.getUserRoleList);
+// WebGame通用接口
+router.get("/webGame/getUserRoleList", ApiController.getUserRoleList);
+router.post("/webgame/loginUrl", ApiController.webgameLoginUrl);
+router.get("/webgame/roleList", ApiController.webgameRoleList);
+router.post("/webgame/createOrder", ApiController.webgameCreateOrder);
+router.post("/webgame/payCallback", ApiController.webgamePayCallback);
+router.post("/webgame/chatMonitor", ApiController.webgameChatMonitor);
 
 module.exports = router;

+ 29 - 0
webServer/src/utils/webgameAuthCache.ts

@@ -0,0 +1,29 @@
+type WebgameAuthInfo = {
+  uid: string;
+  platform: string;
+  time: string;
+  gkey?: string;
+  skey?: string;
+  is_adult?: string;
+  exts?: string;
+};
+
+const TTL_MS = 5 * 60 * 1000;
+const cache = new Map<string, { value: WebgameAuthInfo; expireAt: number }>();
+
+export function setWebgameAuthInfo(sign: string, info: WebgameAuthInfo) {
+  if (!sign) return;
+  cache.set(sign, { value: info, expireAt: Date.now() + TTL_MS });
+}
+
+export function getWebgameAuthInfo(sign: string): WebgameAuthInfo | null {
+  if (!sign) return null;
+  const entry = cache.get(sign);
+  if (!entry) return null;
+  if (Date.now() > entry.expireAt) {
+    cache.delete(sign);
+    return null;
+  }
+  return entry.value;
+}
+

+ 233 - 0
webServer/webGameApi.md

@@ -0,0 +1,233 @@
+# WebGame 渠道 API 文档
+
+## 渠道信息
+- **渠道ID**: 25
+- **渠道名称**: WebGame
+- **服务器地址**: `https://serverkfhero.3ligame.com/api`
+
+---
+
+## 1. 账号登录(WebGame)
+
+### 接口地址
+- **POST** `https://serverkfhero.3ligame.com/api/thirdLogin`
+
+### 请求参数(JSON Body)
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| channel_id | number/string | 是 | 固定传 `25` |
+| uid | string | 是 | 用户ID |
+| token | string | 是 | 登录凭证(当前不校验) |
+| device_no | string | 否 | 设备号 |
+| reg_device | string | 否 | 注册设备信息 |
+| device_type | string | 否 | 设备类型 |
+| device_model | string | 否 | 设备型号 |
+| device_version | string | 否 | 设备版本 |
+| system_version | string | 否 | 系统版本 |
+| platform | string | 否 | 平台ID(落库用,可不传) |
+
+### 请求示例
+```bash
+POST https://serverkfhero.3ligame.com/api/thirdLogin
+Content-Type: application/json
+
+{
+  "channel_id": 25,
+  "uid": "test%401234",
+  "token": "any_token"
+}
+```
+
+### 响应示例
+```json
+{
+  "code": 1,
+  "msg": "success",
+  "data": {
+    "uid": "test@1234",
+    "platform": "unknown"
+  }
+}
+```
+
+---
+
+## 2. 获取游戏 URL(getGameUrl)
+
+### 接口地址
+- **POST** `https://serverkfhero.3ligame.com/api/webgame/gameUrl`
+
+### 请求参数(JSON Body)
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| uid | string | 是 | 用户ID |
+| platform | string | 是 | 平台ID |
+| time | number/string | 是 | 时间戳 |
+| back_url | string | 是 | 登录失败跳转 URL |
+| type | string | 是 | 登录类型:`web` / `pc` |
+| gkey | string | 否 | 游戏名缩写 |
+| skey | string | 否 | 区服ID |
+| is_adult | string/number | 否 | 防沉迷标识 |
+| exts | string | 否 | 透传参数 |
+| device_no | string | 否 | 设备号 |
+| reg_device | string | 否 | 注册设备信息 |
+| device_type | string | 否 | 设备类型 |
+| device_model | string | 否 | 设备型号 |
+| device_version | string | 否 | 设备版本 |
+| system_version | string | 否 | 系统版本 |
+
+### 请求示例
+```bash
+POST https://serverkfhero.3ligame.com/api/webgame/gameUrl
+Content-Type: application/json
+
+{
+  "uid": "test%401234",
+  "platform": "4399",
+  "time": 1710000000,
+  "back_url": "https://www.example.com/back",
+  "type": "web",
+  "exts": "a=1&b=2"
+}
+```
+
+### 响应示例
+```json
+{
+  "code": 1,
+  "msg": "success",
+  "data": {
+    "gameUrl": "https://.../login.html?uid=test%401234&platform=4399&time=1710000000&back_url=https%3A%2F%2Fwww.example.com%2Fback&type=web&exts=a%3D1%26b%3D2&sign=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+  }
+}
+```
+
+---
+
+## 3. WebGame 权限校验(客户端仅传 sign)
+
+### 接口地址
+- **GET/POST** `https://serverkfhero.3ligame.com/api/webgame/auth`
+
+### 请求参数
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| sign | string | 是 | 从 `gameUrl` 上取到的 `sign` |
+
+### 请求示例
+```bash
+GET https://serverkfhero.3ligame.com/api/webgame/auth?sign=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+```
+
+### 响应示例(成功)
+```json
+{
+  "code": 1,
+  "msg": "success",
+  "data": {
+    "uid": "test@1234",
+    "platform": "4399",
+    "time": "1710000000",
+    "gkey": "cssg",
+    "skey": "1",
+    "is_adult": "1",
+    "exts": "a=1&b=2"
+  }
+}
+```
+
+### 响应示例(失败)
+```json
+{
+  "code": 0,
+  "msg": "签名无效或已过期",
+  "data": null
+}
+```
+
+---
+
+## 4. 支付回调接口(WebGame)
+
+### 接口地址
+- **GET/POST** `https://serverkfhero.3ligame.com/api/callback`
+
+### 请求参数(Body 或 Query 均可,建议 POST Body)
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| channel_id | number/string | 是 | 固定传 `25` |
+| id | string | 是 | 渠道侧支付单号 |
+| user_id | string | 否 | 渠道侧用户ID(当前逻辑不参与验签) |
+| game_order | string | 是 | 我方订单号(用于发货) |
+| status | string | 是 | 支付状态,需为 `completed` 才会发货 |
+| amount | string/number | 是 | 支付金额(元) |
+| sign | string | 是 | 支付回调签名 |
+
+### 请求示例
+```bash
+POST https://serverkfhero.3ligame.com/api/callback?channel_id=25
+Content-Type: application/json
+
+{
+  "id": "pay_123",
+  "user_id": "u_1",
+  "game_order": "CP202601010101010001",
+  "status": "completed",
+  "amount": "6.00",
+  "sign": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+}
+```
+
+### 响应示例
+```json
+{
+  "code": 1,
+  "msg": "发货成功",
+  "data": {}
+}
+```
+
+---
+
+## 5. 角色查询(getUserRoleList)
+
+### 接口地址
+- **GET** `https://serverkfhero.3ligame.com/api/getUserRoleList`
+
+### 请求参数(Query)
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| uid | string | 是 | 用户ID |
+| channel_id | number/string | 否 | 渠道ID(不传默认 1;WebGame 建议传 `25`) |
+
+### 请求示例
+```bash
+GET https://serverkfhero.3ligame.com/api/getUserRoleList?uid=test%401234&channel_id=25
+```
+
+### 响应示例(成功)
+```json
+{
+  "code": 1,
+  "msg": "请求成功",
+  "data": [
+    {
+      "roleId": "10001",
+      "roleName": "张三",
+      "zhandouli": 123456,
+      "serverName": "S1",
+      "serverId": 1,
+      "createTime": "2026-04-20 12:00:00"
+    }
+  ]
+}
+```
+
+### 响应示例(无角色)
+```json
+{
+  "code": 1,
+  "msg": "请求成功",
+  "data": []
+}
+```