Files
chuanqi-qycq-web/module/server/koa/registry.js

195 lines
8.5 KiB
JavaScript
Raw Normal View History

2026-03-16 12:05:55 +08:00
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()
2026-03-16 12:05:55 +08:00
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()