新增系统配置页(运营管理系统后台)
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
# 服务器配置
|
# 服务器配置
|
||||||
|
BACKEND_HOST=127.0.0.1
|
||||||
PORT=3000
|
PORT=3000
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
|
|
||||||
@@ -15,6 +16,10 @@ JWT_EXPIRES_IN=2h
|
|||||||
|
|
||||||
# 游戏服务端代理地址
|
# 游戏服务端代理地址
|
||||||
GAME_SERVER_PROXY_URL=http://127.0.0.1:8080/tool/http
|
GAME_SERVER_PROXY_URL=http://127.0.0.1:8080/tool/http
|
||||||
|
GAME_PSK=THIS_IS_A_32_BYTE_FIXED_PSK!!!!1
|
||||||
|
|
||||||
|
# 日志配置
|
||||||
|
LOG_LEVEL=info
|
||||||
|
|
||||||
# CORS配置
|
# CORS配置
|
||||||
CORS_ORIGIN=http://localhost:5173
|
CORS_ORIGIN=http://localhost:5173
|
||||||
@@ -15,6 +15,10 @@ JWT_EXPIRES_IN=2h
|
|||||||
|
|
||||||
# 游戏服务端代理地址
|
# 游戏服务端代理地址
|
||||||
GAME_SERVER_PROXY_URL=http://127.0.0.1:8080/tool/http
|
GAME_SERVER_PROXY_URL=http://127.0.0.1:8080/tool/http
|
||||||
|
GAME_PSK=THIS_IS_A_32_BYTE_FIXED_PSK!!!!!
|
||||||
|
|
||||||
|
# 日志配置
|
||||||
|
LOG_LEVEL=info
|
||||||
|
|
||||||
# CORS配置(生产环境请修改为实际域名)
|
# CORS配置(生产环境请修改为实际域名)
|
||||||
CORS_ORIGIN=http://your-frontend-domain.com
|
CORS_ORIGIN=http://your-frontend-domain.com
|
||||||
|
|||||||
79
backend/database/config_init.sql
Normal file
79
backend/database/config_init.sql
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- 梦幻西游一站式运营管理平台 - 系统配置表初始化脚本
|
||||||
|
-- MySQL 8.4 兼容版本
|
||||||
|
-- 创建日期: 2026-01-05
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 使用数据库
|
||||||
|
USE mhxy_web_vue;
|
||||||
|
|
||||||
|
-- 设置字符集
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 表: configs (系统配置表)
|
||||||
|
-- ============================================
|
||||||
|
DROP TABLE IF EXISTS `configs`;
|
||||||
|
CREATE TABLE `configs` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`config_key` VARCHAR(100) NOT NULL COMMENT '配置键',
|
||||||
|
`config_value` TEXT NOT NULL COMMENT '配置值',
|
||||||
|
`config_type` VARCHAR(50) NOT NULL COMMENT '配置类型 (basic:基础配置, security:安全配置, game:游戏配置, payment:充值配置, email:邮件配置)',
|
||||||
|
`description` VARCHAR(255) NULL COMMENT '配置描述',
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE INDEX `idx_config_key` (`config_key`),
|
||||||
|
INDEX `idx_config_type` (`config_type`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统配置表';
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 插入默认配置数据
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 基础配置
|
||||||
|
INSERT INTO `configs` (`config_key`, `config_value`, `config_type`, `description`) VALUES
|
||||||
|
('backend_host', '127.0.0.1', 'basic', '后端IP或域名地址'),
|
||||||
|
('backend_port', '3000', 'basic', '后端端口'),
|
||||||
|
('log_level', 'info', 'basic', '日志级别 (info/debug/error)'),
|
||||||
|
('login_captcha_enabled', 'true', 'basic', '登录验证码开关 (true:开启, false:关闭)'),
|
||||||
|
('player_service_enabled', 'true', 'basic', '玩家服务中心开关 (true:开启, false:关闭)'),
|
||||||
|
('player_service_close_msg', '玩家服务中心系统维护中', 'basic', '玩家服务中心关闭提示文本');
|
||||||
|
|
||||||
|
-- 安全配置
|
||||||
|
INSERT INTO `configs` (`config_key`, `config_value`, `config_type`, `description`) VALUES
|
||||||
|
('cors_origin', 'http://localhost:5173', 'security', '跨域地址'),
|
||||||
|
('jwt_secret', 'your-secret-key-change-in-production', 'security', 'JWT密钥'),
|
||||||
|
('jwt_expires_in', '2h', 'security', 'JWT有效期 (如: 2h, 7d, 30m)');
|
||||||
|
|
||||||
|
-- 游戏配置
|
||||||
|
INSERT INTO `configs` (`config_key`, `config_value`, `config_type`, `description`) VALUES
|
||||||
|
('game_server_proxy_url', 'http://127.0.0.1:8080/tool/http', 'game', '游戏服务端代理地址'),
|
||||||
|
('game_server_psk', 'THIS_IS_A_32_BYTE_FIXED_PSK!!!!', 'game', '游戏服务端PSK密钥');
|
||||||
|
|
||||||
|
-- 邮件配置
|
||||||
|
INSERT INTO `configs` (`config_key`, `config_value`, `config_type`, `description`) VALUES
|
||||||
|
('mail_from', '', 'email', '发件人邮箱'),
|
||||||
|
('mail_smtp_host', '', 'email', 'SMTP服务器地址'),
|
||||||
|
('mail_smtp_port', '587', 'email', 'SMTP服务器端口'),
|
||||||
|
('mail_smtp_user', '', 'email', 'SMTP用户名'),
|
||||||
|
('mail_smtp_password', '', 'email', 'SMTP密码');
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 验证数据
|
||||||
|
-- ============================================
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
config_key,
|
||||||
|
config_value,
|
||||||
|
config_type,
|
||||||
|
description,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM configs
|
||||||
|
ORDER BY config_type, id;
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|
||||||
|
-- 初始化完成
|
||||||
1377
backend/package-lock.json
generated
1377
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/nodemailer": "^7.0.4",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
@@ -19,6 +20,8 @@
|
|||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"mysql2": "^3.16.0",
|
"mysql2": "^3.16.0",
|
||||||
|
"nodemailer": "^7.0.12",
|
||||||
|
"svg-captcha": "^1.4.0",
|
||||||
"typeorm": "^0.3.28"
|
"typeorm": "^0.3.28"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import jwt from 'jsonwebtoken';
|
|||||||
import { AppDataSource } from '../config/database';
|
import { AppDataSource } from '../config/database';
|
||||||
// 导入管理员用户模型
|
// 导入管理员用户模型
|
||||||
import { AdminUser } from '../models/AdminUser';
|
import { AdminUser } from '../models/AdminUser';
|
||||||
|
// 导入验证码服务
|
||||||
|
import { CaptchaService } from '../services/captchaService';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 管理员认证控制器
|
* 管理员认证控制器
|
||||||
@@ -22,8 +24,8 @@ export class AdminAuthController {
|
|||||||
*/
|
*/
|
||||||
async login(req: Request, res: Response) {
|
async login(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
// 从请求体中获取用户名和密码
|
// 从请求体中获取用户名、密码和验证码信息
|
||||||
const { username, password } = req.body;
|
const { username, password, captchaId, captchaCode } = req.body;
|
||||||
|
|
||||||
// 验证用户名和密码是否为空
|
// 验证用户名和密码是否为空
|
||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
@@ -33,6 +35,33 @@ export class AdminAuthController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建验证码服务实例
|
||||||
|
const captchaService = new CaptchaService();
|
||||||
|
|
||||||
|
// 检查是否启用了验证码功能
|
||||||
|
const captchaEnabled = await captchaService.checkCaptchaEnabled();
|
||||||
|
|
||||||
|
// 如果启用了验证码,则验证验证码
|
||||||
|
if (captchaEnabled) {
|
||||||
|
// 验证验证码参数是否完整
|
||||||
|
if (!captchaId || !captchaCode) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '请输入验证码'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证验证码是否正确
|
||||||
|
const captchaVerify = captchaService.verifyCaptcha(captchaId, captchaCode);
|
||||||
|
|
||||||
|
if (!captchaVerify.success) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: captchaVerify.message || '验证码错误或已过期'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 获取管理员用户仓库
|
// 获取管理员用户仓库
|
||||||
const adminUserRepository = AppDataSource.getRepository(AdminUser);
|
const adminUserRepository = AppDataSource.getRepository(AdminUser);
|
||||||
// 根据用户名查找管理员用户
|
// 根据用户名查找管理员用户
|
||||||
|
|||||||
89
backend/src/controllers/captchaController.ts
Normal file
89
backend/src/controllers/captchaController.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { CaptchaService } from '../services/captchaService';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码控制器
|
||||||
|
* 处理验证码的生成和验证
|
||||||
|
*/
|
||||||
|
export class CaptchaController {
|
||||||
|
private captchaService: CaptchaService;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.captchaService = new CaptchaService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成验证码
|
||||||
|
* @param req - Express请求对象
|
||||||
|
* @param res - Express响应对象
|
||||||
|
*/
|
||||||
|
async generateCaptcha(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { captchaId, svg } = this.captchaService.generateCaptcha();
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
captchaId,
|
||||||
|
svg
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成验证码失败:', error);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '生成验证码失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证验证码
|
||||||
|
* @param req - Express请求对象
|
||||||
|
* @param res - Express响应对象
|
||||||
|
*/
|
||||||
|
async verifyCaptcha(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { captchaId, code } = req.body;
|
||||||
|
|
||||||
|
const result = this.captchaService.verifyCaptcha(captchaId, code);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return res.status(400).json(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
message: '验证码正确'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('验证验证码失败:', error);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '服务器内部错误'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查验证码是否启用
|
||||||
|
* @param req - Express请求对象
|
||||||
|
* @param res - Express响应对象
|
||||||
|
*/
|
||||||
|
async checkCaptchaEnabled(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const enabled = await this.captchaService.checkCaptchaEnabled();
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: { enabled }
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查验证码状态失败:', error);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '服务器内部错误'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
261
backend/src/controllers/configController.ts
Normal file
261
backend/src/controllers/configController.ts
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { AppDataSource } from '../config/database';
|
||||||
|
import { Config } from '../models/Config';
|
||||||
|
import { EnvHelper } from '../utils/envHelper';
|
||||||
|
import nodemailer from 'nodemailer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统配置控制器
|
||||||
|
* 处理系统配置相关的操作,包括获取配置、更新配置、测试邮件等
|
||||||
|
*/
|
||||||
|
export class ConfigController {
|
||||||
|
/**
|
||||||
|
* 获取所有配置
|
||||||
|
* 按配置类型分组返回
|
||||||
|
* @param req - Express请求对象
|
||||||
|
* @param res - Express响应对象
|
||||||
|
*/
|
||||||
|
async getAllConfigs(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const configRepository = AppDataSource.getRepository(Config);
|
||||||
|
|
||||||
|
const configs = await configRepository.find({
|
||||||
|
order: { configType: 'ASC', id: 'ASC' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupedConfigs: Record<string, any[]> = {
|
||||||
|
basic: [],
|
||||||
|
security: [],
|
||||||
|
game: [],
|
||||||
|
payment: [],
|
||||||
|
email: []
|
||||||
|
};
|
||||||
|
|
||||||
|
configs.forEach(config => {
|
||||||
|
if (groupedConfigs[config.configType]) {
|
||||||
|
groupedConfigs[config.configType].push({
|
||||||
|
id: config.id,
|
||||||
|
configKey: config.configKey,
|
||||||
|
configValue: config.configValue,
|
||||||
|
description: config.description
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: groupedConfigs
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取配置失败:', error);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '服务器内部错误'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新配置
|
||||||
|
* 批量更新配置,同时更新数据库和.env文件
|
||||||
|
* @param req - Express请求对象
|
||||||
|
* @param res - Express响应对象
|
||||||
|
*/
|
||||||
|
async updateConfigs(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { configs } = req.body;
|
||||||
|
|
||||||
|
if (!configs || !Array.isArray(configs)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '配置数据格式错误'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const configRepository = AppDataSource.getRepository(Config);
|
||||||
|
const envUpdates: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (const config of configs) {
|
||||||
|
if (!config.configKey || config.configValue === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingConfig = await configRepository.findOne({
|
||||||
|
where: { configKey: config.configKey }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingConfig) {
|
||||||
|
existingConfig.configValue = String(config.configValue);
|
||||||
|
await configRepository.save(existingConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
const envKey = this.mapConfigKeyToEnvKey(config.configKey);
|
||||||
|
if (envKey) {
|
||||||
|
envUpdates[envKey] = String(config.configValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const envFilePath = EnvHelper.getEnvFilePath(process.env.NODE_ENV || 'development');
|
||||||
|
const updateSuccess = EnvHelper.updateEnvFile(envFilePath, envUpdates);
|
||||||
|
|
||||||
|
if (!updateSuccess) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '更新.env文件失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
message: '配置更新成功'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新配置失败:', error);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '服务器内部错误'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试邮件发送
|
||||||
|
* @param req - Express请求对象
|
||||||
|
* @param res - Express响应对象
|
||||||
|
*/
|
||||||
|
async testEmail(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { to } = req.body;
|
||||||
|
|
||||||
|
if (!to || !this.isValidEmail(to)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '请输入有效的邮箱地址'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const configRepository = AppDataSource.getRepository(Config);
|
||||||
|
|
||||||
|
const mailConfigs = await configRepository.find({
|
||||||
|
where: [
|
||||||
|
{ configKey: 'mail_from' },
|
||||||
|
{ configKey: 'mail_smtp_host' },
|
||||||
|
{ configKey: 'mail_smtp_port' },
|
||||||
|
{ configKey: 'mail_smtp_user' },
|
||||||
|
{ configKey: 'mail_smtp_password' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const configMap = new Map(mailConfigs.map(c => [c.configKey, c.configValue]));
|
||||||
|
|
||||||
|
const mailFrom = configMap.get('mail_from');
|
||||||
|
const smtpHost = configMap.get('mail_smtp_host');
|
||||||
|
const smtpPort = configMap.get('mail_smtp_port');
|
||||||
|
const smtpUser = configMap.get('mail_smtp_user');
|
||||||
|
const smtpPassword = configMap.get('mail_smtp_password');
|
||||||
|
|
||||||
|
if (!mailFrom || !smtpHost || !smtpPort || !smtpUser || !smtpPassword) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '邮件配置不完整,请先配置邮件信息'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: smtpHost,
|
||||||
|
port: Number(smtpPort),
|
||||||
|
secure: Number(smtpPort) === 465,
|
||||||
|
auth: {
|
||||||
|
user: smtpUser,
|
||||||
|
pass: smtpPassword
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mailOptions = {
|
||||||
|
from: mailFrom,
|
||||||
|
to: to,
|
||||||
|
subject: '梦幻西游运营管理系统 - 邮件测试',
|
||||||
|
text: '这是一封测试邮件,如果您收到此邮件,说明邮件配置正确。'
|
||||||
|
};
|
||||||
|
|
||||||
|
await transporter.sendMail(mailOptions);
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
message: '邮件发送成功'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('测试邮件发送失败:', error);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '邮件发送失败,请检查配置'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将配置键映射到.env文件中的环境变量键
|
||||||
|
* @param configKey - 配置键
|
||||||
|
* @returns 环境变量键,如果不映射则返回null
|
||||||
|
*/
|
||||||
|
private mapConfigKeyToEnvKey(configKey: string): string | null {
|
||||||
|
const mapping: Record<string, string> = {
|
||||||
|
'backend_host': 'BACKEND_HOST',
|
||||||
|
'backend_port': 'PORT',
|
||||||
|
'log_level': 'LOG_LEVEL',
|
||||||
|
'cors_origin': 'CORS_ORIGIN',
|
||||||
|
'jwt_secret': 'JWT_SECRET',
|
||||||
|
'jwt_expires_in': 'JWT_EXPIRES_IN',
|
||||||
|
'game_server_proxy_url': 'GAME_SERVER_PROXY_URL',
|
||||||
|
'game_server_psk': 'GAME_PSK'
|
||||||
|
};
|
||||||
|
|
||||||
|
return mapping[configKey] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证邮箱格式
|
||||||
|
* @param email - 邮箱地址
|
||||||
|
* @returns 是否有效
|
||||||
|
*/
|
||||||
|
private isValidEmail(email: string): boolean {
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
return emailRegex.test(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查玩家服务中心状态
|
||||||
|
* @param req - Express请求对象
|
||||||
|
* @param res - Express响应对象
|
||||||
|
*/
|
||||||
|
async checkPlayerServiceStatus(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const configRepository = AppDataSource.getRepository(Config);
|
||||||
|
|
||||||
|
const playerServiceEnabledConfig = await configRepository.findOne({
|
||||||
|
where: { configKey: 'player_service_enabled' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const playerServiceCloseMsgConfig = await configRepository.findOne({
|
||||||
|
where: { configKey: 'player_service_close_msg' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const enabled = playerServiceEnabledConfig?.configValue === 'true';
|
||||||
|
const closeMsg = playerServiceCloseMsgConfig?.configValue || '玩家服务中心系统维护中';
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
enabled,
|
||||||
|
closeMsg
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查玩家服务中心状态失败:', error);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '服务器内部错误'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { CaptchaService } from '../services/captchaService';
|
||||||
|
|
||||||
export class PlayerAuthController {
|
export class PlayerAuthController {
|
||||||
private readonly gameServerUrl: string;
|
private readonly gameServerUrl: string;
|
||||||
@@ -10,7 +11,7 @@ export class PlayerAuthController {
|
|||||||
|
|
||||||
async login(req: Request, res: Response) {
|
async login(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const { username, password } = req.body;
|
const { username, password, captchaId, captchaCode } = req.body;
|
||||||
|
|
||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
@@ -19,6 +20,33 @@ export class PlayerAuthController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建验证码服务实例
|
||||||
|
const captchaService = new CaptchaService();
|
||||||
|
|
||||||
|
// 检查是否启用了验证码功能
|
||||||
|
const captchaEnabled = await captchaService.checkCaptchaEnabled();
|
||||||
|
|
||||||
|
// 如果启用了验证码,则验证验证码
|
||||||
|
if (captchaEnabled) {
|
||||||
|
// 验证验证码参数是否完整
|
||||||
|
if (!captchaId || !captchaCode) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '请输入验证码'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证验证码是否正确
|
||||||
|
const captchaVerify = captchaService.verifyCaptcha(captchaId, captchaCode);
|
||||||
|
|
||||||
|
if (!captchaVerify.success) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: captchaVerify.message || '验证码错误或已过期'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
`${this.gameServerUrl}?code=auth/login`,
|
`${this.gameServerUrl}?code=auth/login`,
|
||||||
{
|
{
|
||||||
|
|||||||
25
backend/src/models/Config.ts
Normal file
25
backend/src/models/Config.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('configs')
|
||||||
|
export class Config {
|
||||||
|
@PrimaryGeneratedColumn('increment')
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 100, unique: true, name: 'config_key' })
|
||||||
|
configKey: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', name: 'config_value' })
|
||||||
|
configValue: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 50, name: 'config_type' })
|
||||||
|
configType: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ name: 'updated_at' })
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
@@ -2,12 +2,16 @@ import { Router } from 'express';
|
|||||||
import { AdminAuthController } from '../controllers/adminAuthController';
|
import { AdminAuthController } from '../controllers/adminAuthController';
|
||||||
import { AdminUserController } from '../controllers/adminUserController';
|
import { AdminUserController } from '../controllers/adminUserController';
|
||||||
import { AdminRoleController } from '../controllers/adminRoleController';
|
import { AdminRoleController } from '../controllers/adminRoleController';
|
||||||
|
import { ConfigController } from '../controllers/configController';
|
||||||
|
import { CaptchaController } from '../controllers/captchaController';
|
||||||
import { adminAuthMiddleware, AuthRequest } from '../middleware/adminAuth';
|
import { adminAuthMiddleware, AuthRequest } from '../middleware/adminAuth';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const adminAuthController = new AdminAuthController();
|
const adminAuthController = new AdminAuthController();
|
||||||
const adminUserController = new AdminUserController();
|
const adminUserController = new AdminUserController();
|
||||||
const adminRoleController = new AdminRoleController();
|
const adminRoleController = new AdminRoleController();
|
||||||
|
const configController = new ConfigController();
|
||||||
|
const captchaController = new CaptchaController();
|
||||||
|
|
||||||
// 认证相关路由
|
// 认证相关路由
|
||||||
router.post('/login', (req, res) => adminAuthController.login(req, res));
|
router.post('/login', (req, res) => adminAuthController.login(req, res));
|
||||||
@@ -27,4 +31,14 @@ router.post('/roles', adminAuthMiddleware, (req: AuthRequest, res) => adminRoleC
|
|||||||
router.put('/roles/:id', adminAuthMiddleware, (req: AuthRequest, res) => adminRoleController.updateRole(req, res));
|
router.put('/roles/:id', adminAuthMiddleware, (req: AuthRequest, res) => adminRoleController.updateRole(req, res));
|
||||||
router.delete('/roles/:id', adminAuthMiddleware, (req: AuthRequest, res) => adminRoleController.deleteRole(req, res));
|
router.delete('/roles/:id', adminAuthMiddleware, (req: AuthRequest, res) => adminRoleController.deleteRole(req, res));
|
||||||
|
|
||||||
|
// 系统配置路由(需要认证)
|
||||||
|
router.get('/configs', adminAuthMiddleware, (req: AuthRequest, res) => configController.getAllConfigs(req, res));
|
||||||
|
router.put('/configs', adminAuthMiddleware, (req: AuthRequest, res) => configController.updateConfigs(req, res));
|
||||||
|
router.post('/configs/test-email', adminAuthMiddleware, (req: AuthRequest, res) => configController.testEmail(req, res));
|
||||||
|
|
||||||
|
// 验证码路由
|
||||||
|
router.get('/captcha', (req, res) => captchaController.generateCaptcha(req, res));
|
||||||
|
router.post('/captcha/verify', (req, res) => captchaController.verifyCaptcha(req, res));
|
||||||
|
router.get('/captcha/enabled', (req, res) => captchaController.checkCaptchaEnabled(req, res));
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import { PlayerAuthController } from '../controllers/playerAuthController';
|
import { PlayerAuthController } from '../controllers/playerAuthController';
|
||||||
|
import { CaptchaController } from '../controllers/captchaController';
|
||||||
|
import { ConfigController } from '../controllers/configController';
|
||||||
import { playerAuthMiddleware, PlayerAuthRequest } from '../middleware/playerAuth';
|
import { playerAuthMiddleware, PlayerAuthRequest } from '../middleware/playerAuth';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const playerAuthController = new PlayerAuthController();
|
const playerAuthController = new PlayerAuthController();
|
||||||
|
const captchaController = new CaptchaController();
|
||||||
|
const configController = new ConfigController();
|
||||||
|
|
||||||
router.post('/login', (req, res) => playerAuthController.login(req, res));
|
router.post('/login', (req, res) => playerAuthController.login(req, res));
|
||||||
|
|
||||||
@@ -11,4 +15,12 @@ router.post('/logout', (req, res) => playerAuthController.logout(req, res));
|
|||||||
|
|
||||||
router.get('/account', playerAuthMiddleware, (req: PlayerAuthRequest, res) => playerAuthController.getAccountInfo(req, res));
|
router.get('/account', playerAuthMiddleware, (req: PlayerAuthRequest, res) => playerAuthController.getAccountInfo(req, res));
|
||||||
|
|
||||||
|
// 验证码路由
|
||||||
|
router.get('/captcha', (req, res) => captchaController.generateCaptcha(req, res));
|
||||||
|
router.post('/captcha/verify', (req, res) => captchaController.verifyCaptcha(req, res));
|
||||||
|
router.get('/captcha/enabled', (req, res) => captchaController.checkCaptchaEnabled(req, res));
|
||||||
|
|
||||||
|
// 玩家服务中心状态路由
|
||||||
|
router.get('/service-status', (req, res) => configController.checkPlayerServiceStatus(req, res));
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
130
backend/src/services/captchaService.ts
Normal file
130
backend/src/services/captchaService.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import svgCaptcha from 'svg-captcha';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码存储
|
||||||
|
* 使用内存存储验证码,生产环境建议使用Redis
|
||||||
|
*/
|
||||||
|
const captchaStore = new Map<string, { code: string; expireTime: number }>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理过期的验证码
|
||||||
|
*/
|
||||||
|
setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [key, value] of captchaStore.entries()) {
|
||||||
|
if (value.expireTime < now) {
|
||||||
|
captchaStore.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码服务类
|
||||||
|
* 提供验证码的生成和验证功能
|
||||||
|
*/
|
||||||
|
export class CaptchaService {
|
||||||
|
/**
|
||||||
|
* 生成验证码
|
||||||
|
* @returns 验证码ID和SVG图片
|
||||||
|
*/
|
||||||
|
generateCaptcha(): { captchaId: string; svg: string } {
|
||||||
|
const captcha = svgCaptcha.create({
|
||||||
|
size: 4,
|
||||||
|
ignoreChars: '0o1iIl',
|
||||||
|
noise: 2,
|
||||||
|
color: true,
|
||||||
|
background: '#f5f5f5',
|
||||||
|
width: 120,
|
||||||
|
height: 40,
|
||||||
|
fontSize: 36
|
||||||
|
});
|
||||||
|
|
||||||
|
const captchaId = this.generateCaptchaId();
|
||||||
|
const expireTime = Date.now() + 5 * 60 * 1000;
|
||||||
|
|
||||||
|
captchaStore.set(captchaId, {
|
||||||
|
code: captcha.text.toLowerCase(),
|
||||||
|
expireTime
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
captchaId,
|
||||||
|
svg: captcha.data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证验证码
|
||||||
|
* @param captchaId - 验证码ID
|
||||||
|
* @param code - 用户输入的验证码
|
||||||
|
* @returns 验证结果
|
||||||
|
*/
|
||||||
|
verifyCaptcha(captchaId: string, code: string): { success: boolean; message?: string } {
|
||||||
|
if (!captchaId || !code) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: '验证码参数不完整'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const captchaData = captchaStore.get(captchaId);
|
||||||
|
|
||||||
|
if (!captchaData) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: '验证码已过期或不存在'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (captchaData.expireTime < Date.now()) {
|
||||||
|
captchaStore.delete(captchaId);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: '验证码已过期'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (captchaData.code !== code.toLowerCase()) {
|
||||||
|
captchaStore.delete(captchaId);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: '验证码错误'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
captchaStore.delete(captchaId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查验证码是否启用
|
||||||
|
* @returns 是否启用验证码
|
||||||
|
*/
|
||||||
|
async checkCaptchaEnabled(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const { AppDataSource } = await import('../config/database');
|
||||||
|
const { Config } = await import('../models/Config');
|
||||||
|
|
||||||
|
const configRepository = AppDataSource.getRepository(Config);
|
||||||
|
const config = await configRepository.findOne({
|
||||||
|
where: { configKey: 'login_captcha_enabled' }
|
||||||
|
});
|
||||||
|
|
||||||
|
return config ? config.configValue === 'true' : true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查验证码状态失败:', error);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成验证码ID
|
||||||
|
* @returns 验证码ID
|
||||||
|
*/
|
||||||
|
private generateCaptchaId(): string {
|
||||||
|
return `captcha_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
90
backend/src/utils/envHelper.ts
Normal file
90
backend/src/utils/envHelper.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* .env文件更新工具
|
||||||
|
* 用于读取、更新和保存.env文件
|
||||||
|
*/
|
||||||
|
export class EnvHelper {
|
||||||
|
/**
|
||||||
|
* 读取.env文件内容
|
||||||
|
* @param envFilePath .env文件路径
|
||||||
|
* @returns 环境变量键值对
|
||||||
|
*/
|
||||||
|
static readEnvFile(envFilePath: string): Record<string, string> {
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(envFilePath, 'utf-8');
|
||||||
|
const envVars: Record<string, string> = {};
|
||||||
|
|
||||||
|
const lines = content.split('\n');
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmedLine = line.trim();
|
||||||
|
if (trimmedLine && !trimmedLine.startsWith('#')) {
|
||||||
|
const equalIndex = trimmedLine.indexOf('=');
|
||||||
|
if (equalIndex > 0) {
|
||||||
|
const key = trimmedLine.substring(0, equalIndex).trim();
|
||||||
|
const value = trimmedLine.substring(equalIndex + 1).trim();
|
||||||
|
envVars[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return envVars;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('读取.env文件失败:', error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新.env文件中的指定变量
|
||||||
|
* @param envFilePath .env文件路径
|
||||||
|
* @param updates 要更新的环境变量键值对
|
||||||
|
* @returns 是否更新成功
|
||||||
|
*/
|
||||||
|
static updateEnvFile(envFilePath: string, updates: Record<string, string>): boolean {
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(envFilePath, 'utf-8');
|
||||||
|
const lines = content.split('\n');
|
||||||
|
const updatedLines: string[] = [];
|
||||||
|
const updatedKeys = new Set<string>();
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmedLine = line.trim();
|
||||||
|
if (trimmedLine && !trimmedLine.startsWith('#')) {
|
||||||
|
const equalIndex = trimmedLine.indexOf('=');
|
||||||
|
if (equalIndex > 0) {
|
||||||
|
const key = trimmedLine.substring(0, equalIndex).trim();
|
||||||
|
if (updates.hasOwnProperty(key)) {
|
||||||
|
updatedLines.push(`${key}=${updates[key]}`);
|
||||||
|
updatedKeys.add(key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updatedLines.push(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in updates) {
|
||||||
|
if (!updatedKeys.has(key)) {
|
||||||
|
updatedLines.push(`${key}=${updates[key]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(envFilePath, updatedLines.join('\n'), 'utf-8');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新.env文件失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取.env文件路径
|
||||||
|
* @param env 环境名称 (development/production)
|
||||||
|
* @returns .env文件路径
|
||||||
|
*/
|
||||||
|
static getEnvFilePath(env: string = 'development'): string {
|
||||||
|
return path.join(process.cwd(), '.env');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
import type { ApiResponse, LoginResponseData, UserInfo } from '@/types/api'
|
import type { ApiResponse, LoginResponseData, UserInfo, ConfigItem, CaptchaData, CaptchaEnabled } from '@/types/api'
|
||||||
|
|
||||||
export const login = (username: string, password: string): Promise<ApiResponse<LoginResponseData>> => {
|
export const login = (username: string, password: string, captchaId?: string, captchaCode?: string): Promise<ApiResponse<LoginResponseData>> => {
|
||||||
return request({
|
return request({
|
||||||
url: '/api/admin/login',
|
url: '/api/admin/login',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: { username, password }
|
data: { username, password, captchaId, captchaCode }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,3 +15,48 @@ export const getCurrentUser = (): Promise<ApiResponse<UserInfo>> => {
|
|||||||
method: 'get'
|
method: 'get'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getAllConfigs = (): Promise<ApiResponse<ConfigItem[]>> => {
|
||||||
|
return request({
|
||||||
|
url: '/api/admin/configs',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateConfigs = (configs: Array<{ configKey: string; configValue: string }>): Promise<ApiResponse> => {
|
||||||
|
return request({
|
||||||
|
url: '/api/admin/configs',
|
||||||
|
method: 'put',
|
||||||
|
data: { configs }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const testEmail = (toEmail: string): Promise<ApiResponse> => {
|
||||||
|
return request({
|
||||||
|
url: '/api/admin/configs/test-email',
|
||||||
|
method: 'post',
|
||||||
|
data: { toEmail }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateCaptcha = (): Promise<ApiResponse<CaptchaData>> => {
|
||||||
|
return request({
|
||||||
|
url: '/api/admin/captcha',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const verifyCaptcha = (captchaId: string, code: string): Promise<ApiResponse> => {
|
||||||
|
return request({
|
||||||
|
url: '/api/admin/captcha/verify',
|
||||||
|
method: 'post',
|
||||||
|
data: { captchaId, code }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const checkCaptchaEnabled = (): Promise<ApiResponse<CaptchaEnabled>> => {
|
||||||
|
return request({
|
||||||
|
url: '/api/admin/captcha/enabled',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
|
import type { ApiResponse, CaptchaData, CaptchaEnabled } from '@/types/api'
|
||||||
|
|
||||||
export const login = (username: string, password: string) => {
|
export const login = (username: string, password: string, captchaId?: string, captchaCode?: string) => {
|
||||||
return request({
|
return request({
|
||||||
url: '/api/player/login',
|
url: '/api/player/login',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: { username, password }
|
data: { username, password, captchaId, captchaCode }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,3 +22,32 @@ export const getAccountInfo = () => {
|
|||||||
method: 'get'
|
method: 'get'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const generateCaptcha = (): Promise<ApiResponse<CaptchaData>> => {
|
||||||
|
return request({
|
||||||
|
url: '/api/player/captcha',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const verifyCaptcha = (captchaId: string, code: string): Promise<ApiResponse> => {
|
||||||
|
return request({
|
||||||
|
url: '/api/player/captcha/verify',
|
||||||
|
method: 'post',
|
||||||
|
data: { captchaId, code }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const checkCaptchaEnabled = (): Promise<ApiResponse<CaptchaEnabled>> => {
|
||||||
|
return request({
|
||||||
|
url: '/api/player/captcha/enabled',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const checkPlayerServiceStatus = (): Promise<ApiResponse<{ enabled: boolean; closeMsg: string }>> => {
|
||||||
|
return request({
|
||||||
|
url: '/api/player/service-status',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ import { ref, h } from 'vue'
|
|||||||
import { NLayout, NLayoutSider, NLayoutHeader, NLayoutContent, NLayoutFooter, NMenu, NDropdown } from 'naive-ui'
|
import { NLayout, NLayoutSider, NLayoutHeader, NLayoutContent, NLayoutFooter, NMenu, NDropdown } from 'naive-ui'
|
||||||
import { RouterLink, useRoute, useRouter } from 'vue-router'
|
import { RouterLink, useRoute, useRouter } from 'vue-router'
|
||||||
import { useAdminStore } from '@/stores/admin'
|
import { useAdminStore } from '@/stores/admin'
|
||||||
import { RiDashboardLine, RiArrowDownSLine, RiUserLine, RiLogoutBoxRLine, RiSettings3Line, RiUserSettingsLine } from '@remixicon/vue'
|
import { RiDashboardLine, RiArrowDownSLine, RiUserLine, RiLogoutBoxRLine, RiSettings3Line, RiUserSettingsLine, RiToolsLine } from '@remixicon/vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -71,6 +71,11 @@ const menuOptions = [
|
|||||||
label: () => h(RouterLink, { to: '/admin/user-management' }, { default: () => '用户管理' }),
|
label: () => h(RouterLink, { to: '/admin/user-management' }, { default: () => '用户管理' }),
|
||||||
key: 'UserManagement',
|
key: 'UserManagement',
|
||||||
icon: () => h(RiUserSettingsLine, { size: '20px' })
|
icon: () => h(RiUserSettingsLine, { size: '20px' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: () => h(RouterLink, { to: '/admin/system-config' }, { default: () => '系统配置' }),
|
||||||
|
key: 'SystemConfig',
|
||||||
|
icon: () => h(RiToolsLine, { size: '20px' })
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ const routes = [
|
|||||||
name: 'UserManagement',
|
name: 'UserManagement',
|
||||||
component: () => import('@/views/admin/UserManagement.vue'),
|
component: () => import('@/views/admin/UserManagement.vue'),
|
||||||
meta: { title: '用户管理', requiresAdminAuth: true }
|
meta: { title: '用户管理', requiresAdminAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'system-config',
|
||||||
|
name: 'SystemConfig',
|
||||||
|
component: () => import('@/views/admin/SystemConfig.vue'),
|
||||||
|
meta: { title: '系统配置', requiresAdminAuth: true }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ export const useAdminStore = defineStore('admin', () => {
|
|||||||
localStorage.removeItem('admin_userInfo')
|
localStorage.removeItem('admin_userInfo')
|
||||||
}
|
}
|
||||||
|
|
||||||
const login = async (username: string, password: string) => {
|
const login = async (username: string, password: string, captchaId?: string, captchaCode?: string) => {
|
||||||
try {
|
try {
|
||||||
const data = await loginApi(username, password)
|
const data = await loginApi(username, password, captchaId, captchaCode)
|
||||||
if (data.success && data.data) {
|
if (data.success && data.data) {
|
||||||
setToken(data.data.token)
|
setToken(data.data.token)
|
||||||
setUserInfo(data.data.user)
|
setUserInfo(data.data.user)
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
sessionStorage.removeItem('player_token')
|
sessionStorage.removeItem('player_token')
|
||||||
}
|
}
|
||||||
|
|
||||||
const login = async (username: string, password: string) => {
|
const login = async (username: string, password: string, captchaId?: string, captchaCode?: string) => {
|
||||||
const response = await loginApi(username, password)
|
const response = await loginApi(username, password, captchaId, captchaCode)
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
setToken(response.data)
|
setToken(response.data)
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -33,3 +33,40 @@ export interface UserInfo {
|
|||||||
realName?: string
|
realName?: string
|
||||||
roleId: number
|
roleId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统配置项
|
||||||
|
*/
|
||||||
|
export interface ConfigItem {
|
||||||
|
id: number
|
||||||
|
configKey: string
|
||||||
|
configValue: string
|
||||||
|
configType: string
|
||||||
|
description?: string
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统配置分组
|
||||||
|
*/
|
||||||
|
export interface ConfigGroup {
|
||||||
|
type: string
|
||||||
|
title: string
|
||||||
|
configs: ConfigItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码响应数据
|
||||||
|
*/
|
||||||
|
export interface CaptchaData {
|
||||||
|
captchaId: string
|
||||||
|
svg: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码启用状态
|
||||||
|
*/
|
||||||
|
export interface CaptchaEnabled {
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,16 @@
|
|||||||
placeholder="请输入密码"
|
placeholder="请输入密码"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
<n-form-item v-if="captchaEnabled" path="captchaCode" label="验证码">
|
||||||
|
<n-space style="width: 100%">
|
||||||
|
<n-input
|
||||||
|
v-model:value="formValue.captchaCode"
|
||||||
|
placeholder="请输入验证码"
|
||||||
|
@keyup.enter="handleLogin"
|
||||||
|
/>
|
||||||
|
<div class="captcha-image" @click="handleRefreshCaptcha" v-html="captchaSvg"></div>
|
||||||
|
</n-space>
|
||||||
|
</n-form-item>
|
||||||
<n-form-item>
|
<n-form-item>
|
||||||
<n-button type="primary" block @click="handleLogin" :loading="loading">
|
<n-button type="primary" block @click="handleLogin" :loading="loading">
|
||||||
登录
|
登录
|
||||||
@@ -24,10 +34,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { NCard, NForm, NFormItem, NInput, NButton, useMessage } from 'naive-ui'
|
import { NCard, NForm, NFormItem, NInput, NButton, NSpace, useMessage } from 'naive-ui'
|
||||||
import { useAdminStore } from '@/stores/admin'
|
import { useAdminStore } from '@/stores/admin'
|
||||||
|
import { generateCaptcha, checkCaptchaEnabled } from '@/api/admin'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
@@ -35,10 +46,14 @@ const adminStore = useAdminStore()
|
|||||||
|
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const captchaEnabled = ref(false)
|
||||||
|
const captchaSvg = ref('')
|
||||||
|
const captchaId = ref('')
|
||||||
|
|
||||||
const formValue = ref({
|
const formValue = ref({
|
||||||
username: '',
|
username: '',
|
||||||
password: ''
|
password: '',
|
||||||
|
captchaCode: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
@@ -51,6 +66,17 @@ const rules = {
|
|||||||
required: true,
|
required: true,
|
||||||
message: '请输入密码',
|
message: '请输入密码',
|
||||||
trigger: 'blur'
|
trigger: 'blur'
|
||||||
|
},
|
||||||
|
captchaCode: {
|
||||||
|
required: true,
|
||||||
|
message: '请输入验证码',
|
||||||
|
trigger: 'blur',
|
||||||
|
validator: (rule: any, value: string) => {
|
||||||
|
if (captchaEnabled.value && !value) {
|
||||||
|
return new Error('请输入验证码')
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,20 +85,62 @@ const handleLogin = async () => {
|
|||||||
await formRef.value?.validate()
|
await formRef.value?.validate()
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
const result = await adminStore.login(formValue.value.username, formValue.value.password)
|
const result = await adminStore.login(
|
||||||
|
formValue.value.username,
|
||||||
|
formValue.value.password,
|
||||||
|
captchaId.value,
|
||||||
|
formValue.value.captchaCode
|
||||||
|
)
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
message.success(result.message)
|
message.success(result.message)
|
||||||
router.push('/admin/dashboard')
|
router.push('/admin/dashboard')
|
||||||
} else {
|
} else {
|
||||||
message.error(result.message)
|
message.error(result.message)
|
||||||
|
if (captchaEnabled.value) {
|
||||||
|
handleRefreshCaptcha()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('登录错误:', error)
|
console.error('登录错误:', error)
|
||||||
|
if (captchaEnabled.value) {
|
||||||
|
handleRefreshCaptcha()
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleRefreshCaptcha = async () => {
|
||||||
|
try {
|
||||||
|
const response = await generateCaptcha()
|
||||||
|
if (response.success && response.data) {
|
||||||
|
captchaSvg.value = response.data.svg
|
||||||
|
captchaId.value = response.data.captchaId
|
||||||
|
formValue.value.captchaCode = ''
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取验证码失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkCaptchaStatus = async () => {
|
||||||
|
try {
|
||||||
|
const response = await checkCaptchaEnabled()
|
||||||
|
if (response.success && response.data) {
|
||||||
|
captchaEnabled.value = response.data.enabled
|
||||||
|
if (captchaEnabled.value) {
|
||||||
|
handleRefreshCaptcha()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查验证码状态失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
checkCaptchaStatus()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -83,4 +151,25 @@ const handleLogin = async () => {
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.captcha-image {
|
||||||
|
width: 120px;
|
||||||
|
height: 40px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-image:hover {
|
||||||
|
border-color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-image :deep(svg) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
447
frontend/src/views/admin/SystemConfig.vue
Normal file
447
frontend/src/views/admin/SystemConfig.vue
Normal file
@@ -0,0 +1,447 @@
|
|||||||
|
<template>
|
||||||
|
<div class="system-config-container">
|
||||||
|
<n-card title="系统配置" class="config-card">
|
||||||
|
<n-tabs v-model:value="activeTab" type="line" animated>
|
||||||
|
<!-- 基础配置标签页 -->
|
||||||
|
<n-tab-pane name="basic" tab="基础配置">
|
||||||
|
<n-form
|
||||||
|
ref="basicFormRef"
|
||||||
|
:model="basicForm"
|
||||||
|
label-placement="left"
|
||||||
|
label-width="180px"
|
||||||
|
:rules="basicRules"
|
||||||
|
>
|
||||||
|
<n-grid :cols="24" :x-gap="24">
|
||||||
|
<n-gi :span="12">
|
||||||
|
<n-form-item label="后端域名(IP)地址" path="backend_host">
|
||||||
|
<n-input v-model:value="basicForm.backend_host" placeholder="请输入后端IP地址" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi :span="12">
|
||||||
|
<n-form-item label="后端端口" path="backend_port">
|
||||||
|
<n-input-number v-model:value="basicForm.backend_port" :min="1" :max="65535" style="width: 100%" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi :span="12">
|
||||||
|
<n-form-item label="日志级别" path="log_level">
|
||||||
|
<n-select
|
||||||
|
v-model:value="basicForm.log_level"
|
||||||
|
:options="logLevelOptions"
|
||||||
|
placeholder="请选择日志级别"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi :span="12">
|
||||||
|
<n-form-item label="登录验证码" path="login_captcha_enabled">
|
||||||
|
<n-switch v-model:value="basicForm.login_captcha_enabled" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi :span="12">
|
||||||
|
<n-form-item label="玩家服务中心开启访问" path="player_service_enabled">
|
||||||
|
<n-switch v-model:value="basicForm.player_service_enabled" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi :span="24">
|
||||||
|
<n-form-item label="玩家服务中心关闭提示" path="player_service_close_msg">
|
||||||
|
<n-input
|
||||||
|
v-model:value="basicForm.player_service_close_msg"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="请输入玩家服务中心关闭提示文本"
|
||||||
|
:rows="3"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</n-gi>
|
||||||
|
</n-grid>
|
||||||
|
</n-form>
|
||||||
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<!-- 安全配置标签页 -->
|
||||||
|
<n-tab-pane name="security" tab="安全配置">
|
||||||
|
<n-form
|
||||||
|
ref="securityFormRef"
|
||||||
|
:model="securityForm"
|
||||||
|
label-placement="left"
|
||||||
|
label-width="180px"
|
||||||
|
:rules="securityRules"
|
||||||
|
>
|
||||||
|
<n-grid :cols="24" :x-gap="24">
|
||||||
|
<n-gi :span="24">
|
||||||
|
<n-form-item label="跨域地址" path="cors_origin">
|
||||||
|
<n-input v-model:value="securityForm.cors_origin" placeholder="请输入跨域地址" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi :span="12">
|
||||||
|
<n-form-item label="JWT密钥" path="jwt_secret">
|
||||||
|
<n-input v-model:value="securityForm.jwt_secret" type="password" placeholder="请输入JWT密钥" show-password-on="click" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi :span="12">
|
||||||
|
<n-form-item label="JWT有效期" path="jwt_expires_in">
|
||||||
|
<n-input v-model:value="securityForm.jwt_expires_in" placeholder="如: 2h, 7d, 30m" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-gi>
|
||||||
|
</n-grid>
|
||||||
|
</n-form>
|
||||||
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<!-- 游戏配置标签页 -->
|
||||||
|
<n-tab-pane name="game" tab="游戏配置">
|
||||||
|
<n-form
|
||||||
|
ref="gameFormRef"
|
||||||
|
:model="gameForm"
|
||||||
|
label-placement="left"
|
||||||
|
label-width="180px"
|
||||||
|
:rules="gameRules"
|
||||||
|
>
|
||||||
|
<n-grid :cols="24" :x-gap="24">
|
||||||
|
<n-gi :span="24">
|
||||||
|
<n-form-item label="游戏服务端代理地址" path="game_server_proxy_url">
|
||||||
|
<n-input v-model:value="gameForm.game_server_proxy_url" placeholder="请输入游戏服务端代理地址" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi :span="24">
|
||||||
|
<n-form-item label="游戏服务端PSK密钥" path="game_server_psk">
|
||||||
|
<n-input v-model:value="gameForm.game_server_psk" type="password" placeholder="请输入游戏服务端PSK密钥" show-password-on="click" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-gi>
|
||||||
|
</n-grid>
|
||||||
|
</n-form>
|
||||||
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<!-- 充值配置标签页 -->
|
||||||
|
<n-tab-pane name="payment" tab="充值配置">
|
||||||
|
<div class="development-notice">
|
||||||
|
<n-result status="info" title="功能开发中" description="充值配置功能正在开发中,敬请期待">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="RiToolsLine" size="64" />
|
||||||
|
</template>
|
||||||
|
</n-result>
|
||||||
|
</div>
|
||||||
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<!-- 邮件配置标签页 -->
|
||||||
|
<n-tab-pane name="email" tab="邮件配置">
|
||||||
|
<n-form
|
||||||
|
ref="emailFormRef"
|
||||||
|
:model="emailForm"
|
||||||
|
label-placement="left"
|
||||||
|
label-width="180px"
|
||||||
|
:rules="emailRules"
|
||||||
|
>
|
||||||
|
<n-grid :cols="24" :x-gap="24">
|
||||||
|
<n-gi :span="12">
|
||||||
|
<n-form-item label="发件人邮箱" path="mail_from">
|
||||||
|
<n-input v-model:value="emailForm.mail_from" placeholder="请输入发件人邮箱" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi :span="12">
|
||||||
|
<n-form-item label="SMTP服务器地址" path="mail_smtp_host">
|
||||||
|
<n-input v-model:value="emailForm.mail_smtp_host" placeholder="请输入SMTP服务器地址" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi :span="12">
|
||||||
|
<n-form-item label="SMTP端口" path="mail_smtp_port">
|
||||||
|
<n-input-number v-model:value="emailForm.mail_smtp_port" :min="1" :max="65535" style="width: 100%" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi :span="12">
|
||||||
|
<n-form-item label="SMTP用户名" path="mail_smtp_user">
|
||||||
|
<n-input v-model:value="emailForm.mail_smtp_user" placeholder="请输入SMTP用户名" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi :span="24">
|
||||||
|
<n-form-item label="SMTP密码" path="mail_smtp_password">
|
||||||
|
<n-input v-model:value="emailForm.mail_smtp_password" type="password" placeholder="请输入SMTP密码" show-password-on="click" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-gi>
|
||||||
|
</n-grid>
|
||||||
|
|
||||||
|
<!-- 邮件测试区域 -->
|
||||||
|
<n-divider />
|
||||||
|
<n-space vertical style="width: 100%">
|
||||||
|
<n-alert type="info" title="邮件测试">
|
||||||
|
配置完成后,可以发送测试邮件验证配置是否正确。
|
||||||
|
</n-alert>
|
||||||
|
<n-space>
|
||||||
|
<n-input v-model:value="testEmailInput" placeholder="请输入测试邮箱地址" style="width: 300px" />
|
||||||
|
<n-button type="primary" :loading="emailTestLoading" @click="handleTestEmail">
|
||||||
|
发送测试邮件
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-space>
|
||||||
|
</n-form>
|
||||||
|
</n-tab-pane>
|
||||||
|
</n-tabs>
|
||||||
|
|
||||||
|
<!-- 底部操作按钮 -->
|
||||||
|
<template #footer>
|
||||||
|
<n-space justify="end">
|
||||||
|
<n-button @click="handleReset">重置</n-button>
|
||||||
|
<n-button type="primary" :loading="saveLoading" @click="handleSave">保存配置</n-button>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { NCard, NTabs, NTabPane, NForm, NFormItem, NInput, NInputNumber, NSelect, NSwitch, NGrid, NGi, NSpace, NButton, NDivider, NAlert, NResult, NIcon, useMessage } from 'naive-ui'
|
||||||
|
import { RiToolsLine } from '@remixicon/vue'
|
||||||
|
import { getAllConfigs, updateConfigs, testEmail as sendTestEmail } from '@/api/admin'
|
||||||
|
import type { ConfigItem } from '@/types/api'
|
||||||
|
import type { FormRules } from 'naive-ui'
|
||||||
|
|
||||||
|
// 消息提示
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
// 当前激活的标签页
|
||||||
|
const activeTab = ref('basic')
|
||||||
|
|
||||||
|
// 基础配置表单
|
||||||
|
const basicForm = reactive({
|
||||||
|
backend_host: '',
|
||||||
|
backend_port: 3000,
|
||||||
|
log_level: 'info',
|
||||||
|
login_captcha_enabled: true,
|
||||||
|
player_service_enabled: true,
|
||||||
|
player_service_close_msg: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 安全配置表单
|
||||||
|
const securityForm = reactive({
|
||||||
|
cors_origin: '',
|
||||||
|
jwt_secret: '',
|
||||||
|
jwt_expires_in: '2h'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 游戏配置表单
|
||||||
|
const gameForm = reactive({
|
||||||
|
game_server_proxy_url: '',
|
||||||
|
game_server_psk: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 邮件配置表单
|
||||||
|
const emailForm = reactive({
|
||||||
|
mail_from: '',
|
||||||
|
mail_smtp_host: '',
|
||||||
|
mail_smtp_port: 587,
|
||||||
|
mail_smtp_user: '',
|
||||||
|
mail_smtp_password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表单引用
|
||||||
|
const basicFormRef = ref()
|
||||||
|
const securityFormRef = ref()
|
||||||
|
const gameFormRef = ref()
|
||||||
|
const emailFormRef = ref()
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const basicRules: FormRules = {
|
||||||
|
backend_host: { required: true, message: '请输入后端IP地址', trigger: 'blur' },
|
||||||
|
backend_port: { required: true, type: 'number', message: '请输入后端端口', trigger: 'blur' },
|
||||||
|
log_level: { required: true, message: '请选择日志级别', trigger: 'change' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const securityRules: FormRules = {
|
||||||
|
cors_origin: { required: true, message: '请输入跨域地址', trigger: 'blur' },
|
||||||
|
jwt_secret: { required: true, message: '请输入JWT密钥', trigger: 'blur' },
|
||||||
|
jwt_expires_in: { required: true, message: '请输入JWT有效期', trigger: 'blur' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const gameRules: FormRules = {
|
||||||
|
game_server_proxy_url: { required: true, message: '请输入游戏服务端代理地址', trigger: 'blur' },
|
||||||
|
game_server_psk: { required: true, message: '请输入游戏服务端PSK密钥', trigger: 'blur' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailRules: FormRules = {
|
||||||
|
mail_from: { required: true, message: '请输入发件人邮箱', trigger: 'blur' },
|
||||||
|
mail_smtp_host: { required: true, message: '请输入SMTP服务器地址', trigger: 'blur' },
|
||||||
|
mail_smtp_port: { required: true, type: 'number', message: '请输入SMTP端口', trigger: 'blur' },
|
||||||
|
mail_smtp_user: { required: true, message: '请输入SMTP用户名', trigger: 'blur' },
|
||||||
|
mail_smtp_password: { required: true, message: '请输入SMTP密码', trigger: 'blur' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日志级别选项
|
||||||
|
const logLevelOptions = [
|
||||||
|
{ label: 'info', value: 'info' },
|
||||||
|
{ label: 'debug', value: 'debug' },
|
||||||
|
{ label: 'error', value: 'error' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 测试邮箱输入
|
||||||
|
const testEmailInput = ref('')
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
const saveLoading = ref(false)
|
||||||
|
const emailTestLoading = ref(false)
|
||||||
|
|
||||||
|
// 配置项映射
|
||||||
|
const configMap: Record<string, any> = {
|
||||||
|
// 基础配置
|
||||||
|
backend_host: basicForm,
|
||||||
|
backend_port: basicForm,
|
||||||
|
log_level: basicForm,
|
||||||
|
login_captcha_enabled: basicForm,
|
||||||
|
player_service_enabled: basicForm,
|
||||||
|
player_service_close_msg: basicForm,
|
||||||
|
// 安全配置
|
||||||
|
cors_origin: securityForm,
|
||||||
|
jwt_secret: securityForm,
|
||||||
|
jwt_expires_in: securityForm,
|
||||||
|
// 游戏配置
|
||||||
|
game_server_proxy_url: gameForm,
|
||||||
|
game_server_psk: gameForm,
|
||||||
|
// 邮件配置
|
||||||
|
mail_from: emailForm,
|
||||||
|
mail_smtp_host: emailForm,
|
||||||
|
mail_smtp_port: emailForm,
|
||||||
|
mail_smtp_user: emailForm,
|
||||||
|
mail_smtp_password: emailForm
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取配置列表
|
||||||
|
const fetchConfigs = async () => {
|
||||||
|
try {
|
||||||
|
const response = await getAllConfigs()
|
||||||
|
|
||||||
|
if (response.success && response.data) {
|
||||||
|
const groupedData = response.data as any
|
||||||
|
|
||||||
|
const allConfigs: ConfigItem[] = []
|
||||||
|
Object.keys(groupedData).forEach(type => {
|
||||||
|
if (Array.isArray(groupedData[type])) {
|
||||||
|
allConfigs.push(...groupedData[type])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
allConfigs.forEach((config: ConfigItem) => {
|
||||||
|
const form = configMap[config.configKey]
|
||||||
|
if (form) {
|
||||||
|
const value = config.configValue
|
||||||
|
if (typeof form[config.configKey] === 'boolean') {
|
||||||
|
form[config.configKey] = value === 'true'
|
||||||
|
} else if (typeof form[config.configKey] === 'number') {
|
||||||
|
form[config.configKey] = Number(value)
|
||||||
|
} else {
|
||||||
|
form[config.configKey] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '获取配置失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('获取配置失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存配置
|
||||||
|
const handleSave = async () => {
|
||||||
|
try {
|
||||||
|
saveLoading.value = true
|
||||||
|
|
||||||
|
// 验证所有表单
|
||||||
|
try {
|
||||||
|
await basicFormRef.value?.validate()
|
||||||
|
} catch (error) {
|
||||||
|
message.error('请检查基础配置中的必填项')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await securityFormRef.value?.validate()
|
||||||
|
} catch (error) {
|
||||||
|
message.error('请检查安全配置中的必填项')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await gameFormRef.value?.validate()
|
||||||
|
} catch (error) {
|
||||||
|
message.error('请检查游戏配置中的必填项')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await emailFormRef.value?.validate()
|
||||||
|
} catch (error) {
|
||||||
|
message.error('请检查邮件配置中的必填项')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建配置更新数据
|
||||||
|
const configs = []
|
||||||
|
for (const [key, form] of Object.entries(configMap)) {
|
||||||
|
const value = form[key]
|
||||||
|
configs.push({
|
||||||
|
configKey: key,
|
||||||
|
configValue: String(value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await updateConfigs(configs)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
message.success('配置保存成功')
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '配置保存失败')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('保存配置异常:', error)
|
||||||
|
message.error(error.message || '配置保存失败')
|
||||||
|
} finally {
|
||||||
|
saveLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试邮件
|
||||||
|
const handleTestEmail = async () => {
|
||||||
|
if (!testEmailInput.value) {
|
||||||
|
message.warning('请输入测试邮箱地址')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
emailTestLoading.value = true
|
||||||
|
const response = await sendTestEmail(testEmailInput.value)
|
||||||
|
if (response.success) {
|
||||||
|
message.success('测试邮件发送成功,请检查邮箱')
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '测试邮件发送失败')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.response?.data?.message || '测试邮件发送失败')
|
||||||
|
} finally {
|
||||||
|
emailTestLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置配置
|
||||||
|
const handleReset = async () => {
|
||||||
|
await fetchConfigs()
|
||||||
|
message.info('配置已重置')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时获取配置
|
||||||
|
onMounted(() => {
|
||||||
|
fetchConfigs()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.system-config-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-card {
|
||||||
|
min-height: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.development-notice {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="login-container">
|
<div class="login-container">
|
||||||
<n-card title="玩家登录" style="width: 400px;">
|
<!-- 维护公告遮罩 -->
|
||||||
|
<div v-if="!playerServiceEnabled" class="maintenance-overlay">
|
||||||
|
<n-card title="系统维护公告" style="width: 500px; text-align: center;">
|
||||||
|
<template #header-extra>
|
||||||
|
<n-icon :component="RiAlertLine" size="32" color="#f0a020" />
|
||||||
|
</template>
|
||||||
|
<p style="font-size: 16px; color: #666;">{{ playerServiceCloseMsg }}</p>
|
||||||
|
<template #footer>
|
||||||
|
<n-button type="primary" @click="handleRefreshStatus">刷新状态</n-button>
|
||||||
|
</template>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 登录表单 -->
|
||||||
|
<n-card v-else title="玩家登录" style="width: 400px;">
|
||||||
<n-form ref="formRef" :model="formValue" :rules="rules" size="large">
|
<n-form ref="formRef" :model="formValue" :rules="rules" size="large">
|
||||||
<n-form-item path="username" label="用户名">
|
<n-form-item path="username" label="用户名">
|
||||||
<n-input v-model:value="formValue.username" placeholder="请输入用户名" />
|
<n-input v-model:value="formValue.username" placeholder="请输入用户名" />
|
||||||
@@ -13,6 +27,16 @@
|
|||||||
placeholder="请输入密码"
|
placeholder="请输入密码"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
<n-form-item v-if="captchaEnabled" path="captchaCode" label="验证码">
|
||||||
|
<n-space style="width: 100%">
|
||||||
|
<n-input
|
||||||
|
v-model:value="formValue.captchaCode"
|
||||||
|
placeholder="请输入验证码"
|
||||||
|
@keyup.enter="handleLogin"
|
||||||
|
/>
|
||||||
|
<div class="captcha-image" @click="handleRefreshCaptcha" v-html="captchaSvg"></div>
|
||||||
|
</n-space>
|
||||||
|
</n-form-item>
|
||||||
<n-form-item>
|
<n-form-item>
|
||||||
<n-button type="primary" block @click="handleLogin" :loading="loading">
|
<n-button type="primary" block @click="handleLogin" :loading="loading">
|
||||||
登录
|
登录
|
||||||
@@ -24,10 +48,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { NCard, NForm, NFormItem, NInput, NButton, useMessage } from 'naive-ui'
|
import { NCard, NForm, NFormItem, NInput, NButton, NSpace, NIcon, useMessage } from 'naive-ui'
|
||||||
|
import { RiAlertLine } from '@remixicon/vue'
|
||||||
import { usePlayerStore } from '@/stores/player'
|
import { usePlayerStore } from '@/stores/player'
|
||||||
|
import { generateCaptcha, checkCaptchaEnabled, checkPlayerServiceStatus } from '@/api/player'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
@@ -35,10 +61,16 @@ const playerStore = usePlayerStore()
|
|||||||
|
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const captchaEnabled = ref(false)
|
||||||
|
const captchaSvg = ref('')
|
||||||
|
const captchaId = ref('')
|
||||||
|
const playerServiceEnabled = ref(true)
|
||||||
|
const playerServiceCloseMsg = ref('玩家服务中心系统维护中')
|
||||||
|
|
||||||
const formValue = ref({
|
const formValue = ref({
|
||||||
username: '',
|
username: '',
|
||||||
password: ''
|
password: '',
|
||||||
|
captchaCode: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
@@ -51,6 +83,17 @@ const rules = {
|
|||||||
required: true,
|
required: true,
|
||||||
message: '请输入密码',
|
message: '请输入密码',
|
||||||
trigger: 'blur'
|
trigger: 'blur'
|
||||||
|
},
|
||||||
|
captchaCode: {
|
||||||
|
required: true,
|
||||||
|
message: '请输入验证码',
|
||||||
|
trigger: 'blur',
|
||||||
|
validator: (_rule: any, value: string) => {
|
||||||
|
if (captchaEnabled.value && !value) {
|
||||||
|
return new Error('请输入验证码')
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,20 +102,82 @@ const handleLogin = async () => {
|
|||||||
await formRef.value?.validate()
|
await formRef.value?.validate()
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
const success = await playerStore.login(formValue.value.username, formValue.value.password)
|
const success = await playerStore.login(
|
||||||
|
formValue.value.username,
|
||||||
|
formValue.value.password,
|
||||||
|
captchaId.value,
|
||||||
|
formValue.value.captchaCode
|
||||||
|
)
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
message.success('登录成功')
|
message.success('登录成功')
|
||||||
router.push('/player/dashboard')
|
router.push('/player/dashboard')
|
||||||
} else {
|
} else {
|
||||||
message.error('登录失败,请检查用户名和密码')
|
message.error('登录失败,请检查用户名和密码')
|
||||||
|
if (captchaEnabled.value) {
|
||||||
|
handleRefreshCaptcha()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
}
|
||||||
|
} catch (error: any) {
|
||||||
console.error('登录错误:', error)
|
console.error('登录错误:', error)
|
||||||
|
const errorMessage = error.response?.data?.message || error.message || '登录失败,请稍后重试'
|
||||||
|
message.error(errorMessage)
|
||||||
|
if (captchaEnabled.value) {
|
||||||
|
handleRefreshCaptcha()
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleRefreshCaptcha = async () => {
|
||||||
|
try {
|
||||||
|
const response = await generateCaptcha()
|
||||||
|
if (response.success && response.data) {
|
||||||
|
captchaSvg.value = response.data.svg
|
||||||
|
captchaId.value = response.data.captchaId
|
||||||
|
formValue.value.captchaCode = ''
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取验证码失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkCaptchaStatus = async () => {
|
||||||
|
try {
|
||||||
|
const response = await checkCaptchaEnabled()
|
||||||
|
if (response.success && response.data) {
|
||||||
|
captchaEnabled.value = response.data.enabled
|
||||||
|
if (captchaEnabled.value) {
|
||||||
|
handleRefreshCaptcha()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查验证码状态失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkServiceStatus = async () => {
|
||||||
|
try {
|
||||||
|
const response = await checkPlayerServiceStatus()
|
||||||
|
if (response.success && response.data) {
|
||||||
|
playerServiceEnabled.value = response.data.enabled
|
||||||
|
playerServiceCloseMsg.value = response.data.closeMsg
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查玩家服务中心状态失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRefreshStatus = async () => {
|
||||||
|
await checkServiceStatus()
|
||||||
|
message.info('状态已刷新')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
checkCaptchaStatus()
|
||||||
|
checkServiceStatus()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -83,4 +188,38 @@ const handleLogin = async () => {
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.maintenance-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-image {
|
||||||
|
width: 120px;
|
||||||
|
height: 40px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-image:hover {
|
||||||
|
border-color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-image :deep(svg) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user