forked from datawhale/whale-town-end
602 lines
18 KiB
TypeScript
602 lines
18 KiB
TypeScript
/**
|
||
* 位置广播会话管理服务
|
||
*
|
||
* 功能描述:
|
||
* - 管理游戏会话的创建、配置和生命周期
|
||
* - 处理会话权限验证和用户管理
|
||
* - 提供会话查询和统计功能
|
||
* - 实现会话相关的业务规则
|
||
*
|
||
* 职责分离:
|
||
* - 会话管理:专注于会话的创建、配置和状态管理
|
||
* - 权限控制:处理会话访问权限和用户权限验证
|
||
* - 业务规则:实现会话相关的复杂业务逻辑
|
||
* - 数据查询:提供会话信息的查询和统计接口
|
||
*
|
||
* 技术实现:
|
||
* - 会话配置:支持灵活的会话参数配置
|
||
* - 权限验证:多层次的权限验证机制
|
||
* - 状态管理:会话状态的实时跟踪和更新
|
||
* - 性能优化:高效的会话查询和缓存策略
|
||
*
|
||
* 最近修改:
|
||
* - 2026-01-08: 代码重构 - 提取魔法数字为常量,优化代码质量 (修改者: moyin)
|
||
*
|
||
* @author moyin
|
||
* @version 1.1.0
|
||
* @since 2026-01-08
|
||
* @lastModified 2026-01-08
|
||
*/
|
||
|
||
import { Injectable, Inject, Logger, BadRequestException, NotFoundException, ForbiddenException, ConflictException } from '@nestjs/common';
|
||
import { GameSession, SessionUser, SessionStatus, SessionConfig } from '../../../core/location_broadcast_core/session.interface';
|
||
|
||
/**
|
||
* 创建会话请求DTO
|
||
*/
|
||
export interface CreateSessionRequest {
|
||
/** 会话ID */
|
||
sessionId: string;
|
||
/** 创建者用户ID */
|
||
creatorId: string;
|
||
/** 会话名称 */
|
||
name?: string;
|
||
/** 会话描述 */
|
||
description?: string;
|
||
/** 最大用户数 */
|
||
maxUsers?: number;
|
||
/** 是否允许观察者 */
|
||
allowObservers?: boolean;
|
||
/** 会话密码 */
|
||
password?: string;
|
||
/** 地图限制 */
|
||
allowedMaps?: string[];
|
||
/** 广播范围 */
|
||
broadcastRange?: number;
|
||
/** 扩展配置 */
|
||
metadata?: Record<string, any>;
|
||
}
|
||
|
||
/**
|
||
* 会话配置DTO
|
||
*/
|
||
export interface SessionConfigDTO {
|
||
/** 最大用户数 */
|
||
maxUsers: number;
|
||
/** 是否允许观察者 */
|
||
allowObservers: boolean;
|
||
/** 会话密码 */
|
||
password?: string;
|
||
/** 地图限制 */
|
||
allowedMaps?: string[];
|
||
/** 广播范围 */
|
||
broadcastRange?: number;
|
||
/** 是否公开 */
|
||
isPublic: boolean;
|
||
/** 自动清理时间(分钟) */
|
||
autoCleanupMinutes?: number;
|
||
}
|
||
|
||
/**
|
||
* 会话查询条件DTO
|
||
*/
|
||
export interface SessionQueryRequest {
|
||
/** 会话状态过滤 */
|
||
status?: SessionStatus;
|
||
/** 最小用户数 */
|
||
minUsers?: number;
|
||
/** 最大用户数 */
|
||
maxUsers?: number;
|
||
/** 是否只显示公开会话 */
|
||
publicOnly?: boolean;
|
||
/** 创建者ID */
|
||
creatorId?: string;
|
||
/** 分页偏移 */
|
||
offset?: number;
|
||
/** 分页大小 */
|
||
limit?: number;
|
||
}
|
||
|
||
/**
|
||
* 会话列表响应DTO
|
||
*/
|
||
export interface SessionListResponse {
|
||
/** 会话列表 */
|
||
sessions: GameSession[];
|
||
/** 总数 */
|
||
total: number;
|
||
/** 当前页 */
|
||
page: number;
|
||
/** 页大小 */
|
||
pageSize: number;
|
||
}
|
||
|
||
/**
|
||
* 会话详情响应DTO
|
||
*/
|
||
export interface SessionDetailResponse {
|
||
/** 会话信息 */
|
||
session: GameSession;
|
||
/** 用户列表 */
|
||
users: SessionUser[];
|
||
/** 在线用户数 */
|
||
onlineCount: number;
|
||
/** 活跃地图 */
|
||
activeMaps: string[];
|
||
}
|
||
|
||
@Injectable()
|
||
export class LocationSessionService {
|
||
private readonly logger = new Logger(LocationSessionService.name);
|
||
|
||
/** 默认最大用户数 */
|
||
private static readonly DEFAULT_MAX_USERS = 100;
|
||
/** 默认广播范围 */
|
||
private static readonly DEFAULT_BROADCAST_RANGE = 1000;
|
||
/** 默认自动清理时间(分钟) */
|
||
private static readonly DEFAULT_AUTO_CLEANUP_MINUTES = 60;
|
||
/** 默认超时时间(秒) */
|
||
private static readonly DEFAULT_TIMEOUT_SECONDS = 3600;
|
||
/** 会话ID最大长度 */
|
||
private static readonly MAX_SESSION_ID_LENGTH = 100;
|
||
/** 最大用户数限制 */
|
||
private static readonly MAX_USERS_LIMIT = 1000;
|
||
/** 最小用户数限制 */
|
||
private static readonly MIN_USERS_LIMIT = 1;
|
||
/** 广播范围最大值 */
|
||
private static readonly MAX_BROADCAST_RANGE = 10000;
|
||
/** 默认分页大小 */
|
||
private static readonly DEFAULT_PAGE_SIZE = 10;
|
||
/** 自动清理时间最小值(分钟) */
|
||
private static readonly MIN_AUTO_CLEANUP_MINUTES = 1;
|
||
/** 自动清理时间最大值(分钟) */
|
||
private static readonly MAX_AUTO_CLEANUP_MINUTES = 1440;
|
||
/** 时间转换常量 */
|
||
private static readonly MILLISECONDS_PER_MINUTE = 60 * 1000;
|
||
private static readonly SECONDS_PER_MINUTE = 60;
|
||
|
||
constructor(
|
||
@Inject('ILocationBroadcastCore')
|
||
private readonly locationBroadcastCore: any,
|
||
) {}
|
||
|
||
/**
|
||
* 创建新会话
|
||
*
|
||
* 业务逻辑:
|
||
* 1. 验证会话ID的唯一性
|
||
* 2. 验证创建者权限
|
||
* 3. 构建会话配置
|
||
* 4. 创建会话并设置初始状态
|
||
* 5. 返回创建的会话信息
|
||
*
|
||
* @param request 创建会话请求
|
||
* @returns 创建的会话信息
|
||
*/
|
||
async createSession(request: CreateSessionRequest): Promise<GameSession> {
|
||
const startTime = Date.now();
|
||
|
||
this.logger.log('创建新会话', {
|
||
operation: 'createSession',
|
||
sessionId: request.sessionId,
|
||
creatorId: request.creatorId,
|
||
maxUsers: request.maxUsers,
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
|
||
try {
|
||
// 1. 验证请求参数
|
||
this.validateCreateSessionRequest(request);
|
||
|
||
// 2. 检查会话ID是否已存在
|
||
const existingUsers = await this.locationBroadcastCore.getSessionUsers(request.sessionId);
|
||
if (existingUsers.length > 0) {
|
||
throw new ConflictException('会话ID已存在');
|
||
}
|
||
|
||
// 3. 构建会话配置
|
||
const configDTO: SessionConfigDTO = {
|
||
maxUsers: request.maxUsers || LocationSessionService.DEFAULT_MAX_USERS,
|
||
allowObservers: request.allowObservers !== false,
|
||
password: request.password,
|
||
allowedMaps: request.allowedMaps,
|
||
broadcastRange: request.broadcastRange || LocationSessionService.DEFAULT_BROADCAST_RANGE,
|
||
isPublic: !request.password,
|
||
autoCleanupMinutes: LocationSessionService.DEFAULT_AUTO_CLEANUP_MINUTES
|
||
};
|
||
|
||
const config: SessionConfig = {
|
||
maxUsers: configDTO.maxUsers,
|
||
timeoutSeconds: (configDTO.autoCleanupMinutes || LocationSessionService.DEFAULT_AUTO_CLEANUP_MINUTES) * LocationSessionService.SECONDS_PER_MINUTE,
|
||
allowObservers: configDTO.allowObservers,
|
||
requirePassword: !!configDTO.password,
|
||
password: configDTO.password,
|
||
mapRestriction: configDTO.allowedMaps,
|
||
broadcastRange: configDTO.broadcastRange
|
||
};
|
||
|
||
// 4. 创建会话对象
|
||
const session: GameSession = {
|
||
sessionId: request.sessionId,
|
||
users: [], // 初始为空
|
||
createdAt: Date.now(),
|
||
lastActivity: Date.now(),
|
||
status: SessionStatus.ACTIVE,
|
||
config,
|
||
metadata: {
|
||
name: request.name || request.sessionId,
|
||
description: request.description,
|
||
creatorId: request.creatorId,
|
||
isPublic: configDTO.isPublic,
|
||
...request.metadata
|
||
}
|
||
};
|
||
|
||
// 5. 这里应该将会话信息保存到持久化存储
|
||
// 目前暂时只在内存中管理,后续可以扩展到Redis或数据库
|
||
|
||
const duration = Date.now() - startTime;
|
||
|
||
this.logger.log('会话创建成功', {
|
||
operation: 'createSession',
|
||
sessionId: request.sessionId,
|
||
creatorId: request.creatorId,
|
||
config: configDTO,
|
||
duration,
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
|
||
return session;
|
||
|
||
} catch (error) {
|
||
const duration = Date.now() - startTime;
|
||
|
||
this.logger.error('会话创建失败', {
|
||
operation: 'createSession',
|
||
sessionId: request.sessionId,
|
||
creatorId: request.creatorId,
|
||
error: error instanceof Error ? error.message : String(error),
|
||
duration,
|
||
timestamp: new Date().toISOString()
|
||
}, error instanceof Error ? error.stack : undefined);
|
||
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取会话详情
|
||
*
|
||
* @param sessionId 会话ID
|
||
* @param requestUserId 请求用户ID(用于权限验证)
|
||
* @returns 会话详情
|
||
*/
|
||
async getSessionDetail(sessionId: string, requestUserId?: string): Promise<SessionDetailResponse> {
|
||
try {
|
||
// 1. 获取会话用户列表
|
||
const users = await this.locationBroadcastCore.getSessionUsers(sessionId);
|
||
|
||
if (users.length === 0) {
|
||
throw new NotFoundException('会话不存在或已结束');
|
||
}
|
||
|
||
// 2. 获取会话位置信息
|
||
const positions = await this.locationBroadcastCore.getSessionPositions(sessionId);
|
||
|
||
// 3. 统计活跃地图
|
||
const activeMaps = [...new Set(positions.map(pos => pos.mapId as string))];
|
||
|
||
// 4. 构建会话信息(这里应该从实际存储中获取)
|
||
const session: GameSession = {
|
||
sessionId,
|
||
users,
|
||
createdAt: Date.now(), // 应该从存储中获取
|
||
lastActivity: Date.now(),
|
||
status: SessionStatus.ACTIVE,
|
||
config: {
|
||
maxUsers: LocationSessionService.DEFAULT_MAX_USERS,
|
||
timeoutSeconds: LocationSessionService.DEFAULT_TIMEOUT_SECONDS,
|
||
allowObservers: true,
|
||
requirePassword: false,
|
||
broadcastRange: LocationSessionService.DEFAULT_BROADCAST_RANGE
|
||
},
|
||
metadata: {}
|
||
};
|
||
|
||
// 5. 统计在线用户
|
||
const onlineCount = users.filter(user => user.status === 'online').length;
|
||
|
||
return {
|
||
session,
|
||
users,
|
||
onlineCount,
|
||
activeMaps: activeMaps as string[]
|
||
};
|
||
|
||
} catch (error) {
|
||
this.logger.error('获取会话详情失败', {
|
||
operation: 'getSessionDetail',
|
||
sessionId,
|
||
requestUserId,
|
||
error: error instanceof Error ? error.message : String(error)
|
||
});
|
||
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 查询会话列表
|
||
*
|
||
* @param query 查询条件
|
||
* @returns 会话列表
|
||
*/
|
||
async querySessions(query: SessionQueryRequest): Promise<SessionListResponse> {
|
||
try {
|
||
// 这里应该实现实际的会话查询逻辑
|
||
// 目前返回空列表,后续需要实现持久化存储
|
||
|
||
this.logger.log('查询会话列表', {
|
||
operation: 'querySessions',
|
||
query,
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
|
||
return {
|
||
sessions: [],
|
||
total: 0,
|
||
page: Math.floor((query.offset || 0) / (query.limit || LocationSessionService.DEFAULT_PAGE_SIZE)) + 1,
|
||
pageSize: query.limit || LocationSessionService.DEFAULT_PAGE_SIZE
|
||
};
|
||
|
||
} catch (error) {
|
||
this.logger.error('查询会话列表失败', {
|
||
operation: 'querySessions',
|
||
query,
|
||
error: error instanceof Error ? error.message : String(error)
|
||
});
|
||
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新会话配置
|
||
*
|
||
* @param sessionId 会话ID
|
||
* @param config 新配置
|
||
* @param operatorId 操作者ID
|
||
* @returns 更新后的会话信息
|
||
*/
|
||
async updateSessionConfig(sessionId: string, config: Partial<SessionConfigDTO>, operatorId: string): Promise<GameSession> {
|
||
try {
|
||
// 1. 验证操作权限
|
||
await this.validateSessionOperatorPermission(sessionId, operatorId);
|
||
|
||
// 2. 验证配置参数
|
||
this.validateSessionConfig(config);
|
||
|
||
// 3. 这里应该更新持久化存储中的会话配置
|
||
// 目前暂时跳过实际更新逻辑
|
||
|
||
// 4. 获取更新后的会话信息
|
||
const sessionDetail = await this.getSessionDetail(sessionId, operatorId);
|
||
|
||
this.logger.log('会话配置更新成功', {
|
||
operation: 'updateSessionConfig',
|
||
sessionId,
|
||
operatorId,
|
||
config,
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
|
||
return sessionDetail.session;
|
||
|
||
} catch (error) {
|
||
this.logger.error('会话配置更新失败', {
|
||
operation: 'updateSessionConfig',
|
||
sessionId,
|
||
operatorId,
|
||
config,
|
||
error: error instanceof Error ? error.message : String(error)
|
||
});
|
||
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 结束会话
|
||
*
|
||
* @param sessionId 会话ID
|
||
* @param operatorId 操作者ID
|
||
* @param reason 结束原因
|
||
* @returns 操作是否成功
|
||
*/
|
||
async endSession(sessionId: string, operatorId: string, reason: string = 'manual_end'): Promise<boolean> {
|
||
try {
|
||
// 1. 验证操作权限
|
||
await this.validateSessionOperatorPermission(sessionId, operatorId);
|
||
|
||
// 2. 获取会话中的所有用户
|
||
const users = await this.locationBroadcastCore.getSessionUsers(sessionId);
|
||
|
||
// 3. 移除所有用户
|
||
for (const user of users) {
|
||
try {
|
||
await this.locationBroadcastCore.removeUserFromSession(sessionId, user.userId);
|
||
} catch (error) {
|
||
this.logger.warn('移除用户失败', {
|
||
sessionId,
|
||
userId: user.userId,
|
||
error: error instanceof Error ? error.message : String(error)
|
||
});
|
||
}
|
||
}
|
||
|
||
// 4. 清理空会话
|
||
await this.locationBroadcastCore.cleanupEmptySession(sessionId);
|
||
|
||
this.logger.log('会话结束成功', {
|
||
operation: 'endSession',
|
||
sessionId,
|
||
operatorId,
|
||
reason,
|
||
userCount: users.length,
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
|
||
return true;
|
||
|
||
} catch (error) {
|
||
this.logger.error('会话结束失败', {
|
||
operation: 'endSession',
|
||
sessionId,
|
||
operatorId,
|
||
reason,
|
||
error: error instanceof Error ? error.message : String(error)
|
||
});
|
||
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 验证会话密码
|
||
*
|
||
* @param sessionId 会话ID
|
||
* @param password 密码
|
||
* @returns 验证是否成功
|
||
*/
|
||
async validateSessionPassword(sessionId: string, password: string): Promise<boolean> {
|
||
try {
|
||
// 这里应该从持久化存储中获取会话配置
|
||
// 目前暂时返回true,表示验证通过
|
||
|
||
this.logger.debug('验证会话密码', {
|
||
operation: 'validateSessionPassword',
|
||
sessionId,
|
||
hasPassword: !!password
|
||
});
|
||
|
||
return true;
|
||
|
||
} catch (error) {
|
||
this.logger.error('会话密码验证失败', {
|
||
operation: 'validateSessionPassword',
|
||
sessionId,
|
||
error: error instanceof Error ? error.message : String(error)
|
||
});
|
||
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查用户是否可以加入会话
|
||
*
|
||
* @param sessionId 会话ID
|
||
* @param userId 用户ID
|
||
* @returns 是否可以加入
|
||
*/
|
||
async canUserJoinSession(sessionId: string, userId: string): Promise<{ canJoin: boolean; reason?: string }> {
|
||
try {
|
||
// 1. 获取会话信息
|
||
const sessionDetail = await this.getSessionDetail(sessionId);
|
||
|
||
// 2. 检查会话状态
|
||
if (sessionDetail.session.status !== SessionStatus.ACTIVE) {
|
||
return { canJoin: false, reason: '会话已结束或暂停' };
|
||
}
|
||
|
||
// 3. 检查用户数量限制
|
||
if (sessionDetail.users.length >= sessionDetail.session.config.maxUsers) {
|
||
return { canJoin: false, reason: '会话已满' };
|
||
}
|
||
|
||
// 4. 检查用户是否已在会话中
|
||
const existingUser = sessionDetail.users.find(user => user.userId === userId);
|
||
if (existingUser) {
|
||
return { canJoin: false, reason: '用户已在会话中' };
|
||
}
|
||
|
||
return { canJoin: true };
|
||
|
||
} catch (error) {
|
||
this.logger.error('检查用户加入权限失败', {
|
||
operation: 'canUserJoinSession',
|
||
sessionId,
|
||
userId,
|
||
error: error instanceof Error ? error.message : String(error)
|
||
});
|
||
|
||
return { canJoin: false, reason: '权限检查失败' };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 验证创建会话请求
|
||
*
|
||
* @param request 创建会话请求
|
||
* @private
|
||
*/
|
||
private validateCreateSessionRequest(request: CreateSessionRequest): void {
|
||
if (!request.sessionId) {
|
||
throw new BadRequestException('会话ID不能为空');
|
||
}
|
||
|
||
if (!request.creatorId) {
|
||
throw new BadRequestException('创建者ID不能为空');
|
||
}
|
||
|
||
if (request.sessionId.length > LocationSessionService.MAX_SESSION_ID_LENGTH) {
|
||
throw new BadRequestException(`会话ID长度不能超过${LocationSessionService.MAX_SESSION_ID_LENGTH}个字符`);
|
||
}
|
||
|
||
if (request.maxUsers !== undefined && (request.maxUsers < LocationSessionService.MIN_USERS_LIMIT || request.maxUsers > LocationSessionService.MAX_USERS_LIMIT)) {
|
||
throw new BadRequestException(`最大用户数必须在${LocationSessionService.MIN_USERS_LIMIT}-${LocationSessionService.MAX_USERS_LIMIT}之间`);
|
||
}
|
||
|
||
if (request.broadcastRange !== undefined && (request.broadcastRange < 0 || request.broadcastRange > LocationSessionService.MAX_BROADCAST_RANGE)) {
|
||
throw new BadRequestException(`广播范围必须在0-${LocationSessionService.MAX_BROADCAST_RANGE}之间`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 验证会话配置
|
||
*
|
||
* @param config 会话配置
|
||
* @private
|
||
*/
|
||
private validateSessionConfig(config: Partial<SessionConfigDTO>): void {
|
||
if (config.maxUsers !== undefined && (config.maxUsers < LocationSessionService.MIN_USERS_LIMIT || config.maxUsers > LocationSessionService.MAX_USERS_LIMIT)) {
|
||
throw new BadRequestException(`最大用户数必须在${LocationSessionService.MIN_USERS_LIMIT}-${LocationSessionService.MAX_USERS_LIMIT}之间`);
|
||
}
|
||
|
||
if (config.broadcastRange !== undefined && (config.broadcastRange < 0 || config.broadcastRange > LocationSessionService.MAX_BROADCAST_RANGE)) {
|
||
throw new BadRequestException(`广播范围必须在0-${LocationSessionService.MAX_BROADCAST_RANGE}之间`);
|
||
}
|
||
|
||
if (config.autoCleanupMinutes !== undefined && (config.autoCleanupMinutes < LocationSessionService.MIN_AUTO_CLEANUP_MINUTES || config.autoCleanupMinutes > LocationSessionService.MAX_AUTO_CLEANUP_MINUTES)) {
|
||
throw new BadRequestException(`自动清理时间必须在${LocationSessionService.MIN_AUTO_CLEANUP_MINUTES}-${LocationSessionService.MAX_AUTO_CLEANUP_MINUTES}分钟之间`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 验证会话操作权限
|
||
*
|
||
* @param sessionId 会话ID
|
||
* @param operatorId 操作者ID
|
||
* @private
|
||
*/
|
||
private async validateSessionOperatorPermission(sessionId: string, operatorId: string): Promise<void> {
|
||
// 这里应该实现实际的权限验证逻辑
|
||
// 比如检查操作者是否是会话创建者或管理员
|
||
|
||
// 目前暂时跳过权限验证
|
||
this.logger.debug('验证会话操作权限', {
|
||
sessionId,
|
||
operatorId
|
||
});
|
||
}
|
||
} |