195 lines
8.5 KiB
JavaScript
195 lines
8.5 KiB
JavaScript
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()
|