xiangyixiangqin/miniapp/即时通讯方案说明.md
2026-01-14 20:23:13 +08:00

18 KiB
Raw Blame History

相宜相亲 - 即时通讯方案说明

文档版本: 1.0
创建日期: 2026-01-14
原始设计: 2025-12-28


一、原计划方案SignalR

根据项目技术栈文档(server/技术栈与开发规范.md原先计划使用 SignalR 作为即时通讯方案

1.1 技术选型确认

技术 版本 说明
实时通信 SignalR 8.0

原文引用

| **实时通信** | SignalR | 8.0 | 聊天消息实时推送 |

1.2 SignalR 方案优势

  1. 原生集成 - ASP.NET Core 8.0 内置支持
  2. 自动降级 - WebSocket → Server-Sent Events → Long Polling
  3. 连接管理 - 自动处理断线重连
  4. 分组功能 - 支持用户分组推送
  5. 跨平台 - 支持小程序、Web、移动端

1.3 数据库设计已就绪

数据库已经设计了完整的聊天表结构:

Chat_Session (聊天会话表)

CREATE TABLE Chat_Session (
    Id BIGINT PRIMARY KEY IDENTITY(1,1),
    User1Id BIGINT NOT NULL,
    User2Id BIGINT NOT NULL,
    LastMessageId BIGINT,
    LastMessageTime DATETIME2,
    User1UnreadCount INT NOT NULL DEFAULT 0,
    User2UnreadCount INT NOT NULL DEFAULT 0,
    CreateTime DATETIME2 NOT NULL DEFAULT GETDATE(),
    UpdateTime DATETIME2,
    CONSTRAINT UQ_Chat_Session UNIQUE (User1Id, User2Id)
);

Chat_Message (聊天消息表)

CREATE TABLE Chat_Message (
    Id BIGINT PRIMARY KEY IDENTITY(1,1),
    SessionId BIGINT NOT NULL,
    SenderId BIGINT NOT NULL,
    ReceiverId BIGINT NOT NULL,
    MessageType INT NOT NULL,
    Content NVARCHAR(MAX),
    VoiceUrl NVARCHAR(500),
    VoiceDuration INT,
    ExtraData NVARCHAR(MAX),
    Status INT NOT NULL DEFAULT 1,
    IsRead BIT NOT NULL DEFAULT 0,
    CreateTime DATETIME2 NOT NULL DEFAULT GETDATE()
);

消息类型枚举

public enum MessageType
{
    Text = 1,                   // 文本
    Voice = 2,                  // 语音
    Image = 3,                  // 图片
    ExchangeWeChatRequest = 4,  // 交换微信请求
    ExchangeWeChatResult = 5,   // 交换微信结果
    ExchangePhotoRequest = 6,   // 交换照片请求
    ExchangePhotoResult = 7     // 交换照片结果
}

二、SignalR 实现方案

2.1 后端实现架构

┌─────────────────────────────────────────────────────┐
│                  小程序客户端                        │
│              (uni-app WebSocket)                    │
└────────────────────┬────────────────────────────────┘
                     │ WebSocket 连接
                     ▼
┌─────────────────────────────────────────────────────┐
│              SignalR Hub (ChatHub)                  │
│  - OnConnectedAsync()    用户上线                   │
│  - OnDisconnectedAsync() 用户下线                   │
│  - SendMessage()         发送消息                   │
│  - JoinGroup()           加入会话组                 │
├─────────────────────────────────────────────────────┤
│              业务服务层                              │
│  - ChatService           聊天业务逻辑               │
│  - MessageService        消息处理                   │
│  - NotificationService   推送通知                   │
└────────────────────┬────────────────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────────────────┐
│              数据库 (SQL Server)                     │
│  - Chat_Session          会话表                     │
│  - Chat_Message          消息表                     │
└─────────────────────────────────────────────────────┘

2.2 后端代码示例

ChatHub.cs

using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.Authorization;

namespace XiangYi.AppApi.Hubs
{
    [Authorize]
    public class ChatHub : Hub
    {
        private readonly IChatService _chatService;
        private readonly ILogger<ChatHub> _logger;

        public ChatHub(IChatService chatService, ILogger<ChatHub> logger)
        {
            _chatService = chatService;
            _logger = logger;
        }

        // 用户连接
        public override async Task OnConnectedAsync()
        {
            var userId = Context.User?.FindFirst("UserId")?.Value;
            if (!string.IsNullOrEmpty(userId))
            {
                // 将用户加入自己的组(用于接收消息)
                await Groups.AddToGroupAsync(Context.ConnectionId, $"user_{userId}");
                _logger.LogInformation($"用户 {userId} 已连接ConnectionId: {Context.ConnectionId}");
            }
            await base.OnConnectedAsync();
        }

        // 用户断开
        public override async Task OnDisconnectedAsync(Exception? exception)
        {
            var userId = Context.User?.FindFirst("UserId")?.Value;
            if (!string.IsNullOrEmpty(userId))
            {
                _logger.LogInformation($"用户 {userId} 已断开连接");
            }
            await base.OnDisconnectedAsync(exception);
        }

        // 发送消息
        public async Task SendMessage(SendMessageDto dto)
        {
            var senderId = long.Parse(Context.User?.FindFirst("UserId")?.Value ?? "0");
            
            // 保存消息到数据库
            var message = await _chatService.SendMessageAsync(senderId, dto);
            
            // 推送给接收者
            await Clients.Group($"user_{dto.ReceiverId}")
                .SendAsync("ReceiveMessage", message);
            
            // 确认发送成功给发送者
            await Clients.Caller.SendAsync("MessageSent", new { 
                tempId = dto.TempId, 
                messageId = message.Id 
            });
        }

        // 标记消息已读
        public async Task MarkAsRead(long sessionId)
        {
            var userId = long.Parse(Context.User?.FindFirst("UserId")?.Value ?? "0");
            await _chatService.MarkSessionAsReadAsync(userId, sessionId);
        }

        // 交换微信
        public async Task ExchangeWeChat(long receiverId)
        {
            var senderId = long.Parse(Context.User?.FindFirst("UserId")?.Value ?? "0");
            var message = await _chatService.ExchangeWeChatAsync(senderId, receiverId);
            
            // 推送给接收者
            await Clients.Group($"user_{receiverId}")
                .SendAsync("ReceiveMessage", message);
        }

        // 响应交换请求
        public async Task RespondExchange(long messageId, bool accept)
        {
            var userId = long.Parse(Context.User?.FindFirst("UserId")?.Value ?? "0");
            var result = await _chatService.RespondExchangeAsync(userId, messageId, accept);
            
            // 推送给请求者
            await Clients.Group($"user_{result.RequesterId}")
                .SendAsync("ExchangeResponded", result);
        }
    }
}

Program.cs 配置

// 添加 SignalR
builder.Services.AddSignalR(options =>
{
    options.EnableDetailedErrors = true;
    options.KeepAliveInterval = TimeSpan.FromSeconds(15);
    options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
});

// 映射 Hub
app.MapHub<ChatHub>("/hubs/chat");

2.3 前端实现(小程序)

utils/signalr.js

/**
 * SignalR 连接管理
 */
import { getToken } from './storage'
import config from '../config/index'

class SignalRClient {
  constructor() {
    this.connection = null
    this.isConnected = false
    this.reconnectAttempts = 0
    this.maxReconnectAttempts = 5
    this.messageHandlers = []
  }

  /**
   * 连接 SignalR
   */
  async connect() {
    if (this.isConnected) {
      console.log('SignalR 已连接')
      return
    }

    const token = getToken()
    if (!token) {
      console.error('未登录,无法连接 SignalR')
      return
    }

    try {
      // 构建 WebSocket URL
      const wsUrl = config.API_BASE_URL.replace('http', 'ws') + '/hubs/chat'
      
      // 创建 WebSocket 连接
      this.connection = uni.connectSocket({
        url: `${wsUrl}?access_token=${token}`,
        header: {
          'content-type': 'application/json'
        },
        protocols: ['websocket'],
        success: () => {
          console.log('SignalR WebSocket 创建成功')
        }
      })

      // 监听连接打开
      uni.onSocketOpen(() => {
        console.log('SignalR 连接成功')
        this.isConnected = true
        this.reconnectAttempts = 0
        this.sendHandshake()
      })

      // 监听消息
      uni.onSocketMessage((res) => {
        this.handleMessage(res.data)
      })

      // 监听连接关闭
      uni.onSocketClose(() => {
        console.log('SignalR 连接关闭')
        this.isConnected = false
        this.handleReconnect()
      })

      // 监听错误
      uni.onSocketError((err) => {
        console.error('SignalR 连接错误:', err)
        this.isConnected = false
      })

    } catch (error) {
      console.error('SignalR 连接失败:', error)
      this.handleReconnect()
    }
  }

  /**
   * 发送握手消息SignalR 协议)
   */
  sendHandshake() {
    const handshake = {
      protocol: 'json',
      version: 1
    }
    this.send(JSON.stringify(handshake) + '\x1e')
  }

  /**
   * 处理接收到的消息
   */
  handleMessage(data) {
    try {
      // SignalR 消息以 \x1e 分隔
      const messages = data.split('\x1e').filter(m => m)
      
      messages.forEach(msg => {
        const message = JSON.parse(msg)
        
        // 处理不同类型的消息
        if (message.type === 1) {
          // 调用方法响应
          this.handleInvocation(message)
        } else if (message.type === 3) {
          // 完成响应
          console.log('方法调用完成:', message)
        }
      })
    } catch (error) {
      console.error('解析消息失败:', error)
    }
  }

  /**
   * 处理服务端调用
   */
  handleInvocation(message) {
    const { target, arguments: args } = message
    
    // 触发对应的事件处理器
    this.messageHandlers.forEach(handler => {
      if (handler.event === target) {
        handler.callback(...args)
      }
    })
  }

  /**
   * 发送消息
   */
  send(data) {
    if (!this.isConnected) {
      console.error('SignalR 未连接')
      return
    }

    uni.sendSocketMessage({
      data,
      success: () => {
        console.log('消息发送成功')
      },
      fail: (err) => {
        console.error('消息发送失败:', err)
      }
    })
  }

  /**
   * 调用服务端方法
   */
  invoke(method, ...args) {
    const message = {
      type: 1,
      target: method,
      arguments: args
    }
    this.send(JSON.stringify(message) + '\x1e')
  }

  /**
   * 监听服务端事件
   */
  on(event, callback) {
    this.messageHandlers.push({ event, callback })
  }

  /**
   * 移除事件监听
   */
  off(event, callback) {
    this.messageHandlers = this.messageHandlers.filter(
      h => h.event !== event || h.callback !== callback
    )
  }

  /**
   * 断线重连
   */
  handleReconnect() {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.error('SignalR 重连次数超限')
      return
    }

    this.reconnectAttempts++
    const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000)
    
    console.log(`${delay}ms 后尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`)
    
    setTimeout(() => {
      this.connect()
    }, delay)
  }

  /**
   * 断开连接
   */
  disconnect() {
    if (this.connection) {
      uni.closeSocket()
      this.isConnected = false
      this.connection = null
    }
  }
}

// 导出单例
export const signalR = new SignalRClient()

export default signalR

在聊天页面使用

// pages/chat/index.vue
import { signalR } from '@/utils/signalr'

onMounted(() => {
  // 连接 SignalR
  signalR.connect()
  
  // 监听接收消息
  signalR.on('ReceiveMessage', (message) => {
    console.log('收到新消息:', message)
    messages.value.push(message)
    scrollToBottom()
  })
  
  // 监听消息发送成功
  signalR.on('MessageSent', ({ tempId, messageId }) => {
    const msg = messages.value.find(m => m.id === tempId)
    if (msg) {
      msg.id = messageId
      msg.status = MessageStatus.SENT
    }
  })
  
  // 监听交换响应
  signalR.on('ExchangeResponded', (result) => {
    console.log('交换响应:', result)
    const msg = messages.value.find(m => m.id === result.messageId)
    if (msg) {
      msg.status = result.accepted ? ExchangeStatus.ACCEPTED : ExchangeStatus.REJECTED
      if (result.accepted && result.exchangedData) {
        msg.exchangedContent = result.exchangedData
      }
    }
  })
})

onUnmounted(() => {
  // 移除监听
  signalR.off('ReceiveMessage')
  signalR.off('MessageSent')
  signalR.off('ExchangeResponded')
})

// 发送消息
const handleSendMessage = () => {
  const content = inputText.value.trim()
  if (!content) return
  
  const tempId = Date.now()
  const localMessage = {
    id: tempId,
    sessionId: sessionId.value,
    senderId: myUserId.value,
    receiverId: targetUserId.value,
    messageType: MessageType.TEXT,
    content,
    status: MessageStatus.SENDING,
    createTime: new Date().toISOString(),
    isMine: true
  }
  
  messages.value.push(localMessage)
  inputText.value = ''
  scrollToBottom()
  
  // 通过 SignalR 发送
  signalR.invoke('SendMessage', {
    tempId,
    sessionId: sessionId.value,
    receiverId: targetUserId.value,
    messageType: MessageType.TEXT,
    content
  })
}

三、实施步骤

3.1 后端开发(优先级 P0

  1. 创建 ChatHub

    • 实现 ChatHub.cs
    • 配置 SignalR 中间件
    • 添加 JWT 认证支持
  2. 实现聊天服务

    • ChatService.cs - 聊天业务逻辑
    • MessageService.cs - 消息处理
    • 数据库操作FreeSql
  3. 测试

    • 单元测试
    • 集成测试
    • 压力测试

3.2 前端开发(优先级 P0

  1. SignalR 客户端封装

    • 创建 utils/signalr.js
    • 实现连接管理
    • 实现断线重连
    • 实现心跳检测
  2. 集成到聊天页面

    • 修改 pages/chat/index.vue
    • 接收实时消息
    • 发送消息通过 SignalR
    • 处理交换请求
  3. 测试

    • 功能测试
    • 兼容性测试
    • 性能测试

3.3 部署配置

  1. 服务器配置

    • 开启 WebSocket 支持
    • 配置 Nginx 反向代理
    • 配置 SSL 证书
  2. 监控告警

    • 连接数监控
    • 消息延迟监控
    • 错误日志告警

四、技术难点与解决方案

4.1 小程序 WebSocket 限制

问题: 小程序同时只能有 5 个 WebSocket 连接

解决方案:

  • 使用单例模式管理 SignalR 连接
  • 页面切换时保持连接不断开
  • 只在必要时创建连接

4.2 断线重连

问题: 网络不稳定导致频繁断线

解决方案:

  • 指数退避重连策略
  • 最大重连次数限制
  • 重连成功后重新加载未读消息

4.3 消息可靠性

问题: 消息可能丢失或重复

解决方案:

  • 消息发送确认机制
  • 本地消息队列
  • 消息去重基于消息ID

4.4 离线消息

问题: 用户离线时收不到消息

解决方案:

  • 用户上线时拉取离线消息
  • 服务号推送通知
  • 未读消息计数

五、性能优化

5.1 连接管理

  • 使用 Redis 存储用户连接映射
  • 支持水平扩展(多服务器)
  • 连接池管理

5.2 消息推送

  • 批量推送优化
  • 消息压缩
  • 分组推送

5.3 数据库优化

  • 消息表分表(按月)
  • 索引优化
  • 定期归档历史消息

六、总结

当前状态

  • 数据库设计完成
  • API 接口定义完成
  • 前端页面完成
  • SignalR 后端已完整实现
  • SignalR 前端已完整实现
  • 实时消息推送已实现
  • 断线重连机制已实现
  • 心跳检测已实现

已完成工作

  1. 后端实现 - ChatHub 完整实现
  2. 前端封装 - SignalR 客户端完整封装
  3. 功能集成 - 聊天页面完整集成
  4. 测试验证 - 待进行完整测试
  5. 生产部署 - 待配置生产环境

下一步行动

  1. 功能测试 - 参考 miniapp/SignalR测试指南.md
  2. 性能测试 - 压力测试和性能优化
  3. 生产部署 - 参考 server/SignalR生产环境配置.md
  4. 监控告警 - 配置监控和告警系统

实际工作量

  • 后端开发: 已完成ChatHub.cs 已存在)
  • 前端开发: 已完成signalr.js 已存在)
  • 功能集成: 已完成(聊天页面已集成)
  • 总计:功能已完整实现

七、参考资料