inint
This commit is contained in:
@@ -1,34 +1,267 @@
|
||||
import Router from 'koa-router';
|
||||
import mysql from "../mysql/index.js";
|
||||
import jwt from "jsonwebtoken";
|
||||
import * as log4js from "../log4js.js";
|
||||
import {time} from "../utils.js";
|
||||
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()
|
||||
|
||||
router.post("/api/login", async (ctx) => {
|
||||
const {username, password} = ctx.request.body
|
||||
if (['admin'].includes(username)) return ctx.body = {code: 1, message: "该账户不对外开放"}
|
||||
const [rows] = await mysql.query("SELECT * FROM mir_web.player WHERE username = ? AND password = ?", [username, password])
|
||||
if (rows?.length == 1) {
|
||||
const token = jwt.sign(rows[0], process.env.SECRET_KEY, {expiresIn: '24h'});
|
||||
return ctx.body = {code: 0, message: "登录成功", token}
|
||||
// ─── 工具函数 ────────────────────────────────────────────────────────────────
|
||||
|
||||
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, '传送员无法匹配此账号,请检查!')
|
||||
}
|
||||
log4js.koa.error("用户登录失败", username)
|
||||
return ctx.body = {code: 1, message: "用户名或密码错误"}
|
||||
const token = jwt.sign({ ...rows[0] }, process.env.SECRET_KEY || 'chuanqi_secret', { expiresIn: '24h' })
|
||||
log4js.koa.info('用户登录成功', username, ip)
|
||||
return ok(ctx, { token }, '欢迎来到清渊传奇,正在传送…')
|
||||
})
|
||||
|
||||
router.post("/api/enter_game", async (ctx) => {
|
||||
const {srvId, account} = ctx.request.body
|
||||
if (!srvId || !account) return ctx.body = {code: 1, message: "参数错误"}
|
||||
log4js.koa.info("用户进入游戏", account, ctx.ip)
|
||||
await mysql.query("UPDATE mir_web.player_game SET login_time = ?,login_ip = ? WHERE username = ?", [time(), ctx.ip, account])
|
||||
return ctx.body = {code: 0, message: "进入游戏成功"}
|
||||
// ─── 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}!准备开启传奇之旅..`)
|
||||
})
|
||||
|
||||
router.get("/api/server/list", async (ctx) => {
|
||||
const [rows] = await mysql.query("SELECT * FROM mir_web.server WHERE status >= 1 ORDER BY server_id ASC limit 1000")
|
||||
return ctx.body = {code: 0, message: "获取服务器列表成功", data: rows}
|
||||
// ─── 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()
|
||||
|
||||
Reference in New Issue
Block a user