268 lines
13 KiB
JavaScript
268 lines
13 KiB
JavaScript
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()
|