/** * 位置管理业务服务 * * 功能描述: * - 管理用户位置数据的业务逻辑 * - 处理位置验证、过滤和转换 * - 提供位置查询和统计功能 * - 实现位置相关的业务规则 * * 职责分离: * - 位置业务:专注于位置数据的业务逻辑处理 * - 数据验证:位置数据的格式验证和业务规则验证 * - 查询服务:提供灵活的位置数据查询接口 * - 统计分析:位置数据的统计和分析功能 * * 技术实现: * - 位置验证:多层次的位置数据验证机制 * - 性能优化:高效的位置查询和缓存策略 * - 数据转换:位置数据格式的标准化处理 * - 业务规则:复杂的位置相关业务逻辑实现 * * 最近修改: * - 2026-01-08: 代码重构 - 提取魔法数字为常量,优化代码质量 (修改者: moyin) * * @author moyin * @version 1.2.0 * @since 2026-01-08 * @lastModified 2026-01-08 */ import { Injectable, Inject, Logger, BadRequestException, NotFoundException } from '@nestjs/common'; import { Position, PositionHistory } from '../../../core/location_broadcast_core/position.interface'; /** * 位置查询请求DTO */ export interface PositionQueryRequest { /** 用户ID列表 */ userIds?: string[]; /** 地图ID */ mapId?: string; /** 会话ID */ sessionId?: string; /** 查询范围(中心点和半径) */ range?: { centerX: number; centerY: number; radius: number; }; /** 时间范围 */ timeRange?: { startTime: number; endTime: number; }; /** 是否包含离线用户 */ includeOffline?: boolean; /** 分页参数 */ pagination?: { offset: number; limit: number; }; } /** * 位置查询响应DTO */ export interface PositionQueryResponse { /** 位置列表 */ positions: Position[]; /** 总数 */ total: number; /** 查询时间戳 */ timestamp: number; } /** * 位置统计请求DTO */ export interface PositionStatsRequest { /** 地图ID */ mapId?: string; /** 会话ID */ sessionId?: string; /** 时间范围 */ timeRange?: { startTime: number; endTime: number; }; } /** * 位置统计响应DTO */ export interface PositionStatsResponse { /** 总用户数 */ totalUsers: number; /** 在线用户数 */ onlineUsers: number; /** 活跃地图数 */ activeMaps: number; /** 地图用户分布 */ mapDistribution: Record; /** 位置更新频率(每分钟) */ updateFrequency: number; /** 统计时间戳 */ timestamp: number; } /** * 位置历史查询请求DTO */ export interface PositionHistoryRequest { /** 用户ID */ userId: string; /** 时间范围 */ timeRange?: { startTime: number; endTime: number; }; /** 地图ID过滤 */ mapId?: string; /** 最大记录数 */ limit?: number; } /** * 位置验证结果DTO */ export interface PositionValidationResult { /** 是否有效 */ isValid: boolean; /** 错误信息 */ errors: string[]; /** 警告信息 */ warnings: string[]; /** 修正后的位置(如果有) */ correctedPosition?: Position; } @Injectable() export class LocationPositionService { private readonly logger = new Logger(LocationPositionService.name); /** 坐标最大值 */ /** 坐标最大值 */ private static readonly MAX_COORDINATE = 999999; /** 坐标最小值 */ private static readonly MIN_COORDINATE = -999999; /** 默认查询限制 */ private static readonly DEFAULT_QUERY_LIMIT = 100; /** 时间转换常量 */ private static readonly MILLISECONDS_PER_MINUTE = 60 * 1000; /** 位置时间戳最大偏差(毫秒) */ private static readonly MAX_TIMESTAMP_DIFF = 5 * LocationPositionService.MILLISECONDS_PER_MINUTE; /** 地图ID最大长度 */ private static readonly MAX_MAP_ID_LENGTH = 50; /** 用户ID列表最大数量 */ private static readonly MAX_USER_IDS_COUNT = 1000; /** 查询半径最大值 */ private static readonly MAX_QUERY_RADIUS = 10000; /** 分页限制最大值 */ private static readonly MAX_PAGINATION_LIMIT = 1000; constructor( @Inject('ILocationBroadcastCore') private readonly locationBroadcastCore: any, @Inject('IUserPositionCore') private readonly userPositionCore: any, ) {} /** * 查询位置信息 * * 业务逻辑: * 1. 验证查询参数 * 2. 根据条件构建查询策略 * 3. 执行位置数据查询 * 4. 过滤和排序结果 * 5. 返回格式化的查询结果 * * @param request 位置查询请求 * @returns 位置查询响应 */ async queryPositions(request: PositionQueryRequest): Promise { const startTime = Date.now(); this.logger.log('查询位置信息', { operation: 'queryPositions', userIds: request.userIds?.length, mapId: request.mapId, sessionId: request.sessionId, hasRange: !!request.range, timestamp: new Date().toISOString() }); try { // 1. 验证查询参数 this.validatePositionQuery(request); let positions: Position[] = []; // 2. 根据查询条件执行不同的查询策略 if (request.sessionId) { // 按会话查询 positions = await this.locationBroadcastCore.getSessionPositions(request.sessionId); } else if (request.mapId) { // 按地图查询 positions = await this.locationBroadcastCore.getMapPositions(request.mapId); } else if (request.userIds && request.userIds.length > 0) { // 按用户ID列表查询 positions = await this.queryPositionsByUserIds(request.userIds); } else { // 全量查询(需要谨慎使用) this.logger.warn('执行全量位置查询', { request }); positions = []; } // 3. 应用过滤条件 positions = this.applyPositionFilters(positions, request); // 4. 应用分页 const total = positions.length; if (request.pagination) { const { offset, limit } = request.pagination; positions = positions.slice(offset, offset + limit); } const duration = Date.now() - startTime; this.logger.log('位置查询完成', { operation: 'queryPositions', resultCount: positions.length, total, duration, timestamp: new Date().toISOString() }); return { positions, total, timestamp: Date.now() }; } catch (error) { const duration = Date.now() - startTime; this.logger.error('位置查询失败', { operation: 'queryPositions', request, error: error instanceof Error ? error.message : String(error), duration, timestamp: new Date().toISOString() }, error instanceof Error ? error.stack : undefined); throw error; } } /** * 获取位置统计信息 * * @param request 统计请求 * @returns 统计结果 */ async getPositionStats(request: PositionStatsRequest): Promise { try { let positions: Position[] = []; // 根据条件获取位置数据 if (request.sessionId) { positions = await this.locationBroadcastCore.getSessionPositions(request.sessionId); } else if (request.mapId) { positions = await this.locationBroadcastCore.getMapPositions(request.mapId); } // 应用时间过滤 if (request.timeRange) { positions = positions.filter(pos => pos.timestamp >= request.timeRange!.startTime && pos.timestamp <= request.timeRange!.endTime ); } // 计算统计信息 const totalUsers = positions.length; const onlineUsers = totalUsers; // 缓存中的都是在线用户 // 统计地图分布 const mapDistribution: Record = {}; positions.forEach(pos => { mapDistribution[pos.mapId] = (mapDistribution[pos.mapId] || 0) + 1; }); const activeMaps = Object.keys(mapDistribution).length; // 计算更新频率(简化计算) const updateFrequency = positions.length > 0 ? positions.length / Math.max(1, (Date.now() - Math.min(...positions.map(p => p.timestamp))) / LocationPositionService.MILLISECONDS_PER_MINUTE) : 0; return { totalUsers, onlineUsers, activeMaps, mapDistribution, updateFrequency, timestamp: Date.now() }; } catch (error) { this.logger.error('获取位置统计失败', { operation: 'getPositionStats', request, error: error instanceof Error ? error.message : String(error) }); throw error; } } /** * 获取用户位置历史 * * @param request 历史查询请求 * @returns 位置历史列表 */ async getPositionHistory(request: PositionHistoryRequest): Promise { try { this.logger.log('查询用户位置历史', { operation: 'getPositionHistory', userId: request.userId, mapId: request.mapId, limit: request.limit, timestamp: new Date().toISOString() }); // 从核心服务获取位置历史 const history = await this.userPositionCore.getPositionHistory( request.userId, request.limit || LocationPositionService.DEFAULT_QUERY_LIMIT ); // 应用过滤条件 let filteredHistory = history; if (request.timeRange) { filteredHistory = filteredHistory.filter(h => h.timestamp >= request.timeRange!.startTime && h.timestamp <= request.timeRange!.endTime ); } if (request.mapId) { filteredHistory = filteredHistory.filter(h => h.mapId === request.mapId); } return filteredHistory; } catch (error) { this.logger.error('获取位置历史失败', { operation: 'getPositionHistory', request, error: error instanceof Error ? error.message : String(error) }); throw error; } } /** * 验证位置数据 * * @param position 位置数据 * @returns 验证结果 */ async validatePosition(position: Position): Promise { const errors: string[] = []; const warnings: string[] = []; try { // 1. 基础数据验证 if (!position.userId) { errors.push('用户ID不能为空'); } if (!position.mapId) { errors.push('地图ID不能为空'); } if (typeof position.x !== 'number' || typeof position.y !== 'number') { errors.push('坐标必须是数字'); } if (!isFinite(position.x) || !isFinite(position.y)) { errors.push('坐标必须是有效的数字'); } // 2. 坐标范围验证 if (position.x > LocationPositionService.MAX_COORDINATE || position.x < LocationPositionService.MIN_COORDINATE || position.y > LocationPositionService.MAX_COORDINATE || position.y < LocationPositionService.MIN_COORDINATE) { errors.push('坐标超出允许范围'); } // 3. 时间戳验证 if (position.timestamp) { const now = Date.now(); const timeDiff = Math.abs(now - position.timestamp); if (timeDiff > LocationPositionService.MAX_TIMESTAMP_DIFF) { warnings.push('位置时间戳与当前时间差异较大'); } } // 4. 地图ID格式验证 if (position.mapId && position.mapId.length > 50) { errors.push('地图ID长度不能超过50个字符'); } // 5. 元数据验证 if (position.metadata) { try { JSON.stringify(position.metadata); } catch { errors.push('位置元数据格式无效'); } } return { isValid: errors.length === 0, errors, warnings }; } catch (error) { this.logger.error('位置验证失败', { operation: 'validatePosition', position, error: error instanceof Error ? error.message : String(error) }); return { isValid: false, errors: ['位置验证过程中发生错误'], warnings }; } } /** * 计算两个位置之间的距离 * * @param pos1 位置1 * @param pos2 位置2 * @returns 距离(像素单位) */ calculateDistance(pos1: Position, pos2: Position): number { if (pos1.mapId !== pos2.mapId) { return Infinity; // 不同地图距离为无穷大 } const dx = pos1.x - pos2.x; const dy = pos1.y - pos2.y; return Math.sqrt(dx * dx + dy * dy); } /** * 获取指定范围内的用户 * * @param centerPosition 中心位置 * @param radius 半径 * @returns 范围内的位置列表 */ async getUsersInRange(centerPosition: Position, radius: number): Promise { try { // 获取同地图的所有用户 const mapPositions = await this.locationBroadcastCore.getMapPositions(centerPosition.mapId); // 过滤范围内的用户 return mapPositions.filter(pos => { if (pos.userId === centerPosition.userId) { return false; // 排除自己 } const distance = this.calculateDistance(centerPosition, pos); return distance <= radius; }); } catch (error) { this.logger.error('获取范围内用户失败', { operation: 'getUsersInRange', centerPosition, radius, error: error instanceof Error ? error.message : String(error) }); return []; } } /** * 批量更新用户位置 * * @param positions 位置列表 * @returns 更新结果 */ async batchUpdatePositions(positions: Position[]): Promise<{ success: number; failed: number }> { let success = 0; let failed = 0; for (const position of positions) { try { // 验证位置 const validation = await this.validatePosition(position); if (!validation.isValid) { failed++; continue; } // 更新位置 await this.locationBroadcastCore.setUserPosition(position.userId, position); success++; } catch (error) { this.logger.warn('批量更新位置失败', { userId: position.userId, error: error instanceof Error ? error.message : String(error) }); failed++; } } this.logger.log('批量位置更新完成', { operation: 'batchUpdatePositions', total: positions.length, success, failed }); return { success, failed }; } /** * 根据用户ID列表查询位置 * * @param userIds 用户ID列表 * @returns 位置列表 * @private */ private async queryPositionsByUserIds(userIds: string[]): Promise { const positions: Position[] = []; for (const userId of userIds) { try { const position = await this.locationBroadcastCore.getUserPosition(userId); if (position) { positions.push(position); } } catch (error) { this.logger.warn('获取用户位置失败', { userId, error: error instanceof Error ? error.message : String(error) }); } } return positions; } /** * 应用位置过滤条件 * * @param positions 原始位置列表 * @param request 查询请求 * @returns 过滤后的位置列表 * @private */ private applyPositionFilters(positions: Position[], request: PositionQueryRequest): Position[] { let filtered = positions; // 时间范围过滤 if (request.timeRange) { filtered = filtered.filter(pos => pos.timestamp >= request.timeRange!.startTime && pos.timestamp <= request.timeRange!.endTime ); } // 地图过滤 if (request.mapId) { filtered = filtered.filter(pos => pos.mapId === request.mapId); } // 用户ID过滤 if (request.userIds && request.userIds.length > 0) { const userIdSet = new Set(request.userIds); filtered = filtered.filter(pos => userIdSet.has(pos.userId)); } // 范围过滤 if (request.range) { const { centerX, centerY, radius } = request.range; filtered = filtered.filter(pos => { const distance = Math.sqrt( Math.pow(pos.x - centerX, 2) + Math.pow(pos.y - centerY, 2) ); return distance <= radius; }); } return filtered; } /** * 验证位置查询参数 * * @param request 查询请求 * @private */ private validatePositionQuery(request: PositionQueryRequest): void { if (request.userIds && request.userIds.length > 1000) { throw new BadRequestException('用户ID列表不能超过1000个'); } if (request.range) { const { centerX, centerY, radius } = request.range; if (typeof centerX !== 'number' || typeof centerY !== 'number' || typeof radius !== 'number') { throw new BadRequestException('范围查询参数必须是数字'); } if (radius < 0 || radius > 10000) { throw new BadRequestException('查询半径必须在0-10000之间'); } } if (request.pagination) { const { offset, limit } = request.pagination; if (offset < 0 || limit < 1 || limit > 1000) { throw new BadRequestException('分页参数无效'); } } } }