引言:什么是角色卡服务器及其重要性
角色卡服务器(Character Card Server)是一种用于存储、管理和分享虚拟角色数据的专用服务器系统。在现代游戏开发、虚拟现实应用和数字娱乐产业中,角色卡扮演着至关重要的角色。它不仅包含了角色的视觉外观数据,还存储了角色的属性、技能、背景故事等核心信息。
对于游戏开发者而言,一个高效的角色卡服务器能够实现:
- 数据集中管理:避免数据分散在不同文件中导致的混乱
- 版本控制:追踪角色数据的变更历史
- 多人协作:允许多名开发者同时编辑不同角色
- 动态更新:无需重新发布客户端即可更新角色数据
- 性能优化:通过缓存和负载均衡提升访问速度
本文将从零开始,详细讲解如何搭建一个完整的角色卡服务器,包括环境准备、核心代码实现、API设计、前端集成以及新手常见问题的解决方案。
环境准备与基础配置
硬件要求
- CPU:4核及以上(推荐Intel i5或AMD Ryzen 5以上)
- 内存:8GB及以上(推荐16GB)
- 存储:SSD硬盘,至少50GB可用空间
- 网络:稳定的网络连接,推荐100Mbps以上带宽
软件要求
- 操作系统:Ubuntu 20.04 LTS / Windows Server 2019 / macOS 12+
- 数据库:MongoDB 5.0+ 或 PostgreSQL 13+
- 运行环境:Node.js 16+ / Python 3.9+ / Java 17+
- 版本控制:Git 2.30+
网络环境配置
# Ubuntu系统下安装基础工具
sudo apt update
sudo apt install -y curl wget git build-essential
# 配置防火墙(允许HTTP/HTTPS和自定义端口)
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 3000/tcp # API服务端口
sudo ufw allow 27017/tcp # MongoDB端口(如果本地部署)
sudo ufw enable
核心架构设计
系统架构图
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 客户端应用 │───▶│ API网关层 │───▶│ 业务逻辑层 │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 缓存层 │◀───│ 数据持久层 │◀───│ 文件存储层 │
└─────────────────┘ └─────────────────┘ └─────────────────┘
数据库设计
角色卡的核心数据结构应该包含以下字段:
- 基础信息:ID、名称、描述、创建时间、更新时间
- 视觉数据:模型URL、贴图URL、动画数据
- 属性数据:生命值、攻击力、防御力等数值
- 技能系统:技能列表、冷却时间、消耗资源
- 背景故事:文本描述、语音URL、关系图
- 权限控制:创建者、编辑权限、可见性
详细搭建步骤
第一步:数据库搭建(以MongoDB为例)
# 安装MongoDB(Ubuntu)
wget -qO - https://www.mongodb.org/static/pgp/server-5.0.asc | sudo apt-key add -
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/5.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-5.0.list
sudo apt update
sudo apt install -y mongodb-org
# 启动MongoDB服务
sudo systemctl start mongod
sudo systemctl enable mongod
# 创建角色卡数据库和用户
mongosh
use character_card_db
db.createUser({
user: "card_admin",
pwd: "your_secure_password",
roles: [{ role: "readWrite", db: "character_card_db" }]
})
第二步:后端API服务搭建(Node.js + Express)
// server.js - 主入口文件
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
// 初始化Express应用
const app = express();
// 中间件配置
app.use(cors());
app.use(helmet());
app.use(express.json({ limit: '10mb' })); // 允许大文件上传
// 速率限制(防止DDoS攻击)
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100 // 每个IP最多100次请求
});
app.use('/api/', limiter);
// 数据库连接
mongoose.connect('mongodb://card_admin:your_secure_password@localhost:27017/character_card_db', {
useNewUrlParser: true,
useUnifiedTopology: true
}).then(() => {
console.log('✅ 数据库连接成功');
}).catch(err => {
console.error('❌ 数据库连接失败:', err.message);
});
// 角色卡数据模型
const characterSchema = new mongoose.Schema({
name: { type: String, required: true, trim: true },
description: { type: String, default: '' },
visualData: {
modelUrl: String,
textureUrl: String,
animationData: mongoose.Schema.Types.Mixed
},
attributes: {
health: { type: Number, default: 100 },
attack: { type: Number, default: 10 },
defense: { type: Number, default: 5 },
speed: { type: Number, default: 100 }
},
skills: [{
name: String,
description: String,
cooldown: Number,
cost: Number,
effect: mongoose.Schema.Types.Mixed
}],
backstory: {
text: String,
voiceUrl: String,
relationships: [String]
},
metadata: {
creator: { type: String, required: true },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now },
visibility: { type: String, enum: ['public', 'private', 'team'], default: 'private' },
version: { type: Number, default: 1 }
}
});
// 添加索引优化查询
characterSchema.index({ name: 'text' });
characterSchema.index({ 'metadata.creator': 1 });
characterSchema.index({ 'metadata.createdAt': -1 });
const Character = mongoose.model('Character', characterSchema);
// API路由定义
// 1. 创建新角色卡
app.post('/api/characters', async (req, res) => {
try {
const { name, description, visualData, attributes, skills, backstory, creator } = req.body;
// 数据验证
if (!name || !creator) {
return res.status(400).json({ error: '名称和创建者为必填项' });
}
const newCharacter = new Character({
name,
description,
visualData,
attributes,
skills,
backstory,
'metadata.creator': creator
});
const savedCharacter = await newCharacter.save();
res.status(201).json({
success: true,
data: savedCharacter,
message: '角色卡创建成功'
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 2. 获取角色卡列表(带分页和搜索)
app.get('/api/characters', async (req, res) => {
try {
const { page = 1, limit = 10, search = '', creator } = req.query;
const skip = (page - 1) * limit;
let query = {};
if (search) {
query.$text = { $search: search };
}
if (creator) {
query['metadata.creator'] = creator;
}
const characters = await Character.find(query)
.sort({ 'metadata.createdAt': -1 })
.skip(skip)
.limit(parseInt(limit))
.select('-__v'); // 排除版本字段
const total = await Character.countDocuments(query);
res.json({
success: true,
data: characters,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 3. 获取单个角色卡详情
app.get('/api/characters/:id', async (req, res) => {
try {
const character = await Character.findById(req.params.id);
if (!character) {
return res.status(404).json({ error: '角色卡不存在' });
}
res.json({ success: true, data: character });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 4. 更新角色卡
app.put('/api/characters/:id', async (req, res) => {
try {
const updateData = { ...req.body, 'metadata.updatedAt': new Date() };
// 版本号递增
const current = await Character.findById(req.params.id);
if (current) {
updateData['metadata.version'] = current.metadata.version + 1;
}
const updatedCharacter = await Character.findByIdAndUpdate(
req.params.id,
updateData,
{ new: true, runValidators: true }
);
if (!updatedCharacter) {
return res.status(404).json({ error: '角色卡不存在' });
}
res.json({ success: true, data: updatedCharacter, message: '角色卡更新成功' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 5. 删除角色卡
app.delete('/api/characters/:id', async (req, res) => {
try {
const deleted = await Character.findByIdAndDelete(req.params.id);
if (!deleted) {
return res.status(404).json({ error: '角色卡不存在' });
}
res.json({ success: true, message: '角色卡删除成功' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 错误处理中间件
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: '服务器内部错误' });
});
// 启动服务器
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`🚀 角色卡服务器运行在端口 ${PORT}`);
console.log(`📊 API文档: http://localhost:${PORT}/api/characters`);
});
第三步:前端集成示例(React + Axios)
// api/client.js - API客户端封装
import axios from 'axios';
const API_BASE_URL = 'http://localhost:3000/api';
const apiClient = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
// 请求拦截器(添加认证token)
apiClient.interceptors.request.use(
config => {
const token = localStorage.getItem('authToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
error => Promise.reject(error)
);
// 响应拦截器(统一错误处理)
apiClient.interceptors.response.use(
response => response.data,
error => {
if (error.response) {
const { status, data } = error.response;
console.error(`API Error [${status}]:`, data.error);
// 具体状态码处理
if (status === 401) {
// 未授权,清除token并跳转登录页
localStorage.removeItem('authToken');
window.location.href = '/login';
} else if (status === 429) {
alert('请求过于频繁,请稍后再试');
}
}
return Promise.reject(error);
}
);
export const characterAPI = {
// 创建角色卡
create: (data) => apiClient.post('/characters', data),
// 获取列表
getList: (params = {}) => apiClient.get('/characters', { params }),
// 获取详情
getDetail: (id) => apiClient.get(`/characters/${id}`),
// 更新角色卡
update: (id, data) => apiClient.put(`/characters/${id}`, data),
// 删除角色卡
delete: (id) => apiClient.delete(`/characters/${id}`)
};
// React组件示例:角色卡创建表单
import React, { useState } from 'react';
import { characterAPI } from './api/client';
function CharacterCreator() {
const [formData, setFormData] = useState({
name: '',
description: '',
attributes: { health: 100, attack: 10, defense: 5, speed: 100 },
skills: [],
creator: 'user123'
});
const [loading, setLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
try {
const result = await characterAPI.create(formData);
alert('角色卡创建成功!ID: ' + result.data._id);
// 清空表单
setFormData({
name: '',
description: '',
attributes: { health: 100, attack: 10, defense: 5, speed: 100 },
skills: [],
creator: 'user123'
});
} catch (error) {
console.error('创建失败:', error);
alert('创建失败: ' + (error.response?.data?.error || error.message));
} finally {
setLoading(false);
}
};
const addSkill = () => {
setFormData(prev => ({
...prev,
skills: [...prev.skills, { name: '', description: '', cooldown: 0, cost: 0 }]
}));
};
const updateSkill = (index, field, value) => {
const newSkills = [...formData.skills];
newSkills[index][field] = value;
setFormData(prev => ({ ...prev, skills: newSkills }));
};
return (
<div className="character-creator">
<h2>创建新角色卡</h2>
<form onSubmit={handleSubmit}>
<div>
<label>角色名称:</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({...formData, name: e.target.value})}
required
/>
</div>
<div>
<label>描述:</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({...formData, description: e.target.value})}
/>
</div>
<div>
<h3>基础属性</h3>
<label>生命值: <input type="number" value={formData.attributes.health} onChange={(e) => setFormData({...formData, attributes: {...formData.attributes, health: parseInt(e.target.value)}})} /></label>
<label>攻击力: <input type="number" value={formData.attributes.attack} onChange={(e) => setFormData({...formData, attributes: {...formData.attributes, attack: parseInt(e.target.value)}})} /></label>
<label>防御力: <input type="number" value={formData.attributes.defense} onChange={(e) => setFormData({...formData, attributes: {...formData.attributes, defense: parseInt(e.target.value)}})} /></label>
<label>速度: <input type="number" value={formData.attributes.speed} onChange={(e) => setFormData({...formData, attributes: {...formData.attributes, speed: parseInt(e.target.value)}})} /></label>
</div>
<div>
<h3>技能系统</h3>
{formData.skills.map((skill, index) => (
<div key={index} style={{border: '1px solid #ccc', padding: '10px', margin: '5px 0'}}>
<input
placeholder="技能名称"
value={skill.name}
onChange={(e) => updateSkill(index, 'name', e.target.value)}
/>
<input
placeholder="描述"
value={skill.description}
onChange={(e) => updateSkill(index, 'description', e.target.value)}
/>
<input
type="number"
placeholder="冷却时间"
value={skill.cooldown}
onChange={(e) => updateSkill(index, 'cooldown', parseInt(e.target.value))}
/>
<input
type="number"
placeholder="消耗"
value={skill.cost}
onChange={(e) => updateSkill(index, 'cost', parseInt(e.target.value))}
/>
</div>
))}
<button type="button" onClick={addSkill}>添加技能</button>
</div>
<button type="submit" disabled={loading}>
{loading ? '创建中...' : '创建角色卡'}
</button>
</form>
</div>
);
}
export default CharacterCreator;
API接口详细说明
1. 创建角色卡
Endpoint: POST /api/characters
请求体示例:
{
"name": "暗夜刺客",
"description": "擅长暗杀和潜行的神秘角色",
"visualData": {
"modelUrl": "https://cdn.example.com/models/assassin.glb",
"textureUrl": "https://cdn.example.com/textures/assassin.png"
},
"attributes": {
"health": 80,
"attack": 25,
"defense": 8,
"speed": 150
},
"skills": [
{
"name": "暗影突袭",
"description": "瞬间移动到敌人背后造成200%伤害",
"cooldown": 8,
"cost": 30,
"effect": {
"type": "damage",
"multiplier": 2.0,
"position": "back"
}
}
],
"backstory": {
"text": "出生于刺客世家,为复仇而战...",
"voiceUrl": "https://cdn.example.com/voices/assassin_intro.mp3"
},
"creator": "user123"
}
响应示例:
{
"success": true,
"data": {
"_id": "64f8a1b2c3d4e5f6a7b8c9d0",
"name": "暗夜刺客",
"description": "擅长暗杀和潜行的神秘角色",
"visualData": { ... },
"attributes": { ... },
"skills": [ ... ],
"backstory": { ... },
"metadata": {
"creator": "user123",
"createdAt": "2023-09-06T10:30:00.000Z",
"updatedAt": "2023-09-06T10:30:00.000Z",
"visibility": "private",
"version": 1
}
},
"message": "角色卡创建成功"
}
2. 查询角色卡列表
Endpoint: GET /api/characters
查询参数:
page: 页码(默认1)limit: 每页数量(默认10)search: 搜索关键词(按名称和描述)creator: 按创建者筛选
示例请求:
GET /api/characters?page=1&limit=5&search=刺客&creator=user123
响应示例:
{
"success": true,
"data": [
{
"_id": "64f8a1b2c3d4e5f6a7b8c9d0",
"name": "暗夜刺客",
"description": "擅长暗杀和潜行的神秘角色",
"metadata": {
"creator": "user123",
"createdAt": "2023-09-06T10:30:00.000Z",
"version": 1
}
}
],
"pagination": {
"page": 1,
"limit": 5,
"total": 3,
"pages": 1
}
}
3. 更新角色卡
Endpoint: PUT /api/characters/:id
请求体: 与创建接口类似,可以只包含需要更新的字段
示例请求:
{
"attributes": {
"health": 90,
"attack": 30
}
}
响应: 返回更新后的完整角色卡数据,metadata.version会自动+1
4. 删除角色卡
Endpoint: DELETE /api/characters/:id
响应:
{
"success": true,
"message": "角色卡删除成功"
}
新手常见问题与解决方案
问题1:数据库连接失败
症状: 启动服务器时出现 MongoDB connection failed 错误
解决方案:
# 1. 检查MongoDB服务状态
sudo systemctl status mongod
# 2. 如果未运行,启动服务
sudo systemctl start mongod
# 3. 检查端口是否被监听
netstat -tuln | grep 27017
# 4. 验证连接字符串(注意密码特殊字符需要URL编码)
# 如果密码包含@、:等字符,需要转义
# 例如:密码 "pass@word123" 应写为 "pass%40word123"
# 5. 检查防火墙设置
sudo ufw status
问题2:跨域请求(CORS)被拒绝
症状: 前端调用API时浏览器控制台显示CORS错误
解决方案:
// 在server.js中正确配置CORS
const cors = require('cors');
// 允许特定域名
app.use(cors({
origin: ['http://localhost:3001', 'https://yourdomain.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true // 如果需要cookie
}));
// 或者允许所有(仅开发环境)
if (process.env.NODE_ENV === 'development') {
app.use(cors());
}
问题3:请求体过大导致413错误
症状: 上传角色模型或贴图时失败
解决方案:
// 增加请求体大小限制
app.use(express.json({ limit: '50mb' })); // 默认100kb
app.use(express.urlencoded({ limit: '50mb', extended: true }));
// 如果是文件上传,使用multer
const multer = require('multer');
const upload = multer({
limits: { fileSize: 50 * 1024 * 1024 } // 50MB
});
问题4:数据库查询性能慢
症状: 获取角色卡列表时响应时间超过2秒
解决方案:
// 1. 添加合适的索引
characterSchema.index({ name: 'text' }); // 文本搜索
characterSchema.index({ 'metadata.createdAt': -1 }); // 时间排序
characterSchema.index({ 'metadata.creator': 1 }); // 按创建者查询
// 2. 使用lean()方法加速查询(返回纯JS对象而非Mongoose文档)
app.get('/api/characters', async (req, res) => {
const characters = await Character.find(query)
.lean() // 性能优化
.skip(skip)
.limit(parseInt(limit));
});
// 3. 限制返回字段
.select('name description metadata.createdAt') // 只返回需要的字段
问题5:并发请求导致数据不一致
症状: 多人同时编辑同一角色卡时,后保存的会覆盖先保存的
解决方案:
// 使用乐观锁(版本号控制)
characterSchema.pre('save', function(next) {
if (this.isModified()) {
this.metadata.updatedAt = new Date();
this.metadata.version = (this.metadata.version || 0) + 1;
}
next();
});
// 更新时检查版本号
app.put('/api/characters/:id', async (req, res) => {
const { version } = req.body.metadata || {};
const current = await Character.findById(req.params.id);
if (current.metadata.version !== version) {
return res.status(409).json({
error: '数据已过期,请刷新后重试',
currentVersion: current.metadata.version
});
}
// 继续更新...
});
问题6:敏感数据泄露
症状: API返回了不应该公开的字段(如内部ID、密码哈希等)
解决方案:
// 1. 使用DTO(数据传输对象)过滤字段
function sanitizeCharacter(character) {
const { __v, password, ...safeData } = character.toObject();
return safeData;
}
// 2. 在查询时排除字段
.select('-__v -password') // 排除敏感字段
// 3. 使用Mongoose的transform
characterSchema.set('toJSON', {
transform: (doc, ret) => {
delete ret.__v;
return ret;
}
});
问题7:文件存储和CDN集成
症状: 角色模型和贴图文件存储混乱,访问速度慢
解决方案:
// 使用AWS S3或阿里云OSS存储
const AWS = require('aws-sdk');
const s3 = new AWS.S3({
accessKeyId: process.env.AWS_ACCESS_KEY,
secretAccessKey: process.env.AWS_SECRET_KEY,
region: 'us-east-1'
});
// 上传文件到S3
async function uploadToS3(fileBuffer, fileName) {
const params = {
Bucket: 'your-character-assets',
Key: `characters/${Date.now()}_${fileName}`,
Body: fileBuffer,
ContentType: 'image/png', // 或 model/gltf-binary
ACL: 'public-read'
};
const result = await s3.upload(params).promise();
return result.Location; // 返回CDN URL
}
// 在Express路由中使用
const upload = multer({ storage: multer.memoryStorage() });
app.post('/api/upload-asset', upload.single('file'), async (req, res) => {
try {
const url = await uploadToS3(req.file.buffer, req.file.originalname);
res.json({ success: true, url });
} catch (error) {
res.status(500).json({ error: '上传失败' });
}
});
问题8:API限流和防滥用
症状: 恶意用户频繁请求导致服务器资源耗尽
解决方案:
// 使用express-rate-limit
const rateLimit = require('express-rate-limit');
// 通用限流
const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 每个IP最多100次请求
message: '请求过于频繁,请15分钟后再试'
});
// 严格限流(针对敏感操作)
const strictLimiter = rateLimit({
windowMs: 60 * 1000, // 1分钟
max: 5, // 每分钟最多5次
message: '操作过于频繁,请稍后再试'
});
// 应用到特定路由
app.use('/api/', generalLimiter);
app.post('/api/characters', strictLimiter);
app.put('/api/characters/:id', strictLimiter);
app.delete('/api/characters/:id', strictLimiter);
// IP白名单(管理员)
const whitelist = ['127.0.0.1', '192.168.1.100'];
app.use('/api/admin', (req, res, next) => {
if (whitelist.includes(req.ip)) {
next();
} else {
res.status(403).json({ error: '无权访问' });
}
});
问题9:日志记录和监控
症状: 出现问题时难以定位错误原因
解决方案:
// 使用winston记录日志
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
new winston.transports.Console({
format: winston.format.simple()
})
]
});
// 在路由中记录日志
app.use((req, res, next) => {
logger.info(`${req.method} ${req.url} - IP: ${req.ip}`);
next();
});
// 错误处理
app.use((err, req, res, next) => {
logger.error(`${req.method} ${req.url} - Error: ${err.message}`, { stack: err.stack });
res.status(500).json({ error: '服务器内部错误' });
});
问题10:环境变量管理
症状: 硬编码敏感信息(数据库密码、API密钥)导致安全风险
解决方案:
# 1. 创建.env文件(添加到.gitignore)
# .env
NODE_ENV=production
PORT=3000
DATABASE_URL=mongodb://card_admin:your_secure_password@localhost:27017/character_card_db
JWT_SECRET=your_jwt_secret_key
AWS_ACCESS_KEY=your_aws_key
AWS_SECRET_KEY=your_aws_secret
// 2. 使用dotenv加载环境变量
require('dotenv').config();
// 3. 在代码中使用
const DATABASE_URL = process.env.DATABASE_URL;
const PORT = process.env.PORT || 3000;
// 4. 验证必需的环境变量
const requiredEnvVars = ['DATABASE_URL', 'JWT_SECRET'];
requiredEnvVars.forEach(varName => {
if (!process.env[varName]) {
throw new Error(`必需的环境变量 ${varName} 未设置`);
}
});
高级功能扩展
1. 权限管理系统
// 中间件:检查用户权限
function checkPermission(requiredRole) {
return async (req, res, next) => {
const userId = req.user.id; // 从JWT token中获取
const resourceId = req.params.id;
// 检查是否是资源创建者或有管理权限
const resource = await Character.findById(resourceId);
if (!resource) {
return res.status(404).json({ error: '资源不存在' });
}
if (resource.metadata.creator === userId || req.user.role === 'admin') {
next();
} else {
res.status(403).json({ error: '无权修改此资源' });
}
};
}
// 使用示例
app.put('/api/characters/:id',
authenticateJWT,
checkPermission('editor'),
async (req, res) => {
// 更新逻辑
}
);
2. 实时更新(WebSocket)
// 使用Socket.io实现实时更新
const http = require('http');
const socketIo = require('socket.io');
const server = http.createServer(app);
const io = socketIo(server, {
cors: {
origin: "http://localhost:3001",
methods: ["GET", "POST"]
}
});
io.on('connection', (socket) => {
console.log('用户连接:', socket.id);
// 加入房间(按角色卡ID)
socket.on('join-character', (characterId) => {
socket.join(characterId);
});
// 监听更新
socket.on('update-character', async (data) => {
// 保存到数据库
const updated = await Character.findByIdAndUpdate(data.id, data.update, { new: true });
// 广播给房间内所有用户
io.to(data.id).emit('character-updated', updated);
});
socket.on('disconnect', () => {
console.log('用户断开:', socket.id);
});
});
server.listen(3000);
3. 数据备份和恢复
#!/bin/bash
# backup.sh - 每日备份脚本
BACKUP_DIR="/backups/character_card"
DATE=$(date +%Y%m%d_%H%M%S)
MONGO_URI="mongodb://card_admin:your_secure_password@localhost:27017/character_card_db"
# 创建备份目录
mkdir -p $BACKUP_DIR
# 执行MongoDB备份
mongodump --uri="$MONGO_URI" --out="$BACKUP_DIR/backup_$DATE"
# 压缩备份文件
tar -czf "$BACKUP_DIR/backup_$DATE.tar.gz" -C "$BACKUP_DIR/backup_$DATE" .
# 删除旧备份(保留最近7天)
find $BACKUP_DIR -name "backup_*.tar.gz" -mtime +7 -delete
# 清理临时目录
rm -rf "$BACKUP_DIR/backup_$DATE"
echo "备份完成: $BACKUP_DIR/backup_$DATE.tar.gz"
部署到生产环境
使用PM2进程管理
# 安装PM2
npm install -g pm2
# 启动应用
pm2 start server.js --name character-card-server
# 配置PM2配置文件 ecosystem.config.js
module.exports = {
apps: [{
name: 'character-card-server',
script: './server.js',
instances: 'max', // 使用所有CPU核心
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000
},
error_file: './logs/err.log',
out_file: './logs/out.log',
log_file: './logs/combined.log',
time: true
}]
};
# 使用配置文件启动
pm2 start ecosystem.config.js
# 设置开机自启
pm2 startup
pm2 save
使用Nginx反向代理
# /etc/nginx/sites-available/character-card
server {
listen 80;
server_name api.yourdomain.com;
# 限流配置
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
location / {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 超时设置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# 静态文件缓存
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
proxy_pass http://localhost:3000;
}
}
总结与最佳实践
性能优化清单
- ✅ 数据库索引优化
- ✅ 查询字段限制(select)
- ✅ 使用lean()加速查询
- ✅ 实现缓存层(Redis)
- ✅ 启用Gzip压缩
- ✅ 使用CDN分发静态资源
安全加固清单
- ✅ 使用HTTPS
- ✅ 实施JWT认证
- ✅ 速率限制
- ✅ 输入验证和清理
- ✅ 敏感数据过滤
- ✅ 环境变量管理
- ✅ 定期备份
开发建议
- 从小开始:先实现核心功能,再逐步扩展
- 文档先行:在编码前设计好API接口
- 测试驱动:编写单元测试和集成测试
- 监控先行:部署前设置好日志和监控
- 安全第一:始终考虑安全最佳实践
通过本文的详细指导,你应该能够从零开始搭建一个功能完整、安全可靠的角色卡服务器。记住,良好的架构设计和持续的优化是项目成功的关键。如果在实际操作中遇到问题,可以参考文中的常见问题解决方案,或者在开发者社区寻求帮助。
