/** * 位置广播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, AuthenticatedRequest } 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, JoinSessionDto, UpdatePositionDto, 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: '创建一个新的游戏会话,用于多人位置广播', }) @ApiBody({ type: CreateSessionDto }) @ApiResponse({ status: 201, description: '会话创建成功', schema: { type: 'object', properties: { success: { type: 'boolean', example: true }, data: { type: 'object', properties: { sessionId: { type: 'string', example: 'session_12345' }, createdAt: { type: 'number', example: 1641024000000 }, config: { type: 'object' }, }, }, message: { type: 'string', example: '会话创建成功' }, }, }, }) @ApiResponse({ status: 400, description: '请求参数错误' }) @ApiResponse({ status: 409, description: '会话ID已存在' }) async createSession( @Body() createSessionDto: CreateSessionDto, @CurrentUser() user: JwtPayload, ) { try { this.logger.log('创建会话API请求', { operation: 'createSession', sessionId: createSessionDto.sessionId, userId: user.sub, timestamp: new Date().toISOString(), }); const session = await this.locationSessionService.createSession({ sessionId: createSessionDto.sessionId, creatorId: user.sub, name: createSessionDto.name, description: createSessionDto.description, maxUsers: createSessionDto.maxUsers, allowObservers: createSessionDto.allowObservers, password: createSessionDto.password, allowedMaps: createSessionDto.allowedMaps, broadcastRange: createSessionDto.broadcastRange, metadata: createSessionDto.metadata, }); return { success: true, data: { sessionId: session.sessionId, createdAt: session.createdAt, config: session.config, metadata: session.metadata, }, message: '会话创建成功', }; } catch (error) { this.logger.error('创建会话失败', { operation: 'createSession', sessionId: createSessionDto.sessionId, userId: user.sub, error: error instanceof Error ? error.message : String(error), }); if (error instanceof HttpException) { throw error; } throw new HttpException( { success: false, message: '会话创建失败', error: error instanceof Error ? error.message : String(error), }, HttpStatus.INTERNAL_SERVER_ERROR, ); } } /** * 查询会话列表 */ @Get('sessions') @ApiOperation({ summary: '查询会话列表', description: '根据条件查询游戏会话列表', }) @ApiQuery({ name: 'status', required: false, description: '会话状态' }) @ApiQuery({ name: 'minUsers', required: false, description: '最小用户数' }) @ApiQuery({ name: 'maxUsers', required: false, description: '最大用户数' }) @ApiQuery({ name: 'publicOnly', required: false, description: '只显示公开会话' }) @ApiQuery({ name: 'offset', required: false, description: '分页偏移' }) @ApiQuery({ name: 'limit', required: false, description: '分页大小' }) @ApiResponse({ status: 200, description: '查询成功', schema: { type: 'object', properties: { success: { type: 'boolean', example: true }, data: { type: 'object', properties: { sessions: { type: 'array', items: { type: 'object' } }, total: { type: 'number', example: 10 }, page: { type: 'number', example: 1 }, pageSize: { type: 'number', example: 10 }, }, }, }, }, }) async querySessions(@Query() query: SessionQueryDto) { try { const result = await this.locationSessionService.querySessions({ status: query.status as any, // 类型转换,因为DTO中是string类型 minUsers: query.minUsers, maxUsers: query.maxUsers, publicOnly: query.publicOnly, offset: query.offset || 0, limit: query.limit || 10, }); return { success: true, data: result, }; } catch (error) { this.logger.error('查询会话列表失败', { operation: 'querySessions', query, error: error instanceof Error ? error.message : String(error), }); throw new HttpException( { success: false, message: '查询会话列表失败', error: error instanceof Error ? error.message : String(error), }, 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 }, data: { type: 'object', properties: { session: { type: 'object' }, users: { type: 'array', items: { type: 'object' } }, onlineCount: { type: 'number', example: 5 }, activeMaps: { type: 'array', items: { type: 'string' } }, }, }, }, }, }) @ApiResponse({ status: 404, description: '会话不存在' }) async getSessionDetail( @Param('sessionId') sessionId: string, @CurrentUser() user: JwtPayload, ) { try { const result = await this.locationSessionService.getSessionDetail( sessionId, user.sub, ); return { success: true, data: result, }; } catch (error) { this.logger.error('获取会话详情失败', { operation: 'getSessionDetail', sessionId, userId: user.sub, error: error instanceof Error ? error.message : String(error), }); if (error instanceof HttpException) { throw error; } throw new HttpException( { success: false, message: '获取会话详情失败', error: error instanceof Error ? error.message : String(error), }, HttpStatus.INTERNAL_SERVER_ERROR, ); } } /** * 更新会话配置 */ @Put('sessions/:sessionId/config') @ApiOperation({ summary: '更新会话配置', description: '更新指定会话的配置参数(需要管理员权限)', }) @ApiParam({ name: 'sessionId', description: '会话ID' }) @ApiBody({ type: UpdateSessionConfigDto }) @ApiResponse({ status: 200, description: '更新成功', schema: { type: 'object', properties: { success: { type: 'boolean', example: true }, data: { type: 'object' }, message: { type: 'string', example: '会话配置更新成功' }, }, }, }) @ApiResponse({ status: 403, description: '权限不足' }) @ApiResponse({ status: 404, description: '会话不存在' }) async updateSessionConfig( @Param('sessionId') sessionId: string, @Body() updateConfigDto: UpdateSessionConfigDto, @CurrentUser() user: JwtPayload, ) { try { const session = await this.locationSessionService.updateSessionConfig( sessionId, updateConfigDto, user.sub, ); return { success: true, data: session, message: '会话配置更新成功', }; } catch (error) { this.logger.error('更新会话配置失败', { operation: 'updateSessionConfig', sessionId, userId: user.sub, error: error instanceof Error ? error.message : String(error), }); if (error instanceof HttpException) { throw error; } throw new HttpException( { success: false, message: '更新会话配置失败', error: error instanceof Error ? error.message : String(error), }, HttpStatus.INTERNAL_SERVER_ERROR, ); } } /** * 结束会话 */ @Delete('sessions/:sessionId') @ApiOperation({ summary: '结束会话', description: '结束指定的游戏会话(需要管理员权限)', }) @ApiParam({ name: 'sessionId', description: '会话ID' }) @ApiResponse({ status: 200, description: '会话结束成功', schema: { type: 'object', properties: { success: { type: 'boolean', example: true }, message: { type: 'string', example: '会话结束成功' }, }, }, }) @ApiResponse({ status: 403, description: '权限不足' }) @ApiResponse({ status: 404, description: '会话不存在' }) async endSession( @Param('sessionId') sessionId: string, @CurrentUser() user: JwtPayload, ) { try { await this.locationSessionService.endSession(sessionId, user.sub); return { success: true, message: '会话结束成功', }; } catch (error) { this.logger.error('结束会话失败', { operation: 'endSession', sessionId, userId: user.sub, error: error instanceof Error ? error.message : String(error), }); if (error instanceof HttpException) { throw error; } throw new HttpException( { success: false, message: '结束会话失败', error: error instanceof Error ? error.message : String(error), }, HttpStatus.INTERNAL_SERVER_ERROR, ); } } /** * 查询位置信息 */ @Get('positions') @ApiOperation({ summary: '查询位置信息', description: '根据条件查询用户位置信息', }) @ApiQuery({ name: 'userIds', required: false, description: '用户ID列表(逗号分隔)' }) @ApiQuery({ name: 'mapId', required: false, description: '地图ID' }) @ApiQuery({ name: 'sessionId', required: false, description: '会话ID' }) @ApiQuery({ name: 'centerX', required: false, description: '范围查询中心X坐标' }) @ApiQuery({ name: 'centerY', required: false, description: '范围查询中心Y坐标' }) @ApiQuery({ name: 'radius', required: false, description: '范围查询半径' }) @ApiQuery({ name: 'offset', required: false, description: '分页偏移' }) @ApiQuery({ name: 'limit', required: false, description: '分页大小' }) @ApiResponse({ status: 200, description: '查询成功', schema: { type: 'object', properties: { success: { type: 'boolean', example: true }, data: { type: 'object', properties: { positions: { type: 'array', items: { type: 'object' } }, total: { type: 'number', example: 20 }, timestamp: { type: 'number', example: 1641024000000 }, }, }, }, }, }) async queryPositions(@Query() query: PositionQueryDto) { try { const userIds = query.userIds ? query.userIds.split(',') : undefined; const range = (query.centerX !== undefined && query.centerY !== undefined && query.radius !== undefined) ? { centerX: query.centerX, centerY: query.centerY, radius: query.radius, } : undefined; const result = await this.locationPositionService.queryPositions({ userIds, mapId: query.mapId, sessionId: query.sessionId, range, pagination: { offset: query.offset || 0, limit: query.limit || 50, }, }); return { success: true, data: result, }; } catch (error) { this.logger.error('查询位置信息失败', { operation: 'queryPositions', query, error: error instanceof Error ? error.message : String(error), }); throw new HttpException( { success: false, message: '查询位置信息失败', error: error instanceof Error ? error.message : String(error), }, HttpStatus.INTERNAL_SERVER_ERROR, ); } } /** * 获取位置统计信息 */ @Get('positions/stats') @ApiOperation({ summary: '获取位置统计信息', description: '获取位置数据的统计信息,包括用户分布、活跃地图等', }) @ApiQuery({ name: 'mapId', required: false, description: '地图ID' }) @ApiQuery({ name: 'sessionId', required: false, description: '会话ID' }) @ApiResponse({ status: 200, description: '获取成功', schema: { type: 'object', properties: { success: { type: 'boolean', example: true }, data: { type: 'object', properties: { totalUsers: { type: 'number', example: 100 }, onlineUsers: { type: 'number', example: 85 }, activeMaps: { type: 'number', example: 5 }, mapDistribution: { type: 'object' }, updateFrequency: { type: 'number', example: 2.5 }, timestamp: { type: 'number', example: 1641024000000 }, }, }, }, }, }) async getPositionStats( @Query('mapId') mapId?: string, @Query('sessionId') sessionId?: string, ) { try { const result = await this.locationPositionService.getPositionStats({ mapId, sessionId, }); return { success: true, data: result, }; } catch (error) { this.logger.error('获取位置统计失败', { operation: 'getPositionStats', mapId, sessionId, error: error instanceof Error ? error.message : String(error), }); throw new HttpException( { success: false, message: '获取位置统计失败', error: error instanceof Error ? error.message : String(error), }, HttpStatus.INTERNAL_SERVER_ERROR, ); } } /** * 获取用户位置历史 */ @Get('users/:userId/position-history') @ApiOperation({ summary: '获取用户位置历史', description: '获取指定用户的位置历史记录', }) @ApiParam({ name: 'userId', description: '用户ID' }) @ApiQuery({ name: 'mapId', required: false, description: '地图ID过滤' }) @ApiQuery({ name: 'limit', required: false, description: '最大记录数' }) @ApiResponse({ status: 200, description: '获取成功', schema: { type: 'object', properties: { success: { type: 'boolean', example: true }, data: { type: 'array', items: { type: 'object' }, }, }, }, }) async getUserPositionHistory( @Param('userId') userId: string, @CurrentUser() user: JwtPayload, @Query('mapId') mapId?: string, @Query('limit') limit?: number, ) { try { // 权限检查:只能查看自己的历史记录,或者管理员可以查看所有 if (userId !== user.sub && user.role < 2) { throw new HttpException( { success: false, message: '权限不足,只能查看自己的位置历史', }, HttpStatus.FORBIDDEN, ); } const result = await this.locationPositionService.getPositionHistory({ userId, mapId, limit: limit || 100, }); return { success: true, data: result, }; } catch (error) { this.logger.error('获取用户位置历史失败', { operation: 'getUserPositionHistory', userId, requestUserId: user.sub, error: error instanceof Error ? error.message : String(error), }); if (error instanceof HttpException) { throw error; } throw new HttpException( { success: false, message: '获取用户位置历史失败', error: error instanceof Error ? error.message : String(error), }, 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: '用户数据清理成功' }, }, }, }) @ApiResponse({ status: 403, description: '权限不足' }) async cleanupUserData( @Param('userId') userId: string, @CurrentUser() user: JwtPayload, ) { try { // 权限检查:只有管理员或用户本人可以清理数据 if (userId !== user.sub && user.role < 2) { throw new HttpException( { success: false, message: '权限不足,只能清理自己的数据', }, HttpStatus.FORBIDDEN, ); } const success = await this.locationBroadcastService.cleanupUserData(userId); if (!success) { throw new HttpException( { success: false, message: '用户数据清理失败', }, HttpStatus.INTERNAL_SERVER_ERROR, ); } return { success: true, message: '用户数据清理成功', }; } catch (error) { this.logger.error('清理用户数据失败', { operation: 'cleanupUserData', userId, operatorId: user.sub, error: error instanceof Error ? error.message : String(error), }); if (error instanceof HttpException) { throw error; } throw new HttpException( { success: false, message: '清理用户数据失败', error: error instanceof Error ? error.message : String(error), }, HttpStatus.INTERNAL_SERVER_ERROR, ); } } }