2 Commit-ok 6459c48ca5 ... 96c8ceabae

Szerző SHA1 Üzenet Dátum
  pigflower 96c8ceabae Merge branch '360test' of http://43.226.57.217:3000/yishanyou/GongFuServer into 360test 1 hete
  pigflower 188faff41e web功能优化 1 hete

+ 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": []
+}
+```