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 ) )
2025-12-24 23:48:14 +08:00
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 请留意您的收款账户余额。 ` )
} )
2025-12-24 23:48:14 +08:00
export default router . routes ( )