724 lines
20 KiB
JavaScript
724 lines
20 KiB
JavaScript
// 确保在最开始就加载环境变量
|
||
require('dotenv').config();
|
||
console.log('Environment loaded:', {
|
||
DB_HOST: process.env.DB_HOST,
|
||
DB_USER: process.env.DB_USER,
|
||
DB_DATABASE: process.env.DB_DATABASE,
|
||
PORT: process.env.PORT
|
||
});
|
||
|
||
const express = require('express');
|
||
const http = require('http');
|
||
const cors = require('cors');
|
||
const multer = require('multer');
|
||
const path = require('path');
|
||
const jwt = require('jsonwebtoken');
|
||
const bcrypt = require('bcryptjs');
|
||
const auth = require('./middleware/auth');
|
||
const checkPermission = require('./middleware/checkPermission');
|
||
const db = require('./utils/db');
|
||
const app = express();
|
||
const systemSettingsRoutes = require('./routes/systemSettings');
|
||
const messagesRoutes = require('./routes/messages');
|
||
const tagsRoutes = require('./routes/tags');
|
||
const adminRoutes = require('./routes/admin');
|
||
const server = http.createServer(app);
|
||
const { initWebSocket } = require('./websocket');
|
||
|
||
// 请求日志中间件
|
||
app.use((req, res, next) => {
|
||
console.log(`${new Date().toISOString()} ${req.method} ${req.url}`);
|
||
next();
|
||
});
|
||
|
||
// 中间件
|
||
app.use(cors());
|
||
app.use(express.json());
|
||
|
||
// 配置文件上传
|
||
const storage = multer.diskStorage({
|
||
destination: (req, file, cb) => {
|
||
let uploadPath = 'uploads';
|
||
if (file.mimetype.startsWith('image/')) {
|
||
uploadPath += '/images';
|
||
} else if (file.mimetype.startsWith('video/')) {
|
||
uploadPath += '/videos';
|
||
}
|
||
// 确保目录存在
|
||
require('fs').mkdirSync(path.join(__dirname, uploadPath), { recursive: true });
|
||
cb(null, uploadPath);
|
||
},
|
||
filename: (req, file, cb) => {
|
||
cb(null, Date.now() + path.extname(file.originalname));
|
||
}
|
||
});
|
||
|
||
const upload = multer({
|
||
storage,
|
||
limits: {
|
||
fileSize: 10 * 1024 * 1024, // 10MB limit
|
||
},
|
||
fileFilter: (req, file, cb) => {
|
||
const allowedTypes = /jpeg|jpg|png|gif|mp4|webm/;
|
||
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
|
||
const mimetype = allowedTypes.test(file.mimetype);
|
||
if (extname && mimetype) {
|
||
return cb(null, true);
|
||
}
|
||
cb(new Error('只支持图片和视频文件!'));
|
||
}
|
||
});
|
||
|
||
// API路由
|
||
app.get('/api/categories', async (req, res) => {
|
||
try {
|
||
const [categories] = await db.query('SELECT * FROM categories');
|
||
res.json(categories);
|
||
} catch (error) {
|
||
res.status(500).json({ message: error.message });
|
||
}
|
||
});
|
||
|
||
app.get('/api/games', async (req, res) => {
|
||
try {
|
||
const [games] = await db.query(`
|
||
SELECT g.*, c.name as category_name,
|
||
GROUP_CONCAT(DISTINCT gp.platform) as platforms,
|
||
GROUP_CONCAT(DISTINCT gt.tag) as tags
|
||
FROM games g
|
||
LEFT JOIN categories c ON g.category_id = c.id
|
||
LEFT JOIN game_platforms gp ON g.id = gp.game_id
|
||
LEFT JOIN game_tags gt ON g.id = gt.game_id
|
||
GROUP BY g.id
|
||
`);
|
||
|
||
// 处理平台和标签字符串
|
||
games.forEach(game => {
|
||
game.platforms = game.platforms ? game.platforms.split(',') : [];
|
||
game.tags = game.tags ? game.tags.split(',') : [];
|
||
});
|
||
|
||
res.json(games);
|
||
} catch (error) {
|
||
res.status(500).json({ message: error.message });
|
||
}
|
||
});
|
||
|
||
app.get('/api/games/:id', async (req, res) => {
|
||
try {
|
||
const game = await Game.findById(req.params.id);
|
||
if (!game) {
|
||
return res.status(404).json({ message: 'Game not found' });
|
||
}
|
||
|
||
// 获取游戏对应的分类信息
|
||
const category = await Category.findById(game.categoryId);
|
||
const gameWithCategory = {
|
||
...game.toObject(),
|
||
category
|
||
};
|
||
|
||
res.json(gameWithCategory);
|
||
} catch (error) {
|
||
res.status(500).json({ message: error.message });
|
||
}
|
||
});
|
||
|
||
app.get('/api/games/search', async (req, res) => {
|
||
try {
|
||
const { query, category } = req.query;
|
||
let searchQuery = {};
|
||
|
||
if (query) {
|
||
searchQuery.$or = [
|
||
{ title: new RegExp(query, 'i') },
|
||
{ description: new RegExp(query, 'i') }
|
||
];
|
||
}
|
||
|
||
if (category) {
|
||
searchQuery.categoryId = category;
|
||
}
|
||
|
||
const games = await Game.find(searchQuery);
|
||
res.json(games);
|
||
} catch (error) {
|
||
res.status(500).json({ message: error.message });
|
||
}
|
||
});
|
||
|
||
// 添加游戏
|
||
app.post('/api/games', auth, upload.single('image'), async (req, res) => {
|
||
try {
|
||
const { title, description, category_id, developer, release_date, platforms, tags } = req.body;
|
||
|
||
// 验证必填字段
|
||
if (!title || !category_id) {
|
||
return res.status(400).json({ message: '标题和分类为必填项' });
|
||
}
|
||
|
||
// 解析平台和标签数据(因为是通过FormData传输的)
|
||
const platformsArray = Array.isArray(platforms) ? platforms : platforms ? [platforms] : [];
|
||
const tagsArray = Array.isArray(tags) ? tags : tags ? [tags] : [];
|
||
|
||
const [result] = await db.query(
|
||
'INSERT INTO games (title, description, category_id, developer, release_date, image) VALUES (?, ?, ?, ?, ?, ?)',
|
||
[
|
||
title,
|
||
description || '',
|
||
parseInt(category_id),
|
||
developer || '',
|
||
release_date || null,
|
||
req.file ? req.file.path : null
|
||
]
|
||
);
|
||
|
||
const gameId = result.insertId;
|
||
|
||
// 添加平台信息
|
||
if (platformsArray.length > 0) {
|
||
const platformValues = platformsArray.map(platform => [gameId, platform]);
|
||
await db.query(
|
||
'INSERT INTO game_platforms (game_id, platform) VALUES ?',
|
||
[platformValues]
|
||
);
|
||
}
|
||
|
||
// 添加标签信息
|
||
if (tagsArray.length > 0) {
|
||
const tagValues = tagsArray.map(tag => [gameId, tag]);
|
||
await db.query(
|
||
'INSERT INTO game_tags (game_id, tag) VALUES ?',
|
||
[tagValues]
|
||
);
|
||
}
|
||
|
||
res.status(201).json({
|
||
message: '游戏添加成功',
|
||
id: gameId
|
||
});
|
||
} catch (error) {
|
||
console.error('Error adding game:', error);
|
||
res.status(500).json({
|
||
message: '添加游戏失败',
|
||
error: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||
});
|
||
}
|
||
});
|
||
|
||
// 管理员注册
|
||
app.post('/api/admin/register', async (req, res) => {
|
||
try {
|
||
const { username, password, email } = req.body;
|
||
|
||
// 验证必填字段
|
||
if (!username || !password || !email) {
|
||
return res.status(400).json({ message: '用户名、密码和邮箱都是必填项' });
|
||
}
|
||
|
||
// 检查用户名是否已存在
|
||
const [existingUsers] = await db.query(
|
||
'SELECT id FROM admins WHERE username = ?',
|
||
[username]
|
||
);
|
||
|
||
if (existingUsers.length > 0) {
|
||
return res.status(400).json({ message: '用户名已存在' });
|
||
}
|
||
|
||
// 检查邮箱是否已存在
|
||
const [existingEmails] = await db.query(
|
||
'SELECT id FROM admins WHERE email = ?',
|
||
[email]
|
||
);
|
||
|
||
if (existingEmails.length > 0) {
|
||
return res.status(400).json({ message: '邮箱已被使用' });
|
||
}
|
||
|
||
// 加密密码
|
||
const hashedPassword = await bcrypt.hash(password, 10);
|
||
console.log('Hashed password:', hashedPassword);
|
||
|
||
// 开始事务
|
||
const connection = await db.getConnection();
|
||
await connection.beginTransaction();
|
||
|
||
try {
|
||
// 插入管理员记录
|
||
const [result] = await connection.query(
|
||
'INSERT INTO admins (username, password, email, role, status) VALUES (?, ?, ?, ?, ?)',
|
||
[username, hashedPassword, email, 'editor', 'active']
|
||
);
|
||
|
||
// 插入默认权限
|
||
await connection.query(
|
||
'INSERT INTO admin_permissions (admin_id, permission) VALUES (?, ?), (?, ?)',
|
||
[result.insertId, 'game:manage', result.insertId, 'media:manage']
|
||
);
|
||
|
||
await connection.commit();
|
||
|
||
// 生成JWT令牌
|
||
const token = jwt.sign(
|
||
{ id: result.insertId, role: 'editor' },
|
||
process.env.JWT_SECRET,
|
||
{ expiresIn: '24h' }
|
||
);
|
||
|
||
res.status(201).json({
|
||
message: '注册成功',
|
||
token,
|
||
admin: {
|
||
username,
|
||
role: 'editor',
|
||
permissions: ['game:manage', 'media:manage']
|
||
}
|
||
});
|
||
} catch (error) {
|
||
await connection.rollback();
|
||
throw error;
|
||
} finally {
|
||
connection.release();
|
||
}
|
||
} catch (error) {
|
||
console.error('Registration error:', error);
|
||
res.status(500).json({ message: '注册失败', error: error.message });
|
||
}
|
||
});
|
||
|
||
// 管理员登录
|
||
app.post('/api/admin/login', async (req, res) => {
|
||
try {
|
||
const { username, password } = req.body;
|
||
console.log('Login attempt:', { username }); // 添加登录尝试日志
|
||
|
||
const [admins] = await db.query(
|
||
'SELECT a.*, GROUP_CONCAT(ap.permission) as permissions FROM admins a ' +
|
||
'LEFT JOIN admin_permissions ap ON a.id = ap.admin_id ' +
|
||
'WHERE a.username = ? AND a.status = "active" ' +
|
||
'GROUP BY a.id',
|
||
[username]
|
||
);
|
||
|
||
console.log('Query result:', admins); // 添加查询结果日志
|
||
|
||
const admin = admins[0];
|
||
if (!admin) {
|
||
console.log('Admin not found');
|
||
return res.status(401).json({ message: '用户名或密码错误' });
|
||
}
|
||
|
||
console.log('Comparing password for:', admin.username);
|
||
console.log('Encrypted password:', admin.password);
|
||
const hashedPassword = await bcrypt.hash(password, 10);
|
||
console.log('Hashed password:', hashedPassword);
|
||
const isMatch = await bcrypt.compare(password, admin.password);
|
||
console.log('Password match result:', isMatch);
|
||
|
||
if (!isMatch) {
|
||
console.log('Password does not match');
|
||
return res.status(401).json({ message: '用户名或密码错误' });
|
||
}
|
||
|
||
// 更新最后登录时间
|
||
await db.query(
|
||
'UPDATE admins SET last_login = NOW() WHERE id = ?',
|
||
[admin.id]
|
||
);
|
||
|
||
const token = jwt.sign(
|
||
{ id: admin.id, role: admin.role },
|
||
process.env.JWT_SECRET,
|
||
{ expiresIn: '24h' }
|
||
);
|
||
|
||
console.log('Login successful for:', admin.username);
|
||
|
||
res.json({
|
||
token,
|
||
admin: {
|
||
username: admin.username,
|
||
role: admin.role,
|
||
permissions: admin.permissions ? admin.permissions.split(',') : []
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('Login error:', error);
|
||
res.status(500).json({ message: error.message });
|
||
}
|
||
});
|
||
|
||
// 媒体文件上传
|
||
app.post('/api/media/upload', auth, upload.single('file'), async (req, res) => {
|
||
try {
|
||
if (!req.file) {
|
||
return res.status(400).json({ message: '请选择要上传的文件' });
|
||
}
|
||
|
||
const media = new Media({
|
||
filename: req.file.filename,
|
||
originalname: req.file.originalname,
|
||
mimetype: req.file.mimetype,
|
||
size: req.file.size,
|
||
url: `/uploads/${req.file.filename}`,
|
||
type: req.file.mimetype.startsWith('image/') ? 'image' : 'video',
|
||
uploadedBy: req.admin.id
|
||
});
|
||
|
||
await media.save();
|
||
res.json(media);
|
||
} catch (error) {
|
||
res.status(500).json({ message: error.message });
|
||
}
|
||
});
|
||
|
||
// 获取媒体文件列表
|
||
app.get('/api/media', auth, async (req, res) => {
|
||
try {
|
||
const { type, gameId } = req.query;
|
||
let query = {};
|
||
|
||
if (type) query.type = type;
|
||
if (gameId) query.gameId = gameId;
|
||
|
||
const media = await Media.find(query)
|
||
.populate('uploadedBy', 'username')
|
||
.sort('-uploadedAt');
|
||
|
||
res.json(media);
|
||
} catch (error) {
|
||
res.status(500).json({ message: error.message });
|
||
}
|
||
});
|
||
|
||
// 删除媒体文件
|
||
app.delete('/api/media/:id', auth, async (req, res) => {
|
||
try {
|
||
const media = await Media.findById(req.params.id);
|
||
if (!media) {
|
||
return res.status(404).json({ message: '文件不存在' });
|
||
}
|
||
|
||
// 删除物理文件
|
||
const filePath = path.join(__dirname, media.url);
|
||
fs.unlink(filePath, async (err) => {
|
||
if (err) {
|
||
console.error('Error deleting file:', err);
|
||
}
|
||
await media.remove();
|
||
res.json({ message: '文件已删除' });
|
||
});
|
||
} catch (error) {
|
||
res.status(500).json({ message: error.message });
|
||
}
|
||
});
|
||
|
||
// 更新游戏
|
||
app.put('/api/games/:id', auth, upload.single('image'), async (req, res) => {
|
||
try {
|
||
const { title, description, category_id, developer, release_date, platforms, tags } = req.body;
|
||
|
||
// 验证游戏是否存在
|
||
const [existingGame] = await db.query(
|
||
'SELECT * FROM games WHERE id = ?',
|
||
[req.params.id]
|
||
);
|
||
|
||
if (!existingGame.length) {
|
||
return res.status(404).json({ message: '游戏不存在' });
|
||
}
|
||
|
||
// 更新游戏基本信息
|
||
await db.query(
|
||
`UPDATE games SET
|
||
title = ?,
|
||
description = ?,
|
||
category_id = ?,
|
||
developer = ?,
|
||
release_date = ?,
|
||
image = ?
|
||
WHERE id = ?`,
|
||
[
|
||
title,
|
||
description || '',
|
||
parseInt(category_id),
|
||
developer || '',
|
||
release_date || null,
|
||
req.file ? req.file.path : existingGame[0].image,
|
||
req.params.id
|
||
]
|
||
);
|
||
|
||
// 更新平台信息
|
||
if (platforms) {
|
||
// 先删除旧的平台信息
|
||
await db.query('DELETE FROM game_platforms WHERE game_id = ?', [req.params.id]);
|
||
|
||
// 添加新的平台信息
|
||
if (platforms.length > 0) {
|
||
const platformValues = platforms.map(platform => [req.params.id, platform]);
|
||
await db.query(
|
||
'INSERT INTO game_platforms (game_id, platform) VALUES ?',
|
||
[platformValues]
|
||
);
|
||
}
|
||
}
|
||
|
||
// 更新标签信息
|
||
if (tags) {
|
||
// 先删除旧的标签信息
|
||
await db.query('DELETE FROM game_tags WHERE game_id = ?', [req.params.id]);
|
||
|
||
// 添加新的标签信息
|
||
if (tags.length > 0) {
|
||
const tagValues = tags.map(tag => [req.params.id, tag]);
|
||
await db.query(
|
||
'INSERT INTO game_tags (game_id, tag) VALUES ?',
|
||
[tagValues]
|
||
);
|
||
}
|
||
}
|
||
|
||
res.json({ message: '游戏更新成功' });
|
||
} catch (error) {
|
||
console.error('Error updating game:', error);
|
||
res.status(500).json({ message: error.message });
|
||
}
|
||
});
|
||
|
||
// 添加统计API路由
|
||
app.get('/api/stats', auth, async (req, res) => {
|
||
try {
|
||
const [gamesCount, categoriesCount, mediaCount] = await Promise.all([
|
||
Game.countDocuments(),
|
||
Category.countDocuments(),
|
||
Media.countDocuments()
|
||
]);
|
||
|
||
res.json({
|
||
gamesCount,
|
||
categoriesCount,
|
||
mediaCount
|
||
});
|
||
} catch (error) {
|
||
res.status(500).json({ message: error.message });
|
||
}
|
||
});
|
||
|
||
// 用户管理API
|
||
app.get('/api/admins', auth, checkPermission('user:manage'), async (req, res) => {
|
||
try {
|
||
const admins = await Admin.find()
|
||
.select('-password')
|
||
.populate('createdBy', 'username');
|
||
res.json(admins);
|
||
} catch (error) {
|
||
res.status(500).json({ message: error.message });
|
||
}
|
||
});
|
||
|
||
app.post('/api/admins', auth, checkPermission('user:manage'), async (req, res) => {
|
||
try {
|
||
const { username, email, password, role } = req.body;
|
||
|
||
const admin = new Admin({
|
||
username,
|
||
email,
|
||
password,
|
||
role,
|
||
createdBy: req.admin.id
|
||
});
|
||
|
||
await admin.save();
|
||
res.status(201).json({ message: '用户创建成功' });
|
||
} catch (error) {
|
||
res.status(500).json({ message: error.message });
|
||
}
|
||
});
|
||
|
||
app.put('/api/admins/:id', auth, checkPermission('user:manage'), async (req, res) => {
|
||
try {
|
||
const { id } = req.params;
|
||
const updates = req.body;
|
||
|
||
// 不允许修改超级管理员
|
||
const admin = await Admin.findById(id);
|
||
if (admin.role === 'superadmin' && req.admin.role !== 'superadmin') {
|
||
return res.status(403).json({ message: '无权修改超级管理员' });
|
||
}
|
||
|
||
// 如果要修改密码,需要加密
|
||
if (updates.password) {
|
||
updates.password = await bcrypt.hash(updates.password, 10);
|
||
}
|
||
|
||
const updatedAdmin = await Admin.findByIdAndUpdate(
|
||
id,
|
||
updates,
|
||
{ new: true }
|
||
).select('-password');
|
||
|
||
res.json(updatedAdmin);
|
||
} catch (error) {
|
||
res.status(500).json({ message: error.message });
|
||
}
|
||
});
|
||
|
||
app.delete('/api/admins/:id', auth, checkPermission('user:manage'), async (req, res) => {
|
||
try {
|
||
const { id } = req.params;
|
||
|
||
// 不允许删除超级管理员
|
||
const admin = await Admin.findById(id);
|
||
if (admin.role === 'superadmin') {
|
||
return res.status(403).json({ message: '不能删除超级管理员' });
|
||
}
|
||
|
||
// 不能删除自己
|
||
if (id === req.admin.id) {
|
||
return res.status(400).json({ message: '不能删除自己的账号' });
|
||
}
|
||
|
||
await Admin.findByIdAndDelete(id);
|
||
res.json({ message: '用户删除成功' });
|
||
} catch (error) {
|
||
res.status(500).json({ message: error.message });
|
||
}
|
||
});
|
||
|
||
// 获取当前用户信息
|
||
app.get('/api/admins/me', auth, async (req, res) => {
|
||
try {
|
||
const admin = await Admin.findById(req.admin.id)
|
||
.select('-password');
|
||
res.json(admin);
|
||
} catch (error) {
|
||
res.status(500).json({ message: error.message });
|
||
}
|
||
});
|
||
|
||
// 修改密码
|
||
app.post('/api/admins/change-password', auth, async (req, res) => {
|
||
try {
|
||
const { oldPassword, newPassword } = req.body;
|
||
|
||
const admin = await Admin.findById(req.admin.id);
|
||
const isMatch = await bcrypt.compare(oldPassword, admin.password);
|
||
|
||
if (!isMatch) {
|
||
return res.status(400).json({ message: '原密码错误' });
|
||
}
|
||
|
||
admin.password = newPassword;
|
||
await admin.save();
|
||
|
||
res.json({ message: '密码修改成功' });
|
||
} catch (error) {
|
||
res.status(500).json({ message: error.message });
|
||
}
|
||
});
|
||
|
||
// 系统设置路由
|
||
app.use('/api/settings', systemSettingsRoutes);
|
||
|
||
// 留言管理路由
|
||
app.use('/api/messages', messagesRoutes);
|
||
|
||
// 标签管理路由
|
||
app.use('/api/tags', tagsRoutes);
|
||
|
||
// 用户管理路由
|
||
app.use('/api/admin', adminRoutes);
|
||
|
||
// 提供静态文件访问
|
||
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
|
||
|
||
// 删除游戏
|
||
app.delete('/api/games/:id', auth, async (req, res) => {
|
||
try {
|
||
// 验证游戏是否存在
|
||
const [existingGame] = await db.query(
|
||
'SELECT * FROM games WHERE id = ?',
|
||
[req.params.id]
|
||
);
|
||
|
||
if (!existingGame.length) {
|
||
return res.status(404).json({ message: '游戏不存在' });
|
||
}
|
||
|
||
// 开始事务
|
||
const connection = await db.getConnection();
|
||
await connection.beginTransaction();
|
||
|
||
try {
|
||
// 删除游戏图片
|
||
if (existingGame[0].image) {
|
||
const imagePath = path.join(__dirname, existingGame[0].image);
|
||
try {
|
||
if (require('fs').existsSync(imagePath)) {
|
||
require('fs').unlinkSync(imagePath);
|
||
console.log(`Deleted image file: ${imagePath}`);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error deleting image file:', error);
|
||
// 继续执行,不因文件删除失败而中断整个操作
|
||
}
|
||
}
|
||
|
||
// 删除相关的平台信息
|
||
await connection.query('DELETE FROM game_platforms WHERE game_id = ?', [req.params.id]);
|
||
|
||
// 删除相关的标签信息
|
||
await connection.query('DELETE FROM game_tags WHERE game_id = ?', [req.params.id]);
|
||
|
||
// 删除游戏记录
|
||
await connection.query('DELETE FROM games WHERE id = ?', [req.params.id]);
|
||
|
||
await connection.commit();
|
||
console.log(`Successfully deleted game with ID: ${req.params.id}`);
|
||
res.json({ message: '游戏删除成功' });
|
||
} catch (error) {
|
||
await connection.rollback();
|
||
console.error('Transaction failed:', error);
|
||
throw error;
|
||
} finally {
|
||
connection.release();
|
||
}
|
||
} catch (error) {
|
||
console.error('Error deleting game:', error);
|
||
res.status(500).json({
|
||
message: '删除游戏失败',
|
||
error: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||
});
|
||
}
|
||
});
|
||
|
||
// 获取管理员数量
|
||
app.get('/api/admin/count', auth, async (req, res) => {
|
||
try {
|
||
const [result] = await db.query('SELECT COUNT(*) as count FROM admins');
|
||
res.json({ count: result[0].count });
|
||
} catch (error) {
|
||
console.error('Error getting admin count:', error);
|
||
res.status(500).json({ message: error.message });
|
||
}
|
||
});
|
||
|
||
// 错误处理中间件
|
||
app.use((err, req, res, next) => {
|
||
console.error('Server error:', err);
|
||
res.status(500).json({
|
||
message: 'Internal server error',
|
||
error: process.env.NODE_ENV === 'development' ? err.message : undefined
|
||
});
|
||
});
|
||
|
||
// 启动服务器
|
||
const PORT = process.env.PORT || 3000;
|
||
server.listen(PORT, () => {
|
||
console.log(`Server is running on port ${PORT}`);
|
||
});
|
||
|
||
// 初始化 WebSocket
|
||
initWebSocket(server);
|