import Router from 'koa-router' import mysql from '../mysql/index.js' import jwt from 'jsonwebtoken' import * as log4js from '../log4js.js' import config from '../config/index.js' import { time, unixTime, encryptPassword, generateCode, getClientIp, isValidAccount, isValidEmail, getDeviceInfo } from '../utils.js' import { sendCodeMail } from '../mail.js' 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 } } // ─── POST /api/login 登录 ──────────────────────────────────────────────────── router.post('/api/login', async (ctx) => { const { username, password } = ctx.request.body if (!username || !password) return fail(ctx, '请输入账号和密码') if (config.account.adminAccount === username) return fail(ctx, '该账户不对外开放') if (!config.account.loginOpen) return fail(ctx, '内部测试中,未开放登录,如需体验请联系客服。') const ip = getClientIp(ctx) const encPwd = encryptPassword(password) const [rows] = await mysql.query( 'SELECT * FROM player WHERE username = ? AND password = ?', [username, encPwd] ) if (rows?.length !== 1) { log4js.koa.warn('登录失败', username, ip) return fail(ctx, '传送员无法匹配此账号,请检查!') } const token = jwt.sign({ ...rows[0] }, process.env.SECRET_KEY || 'chuanqi_secret', { expiresIn: '24h' }) log4js.koa.info('用户登录成功', username, ip) return ok(ctx, { token }, '欢迎来到清渊传奇,正在传送…') }) // ─── POST /api/register 注册 ───────────────────────────────────────────────── router.post('/api/register', async (ctx) => { if (!config.account.regOpen) return fail(ctx, '内部测试中,未开放注册,如需体验请联系客服。') const { username, password, password2, serverId, email, code } = ctx.request.body const ip = getClientIp(ctx) // 校验账号 if (!username) return fail(ctx, `请输入${config.account.name}${config.account.nameSuffix}`) if (!isValidAccount(username)) return fail(ctx, `${config.account.name}${config.account.nameSuffix}为6-16位字母/数字/下划线`) if (config.account.retainAccounts.includes(username.toLowerCase())) return fail(ctx, `抱歉!此${config.account.name}已被占用,请更换。`) // 校验密码 if (!password) return fail(ctx, `请输入${config.account.name}${config.account.passwordSuffix}`) if (password.length < 6 || password.length > 16) return fail(ctx, `${config.account.passwordSuffix}长度为6-16个字符`) if (password !== password2) return fail(ctx, `两次输入的${config.account.passwordSuffix}不一致!`) // 校验区服 if (!serverId) return fail(ctx, '请选择区服!') // 邮箱校验(选填时只校验格式) if (email && !isValidEmail(email)) return fail(ctx, '邮箱地址格式错误!') // 验证码校验 if (config.code.open && config.code.regCodeOpen) { if (!email) return fail(ctx, '请输入邮箱地址!') if (!code || code.length !== config.code.length) return fail(ctx, `验证码长度为${config.code.length}位!`) const [verifyRows] = await mysql.query( 'SELECT id, code FROM verify WHERE account = ? AND email = ? AND type = 1', [username, email] ) if (!verifyRows.length || verifyRows[0].code !== code) return fail(ctx, '验证码无效!') } // 每日注册限制 if (config.account.dayMaxReg) { const [regRows] = await mysql.query( "SELECT id FROM player WHERE reg_ip = ? AND DATE(FROM_UNIXTIME(reg_time)) = CURDATE()", [ip] ) if (regRows.length >= config.account.dayMaxReg) return fail(ctx, '您今日注册量已达上限,请明日再试~', 10) } // 检查账号是否已存在 const [existRows] = await mysql.query('SELECT id FROM player WHERE username = ?', [username]) if (existRows.length > 0) return fail(ctx, `此${config.account.name}已被其他勇士占用!请更换。`) // 检查邮箱是否已被占用 if (email) { const [emailRows] = await mysql.query('SELECT id FROM player WHERE email = ?', [email]) if (emailRows.length > 0) return fail(ctx, '此邮箱地址已被其他勇士占用!请更换。') } const encPwd = encryptPassword(password) const nowTime = unixTime() // 获取设备信息 const ua = ctx.request.headers['user-agent'] || '' const deviceInfo = getDeviceInfo(ua) // 读取代理人 ID(来自 query 参数 agent 或请求体) const agentId = parseInt(ctx.request.body.agent_id || ctx.query.agent_id) || 0 const [result] = await mysql.query( 'INSERT INTO player (username, password, server_id, email, agent_id, reg_time, reg_ip, device, os, browse) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [username, encPwd, parseInt(serverId), email || '', agentId, nowTime, ip, deviceInfo.device, deviceInfo.os, deviceInfo.browse] ) if (result.affectedRows < 1) return fail(ctx, `${config.account.name}获取失败,请重试~`) // 删除验证码 if (config.code.open && config.code.regCodeOpen && email) { await mysql.query('DELETE FROM verify WHERE account = ? AND email = ? AND type = 1', [username, email]) } log4js.koa.info('用户注册成功', username, ip) return ok(ctx, { token: encPwd }, `恭喜勇士!获得${config.account.name},请牢记${config.account.passwordSuffix}!准备开启传奇之旅..`) }) // ─── POST /api/reset_password 找回/修改密码 ────────────────────────────────── router.post('/api/reset_password', async (ctx) => { if (!config.code.open) return fail(ctx, '验证码系统尚未开启!找回密码请联系客服。') const { username, email, password, password2, code } = ctx.request.body if (!username || !isValidAccount(username)) return fail(ctx, `请输入正确的${config.account.name}${config.account.nameSuffix}`) if (!email || !isValidEmail(email)) return fail(ctx, '请输入正确的邮箱地址!') if (!password || password.length < 6 || password.length > 16) return fail(ctx, `${config.account.passwordSuffix}长度为6-16个字符`) if (password !== password2) return fail(ctx, `两次输入的${config.account.passwordSuffix}不一致!`) if (!code || code.length !== config.code.length) return fail(ctx, `请输入${config.code.length}位验证码!`) // 检查账号+邮箱是否匹配 const [playerRows] = await mysql.query( 'SELECT id FROM player WHERE username = ? AND email = ?', [username, email] ) if (!playerRows.length) return fail(ctx, '传送员无法匹配此账号,请检查!') // 检查验证码 const [verifyRows] = await mysql.query( 'SELECT id, code FROM verify WHERE email = ? AND type = 2', [email] ) if (!verifyRows.length || verifyRows[0].code !== code) return fail(ctx, '验证码不正确!') const encPwd = encryptPassword(password) await mysql.query('UPDATE player SET password = ? WHERE username = ? AND email = ?', [encPwd, username, email]) await mysql.query('DELETE FROM verify WHERE id = ? AND type = 2', [verifyRows[0].id]) log4js.koa.info('用户重置密码成功', username) return ok(ctx, {}, `${config.account.passwordSuffix}修改成功!`) }) // ─── POST /api/send_code 发送邮箱验证码 ────────────────────────────────────── router.post('/api/send_code', async (ctx) => { if (!config.code.open) return fail(ctx, '验证码系统尚未开启!') const { username, email, type } = ctx.request.body // type: 1=注册 2=找回密码 const typeInt = parseInt(type) const ip = getClientIp(ctx) if (![1, 2].includes(typeInt)) return fail(ctx, '参数错误!') if (!username || !isValidAccount(username)) return fail(ctx, `请输入${config.account.name}${config.account.nameSuffix}`) if (!email || !isValidEmail(email)) return fail(ctx, '请输入正确的邮箱地址!') if (1 === typeInt) { if (!config.account.regOpen) return fail(ctx, '内部测试中,未开放注册,如需体验请联系客服。') if (config.account.retainAccounts.includes(username.toLowerCase())) return fail(ctx, `此${config.account.name}已被占用,请更换。`) // 每日注册限制 if (config.account.dayMaxReg) { const [regRows] = await mysql.query( "SELECT id FROM player WHERE reg_ip = ? AND DATE(FROM_UNIXTIME(reg_time)) = CURDATE()", [ip] ) if (regRows.length >= config.account.dayMaxReg) return fail(ctx, '您今日注册量已达上限,请明日再试~', 10) } // 检查账号是否已存在 const [existRows] = await mysql.query('SELECT id FROM player WHERE username = ?', [username]) if (existRows.length > 0) return fail(ctx, `此${config.account.name}已被其他勇士占用!请更换。`) // 检查邮箱是否已被占用 const [emailRows] = await mysql.query('SELECT id FROM player WHERE email = ?', [email]) if (emailRows.length > 0) return fail(ctx, '此邮箱地址已被其他勇士占用!请更换。') } else { // 找回密码:检查账号+邮箱是否匹配 const [playerRows] = await mysql.query( 'SELECT id FROM player WHERE username = ? AND email = ?', [username, email] ) if (!playerRows.length) return fail(ctx, '传送员无法匹配此账号,请检查!') } // 检查发送间隔 const nowTime = unixTime() const [existVerify] = await mysql.query( 'SELECT id, time FROM verify WHERE account = ? AND email = ? AND type = ?', [username, email, typeInt] ) if (existVerify.length > 0) { const leftTime = config.code.sendInterval - (nowTime - existVerify[0].time) if (leftTime > 0) return fail(ctx, `操作频繁!请${leftTime}秒后再发送~`, 1) } const code = generateCode(config.code.length, 'NUMBER') const sent = await sendCodeMail(email, username, code, typeInt) if (!sent) return fail(ctx, '验证码发送失败!请重试~') if (existVerify.length > 0) { await mysql.query( 'UPDATE verify SET code = ?, time = ?, ip = ? WHERE id = ? AND type = ?', [code, nowTime, ip, existVerify[0].id, typeInt] ) } else { await mysql.query( 'INSERT INTO verify (account, type, email, code, time, ip) VALUES (?, ?, ?, ?, ?, ?)', [username, typeInt, email, code, nowTime, ip] ) } return ok(ctx, { time: config.code.sendInterval }, `验证码已发送到您的邮箱:${email},请查收!`) }) // ─── POST /api/enter_game 进入游戏 ─────────────────────────────────────────── router.post('/api/enter_game', async (ctx) => { const { srvId, account } = ctx.request.body if (!srvId || !account) return fail(ctx, '参数错误') const ip = getClientIp(ctx) log4js.koa.info('用户进入游戏', account, `srvId=${srvId}`, ip) await mysql.query( 'UPDATE player SET login_time = ?, login_ip = ? WHERE username = ?', [time(), ip, account] ) return ok(ctx, {}, '进入游戏成功') }) // ─── POST /api/check Token 校验(兼容旧版游戏客户端,接受 md5 密码 token)──── // 旧版游戏客户端传 account + token(md5密码哈希),此接口验证并返回 JWT router.post('/api/check', async (ctx) => { const { account, token } = ctx.request.body if (!account || !token) return fail(ctx, '参数错误') const [rows] = await mysql.query( 'SELECT * FROM player WHERE username = ? AND password = ?', [account, token] ) if (!rows?.length) return fail(ctx, '账号验证失败') // 签发 JWT 供后续接口使用 const jwtToken = jwt.sign({ ...rows[0] }, process.env.SECRET_KEY || 'chuanqi_secret', { expiresIn: '24h' }) return ok(ctx, { token: jwtToken, account }, '验证成功') }) // ─── GET /api/check Token 验证(GET 方式,部分游戏客户端使用 query 参数)──── router.get('/api/check', async (ctx) => { const { account, token } = ctx.query if (!account || !token) return fail(ctx, '参数错误') const [rows] = await mysql.query( 'SELECT id, username FROM player WHERE username = ? AND password = ?', [account, token] ) if (!rows?.length) return fail(ctx, '账号验证失败') return ok(ctx, { account }, '验证成功') }) export default router.routes()