# 后端技术架构详设文档 > **文档版本**: v1.0 > **撰写人**: 后端架构师 > **创建日期**: 2024年9月11日 ## 1. 后端架构总览 ### 1.1 技术栈详细说明 ```typescript // 后端技术栈配置 { "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应用配置 ```typescript // 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 => { 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 认证服务设计 ```typescript // 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 { 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 { 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 { 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 { 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 房间管理服务 ```typescript // 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 { 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 { 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 { 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 { const rooms = await Room.find({ status: 'waiting', currentPlayers: { $lt: 2 } }).populate('host', 'nickname avatar').lean() return rooms } // 获取房间详情 async getRoomDetails(roomCode: string): Promise { // 先从缓存获取 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 { 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 { await redisClient.setex( `room:${room.code}`, 30 * 60, // 30分钟过期 JSON.stringify(room) ) } } ``` ### 2.4 游戏核心服务 ```typescript // 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 { 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 { 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 { 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 { 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 { 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 { 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连接管理 ```typescript // 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() private userConnections = new Map() // userId -> connectionIds[] private roomConnections = new Map() // 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})`) } }