Files
chuanqi-qycq-web/module/server/koa/registry.js
艾贤凌 d676faa704 docs(migration): 添加清渊传奇PHP到Vue+Node.js移植计划文档
- 新增 MIGRATION.md 详细记录PHP到Node.js架构迁移方案
- 包含项目背景、现有资产盘点、架构设计、任务清单等完整规划
- 记录后端补全、前端补全、PHP停用、部署运维四个阶段实施计划
- 提供技术决策、数据库说明、进度总览等关键信息
- 更新 .gitignore 添加 *_out.txt build_output.txt 构建输出文件过滤
- 修复 utils.js 路径引用问题确保代码正常运行
2026-04-24 17:57:06 +08:00

195 lines
8.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/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()