import Router from 'koa-router' import mysql from '../mysql/index.js' import getGameDB from '../mysql/gameDB.js' import * as log4js from '../log4js.js' import config from '../config/index.js' import { time, unixTime, getClientIp } from '../utils.js' import { readFileSync, existsSync } from 'fs' import { fileURLToPath } from 'url' import { dirname, join } from 'path' const __dirname = dirname(fileURLToPath(import.meta.url)) const router = new Router() function ok(ctx, data = {}, message = '操作成功') { ctx.body = { code: 0, message, ...data } } function fail(ctx, message = '操作失败', code = 1) { ctx.body = { code, message } } // ─── GET /api/server/list 区服列表 ────────────────────────────────────────── router.get('/api/server/list', async (ctx) => { const account = ctx.query.account || '' const nowTime = unixTime() const newSrvTime = 7 * 24 * 60 * 60 const [rows] = await mysql.query( 'SELECT id, server_id, name, host, port, status, UNIX_TIMESTAMP(time) as time, merge_id FROM server WHERE status >= 1 ORDER BY server_id ASC LIMIT 1000' ) const serverlist = rows.map(row => { const sid = row.merge_id || row.server_id return { id: row.id, serverName: (row.name || config.game.firstName) + row.server_id + '区', srvaddr: (row.host && row.host !== '127.0.0.1') ? row.host : config.game.host, srvport: row.port || (config.game.port + sid), srvid: sid, type: row.status === 3 ? 3 : (nowTime - row.time <= newSrvTime ? 1 : 2), // 1:新 2:火爆 3:维护 opentime: time(row.time * 1000), pf: config.game.pf, serverAlias: 's' + sid, originalSrvid: sid, } }) return ok(ctx, { login: [999, 997, 990], serverlist: [{ name: '全部区服', serverlist }] }, '获取服务器列表成功') }) // ─── GET /api/misc/agree 用户协议 ─────────────────────────────────────────── // 优先从 config/agreement.html 文件读取,不存在则回退到 config.agree 字符串配置 router.get('/api/misc/agree', async (ctx) => { const agreePath = join(__dirname, '../config/agreement.html') if (existsSync(agreePath)) { ctx.type = 'html' ctx.body = readFileSync(agreePath, 'utf-8') } else { ctx.body = config.agree } }) // ─── POST /api/report/chat 上报聊天 ───────────────────────────────────────── router.post('/api/report/chat', async (ctx) => { const { server_id, account, role_id, channel_id, content, cross } = ctx.request.body const serverId = parseInt((server_id || '').toString().replace(/^s/, '')) if (!serverId || !account || !role_id || !content) return fail(ctx, 'param error') if (account.length > 26) return fail(ctx, 'param error') if (parseInt(channel_id) > 10) return fail(ctx, 'param error') if (content.length > 255) return fail(ctx, 'param error') // 验证账号 token const token = ctx.request.headers.authorization?.split(' ')[1] if (!token) return fail(ctx, '未授权', 401) const nowTime = time() await mysql.query( 'INSERT INTO chat (account, server_id, role_id, channel_id, content, is_cross, time) VALUES (?, ?, ?, ?, ?, ?, ?)', [account, serverId, parseInt(role_id), parseInt(channel_id) || 0, content, cross == 1 ? 1 : 0, nowTime] ) return ok(ctx) }) // ─── POST /api/game/withdraw 提现 ─────────────────────────────────────────── router.post('/api/game/withdraw', async (ctx) => { const { server_id, account, role_id, role_name, pay_type, pay_account, amount } = ctx.request.body const serverId = parseInt((server_id || '').toString().replace(/^s/, '')) const roleId = parseInt(role_id) const payType = parseInt(pay_type) const amountInt = parseInt(amount) if (!serverId || !account || !roleId || !role_name || !pay_account || !amountInt) return fail(ctx, '参数错误!') if (account.length > 26) return fail(ctx, '参数错误!') if (role_name.length > 24) return fail(ctx, '参数错误!') if (![0, 1].includes(payType)) return fail(ctx, '收款账户类型不正确!') if (pay_account.length > 30) return fail(ctx, '收款账户格式不正确!') const withdrawCfg = config.withdraw const currencyName = config.currency.list[withdrawCfg.type] const currencyField = config.currency.field[withdrawCfg.type] if (amountInt < withdrawCfg.ratio) return fail(ctx, `最低提现数量为${withdrawCfg.ratio}`) const minAmount = withdrawCfg.ratio * withdrawCfg.minOnce if (amountInt < minAmount) return fail(ctx, `单次提现数量不能低于${minAmount}`) // 验证账号 const [playerRows] = await mysql.query( 'SELECT id FROM player WHERE username = ?', [account] ) if (!playerRows.length) return fail(ctx, '账号不存在!') // 提现间隔限制 const nowTime = unixTime() const [lastWithdraw] = await mysql.query( 'SELECT UNIX_TIMESTAMP(time) as time FROM withdraw WHERE server_id = ? AND role_id = ? ORDER BY id DESC LIMIT 1', [serverId, roleId] ) if (lastWithdraw.length > 0 && nowTime - lastWithdraw[0].time < withdrawCfg.intervalSec) { const leftSec = withdrawCfg.intervalSec - (nowTime - lastWithdraw[0].time) return fail(ctx, `请等待 ${leftSec} 秒后再试~`) } // ── 连接游戏区服数据库,验证货币余额 ───────────────────────────────────────── let gameDB try { gameDB = getGameDB(serverId) } catch (e) { log4js.koa.error('连接游戏DB失败', serverId, e.message) return fail(ctx, '游戏服务器连接失败,请稍后再试!') } // 查询角色货币余额(表名为 characters,字段由 currency.field 配置) let currentBalance = 0 if (gameDB && currencyField) { try { const [charRows] = await gameDB.query( `SELECT \`${currencyField}\` as balance FROM characters WHERE id = ? LIMIT 1`, [roleId] ) if (!charRows.length) return fail(ctx, '角色不存在,请确认区服和角色是否正确!') currentBalance = parseInt(charRows[0].balance) || 0 } catch (e) { log4js.koa.error('查询角色余额失败', serverId, roleId, e.message) return fail(ctx, '查询角色数据失败,请稍后再试!') } if (currentBalance < amountInt) { return fail(ctx, `您的${currencyName}余额不足(当前:${currentBalance},需要:${amountInt})`) } } const money = Math.floor(amountInt / withdrawCfg.ratio) // ── 调用游戏 GM 命令接口扣除货币 ───────────────────────────────────────────── const gmHost = config.game.host const gmPort = config.game.gmPort // GM 接口格式:operid=10030,扣除货币 const gmUrl = `http://${gmHost}:${gmPort}/?operid=10030&serverid=${serverId}&roleid=${roleId}&type=${withdrawCfg.type}&num=${amountInt}` let gmSuccess = false try { const gmRes = await fetch(gmUrl, { signal: AbortSignal.timeout(5000) }) const gmText = await gmRes.text() // GM 接口返回 0 表示成功 gmSuccess = gmText.trim() === '0' || gmText.includes('"result":0') || gmText.includes('success') log4js.koa.info(`GM 命令返回: ${gmText.trim()}`, gmUrl) } catch (e) { log4js.koa.error('GM 命令调用失败', e.message, gmUrl) return fail(ctx, '扣除货币失败,请联系客服!') } if (!gmSuccess) { log4js.koa.warn('GM 命令返回失败', gmUrl) return fail(ctx, '货币扣除失败,请联系客服处理!') } // ── 写入提现记录 ────────────────────────────────────────────────────────────── await mysql.query( 'INSERT INTO withdraw (account, server_id, role_id, pay_type, pay_account, amount, money, time) VALUES (?, ?, ?, ?, ?, ?, ?, NOW())', [account, serverId, roleId, payType, pay_account, amountInt, money] ) log4js.koa.info(`提现成功: ${account} s${serverId} ${role_name} ${amountInt}${currencyName}=${money}元`) return ok(ctx, {}, `成功提现:${amountInt}${currencyName}\n收益人民币:${money}元\n\n请留意您的收款账户余额。`) }) export default router.routes()