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

644 lines
17 KiB
TypeScript

/**
* 位置管理业务服务
*
* 功能描述:
* - 管理用户位置数据的业务逻辑
* - 处理位置验证、过滤和转换
* - 提供位置查询和统计功能
* - 实现位置相关的业务规则
*
* 职责分离:
* - 位置业务:专注于位置数据的业务逻辑处理
* - 数据验证:位置数据的格式验证和业务规则验证
* - 查询服务:提供灵活的位置数据查询接口
* - 统计分析:位置数据的统计和分析功能
*
* 技术实现:
* - 位置验证:多层次的位置数据验证机制
* - 性能优化:高效的位置查询和缓存策略
* - 数据转换:位置数据格式的标准化处理
* - 业务规则:复杂的位置相关业务逻辑实现
*
* 最近修改:
* - 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<string, number>;
/** 位置更新频率(每分钟) */
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<PositionQueryResponse> {
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<PositionStatsResponse> {
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<string, number> = {};
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<PositionHistory[]> {
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<PositionValidationResult> {
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<Position[]> {
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<Position[]> {
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('分页参数无效');
}
}
}
}