项目初始化

This commit is contained in:
Stev_Wang
2025-12-22 23:51:21 +08:00
commit 4a97b964ac
64 changed files with 8371 additions and 0 deletions

26
backend/.env.example Normal file
View 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
View 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
View 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"
}
}

View 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
View 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()

View 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 }

View 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

View 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
}

View 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
}

View 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

View 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

View 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

View 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

View 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()
}

View 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
View 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"]
}