887 lines
27 KiB
Markdown
887 lines
27 KiB
Markdown
# 后端技术架构详设文档
|
|
|
|
> **文档版本**: 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<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 认证服务设计
|
|
|
|
```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<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 房间管理服务
|
|
|
|
```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<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 游戏核心服务
|
|
|
|
```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<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连接管理
|
|
|
|
```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<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})`)
|
|
}
|
|
} |