feat:实现位置广播系统

- 添加位置广播核心控制器和服务
- 实现健康检查和位置同步功能
- 添加WebSocket实时位置更新支持
- 完善位置广播的测试覆盖
This commit is contained in:
moyin
2026-01-08 23:05:52 +08:00
parent 6924416bbd
commit c31cbe559d
27 changed files with 12212 additions and 0 deletions

View File

@@ -0,0 +1,351 @@
/**
* 位置广播HTTP API控制器
*
* 功能描述:
* - 提供位置广播系统的REST API接口
* - 处理HTTP请求和响应格式化
* - 集成JWT认证和权限验证
* - 提供完整的API文档和错误处理
*
* 职责分离:
* - HTTP处理专注于HTTP请求和响应的处理
* - 数据转换:请求参数和响应数据的格式转换
* - 权限验证API访问权限的验证和控制
* - 文档生成Swagger API文档的自动生成
*
* 技术实现:
* - NestJS控制器使用装饰器定义API端点
* - Swagger集成自动生成API文档
* - 数据验证使用DTO进行请求数据验证
* - 异常处理统一的HTTP异常处理机制
*
* 最近修改:
* - 2026-01-08: 功能新增 - 创建位置广播HTTP API控制器
*
* @author moyin
* @version 1.0.0
* @since 2026-01-08
* @lastModified 2026-01-08
*/
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
UseGuards,
HttpStatus,
HttpException,
Logger,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiParam,
ApiQuery,
ApiBearerAuth,
ApiBody,
} from '@nestjs/swagger';
import { JwtAuthGuard } from '../../auth/jwt_auth.guard';
import { CurrentUser } from '../../auth/current_user.decorator';
import { JwtPayload } from '../../../core/login_core/login_core.service';
// 导入业务服务
import {
LocationBroadcastService,
LocationSessionService,
LocationPositionService,
} from '../services';
// 导入DTO
import {
CreateSessionDto,
SessionQueryDto,
PositionQueryDto,
UpdateSessionConfigDto,
} from '../dto/api.dto';
/**
* 位置广播API控制器
*
* 提供以下API端点
* - 会话管理:创建、查询、配置会话
* - 位置管理:查询位置、获取统计信息
* - 用户管理:获取用户状态、清理数据
*/
@ApiTags('位置广播')
@Controller('location-broadcast')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
export class LocationBroadcastController {
private readonly logger = new Logger(LocationBroadcastController.name);
constructor(
private readonly locationBroadcastService: LocationBroadcastService,
private readonly locationSessionService: LocationSessionService,
private readonly locationPositionService: LocationPositionService,
) {}
/**
* 创建新会话
*/
@Post('sessions')
@ApiOperation({
summary: '创建新游戏会话',
description: '创建一个新的位置广播会话,支持自定义配置',
})
@ApiResponse({
status: 201,
description: '会话创建成功',
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
sessionId: { type: 'string', example: 'session_12345' },
message: { type: 'string', example: '会话创建成功' },
},
},
})
@ApiResponse({ status: 400, description: '请求参数错误' })
@ApiResponse({ status: 409, description: '会话ID已存在' })
async createSession(
@Body() createSessionDto: CreateSessionDto,
@CurrentUser() user: JwtPayload,
) {
try {
const result = await this.locationSessionService.createSession({
...createSessionDto,
creatorId: user.sub,
});
return {
success: true,
session: result,
message: '会话创建成功',
};
} catch (error: any) {
this.logger.error('创建会话失败', error);
throw new HttpException(
error.message || '创建会话失败',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
/**
* 查询会话列表
*/
@Get('sessions')
@ApiOperation({
summary: '查询会话列表',
description: '根据条件查询游戏会话列表,支持分页和过滤',
})
@ApiQuery({ name: 'status', required: false, description: '会话状态' })
@ApiQuery({ name: 'limit', required: false, description: '分页大小' })
@ApiQuery({ name: 'offset', required: false, description: '分页偏移' })
@ApiResponse({
status: 200,
description: '查询成功',
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
sessions: { type: 'array', items: { type: 'object' } },
total: { type: 'number', example: 10 },
message: { type: 'string', example: '查询成功' },
},
},
})
async querySessions(
@Query() query: SessionQueryDto,
@CurrentUser() user: JwtPayload,
) {
try {
const result = await this.locationSessionService.querySessions(query as any);
return {
success: true,
...result,
message: '查询成功',
};
} catch (error: any) {
this.logger.error('查询会话失败', error);
throw new HttpException(
error.message || '查询会话失败',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
/**
* 获取会话详情
*/
@Get('sessions/:sessionId')
@ApiOperation({
summary: '获取会话详情',
description: '获取指定会话的详细信息,包括用户列表和位置信息',
})
@ApiParam({ name: 'sessionId', description: '会话ID' })
@ApiResponse({
status: 200,
description: '获取成功',
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
session: { type: 'object' },
users: { type: 'array', items: { type: 'object' } },
message: { type: 'string', example: '获取成功' },
},
},
})
@ApiResponse({ status: 404, description: '会话不存在' })
async getSessionDetail(
@Param('sessionId') sessionId: string,
@CurrentUser() user: JwtPayload,
) {
try {
const result = await this.locationSessionService.getSessionDetail(sessionId);
return {
success: true,
...result,
message: '获取成功',
};
} catch (error: any) {
this.logger.error('获取会话详情失败', error);
throw new HttpException(
error.message || '获取会话详情失败',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
/**
* 查询位置信息
*/
@Get('positions')
@ApiOperation({
summary: '查询位置信息',
description: '根据条件查询用户位置信息,支持范围查询和地图过滤',
})
@ApiQuery({ name: 'mapId', required: false, description: '地图ID' })
@ApiQuery({ name: 'sessionId', required: false, description: '会话ID' })
@ApiQuery({ name: 'limit', required: false, description: '分页大小' })
@ApiResponse({
status: 200,
description: '查询成功',
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
positions: { type: 'array', items: { type: 'object' } },
total: { type: 'number', example: 5 },
message: { type: 'string', example: '查询成功' },
},
},
})
async queryPositions(
@Query() query: PositionQueryDto,
@CurrentUser() user: JwtPayload,
) {
try {
const result = await this.locationPositionService.queryPositions(query as any);
return {
success: true,
...result,
message: '查询成功',
};
} catch (error: any) {
this.logger.error('查询位置失败', error);
throw new HttpException(
error.message || '查询位置失败',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
/**
* 获取位置统计信息
*/
@Get('positions/stats')
@ApiOperation({
summary: '获取位置统计信息',
description: '获取系统位置数据的统计信息,包括用户分布和活跃度',
})
@ApiResponse({
status: 200,
description: '获取成功',
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
stats: { type: 'object' },
message: { type: 'string', example: '获取成功' },
},
},
})
async getPositionStats(@CurrentUser() user: JwtPayload) {
try {
const stats = await this.locationPositionService.getPositionStats({});
return {
success: true,
stats,
message: '获取成功',
};
} catch (error: any) {
this.logger.error('获取位置统计失败', error);
throw new HttpException(
error.message || '获取位置统计失败',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
/**
* 清理用户数据
*/
@Delete('users/:userId/data')
@ApiOperation({
summary: '清理用户数据',
description: '清理指定用户的位置数据和会话信息',
})
@ApiParam({ name: 'userId', description: '用户ID' })
@ApiResponse({
status: 200,
description: '清理成功',
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: '清理成功' },
},
},
})
async cleanupUserData(
@Param('userId') userId: string,
@CurrentUser() user: JwtPayload,
) {
try {
// 只允许用户清理自己的数据,或管理员清理任意用户数据
if (user.sub !== userId && user.role !== 2) {
throw new HttpException('权限不足', HttpStatus.FORBIDDEN);
}
await this.locationBroadcastService.cleanupUserData(userId);
return {
success: true,
message: '清理成功',
};
} catch (error: any) {
this.logger.error('清理用户数据失败', error);
throw new HttpException(
error.message || '清理用户数据失败',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}