refactor:重构Zulip模块按业务功能模块化架构

- 将技术实现服务从business层迁移到core层
- 创建src/core/zulip/核心服务模块,包含API客户端、连接池等技术服务
- 保留src/business/zulip/业务逻辑,专注游戏相关的业务规则
- 通过依赖注入实现业务层与核心层的解耦
- 更新模块导入关系,确保架构分层清晰

重构后的架构符合单一职责原则,提高了代码的可维护性和可测试性
This commit is contained in:
moyin
2025-12-31 15:44:36 +08:00
parent 5140bd1a54
commit 2d10131838
36 changed files with 2773 additions and 125 deletions

View File

@@ -0,0 +1,990 @@
/**
* 会话管理服务
*
* 功能描述:
* - 维护WebSocket连接ID与Zulip队列ID的映射关系
* - 管理玩家位置跟踪和上下文注入
* - 提供空间过滤和会话查询功能
* - 支持会话状态的序列化和反序列化
* - 支持服务重启后的状态恢复
*
* 主要方法:
* - createSession(): 创建会话并绑定Socket_ID与Zulip_Queue_ID
* - getSession(): 获取会话信息
* - injectContext(): 上下文注入根据位置确定Stream/Topic
* - getSocketsInMap(): 空间过滤获取指定地图的所有Socket
* - updatePlayerPosition(): 更新玩家位置
* - destroySession(): 销毁会话
* - cleanupExpiredSessions(): 清理过期会话
*
* Redis存储结构
* - 会话数据: zulip:session:{socketId} -> JSON(GameSession)
* - 地图玩家列表: zulip:map_players:{mapId} -> Set<socketId>
* - 用户会话映射: zulip:user_session:{userId} -> socketId
*
* 使用场景:
* - 玩家登录时创建会话映射
* - 消息路由时进行上下文注入
* - 消息分发时进行空间过滤
* - 玩家登出时清理会话数据
*
* @author angjustinl
* @version 1.0.0
* @since 2025-12-25
*/
import { Injectable, Logger, Inject } from '@nestjs/common';
import { IRedisService } from '../../../core/redis/redis.interface';
import { IZulipConfigService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
import { Internal, Constants } from '../../../core/zulip/interfaces/zulip.interfaces';
/**
* 游戏会话接口 - 重新导出以保持向后兼容
*/
export type GameSession = Internal.GameSession;
/**
* 位置信息接口 - 重新导出以保持向后兼容
*/
export type Position = Internal.Position;
/**
* 上下文信息接口
*/
export interface ContextInfo {
stream: string;
topic?: string;
}
/**
* 创建会话请求接口
*/
export interface CreateSessionRequest {
socketId: string;
userId: string;
username?: string;
zulipQueueId: string;
initialMap?: string;
initialPosition?: Position;
}
/**
* 会话统计信息接口
*/
export interface SessionStats {
totalSessions: number;
mapDistribution: Record<string, number>;
oldestSession?: Date;
newestSession?: Date;
}
/**
* 会话管理服务类
*
* 职责:
* - 维护WebSocket连接ID与Zulip队列ID的映射关系
* - 管理玩家位置跟踪和上下文注入
* - 提供空间过滤和会话查询功能
* - 支持会话状态的序列化和反序列化
*
* 主要方法:
* - createSession(): 创建会话并绑定Socket_ID与Zulip_Queue_ID
* - getSession(): 获取会话信息
* - injectContext(): 上下文注入根据位置确定Stream/Topic
* - getSocketsInMap(): 空间过滤获取指定地图的所有Socket
* - updatePlayerPosition(): 更新玩家位置
* - destroySession(): 销毁会话
*
* 使用场景:
* - 玩家登录时创建会话映射
* - 消息路由时进行上下文注入
* - 消息分发时进行空间过滤
* - 玩家登出时清理会话数据
*/
@Injectable()
export class SessionManagerService {
private readonly SESSION_PREFIX = 'zulip:session:';
private readonly MAP_PLAYERS_PREFIX = 'zulip:map_players:';
private readonly USER_SESSION_PREFIX = 'zulip:user_session:';
private readonly SESSION_TIMEOUT = 3600; // 1小时
private readonly DEFAULT_MAP = 'novice_village';
private readonly DEFAULT_POSITION: Position = { x: 400, y: 300 };
private readonly logger = new Logger(SessionManagerService.name);
constructor(
@Inject('REDIS_SERVICE')
private readonly redisService: IRedisService,
@Inject('ZULIP_CONFIG_SERVICE')
private readonly configManager: IZulipConfigService,
) {
this.logger.log('SessionManagerService初始化完成');
}
/**
* 序列化会话对象为JSON字符串
*
* 功能描述:
* 将GameSession对象转换为可存储在Redis中的JSON字符串
*
* @param session 会话对象
* @returns string JSON字符串
* @private
*/
private serializeSession(session: GameSession): string {
const serialized: Internal.GameSessionSerialized = {
socketId: session.socketId,
userId: session.userId,
username: session.username,
zulipQueueId: session.zulipQueueId,
currentMap: session.currentMap,
position: session.position,
lastActivity: session.lastActivity instanceof Date
? session.lastActivity.toISOString()
: session.lastActivity,
createdAt: session.createdAt instanceof Date
? session.createdAt.toISOString()
: session.createdAt,
};
return JSON.stringify(serialized);
}
/**
* 反序列化JSON字符串为会话对象
*
* 功能描述:
* 将Redis中存储的JSON字符串转换回GameSession对象
*
* @param data JSON字符串
* @returns GameSession 会话对象
* @private
*/
private deserializeSession(data: string): GameSession {
const parsed: Internal.GameSessionSerialized = JSON.parse(data);
return {
socketId: parsed.socketId,
userId: parsed.userId,
username: parsed.username,
zulipQueueId: parsed.zulipQueueId,
currentMap: parsed.currentMap,
position: parsed.position,
lastActivity: new Date(parsed.lastActivity),
createdAt: new Date(parsed.createdAt),
};
}
/**
* 创建会话并绑定Socket_ID与Zulip_Queue_ID
*
* 功能描述:
* 创建新的游戏会话建立WebSocket连接与Zulip队列的映射关系
*
* 业务逻辑:
* 1. 验证输入参数
* 2. 检查用户是否已有会话(如有则先清理)
* 3. 创建会话对象
* 4. 存储到Redis缓存
* 5. 添加到地图玩家列表
* 6. 建立用户到会话的映射
* 7. 设置过期时间
*
* @param socketId WebSocket连接ID
* @param userId 用户ID
* @param zulipQueueId Zulip事件队列ID
* @param username 用户名(可选)
* @param initialMap 初始地图(可选)
* @param initialPosition 初始位置(可选)
* @returns Promise<GameSession> 创建的会话对象
*
* @throws Error 当参数验证失败时
* @throws Error 当Redis操作失败时
*/
async createSession(
socketId: string,
userId: string,
zulipQueueId: string,
username?: string,
initialMap?: string,
initialPosition?: Position,
): Promise<GameSession> {
const startTime = Date.now();
this.logger.log('开始创建游戏会话', {
operation: 'createSession',
socketId,
userId,
zulipQueueId,
timestamp: new Date().toISOString(),
});
try {
// 1. 参数验证
if (!socketId || !socketId.trim()) {
throw new Error('socketId不能为空');
}
if (!userId || !userId.trim()) {
throw new Error('userId不能为空');
}
if (!zulipQueueId || !zulipQueueId.trim()) {
throw new Error('zulipQueueId不能为空');
}
// 2. 检查用户是否已有会话,如有则先清理
const existingSocketId = await this.redisService.get(`${this.USER_SESSION_PREFIX}${userId}`);
if (existingSocketId) {
this.logger.log('用户已有会话,先清理旧会话', {
operation: 'createSession',
userId,
existingSocketId,
});
await this.destroySession(existingSocketId);
}
// 3. 创建会话对象
const now = new Date();
const session: GameSession = {
socketId,
userId,
username: username || `user_${userId}`,
zulipQueueId,
currentMap: initialMap || this.DEFAULT_MAP,
position: initialPosition || { ...this.DEFAULT_POSITION },
lastActivity: now,
createdAt: now,
};
// 4. 存储会话到Redis
const sessionKey = `${this.SESSION_PREFIX}${socketId}`;
await this.redisService.setex(sessionKey, this.SESSION_TIMEOUT, this.serializeSession(session));
// 5. 添加到地图玩家列表
const mapKey = `${this.MAP_PLAYERS_PREFIX}${session.currentMap}`;
await this.redisService.sadd(mapKey, socketId);
await this.redisService.expire(mapKey, this.SESSION_TIMEOUT);
// 6. 建立用户到会话的映射
const userSessionKey = `${this.USER_SESSION_PREFIX}${userId}`;
await this.redisService.setex(userSessionKey, this.SESSION_TIMEOUT, socketId);
const duration = Date.now() - startTime;
this.logger.log('游戏会话创建成功', {
operation: 'createSession',
socketId,
userId,
zulipQueueId,
currentMap: session.currentMap,
duration,
timestamp: new Date().toISOString(),
});
return session;
} catch (error) {
const err = error as Error;
const duration = Date.now() - startTime;
this.logger.error('创建游戏会话失败', {
operation: 'createSession',
socketId,
userId,
zulipQueueId,
error: err.message,
duration,
timestamp: new Date().toISOString(),
}, err.stack);
throw error;
}
}
/**
* 获取会话信息
*
* 功能描述:
* 根据socketId获取会话信息并更新最后活动时间
*
* @param socketId WebSocket连接ID
* @returns Promise<GameSession | null> 会话信息不存在时返回null
*/
async getSession(socketId: string): Promise<GameSession | null> {
try {
if (!socketId || !socketId.trim()) {
this.logger.warn('获取会话失败socketId为空', {
operation: 'getSession',
});
return null;
}
const sessionKey = `${this.SESSION_PREFIX}${socketId}`;
const sessionData = await this.redisService.get(sessionKey);
if (!sessionData) {
this.logger.debug('会话不存在', {
operation: 'getSession',
socketId,
});
return null;
}
const session = this.deserializeSession(sessionData);
// 更新最后活动时间
session.lastActivity = new Date();
await this.redisService.setex(sessionKey, this.SESSION_TIMEOUT, this.serializeSession(session));
this.logger.debug('获取会话信息成功', {
operation: 'getSession',
socketId,
userId: session.userId,
currentMap: session.currentMap,
});
return session;
} catch (error) {
const err = error as Error;
this.logger.error('获取会话信息失败', {
operation: 'getSession',
socketId,
error: err.message,
timestamp: new Date().toISOString(),
}, err.stack);
return null;
}
}
/**
* 根据用户ID获取会话信息
*
* 功能描述:
* 根据userId查找对应的会话信息
*
* @param userId 用户ID
* @returns Promise<GameSession | null> 会话信息不存在时返回null
*/
async getSessionByUserId(userId: string): Promise<GameSession | null> {
try {
if (!userId || !userId.trim()) {
return null;
}
const userSessionKey = `${this.USER_SESSION_PREFIX}${userId}`;
const socketId = await this.redisService.get(userSessionKey);
if (!socketId) {
return null;
}
return this.getSession(socketId);
} catch (error) {
const err = error as Error;
this.logger.error('根据用户ID获取会话失败', {
operation: 'getSessionByUserId',
userId,
error: err.message,
}, err.stack);
return null;
}
}
/**
* 上下文注入根据位置确定Stream/Topic
*
* 功能描述:
* 根据玩家当前位置和地图信息确定消息应该发送到的Zulip Stream和Topic
*
* 业务逻辑:
* 1. 获取玩家会话信息
* 2. 根据地图ID查找对应的Stream
* 3. 根据玩家位置确定Topic如果有交互对象
* 4. 返回上下文信息
*
* @param socketId WebSocket连接ID
* @param mapId 地图ID可选用于覆盖当前地图
* @returns Promise<ContextInfo> 上下文信息
*
* @throws Error 当会话不存在时
*/
async injectContext(socketId: string, mapId?: string): Promise<ContextInfo> {
this.logger.debug('开始上下文注入', {
operation: 'injectContext',
socketId,
mapId,
timestamp: new Date().toISOString(),
});
try {
const session = await this.getSession(socketId);
if (!session) {
throw new Error('会话不存在');
}
const targetMapId = mapId || session.currentMap;
// 从ConfigManager获取地图对应的Stream
const stream = this.configManager.getStreamByMap(targetMapId) || 'General';
// TODO: 根据玩家位置确定Topic
// 检查是否靠近交互对象
const context: ContextInfo = {
stream,
topic: undefined, // 暂时不设置Topic使用默认的General
};
this.logger.debug('上下文注入完成', {
operation: 'injectContext',
socketId,
targetMapId,
stream: context.stream,
topic: context.topic,
});
return context;
} catch (error) {
const err = error as Error;
this.logger.error('上下文注入失败', {
operation: 'injectContext',
socketId,
mapId,
error: err.message,
timestamp: new Date().toISOString(),
}, err.stack);
// 返回默认上下文
return {
stream: 'General',
};
}
}
/**
* 空间过滤获取指定地图的所有Socket
*
* 功能描述:
* 获取指定地图中所有在线玩家的Socket ID列表用于消息分发
*
* @param mapId 地图ID
* @returns Promise<string[]> Socket ID列表
*/
async getSocketsInMap(mapId: string): Promise<string[]> {
try {
const mapKey = `${this.MAP_PLAYERS_PREFIX}${mapId}`;
const socketIds = await this.redisService.smembers(mapKey);
this.logger.debug('获取地图玩家列表', {
operation: 'getSocketsInMap',
mapId,
playerCount: socketIds.length,
});
return socketIds;
} catch (error) {
const err = error as Error;
this.logger.error('获取地图玩家列表失败', {
operation: 'getSocketsInMap',
mapId,
error: err.message,
timestamp: new Date().toISOString(),
}, err.stack);
return [];
}
}
/**
* 更新玩家位置
*
* 功能描述:
* 更新玩家在游戏世界中的位置信息,如果切换地图则更新地图玩家列表
*
* 业务逻辑:
* 1. 获取当前会话
* 2. 检查是否切换地图
* 3. 更新会话位置信息
* 4. 如果切换地图,更新地图玩家列表
* 5. 保存更新后的会话
*
* @param socketId WebSocket连接ID
* @param mapId 地图ID
* @param x X坐标
* @param y Y坐标
* @returns Promise<boolean> 是否更新成功
*/
async updatePlayerPosition(socketId: string, mapId: string, x: number, y: number): Promise<boolean> {
this.logger.debug('开始更新玩家位置', {
operation: 'updatePlayerPosition',
socketId,
mapId,
position: { x, y },
timestamp: new Date().toISOString(),
});
try {
// 参数验证
if (!socketId || !socketId.trim()) {
this.logger.warn('更新位置失败socketId为空', {
operation: 'updatePlayerPosition',
});
return false;
}
if (!mapId || !mapId.trim()) {
this.logger.warn('更新位置失败mapId为空', {
operation: 'updatePlayerPosition',
socketId,
});
return false;
}
const sessionKey = `${this.SESSION_PREFIX}${socketId}`;
const sessionData = await this.redisService.get(sessionKey);
if (!sessionData) {
this.logger.warn('更新位置失败:会话不存在', {
operation: 'updatePlayerPosition',
socketId,
});
return false;
}
const session = this.deserializeSession(sessionData);
const oldMapId = session.currentMap;
const mapChanged = oldMapId !== mapId;
// 更新会话信息
session.currentMap = mapId;
session.position = { x, y };
session.lastActivity = new Date();
await this.redisService.setex(sessionKey, this.SESSION_TIMEOUT, this.serializeSession(session));
// 如果切换了地图,更新地图玩家列表
if (mapChanged) {
// 从旧地图移除
const oldMapKey = `${this.MAP_PLAYERS_PREFIX}${oldMapId}`;
await this.redisService.srem(oldMapKey, socketId);
// 添加到新地图
const newMapKey = `${this.MAP_PLAYERS_PREFIX}${mapId}`;
await this.redisService.sadd(newMapKey, socketId);
await this.redisService.expire(newMapKey, this.SESSION_TIMEOUT);
this.logger.log('玩家切换地图', {
operation: 'updatePlayerPosition',
socketId,
userId: session.userId,
oldMapId,
newMapId: mapId,
position: { x, y },
});
}
this.logger.debug('玩家位置更新成功', {
operation: 'updatePlayerPosition',
socketId,
mapId,
position: { x, y },
mapChanged,
});
return true;
} catch (error) {
const err = error as Error;
this.logger.error('更新玩家位置失败', {
operation: 'updatePlayerPosition',
socketId,
mapId,
position: { x, y },
error: err.message,
timestamp: new Date().toISOString(),
}, err.stack);
return false;
}
}
/**
* 销毁会话
*
* 功能描述:
* 清理玩家会话数据,从地图玩家列表中移除,释放相关资源
*
* 业务逻辑:
* 1. 获取会话信息
* 2. 从地图玩家列表中移除
* 3. 删除用户会话映射
* 4. 删除会话数据
*
* @param socketId WebSocket连接ID
* @returns Promise<boolean> 是否销毁成功
*/
async destroySession(socketId: string): Promise<boolean> {
this.logger.log('开始销毁游戏会话', {
operation: 'destroySession',
socketId,
timestamp: new Date().toISOString(),
});
try {
if (!socketId || !socketId.trim()) {
this.logger.warn('销毁会话失败socketId为空', {
operation: 'destroySession',
});
return false;
}
// 获取会话信息
const sessionKey = `${this.SESSION_PREFIX}${socketId}`;
const sessionData = await this.redisService.get(sessionKey);
if (!sessionData) {
this.logger.log('会话不存在,跳过销毁', {
operation: 'destroySession',
socketId,
});
return true;
}
const session = this.deserializeSession(sessionData);
// 从地图玩家列表中移除
const mapKey = `${this.MAP_PLAYERS_PREFIX}${session.currentMap}`;
await this.redisService.srem(mapKey, socketId);
// 删除用户会话映射
const userSessionKey = `${this.USER_SESSION_PREFIX}${session.userId}`;
await this.redisService.del(userSessionKey);
// 删除会话数据
await this.redisService.del(sessionKey);
this.logger.log('游戏会话销毁成功', {
operation: 'destroySession',
socketId,
userId: session.userId,
currentMap: session.currentMap,
timestamp: new Date().toISOString(),
});
return true;
} catch (error) {
const err = error as Error;
this.logger.error('销毁游戏会话失败', {
operation: 'destroySession',
socketId,
error: err.message,
timestamp: new Date().toISOString(),
}, err.stack);
// 即使失败也要尝试清理会话数据
try {
const sessionKey = `${this.SESSION_PREFIX}${socketId}`;
await this.redisService.del(sessionKey);
} catch (cleanupError) {
const cleanupErr = cleanupError as Error;
this.logger.error('会话清理失败', {
operation: 'destroySession',
socketId,
error: cleanupErr.message,
});
}
return false;
}
}
/**
* 清理过期会话
*
* 功能描述:
* 定时任务,清理超时的会话数据和相关资源
*
* 业务逻辑:
* 1. 获取所有地图的玩家列表
* 2. 检查每个会话的最后活动时间
* 3. 清理超过30分钟未活动的会话
* 4. 返回需要注销的Zulip队列ID列表
*
* @param timeoutMinutes 超时时间分钟默认30分钟
* @returns Promise<{cleanedCount: number, zulipQueueIds: string[]}> 清理结果
*/
async cleanupExpiredSessions(timeoutMinutes: number = 30): Promise<{
cleanedCount: number;
zulipQueueIds: string[];
}> {
const startTime = Date.now();
this.logger.log('开始清理过期会话', {
operation: 'cleanupExpiredSessions',
timeoutMinutes,
timestamp: new Date().toISOString(),
});
const expiredSessions: GameSession[] = [];
const zulipQueueIds: string[] = [];
const timeoutMs = timeoutMinutes * 60 * 1000;
const now = Date.now();
try {
// 获取所有地图的玩家列表
const mapIds = ['novice_village', 'tavern', 'market']; // TODO: 从配置获取
for (const mapId of mapIds) {
const socketIds = await this.getSocketsInMap(mapId);
for (const socketId of socketIds) {
try {
const sessionKey = `${this.SESSION_PREFIX}${socketId}`;
const sessionData = await this.redisService.get(sessionKey);
if (!sessionData) {
// 会话数据不存在,从地图列表中移除
await this.redisService.srem(`${this.MAP_PLAYERS_PREFIX}${mapId}`, socketId);
continue;
}
const session = this.deserializeSession(sessionData);
const lastActivityTime = session.lastActivity instanceof Date
? session.lastActivity.getTime()
: new Date(session.lastActivity).getTime();
// 检查是否超时
if (now - lastActivityTime > timeoutMs) {
expiredSessions.push(session);
zulipQueueIds.push(session.zulipQueueId);
this.logger.log('发现过期会话', {
operation: 'cleanupExpiredSessions',
socketId: session.socketId,
userId: session.userId,
lastActivity: session.lastActivity,
idleMinutes: Math.round((now - lastActivityTime) / 60000),
});
}
} catch (sessionError) {
const err = sessionError as Error;
this.logger.warn('检查会话时出错', {
operation: 'cleanupExpiredSessions',
socketId,
error: err.message,
});
}
}
}
// 清理过期会话
for (const session of expiredSessions) {
try {
await this.destroySession(session.socketId);
} catch (destroyError) {
const err = destroyError as Error;
this.logger.error('清理过期会话失败', {
operation: 'cleanupExpiredSessions',
socketId: session.socketId,
error: err.message,
});
}
}
const duration = Date.now() - startTime;
this.logger.log('过期会话清理完成', {
operation: 'cleanupExpiredSessions',
cleanedCount: expiredSessions.length,
zulipQueueCount: zulipQueueIds.length,
duration,
timestamp: new Date().toISOString(),
});
return {
cleanedCount: expiredSessions.length,
zulipQueueIds,
};
} catch (error) {
const err = error as Error;
this.logger.error('清理过期会话失败', {
operation: 'cleanupExpiredSessions',
error: err.message,
timestamp: new Date().toISOString(),
}, err.stack);
return {
cleanedCount: 0,
zulipQueueIds: [],
};
}
}
/**
* 检查会话是否过期
*
* @param socketId WebSocket连接ID
* @param timeoutMinutes 超时时间(分钟)
* @returns Promise<boolean> 是否过期
*/
async isSessionExpired(socketId: string, timeoutMinutes: number = 30): Promise<boolean> {
try {
const sessionKey = `${this.SESSION_PREFIX}${socketId}`;
const sessionData = await this.redisService.get(sessionKey);
if (!sessionData) {
return true; // 会话不存在视为过期
}
const session = this.deserializeSession(sessionData);
const lastActivityTime = session.lastActivity instanceof Date
? session.lastActivity.getTime()
: new Date(session.lastActivity).getTime();
const timeoutMs = timeoutMinutes * 60 * 1000;
return Date.now() - lastActivityTime > timeoutMs;
} catch (error) {
return true; // 出错时视为过期
}
}
/**
* 刷新会话活动时间
*
* @param socketId WebSocket连接ID
* @returns Promise<boolean> 是否刷新成功
*/
async refreshSession(socketId: string): Promise<boolean> {
try {
const sessionKey = `${this.SESSION_PREFIX}${socketId}`;
const sessionData = await this.redisService.get(sessionKey);
if (!sessionData) {
return false;
}
const session = this.deserializeSession(sessionData);
session.lastActivity = new Date();
await this.redisService.setex(sessionKey, this.SESSION_TIMEOUT, this.serializeSession(session));
// 同时刷新用户会话映射的过期时间
const userSessionKey = `${this.USER_SESSION_PREFIX}${session.userId}`;
await this.redisService.expire(userSessionKey, this.SESSION_TIMEOUT);
return true;
} catch (error) {
const err = error as Error;
this.logger.error('刷新会话失败', {
operation: 'refreshSession',
socketId,
error: err.message,
});
return false;
}
}
/**
* 获取会话统计信息
*
* 功能描述:
* 获取当前系统中的会话统计信息,包括总会话数和地图分布
*
* @returns Promise<SessionStats> 会话统计信息
*/
async getSessionStats(): Promise<SessionStats> {
try {
// 获取所有地图的玩家列表
const mapIds = ['novice_village', 'tavern', 'market']; // TODO: 从配置获取
const mapDistribution: Record<string, number> = {};
let totalSessions = 0;
for (const mapId of mapIds) {
const mapKey = `${this.MAP_PLAYERS_PREFIX}${mapId}`;
const players = await this.redisService.smembers(mapKey);
mapDistribution[mapId] = players.length;
totalSessions += players.length;
}
this.logger.debug('获取会话统计信息', {
operation: 'getSessionStats',
totalSessions,
mapDistribution,
});
return {
totalSessions,
mapDistribution,
};
} catch (error) {
const err = error as Error;
this.logger.error('获取会话统计失败', {
operation: 'getSessionStats',
error: err.message,
});
return {
totalSessions: 0,
mapDistribution: {},
};
}
}
/**
* 获取所有活跃会话
*
* 功能描述:
* 获取指定地图中所有活跃会话的详细信息
*
* @param mapId 地图ID可选不传则获取所有地图
* @returns Promise<GameSession[]> 会话列表
*/
async getAllSessions(mapId?: string): Promise<GameSession[]> {
try {
const sessions: GameSession[] = [];
if (mapId) {
// 获取指定地图的会话
const socketIds = await this.getSocketsInMap(mapId);
for (const socketId of socketIds) {
const session = await this.getSession(socketId);
if (session) {
sessions.push(session);
}
}
} else {
// 获取所有地图的会话
const mapIds = ['novice_village', 'tavern', 'market']; // TODO: 从配置获取
for (const map of mapIds) {
const socketIds = await this.getSocketsInMap(map);
for (const socketId of socketIds) {
const session = await this.getSession(socketId);
if (session) {
sessions.push(session);
}
}
}
}
return sessions;
} catch (error) {
const err = error as Error;
this.logger.error('获取所有会话失败', {
operation: 'getAllSessions',
mapId,
error: err.message,
});
return [];
}
}
}