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

727 lines
20 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.
/**
* 位置广播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,
);
}
}
}