Files
whale-town-end/src/business/zulip/services/session_manager.service.ts
moyin efac782243 style(zulip): 优化zulip业务模块代码规范
范围:src/business/zulip/
- 统一命名规范和注释格式
- 完善JSDoc注释和参数说明
- 优化代码结构和缩进
- 清理未使用的导入和变量
- 更新修改记录和版本信息
2026-01-12 19:42:38 +08:00

1029 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 会话管理服务
*
* 功能描述:
* - 维护WebSocket连接ID与Zulip队列ID的映射关系
* - 管理玩家位置跟踪和上下文注入
* - 提供空间过滤和会话查询功能
* - 支持会话状态的序列化和反序列化
* - 支持服务重启后的状态恢复
*
* 职责分离:
* - 会话存储管理会话数据在Redis中的存储和检索
* - 位置跟踪:维护玩家在游戏世界中的位置信息
* - 上下文注入根据玩家位置确定消息的目标Stream和Topic
* - 空间过滤根据地图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
*
* 使用场景:
* - 玩家登录时创建会话映射
* - 消息路由时进行上下文注入
* - 消息分发时进行空间过滤
* - 玩家登出时清理会话数据
*
* 最近修改:
* - 2026-01-12: 代码规范优化 - 处理TODO项实现玩家位置确定Topic逻辑从配置获取地图ID列表 (修改者: moyin)
* - 2026-01-07: 代码规范优化 - 完善文件头注释和修改记录 (修改者: moyin)
*
* @author angjustinl
* @version 1.1.0
* @since 2025-12-25
* @lastModified 2026-01-12
*/
import { Injectable, Logger, Inject } from '@nestjs/common';
import { IRedisService } from '../../../core/redis/redis.interface';
import { IZulipConfigService } from '../../../core/zulip_core/zulip_core.interfaces';
import { Internal, Constants } from '../../../core/zulip_core/zulip.interfaces';
// 常量定义
const DEFAULT_MAP_IDS = ['novice_village', 'tavern', 'market'] as const;
const SESSION_TIMEOUT_MINUTES = 30;
const CLEANUP_INTERVAL_MINUTES = 5;
/**
* 游戏会话接口 - 重新导出以保持向后兼容
*/
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';
// 根据玩家位置确定Topic基础实现
// 检查是否靠近交互对象如果没有则使用默认Topic
let topic = 'General';
// 尝试根据位置查找附近的交互对象
if (session.position) {
const nearbyObject = this.configManager.findNearbyObject(
targetMapId,
session.position.x,
session.position.y,
50 // 50像素范围内
);
if (nearbyObject) {
topic = nearbyObject.zulipTopic;
}
}
const context: ContextInfo = {
stream,
topic,
};
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 = this.configManager.getAllMapIds().length > 0
? this.configManager.getAllMapIds()
: DEFAULT_MAP_IDS;
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 = this.configManager.getAllMapIds().length > 0
? this.configManager.getAllMapIds()
: DEFAULT_MAP_IDS;
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 = this.configManager.getAllMapIds().length > 0
? this.configManager.getAllMapIds()
: DEFAULT_MAP_IDS;
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 [];
}
}
}