27 KiB
27 KiB
后端技术架构详设文档
文档版本: v1.0
撰写人: 后端架构师
创建日期: 2024年9月11日
1. 后端架构总览
1.1 技术栈详细说明
// 后端技术栈配置
{
"runtime": "Node.js 18+", // 运行环境
"framework": "Fastify 4.x", // Web框架
"language": "TypeScript 5.0+", // 开发语言
"database": "MongoDB 6.0", // 主数据库
"cache": "Redis 7.0", // 缓存数据库
"websocket": "ws + socket.io", // WebSocket库
"orm": "Mongoose 7.x", // ODM工具
"validation": "Joi", // 数据验证
"auth": "JWT + 微信登录", // 认证方案
"logging": "Winston + Morgan", // 日志系统
"testing": "Jest + Supertest", // 测试框架
"process": "PM2", // 进程管理
"containerization": "Docker" // 容器化
}
1.2 服务架构设计
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Load Balancer │ │ Gateway │ │ Static CDN │
│ (Nginx) │ │ (API Route) │ │ (Assets) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
└───────────────────────┼───────────────────────┘
│
┌───────────────────────┴───────────────────────┐
│ Application │
│ Layer (Fastify) │
└───────────────────────┬───────────────────────┘
│
┌────────────────────────────┼────────────────────────────┐
│ │ │
┌───▼────┐ ┌─────────▼─────────┐ ┌─────────▼─────────┐
│ Auth │ │ Game Service │ │ WebSocket │
│Service │ │ (Core Logic) │ │ Service │
└────────┘ └───────────────────┘ └───────────────────┘
│ │ │
└───────────────────┼────────────────────────┘
│
┌───────────────────┼───────────────────┐
│ │ │
┌───▼─────┐ ┌───────▼──────┐ ┌──────▼─────┐
│MongoDB │ │ Redis │ │ External │
│(主数据) │ │ (缓存) │ │ APIs │
└─────────┘ └──────────────┘ └────────────┘
1.3 项目目录结构
src/
├── app.ts # 应用入口
├── server.ts # 服务器启动
├── config/ # 配置文件
│ ├── database.ts # 数据库配置
│ ├── redis.ts # Redis配置
│ ├── jwt.ts # JWT配置
│ └── environment.ts # 环境变量
├── controllers/ # 控制器层
│ ├── auth.controller.ts # 认证控制器
│ ├── room.controller.ts # 房间控制器
│ ├── game.controller.ts # 游戏控制器
│ └── user.controller.ts # 用户控制器
├── services/ # 业务逻辑层
│ ├── auth.service.ts # 认证服务
│ ├── room.service.ts # 房间服务
│ ├── game.service.ts # 游戏服务
│ ├── user.service.ts # 用户服务
│ └── websocket.service.ts # WebSocket服务
├── models/ # 数据模型
│ ├── User.ts # 用户模型
│ ├── Room.ts # 房间模型
│ ├── Game.ts # 游戏模型
│ └── GameSession.ts # 游戏会话模型
├── middleware/ # 中间件
│ ├── auth.middleware.ts # 认证中间件
│ ├── validation.middleware.ts # 验证中间件
│ ├── error.middleware.ts # 错误处理中间件
│ └── logging.middleware.ts # 日志中间件
├── routes/ # 路由定义
│ ├── auth.routes.ts # 认证路由
│ ├── room.routes.ts # 房间路由
│ ├── game.routes.ts # 游戏路由
│ └── user.routes.ts # 用户路由
├── utils/ # 工具函数
│ ├── gameLogic.ts # 游戏逻辑工具
│ ├── encryption.ts # 加密工具
│ ├── validators.ts # 验证工具
│ └── helpers.ts # 通用工具
├── websocket/ # WebSocket处理
│ ├── handlers/ # 消息处理器
│ ├── events.ts # 事件定义
│ └── connection.ts # 连接管理
└── tests/ # 测试文件
├── unit/ # 单元测试
├── integration/ # 集成测试
└── e2e/ # 端到端测试
2. 核心服务设计
2.1 Fastify应用配置
// app.ts - 应用主配置
import Fastify, { FastifyInstance } from 'fastify'
import fastifyJwt from '@fastify/jwt'
import fastifyWebsocket from '@fastify/websocket'
import fastifyCors from '@fastify/cors'
import fastifyRateLimit from '@fastify/rate-limit'
export const createApp = async (): Promise<FastifyInstance> => {
const app = Fastify({
logger: {
level: process.env.LOG_LEVEL || 'info',
transport: process.env.NODE_ENV === 'development' ? {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'HH:MM:ss',
ignore: 'pid,hostname'
}
} : undefined
}
})
// 注册插件
await app.register(fastifyJwt, {
secret: process.env.JWT_SECRET!,
sign: { expiresIn: '7d' }
})
await app.register(fastifyWebsocket, {
options: {
maxPayload: 1048576, // 1MB
verifyClient: (info) => {
// WebSocket连接验证
return verifyWebSocketClient(info)
}
}
})
await app.register(fastifyCors, {
origin: process.env.CORS_ORIGINS?.split(',') || ['http://localhost:3000'],
credentials: true
})
await app.register(fastifyRateLimit, {
max: 100,
timeWindow: '1 minute',
errorResponseBuilder: (request, context) => {
return {
code: 429,
error: 'Too Many Requests',
message: `请求过于频繁,请${Math.round(context.ttl / 1000)}秒后重试`
}
}
})
// 注册路由
await app.register(authRoutes, { prefix: '/api/auth' })
await app.register(userRoutes, { prefix: '/api/users' })
await app.register(roomRoutes, { prefix: '/api/rooms' })
await app.register(gameRoutes, { prefix: '/api/games' })
await app.register(websocketRoutes, { prefix: '/ws' })
// 错误处理
app.setErrorHandler(errorHandler)
app.setNotFoundHandler(notFoundHandler)
return app
}
// 健康检查端点
app.get('/health', async (request, reply) => {
const health = {
status: 'ok',
timestamp: new Date().toISOString(),
services: {
database: await checkDatabaseHealth(),
redis: await checkRedisHealth(),
websocket: checkWebSocketHealth()
}
}
reply.send(health)
})
2.2 认证服务设计
// services/auth.service.ts
import jwt from 'jsonwebtoken'
import { User } from '../models/User'
import { redisClient } from '../config/redis'
export class AuthService {
// 微信小程序登录
async wxLogin(code: string): Promise<{ user: User; token: string }> {
try {
// 1. 通过code获取openid
const wxSession = await this.getWxSession(code)
// 2. 查找或创建用户
let user = await User.findOne({ openid: wxSession.openid })
if (!user) {
user = await User.create({
openid: wxSession.openid,
sessionKey: wxSession.session_key,
unionid: wxSession.unionid,
createdAt: new Date()
})
} else {
// 更新session_key
user.sessionKey = wxSession.session_key
await user.save()
}
// 3. 生成JWT token
const token = this.generateToken(user._id.toString())
// 4. 缓存用户会话
await this.cacheUserSession(user._id.toString(), token)
return { user, token }
} catch (error) {
throw new Error('微信登录失败')
}
}
// 获取微信session
private async getWxSession(code: string): Promise<WxSessionResponse> {
const response = await fetch(`https://api.weixin.qq.com/sns/jscode2session`, {
method: 'GET',
params: {
appid: process.env.WX_APPID!,
secret: process.env.WX_APP_SECRET!,
js_code: code,
grant_type: 'authorization_code'
}
})
const data = await response.json()
if (data.errcode) {
throw new Error(`微信API错误: ${data.errmsg}`)
}
return data
}
// 生成JWT token
private generateToken(userId: string): string {
return jwt.sign(
{ userId, type: 'access' },
process.env.JWT_SECRET!,
{ expiresIn: '7d' }
)
}
// 验证token
async verifyToken(token: string): Promise<{ userId: string; valid: boolean }> {
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any
// 检查会话是否仍然有效
const sessionExists = await redisClient.exists(`session:${decoded.userId}`)
return {
userId: decoded.userId,
valid: sessionExists === 1
}
} catch (error) {
return { userId: '', valid: false }
}
}
// 缓存用户会话
private async cacheUserSession(userId: string, token: string): Promise<void> {
const sessionData = {
token,
createdAt: new Date().toISOString(),
lastActive: new Date().toISOString()
}
await redisClient.setex(
`session:${userId}`,
7 * 24 * 60 * 60, // 7天过期
JSON.stringify(sessionData)
)
}
// 刷新用户活跃时间
async refreshUserActivity(userId: string): Promise<void> {
const sessionKey = `session:${userId}`
const sessionData = await redisClient.get(sessionKey)
if (sessionData) {
const session = JSON.parse(sessionData)
session.lastActive = new Date().toISOString()
await redisClient.setex(
sessionKey,
7 * 24 * 60 * 60,
JSON.stringify(session)
)
}
}
// 用户登出
async logout(userId: string): Promise<void> {
await redisClient.del(`session:${userId}`)
}
}
// 认证中间件
export const authMiddleware = async (request: FastifyRequest, reply: FastifyReply) => {
try {
const authHeader = request.headers.authorization
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return reply.code(401).send({ error: '缺少认证令牌' })
}
const token = authHeader.substring(7)
const authService = new AuthService()
const { userId, valid } = await authService.verifyToken(token)
if (!valid) {
return reply.code(401).send({ error: '无效的认证令牌' })
}
// 刷新用户活跃时间
await authService.refreshUserActivity(userId)
// 将用户ID添加到请求上下文
request.user = { id: userId }
} catch (error) {
return reply.code(401).send({ error: '认证失败' })
}
}
2.3 房间管理服务
// services/room.service.ts
import { Room } from '../models/Room'
import { User } from '../models/User'
import { redisClient } from '../config/redis'
import { websocketService } from './websocket.service'
export class RoomService {
// 创建房间
async createRoom(hostId: string): Promise<Room> {
const roomCode = this.generateRoomCode()
const room = await Room.create({
code: roomCode,
hostId,
status: 'waiting',
maxPlayers: 2,
currentPlayers: 1,
createdAt: new Date()
})
// 缓存房间信息
await this.cacheRoomData(room)
// 通知房间列表更新
await websocketService.broadcastRoomListUpdate()
return room
}
// 加入房间
async joinRoom(roomCode: string, playerId: string): Promise<Room> {
const room = await Room.findOne({ code: roomCode })
if (!room) {
throw new Error('房间不存在')
}
if (room.status !== 'waiting') {
throw new Error('房间已开始游戏或已结束')
}
if (room.currentPlayers >= room.maxPlayers) {
throw new Error('房间已满')
}
if (room.hostId === playerId) {
throw new Error('不能加入自己创建的房间')
}
// 更新房间信息
room.guestId = playerId
room.currentPlayers = 2
room.status = 'ready'
await room.save()
// 更新缓存
await this.cacheRoomData(room)
// 通知房间内玩家
await websocketService.notifyRoomUpdate(roomCode, {
type: 'PLAYER_JOINED',
room: room.toObject(),
playerId
})
return room
}
// 离开房间
async leaveRoom(roomCode: string, playerId: string): Promise<void> {
const room = await Room.findOne({ code: roomCode })
if (!room) {
throw new Error('房间不存在')
}
if (room.hostId === playerId) {
// 房主离开,删除房间
await Room.deleteOne({ code: roomCode })
await redisClient.del(`room:${roomCode}`)
// 通知客人
if (room.guestId) {
await websocketService.notifyPlayer(room.guestId, {
type: 'ROOM_CLOSED',
message: '房主已离开,房间关闭'
})
}
} else if (room.guestId === playerId) {
// 客人离开
room.guestId = undefined
room.currentPlayers = 1
room.status = 'waiting'
await room.save()
await this.cacheRoomData(room)
// 通知房主
await websocketService.notifyPlayer(room.hostId, {
type: 'PLAYER_LEFT',
room: room.toObject()
})
}
// 更新房间列表
await websocketService.broadcastRoomListUpdate()
}
// 获取房间列表
async getRoomList(): Promise<Room[]> {
const rooms = await Room.find({
status: 'waiting',
currentPlayers: { $lt: 2 }
}).populate('host', 'nickname avatar').lean()
return rooms
}
// 获取房间详情
async getRoomDetails(roomCode: string): Promise<Room | null> {
// 先从缓存获取
const cachedRoom = await redisClient.get(`room:${roomCode}`)
if (cachedRoom) {
return JSON.parse(cachedRoom)
}
// 从数据库获取
const room = await Room.findOne({ code: roomCode })
.populate('host', 'nickname avatar')
.populate('guest', 'nickname avatar')
.lean()
if (room) {
await this.cacheRoomData(room)
}
return room
}
// 开始游戏
async startGame(roomCode: string, hostId: string): Promise<void> {
const room = await Room.findOne({ code: roomCode })
if (!room) {
throw new Error('房间不存在')
}
if (room.hostId !== hostId) {
throw new Error('只有房主可以开始游戏')
}
if (room.currentPlayers < 2) {
throw new Error('等待其他玩家加入')
}
// 更新房间状态
room.status = 'playing'
await room.save()
// 创建游戏会话
const gameService = new GameService()
const gameSession = await gameService.createGameSession(room)
// 通知玩家游戏开始
await websocketService.notifyRoomUpdate(roomCode, {
type: 'GAME_STARTED',
gameSession: gameSession.toObject()
})
}
// 生成房间码
private generateRoomCode(): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
let result = ''
for (let i = 0; i < 6; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length))
}
return result
}
// 缓存房间数据
private async cacheRoomData(room: any): Promise<void> {
await redisClient.setex(
`room:${room.code}`,
30 * 60, // 30分钟过期
JSON.stringify(room)
)
}
}
2.4 游戏核心服务
// services/game.service.ts
import { GameSession } from '../models/GameSession'
import { Room } from '../models/Room'
import { GameLogic } from '../utils/gameLogic'
import { websocketService } from './websocket.service'
export class GameService {
// 创建游戏会话
async createGameSession(room: Room): Promise<GameSession> {
const gameSession = await GameSession.create({
roomId: room._id,
roomCode: room.code,
players: [room.hostId, room.guestId],
gameState: {
phase: 'PLACING',
currentPlayer: room.hostId, // 房主先开始
boards: {
[room.hostId]: GameLogic.createEmptyBoard(),
[room.guestId!]: GameLogic.createEmptyBoard()
},
moves: [],
startedAt: new Date()
},
createdAt: new Date()
})
return gameSession
}
// 放置飞机
async placePlanes(
gameSessionId: string,
playerId: string,
planes: PlaneData[]
): Promise<void> {
const session = await GameSession.findById(gameSessionId)
if (!session) {
throw new Error('游戏会话不存在')
}
if (session.gameState.phase !== 'PLACING') {
throw new Error('当前不是飞机放置阶段')
}
// 验证飞机放置是否合法
const isValid = GameLogic.validatePlanesPlacement(planes)
if (!isValid) {
throw new Error('飞机放置不合法')
}
// 更新游戏状态
session.gameState.boards[playerId] = GameLogic.placePlanesOnBoard(
session.gameState.boards[playerId],
planes
)
// 标记玩家已准备
session.gameState.playersReady = session.gameState.playersReady || {}
session.gameState.playersReady[playerId] = true
// 检查是否所有玩家都已准备
const allReady = session.players.every(p =>
session.gameState.playersReady[p]
)
if (allReady) {
// 开始对战阶段
session.gameState.phase = 'BATTLING'
session.gameState.battleStartedAt = new Date()
}
await session.save()
// 通知所有玩家状态更新
await websocketService.notifyGameUpdate(session.roomCode, {
type: 'PLACEMENT_UPDATE',
playerId,
ready: true,
allReady,
gameState: session.gameState
})
}
// 执行攻击
async attack(
gameSessionId: string,
attackerId: string,
position: Position
): Promise<AttackResult> {
const session = await GameSession.findById(gameSessionId)
if (!session) {
throw new Error('游戏会话不存在')
}
if (session.gameState.phase !== 'BATTLING') {
throw new Error('当前不是对战阶段')
}
if (session.gameState.currentPlayer !== attackerId) {
throw new Error('不是你的回合')
}
// 确定被攻击的玩家
const defenderId = session.players.find(p => p !== attackerId)!
const defenderBoard = session.gameState.boards[defenderId]
// 执行攻击逻辑
const attackResult = GameLogic.processAttack(defenderBoard, position)
// 更新游戏状态
session.gameState.boards[defenderId] = attackResult.updatedBoard
session.gameState.moves.push({
playerId: attackerId,
type: 'ATTACK',
position,
result: attackResult.type,
timestamp: new Date()
})
// 检查游戏是否结束
if (attackResult.gameEnded) {
session.gameState.phase = 'FINISHED'
session.gameState.winner = attackerId
session.gameState.endedAt = new Date()
// 更新玩家统计
await this.updatePlayerStats(attackerId, defenderId)
} else {
// 切换当前玩家
session.gameState.currentPlayer = defenderId
}
await session.save()
// 通知所有玩家攻击结果
await websocketService.notifyGameUpdate(session.roomCode, {
type: 'ATTACK_RESULT',
attackerId,
defenderId,
position,
result: attackResult,
gameState: session.gameState
})
return attackResult
}
// 获取游戏状态
async getGameState(gameSessionId: string, playerId: string): Promise<any> {
const session = await GameSession.findById(gameSessionId)
if (!session) {
throw new Error('游戏会话不存在')
}
if (!session.players.includes(playerId)) {
throw new Error('你不在这个游戏中')
}
// 返回过滤后的游戏状态(隐藏对手飞机位置)
const filteredState = {
...session.gameState,
boards: {
[playerId]: session.gameState.boards[playerId],
// 对手棋盘只显示攻击结果,不显示飞机位置
opponent: GameLogic.filterOpponentBoard(
session.gameState.boards[session.players.find(p => p !== playerId)!]
)
}
}
return filteredState
}
// 玩家投降
async surrender(gameSessionId: string, playerId: string): Promise<void> {
const session = await GameSession.findById(gameSessionId)
if (!session) {
throw new Error('游戏会话不存在')
}
if (session.gameState.phase === 'FINISHED') {
throw new Error('游戏已结束')
}
const winnerId = session.players.find(p => p !== playerId)!
// 更新游戏状态
session.gameState.phase = 'FINISHED'
session.gameState.winner = winnerId
session.gameState.endedAt = new Date()
session.gameState.surrendered = true
await session.save()
// 更新玩家统计
await this.updatePlayerStats(winnerId, playerId)
// 通知游戏结束
await websocketService.notifyGameUpdate(session.roomCode, {
type: 'GAME_ENDED',
winner: winnerId,
reason: 'SURRENDER',
gameState: session.gameState
})
}
// 更新玩家统计
private async updatePlayerStats(winnerId: string, loserId: string): Promise<void> {
const User = require('../models/User').User
// 更新获胜者统计
await User.updateOne(
{ _id: winnerId },
{
$inc: {
'stats.totalGames': 1,
'stats.wins': 1
}
}
)
// 更新失败者统计
await User.updateOne(
{ _id: loserId },
{
$inc: {
'stats.totalGames': 1
}
}
)
// 重新计算胜率
const users = await User.find({ _id: { $in: [winnerId, loserId] } })
for (const user of users) {
user.stats.winRate = user.stats.totalGames > 0
? (user.stats.wins / user.stats.totalGames * 100).toFixed(2)
: 0
await user.save()
}
}
}
3. WebSocket服务设计
3.1 WebSocket连接管理
// websocket/connection.ts
import { WebSocket } from 'ws'
import { EventEmitter } from 'events'
import { redisClient } from '../config/redis'
export class WebSocketManager extends EventEmitter {
private connections = new Map<string, WebSocketConnection>()
private userConnections = new Map<string, string[]>() // userId -> connectionIds[]
private roomConnections = new Map<string, string[]>() // roomCode -> connectionIds[]
// 添加连接
addConnection(connectionId: string, ws: WebSocket, userId: string): void {
const connection = new WebSocketConnection(connectionId, ws, userId)
this.connections.set(connectionId, connection)
// 建立用户映射
if (!this.userConnections.has(userId)) {
this.userConnections.set(userId, [])
}
this.userConnections.get(userId)!.push(connectionId)
// 设置连接事件处理
connection.on('message', (message) => {
this.handleMessage(connectionId, message)
})
connection.on('close', () => {
this.removeConnection(connectionId)
})
connection.on('error', (error) => {
console.error(`WebSocket连接错误 ${connectionId}:`, error)
this.removeConnection(connectionId)
})
console.log(`WebSocket连接已建立: ${connectionId} (用户: ${userId})`)
}
// 移除连接
removeConnection(connectionId: string): void {
const connection = this.connections.get(connectionId)
if (!connection) return
const userId = connection.userId
// 移除连接映射
this.connections.delete(connectionId)
// 移除用户映射
const userConns = this.userConnections.get(userId)
if (userConns) {
const index = userConns.indexOf(connectionId)
if (index > -1) {
userConns.splice(index, 1)
}
if (userConns.length === 0) {
this.userConnections.delete(userId)
}
}
// 移除房间映射
for (const [roomCode, connections] of this.roomConnections.entries()) {
const index = connections.indexOf(connectionId)
if (index > -1) {
connections.splice(index, 1)
if (connections.length === 0) {
this.roomConnections.delete(roomCode)
}
// 通知房间内其他用户
this.notifyRoomUpdate(roomCode, {
type: 'PLAYER_DISCONNECTED',
playerId: userId
})
}
}
console.log(`WebSocket连接已关闭: ${connectionId} (用户: ${userId})`)
}
}