Files
chuanqi-qycq-web/module/server/koa/login.js
艾贤凌 6d4a72161f inint
2026-03-16 12:05:55 +08:00

268 lines
13 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 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 + tokenmd5密码哈希此接口验证并返回 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()