🚀 Node.js全栈开发实战指南

📖 概述

Node.js作为现代全栈开发的核心技术,以其高性能、轻量级和丰富的生态系统成为开发者的首选。本指南将深入探讨Node.js全栈开发的各个方面,从基础架构搭建到高级性能优化。

🏗️ 项目架构设计

1. 现代Node.js项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
nodejs-fullstack-app/
├── src/
│ ├── controllers/ # 控制器层
│ ├── services/ # 业务逻辑层
│ ├── models/ # 数据模型层
│ ├── middleware/ # 中间件
│ ├── routes/ # 路由定义
│ ├── utils/ # 工具函数
│ ├── config/ # 配置文件
│ ├── validators/ # 数据验证
│ └── types/ # TypeScript类型定义
├── tests/ # 测试文件
├── docs/ # 文档
├── scripts/ # 构建脚本
├── docker/ # Docker配置
├── .env.example # 环境变量示例
├── package.json
├── tsconfig.json
├── jest.config.js
└── docker-compose.yml

2. 基础配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// package.json
{
"name": "nodejs-fullstack-app",
"version": "1.0.0",
"description": "Modern Node.js fullstack application",
"main": "dist/index.js",
"scripts": {
"dev": "nodemon --exec ts-node src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint src/**/*.ts",
"lint:fix": "eslint src/**/*.ts --fix",
"format": "prettier --write src/**/*.ts",
"docker:build": "docker build -t nodejs-app .",
"docker:run": "docker run -p 3000:3000 nodejs-app",
"migrate": "knex migrate:latest",
"seed": "knex seed:run"
},
"dependencies": {
"express": "^4.18.2",
"koa": "^2.14.2",
"mongoose": "^7.5.0",
"pg": "^8.11.3",
"redis": "^4.6.8",
"socket.io": "^4.7.2",
"apollo-server-express": "^3.12.1",
"graphql": "^16.8.1",
"jsonwebtoken": "^9.0.2",
"bcryptjs": "^2.4.3",
"joi": "^17.9.2",
"helmet": "^7.0.0",
"cors": "^2.8.5",
"compression": "^1.7.4",
"winston": "^3.10.0",
"dotenv": "^16.3.1"
},
"devDependencies": {
"@types/node": "^20.5.0",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.4",
"typescript": "^5.1.6",
"ts-node": "^10.9.1",
"nodemon": "^3.0.1",
"jest": "^29.6.2",
"supertest": "^6.3.3",
"eslint": "^8.47.0",
"prettier": "^3.0.2"

2. 缓存中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// src/middleware/cache.ts - 缓存中间件
import { Request, Response, NextFunction } from 'express';
import { CacheService } from '@/services/CacheService';
import { logger } from '@/utils/logger';

interface CacheOptions {
ttl?: number;
keyGenerator?: (req: Request) => string;
condition?: (req: Request, res: Response) => boolean;
skipCache?: (req: Request) => boolean;
}

export const cacheMiddleware = (options: CacheOptions = {}) => {
const {
ttl = 300,
keyGenerator = (req: Request) => `cache:${req.method}:${req.originalUrl}`,
condition = (req: Request, res: Response) => res.statusCode === 200,
skipCache = (req: Request) => req.method !== 'GET'
} = options;

return async (req: Request, res: Response, next: NextFunction) => {
// 跳过缓存条件
if (skipCache(req)) {
return next();
}

const cacheKey = keyGenerator(req);

try {
// 尝试从缓存获取
const cached = await CacheService.get(cacheKey);
if (cached) {
logger.debug(`Cache hit for key: ${cacheKey}`);
return res.json(cached);
}

// 缓存未命中,继续处理请求
logger.debug(`Cache miss for key: ${cacheKey}`);

// 拦截响应
const originalSend = res.send;
const originalJson = res.json;

let responseData: any;

res.send = function(data: any) {
responseData = data;
return originalSend.call(this, data);
};

res.json = function(data: any) {
responseData = data;
return originalJson.call(this, data);
};

// 响应结束后缓存数据
res.on('finish', async () => {
if (condition(req, res) && responseData) {
try {
await CacheService.set(cacheKey, responseData, ttl);
logger.debug(`Cached response for key: ${cacheKey}`);
} catch (error) {
logger.error(`Failed to cache response for key ${cacheKey}:`, error);
}
}
});

next();
} catch (error) {
logger.error(`Cache middleware error for key ${cacheKey}:`, error);
next();
}
};
};

// 用户相关缓存中间件
export const userCacheMiddleware = cacheMiddleware({
ttl: 600, // 10分钟
keyGenerator: (req: Request) => `user:${req.params.id || req.user?.id}:${req.originalUrl}`,
condition: (req: Request, res: Response) => res.statusCode === 200 && req.method === 'GET'
});

// API响应缓存中间件
export const apiCacheMiddleware = cacheMiddleware({
ttl: 300, // 5分钟
keyGenerator: (req: Request) => {
const query = JSON.stringify(req.query);
return `api:${req.route?.path || req.path}:${query}`;
},
skipCache: (req: Request) => ['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method)
});

// 缓存失效中间件
export const cacheInvalidationMiddleware = (patterns: string[]) => {
return async (req: Request, res: Response, next: NextFunction) => {
// 先执行请求
const originalSend = res.send;
const originalJson = res.json;

res.send = function(data: any) {
// 请求成功后清除相关缓存
if (res.statusCode >= 200 && res.statusCode < 300) {
invalidateCache(patterns, req);
}
return originalSend.call(this, data);
};

res.json = function(data: any) {
// 请求成功后清除相关缓存
if (res.statusCode >= 200 && res.statusCode < 300) {
invalidateCache(patterns, req);
}
return originalJson.call(this, data);
};

next();
};
};

async function invalidateCache(patterns: string[], req: Request) {
try {
for (const pattern of patterns) {
// 替换模式中的占位符
const resolvedPattern = pattern
.replace(':userId', req.params.userId || req.user?.id || '*')
.replace(':id', req.params.id || '*');

const deletedCount = await CacheService.deletePattern(resolvedPattern);
logger.debug(`Invalidated ${deletedCount} cache entries for pattern: ${resolvedPattern}`);
}
} catch (error) {
logger.error('Cache invalidation error:', error);
}

🧪 测试策略

1. 单元测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
// tests/unit/services/UserService.test.ts - 用户服务单元测试
import { UserService } from '@/services/UserService';
import { User } from '@/models/User';
import { AppError } from '@/utils/AppError';

// Mock数据库模型
jest.mock('@/models/User');
const MockedUser = User as jest.Mocked<typeof User>;

describe('UserService', () => {
let userService: UserService;

beforeEach(() => {
userService = new UserService();
jest.clearAllMocks();
});

describe('createUser', () => {
it('should create a new user successfully', async () => {
const userData = {
username: 'testuser',
email: 'test@example.com',
password: 'password123',
firstName: 'Test',
lastName: 'User'
};

const mockUser = {
id: '1',
...userData,
createdAt: new Date(),
updatedAt: new Date()
};

MockedUser.findOne.mockResolvedValue(null);
MockedUser.create.mockResolvedValue(mockUser as any);

const result = await userService.createUser(userData);

expect(MockedUser.findOne).toHaveBeenCalledWith({
$or: [
{ email: userData.email },
{ username: userData.username }
]
});
expect(MockedUser.create).toHaveBeenCalledWith(userData);
expect(result).toEqual(mockUser);
});

it('should throw error if user already exists', async () => {
const userData = {
username: 'testuser',
email: 'test@example.com',
password: 'password123',
firstName: 'Test',
lastName: 'User'
};

MockedUser.findOne.mockResolvedValue({ id: '1' } as any);

await expect(userService.createUser(userData)).rejects.toThrow(AppError);
expect(MockedUser.create).not.toHaveBeenCalled();
});
});

describe('getUserById', () => {
it('should return user if found', async () => {
const userId = '1';
const mockUser = {
id: userId,
username: 'testuser',
email: 'test@example.com'
};

MockedUser.findById.mockResolvedValue(mockUser as any);

const result = await userService.getUserById(userId);

expect(MockedUser.findById).toHaveBeenCalledWith(userId);
expect(result).toEqual(mockUser);
});

it('should return null if user not found', async () => {
const userId = '1';

MockedUser.findById.mockResolvedValue(null);

const result = await userService.getUserById(userId);

expect(result).toBeNull();
});
});

describe('updateUser', () => {
it('should update user successfully', async () => {
const userId = '1';
const updateData = { firstName: 'Updated' };
const mockUser = {
id: userId,
username: 'testuser',
firstName: 'Updated'
};

MockedUser.findByIdAndUpdate.mockResolvedValue(mockUser as any);

const result = await userService.updateUser(userId, updateData);

expect(MockedUser.findByIdAndUpdate).toHaveBeenCalledWith(
userId,
updateData,
{ new: true, runValidators: true }
);
expect(result).toEqual(mockUser);
});
});
});

2. 集成测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
// tests/integration/auth.test.ts - 认证集成测试
import request from 'supertest';
import { app } from '@/app';
import { User } from '@/models/User';
import { connectDB, disconnectDB } from '@/config/database';

describe('Authentication Integration Tests', () => {
beforeAll(async () => {
await connectDB();
});

afterAll(async () => {
await disconnectDB();
});

beforeEach(async () => {
await User.deleteMany({});
});

describe('POST /api/auth/register', () => {
it('should register a new user', async () => {
const userData = {
username: 'testuser',
email: 'test@example.com',
password: 'password123',
firstName: 'Test',
lastName: 'User'
};

const response = await request(app)
.post('/api/auth/register')
.send(userData)
.expect(201);

expect(response.body.success).toBe(true);
expect(response.body.data.user.email).toBe(userData.email);
expect(response.body.data.tokens).toBeDefined();

// 验证用户已保存到数据库
const user = await User.findOne({ email: userData.email });
expect(user).toBeTruthy();
});

it('should return error for duplicate email', async () => {
const userData = {
username: 'testuser',
email: 'test@example.com',
password: 'password123',
firstName: 'Test',
lastName: 'User'
};

// 创建第一个用户
await request(app)
.post('/api/auth/register')
.send(userData)
.expect(201);

// 尝试创建重复用户
const response = await request(app)
.post('/api/auth/register')
.send(userData)
.expect(400);

expect(response.body.success).toBe(false);
expect(response.body.message).toContain('already exists');
});
});

describe('POST /api/auth/login', () => {
beforeEach(async () => {
// 创建测试用户
const userData = {
username: 'testuser',
email: 'test@example.com',
password: 'password123',
firstName: 'Test',
lastName: 'User'
};

await request(app)
.post('/api/auth/register')
.send(userData);
});

it('should login with valid credentials', async () => {
const loginData = {
email: 'test@example.com',
password: 'password123'
};

const response = await request(app)
.post('/api/auth/login')
.send(loginData)
.expect(200);

expect(response.body.success).toBe(true);
expect(response.body.data.user.email).toBe(loginData.email);
expect(response.body.data.tokens).toBeDefined();
});

it('should return error for invalid credentials', async () => {
const loginData = {
email: 'test@example.com',
password: 'wrongpassword'
};

const response = await request(app)
.post('/api/auth/login')
.send(loginData)
.expect(401);

expect(response.body.success).toBe(false);
});
});

describe('Protected Routes', () => {
let authToken: string;

beforeEach(async () => {
// 注册并登录用户
const userData = {
username: 'testuser',
email: 'test@example.com',
password: 'password123',
firstName: 'Test',
lastName: 'User'
};

const registerResponse = await request(app)
.post('/api/auth/register')
.send(userData);

authToken = registerResponse.body.data.tokens.accessToken;
});

it('should access protected route with valid token', async () => {
const response = await request(app)
.get('/api/auth/me')
.set('Authorization', `Bearer ${authToken}`)
.expect(200);

expect(response.body.success).toBe(true);
expect(response.body.data.email).toBe('test@example.com');
});

it('should return error for protected route without token', async () => {
const response = await request(app)
.get('/api/auth/me')
.expect(401);

expect(response.body.success).toBe(false);
});
});
});

3. E2E测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// tests/e2e/user-flow.test.ts - 用户流程E2E测试
import { test, expect } from '@playwright/test';

test.describe('User Registration and Login Flow', () => {
test('complete user registration and login flow', async ({ page }) => {
// 访问注册页面
await page.goto('/register');

// 填写注册表单
await page.fill('[data-testid="username"]', 'testuser');
await page.fill('[data-testid="email"]', 'test@example.com');
await page.fill('[data-testid="password"]', 'password123');
await page.fill('[data-testid="firstName"]', 'Test');
await page.fill('[data-testid="lastName"]', 'User');

// 提交注册表单
await page.click('[data-testid="register-button"]');

// 验证注册成功
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();

// 跳转到登录页面
await page.goto('/login');

// 填写登录表单
await page.fill('[data-testid="email"]', 'test@example.com');
await page.fill('[data-testid="password"]', 'password123');

// 提交登录表单
await page.click('[data-testid="login-button"]');

// 验证登录成功,跳转到仪表板
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('[data-testid="user-name"]')).toContainText('Test User');
});

test('should show error for invalid login', async ({ page }) => {
await page.goto('/login');

await page.fill('[data-testid="email"]', 'invalid@example.com');
await page.fill('[data-testid="password"]', 'wrongpassword');

await page.click('[data-testid="login-button"]');

await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
await expect(page.locator('[data-testid="error-message"]')).toContainText('Invalid credentials');
});
});

test.describe('File Upload Flow', () => {
test.beforeEach(async ({ page }) => {
// 登录用户
await page.goto('/login');
await page.fill('[data-testid="email"]', 'test@example.com');
await page.fill('[data-testid="password"]', 'password123');
await page.click('[data-testid="login-button"]');
await expect(page).toHaveURL('/dashboard');
});

test('should upload file successfully', async ({ page }) => {
await page.goto('/upload');

// 选择文件
const fileInput = page.locator('[data-testid="file-input"]');
await fileInput.setInputFiles('tests/fixtures/test-image.jpg');

// 上传文件
await page.click('[data-testid="upload-button"]');

// 验证上传成功
await expect(page.locator('[data-testid="upload-success"]')).toBeVisible();
await expect(page.locator('[data-testid="uploaded-file"]')).toBeVisible();
});
});

🚀 部署与运维

1. Docker部署

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# Dockerfile
FROM node:18-alpine AS builder

WORKDIR /app

# 复制package文件
COPY package*.json ./
COPY tsconfig.json ./

# 安装依赖
RUN npm ci --only=production

# 复制源代码
COPY src ./src

# 构建应用
RUN npm run build

# 生产环境镜像
FROM node:18-alpine AS production

WORKDIR /app

# 创建非root用户
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001

# 复制构建产物和依赖
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package*.json ./

# 创建上传目录
RUN mkdir -p uploads && chown -R nodejs:nodejs uploads

# 切换到非root用户
USER nodejs

# 暴露端口
EXPOSE 3000

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node dist/healthcheck.js

# 启动应用
CMD ["node", "dist/server.js"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# docker-compose.yml
version: '3.8'

services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=mongodb://mongo:27017/myapp
- REDIS_HOST=redis
- REDIS_PORT=6379
depends_on:
- mongo
- redis
volumes:
- ./uploads:/app/uploads
restart: unless-stopped

mongo:
image: mongo:6.0
ports:
- "27017:27017"
environment:
- MONGO_INITDB_ROOT_USERNAME=admin
- MONGO_INITDB_ROOT_PASSWORD=password
volumes:
- mongo_data:/data/db
restart: unless-stopped

redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
restart: unless-stopped

nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/nginx/ssl
depends_on:
- app
restart: unless-stopped

volumes:
mongo_data:
redis_data:

2. CI/CD流水线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# .github/workflows/deploy.yml
name: Deploy to Production

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest

services:
mongodb:
image: mongo:6.0
ports:
- 27017:27017
redis:
image: redis:7-alpine
ports:
- 6379:6379

steps:
- uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Run linting
run: npm run lint

- name: Run type checking
run: npm run type-check

- name: Run unit tests
run: npm run test:unit
env:
NODE_ENV: test
DATABASE_URL: mongodb://localhost:27017/test
REDIS_HOST: localhost
REDIS_PORT: 6379

- name: Run integration tests
run: npm run test:integration
env:
NODE_ENV: test
DATABASE_URL: mongodb://localhost:27017/test
REDIS_HOST: localhost
REDIS_PORT: 6379

- name: Install Playwright
run: npx playwright install

- name: Run E2E tests
run: npm run test:e2e

- name: Upload test coverage
uses: codecov/codecov-action@v3

build:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'

steps:
- uses: actions/checkout@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2

- name: Login to Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max

deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'

steps:
- name: Deploy to production
uses: appleboy/ssh-action@v0.1.5
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.SSH_KEY }}
script: |
cd /opt/myapp
docker-compose pull
docker-compose up -d
docker system prune -f

3. 监控和日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// src/utils/monitoring.ts - 监控工具
import { Request, Response, NextFunction } from 'express';
import { logger } from './logger';

export interface MetricData {
name: string;
value: number;
tags?: Record<string, string>;
timestamp?: Date;
}

export class MonitoringService {
private static metrics: MetricData[] = [];

public static recordMetric(metric: MetricData): void {
this.metrics.push({
...metric,
timestamp: metric.timestamp || new Date()
});

// 发送到监控系统(如Prometheus、DataDog等)
this.sendToMonitoringSystem(metric);
}

public static recordRequestMetrics() {
return (req: Request, res: Response, next: NextFunction) => {
const startTime = Date.now();

res.on('finish', () => {
const duration = Date.now() - startTime;

this.recordMetric({
name: 'http_request_duration_ms',
value: duration,
tags: {
method: req.method,
route: req.route?.path || req.path,
status_code: res.statusCode.toString()
}
});

this.recordMetric({
name: 'http_requests_total',
value: 1,
tags: {
method: req.method,
route: req.route?.path || req.path,
status_code: res.statusCode.toString()
}
});
});

next();
};
}

private static sendToMonitoringSystem(metric: MetricData): void {
// 实际实现中,这里会发送到监控系统
logger.debug('Metric recorded:', metric);
}

public static getMetrics(): MetricData[] {
return this.metrics;
}

public static clearMetrics(): void {
this.metrics = [];
}
}

// 健康检查端点
export const healthCheck = async (req: Request, res: Response): Promise<void> => {
const health = {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage(),
version: process.env.npm_package_version || '1.0.0'
};

res.json(health);
};

📋 总结与最佳实践

核心要点

  1. 架构设计

    • 采用分层架构,职责分离
    • 使用依赖注入提高可测试性
    • 实现统一的错误处理机制
  2. 性能优化

    • 合理使用缓存策略
    • 数据库查询优化
    • 异步处理和队列机制
  3. 安全防护

    • 输入验证和参数化查询
    • 认证授权机制
    • 安全头和HTTPS
  4. 开发效率

    • TypeScript类型安全
    • 自动化测试覆盖
    • CI/CD流水线
  5. 运维监控

    • 结构化日志记录
    • 性能指标监控
    • 健康检查机制

未来发展方向

  • 微服务架构:随着业务复杂度增加,考虑拆分为微服务
  • Serverless部署:利用云函数降低运维成本
  • GraphQL Federation:大型应用的GraphQL架构
  • 实时功能增强:WebRTC、Server-Sent Events等
  • AI集成:机器学习模型集成和智能化功能

通过本指南的学习和实践,你将掌握现代Node.js全栈开发的核心技能,能够构建高质量、可扩展的Web应用程序。记住,技术在不断发展,保持学习和实践是成为优秀开发者的关键。

📁 文件上传与处理

1. 文件上传服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
// src/services/FileUploadService.ts - 文件上传服务
import multer from 'multer';
import path from 'path';
import fs from 'fs/promises';
import sharp from 'sharp';
import { v4 as uuidv4 } from 'uuid';
import { config } from '@/config';
import { logger } from '@/utils/logger';
import { AppError } from '@/utils/AppError';

export interface UploadedFile {
id: string;
originalName: string;
filename: string;
path: string;
url: string;
mimetype: string;
size: number;
uploadedAt: Date;
}

export class FileUploadService {
private static uploadDir = path.join(process.cwd(), 'uploads');
private static maxFileSize = 10 * 1024 * 1024; // 10MB
private static allowedMimeTypes = [
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'application/pdf',
'text/plain',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
];

public static async initialize(): Promise<void> {
try {
await fs.mkdir(this.uploadDir, { recursive: true });
await fs.mkdir(path.join(this.uploadDir, 'images'), { recursive: true });
await fs.mkdir(path.join(this.uploadDir, 'documents'), { recursive: true });
await fs.mkdir(path.join(this.uploadDir, 'temp'), { recursive: true });
logger.info('Upload directories initialized');
} catch (error) {
logger.error('Failed to initialize upload directories:', error);
throw error;
}
}

// Multer配置
public static getMulterConfig() {
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const isImage = file.mimetype.startsWith('image/');
const subDir = isImage ? 'images' : 'documents';
cb(null, path.join(this.uploadDir, subDir));
},
filename: (req, file, cb) => {
const uniqueName = `${uuidv4()}${path.extname(file.originalname)}`;
cb(null, uniqueName);
}
});

const fileFilter = (req: any, file: Express.Multer.File, cb: multer.FileFilterCallback) => {
if (this.allowedMimeTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new AppError(`File type ${file.mimetype} is not allowed`, 400));
}
};

return multer({
storage,
fileFilter,
limits: {
fileSize: this.maxFileSize,
files: 5 // 最多5个文件
}
});
}

// 单文件上传
public static async uploadSingle(file: Express.Multer.File, userId?: string): Promise<UploadedFile> {
try {
const fileId = uuidv4();
const isImage = file.mimetype.startsWith('image/');

let processedPath = file.path;
let processedSize = file.size;

// 图片处理
if (isImage) {
processedPath = await this.processImage(file.path, {
maxWidth: 1920,
maxHeight: 1080,
quality: 85
});

const stats = await fs.stat(processedPath);
processedSize = stats.size;
}

const uploadedFile: UploadedFile = {
id: fileId,
originalName: file.originalname,
filename: path.basename(processedPath),
path: processedPath,
url: this.getFileUrl(processedPath),
mimetype: file.mimetype,
size: processedSize,
uploadedAt: new Date()
};

// 保存文件信息到数据库(这里省略具体实现)
await this.saveFileInfo(uploadedFile, userId);

logger.info(`File uploaded successfully: ${uploadedFile.filename}`);
return uploadedFile;
} catch (error) {
logger.error('File upload error:', error);
// 清理失败的文件
if (file.path) {
await this.deleteFile(file.path).catch(() => {});
}
throw error;
}
}

// 多文件上传
public static async uploadMultiple(files: Express.Multer.File[], userId?: string): Promise<UploadedFile[]> {
const uploadedFiles: UploadedFile[] = [];
const errors: Error[] = [];

for (const file of files) {
try {
const uploadedFile = await this.uploadSingle(file, userId);
uploadedFiles.push(uploadedFile);
} catch (error) {
errors.push(error as Error);
logger.error(`Failed to upload file ${file.originalname}:`, error);
}
}

if (errors.length > 0 && uploadedFiles.length === 0) {
throw new AppError('All file uploads failed', 400);
}

return uploadedFiles;
}

// 图片处理
private static async processImage(inputPath: string, options: {
maxWidth?: number;
maxHeight?: number;
quality?: number;
}): Promise<string> {
const { maxWidth = 1920, maxHeight = 1080, quality = 85 } = options;

try {
const outputPath = inputPath.replace(/\.[^/.]+$/, '_processed.jpg');

await sharp(inputPath)
.resize(maxWidth, maxHeight, {
fit: 'inside',
withoutEnlargement: true
})
.jpeg({ quality })
.toFile(outputPath);

// 删除原文件
await this.deleteFile(inputPath);

return outputPath;
} catch (error) {
logger.error('Image processing error:', error);
return inputPath; // 返回原文件路径
}
}

// 生成缩略图
public static async generateThumbnail(imagePath: string, size: number = 200): Promise<string> {
try {
const thumbnailPath = imagePath.replace(/\.[^/.]+$/, `_thumb_${size}.jpg`);

await sharp(imagePath)
.resize(size, size, {
fit: 'cover',
position: 'center'
})
.jpeg({ quality: 80 })
.toFile(thumbnailPath);

return thumbnailPath;
} catch (error) {
logger.error('Thumbnail generation error:', error);
throw error;
}
}

// 获取文件URL
private static getFileUrl(filePath: string): string {
const relativePath = path.relative(this.uploadDir, filePath);
return `${config.BASE_URL}/uploads/${relativePath.replace(/\\/g, '/')}`;
}

// 删除文件
public static async deleteFile(filePath: string): Promise<boolean> {
try {
await fs.unlink(filePath);
return true;
} catch (error) {
logger.error(`Failed to delete file ${filePath}:`, error);
return false;
}
}

// 删除用户文件
public static async deleteUserFile(fileId: string, userId: string): Promise<boolean> {
try {
// 从数据库获取文件信息
const fileInfo = await this.getFileInfo(fileId, userId);
if (!fileInfo) {
throw new AppError('File not found', 404);
}

// 删除物理文件
await this.deleteFile(fileInfo.path);

// 删除缩略图(如果存在)
const thumbnailPath = fileInfo.path.replace(/\.[^/.]+$/, '_thumb_200.jpg');
await this.deleteFile(thumbnailPath).catch(() => {});

// 从数据库删除记录
await this.removeFileInfo(fileId);

logger.info(`File deleted successfully: ${fileInfo.filename}`);
return true;
} catch (error) {
logger.error(`Failed to delete user file ${fileId}:`, error);
throw error;
}
}

// 获取文件信息
public static async getFileInfo(fileId: string, userId?: string): Promise<UploadedFile | null> {
// 这里应该从数据库获取文件信息
// 实际实现需要根据使用的数据库进行调整
return null;
}

// 保存文件信息到数据库
private static async saveFileInfo(fileInfo: UploadedFile, userId?: string): Promise<void> {
// 这里应该保存文件信息到数据库
// 实际实现需要根据使用的数据库进行调整
}

// 从数据库删除文件信息
private static async removeFileInfo(fileId: string): Promise<void> {
// 这里应该从数据库删除文件信息
// 实际实现需要根据使用的数据库进行调整
}

// 清理临时文件
public static async cleanupTempFiles(): Promise<void> {
try {
const tempDir = path.join(this.uploadDir, 'temp');
const files = await fs.readdir(tempDir);
const now = Date.now();
const maxAge = 24 * 60 * 60 * 1000; // 24小时

for (const file of files) {
const filePath = path.join(tempDir, file);
const stats = await fs.stat(filePath);

if (now - stats.mtime.getTime() > maxAge) {
await this.deleteFile(filePath);
logger.debug(`Cleaned up temp file: ${file}`);
}
}
} catch (error) {
logger.error('Temp file cleanup error:', error);
}
}

// 获取文件统计信息
public static async getUploadStats(userId?: string): Promise<{
totalFiles: number;
totalSize: number;
fileTypes: Record<string, number>;
}> {
// 这里应该从数据库获取统计信息
// 实际实现需要根据使用的数据库进行调整
return {
totalFiles: 0,
totalSize: 0,
fileTypes: {}
};
}
}

2. 文件上传控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// src/controllers/FileController.ts - 文件上传控制器
import { Request, Response } from 'express';
import { FileUploadService } from '@/services/FileUploadService';
import { AppError } from '@/utils/AppError';
import { logger } from '@/utils/logger';

export class FileController {

// 单文件上传
public static async uploadSingle(req: Request, res: Response): Promise<void> {
try {
if (!req.file) {
throw new AppError('No file provided', 400);
}

const uploadedFile = await FileUploadService.uploadSingle(req.file, req.user?.id);

res.status(201).json({
success: true,
message: 'File uploaded successfully',
data: uploadedFile
});
} catch (error) {
logger.error('Single file upload error:', error);
throw error;
}
}

// 多文件上传
public static async uploadMultiple(req: Request, res: Response): Promise<void> {
try {
if (!req.files || !Array.isArray(req.files) || req.files.length === 0) {
throw new AppError('No files provided', 400);
}

const uploadedFiles = await FileUploadService.uploadMultiple(req.files, req.user?.id);

res.status(201).json({
success: true,
message: `${uploadedFiles.length} files uploaded successfully`,
data: uploadedFiles
});
} catch (error) {
logger.error('Multiple file upload error:', error);
throw error;
}
}

// 头像上传
public static async uploadAvatar(req: Request, res: Response): Promise<void> {
try {
if (!req.file) {
throw new AppError('No avatar file provided', 400);
}

// 验证文件类型
if (!req.file.mimetype.startsWith('image/')) {
throw new AppError('Avatar must be an image file', 400);
}

const uploadedFile = await FileUploadService.uploadSingle(req.file, req.user?.id);

// 生成缩略图
const thumbnailPath = await FileUploadService.generateThumbnail(uploadedFile.path, 200);

res.status(201).json({
success: true,
message: 'Avatar uploaded successfully',
data: {
...uploadedFile,
thumbnail: FileUploadService.getFileUrl(thumbnailPath)
}
});
} catch (error) {
logger.error('Avatar upload error:', error);
throw error;
}
}

// 获取文件信息
public static async getFileInfo(req: Request, res: Response): Promise<void> {
try {
const { fileId } = req.params;
const fileInfo = await FileUploadService.getFileInfo(fileId, req.user?.id);

if (!fileInfo) {
throw new AppError('File not found', 404);
}

res.json({
success: true,
data: fileInfo
});
} catch (error) {
logger.error('Get file info error:', error);
throw error;
}
}

// 删除文件
public static async deleteFile(req: Request, res: Response): Promise<void> {
try {
const { fileId } = req.params;
const success = await FileUploadService.deleteUserFile(fileId, req.user?.id);

if (!success) {
throw new AppError('Failed to delete file', 500);
}

res.json({
success: true,
message: 'File deleted successfully'
});
} catch (error) {
logger.error('Delete file error:', error);
throw error;
}
}

// 获取上传统计
public static async getUploadStats(req: Request, res: Response): Promise<void> {
try {
const stats = await FileUploadService.getUploadStats(req.user?.id);

res.json({
success: true,
data: stats
});
} catch (error) {
logger.error('Get upload stats error:', error);
throw error;
}
}
}

2. GraphQL类型定义和解析器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// src/graphql/types/User.ts - 用户类型定义
import { ObjectType, Field, ID, Int } from 'type-graphql';
import { IsEmail, Length } from 'class-validator';

@ObjectType()
export class User {
@Field(() => ID)
id: string;

@Field()
@Length(3, 30)
username: string;

@Field()
@IsEmail()
email: string;

@Field()
firstName: string;

@Field()
lastName: string;

@Field({ nullable: true })
avatar?: string;

@Field()
role: string;

@Field()
isActive: boolean;

@Field()
isEmailVerified: boolean;

@Field({ nullable: true })
lastLoginAt?: Date;

@Field()
createdAt: Date;

@Field()
updatedAt: Date;

@Field()
fullName: string;
}

@ObjectType()
export class PaginatedUsers {
@Field(() => [User])
users: User[];

@Field(() => Int)
total: number;

@Field(() => Int)
page: number;

@Field(() => Int)
pages: number;

@Field(() => Boolean)
hasNext: boolean;

@Field(() => Boolean)
hasPrev: boolean;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// src/graphql/inputs/UserInput.ts - 用户输入类型
import { InputType, Field } from 'type-graphql';
import { IsEmail, Length, IsOptional } from 'class-validator';

@InputType()
export class CreateUserInput {
@Field()
@Length(3, 30)
username: string;

@Field()
@IsEmail()
email: string;

@Field()
@Length(8, 100)
password: string;

@Field()
@Length(2, 50)
firstName: string;

@Field()
@Length(2, 50)
lastName: string;

@Field({ nullable: true })
@IsOptional()
avatar?: string;
}

@InputType()
export class UpdateUserInput {
@Field({ nullable: true })
@IsOptional()
@Length(3, 30)
username?: string;

@Field({ nullable: true })
@IsOptional()
@IsEmail()
email?: string;

@Field({ nullable: true })
@IsOptional()
@Length(2, 50)
firstName?: string;

@Field({ nullable: true })
@IsOptional()
@Length(2, 50)
lastName?: string;

@Field({ nullable: true })
@IsOptional()
avatar?: string;
}

@InputType()
export class UsersFilterInput {
@Field({ nullable: true })
@IsOptional()
search?: string;

@Field({ nullable: true })
@IsOptional()
role?: string;

@Field({ nullable: true })
@IsOptional()
isActive?: boolean;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// src/graphql/resolvers/UserResolver.ts - 用户解析器
import { Resolver, Query, Mutation, Arg, Authorized, Ctx, Int, FieldResolver, Root } from 'type-graphql';
import { Service } from 'typedi';

import { User, PaginatedUsers } from '@/graphql/types/User';
import { CreateUserInput, UpdateUserInput, UsersFilterInput } from '@/graphql/inputs/UserInput';
import { UserService } from '@/services/UserService';
import { Context } from '@/graphql/context';
import { AppError } from '@/utils/AppError';

@Service()
@Resolver(() => User)
export class UserResolver {

constructor(private userService: UserService) {}

@Query(() => PaginatedUsers)
@Authorized(['admin', 'moderator'])
async users(
@Arg('page', () => Int, { defaultValue: 1 }) page: number,
@Arg('limit', () => Int, { defaultValue: 10 }) limit: number,
@Arg('filters', { nullable: true }) filters?: UsersFilterInput
): Promise<PaginatedUsers> {
const result = await this.userService.getUsers({
page,
limit,
search: filters?.search,
role: filters?.role
});

return {
users: result.users,
total: result.pagination.total,
page: result.pagination.page,
pages: result.pagination.pages,
hasNext: result.pagination.page < result.pagination.pages,
hasPrev: result.pagination.page > 1
};
}

@Query(() => User, { nullable: true })
async user(@Arg('id') id: string): Promise<User | null> {
return await this.userService.getUserById(id);
}

@Query(() => User)
@Authorized()
async me(@Ctx() ctx: Context): Promise<User> {
if (!ctx.user) {
throw new AppError('User not authenticated', 401);
}
return ctx.user;
}

@Mutation(() => User)
@Authorized(['admin'])
async createUser(@Arg('input') input: CreateUserInput): Promise<User> {
return await this.userService.createUser(input);
}

@Mutation(() => User)
@Authorized()
async updateUser(
@Arg('id') id: string,
@Arg('input') input: UpdateUserInput,
@Ctx() ctx: Context
): Promise<User> {
// 用户只能更新自己的信息,除非是管理员
if (ctx.user?.id !== id && !['admin', 'moderator'].includes(ctx.user?.role)) {
throw new AppError('Insufficient permissions', 403);
}

const user = await this.userService.updateUser(id, input);
if (!user) {
throw new AppError('User not found', 404);
}

return user;
}

@Mutation(() => Boolean)
@Authorized(['admin'])
async deleteUser(@Arg('id') id: string): Promise<boolean> {
return await this.userService.deleteUser(id);
}

@Mutation(() => String)
@Authorized()
async uploadAvatar(
@Arg('userId') userId: string,
@Arg('file') file: any, // 实际项目中需要处理文件上传
@Ctx() ctx: Context
): Promise<string> {
// 用户只能上传自己的头像,除非是管理员
if (ctx.user?.id !== userId && ctx.user?.role !== 'admin') {
throw new AppError('Insufficient permissions', 403);
}

return await this.userService.uploadAvatar(userId, file);
}

// 字段解析器
@FieldResolver(() => String)
fullName(@Root() user: User): string {
return `${user.firstName} ${user.lastName}`;
}

@FieldResolver(() => Int)
async postCount(@Root() user: User): Promise<number> {
// 假设有PostService来获取用户的文章数量
// return await this.postService.getPostCountByUser(user.id);
return 0; // 占位符
}
}

📊 缓存策略与Redis集成

1. Redis缓存服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
// src/services/CacheService.ts - 缓存服务
import Redis from 'ioredis';
import { config } from '@/config';
import { logger } from '@/utils/logger';

export class CacheService {
private static redis: Redis;
private static cluster: Redis.Cluster;

public static async initialize(): Promise<void> {
try {
if (config.REDIS_CLUSTER_ENABLED) {
// 集群模式
this.cluster = new Redis.Cluster(config.REDIS_CLUSTER_NODES, {
redisOptions: {
password: config.REDIS_PASSWORD,
db: config.REDIS_DB
},
enableOfflineQueue: false,
retryDelayOnFailover: 100,
maxRetriesPerRequest: 3
});

this.cluster.on('connect', () => {
logger.info('Redis cluster connected');
});

this.cluster.on('error', (error) => {
logger.error('Redis cluster error:', error);
});
} else {
// 单实例模式
this.redis = new Redis({
host: config.REDIS_HOST,
port: config.REDIS_PORT,
password: config.REDIS_PASSWORD,
db: config.REDIS_DB,
retryDelayOnFailover: 100,
maxRetriesPerRequest: 3,
lazyConnect: true
});

this.redis.on('connect', () => {
logger.info('Redis connected');
});

this.redis.on('error', (error) => {
logger.error('Redis error:', error);
});

await this.redis.connect();
}
} catch (error) {
logger.error('Failed to initialize Redis:', error);
throw error;
}
}

private static getClient(): Redis | Redis.Cluster {
return this.cluster || this.redis;
}

// 基础操作
public static async get<T = any>(key: string): Promise<T | null> {
try {
const value = await this.getClient().get(key);
return value ? JSON.parse(value) : null;
} catch (error) {
logger.error(`Cache get error for key ${key}:`, error);
return null;
}
}

public static async set(key: string, value: any, ttl?: number): Promise<boolean> {
try {
const serialized = JSON.stringify(value);

if (ttl) {
await this.getClient().setex(key, ttl, serialized);
} else {
await this.getClient().set(key, serialized);
}

return true;
} catch (error) {
logger.error(`Cache set error for key ${key}:`, error);
return false;
}
}

public static async delete(key: string): Promise<boolean> {
try {
const result = await this.getClient().del(key);
return result > 0;
} catch (error) {
logger.error(`Cache delete error for key ${key}:`, error);
return false;
}
}

public static async exists(key: string): Promise<boolean> {
try {
const result = await this.getClient().exists(key);
return result === 1;
} catch (error) {
logger.error(`Cache exists error for key ${key}:`, error);
return false;
}
}

// 批量操作
public static async mget<T = any>(keys: string[]): Promise<(T | null)[]> {
try {
const values = await this.getClient().mget(...keys);
return values.map(value => value ? JSON.parse(value) : null);
} catch (error) {
logger.error('Cache mget error:', error);
return keys.map(() => null);
}
}

public static async mset(keyValuePairs: Record<string, any>, ttl?: number): Promise<boolean> {
try {
const pipeline = this.getClient().pipeline();

Object.entries(keyValuePairs).forEach(([key, value]) => {
const serialized = JSON.stringify(value);
if (ttl) {
pipeline.setex(key, ttl, serialized);
} else {
pipeline.set(key, serialized);
}
});

await pipeline.exec();
return true;
} catch (error) {
logger.error('Cache mset error:', error);
return false;
}
}

// 模式匹配删除
public static async deletePattern(pattern: string): Promise<number> {
try {
const keys = await this.getClient().keys(pattern);
if (keys.length === 0) return 0;

const result = await this.getClient().del(...keys);
return result;
} catch (error) {
logger.error(`Cache deletePattern error for pattern ${pattern}:`, error);
return 0;
}
}

// 哈希操作
public static async hget<T = any>(key: string, field: string): Promise<T | null> {
try {
const value = await this.getClient().hget(key, field);
return value ? JSON.parse(value) : null;
} catch (error) {
logger.error(`Cache hget error for key ${key}, field ${field}:`, error);
return null;
}
}

public static async hset(key: string, field: string, value: any): Promise<boolean> {
try {
const serialized = JSON.stringify(value);
await this.getClient().hset(key, field, serialized);
return true;
} catch (error) {
logger.error(`Cache hset error for key ${key}, field ${field}:`, error);
return false;
}
}

public static async hgetall<T = any>(key: string): Promise<Record<string, T>> {
try {
const hash = await this.getClient().hgetall(key);
const result: Record<string, T> = {};

Object.entries(hash).forEach(([field, value]) => {
result[field] = JSON.parse(value);
});

return result;
} catch (error) {
logger.error(`Cache hgetall error for key ${key}:`, error);
return {};
}
}

// 列表操作
public static async lpush(key: string, ...values: any[]): Promise<number> {
try {
const serialized = values.map(v => JSON.stringify(v));
return await this.getClient().lpush(key, ...serialized);
} catch (error) {
logger.error(`Cache lpush error for key ${key}:`, error);
return 0;
}
}

public static async rpop<T = any>(key: string): Promise<T | null> {
try {
const value = await this.getClient().rpop(key);
return value ? JSON.parse(value) : null;
} catch (error) {
logger.error(`Cache rpop error for key ${key}:`, error);
return null;
}
}

public static async lrange<T = any>(key: string, start: number, stop: number): Promise<T[]> {
try {
const values = await this.getClient().lrange(key, start, stop);
return values.map(value => JSON.parse(value));
} catch (error) {
logger.error(`Cache lrange error for key ${key}:`, error);
return [];
}
}

// 集合操作
public static async sadd(key: string, ...members: any[]): Promise<number> {
try {
const serialized = members.map(m => JSON.stringify(m));
return await this.getClient().sadd(key, ...serialized);
} catch (error) {
logger.error(`Cache sadd error for key ${key}:`, error);
return 0;
}
}

public static async smembers<T = any>(key: string): Promise<T[]> {
try {
const members = await this.getClient().smembers(key);
return members.map(member => JSON.parse(member));
} catch (error) {
logger.error(`Cache smembers error for key ${key}:`, error);
return [];
}
}

// 有序集合操作
public static async zadd(key: string, score: number, member: any): Promise<number> {
try {
const serialized = JSON.stringify(member);
return await this.getClient().zadd(key, score, serialized);
} catch (error) {
logger.error(`Cache zadd error for key ${key}:`, error);
return 0;
}
}

public static async zrange<T = any>(key: string, start: number, stop: number): Promise<T[]> {
try {
const members = await this.getClient().zrange(key, start, stop);
return members.map(member => JSON.parse(member));
} catch (error) {
logger.error(`Cache zrange error for key ${key}:`, error);
return [];
}
}

// 过期时间操作
public static async expire(key: string, seconds: number): Promise<boolean> {
try {
const result = await this.getClient().expire(key, seconds);
return result === 1;
} catch (error) {
logger.error(`Cache expire error for key ${key}:`, error);
return false;
}
}

public static async ttl(key: string): Promise<number> {
try {
return await this.getClient().ttl(key);
} catch (error) {
logger.error(`Cache ttl error for key ${key}:`, error);
return -1;
}
}

// 缓存装饰器
public static cache(ttl: number = 300) {
return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
const method = descriptor.value;

descriptor.value = async function (...args: any[]) {
const cacheKey = `${target.constructor.name}:${propertyName}:${JSON.stringify(args)}`;

// 尝试从缓存获取
const cached = await CacheService.get(cacheKey);
if (cached !== null) {
return cached;
}

// 执行原方法
const result = await method.apply(this, args);

// 缓存结果
await CacheService.set(cacheKey, result, ttl);

return result;
};
};
}

// 缓存统计
public static async getStats(): Promise<{
memory: any;
keyspace: any;
clients: any;
}> {
try {
const info = await this.getClient().info();
const lines = info.split('\r\n');

const stats = {
memory: {},
keyspace: {},
clients: {}
};

lines.forEach(line => {
if (line.includes(':')) {
const [key, value] = line.split(':');
if (key.startsWith('used_memory')) {
stats.memory[key] = value;
} else if (key.startsWith('connected_clients')) {
stats.clients[key] = value;
} else if (key.startsWith('db')) {
stats.keyspace[key] = value;
}
}
});

return stats;
} catch (error) {
logger.error('Cache stats error:', error);
return { memory: {}, keyspace: {}, clients: {} };
}
}

// 清空缓存
public static async flushAll(): Promise<boolean> {
try {
await this.getClient().flushall();
return true;
} catch (error) {
logger.error('Cache flushAll error:', error);
return false;
}
}

// 关闭连接
public static async disconnect(): Promise<void> {
try {
if (this.cluster) {
await this.cluster.disconnect();
}
if (this.redis) {
await this.redis.disconnect();
}
logger.info('Redis disconnected');
} catch (error) {
logger.error('Redis disconnect error:', error);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"baseUrl": "./src",
"paths": {
"@/*": ["*"],
"@/controllers/*": ["controllers/*"],
"@/services/*": ["services/*"],
"@/models/*": ["models/*"],
"@/utils/*": ["utils/*"],
"@/config/*": ["config/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests"]
}

🌐 Express.js 高级应用

1. 现代Express应用架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
// src/app.ts - Express应用主文件
import express, { Application, Request, Response, NextFunction } from 'express';
import helmet from 'helmet';
import cors from 'cors';
import compression from 'compression';
import rateLimit from 'express-rate-limit';
import { createServer } from 'http';
import { Server as SocketIOServer } from 'socket.io';

import { config } from '@/config';
import { logger } from '@/utils/logger';
import { errorHandler } from '@/middleware/errorHandler';
import { authMiddleware } from '@/middleware/auth';
import { validationMiddleware } from '@/middleware/validation';
import routes from '@/routes';

class App {
public app: Application;
public server: any;
public io: SocketIOServer;

constructor() {
this.app = express();
this.server = createServer(this.app);
this.io = new SocketIOServer(this.server, {
cors: {
origin: config.CLIENT_URL,
methods: ['GET', 'POST']
}
});

this.initializeMiddlewares();
this.initializeRoutes();
this.initializeErrorHandling();
this.initializeSocketIO();
}

private initializeMiddlewares(): void {
// 安全中间件
this.app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
}
}
}));

// CORS配置
this.app.use(cors({
origin: config.ALLOWED_ORIGINS,
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization']
}));

// 压缩响应
this.app.use(compression());

// 请求解析
this.app.use(express.json({ limit: '10mb' }));
this.app.use(express.urlencoded({ extended: true, limit: '10mb' }));

// 速率限制
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 限制每个IP 100个请求
message: {
error: 'Too many requests from this IP, please try again later.'
},
standardHeaders: true,
legacyHeaders: false,
});
this.app.use('/api/', limiter);

// 请求日志
this.app.use((req: Request, res: Response, next: NextFunction) => {
logger.info(`${req.method} ${req.path}`, {
ip: req.ip,
userAgent: req.get('User-Agent'),
timestamp: new Date().toISOString()
});
next();
});
}

private initializeRoutes(): void {
// 健康检查
this.app.get('/health', (req: Request, res: Response) => {
res.status(200).json({
status: 'OK',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage(),
version: process.version
});
});

// API路由
this.app.use('/api', routes);

// 404处理
this.app.use('*', (req: Request, res: Response) => {
res.status(404).json({
error: 'Route not found',
path: req.originalUrl
});
});
}

private initializeErrorHandling(): void {
this.app.use(errorHandler);
}

private initializeSocketIO(): void {
this.io.use((socket, next) => {
// Socket.IO认证中间件
const token = socket.handshake.auth.token;
if (token) {
// 验证JWT token
// authMiddleware.verifySocketToken(token, next);
}
next();
});

this.io.on('connection', (socket) => {
logger.info(`Socket connected: ${socket.id}`);

socket.on('join-room', (roomId: string) => {
socket.join(roomId);
logger.info(`Socket ${socket.id} joined room ${roomId}`);
});

socket.on('disconnect', () => {
logger.info(`Socket disconnected: ${socket.id}`);
});
});
}

public listen(): void {
this.server.listen(config.PORT, () => {
logger.info(`Server running on port ${config.PORT}`);
logger.info(`Environment: ${config.NODE_ENV}`);
});
}
}

export default App;

2. 高级中间件实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// src/middleware/auth.ts - 认证中间件
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { config } from '@/config';
import { UserService } from '@/services/UserService';
import { AppError } from '@/utils/AppError';

interface AuthRequest extends Request {
user?: any;
}

export class AuthMiddleware {

// JWT认证中间件
public static async authenticate(req: AuthRequest, res: Response, next: NextFunction): Promise<void> {
try {
const token = req.header('Authorization')?.replace('Bearer ', '');

if (!token) {
throw new AppError('Access token is required', 401);
}

const decoded = jwt.verify(token, config.JWT_SECRET) as any;
const user = await UserService.findById(decoded.userId);

if (!user) {
throw new AppError('Invalid token', 401);
}

if (!user.isActive) {
throw new AppError('Account is deactivated', 401);
}

req.user = user;
next();
} catch (error) {
if (error instanceof jwt.JsonWebTokenError) {
next(new AppError('Invalid token', 401));
} else {
next(error);
}
}
}

// 权限检查中间件
public static authorize(roles: string[]) {
return (req: AuthRequest, res: Response, next: NextFunction): void => {
if (!req.user) {
return next(new AppError('Authentication required', 401));
}

if (!roles.includes(req.user.role)) {
return next(new AppError('Insufficient permissions', 403));
}

next();
};
}

// API密钥认证
public static apiKeyAuth(req: Request, res: Response, next: NextFunction): void {
const apiKey = req.header('X-API-Key');

if (!apiKey || apiKey !== config.API_KEY) {
return next(new AppError('Invalid API key', 401));
}

next();
}

// 刷新令牌验证
public static async refreshToken(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const refreshToken = req.body.refreshToken;

if (!refreshToken) {
throw new AppError('Refresh token is required', 400);
}

const decoded = jwt.verify(refreshToken, config.JWT_REFRESH_SECRET) as any;
const user = await UserService.findById(decoded.userId);

if (!user || user.refreshToken !== refreshToken) {
throw new AppError('Invalid refresh token', 401);
}

req.user = user;
next();
} catch (error) {
next(error);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
// src/middleware/validation.ts - 数据验证中间件
import { Request, Response, NextFunction } from 'express';
import Joi from 'joi';
import { AppError } from '@/utils/AppError';

export class ValidationMiddleware {

// 通用验证中间件
public static validate(schema: Joi.ObjectSchema) {
return (req: Request, res: Response, next: NextFunction): void => {
const { error, value } = schema.validate(req.body, {
abortEarly: false,
stripUnknown: true
});

if (error) {
const errorMessage = error.details
.map(detail => detail.message)
.join(', ');

return next(new AppError(errorMessage, 400));
}

req.body = value;
next();
};
}

// 查询参数验证
public static validateQuery(schema: Joi.ObjectSchema) {
return (req: Request, res: Response, next: NextFunction): void => {
const { error, value } = schema.validate(req.query, {
abortEarly: false,
stripUnknown: true
});

if (error) {
const errorMessage = error.details
.map(detail => detail.message)
.join(', ');

return next(new AppError(errorMessage, 400));
}

req.query = value;
next();
};
}

// 路径参数验证
public static validateParams(schema: Joi.ObjectSchema) {
return (req: Request, res: Response, next: NextFunction): void => {
const { error, value } = schema.validate(req.params, {
abortEarly: false
});

if (error) {
const errorMessage = error.details
.map(detail => detail.message)
.join(', ');

return next(new AppError(errorMessage, 400));
}

req.params = value;
next();
};
}
}

// 常用验证模式
export const ValidationSchemas = {
// 用户注册
userRegistration: Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
email: Joi.string().email().required(),
password: Joi.string().min(8).pattern(new RegExp('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])')).required(),
firstName: Joi.string().min(2).max(50).required(),
lastName: Joi.string().min(2).max(50).required(),
phone: Joi.string().pattern(/^[0-9]{10,15}$/).optional(),
dateOfBirth: Joi.date().max('now').optional()
}),

// 用户登录
userLogin: Joi.object({
email: Joi.string().email().required(),
password: Joi.string().required()
}),

// 分页查询
pagination: Joi.object({
page: Joi.number().integer().min(1).default(1),
limit: Joi.number().integer().min(1).max(100).default(10),
sort: Joi.string().valid('asc', 'desc').default('desc'),
sortBy: Joi.string().default('createdAt')
}),

// MongoDB ObjectId
mongoId: Joi.object({
id: Joi.string().pattern(/^[0-9a-fA-F]{24}$/).required()
}),

// 文件上传
fileUpload: Joi.object({
filename: Joi.string().required(),
mimetype: Joi.string().valid('image/jpeg', 'image/png', 'image/gif', 'application/pdf').required(),
size: Joi.number().max(10 * 1024 * 1024) // 10MB
})
};

🗄️ 数据库集成与ORM

1. MongoDB与Mongoose

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// src/config/database.ts - 数据库配置
import mongoose from 'mongoose';
import { config } from '@/config';
import { logger } from '@/utils/logger';

export class DatabaseConnection {

public static async connectMongoDB(): Promise<void> {
try {
const options = {
useNewUrlParser: true,
useUnifiedTopology: true,
maxPoolSize: 10,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
bufferCommands: false,
bufferMaxEntries: 0
};

await mongoose.connect(config.MONGODB_URI, options);

logger.info('MongoDB connected successfully');

// 监听连接事件
mongoose.connection.on('error', (error) => {
logger.error('MongoDB connection error:', error);
});

mongoose.connection.on('disconnected', () => {
logger.warn('MongoDB disconnected');
});

mongoose.connection.on('reconnected', () => {
logger.info('MongoDB reconnected');
});

} catch (error) {
logger.error('MongoDB connection failed:', error);
process.exit(1);
}
}

public static async disconnectMongoDB(): Promise<void> {
try {
await mongoose.disconnect();
logger.info('MongoDB disconnected');
} catch (error) {
logger.error('Error disconnecting from MongoDB:', error);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
// src/models/User.ts - 用户模型
import mongoose, { Document, Schema } from 'mongoose';
import bcrypt from 'bcryptjs';

export interface IUser extends Document {
username: string;
email: string;
password: string;
firstName: string;
lastName: string;
avatar?: string;
role: 'user' | 'admin' | 'moderator';
isActive: boolean;
isEmailVerified: boolean;
refreshToken?: string;
lastLoginAt?: Date;
createdAt: Date;
updatedAt: Date;

// 方法
comparePassword(candidatePassword: string): Promise<boolean>;
generateAuthTokens(): { accessToken: string; refreshToken: string };
toJSON(): any;
}

const userSchema = new Schema<IUser>({
username: {
type: String,
required: true,
unique: true,
trim: true,
minlength: 3,
maxlength: 30,
index: true
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
trim: true,
index: true
},
password: {
type: String,
required: true,
minlength: 8,
select: false // 默认不返回密码字段
},
firstName: {
type: String,
required: true,
trim: true,
maxlength: 50
},
lastName: {
type: String,
required: true,
trim: true,
maxlength: 50
},
avatar: {
type: String,
default: null
},
role: {
type: String,
enum: ['user', 'admin', 'moderator'],
default: 'user',
index: true
},
isActive: {
type: Boolean,
default: true,
index: true
},
isEmailVerified: {
type: Boolean,
default: false
},
refreshToken: {
type: String,
select: false
},
lastLoginAt: {
type: Date,
default: null
}
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
});

// 虚拟字段
userSchema.virtual('fullName').get(function() {
return `${this.firstName} ${this.lastName}`;
});

// 索引
userSchema.index({ email: 1, isActive: 1 });
userSchema.index({ username: 1, isActive: 1 });
userSchema.index({ createdAt: -1 });

// 中间件 - 密码加密
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();

try {
const salt = await bcrypt.genSalt(12);
this.password = await bcrypt.hash(this.password, salt);
next();
} catch (error) {
next(error);
}
});

// 实例方法 - 密码比较
userSchema.methods.comparePassword = async function(candidatePassword: string): Promise<boolean> {
return bcrypt.compare(candidatePassword, this.password);
};

// 实例方法 - 生成认证令牌
userSchema.methods.generateAuthTokens = function(): { accessToken: string; refreshToken: string } {
const jwt = require('jsonwebtoken');
const { config } = require('@/config');

const payload = {
userId: this._id,
email: this.email,
role: this.role
};

const accessToken = jwt.sign(payload, config.JWT_SECRET, {
expiresIn: config.JWT_EXPIRES_IN
});

const refreshToken = jwt.sign(
{ userId: this._id },
config.JWT_REFRESH_SECRET,
{ expiresIn: config.JWT_REFRESH_EXPIRES_IN }
);

return { accessToken, refreshToken };
};

// 实例方法 - 自定义JSON序列化
userSchema.methods.toJSON = function() {
const userObject = this.toObject();
delete userObject.password;
delete userObject.refreshToken;
delete userObject.__v;
return userObject;
};

// 静态方法 - 查找活跃用户
userSchema.statics.findActiveUsers = function(filter = {}) {
return this.find({ ...filter, isActive: true });
};

export const User = mongoose.model<IUser>('User', userSchema);

2. PostgreSQL与Knex.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// src/config/knex.ts - Knex配置
import knex from 'knex';
import { config } from '@/config';

const knexConfig = {
client: 'postgresql',
connection: {
host: config.DB_HOST,
port: config.DB_PORT,
user: config.DB_USER,
password: config.DB_PASSWORD,
database: config.DB_NAME,
ssl: config.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
},
pool: {
min: 2,
max: 10,
acquireTimeoutMillis: 60000,
createTimeoutMillis: 30000,
destroyTimeoutMillis: 5000,
idleTimeoutMillis: 30000,
reapIntervalMillis: 1000,
createRetryIntervalMillis: 200
},
migrations: {
directory: './src/database/migrations',
extension: 'ts'
},
seeds: {
directory: './src/database/seeds',
extension: 'ts'
},
debug: config.NODE_ENV === 'development'
};

export const db = knex(knexConfig);

// 数据库连接测试
export const testConnection = async (): Promise<void> => {
try {
await db.raw('SELECT 1');
console.log('PostgreSQL connected successfully');
} catch (error) {
console.error('PostgreSQL connection failed:', error);
process.exit(1);
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// src/models/BaseModel.ts - 基础模型类
import { db } from '@/config/knex';
import { QueryBuilder } from 'knex';

export abstract class BaseModel {
protected static tableName: string;
protected static db = db;

// 查找所有记录
public static findAll<T>(this: new() => T, filters: any = {}): QueryBuilder {
return this.db(this.tableName).where(filters);
}

// 根据ID查找
public static findById<T>(this: new() => T, id: string | number): QueryBuilder {
return this.db(this.tableName).where('id', id).first();
}

// 创建记录
public static create<T>(this: new() => T, data: Partial<T>): QueryBuilder {
return this.db(this.tableName).insert(data).returning('*');
}

// 更新记录
public static update<T>(this: new() => T, id: string | number, data: Partial<T>): QueryBuilder {
return this.db(this.tableName)
.where('id', id)
.update({ ...data, updated_at: new Date() })
.returning('*');
}

// 删除记录
public static delete<T>(this: new() => T, id: string | number): QueryBuilder {
return this.db(this.tableName).where('id', id).del();
}

// 软删除
public static softDelete<T>(this: new() => T, id: string | number): QueryBuilder {
return this.db(this.tableName)
.where('id', id)
.update({
deleted_at: new Date(),
updated_at: new Date()
});
}

// 分页查询
public static paginate<T>(
this: new() => T,
page: number = 1,
limit: number = 10,
filters: any = {}
): QueryBuilder {
const offset = (page - 1) * limit;

return this.db(this.tableName)
.where(filters)
.whereNull('deleted_at')
.limit(limit)
.offset(offset)
.orderBy('created_at', 'desc');
}

// 计数
public static count<T>(this: new() => T, filters: any = {}): QueryBuilder {
return this.db(this.tableName)
.where(filters)
.whereNull('deleted_at')
.count('* as total')
.first();
}
}

🔄 RESTful API设计

1. 控制器层实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
// src/controllers/UserController.ts - 用户控制器
import { Request, Response, NextFunction } from 'express';
import { UserService } from '@/services/UserService';
import { AppError } from '@/utils/AppError';
import { ApiResponse } from '@/utils/ApiResponse';
import { CacheService } from '@/services/CacheService';

export class UserController {

// 获取用户列表
public static async getUsers(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const { page = 1, limit = 10, search, role } = req.query;

const cacheKey = `users:${page}:${limit}:${search || ''}:${role || ''}`;

// 尝试从缓存获取
let result = await CacheService.get(cacheKey);

if (!result) {
result = await UserService.getUsers({
page: Number(page),
limit: Number(limit),
search: search as string,
role: role as string
});

// 缓存结果5分钟
await CacheService.set(cacheKey, result, 300);
}

ApiResponse.success(res, result, 'Users retrieved successfully');
} catch (error) {
next(error);
}
}

// 获取单个用户
public static async getUser(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const { id } = req.params;

const cacheKey = `user:${id}`;
let user = await CacheService.get(cacheKey);

if (!user) {
user = await UserService.getUserById(id);
if (!user) {
throw new AppError('User not found', 404);
}

// 缓存用户信息10分钟
await CacheService.set(cacheKey, user, 600);
}

ApiResponse.success(res, user, 'User retrieved successfully');
} catch (error) {
next(error);
}
}

// 创建用户
public static async createUser(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const userData = req.body;

// 检查邮箱是否已存在
const existingUser = await UserService.getUserByEmail(userData.email);
if (existingUser) {
throw new AppError('Email already exists', 409);
}

const user = await UserService.createUser(userData);

// 清除相关缓存
await CacheService.deletePattern('users:*');

ApiResponse.created(res, user, 'User created successfully');
} catch (error) {
next(error);
}
}

// 更新用户
public static async updateUser(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const { id } = req.params;
const updateData = req.body;

const user = await UserService.updateUser(id, updateData);
if (!user) {
throw new AppError('User not found', 404);
}

// 清除相关缓存
await CacheService.delete(`user:${id}`);
await CacheService.deletePattern('users:*');

ApiResponse.success(res, user, 'User updated successfully');
} catch (error) {
next(error);
}
}

// 删除用户
public static async deleteUser(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const { id } = req.params;

const deleted = await UserService.deleteUser(id);
if (!deleted) {
throw new AppError('User not found', 404);
}

// 清除相关缓存
await CacheService.delete(`user:${id}`);
await CacheService.deletePattern('users:*');

ApiResponse.success(res, null, 'User deleted successfully');
} catch (error) {
next(error);
}
}

// 上传头像
public static async uploadAvatar(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const { id } = req.params;
const file = req.file;

if (!file) {
throw new AppError('No file uploaded', 400);
}

const avatarUrl = await UserService.uploadAvatar(id, file);

// 清除用户缓存
await CacheService.delete(`user:${id}`);

ApiResponse.success(res, { avatarUrl }, 'Avatar uploaded successfully');
} catch (error) {
next(error);
}
}

// 获取用户统计信息
public static async getUserStats(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const cacheKey = 'user:stats';
let stats = await CacheService.get(cacheKey);

if (!stats) {
stats = await UserService.getUserStats();
// 缓存统计信息1小时
await CacheService.set(cacheKey, stats, 3600);
}

ApiResponse.success(res, stats, 'User statistics retrieved successfully');
} catch (error) {
next(error);
}
}
}

2. 服务层实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
// src/services/UserService.ts - 用户服务
import { User, IUser } from '@/models/User';
import { AppError } from '@/utils/AppError';
import { FileUploadService } from '@/services/FileUploadService';
import { EmailService } from '@/services/EmailService';
import { logger } from '@/utils/logger';

interface GetUsersOptions {
page: number;
limit: number;
search?: string;
role?: string;
}

interface UserStats {
totalUsers: number;
activeUsers: number;
newUsersThisMonth: number;
usersByRole: Record<string, number>;
}

export class UserService {

// 获取用户列表
public static async getUsers(options: GetUsersOptions): Promise<{
users: IUser[];
pagination: {
page: number;
limit: number;
total: number;
pages: number;
};
}> {
const { page, limit, search, role } = options;
const skip = (page - 1) * limit;

// 构建查询条件
const query: any = { isActive: true };

if (role) {
query.role = role;
}

if (search) {
query.$or = [
{ username: { $regex: search, $options: 'i' } },
{ email: { $regex: search, $options: 'i' } },
{ firstName: { $regex: search, $options: 'i' } },
{ lastName: { $regex: search, $options: 'i' } }
];
}

// 执行查询
const [users, total] = await Promise.all([
User.find(query)
.select('-password -refreshToken')
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit)
.lean(),
User.countDocuments(query)
]);

return {
users,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
};
}

// 根据ID获取用户
public static async getUserById(id: string): Promise<IUser | null> {
const user = await User.findById(id)
.select('-password -refreshToken')
.lean();

return user;
}

// 根据邮箱获取用户
public static async getUserByEmail(email: string): Promise<IUser | null> {
const user = await User.findOne({ email, isActive: true })
.select('-password -refreshToken')
.lean();

return user;
}

// 创建用户
public static async createUser(userData: Partial<IUser>): Promise<IUser> {
try {
const user = new User(userData);
await user.save();

// 发送欢迎邮件
await EmailService.sendWelcomeEmail(user.email, user.firstName);

logger.info(`User created: ${user.email}`);

return user.toJSON();
} catch (error) {
if (error.code === 11000) {
const field = Object.keys(error.keyPattern)[0];
throw new AppError(`${field} already exists`, 409);
}
throw error;
}
}

// 更新用户
public static async updateUser(id: string, updateData: Partial<IUser>): Promise<IUser | null> {
// 移除不允许更新的字段
const { password, refreshToken, ...allowedUpdates } = updateData;

const user = await User.findByIdAndUpdate(
id,
{ $set: allowedUpdates },
{ new: true, runValidators: true }
).select('-password -refreshToken');

if (user) {
logger.info(`User updated: ${user.email}`);
}

return user;
}

// 删除用户(软删除)
public static async deleteUser(id: string): Promise<boolean> {
const user = await User.findByIdAndUpdate(
id,
{ $set: { isActive: false } },
{ new: true }
);

if (user) {
logger.info(`User deleted: ${user.email}`);
return true;
}

return false;
}

// 上传头像
public static async uploadAvatar(userId: string, file: Express.Multer.File): Promise<string> {
const user = await User.findById(userId);
if (!user) {
throw new AppError('User not found', 404);
}

// 上传文件到云存储
const avatarUrl = await FileUploadService.uploadImage(file, 'avatars');

// 更新用户头像
await User.findByIdAndUpdate(userId, { avatar: avatarUrl });

logger.info(`Avatar uploaded for user: ${user.email}`);

return avatarUrl;
}

// 更改密码
public static async changePassword(userId: string, oldPassword: string, newPassword: string): Promise<void> {
const user = await User.findById(userId).select('+password');
if (!user) {
throw new AppError('User not found', 404);
}

// 验证旧密码
const isValidPassword = await user.comparePassword(oldPassword);
if (!isValidPassword) {
throw new AppError('Invalid current password', 400);
}

// 更新密码
user.password = newPassword;
await user.save();

logger.info(`Password changed for user: ${user.email}`);
}

// 获取用户统计信息
public static async getUserStats(): Promise<UserStats> {
const now = new Date();
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);

const [
totalUsers,
activeUsers,
newUsersThisMonth,
usersByRole
] = await Promise.all([
User.countDocuments({}),
User.countDocuments({ isActive: true }),
User.countDocuments({
createdAt: { $gte: startOfMonth },
isActive: true
}),
User.aggregate([
{ $match: { isActive: true } },
{ $group: { _id: '$role', count: { $sum: 1 } } }
])
]);

const roleStats = usersByRole.reduce((acc, item) => {
acc[item._id] = item.count;
return acc;
}, {});

return {
totalUsers,
activeUsers,
newUsersThisMonth,
usersByRole: roleStats
};
}

// 批量操作
public static async bulkUpdateUsers(userIds: string[], updateData: Partial<IUser>): Promise<number> {
const result = await User.updateMany(
{ _id: { $in: userIds } },
{ $set: updateData }
);

logger.info(`Bulk updated ${result.modifiedCount} users`);

return result.modifiedCount;
}

// 导出用户数据
public static async exportUsers(filters: any = {}): Promise<IUser[]> {
const users = await User.find({ ...filters, isActive: true })
.select('-password -refreshToken')
.sort({ createdAt: -1 })
.lean();

logger.info(`Exported ${users.length} users`);

return users;
}
}

🔐 认证与授权系统

1. JWT认证实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
// src/services/AuthService.ts - 认证服务
import jwt from 'jsonwebtoken';
import crypto from 'crypto';
import { User, IUser } from '@/models/User';
import { config } from '@/config';
import { AppError } from '@/utils/AppError';
import { EmailService } from '@/services/EmailService';
import { CacheService } from '@/services/CacheService';
import { logger } from '@/utils/logger';

interface LoginCredentials {
email: string;
password: string;
}

interface RegisterData {
username: string;
email: string;
password: string;
firstName: string;
lastName: string;
}

interface AuthTokens {
accessToken: string;
refreshToken: string;
}

export class AuthService {

// 用户注册
public static async register(userData: RegisterData): Promise<{
user: IUser;
tokens: AuthTokens;
}> {
// 检查用户是否已存在
const existingUser = await User.findOne({
$or: [
{ email: userData.email },
{ username: userData.username }
]
});

if (existingUser) {
throw new AppError('User already exists', 409);
}

// 创建用户
const user = new User(userData);
await user.save();

// 生成认证令牌
const tokens = user.generateAuthTokens();

// 保存刷新令牌
user.refreshToken = tokens.refreshToken;
await user.save();

// 发送验证邮件
const verificationToken = await this.generateEmailVerificationToken(user._id);
await EmailService.sendEmailVerification(user.email, verificationToken);

logger.info(`User registered: ${user.email}`);

return {
user: user.toJSON(),
tokens
};
}

// 用户登录
public static async login(credentials: LoginCredentials): Promise<{
user: IUser;
tokens: AuthTokens;
}> {
const { email, password } = credentials;

// 查找用户(包含密码字段)
const user = await User.findOne({ email, isActive: true }).select('+password');

if (!user) {
throw new AppError('Invalid credentials', 401);
}

// 验证密码
const isValidPassword = await user.comparePassword(password);
if (!isValidPassword) {
throw new AppError('Invalid credentials', 401);
}

// 检查账户是否被锁定
await this.checkAccountLockout(user._id);

// 生成认证令牌
const tokens = user.generateAuthTokens();

// 保存刷新令牌和最后登录时间
user.refreshToken = tokens.refreshToken;
user.lastLoginAt = new Date();
await user.save();

// 清除登录失败记录
await this.clearLoginAttempts(user._id);

logger.info(`User logged in: ${user.email}`);

return {
user: user.toJSON(),
tokens
};
}

// 刷新令牌
public static async refreshTokens(refreshToken: string): Promise<AuthTokens> {
try {
const decoded = jwt.verify(refreshToken, config.JWT_REFRESH_SECRET) as any;

const user = await User.findById(decoded.userId);
if (!user || user.refreshToken !== refreshToken || !user.isActive) {
throw new AppError('Invalid refresh token', 401);
}

// 生成新的令牌
const tokens = user.generateAuthTokens();

// 更新刷新令牌
user.refreshToken = tokens.refreshToken;
await user.save();

return tokens;
} catch (error) {
throw new AppError('Invalid refresh token', 401);
}
}

// 用户登出
public static async logout(userId: string): Promise<void> {
await User.findByIdAndUpdate(userId, {
$unset: { refreshToken: 1 }
});

logger.info(`User logged out: ${userId}`);
}

// 全部设备登出
public static async logoutAllDevices(userId: string): Promise<void> {
await User.findByIdAndUpdate(userId, {
$unset: { refreshToken: 1 }
});

// 将用户添加到黑名单缓存
await CacheService.set(`blacklist:${userId}`, true, 86400); // 24小时

logger.info(`User logged out from all devices: ${userId}`);
}

// 忘记密码
public static async forgotPassword(email: string): Promise<void> {
const user = await User.findOne({ email, isActive: true });
if (!user) {
// 为了安全,不暴露用户是否存在
return;
}

// 生成重置令牌
const resetToken = crypto.randomBytes(32).toString('hex');
const resetTokenHash = crypto.createHash('sha256').update(resetToken).digest('hex');

// 保存重置令牌(有效期1小时)
await CacheService.set(
`password_reset:${resetTokenHash}`,
user._id.toString(),
3600
);

// 发送重置邮件
await EmailService.sendPasswordReset(user.email, resetToken);

logger.info(`Password reset requested: ${user.email}`);
}

// 重置密码
public static async resetPassword(token: string, newPassword: string): Promise<void> {
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');

const userId = await CacheService.get(`password_reset:${tokenHash}`);
if (!userId) {
throw new AppError('Invalid or expired reset token', 400);
}

const user = await User.findById(userId);
if (!user) {
throw new AppError('User not found', 404);
}

// 更新密码
user.password = newPassword;
user.refreshToken = undefined; // 清除刷新令牌
await user.save();

// 删除重置令牌
await CacheService.delete(`password_reset:${tokenHash}`);

logger.info(`Password reset completed: ${user.email}`);
}

// 邮箱验证
public static async verifyEmail(token: string): Promise<void> {
const userId = await CacheService.get(`email_verification:${token}`);
if (!userId) {
throw new AppError('Invalid or expired verification token', 400);
}

const user = await User.findById(userId);
if (!user) {
throw new AppError('User not found', 404);
}

user.isEmailVerified = true;
await user.save();

// 删除验证令牌
await CacheService.delete(`email_verification:${token}`);

logger.info(`Email verified: ${user.email}`);
}

// 生成邮箱验证令牌
private static async generateEmailVerificationToken(userId: string): Promise<string> {
const token = crypto.randomBytes(32).toString('hex');

// 保存验证令牌(有效期24小时)
await CacheService.set(`email_verification:${token}`, userId.toString(), 86400);

return token;
}

// 检查账户锁定
private static async checkAccountLockout(userId: string): Promise<void> {
const lockoutKey = `lockout:${userId}`;
const attemptsKey = `login_attempts:${userId}`;

const isLocked = await CacheService.get(lockoutKey);
if (isLocked) {
throw new AppError('Account is temporarily locked. Please try again later.', 423);
}

// 增加登录失败次数
const attempts = await CacheService.get(attemptsKey) || 0;
await CacheService.set(attemptsKey, attempts + 1, 900); // 15分钟过期

// 如果失败次数超过限制,锁定账户
if (attempts >= 5) {
await CacheService.set(lockoutKey, true, 1800); // 锁定30分钟
throw new AppError('Too many failed login attempts. Account locked for 30 minutes.', 423);
}
}

// 清除登录失败记录
private static async clearLoginAttempts(userId: string): Promise<void> {
await CacheService.delete(`login_attempts:${userId}`);
await CacheService.delete(`lockout:${userId}`);
}

// 验证令牌
public static async verifyToken(token: string): Promise<IUser> {
try {
const decoded = jwt.verify(token, config.JWT_SECRET) as any;

// 检查用户是否在黑名单中
const isBlacklisted = await CacheService.get(`blacklist:${decoded.userId}`);
if (isBlacklisted) {
throw new AppError('Token is blacklisted', 401);
}

const user = await User.findById(decoded.userId);
if (!user || !user.isActive) {
throw new AppError('User not found or inactive', 401);
}

return user;
} catch (error) {
if (error instanceof jwt.JsonWebTokenError) {
throw new AppError('Invalid token', 401);
}
throw error;
}
}
}

🚀 实时通信与WebSocket

1. Socket.IO集成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
// src/services/SocketService.ts - Socket服务
import { Server as SocketIOServer, Socket } from 'socket.io';
import { Server } from 'http';
import jwt from 'jsonwebtoken';
import { config } from '@/config';
import { User } from '@/models/User';
import { logger } from '@/utils/logger';
import { CacheService } from '@/services/CacheService';

interface AuthenticatedSocket extends Socket {
userId?: string;
user?: any;
}

export class SocketService {
private io: SocketIOServer;
private connectedUsers: Map<string, string> = new Map(); // userId -> socketId

constructor(server: Server) {
this.io = new SocketIOServer(server, {
cors: {
origin: config.CLIENT_URL,
methods: ['GET', 'POST'],
credentials: true
},
pingTimeout: 60000,
pingInterval: 25000
});

this.initializeMiddleware();
this.initializeEventHandlers();
}

private initializeMiddleware(): void {
// 认证中间件
this.io.use(async (socket: AuthenticatedSocket, next) => {
try {
const token = socket.handshake.auth.token || socket.handshake.headers.authorization?.replace('Bearer ', '');

if (!token) {
return next(new Error('Authentication token required'));
}

const decoded = jwt.verify(token, config.JWT_SECRET) as any;
const user = await User.findById(decoded.userId);

if (!user || !user.isActive) {
return next(new Error('Invalid user'));
}

socket.userId = user._id.toString();
socket.user = user;
next();
} catch (error) {
next(new Error('Authentication failed'));
}
});

// 速率限制中间件
this.io.use(async (socket: AuthenticatedSocket, next) => {
const rateLimitKey = `socket_rate_limit:${socket.userId}`;
const currentCount = await CacheService.get(rateLimitKey) || 0;

if (currentCount > 100) { // 每分钟最多100个事件
return next(new Error('Rate limit exceeded'));
}

await CacheService.set(rateLimitKey, currentCount + 1, 60);
next();
});
}

private initializeEventHandlers(): void {
this.io.on('connection', (socket: AuthenticatedSocket) => {
this.handleConnection(socket);
});
}

private handleConnection(socket: AuthenticatedSocket): void {
const userId = socket.userId!;

logger.info(`Socket connected: ${socket.id} (User: ${userId})`);

// 记录用户连接
this.connectedUsers.set(userId, socket.id);

// 加入用户专属房间
socket.join(`user:${userId}`);

// 通知用户上线
this.broadcastUserStatus(userId, 'online');

// 处理加入房间
socket.on('join-room', (roomId: string) => {
this.handleJoinRoom(socket, roomId);
});

// 处理离开房间
socket.on('leave-room', (roomId: string) => {
this.handleLeaveRoom(socket, roomId);
});

// 处理私聊消息
socket.on('private-message', (data) => {
this.handlePrivateMessage(socket, data);
});

// 处理群聊消息
socket.on('group-message', (data) => {
this.handleGroupMessage(socket, data);
});

// 处理文件共享
socket.on('file-share', (data) => {
this.handleFileShare(socket, data);
});

// 处理视频通话信令
socket.on('video-call-offer', (data) => {
this.handleVideoCallOffer(socket, data);
});

socket.on('video-call-answer', (data) => {
this.handleVideoCallAnswer(socket, data);
});

socket.on('video-call-ice-candidate', (data) => {
this.handleVideoCallIceCandidate(socket, data);
});

// 处理断开连接
socket.on('disconnect', () => {
this.handleDisconnection(socket);
});

// 处理错误
socket.on('error', (error) => {
logger.error(`Socket error for user ${userId}:`, error);
});
}

private handleJoinRoom(socket: AuthenticatedSocket, roomId: string): void {
socket.join(roomId);

// 通知房间内其他用户
socket.to(roomId).emit('user-joined', {
userId: socket.userId,
username: socket.user.username,
timestamp: new Date()
});

logger.info(`User ${socket.userId} joined room ${roomId}`);
}

private handleLeaveRoom(socket: AuthenticatedSocket, roomId: string): void {
socket.leave(roomId);

// 通知房间内其他用户
socket.to(roomId).emit('user-left', {
userId: socket.userId,
username: socket.user.username,
timestamp: new Date()
});

logger.info(`User ${socket.userId} left room ${roomId}`);
}

private async handlePrivateMessage(socket: AuthenticatedSocket, data: any): Promise<void> {
const { recipientId, message, type = 'text' } = data;

try {
// 验证接收者
const recipient = await User.findById(recipientId);
if (!recipient) {
socket.emit('error', { message: 'Recipient not found' });
return;
}

const messageData = {
id: this.generateMessageId(),
senderId: socket.userId,
senderName: socket.user.username,
recipientId,
message,
type,
timestamp: new Date(),
delivered: false,
read: false
};

// 保存消息到数据库
await this.saveMessage(messageData);

// 发送给接收者
const recipientSocketId = this.connectedUsers.get(recipientId);
if (recipientSocketId) {
this.io.to(recipientSocketId).emit('private-message', messageData);
messageData.delivered = true;
}

// 确认发送给发送者
socket.emit('message-sent', messageData);

logger.info(`Private message sent from ${socket.userId} to ${recipientId}`);
} catch (error) {
logger.error('Error handling private message:', error);
socket.emit('error', { message: 'Failed to send message' });
}
}

private async handleGroupMessage(socket: AuthenticatedSocket, data: any): Promise<void> {
const { roomId, message, type = 'text' } = data;

try {
const messageData = {
id: this.generateMessageId(),
senderId: socket.userId,
senderName: socket.user.username,
roomId,
message,
type,
timestamp: new Date()
};

// 保存消息到数据库
await this.saveGroupMessage(messageData);

// 广播给房间内所有用户(除了发送者)
socket.to(roomId).emit('group-message', messageData);

// 确认发送给发送者
socket.emit('message-sent', messageData);

logger.info(`Group message sent by ${socket.userId} to room ${roomId}`);
} catch (error) {
logger.error('Error handling group message:', error);
socket.emit('error', { message: 'Failed to send message' });
}
}

private handleFileShare(socket: AuthenticatedSocket, data: any): void {
const { roomId, fileName, fileSize, fileType, fileUrl } = data;

const fileData = {
id: this.generateMessageId(),
senderId: socket.userId,
senderName: socket.user.username,
roomId,
fileName,
fileSize,
fileType,
fileUrl,
type: 'file',
timestamp: new Date()
};

// 广播文件信息
if (roomId) {
socket.to(roomId).emit('file-shared', fileData);
}

socket.emit('file-share-confirmed', fileData);

logger.info(`File shared by ${socket.userId}: ${fileName}`);
}

private handleVideoCallOffer(socket: AuthenticatedSocket, data: any): void {
const { recipientId, offer } = data;

const recipientSocketId = this.connectedUsers.get(recipientId);
if (recipientSocketId) {
this.io.to(recipientSocketId).emit('video-call-offer', {
callerId: socket.userId,
callerName: socket.user.username,
offer
});
} else {
socket.emit('call-failed', { reason: 'User not available' });
}
}

private handleVideoCallAnswer(socket: AuthenticatedSocket, data: any): void {
const { callerId, answer } = data;

const callerSocketId = this.connectedUsers.get(callerId);
if (callerSocketId) {
this.io.to(callerSocketId).emit('video-call-answer', {
answerId: socket.userId,
answer
});
}
}

private handleVideoCallIceCandidate(socket: AuthenticatedSocket, data: any): void {
const { targetId, candidate } = data;

const targetSocketId = this.connectedUsers.get(targetId);
if (targetSocketId) {
this.io.to(targetSocketId).emit('video-call-ice-candidate', {
senderId: socket.userId,
candidate
});
}
}

private handleDisconnection(socket: AuthenticatedSocket): void {
const userId = socket.userId!;

logger.info(`Socket disconnected: ${socket.id} (User: ${userId})`);

// 移除用户连接记录
this.connectedUsers.delete(userId);

// 通知用户下线
this.broadcastUserStatus(userId, 'offline');
}

private broadcastUserStatus(userId: string, status: 'online' | 'offline'): void {
this.io.emit('user-status-change', {
userId,
status,
timestamp: new Date()
});
}

private generateMessageId(): string {
return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}

private async saveMessage(messageData: any): Promise<void> {
// 实现消息保存逻辑
// 可以保存到MongoDB或其他数据库
}

private async saveGroupMessage(messageData: any): Promise<void> {
// 实现群组消息保存逻辑
}

// 公共方法
public sendToUser(userId: string, event: string, data: any): void {
const socketId = this.connectedUsers.get(userId);
if (socketId) {
this.io.to(socketId).emit(event, data);
}
}

public sendToRoom(roomId: string, event: string, data: any): void {
this.io.to(roomId).emit(event, data);
}

public broadcastToAll(event: string, data: any): void {
this.io.emit(event, data);
}

public getConnectedUsers(): string[] {
return Array.from(this.connectedUsers.keys());
}

public isUserConnected(userId: string): boolean {
return this.connectedUsers.has(userId);
}
}

🔍 GraphQL API设计

1. GraphQL服务器设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// src/graphql/server.ts - GraphQL服务器
import { ApolloServer } from 'apollo-server-express';
import { buildSchema } from 'type-graphql';
import { Application } from 'express';
import { Container } from 'typedi';

import { UserResolver } from '@/graphql/resolvers/UserResolver';
import { PostResolver } from '@/graphql/resolvers/PostResolver';
import { AuthResolver } from '@/graphql/resolvers/AuthResolver';
import { authChecker } from '@/graphql/auth/authChecker';
import { formatError } from '@/graphql/utils/errorFormatter';
import { createContext } from '@/graphql/context';
import { logger } from '@/utils/logger';

export class GraphQLServer {
private server: ApolloServer;

constructor() {
this.initializeServer();
}

private async initializeServer(): Promise<void> {
try {
const schema = await buildSchema({
resolvers: [UserResolver, PostResolver, AuthResolver],
container: Container,
authChecker: authChecker,
validate: true,
emitSchemaFile: process.env.NODE_ENV === 'development' ? 'schema.gql' : false
});

this.server = new ApolloServer({
schema,
context: createContext,
formatError: formatError,
introspection: process.env.NODE_ENV !== 'production',
playground: process.env.NODE_ENV !== 'production',
plugins: [
{
requestDidStart() {
return {
didResolveOperation(requestContext) {
logger.info(`GraphQL Operation: ${requestContext.request.operationName}`);
},
didEncounterErrors(requestContext) {
logger.error('GraphQL Errors:', requestContext.errors);
}
};
}
}
]
});

logger.info('GraphQL server initialized');
} catch (error) {
logger.error('Failed to initialize GraphQL server:', error);
throw error;
}
}

public async applyMiddleware(app: Application): Promise<void> {
await this.server.start();
this.server.applyMiddleware({
app,
path: '/graphql',
cors: false // 使用Express的CORS配置
});

logger.info(`GraphQL server ready at /graphql`);
}

public async stop(): Promise<void> {
await this.server.stop();
logger.info('GraphQL server stopped');
}
}