项目初始化
This commit is contained in:
26
backend/.env.example
Normal file
26
backend/.env.example
Normal file
@@ -0,0 +1,26 @@
|
||||
# 服务器配置
|
||||
PORT=3000
|
||||
HOST=0.0.0.0
|
||||
|
||||
# 数据库配置
|
||||
DB_HOST=database
|
||||
DB_PORT=3306
|
||||
DB_NAME=mhxy_web
|
||||
DB_USER=root
|
||||
DB_PASSWORD=password
|
||||
|
||||
# JWT配置
|
||||
JWT_SECRET=your_jwt_secret_key_here
|
||||
JWT_EXPIRES_IN=24h
|
||||
JWT_REFRESH_SECRET=your_jwt_refresh_secret_key_here
|
||||
JWT_REFRESH_EXPIRES_IN=7d
|
||||
|
||||
# 游戏服务端API配置
|
||||
GAME_API_URL=http://your_game_server_url/tool/http
|
||||
GAME_PSK=THIS_IS_A_32_BYTE_FIXED_PSK!!!!! # 必须是32字节固定长度
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL=info
|
||||
|
||||
# 环境配置
|
||||
NODE_ENV=production
|
||||
43
backend/Dockerfile
Normal file
43
backend/Dockerfile
Normal file
@@ -0,0 +1,43 @@
|
||||
# 多阶段构建:构建阶段
|
||||
FROM node:18-alpine AS build-stage
|
||||
WORKDIR /app
|
||||
|
||||
# 设置国内镜像源以加速依赖安装(可选,根据实际情况调整)
|
||||
# RUN npm config set registry https://registry.npmmirror.com
|
||||
|
||||
# 复制package.json和package-lock.json
|
||||
COPY package*.json ./
|
||||
|
||||
# 安装所有依赖(包括开发依赖)
|
||||
RUN npm ci
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 编译TypeScript代码
|
||||
RUN npm run build
|
||||
|
||||
# 多阶段构建:运行阶段
|
||||
FROM node:18-alpine AS production-stage
|
||||
WORKDIR /app
|
||||
|
||||
# 设置时区(可选,根据需要调整)
|
||||
# RUN apk --no-cache add tzdata && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
|
||||
|
||||
# 复制package.json和package-lock.json
|
||||
COPY package*.json ./
|
||||
|
||||
# 安装仅生产依赖
|
||||
RUN npm ci --only=production
|
||||
|
||||
# 复制编译结果
|
||||
COPY --from=build-stage /app/dist ./dist
|
||||
|
||||
# 复制环境变量示例文件(如果存在)
|
||||
COPY .env.example .env
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 3000
|
||||
|
||||
# 启动应用
|
||||
CMD ["node", "dist/index.js"]
|
||||
48
backend/package.json
Normal file
48
backend/package.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "game-operation-platform-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "Game operation platform backend API",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "concurrently \"tsc --watch\" \"nodemon -q dist/index.js\"",
|
||||
"typeorm": "ts-node ./node_modules/typeorm/cli.js",
|
||||
"init:admin": "ts-node src/scripts/initAdmin.ts"
|
||||
},
|
||||
"keywords": [
|
||||
"game",
|
||||
"operation",
|
||||
"platform",
|
||||
"backend"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.13.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mysql2": "^3.9.7",
|
||||
"typeorm": "^0.3.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/chai": "^5.2.3",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/deep-eql": "^4.0.2",
|
||||
"@types/estree": "^1.0.8",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"@types/node": "^20.10.5",
|
||||
"concurrently": "^9.2.1",
|
||||
"nodemon": "^3.1.11",
|
||||
"ts-node": "^10.9.2",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
37
backend/src/config/typeorm.config.ts
Normal file
37
backend/src/config/typeorm.config.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { DataSource } from 'typeorm'
|
||||
import * as dotenv from 'dotenv'
|
||||
|
||||
// 加载环境变量
|
||||
dotenv.config()
|
||||
|
||||
export const AppDataSource = new DataSource({
|
||||
type: 'mysql',
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT || '3306'),
|
||||
username: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASSWORD || 'password',
|
||||
database: process.env.DB_NAME || 'mhxy_web',
|
||||
entities: [__dirname + '/../models/*.{ts,js}'],
|
||||
migrations: [__dirname + '/../../migrations/*.{ts,js}'],
|
||||
subscribers: [],
|
||||
synchronize: true, // 生产环境建议设置为false
|
||||
logging: ['error', 'warn', 'schema'], // 只记录错误、警告和架构变更,不记录查询日志
|
||||
charset: 'utf8mb4_unicode_ci'
|
||||
})
|
||||
|
||||
// 初始化数据库连接
|
||||
export const initializeDatabase = async () => {
|
||||
try {
|
||||
console.log(`🔄 正在连接数据库 ${process.env.DB_HOST || 'localhost'}:${process.env.DB_PORT || '3306'}/${process.env.DB_NAME || 'mhxy_web'}...`)
|
||||
await AppDataSource.initialize()
|
||||
console.log('✅ 数据库连接成功')
|
||||
} catch (error) {
|
||||
console.error('❌ 数据库连接失败:', error)
|
||||
console.error('📋 请检查以下配置:')
|
||||
console.error(' • 数据库主机地址、端口是否正确')
|
||||
console.error(' • 数据库用户名、密码是否正确')
|
||||
console.error(' • 数据库服务是否正常运行')
|
||||
console.error(' • 数据库是否已创建')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
138
backend/src/index.ts
Normal file
138
backend/src/index.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import * as dotenv from 'dotenv'
|
||||
import express from 'express'
|
||||
import cors from 'cors'
|
||||
import { initializeDatabase, AppDataSource } from './config/typeorm.config'
|
||||
import authRoutes from './routes/auth.routes'
|
||||
import userRoutes from './routes/user.routes'
|
||||
import configRoutes from './routes/config.routes'
|
||||
import gameRoutes from './routes/game.routes'
|
||||
import errorHandler from './middleware/errorHandler'
|
||||
import { Config } from './models/Config'
|
||||
import { configService } from './services/ConfigService'
|
||||
|
||||
// 加载环境变量
|
||||
dotenv.config()
|
||||
|
||||
// 创建Express应用
|
||||
const app = express()
|
||||
|
||||
// 配置中间件
|
||||
app.use(cors())
|
||||
app.use(express.json())
|
||||
app.use(express.urlencoded({ extended: true }))
|
||||
|
||||
// 配置路由
|
||||
app.use('/api/auth', authRoutes)
|
||||
app.use('/api/users', userRoutes)
|
||||
app.use('/api/config', configRoutes)
|
||||
app.use('/api/game', gameRoutes)
|
||||
|
||||
// 错误处理中间件
|
||||
app.use(errorHandler)
|
||||
|
||||
// 启动信息
|
||||
console.log('🚀 正在启动梦幻西游Web管理系统后端服务...')
|
||||
console.log(`📋 环境模式: ${process.env.NODE_ENV || 'development'}`)
|
||||
console.log(`🔧 服务器配置: ${process.env.HOST || 'localhost'}:${process.env.PORT || 3001}`)
|
||||
|
||||
// 启动服务器
|
||||
let PORT = process.env.PORT || 3001
|
||||
let HOST = process.env.HOST || 'localhost'
|
||||
|
||||
/**
|
||||
* 将.env文件中的配置同步到数据库中
|
||||
* 仅在数据库中没有对应配置时才插入,保留数据库中已有的配置
|
||||
*/
|
||||
const syncEnvToDatabase = async () => {
|
||||
try {
|
||||
const configRepository = AppDataSource.getRepository(Config)
|
||||
|
||||
// 需要同步的配置项映射:.env键名 → 数据库键名
|
||||
const envToDbMapping = {
|
||||
'HOST': { key: 'server_host', description: '后端服务器主机地址' },
|
||||
'PORT': { key: 'server_port', description: '后端服务器端口' },
|
||||
'GAME_API_URL': { key: 'game_api_url', description: '游戏服务API地址' },
|
||||
'GAME_PSK': { key: 'game_psk', description: '游戏服务端的PSK' },
|
||||
'JWT_SECRET': { key: 'jwt_secret', description: 'JWT密钥' },
|
||||
'JWT_EXPIRES_IN': { key: 'jwt_expires_in', description: 'JWT过期时间' },
|
||||
'JWT_REFRESH_SECRET': { key: 'jwt_refresh_secret', description: 'JWT刷新令牌密钥' },
|
||||
'JWT_REFRESH_EXPIRES_IN': { key: 'jwt_refresh_expires_in', description: 'JWT刷新令牌过期时间' }
|
||||
}
|
||||
|
||||
// 获取数据库中已有的配置
|
||||
const existingConfigs = await configRepository.find()
|
||||
const existingConfigKeys = existingConfigs.map(config => config.key)
|
||||
|
||||
// 遍历需要同步的配置项,仅插入不存在的配置
|
||||
let syncCount = 0
|
||||
for (const [envKey, dbConfig] of Object.entries(envToDbMapping)) {
|
||||
if (!existingConfigKeys.includes(dbConfig.key)) {
|
||||
// 数据库中不存在该配置,从.env插入
|
||||
await configRepository.insert({
|
||||
key: dbConfig.key,
|
||||
value: process.env[envKey] || '',
|
||||
description: dbConfig.description
|
||||
})
|
||||
console.log(`✅ 从.env同步配置: ${envKey} → ${dbConfig.key}`)
|
||||
syncCount++
|
||||
}
|
||||
}
|
||||
|
||||
if (syncCount > 0) {
|
||||
console.log(`📊 成功同步 ${syncCount} 个配置项到数据库`)
|
||||
} else {
|
||||
console.log('📊 数据库配置已是最新,无需同步')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('同步环境变量到数据库失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const startServer = async () => {
|
||||
try {
|
||||
console.log('🔄 正在初始化数据库连接...')
|
||||
// 初始化数据库连接
|
||||
await initializeDatabase()
|
||||
|
||||
console.log('🔄 正在同步环境变量到数据库...')
|
||||
// 将.env文件中的配置同步到数据库
|
||||
await syncEnvToDatabase()
|
||||
|
||||
console.log('🔄 正在初始化配置服务...')
|
||||
// 初始化配置服务,从数据库加载配置
|
||||
await configService.initialize()
|
||||
|
||||
// 将数据库配置应用到环境变量,覆盖.env文件中的配置
|
||||
configService.applyToEnv()
|
||||
|
||||
// 更新服务器配置
|
||||
PORT = process.env.PORT || 3001
|
||||
HOST = process.env.HOST || 'localhost'
|
||||
|
||||
// 启动服务器
|
||||
app.listen(PORT, () => {
|
||||
console.log('')
|
||||
console.log('🎉 梦幻西游Web管理系统后端服务启动成功!')
|
||||
console.log(`🌐 服务地址: http://${HOST}:${PORT}`)
|
||||
console.log(`📅 启动时间: ${new Date().toLocaleString('zh-CN')}`)
|
||||
console.log('')
|
||||
console.log('📋 已启用的API端点:')
|
||||
console.log(' • 认证服务: /api/auth')
|
||||
console.log(' • 用户管理: /api/users')
|
||||
console.log(' • 系统配置: /api/config')
|
||||
console.log(' • 游戏服务: /api/game')
|
||||
console.log('')
|
||||
console.log('✨ 系统已就绪,等待客户端连接...')
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('❌ 服务器启动失败:', error)
|
||||
console.error('📋 请检查以下配置:')
|
||||
console.error(' • 数据库连接配置是否正确')
|
||||
console.error(' • 数据库服务是否正常运行')
|
||||
console.error(' • 端口是否被占用')
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// 启动服务器
|
||||
startServer()
|
||||
74
backend/src/middleware/authMiddleware.ts
Normal file
74
backend/src/middleware/authMiddleware.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Request, Response, NextFunction } from 'express'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { User } from '../models/User'
|
||||
import { AppDataSource } from '../config/typeorm.config'
|
||||
|
||||
// 定义请求类型扩展
|
||||
interface AuthRequest extends Request {
|
||||
user?: User
|
||||
}
|
||||
|
||||
// JWT认证中间件
|
||||
const authMiddleware = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
// 获取Authorization头
|
||||
const authHeader = req.header('Authorization')
|
||||
|
||||
// 检查Authorization头是否存在且格式正确
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '未授权,请提供有效的认证令牌'
|
||||
})
|
||||
}
|
||||
|
||||
// 提取令牌
|
||||
const token = authHeader.replace('Bearer ', '')
|
||||
|
||||
// 验证令牌
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { userId: number }
|
||||
|
||||
// 查找用户
|
||||
const userRepository = AppDataSource.getRepository(User)
|
||||
const user = await userRepository.findOne({ where: { id: decoded.userId } })
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '认证失败,用户不存在'
|
||||
})
|
||||
}
|
||||
|
||||
// 将用户信息添加到请求对象
|
||||
req.user = user
|
||||
|
||||
// 继续处理请求
|
||||
next()
|
||||
} catch (error) {
|
||||
if (error instanceof jwt.TokenExpiredError) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '认证令牌已过期,请重新登录'
|
||||
})
|
||||
}
|
||||
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '认证失败,请重新登录'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 管理员权限中间件
|
||||
const adminMiddleware = (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
if (!req.user || req.user.role !== 'admin') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '权限不足,需要管理员权限'
|
||||
})
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
export { authMiddleware, adminMiddleware }
|
||||
38
backend/src/middleware/errorHandler.ts
Normal file
38
backend/src/middleware/errorHandler.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Request, Response, NextFunction } from 'express'
|
||||
|
||||
// 定义错误接口
|
||||
interface AppError {
|
||||
statusCode: number
|
||||
message: string
|
||||
isOperational?: boolean
|
||||
stack?: string
|
||||
}
|
||||
|
||||
// 错误处理中间件
|
||||
const errorHandler = (err: AppError, req: Request, res: Response, next: NextFunction) => {
|
||||
// 设置默认错误状态码和消息
|
||||
const statusCode = err.statusCode || 500
|
||||
const message = err.message || 'Internal Server Error'
|
||||
|
||||
// 构建错误响应
|
||||
const errorResponse = {
|
||||
success: false,
|
||||
message,
|
||||
// 在开发环境中包含堆栈信息
|
||||
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
|
||||
}
|
||||
|
||||
// 记录错误日志
|
||||
console.error(`Error: ${message}`, {
|
||||
statusCode,
|
||||
path: req.path,
|
||||
method: req.method,
|
||||
ip: req.ip,
|
||||
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
|
||||
})
|
||||
|
||||
// 返回错误响应
|
||||
res.status(statusCode).json(errorResponse)
|
||||
}
|
||||
|
||||
export default errorHandler
|
||||
22
backend/src/models/Config.ts
Normal file
22
backend/src/models/Config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'
|
||||
|
||||
@Entity()
|
||||
export class Config {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number
|
||||
|
||||
@Column({ unique: true, length: 50 })
|
||||
key: string
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
value: string
|
||||
|
||||
@Column({ length: 100, nullable: true })
|
||||
description?: string
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date
|
||||
}
|
||||
40
backend/src/models/User.ts
Normal file
40
backend/src/models/User.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'
|
||||
|
||||
export enum UserRole {
|
||||
ADMIN = 'admin'
|
||||
}
|
||||
|
||||
export enum UserStatus {
|
||||
ACTIVE = 'ACTIVE',
|
||||
INACTIVE = 'INACTIVE'
|
||||
}
|
||||
|
||||
@Entity({ name: 'users' })
|
||||
export class User {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number
|
||||
|
||||
@Column({ unique: true, length: 50 })
|
||||
username: string
|
||||
|
||||
@Column({ length: 100 })
|
||||
password: string
|
||||
|
||||
@Column({ unique: true, nullable: true, length: 100 })
|
||||
email?: string
|
||||
|
||||
@Column({ unique: true, nullable: true, length: 20 })
|
||||
phone?: string
|
||||
|
||||
@Column({ type: 'enum', enum: UserRole, default: UserRole.ADMIN })
|
||||
role: UserRole
|
||||
|
||||
@Column({ type: 'enum', enum: UserStatus, default: UserStatus.ACTIVE })
|
||||
status: UserStatus
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date
|
||||
}
|
||||
195
backend/src/routes/auth.routes.ts
Normal file
195
backend/src/routes/auth.routes.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { Router, Request, Response } from 'express'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { User, UserRole } from '../models/User'
|
||||
import { AppDataSource } from '../config/typeorm.config'
|
||||
|
||||
const router = Router()
|
||||
const userRepository = AppDataSource.getRepository(User)
|
||||
|
||||
// 注册路由
|
||||
router.post('/register', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { username, password, email, phone, role } = req.body
|
||||
|
||||
// 检查用户名是否已存在
|
||||
const existingUser = await userRepository.findOne({ where: { username } })
|
||||
if (existingUser) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '用户名已存在'
|
||||
})
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
if (email) {
|
||||
const existingEmail = await userRepository.findOne({ where: { email } })
|
||||
if (existingEmail) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '邮箱已被注册'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 检查手机号是否已存在
|
||||
if (phone) {
|
||||
const existingPhone = await userRepository.findOne({ where: { phone } })
|
||||
if (existingPhone) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '手机号已被注册'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
const hashedPassword = await bcrypt.hash(password, 12)
|
||||
|
||||
// 创建用户
|
||||
const user = userRepository.create({
|
||||
username,
|
||||
password: hashedPassword,
|
||||
email,
|
||||
phone,
|
||||
role: role || UserRole.ADMIN
|
||||
})
|
||||
|
||||
await userRepository.save(user)
|
||||
|
||||
// 生成JWT令牌
|
||||
const token = jwt.sign(
|
||||
{ userId: user.id },
|
||||
process.env.JWT_SECRET as jwt.Secret,
|
||||
{ expiresIn: (process.env.JWT_EXPIRES_IN || '24h') as jwt.SignOptions['expiresIn'] }
|
||||
)
|
||||
|
||||
// 返回用户信息和令牌
|
||||
const { password: _, ...userWithoutPassword } = user
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '注册成功',
|
||||
data: {
|
||||
user: userWithoutPassword,
|
||||
token
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('注册失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '注册失败,请稍后重试'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 登录路由
|
||||
router.post('/login', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { username, password } = req.body
|
||||
|
||||
// 查找用户
|
||||
const user = await userRepository.findOne({ where: { username } })
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '用户名或密码错误'
|
||||
})
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const isPasswordValid = await bcrypt.compare(password, user.password)
|
||||
if (!isPasswordValid) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '用户名或密码错误'
|
||||
})
|
||||
}
|
||||
|
||||
// 生成JWT令牌
|
||||
const token = jwt.sign(
|
||||
{ userId: user.id },
|
||||
process.env.JWT_SECRET as jwt.Secret,
|
||||
{ expiresIn: (process.env.JWT_EXPIRES_IN || '24h') as jwt.SignOptions['expiresIn'] }
|
||||
)
|
||||
|
||||
// 返回用户信息和令牌
|
||||
const { password: _, ...userWithoutPassword } = user
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: '登录成功',
|
||||
data: {
|
||||
user: userWithoutPassword,
|
||||
token
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '登录失败,请稍后重试'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 获取当前用户信息路由
|
||||
router.get('/me', async (req: Request, res: Response) => {
|
||||
try {
|
||||
// 从Authorization头获取令牌
|
||||
const authHeader = req.header('Authorization')
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '未授权,请提供有效的认证令牌'
|
||||
})
|
||||
}
|
||||
|
||||
const token = authHeader.replace('Bearer ', '')
|
||||
|
||||
// 验证令牌
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { userId: number }
|
||||
|
||||
// 查找用户
|
||||
const user = await userRepository.findOne({ where: { id: decoded.userId } })
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
})
|
||||
}
|
||||
|
||||
// 返回用户信息
|
||||
const { password: _, ...userWithoutPassword } = user
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: userWithoutPassword
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof jwt.TokenExpiredError) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '认证令牌已过期,请重新登录'
|
||||
})
|
||||
}
|
||||
|
||||
console.error('获取用户信息失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取用户信息失败,请稍后重试'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 登出路由
|
||||
router.post('/logout', async (req: Request, res: Response) => {
|
||||
// 客户端需要自行删除令牌
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: '登出成功'
|
||||
})
|
||||
})
|
||||
|
||||
export default router
|
||||
118
backend/src/routes/config.routes.ts
Normal file
118
backend/src/routes/config.routes.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { Router, Request, Response } from 'express'
|
||||
import { Config } from '../models/Config'
|
||||
import { AppDataSource } from '../config/typeorm.config'
|
||||
import { authMiddleware, adminMiddleware } from '../middleware/authMiddleware'
|
||||
import { configService } from '../services/ConfigService'
|
||||
|
||||
const router = Router()
|
||||
const configRepository = AppDataSource.getRepository(Config)
|
||||
|
||||
// 获取所有配置(需要管理员权限)
|
||||
router.get('/', [authMiddleware, adminMiddleware], async (req: Request, res: Response) => {
|
||||
try {
|
||||
const configs = await configRepository.find()
|
||||
|
||||
// 转换为键值对格式
|
||||
const configMap = configs.reduce((map, config) => {
|
||||
map[config.key] = config.value
|
||||
return map
|
||||
}, {} as Record<string, string>)
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: configMap
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取配置失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取配置失败,请稍后重试'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 获取单个配置
|
||||
router.get('/:key', [authMiddleware], async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { key } = req.params
|
||||
|
||||
const config = await configRepository.findOne({ where: { key } })
|
||||
|
||||
if (!config) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '配置不存在'
|
||||
})
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
[config.key]: config.value
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取配置失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取配置失败,请稍后重试'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 设置配置(需要管理员权限)
|
||||
router.put('/:key', [authMiddleware, adminMiddleware], async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { key } = req.params
|
||||
const { value, description } = req.body
|
||||
|
||||
// 使用ConfigService更新配置,会自动处理数据库更新、.env更新和环境变量应用
|
||||
await configService.update(key, value, description)
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: '配置更新成功',
|
||||
data: {
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('更新配置失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新配置失败,请稍后重试'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 删除配置(需要管理员权限)
|
||||
router.delete('/:key', [authMiddleware, adminMiddleware], async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { key } = req.params
|
||||
|
||||
// 查找配置
|
||||
const config = await configRepository.findOne({ where: { key } })
|
||||
|
||||
if (!config) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '配置不存在'
|
||||
})
|
||||
}
|
||||
|
||||
await configRepository.remove(config)
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: '配置删除成功'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('删除配置失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除配置失败,请稍后重试'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
153
backend/src/routes/game.routes.ts
Normal file
153
backend/src/routes/game.routes.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { Router, Request, Response } from 'express'
|
||||
import axios from 'axios'
|
||||
import { authMiddleware } from '../middleware/authMiddleware'
|
||||
import * as dotenv from 'dotenv'
|
||||
|
||||
// 加载环境变量
|
||||
dotenv.config()
|
||||
|
||||
const router = Router()
|
||||
|
||||
// 游戏服务端API基础URL
|
||||
const GAME_API_URL = process.env.GAME_API_URL || 'http://127.0.0.1:8080/tool/http'
|
||||
|
||||
// 检查游戏服务状态
|
||||
router.get('/status', authMiddleware, async (req: Request, res: Response) => {
|
||||
try {
|
||||
// 获取PSK密钥
|
||||
const GAME_PSK = process.env.GAME_PSK
|
||||
if (!GAME_PSK) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '游戏服务端PSK密钥未配置'
|
||||
})
|
||||
}
|
||||
|
||||
// 发送测试请求到游戏服务端API
|
||||
const response = await axios.post(GAME_API_URL, { code: 'account/get_account_list', page: 1, pageSize: 1 }, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'psk': GAME_PSK // 添加PSK认证头
|
||||
},
|
||||
timeout: 5000, // 设置5秒超时
|
||||
proxy: false // 禁用代理,确保请求直接发送到目标服务器
|
||||
})
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: '游戏服务端连接成功',
|
||||
data: {
|
||||
status: 'online',
|
||||
responseTime: response.config.timeout
|
||||
}
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
console.error('游戏服务状态检查失败:', error)
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
// Axios错误处理
|
||||
if (error.code === 'ECONNREFUSED') {
|
||||
return res.status(200).json({
|
||||
success: false,
|
||||
message: '游戏服务端连接失败',
|
||||
data: {
|
||||
status: 'offline',
|
||||
error: '游戏服务端未响应,请检查游戏服务端是否正常运行'
|
||||
}
|
||||
})
|
||||
} else if (error.code === 'ECONNABORTED') {
|
||||
return res.status(200).json({
|
||||
success: false,
|
||||
message: '游戏服务端请求超时',
|
||||
data: {
|
||||
status: 'timeout',
|
||||
error: '游戏服务端响应超时'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
success: false,
|
||||
message: '游戏服务端状态检查失败',
|
||||
data: {
|
||||
status: 'error',
|
||||
error: '未知错误'
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 转发请求到游戏服务端API(需要认证)
|
||||
router.post('/', authMiddleware, async (req: Request, res: Response) => {
|
||||
try {
|
||||
// 获取游戏服务端API路径和参数
|
||||
const { path, params } = req.body
|
||||
|
||||
if (!path) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '缺少游戏服务端API路径'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取PSK密钥
|
||||
const GAME_PSK = process.env.GAME_PSK
|
||||
if (!GAME_PSK) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '游戏服务端PSK密钥未配置'
|
||||
})
|
||||
}
|
||||
|
||||
// 构建完整的请求URL
|
||||
const apiUrl = `${GAME_API_URL}`
|
||||
|
||||
// 发送请求到游戏服务端API(禁用代理,确保直接连接)
|
||||
const response = await axios.post(apiUrl, { code: path, ...params }, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'psk': GAME_PSK // 添加PSK认证头
|
||||
},
|
||||
timeout: 10000, // 设置10秒超时
|
||||
proxy: false // 禁用代理,确保请求直接发送到目标服务器
|
||||
})
|
||||
|
||||
// 返回游戏服务端API的响应
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: '游戏服务端API调用成功',
|
||||
data: response.data
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
console.error('游戏服务端API调用失败:', error)
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
// Axios错误处理
|
||||
if (error.code === 'ECONNREFUSED') {
|
||||
return res.status(503).json({
|
||||
success: false,
|
||||
message: '游戏服务端连接失败,请检查游戏服务端是否正常运行'
|
||||
})
|
||||
} else if (error.code === 'ECONNABORTED') {
|
||||
return res.status(504).json({
|
||||
success: false,
|
||||
message: '游戏服务端API请求超时'
|
||||
})
|
||||
}
|
||||
|
||||
return res.status(error.response?.status || 500).json({
|
||||
success: false,
|
||||
message: error.response?.data?.message || '游戏服务端API调用失败',
|
||||
data: error.response?.data
|
||||
})
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '游戏服务端API调用失败,请稍后重试'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
375
backend/src/routes/user.routes.ts
Normal file
375
backend/src/routes/user.routes.ts
Normal file
@@ -0,0 +1,375 @@
|
||||
import { Router, Request, Response } from 'express'
|
||||
import { User, UserRole, UserStatus } from '../models/User'
|
||||
import { AppDataSource } from '../config/typeorm.config'
|
||||
import { authMiddleware, adminMiddleware } from '../middleware/authMiddleware'
|
||||
import bcrypt from 'bcryptjs'
|
||||
|
||||
const router = Router()
|
||||
const userRepository = AppDataSource.getRepository(User)
|
||||
// 获取用户列表(需要管理员权限)
|
||||
router.get('/', [authMiddleware, adminMiddleware], async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { page = 1, limit = 10, role, status } = req.query
|
||||
|
||||
const skip = (parseInt(page as string) - 1) * parseInt(limit as string)
|
||||
const take = parseInt(limit as string)
|
||||
|
||||
const whereCondition: any = {}
|
||||
if (role && Object.values(UserRole).includes(role as UserRole)) {
|
||||
whereCondition.role = role
|
||||
}
|
||||
if (status && Object.values(UserStatus).includes(status as UserStatus)) {
|
||||
whereCondition.status = status
|
||||
}
|
||||
|
||||
const [users, total] = await userRepository.findAndCount({
|
||||
where: whereCondition,
|
||||
skip,
|
||||
take,
|
||||
order: { createdAt: 'DESC' }
|
||||
})
|
||||
|
||||
// 移除密码字段
|
||||
const usersWithoutPassword = users.map(user => {
|
||||
const { password, ...userData } = user
|
||||
return userData
|
||||
})
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
users: usersWithoutPassword,
|
||||
total,
|
||||
page: parseInt(page as string),
|
||||
limit: parseInt(limit as string),
|
||||
pages: Math.ceil(total / take)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取用户列表失败,请稍后重试'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 创建用户(需要管理员权限)
|
||||
router.post('/', [authMiddleware, adminMiddleware], async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { username, password, email, phone, role, status } = req.body
|
||||
|
||||
// 检查用户名是否已存在
|
||||
const existingUser = await userRepository.findOne({ where: { username } })
|
||||
if (existingUser) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '用户名已存在'
|
||||
})
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
if (email) {
|
||||
const existingEmail = await userRepository.findOne({ where: { email } })
|
||||
if (existingEmail) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '邮箱已被注册'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 检查手机号是否已存在
|
||||
if (phone) {
|
||||
const existingPhone = await userRepository.findOne({ where: { phone } })
|
||||
if (existingPhone) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '手机号已被注册'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
const hashedPassword = await bcrypt.hash(password, 12)
|
||||
|
||||
// 创建新用户
|
||||
const newUser = userRepository.create({
|
||||
username,
|
||||
password: hashedPassword,
|
||||
email,
|
||||
phone,
|
||||
role: role || UserRole.ADMIN,
|
||||
status: status || UserStatus.ACTIVE
|
||||
})
|
||||
|
||||
await userRepository.save(newUser)
|
||||
|
||||
// 移除密码字段
|
||||
const { password: _, ...userData } = newUser
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '用户创建成功',
|
||||
data: userData
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('创建用户失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建用户失败,请稍后重试'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 获取用户详情
|
||||
router.get('/:id', authMiddleware, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const user = await userRepository.findOne({ where: { id: parseInt(id) } })
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
})
|
||||
}
|
||||
|
||||
// 移除密码字段
|
||||
const { password, ...userData } = user
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: userData
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取用户详情失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取用户详情失败,请稍后重试'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 更新用户信息
|
||||
router.put('/:id', authMiddleware, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const { username, email, phone, role, status } = req.body
|
||||
|
||||
// 查找用户
|
||||
const user = await userRepository.findOne({ where: { id: parseInt(id) } })
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
})
|
||||
}
|
||||
|
||||
// 检查用户名是否已存在(排除当前用户)
|
||||
if (username && username !== user.username) {
|
||||
const existingUser = await userRepository.findOne({ where: { username } })
|
||||
if (existingUser) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '用户名已存在'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在(排除当前用户)
|
||||
if (email && email !== user.email) {
|
||||
const existingEmail = await userRepository.findOne({ where: { email } })
|
||||
if (existingEmail) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '邮箱已被注册'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 检查手机号是否已存在(排除当前用户)
|
||||
if (phone && phone !== user.phone) {
|
||||
const existingPhone = await userRepository.findOne({ where: { phone } })
|
||||
if (existingPhone) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '手机号已被注册'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
if (username) user.username = username
|
||||
if (email) user.email = email
|
||||
if (phone) user.phone = phone
|
||||
if (role && Object.values(UserRole).includes(role)) {
|
||||
// 只有管理员可以更改角色
|
||||
if ((req as any).user?.role !== UserRole.ADMIN) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '权限不足,无法更改用户角色'
|
||||
})
|
||||
}
|
||||
user.role = role
|
||||
}
|
||||
if (status && Object.values(UserStatus).includes(status)) {
|
||||
// 只有管理员可以更改状态
|
||||
if ((req as any).user?.role !== UserRole.ADMIN) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '权限不足,无法更改用户状态'
|
||||
})
|
||||
}
|
||||
user.status = status
|
||||
}
|
||||
|
||||
await userRepository.save(user)
|
||||
|
||||
// 移除密码字段
|
||||
const { password, ...userData } = user
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: '用户信息更新成功',
|
||||
data: userData
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('更新用户信息失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新用户信息失败,请稍后重试'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 更新用户密码
|
||||
router.put('/:id/password', authMiddleware, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const { oldPassword, newPassword } = req.body
|
||||
|
||||
// 查找用户
|
||||
const user = await userRepository.findOne({ where: { id: parseInt(id) } })
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
})
|
||||
}
|
||||
|
||||
// 验证旧密码
|
||||
if ((req as any).user?.role !== UserRole.ADMIN) {
|
||||
// 非管理员需要验证旧密码
|
||||
const isPasswordValid = await bcrypt.compare(oldPassword, user.password)
|
||||
if (!isPasswordValid) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '旧密码错误'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 加密新密码
|
||||
const hashedPassword = await bcrypt.hash(newPassword, 12)
|
||||
|
||||
// 更新密码
|
||||
user.password = hashedPassword
|
||||
await userRepository.save(user)
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: '密码更新成功'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('更新密码失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新密码失败,请稍后重试'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 批量更新用户状态(需要管理员权限)
|
||||
router.put('/batch/status', [authMiddleware, adminMiddleware], async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { ids, status } = req.body
|
||||
|
||||
if (!Array.isArray(ids) || ids.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请提供有效的用户ID列表'
|
||||
})
|
||||
}
|
||||
|
||||
if (!status || !Object.values(UserStatus).includes(status as UserStatus)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请提供有效的用户状态'
|
||||
})
|
||||
}
|
||||
|
||||
// 不能修改当前登录用户的状态
|
||||
const currentUserId = (req as any).user?.id
|
||||
const filteredIds = ids.filter(id => id !== currentUserId)
|
||||
|
||||
if (filteredIds.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '不能修改当前登录用户的状态'
|
||||
})
|
||||
}
|
||||
|
||||
await userRepository.update(filteredIds, { status })
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: `成功更新${filteredIds.length}个用户的状态`
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('批量更新用户状态失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '批量更新用户状态失败,请稍后重试'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 删除用户(需要管理员权限)
|
||||
router.delete('/:id', [authMiddleware, adminMiddleware], async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
|
||||
// 查找用户
|
||||
const user = await userRepository.findOne({ where: { id: parseInt(id) } })
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
})
|
||||
}
|
||||
|
||||
// 不能删除自己
|
||||
if ((req as any).user?.id === user.id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '不能删除当前登录的用户'
|
||||
})
|
||||
}
|
||||
|
||||
await userRepository.remove(user)
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: '用户删除成功'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('删除用户失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除用户失败,请稍后重试'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
52
backend/src/scripts/initAdmin.ts
Normal file
52
backend/src/scripts/initAdmin.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { AppDataSource } from '../config/typeorm.config'
|
||||
import { User, UserRole } from '../models/User'
|
||||
import bcrypt from 'bcryptjs'
|
||||
|
||||
// 初始化管理员用户
|
||||
const initAdmin = async () => {
|
||||
try {
|
||||
// 连接数据库
|
||||
await AppDataSource.initialize()
|
||||
console.log('数据库连接成功')
|
||||
|
||||
const userRepository = AppDataSource.getRepository(User)
|
||||
|
||||
// 检查是否已存在管理员用户
|
||||
const existingAdmin = await userRepository.findOne({ where: { username: 'admin' } })
|
||||
|
||||
if (existingAdmin) {
|
||||
console.log('管理员用户已存在')
|
||||
return
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
const hashedPassword = await bcrypt.hash('admin123', 12)
|
||||
|
||||
// 创建管理员用户
|
||||
const adminUser = userRepository.create({
|
||||
username: 'admin',
|
||||
password: hashedPassword,
|
||||
role: UserRole.ADMIN
|
||||
})
|
||||
|
||||
await userRepository.save(adminUser)
|
||||
|
||||
console.log('管理员用户创建成功')
|
||||
console.log('用户名: admin')
|
||||
console.log('密码: admin123')
|
||||
|
||||
// 断开数据库连接
|
||||
await AppDataSource.destroy()
|
||||
} catch (error) {
|
||||
console.error('初始化管理员用户失败:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// 执行初始化
|
||||
export default initAdmin
|
||||
|
||||
// 直接执行脚本
|
||||
if (require.main === module) {
|
||||
initAdmin()
|
||||
}
|
||||
227
backend/src/services/ConfigService.ts
Normal file
227
backend/src/services/ConfigService.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import { AppDataSource } from '../config/typeorm.config'
|
||||
import { Config } from '../models/Config'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
/**
|
||||
* 配置服务,用于从数据库中读取和管理配置
|
||||
*/
|
||||
export class ConfigService {
|
||||
private configRepository = AppDataSource.getRepository(Config)
|
||||
private configCache: Record<string, string> = {}
|
||||
private envFilePath: string = path.resolve(__dirname, '../../.env')
|
||||
|
||||
/**
|
||||
* 初始化配置服务
|
||||
* 从数据库中加载所有配置项到缓存中
|
||||
*/
|
||||
async initialize() {
|
||||
try {
|
||||
console.log('🔄 正在从数据库加载系统配置...')
|
||||
const configs = await this.configRepository.find()
|
||||
this.configCache = configs.reduce((map, config) => {
|
||||
map[config.key] = config.value
|
||||
return map
|
||||
}, {} as Record<string, string>)
|
||||
console.log(`✅ 配置服务初始化成功,已加载 ${configs.length} 个配置项`)
|
||||
|
||||
// 显示关键配置信息(脱敏显示)
|
||||
const importantKeys = ['server_host', 'server_port', 'game_api_url', 'game_psk']
|
||||
importantKeys.forEach(key => {
|
||||
if (this.configCache[key]) {
|
||||
let displayValue = this.configCache[key]
|
||||
// 对敏感信息进行脱敏处理
|
||||
if (key.includes('secret')) {
|
||||
displayValue = displayValue ? '*'.repeat(Math.min(displayValue.length, 8)) : '未设置'
|
||||
}
|
||||
console.log(` 📋 ${key}: ${displayValue}`)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('❌ 配置服务初始化失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置值
|
||||
* @param key 配置键名
|
||||
* @param defaultValue 默认值,当配置不存在时返回
|
||||
* @returns 配置值
|
||||
*/
|
||||
get(key: string, defaultValue?: string): string {
|
||||
return this.configCache[key] || defaultValue || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有配置
|
||||
* @returns 所有配置项
|
||||
*/
|
||||
getAll(): Record<string, string> {
|
||||
return { ...this.configCache }
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
* @param key 配置键名
|
||||
* @param value 配置值
|
||||
* @param description 配置描述
|
||||
*/
|
||||
async update(key: string, value: string, description?: string) {
|
||||
try {
|
||||
// 更新数据库
|
||||
await this.configRepository.upsert(
|
||||
{ key, value, description },
|
||||
['key']
|
||||
)
|
||||
|
||||
// 更新缓存
|
||||
this.configCache[key] = value
|
||||
|
||||
// 更新.env文件
|
||||
await this.updateEnvFile(key, value)
|
||||
|
||||
// 应用到环境变量
|
||||
this.applyToEnvKey(key, value)
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('更新配置失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数据库配置应用到环境变量
|
||||
* 覆盖process.env中的配置
|
||||
*/
|
||||
applyToEnv() {
|
||||
console.log('🔄 正在应用数据库配置到环境变量...')
|
||||
let appliedCount = 0
|
||||
|
||||
// 服务器配置
|
||||
if (this.configCache.server_host) {
|
||||
process.env.HOST = this.configCache.server_host
|
||||
appliedCount++
|
||||
}
|
||||
if (this.configCache.server_port) {
|
||||
process.env.PORT = this.configCache.server_port
|
||||
appliedCount++
|
||||
}
|
||||
|
||||
// 游戏服务API配置
|
||||
if (this.configCache.game_api_url) {
|
||||
process.env.GAME_API_URL = this.configCache.game_api_url
|
||||
appliedCount++
|
||||
}
|
||||
// 游戏服务PSK配置
|
||||
if (this.configCache.game_psk) {
|
||||
process.env.GAME_PSK = this.configCache.game_psk
|
||||
appliedCount++
|
||||
}
|
||||
|
||||
// JWT配置
|
||||
if (this.configCache.jwt_secret) {
|
||||
process.env.JWT_SECRET = this.configCache.jwt_secret
|
||||
appliedCount++
|
||||
}
|
||||
if (this.configCache.jwt_expires_in) {
|
||||
process.env.JWT_EXPIRES_IN = this.configCache.jwt_expires_in
|
||||
appliedCount++
|
||||
}
|
||||
if (this.configCache.jwt_refresh_secret) {
|
||||
process.env.JWT_REFRESH_SECRET = this.configCache.jwt_refresh_secret
|
||||
appliedCount++
|
||||
}
|
||||
if (this.configCache.jwt_refresh_expires_in) {
|
||||
process.env.JWT_REFRESH_EXPIRES_IN = this.configCache.jwt_refresh_expires_in
|
||||
appliedCount++
|
||||
}
|
||||
|
||||
console.log(`✅ 数据库配置应用完成,已更新 ${appliedCount} 个环境变量`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 将单个配置键应用到环境变量
|
||||
* @param key 配置键名
|
||||
* @param value 配置值
|
||||
*/
|
||||
private applyToEnvKey(key: string, value: string) {
|
||||
// 配置键映射:数据库键名 → .env键名
|
||||
const keyMapping: Record<string, string> = {
|
||||
server_host: 'HOST',
|
||||
server_port: 'PORT',
|
||||
game_api_url: 'GAME_API_URL',
|
||||
game_psk: 'GAME_PSK',
|
||||
jwt_secret: 'JWT_SECRET',
|
||||
jwt_expires_in: 'JWT_EXPIRES_IN',
|
||||
jwt_refresh_secret: 'JWT_REFRESH_SECRET',
|
||||
jwt_refresh_expires_in: 'JWT_REFRESH_EXPIRES_IN'
|
||||
}
|
||||
|
||||
// 应用到环境变量
|
||||
const envKey = keyMapping[key]
|
||||
if (envKey) {
|
||||
process.env[envKey] = value
|
||||
console.log(`配置 ${key} 已应用到环境变量 ${envKey}=${value}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新.env文件中的配置项
|
||||
* @param key 数据库中的配置键名
|
||||
* @param value 配置值
|
||||
*/
|
||||
private async updateEnvFile(key: string, value: string) {
|
||||
try {
|
||||
// 配置键映射:数据库键名 → .env键名
|
||||
const keyMapping: Record<string, string> = {
|
||||
server_host: 'HOST',
|
||||
server_port: 'PORT',
|
||||
game_api_url: 'GAME_API_URL',
|
||||
game_psk: 'GAME_PSK',
|
||||
jwt_secret: 'JWT_SECRET',
|
||||
jwt_expires_in: 'JWT_EXPIRES_IN',
|
||||
jwt_refresh_secret: 'JWT_REFRESH_SECRET',
|
||||
jwt_refresh_expires_in: 'JWT_REFRESH_EXPIRES_IN'
|
||||
}
|
||||
|
||||
// 获取对应的.env键名
|
||||
const envKey = keyMapping[key]
|
||||
if (!envKey) {
|
||||
console.log(` ℹ️ 配置 ${key} 无需更新到.env文件`)
|
||||
return
|
||||
}
|
||||
|
||||
// 读取.env文件内容
|
||||
let envContent = ''
|
||||
if (fs.existsSync(this.envFilePath)) {
|
||||
envContent = fs.readFileSync(this.envFilePath, 'utf8')
|
||||
}
|
||||
|
||||
// 构建新的配置行
|
||||
const newConfigLine = `${envKey}=${value}`
|
||||
const envKeyRegex = new RegExp(`^${envKey}=.*$`, 'gm')
|
||||
|
||||
// 更新或添加配置行
|
||||
let updatedContent = ''
|
||||
if (envKeyRegex.test(envContent)) {
|
||||
// 替换现有配置行
|
||||
updatedContent = envContent.replace(envKeyRegex, newConfigLine)
|
||||
} else {
|
||||
// 添加新配置行
|
||||
updatedContent = envContent ? `${envContent}\n${newConfigLine}` : newConfigLine
|
||||
}
|
||||
|
||||
// 写入.env文件
|
||||
fs.writeFileSync(this.envFilePath, updatedContent, 'utf8')
|
||||
console.log(` ✍️ .env文件已更新: ${envKey}=${value}`)
|
||||
} catch (error) {
|
||||
console.error('❌ 更新.env文件失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const configService = new ConfigService()
|
||||
18
backend/tsconfig.json
Normal file
18
backend/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"resolveJsonModule": true,
|
||||
"strictPropertyInitialization": false
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user