feat: add nearby social services
This commit is contained in:
@@ -33,6 +33,7 @@
|
||||
import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||
import * as WebSocket from 'ws';
|
||||
import { ChatService } from '../../business/chat/chat.service';
|
||||
import { SocialService } from '../../business/social/social.service';
|
||||
|
||||
/** WebSocket 服务器默认端口 */
|
||||
const DEFAULT_WEBSOCKET_PORT = 3001;
|
||||
@@ -102,7 +103,10 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
private mapRooms = new Map<string, Set<string>>();
|
||||
private lastWelcomeAtByUserId = new Map<string, number>();
|
||||
|
||||
constructor(private readonly chatService: ChatService) {}
|
||||
constructor(
|
||||
private readonly chatService: ChatService,
|
||||
private readonly socialService: SocialService,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
const port = process.env.WEBSOCKET_PORT ? parseInt(process.env.WEBSOCKET_PORT) : DEFAULT_WEBSOCKET_PORT;
|
||||
@@ -140,6 +144,7 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
|
||||
// 设置网关引用到业务层
|
||||
this.chatService.setWebSocketGateway(this);
|
||||
this.socialService.setRealtimeGateway(this);
|
||||
this.logger.log(`WebSocket服务器启动成功,端口: ${port},路径: /game`);
|
||||
}
|
||||
|
||||
@@ -186,6 +191,12 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
case 'chat':
|
||||
await this.handleChat(ws, message);
|
||||
break;
|
||||
case 'dm_send':
|
||||
await this.handleDirectMessage(ws, message);
|
||||
break;
|
||||
case 'dm_read':
|
||||
await this.handleDirectMessageRead(ws, message);
|
||||
break;
|
||||
case 'position':
|
||||
await this.handlePosition(ws, message);
|
||||
break;
|
||||
@@ -260,6 +271,7 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
});
|
||||
|
||||
this.logger.log(`用户登录成功: ${result.username} (${ws.id})`);
|
||||
await this.socialService.notifyPresenceChanged(String(result.userId), true);
|
||||
} else {
|
||||
this.sendMessage(ws, {
|
||||
t: 'login_error',
|
||||
@@ -316,6 +328,12 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
return;
|
||||
}
|
||||
|
||||
const scope = String(message.scope || 'local').trim().toLowerCase();
|
||||
if (scope === 'private' || scope === 'whisper' || scope === 'dm') {
|
||||
await this.handleDirectMessage(ws, message);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.chatService.sendChatMessage({
|
||||
socketId: ws.id,
|
||||
@@ -347,6 +365,43 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
}
|
||||
}
|
||||
|
||||
private async handleDirectMessage(ws: ExtendedWebSocket, message: any) {
|
||||
if (!ws.authenticated || !ws.userId) {
|
||||
this.sendError(ws, '请先登录');
|
||||
return;
|
||||
}
|
||||
const targetUserId = String(message.targetUserId || message.target_user_id || message.userId || message.user_id || '').trim();
|
||||
const content = String(message.content || message.txt || '').trim();
|
||||
if (!/^\d+$/.test(targetUserId) || !content) {
|
||||
this.sendMessage(ws, { t: 'chat_error', code: 'CHAT_ERROR', message: '私聊目标或内容无效' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await this.socialService.sendDirectMessage(BigInt(ws.userId), BigInt(targetUserId), content);
|
||||
this.sendMessage(ws, { t: 'chat_sent', messageId: result.id, message: '消息发送成功' });
|
||||
} catch (error) {
|
||||
this.sendMessage(ws, { t: 'chat_error', code: this.toClientErrorCode((error as Error).message), message: (error as Error).message || '私聊发送失败' });
|
||||
}
|
||||
}
|
||||
|
||||
private async handleDirectMessageRead(ws: ExtendedWebSocket, message: any) {
|
||||
if (!ws.authenticated || !ws.userId) {
|
||||
this.sendError(ws, '请先登录');
|
||||
return;
|
||||
}
|
||||
const targetUserId = String(message.userId || message.user_id || message.targetUserId || '').trim();
|
||||
if (!/^\d+$/.test(targetUserId)) {
|
||||
this.sendError(ws, '私聊对象无效');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await this.socialService.markConversationRead(BigInt(ws.userId), BigInt(targetUserId));
|
||||
this.sendMessage(ws, { t: 'dm_read_success', ...result, userId: targetUserId });
|
||||
} catch (error) {
|
||||
this.sendError(ws, (error as Error).message || '标记私聊已读失败');
|
||||
}
|
||||
}
|
||||
|
||||
private async handleFriendAdd(ws: ExtendedWebSocket, message: any) {
|
||||
if (!ws.authenticated) {
|
||||
this.sendError(ws, '请先登录');
|
||||
@@ -359,26 +414,12 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.chatService.addFriend({
|
||||
socketId: ws.id,
|
||||
friendUserId,
|
||||
friendUsername: message.friendUsername || message.friend_username || message.username,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
this.sendMessage(ws, {
|
||||
t: 'friend_added',
|
||||
friend: result.friend,
|
||||
});
|
||||
await this.sendFriendList(ws);
|
||||
return;
|
||||
try {
|
||||
const request = await this.socialService.createFriendRequest(BigInt(String(ws.userId)), BigInt(String(friendUserId)));
|
||||
this.sendMessage(ws, { t: 'friend_request_sent', request });
|
||||
} catch (error) {
|
||||
this.sendMessage(ws, { t: 'friend_error', code: 'CHAT_ERROR', message: (error as Error).message || '好友请求发送失败' });
|
||||
}
|
||||
|
||||
this.sendMessage(ws, {
|
||||
t: 'friend_error',
|
||||
code: this.toClientErrorCode(result.error),
|
||||
message: result.error || '添加好友失败',
|
||||
});
|
||||
}
|
||||
|
||||
private async handleFriendRequest(ws: ExtendedWebSocket, message: any) {
|
||||
@@ -393,25 +434,12 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.chatService.requestFriend({
|
||||
socketId: ws.id,
|
||||
friendUserId,
|
||||
friendUsername: message.friendUsername || message.friend_username || message.username,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
this.sendMessage(ws, {
|
||||
t: 'friend_request_sent',
|
||||
request: result.friendRequest,
|
||||
});
|
||||
return;
|
||||
try {
|
||||
const request = await this.socialService.createFriendRequest(BigInt(String(ws.userId)), BigInt(String(friendUserId)));
|
||||
this.sendMessage(ws, { t: 'friend_request_sent', request });
|
||||
} catch (error) {
|
||||
this.sendMessage(ws, { t: 'friend_error', code: 'CHAT_ERROR', message: (error as Error).message || '好友请求发送失败' });
|
||||
}
|
||||
|
||||
this.sendMessage(ws, {
|
||||
t: 'friend_error',
|
||||
code: this.toClientErrorCode(result.error),
|
||||
message: result.error || '好友请求发送失败',
|
||||
});
|
||||
}
|
||||
|
||||
private async handleFriendAccept(ws: ExtendedWebSocket, message: any) {
|
||||
@@ -426,26 +454,16 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.chatService.acceptFriendRequest({
|
||||
socketId: ws.id,
|
||||
friendUserId,
|
||||
friendUsername: message.friendUsername || message.friend_username || message.username,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
this.sendMessage(ws, {
|
||||
t: 'friend_added',
|
||||
friend: result.friend,
|
||||
});
|
||||
try {
|
||||
const requests = await this.socialService.getFriendRequests(BigInt(String(ws.userId)));
|
||||
const request = requests.find((item) => item.requester.id === String(friendUserId));
|
||||
if (!request) throw new Error('好友请求不存在或已过期');
|
||||
const result = await this.socialService.acceptFriendRequest(BigInt(String(ws.userId)), BigInt(request.id));
|
||||
this.sendMessage(ws, { t: 'friend_added', friend: this.legacyFriend(result.friend) });
|
||||
await this.sendFriendList(ws);
|
||||
return;
|
||||
} catch (error) {
|
||||
this.sendMessage(ws, { t: 'friend_error', code: 'CHAT_ERROR', message: (error as Error).message || '接受好友请求失败' });
|
||||
}
|
||||
|
||||
this.sendMessage(ws, {
|
||||
t: 'friend_error',
|
||||
code: this.toClientErrorCode(result.error),
|
||||
message: result.error || '接受好友请求失败',
|
||||
});
|
||||
}
|
||||
|
||||
private async handleFriendReject(ws: ExtendedWebSocket, message: any) {
|
||||
@@ -460,26 +478,16 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.chatService.rejectFriendRequest({
|
||||
socketId: ws.id,
|
||||
friendUserId,
|
||||
friendUsername: message.friendUsername || message.friend_username || message.username,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
this.sendMessage(ws, {
|
||||
t: 'friend_request_rejected',
|
||||
userId: friendUserId,
|
||||
});
|
||||
try {
|
||||
const requests = await this.socialService.getFriendRequests(BigInt(String(ws.userId)));
|
||||
const request = requests.find((item) => item.requester.id === String(friendUserId));
|
||||
if (!request) throw new Error('好友请求不存在或已过期');
|
||||
await this.socialService.rejectFriendRequest(BigInt(String(ws.userId)), BigInt(request.id));
|
||||
this.sendMessage(ws, { t: 'friend_request_rejected', userId: String(friendUserId) });
|
||||
await this.sendFriendList(ws);
|
||||
return;
|
||||
} catch (error) {
|
||||
this.sendMessage(ws, { t: 'friend_error', code: 'CHAT_ERROR', message: (error as Error).message || '拒绝好友请求失败' });
|
||||
}
|
||||
|
||||
this.sendMessage(ws, {
|
||||
t: 'friend_error',
|
||||
code: this.toClientErrorCode(result.error),
|
||||
message: result.error || '拒绝好友请求失败',
|
||||
});
|
||||
}
|
||||
|
||||
private async handleFriendRemove(ws: ExtendedWebSocket, message: any) {
|
||||
@@ -494,25 +502,13 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.chatService.removeFriend({
|
||||
socketId: ws.id,
|
||||
friendUserId,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
this.sendMessage(ws, {
|
||||
t: 'friend_removed',
|
||||
friendUserId,
|
||||
});
|
||||
try {
|
||||
await this.socialService.removeFriend(BigInt(String(ws.userId)), BigInt(String(friendUserId)));
|
||||
this.sendMessage(ws, { t: 'friend_removed', friendUserId: String(friendUserId) });
|
||||
await this.sendFriendList(ws);
|
||||
return;
|
||||
} catch (error) {
|
||||
this.sendMessage(ws, { t: 'friend_error', code: 'CHAT_ERROR', message: (error as Error).message || '移除好友失败' });
|
||||
}
|
||||
|
||||
this.sendMessage(ws, {
|
||||
t: 'friend_error',
|
||||
code: this.toClientErrorCode(result.error),
|
||||
message: result.error || '移除好友失败',
|
||||
});
|
||||
}
|
||||
|
||||
private async handleFriendList(ws: ExtendedWebSocket) {
|
||||
@@ -525,21 +521,13 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
}
|
||||
|
||||
private async sendFriendList(ws: ExtendedWebSocket) {
|
||||
const result = await this.chatService.getFriends(ws.id);
|
||||
if (result.success) {
|
||||
this.sendMessage(ws, {
|
||||
t: 'friend_list',
|
||||
friends: result.friends || [],
|
||||
requests: result.requests || [],
|
||||
});
|
||||
return;
|
||||
try {
|
||||
const userId = BigInt(String(ws.userId));
|
||||
const [friends, requests] = await Promise.all([this.socialService.getFriends(userId), this.socialService.getFriendRequests(userId)]);
|
||||
this.sendMessage(ws, { t: 'friend_list', friends: friends.map((friend) => this.legacyFriend(friend)), requests: requests.map((request) => ({ userId: request.requester.id, username: request.requester.nickname, createdAt: request.createdAt, requestId: request.id })) });
|
||||
} catch (error) {
|
||||
this.sendMessage(ws, { t: 'friend_error', code: 'CHAT_ERROR', message: (error as Error).message || '获取好友列表失败' });
|
||||
}
|
||||
|
||||
this.sendMessage(ws, {
|
||||
t: 'friend_error',
|
||||
code: this.toClientErrorCode(result.error),
|
||||
message: result.error || '获取好友列表失败',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -868,27 +856,29 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
}
|
||||
}
|
||||
|
||||
public broadcastToMap(mapId: string, data: any, excludeId?: string): void {
|
||||
public async broadcastToMap(mapId: string, data: any, excludeId?: string): Promise<void> {
|
||||
const room = this.mapRooms.get(mapId);
|
||||
if (!room) return;
|
||||
|
||||
room.forEach(clientId => {
|
||||
for (const clientId of room) {
|
||||
if (clientId !== excludeId) {
|
||||
const client = this.clients.get(clientId);
|
||||
if (client && client.authenticated && client.readyState === WebSocket.OPEN) {
|
||||
if (data?.t === 'chat_render' && data?.fromUserId && client.userId && !(await this.socialService.canSeeChat(String(data.fromUserId), String(client.userId)))) continue;
|
||||
this.sendMessage(client, data);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public broadcastToAll(data: any, excludeId?: string): void {
|
||||
this.clients.forEach((client, clientId) => {
|
||||
if (clientId === excludeId) return;
|
||||
public async broadcastToAll(data: any, excludeId?: string): Promise<void> {
|
||||
for (const [clientId, client] of this.clients) {
|
||||
if (clientId === excludeId) continue;
|
||||
if (client.authenticated && client.readyState === WebSocket.OPEN) {
|
||||
if (data?.t === 'chat_render' && data?.fromUserId && client.userId && !(await this.socialService.canSeeChat(String(data.fromUserId), String(client.userId)))) continue;
|
||||
this.sendMessage(client, data);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public getConnectionCount(): number {
|
||||
@@ -1005,6 +995,7 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
}
|
||||
if (ws.authenticated && ws.id) {
|
||||
await this.chatService.handlePlayerLogout(ws.id, reason);
|
||||
if (ws.userId) await this.socialService.notifyPresenceChanged(String(ws.userId), false);
|
||||
}
|
||||
if (ws.currentMap) {
|
||||
this.leaveMapRoom(ws.id, ws.currentMap);
|
||||
@@ -1044,4 +1035,8 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
private generateClientId(): string {
|
||||
return `ws_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
||||
}
|
||||
|
||||
private legacyFriend(profile: any) {
|
||||
return { userId: String(profile.id), username: String(profile.nickname || profile.username || '玩家'), online: Boolean(profile.online) };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user