Files
whale-town-end/src/business/location_broadcast/services/location_session.service.ts
moyin c31cbe559d feat:实现位置广播系统
- 添加位置广播核心控制器和服务
- 实现健康检查和位置同步功能
- 添加WebSocket实时位置更新支持
- 完善位置广播的测试覆盖
2026-01-08 23:05:52 +08:00

602 lines
18 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.
/**
* 位置广播会话管理服务
*
* 功能描述:
* - 管理游戏会话的创建、配置和生命周期
* - 处理会话权限验证和用户管理
* - 提供会话查询和统计功能
* - 实现会话相关的业务规则
*
* 职责分离:
* - 会话管理:专注于会话的创建、配置和状态管理
* - 权限控制:处理会话访问权限和用户权限验证
* - 业务规则:实现会话相关的复杂业务逻辑
* - 数据查询:提供会话信息的查询和统计接口
*
* 技术实现:
* - 会话配置:支持灵活的会话参数配置
* - 权限验证:多层次的权限验证机制
* - 状态管理:会话状态的实时跟踪和更新
* - 性能优化:高效的会话查询和缓存策略
*
* 最近修改:
* - 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
});
}
}