UniApp + SignalR + Asp.net Core 做一个聊天IM,含emoji 表情包

发布于:2025-09-02 ⋅ 阅读:(14) ⋅ 点赞:(0)

功能是模仿Boss 直聘,主要是求职者与招聘者的聊天功能,聊天的时候索要联系方式,加黑名单,举报,标记/置顶这些,跟聊天的功能无关,我也懒得剔除了,主要是实现方式和样式保留好,收藏,以后说不定还要用到,核心还是聊天功能的实现

以上的聊天功能使用了Ubest 框架创建的 UniApp,在线聊天的后台技术使用了SignalR,上代码,首先是 UniApp 端

<route lang="json5" type="page">
{
  layout: 'default',
  style: {
    navigationBarTitleText: '',
  },
}
</route>
<template>
  <view class="chat">
    <!-- 顶部标题 -->
    <view class="topTabbar">
      <!-- 返回按钮 -->
      <wd-button
        class="back-button"
        type="text"
        icon="arrow-left"
        size="small"
        @click="goback()"
      ></wd-button>
      <wd-row>
        <wd-col :span="24" style="text-align: center; padding: 0 60rpx">
          {{ recruiterInfo ? `${recruiterInfo.surname} ${recruiterInfo.givenName}` : '' }}
        </wd-col>
      </wd-row>
      <wd-row>
        <wd-tabbar v-model="tabbar" @change="handleChange">
          <wd-tabbar-item
            :title="$t('chat.message.exchangeContacts')"
            icon="phone"
          ></wd-tabbar-item>
          <wd-tabbar-item :title="$t('chat.message.pin')" icon="pin"></wd-tabbar-item>
          <wd-tabbar-item :title="$t('chat.message.unsuitable')" icon="close"></wd-tabbar-item>
          <wd-tabbar-item :title="$t('chat.message.report')" icon="warning"></wd-tabbar-item>
          <wd-tabbar-item
            :title="$t('chat.message.blacklist')"
            icon="usergroup-clear"
          ></wd-tabbar-item>
        </wd-tabbar>
      </wd-row>
    </view>
    <scroll-view
      :style="{ height: `calc(100vh + ${2 * inputHeight}rpx)` }"
      id="scrollview"
      scroll-y
      :scroll-top="scrollTop"
      class="scroll-view"
    >
      <!-- 聊天主体 -->
      <view id="msglistview" class="chat-body">
        <!-- 聊天记录 -->
        <view v-for="(item, index) in msgList" :key="item.id">
          <!-- 系统消息 -->
          <view class="item system" v-if="item.isSystem">
            <view class="content system">
              {{ item.content }}
            </view>
          </view>
          <!-- 自己发的消息 -->
          <view class="item self" v-else-if="item.isSelf">
            <!-- 文字内容 -->
            <view class="content right">
              {{ item.content }}
            </view>
            <!-- 头像 -->
            <wd-img
              class="avatar"
              :src="item.image || '/static/images/default-avatar.png'"
              width="78rpx"
              height="78rpx"
              radius="50%"
            ></wd-img>
          </view>
          <!-- 对方发的消息 -->
          <view class="item Ai" v-else>
            <!-- 头像 -->
            <wd-img
              class="avatar"
              :src="item.image || '/static/images/default-avatar.png'"
              width="78rpx"
              height="78rpx"
              radius="50%"
            ></wd-img>
            <!-- 文字内容 -->
            <view class="content left">
              {{ item.content }}
            </view>
          </view>
        </view>
      </view>
    </scroll-view>
    <!-- 底部消息发送栏 -->
    <!-- 用来占位,防止聊天消息被发送框遮挡 -->
    <view class="chat-bottom" :style="{ height: `${inputHeight}rpx` }">
      <view class="send-msg" :style="{ bottom: `${keyboardHeight - 60}rpx` }">
        <view class="uni-textarea">
          <div class="textarea-container">
            <!-- <button class="embed-btn left-btn" @click="handleEmbedButtonClick">
              <wd-icon name="chat1" size="22px"></wd-icon>
            </button> -->
            <wd-button type="icon" icon="chat1" @click="handleEmbedButtonClick"></wd-button>
            <textarea
              v-model="chatMsg"
              maxlength="300"
              confirm-type="send"
              @confirm="handleSend"
              :placeholder="$t('chat.message.placeholder')"
              :show-confirm-bar="false"
              :adjust-position="false"
              @linechange="sendHeight"
              @focus="focus"
              @blur="blur"
              auto-height
            ></textarea>
            <wd-button type="icon" icon="dong" @click="toggleEmojiPicker"></wd-button>
          </div>
        </view>
        <button @click="handleSend" class="send-btn">{{ $t('chat.message.sendBtn') }}</button>
      </view>
    </view>

    <!-- emoji表情选择器 -->
    <view v-if="showEmojiPicker" class="emoji-picker-container" @click.stop="stopPropagation">
      <scroll-view scroll-y class="emoji-scroll-view">
        <view class="emoji-category">
          <view class="emoji-grid">
            <view
              v-for="emoji in emojiList"
              :key="emoji.name"
              class="emoji-item"
              @click="selectEmoji(emoji, $event)"
            >
              <span class="emoji-char">{{ emoji.char }}</span>
            </view>
          </view>
        </view>
      </scroll-view>
    </view>
    <wd-action-sheet
      :title="$t('chat.commonPhrase.title')"
      v-model="showCommonPhrase"
      @close="closeCommonPhrase"
    >
      <!-- 常用语列表 -->
      <wd-row v-if="!showAddPhraseInput">
        <wd-col span="24" style="text-align: right; height: 60rpx; padding: 0 15rpx; margin: 0px">
          <wd-button type="icon" icon="add-circle" @click="showAddPhraseForm"></wd-button>
        </wd-col>
      </wd-row>

      <!-- 常用语列表项 -->
      <wd-row
        v-if="!showAddPhraseInput && commonPhrases.length > 0"
        v-for="(phrase, index) in commonPhrases"
        :key="index"
      >
        <wd-col span="20" style="padding: 10rpx 15rpx">
          <view class="phrase-item" @click="selectCommonPhrase(phrase)">
            {{ phrase.commonText }}
          </view>
        </wd-col>
        <wd-col span="4" style="padding: 10rpx 5rpx">
          <wd-button
            type="icon"
            icon="delete"
            size="small"
            @click.stop="deleteCommonPhrase(phrase)"
          ></wd-button>
        </wd-col>
      </wd-row>

      <!-- 空状态提示 -->
      <wd-row v-if="!showAddPhraseInput && commonPhrases.length === 0">
        <wd-col span="24" style="text-align: center; padding: 30rpx 0">
          <text style="color: #999">{{ $t('chat.commonPhrase.empty') }}</text>
        </wd-col>
      </wd-row>

      <!-- 添加常用语表单 -->
      <wd-row v-if="showAddPhraseInput">
        <wd-col span="24" style="padding: 15rpx">
          <wd-input
            v-model="newPhraseInput"
            :placeholder="$t('chat.commonPhrase.addPlaceholder')"
          ></wd-input>
        </wd-col>
        <wd-col span="24" style="text-align: right; padding: 10rpx 15rpx">
          <wd-button
            size="small"
            type="success"
            @click="showAddPhraseInput = false"
            style="margin-right: 10px"
          >
            {{ $t('common.cancel') }}
          </wd-button>
          <wd-button size="small" type="primary" @click="addCommonPhrase">
            {{ $t('common.confirm') }}
          </wd-button>
        </wd-col>
      </wd-row>
    </wd-action-sheet>
  </view>
</template>
<script lang="ts" setup>
import { ref, computed, onUpdated, Ref, onMounted } from 'vue'
import { useUserStore } from '@/store'
import { storeToRefs } from 'pinia'
import { getCommonPhrases, createCommonPhrase, removeCommonPhrase } from '@/api/commonPhrase'
import { useToast } from 'wot-design-uni'
import { i18n } from '@/locale/index'
import * as signalR from '@microsoft/signalr'
import { useMessage } from 'wot-design-uni'
import type { CommonPhraseEntity } from '@/api/commonPhrase.typings'
import { RoleEnum } from '@/typings'

// emoji类型定义
interface Emoji {
  name: string
  char: string
  category: string
}
const message = useMessage()

const tabbar = ref(-1)
// 用户信息
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)

// 常用语相关状态
const showCommonPhrase = ref(false)
// 常用语列表应该是CommonPhraseEntity类型的数组
const commonPhrases = ref<CommonPhraseEntity[]>([])
const newPhraseInput = ref('')
const showAddPhraseInput = ref(false)

// 打开常用语面板
const handleEmbedButtonClick = () => {
  showCommonPhrase.value = true
  loadCommonPhrases()
}

// 关闭常用语面板
const closeommonPhrase = () => {
  showCommonPhrase.value = false
  showAddPhraseInput.value = false
}

// 加载常用语列表
const loadCommonPhrases = async () => {
  try {
    const phrases = await getCommonPhrases(userInfo.value?.id)
    // 确保data始终是数组类型
    commonPhrases.value = Array.isArray(phrases.data)
      ? phrases.data
      : phrases.data
        ? [phrases.data]
        : []
  } catch (error) {
    console.error('加载常用语失败:', error)
  }
}

// 选择常用语
const selectCommonPhrase = (phrase: CommonPhraseEntity) => {
  chatMsg.value += phrase.commonText
  showCommonPhrase.value = false
}

// 显示添加常用语输入框
const showAddPhraseForm = () => {
  showAddPhraseInput.value = true
  newPhraseInput.value = ''
}

// 添加新常用语
const addCommonPhrase = async () => {
  if (!newPhraseInput.value.trim()) {
    return
  }

  // 获取当前用户角色
  const currentRole = Number(userInfo.value?.currentRole) || 0

  try {
    // 构造CommonPhraseEntity类型的参数
    const newPhrase: CommonPhraseEntity = {
      Id: 0,
      UserId: userInfo.value?.id || 0,
      Role: currentRole,
      CommonText: newPhraseInput.value.trim(),
      SortOrder: commonPhrases.value.length + 1,
      LastEditTime: new Date().toISOString(),
    }

    await createCommonPhrase(newPhrase)
    newPhraseInput.value = ''
    loadCommonPhrases()
    showAddPhraseInput.value = false
  } catch (error) {
    console.error('添加常用语失败:', error)
  }
}

// 删除常用语
const deleteCommonPhrase = async (phrase: CommonPhraseEntity) => {
  try {
    await removeCommonPhrase(phrase.id)
    loadCommonPhrases()
  } catch (error) {
    console.error('删除常用语失败:', error)
  }
}

// SignalR连接对象
let hubConnection: signalR.HubConnection | null = null

// 连接状态
const connectionState = ref<signalR.HubConnectionState>(signalR.HubConnectionState.Disconnected)

// SignalR URL
const hubUrl = `${import.meta.env.VITE_SERVER_BASEURL}/chatHub`

// 启动连接
async function startConnection() {
  // 防止重复连接请求
  if (
    connectionState.value === signalR.HubConnectionState.Connecting ||
    connectionState.value === signalR.HubConnectionState.Connected
  ) {
    console.log(`Connection already in progress or connected: ${connectionState.value}`)
    return
  }

  connectionState.value = signalR.HubConnectionState.Connecting

  // 关闭已有的连接
  if (hubConnection) {
    console.log('Closing existing connection...')
    await hubConnection.stop()
    hubConnection = null
    console.log('Existing connection closed')
  }

  try {
    // 检查token是否存在
    const token = uni.getStorageSync('token')
    if (!token) {
      console.warn('No authentication token found')
      toast.show('未找到认证信息,请重新登录')
      return
    }

    // 验证hubUrl
    if (!hubUrl) {
      console.error('Hub URL is not defined')
      toast.show('服务器地址未配置')
      return
    }

    // 创建新的SignalR连接
    hubConnection = new signalR.HubConnectionBuilder()
      .withUrl(hubUrl, {
        accessTokenFactory: () => token,
        skipNegotiation: true,
        transport: signalR.HttpTransportType.WebSockets,
      })
      .withAutomaticReconnect()
      .build()

    // 接收消息事件
    hubConnection.on('ReceiveMessage', (senderId, message) => {
      const newMessage: MessageItem = {
        id: Date.now().toString(),
        senderId: senderId.toString(),
        receiverId: currentUserId.value,
        content: message,
        sentTime: new Date().toISOString(),
        isSelf: senderId.toString() === currentUserId.value.toString(),
        image: recruiterInfo.value?.image || '',
      }
      msgList.value.push(newMessage)
      // 滚动到底部
      setTimeout(() => {
        scrollToBottom().catch((err) =>
          console.error('Error in ReceiveMessage scrollToBottom:', err),
        )
      }, 100)
    })

    // 接收交换联系方式请求事件
    hubConnection.on('ReceiveContactRequest', (senderId, senderInfo) => {
      // 只有当当前页面是与请求方的聊天界面时才弹出对话框
      if (receiverId.value && receiverId.value.toString() === senderId.toString()) {
        message
          .confirm({
            msg: i18n.global.t('chat.message.receiveContactRequestMsg', {
              senderName: senderInfo?.name || '对方',
            }),
            title: i18n.global.t('chat.message.receiveContactRequestTitle'),
          })
          .then(() => {
            // 同意交换联系方式
            sendSocketMessage('ApproveContactRequest', senderId).then((success) => {
              if (success) {
                toast.show(i18n.global.t('chat.message.approveContactSuccess'))
                // 获取对方联系方式
                if (senderInfo?.contactInfo) {
                  // 这里可以添加显示对方联系方式的逻辑
                  toast.show(`已获取对方联系方式: ${senderInfo.contactInfo}`)
                }
              } else {
                toast.show(i18n.global.t('chat.message.approveContactFailed'))
              }
            })
          })
          .catch(() => {
            // 拒绝交换联系方式
            sendSocketMessage('RejectContactRequest', senderId)
            toast.show(i18n.global.t('chat.message.rejectContactSuccess'))
          })
      }
    })

    // 接收同意交换联系方式响应事件
    hubConnection.on('ContactRequestApproved', (senderId, contactInfo) => {
      if (receiverId.value && receiverId.value.toString() === senderId.toString()) {
        // 创建交换联系方式成功系统消息
        const exchangeSuccessMessage: MessageItem = {
          id: Date.now().toString(),
          senderId: 'system',
          receiverId: '',
          content: i18n.global.t('chat.message.exchangeContactsSuccess'),
          sentTime: new Date().toISOString(),
          isSelf: false,
          isSystem: true,
          image: '',
        }
        msgList.value.push(exchangeSuccessMessage)

        if (contactInfo) {
          // 创建获取联系方式系统消息
          const contactInfoMessage: MessageItem = {
            id: Date.now().toString() + '_contact',
            senderId: 'system',
            receiverId: '',
            content: i18n.global.t('chat.message.getContactInfoSuccess', { contactInfo }),
            sentTime: new Date().toISOString(),
            isSelf: false,
            isSystem: true,
            image: '',
          }
          msgList.value.push(contactInfoMessage)
        }
      }
    })

    // 接收拒绝交换联系方式响应事件
    hubConnection.on('ContactRequestRejected', (senderId) => {
      if (receiverId.value && receiverId.value.toString() === senderId.toString()) {
        // 创建联系方式请求被拒绝系统消息
        const rejectedMessage: MessageItem = {
          id: Date.now().toString(),
          senderId: 'system',
          receiverId: '',
          content: i18n.global.t('chat.message.contactRequestRejected'),
          sentTime: new Date().toISOString(),
          isSelf: false,
          isSystem: true,
          image: '',
        }
        msgList.value.push(rejectedMessage)
      }
    })

    // 监听被阻断事件(防骚扰机制)
    hubConnection.on('Blocked', (message) => {
      // 创建系统消息
      const systemMessage: MessageItem = {
        id: Date.now().toString(),
        senderId: 'system',
        receiverId: '',
        content: i18n.global.t('chat.message.blocked'),
        sentTime: new Date().toISOString(),
        isSelf: false,
        isSystem: true,
        image: '',
      }
      msgList.value.push(systemMessage)
      // 滚动到底部
      setTimeout(() => {
        scrollToBottom().catch((err) => console.error('Error in Blocked scrollToBottom:', err))
      }, 100)
    })

    // 监听黑名单消息阻断事件
    hubConnection.on('BlacklistMessageBlocked', (type) => {
      // 根据类型选择不同的国际化提示
      const content =
        Number(type) === 1
          ? i18n.global.t('chat.message.youAddedBlacklist')
          : i18n.global.t('chat.message.otherAddedBlacklist')

      // 创建系统消息
      const systemMessage: MessageItem = {
        id: Date.now().toString(),
        senderId: 'system',
        receiverId: '',
        content,
        sentTime: new Date().toISOString(),
        isSelf: false,
        isSystem: true,
        image: '',
      }
      msgList.value.push(systemMessage)
      // 滚动到底部
      setTimeout(() => {
        scrollToBottom().catch((err) =>
          console.error('Error in BlacklistMessageBlocked scrollToBottom:', err),
        )
      }, 100)
    })

    // 监听联系方式请求已发送事件
    hubConnection.on('ContactRequestAlreadySent', (senderId) => {
      // 设置标志,表示已接收到此事件
      contactRequestAlreadySent.value = true

      const contactAlreadySentMessage: MessageItem = {
        id: Date.now().toString(),
        senderId: 'system',
        receiverId: '',
        content: i18n.global.t('chat.message.contactAlreadyExchanged'),
        sentTime: new Date().toISOString(),
        isSelf: false,
        isSystem: true,
        image: '',
      }
      msgList.value.push(contactAlreadySentMessage)
      // 滚动到底部
      setTimeout(() => {
        scrollToBottom().catch((err) =>
          console.error('Error in ContactRequestAlreadySent scrollToBottom:', err),
        )
      }, 100)
    })

    // 监听连接状态变化
    hubConnection.onreconnecting((error) => {
      connectionState.value = signalR.HubConnectionState.Reconnecting
    })

    hubConnection.onreconnected((connectionId) => {
      connectionState.value = signalR.HubConnectionState.Connected
    })

    hubConnection.onclose((error) => {
      connectionState.value = signalR.HubConnectionState.Disconnected
      if (error) {
        toast.show(`Connection lost: ${error.message}. Reconnecting...`)
      }
    })

    // 启动连接
    await hubConnection
      .start()
      .then(() => console.log('Connected to SignalR Hub'))
      .catch((err) => console.error('Connection failed:', err))
    connectionState.value = signalR.HubConnectionState.Connected
  } catch (error) {
    connectionState.value = signalR.HubConnectionState.Disconnected

    // 检查网络状态
    uni.getNetworkType({
      success: (res) => {
        console.log('Network type:', res.networkType)
      },
    })
    console.log(`Failed to connect: ${error instanceof Error ? error.message : String(error)}`)
  }
}

// 发送SignalR消息
async function sendSocketMessage(methodName: string, ...args: any[]) {
  if (!hubConnection || connectionState.value !== signalR.HubConnectionState.Connected) {
    console.error(
      `[${new Date().toISOString()}] Cannot send message: Connection is not in Connected state (current: ${connectionState.value})`,
    )
    toast.show(i18n.global.t('chat.message.notConnected'))
    return false
  }

  try {
    console.log(`[${new Date().toISOString()}] Sending message: ${methodName}`, args)
    // 捕获服务器返回的结果
    const result = await hubConnection.invoke(methodName, ...args)
    console.log(
      `[${new Date().toISOString()}] Message sent successfully: ${methodName}, result:`,
      result,
    )
    // 返回服务器返回的结果
    return result
  } catch (error) {
    console.error(`[${new Date().toISOString()}] Failed to send message: ${methodName}`, error)
    toast.show(i18n.global.t('chat.message.sendError'))
    return false
  }
}

type MessageItem = {
  id: string
  senderId: string
  receiverId: string
  content: string
  sentTime: string
  isSelf: boolean
  isSystem?: boolean
  image: string
}

type RecruiterInfo = {
  id: string
  surname: string
  givenName: string
  image?: string
}

// 状态定义
const keyboardHeight = ref(0)
const bottomHeight = ref(0)
const scrollTop = ref(0)
const chatMsg = ref('')
const recruiterInfo = ref<RecruiterInfo | null>(null)
const msgList = ref<MessageItem[]>([])
const isBlocked = ref(false)
const blockedMessage = ref('')
// 用于跟踪是否已接收到"联系方式已交换"的事件
const contactRequestAlreadySent = ref(false)
// 用于控制emoji表情选择器的显示和隐藏
const showEmojiPicker = ref(false)
// emoji列表
const emojiList = ref<Emoji[]>([])

const toast = useToast()

// 切换emoji表情选择器的显示和隐藏
const toggleEmojiPicker = () => {
  showEmojiPicker.value = !showEmojiPicker.value
  // 关闭常用语面板
  showCommonPhrase.value = false
}

// 阻止表情选择器内部点击事件冒泡
const stopPropagation = (event: Event) => {
  event.stopPropagation()
}

// 选择emoji并添加到输入框
const selectEmoji = (emoji: Emoji, event: Event) => {
  // 阻止事件冒泡,防止触发其他关闭表情面板的逻辑
  event.stopPropagation()
  chatMsg.value += emoji.char
  // 点击表情后关闭表情面板
  showEmojiPicker.value = false
  // 滚动到底部
  setTimeout(() => {
    scrollToBottom().catch((err) => console.error('Error in selectEmoji scrollToBottom:', err))
  }, 100)
}

// 从API加载emoji数据
const loadEmojiData = async () => {
  try {
    const response = await fetch('https://unpkg.com/emoji.json@16.0.0/emoji.json')
    const data = await response.json()

    // 打印第一个item查看数据结构
    console.log('API返回的emoji数据结构:', data[0])

    // 处理emoji数据,转换为我们需要的格式
    emojiList.value = data
      .slice(0, 100)
      .map((item: any) => ({
        name: item.slug || item.name || 'emoji',
        char: item.character || item.char || '',
        category: item.category || 'unknown',
      }))
      .filter((emoji: Emoji) => emoji.char)

    console.log('处理后的emoji列表:', emojiList.value)
  } catch (error) {
    console.error('加载emoji数据失败:', error)
    // 添加一些默认emoji作为备用
    emojiList.value = [
      { name: 'smile', char: '😊', category: 'face' },
      { name: 'heart', char: '❤️', category: 'heart' },
      { name: 'thumbsup', char: '👍', category: 'hand' },
      { name: 'laugh', char: '😂', category: 'face' },
      { name: 'love', char: '😍', category: 'face' },
      { name: 'clap', char: '👏', category: 'hand' },
    ]
  }
}

// 在组件挂载时加载emoji数据
onMounted(() => {
  loadEmojiData()
})

// 当前用户ID (从本地存储获取,实际应用中应从身份验证系统获取)
const currentUserId = ref('')

// 从本地存储获取用户ID
function getCurrentUserId() {
  try {
    const userInfo = uni.getStorageSync('userInfo')
    if (userInfo) {
      currentUserId.value = userInfo.id || ''
    }
  } catch (e) {
    console.error('Error getting user info:', e)
  }
}

// 对方用户ID
const receiverId = ref('')

// 计算属性
const windowHeight = computed(() => {
  return rpxTopx(uni.getSystemInfoSync().windowHeight)
})

const inputHeight = computed(() => {
  return bottomHeight.value + keyboardHeight.value
})

// Define the keyboard height change handler
const keyboardHeightChangeHandler = (res: any) => {
  keyboardHeight.value = rpxTopx(res.height)
  if (keyboardHeight.value < 0) keyboardHeight.value = 0
}

// 生命周期
onLoad(() => {
  // 获取当前用户ID
  getCurrentUserId()

  // 获取页面参数
  const pages = getCurrentPages()
  const currentPage = pages[pages.length - 1]
  const options = currentPage.options

  // 解析传递过来的招聘者信息
  if (options && options.recruiterInfo) {
    try {
      const decodedInfo = decodeURIComponent(options.recruiterInfo)
      recruiterInfo.value = JSON.parse(decodedInfo)
      receiverId.value = recruiterInfo.value?.id || ''
    } catch (error) {
      console.error('Error parsing recruiterInfo:', error)
    }
  } else {
    console.error('No recruiterInfo in options')
    toast.show('未找到招聘者信息')
  }

  // 初始化消息列表
  initMsgList()

  // 启动 WebSocket 连接
  startConnection()
  // 重新连接事件监听
  function onReconnecting() {
    console.log('WebSocket reconnecting...')
    connectionState.value = signalR.HubConnectionState.Reconnecting
    toast.show('Reconnecting to server...')
  }

  // 重新连接成功事件监听 (模拟)
  function onReconnected() {
    console.log('WebSocket reconnected')
    connectionState.value = signalR.HubConnectionState.Connected
    toast.show('Reconnected to server.')
  }

  // Register the event listener
  uni.onKeyboardHeightChange(keyboardHeightChangeHandler)
})

onUnload(async () => {
  if (hubConnection) {
    try {
      await hubConnection.stop()
      hubConnection = null
      console.log('Disconnected from SignalR server')
    } catch (error) {
      console.error('Error closing SignalR connection:', error)
    }
  }
  // Unregister the keyboard height change handler
  uni.offKeyboardHeightChange(keyboardHeightChangeHandler)
})

onUpdated(() => {
  // 正确处理异步函数
  scrollToBottom().catch((err) => console.error('Error in onUpdated scrollToBottom:', err))
})

// 引入用户API
import { getUserInfoById } from '@/api/login'
// 引入联系人关系API
import { addToBlacklist, markAsNotSuitable, togglePinOrFollow } from '@/api/contactRelationship'

// 验证接收者ID是否存在
async function verifyReceiverId(receiverId: string) {
  console.log(`=== Verifying receiverId: ${receiverId} ===`)
  try {
    // 使用现有API检查用户是否存在
    const userInfo = await getUserInfoById(parseInt(receiverId))
    const exists = !!userInfo
    console.log(`Receiver verification result: ${exists ? 'Exists' : 'Does not exist'}`)
    return exists
  } catch (error) {
    console.error(`Failed to verify receiverId: ${receiverId}`, error)
    return false
  }
}

// 初始化消息列表
function initMsgList() {
  // 实际应用中应从API获取历史消息
  msgList.value = []
}

// 方法定义
function goback() {
  uni.switchTab({
    url: '/pages/tutorship/tutorship',
  })
}

function focus() {
  // 正确处理异步函数
  scrollToBottom().catch((err) => console.error('Error in focus scrollToBottom:', err))
}

// 滚动到底部
async function scrollToBottom() {
  console.log('scrollToBottom function called')
  try {
    // 等待DOM更新
    await new Promise((resolve) => setTimeout(resolve, 50))
    console.log('DOM updated, proceeding with scroll')
    // 获取滚动元素和内容元素
    const query = uni.createSelectorQuery().in(getCurrentInstance())
    query.select('.scroll-view').boundingClientRect()
    query.select('.chat-body').boundingClientRect()
    const res = await query.exec()
    if (res && res[0] && res[1]) {
      console.log('Scrolling to bottom with values:', res[1].height, res[0].height)
      // 直接使用像素值,不进行rpx转换
      scrollTop.value = res[1].height - res[0].height + 40 // 增加偏移量到40像素
      console.log('Set scrollTop to:', scrollTop.value)
    } else {
      console.warn('Could not get element dimensions for scrolling')
    }
  } catch (err) {
    console.error('Error scrolling to bottom:', err)
  }
}

function blur() {
  scrollToBottom()
}

// px转换成rpx
function rpxTopx(px: number): number {
  const deviceWidth = uni.getSystemInfoSync().windowWidth
  const rpx = (750 / deviceWidth) * Number(px)
  return Math.floor(rpx)
}

// 监视聊天发送栏高度
function sendHeight() {
  setTimeout(() => {
    const query = uni.createSelectorQuery()
    query.select('.send-msg').boundingClientRect()
    query.exec((res) => {
      if (res && res[0]) {
        bottomHeight.value = rpxTopx(res[0].height)
      }
    })
  }, 10)
}

// 发送消息
async function handleSend() {
  //如果消息不为空
  if (chatMsg.value && !/^\s+$/.test(chatMsg.value)) {
    if (!receiverId.value) {
      toast.show(i18n.global.t('chat.message.selectReceiver'))
      return
    }

    // 检查连接状态
    const isConnected = connectionState.value === signalR.HubConnectionState.Connected
    if (!isConnected) {
      if (connectionState.value === signalR.HubConnectionState.Connecting) {
        toast.show('Connecting to server. Please wait...')
      } else {
        toast.show('Connection not established. Reconnecting...')
        // 尝试重新连接
        startConnection()
      }
      return
    }

    try {
      // 发送消息到服务器
      const success = await sendSocketMessage(
        'SendPrivateMessage',
        receiverId.value.toString(),
        chatMsg.value,
      )
      console.log(`success:`, success)
      if (success) {
        // 创建新消息并添加到列表
        const newMessage: MessageItem = {
          id: Date.now().toString(),
          senderId: currentUserId.value,
          receiverId: receiverId.value,
          content: chatMsg.value,
          sentTime: new Date().toISOString(),
          isSelf: true,
          image: userInfo.value?.image || '/static/images/default-avatar.png',
        }
        msgList.value.push(newMessage)

        // 清空输入框
        chatMsg.value = ''

        // 滚动到底部
        setTimeout(() => {
          scrollToBottom().catch((err) => console.error('Error in handleSend scrollToBottom:', err))
        }, 100)
      } else {
        //toast.show('Failed to send message. Please try again.')
      }
    } catch (err) {
      console.error('Error sending message:', err)
      //toast.show('Failed to send message. Please try again.')
    }
  } else {
    toast.show(i18n.global.t('chat.message.emptyError'))
  }
}

// 回复消息
function handleReply(senderId: string) {
  receiverId.value = senderId
  blockedMessage.value = ''

  // 聚焦到输入框
  const textarea = uni.createSelectorQuery().select('textarea')
  textarea.focus()
}

function handleChange({ value }: { value: string }) {
  const tabIndex = parseInt(value)
  switch (tabIndex) {
    case 0:
      // 交换联系方式
      handleExchangeContacts()
      break
    case 1:
      // 置顶
      handlePinConversation()
      break
    case 2:
      // 不合适
      handleMarkUnsuitable()
      break
    case 3:
      // 举报
      handleReport()
      break
    case 4:
      // 黑名单
      handleAddToBlacklist()
      break
    default:
      console.warn(`Unknown tab index: ${tabIndex}`)
  }
}

// 交换联系方式处理函数
function handleExchangeContacts() {
  // 在发送请求前重置标志
  contactRequestAlreadySent.value = false

  message
    .confirm({
      msg: i18n.global.t('chat.message.confirmExchangeContacts'),
      title: i18n.global.t('chat.message.exchangeContactsTitle'),
    })
    .then(() => {
      // 发送交换联系方式请求
      if (receiverId.value) {
        sendSocketMessage('ExchangeContactRequest', receiverId.value).then((success) => {
          if (success) {
            // 创建交换联系方式请求成功系统消息
            const requestSuccessMessage: MessageItem = {
              id: Date.now().toString(),
              senderId: 'system',
              receiverId: '',
              content: i18n.global.t('chat.message.requestSendSuccess'),
              sentTime: new Date().toISOString(),
              isSelf: false,
              isSystem: true,
              image: '',
            }
            msgList.value.push(requestSuccessMessage)
          } else {
            // 如果是因为"已交换过联系方式"而失败,则不显示失败消息
            // 因为ContactRequestAlreadySent事件处理器会显示专门的消息
            if (!contactRequestAlreadySent.value) {
              // 创建交换联系方式请求失败系统消息
              const requestFailedMessage: MessageItem = {
                id: Date.now().toString(),
                senderId: 'system',
                receiverId: '',
                content: i18n.global.t('chat.message.requestSendFailed'),
                sentTime: new Date().toISOString(),
                isSelf: false,
                isSystem: true,
                image: '',
              }
              msgList.value.push(requestFailedMessage)
            } else {
              // 重置标志,以便下一次操作
              contactRequestAlreadySent.value = false
            }
          }
        })
      } else {
        // 创建未找到接收方信息系统消息
        const noReceiverMessage: MessageItem = {
          id: Date.now().toString(),
          senderId: 'system',
          receiverId: '',
          content: '未找到接收方信息',
          sentTime: new Date().toISOString(),
          isSelf: false,
          isSystem: true,
          image: '',
        }
        msgList.value.push(noReceiverMessage)
      }
    })
    .catch(() => {})
}

// 置顶处理函数
function handlePinConversation() {
  message
    .confirm({
      msg: i18n.global.t('chat.message.confirmPinConversation'),
      title: i18n.global.t('chat.message.pinConversationTitle'),
    })
    .then(() => {
      if (!currentUserId.value || !receiverId.value) {
        toast.error(i18n.global.t('chat.message.selectReceiver'))
        return
      }

      // 调用togglePinOrFollow接口
      togglePinOrFollow(parseInt(currentUserId.value), parseInt(receiverId.value), true)
        .then((result) => {
          if (result.Success) {
            toast.show(i18n.global.t('chat.message.pinConversationSuccess'))
          } else {
            toast.error(result.Message || i18n.global.t('message.relation.togglePinOrFollowError'))
          }
        })
        .catch(() => {
          toast.error(i18n.global.t('message.relation.togglePinOrFollowError'))
        })
    })
    .catch(() => {})
}

// 标记不合适处理函数
function handleMarkUnsuitable() {
  message
    .confirm({
      msg: i18n.global.t('chat.message.confirmMarkUnsuitable'),
      title: i18n.global.t('chat.message.markUnsuitableTitle'),
    })
    .then(() => {
      if (!currentUserId.value || !receiverId.value) {
        toast.error(i18n.global.t('chat.message.selectReceiver'))
        return
      }

      markAsNotSuitable(parseInt(currentUserId.value), parseInt(receiverId.value))
        .then((result) => {
          if (result.Success) {
            toast.show(i18n.global.t('chat.message.markUnsuitableSuccess'))
          } else {
            toast.error(result.Message || i18n.global.t('message.relation.markNotSuitableError'))
          }
        })
        .catch(() => {
          toast.error(i18n.global.t('message.relation.markNotSuitableError'))
        })
    })
    .catch(() => {})
}

// 举报处理函数
function handleReport() {
  message
    .confirm({
      msg: i18n.global.t('chat.message.confirmReport'),
      title: i18n.global.t('chat.message.reportTitle'),
    })
    .then(() => {
      // 跳转到举报页面,并传递用户ID参数
      uni.navigateTo({
        url: `/pages/chat/report?currentUserId=${currentUserId.value}&receiverId=${receiverId.value}`,
      })
    })
    .catch(() => {})
}

// 添加到黑名单处理函数
function handleAddToBlacklist() {
  message
    .confirm({
      msg: i18n.global.t('chat.message.confirmAddToBlacklist'),
      title: i18n.global.t('chat.message.addToBlacklistTitle'),
    })
    .then(() => {
      if (!receiverId.value) {
        toast.show(i18n.global.t('chat.message.selectReceiver'))
        return
      }

      addToBlacklist(parseInt(currentUserId.value), parseInt(receiverId.value))
        .then((result) => {
          if (result.Success) {
            toast.show(i18n.global.t('chat.message.addToBlacklistSuccess'))
            // 更新黑名单状态
            blockedMessage.value = i18n.global.t('chat.message.youAddedBlacklist')
          } else {
            toast.error(result.Message || i18n.global.t('message.relation.blacklistError'))
          }
        })
        .catch((error) => {
          console.error('Failed to add to blacklist:', error)
          toast.error(i18n.global.t('message.relation.blacklistError'))
        })
    })
    .catch(() => {})
}
// 关闭常用语面板
const closeCommonPhrase = () => {
  showCommonPhrase.value = false
  showAddPhraseInput.value = false
}
</script>
<style lang="scss" scoped>
/* emoji表情选择器样式 */
.emoji-picker-container {
  position: fixed;
  bottom: 100rpx;
  left: 0;
  right: 0;
  z-index: 9999;
  background-color: #fff;
  border-top: 1px solid #eee;
  height: 500rpx;
}

.emoji-scroll-view {
  height: 100%;
}

.emoji-category {
  padding: 20rpx;
}

.emoji-grid {
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  gap: 10rpx;
}

.emoji-item {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 80rpx;
  cursor: pointer;
}

.emoji-char {
  font-size: 44rpx;
  line-height: 1;
  display: inline-block;
}

.emoji-item:active {
  background-color: #f0f0f0;
  border-radius: 8rpx;
}

.emoji-btn {
  width: 80rpx;
  height: 80rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  color: #666;
  margin-left: 10rpx;
}
.wd-action-sheet__header {
  height: 50px !important;
}

/* 常用语样式 */
.phrase-item {
  padding: 10rpx;
  background-color: #f5f5f5;
  border-radius: 8rpx;
  font-size: 28rpx;
  color: #333;
}

.phrase-item:hover {
  background-color: #e6e6e6;
}

wd-input {
  width: 100%;
}

wd-button[size='small'] {
  margin-left: 10rpx;
}
.uni-scroll-view-content {
  background-color: #f6f6f6;
}

$chatContentbgc: #c2dcff;
$sendBtnbgc: #4f7df5;

view,
button,
text,
input,
textarea {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

/* 聊天消息 */
.chat {
  height: 100%;
  .topTabbar {
    width: 100%;
    height: auto;
    min-height: 90rpx;
    display: flex;
    flex-direction: column;
    position: fixed;
    top: 0;
    left: 0;
    background-color: #fff;
    z-index: 999;
    padding: 0 20rpx;

    .back-button {
      position: absolute;
      left: 0rpx;
      top: 0rpx;
      padding: 10rpx;
      z-index: 1000;
    }
    .back-button:active {
      background-color: rgba(0, 0, 0, 0.1);
      border-radius: 50%;
    }
  }
  .scroll-view {
    // 移动背景颜色声明到嵌套规则之前
    background-color: #f6f6f6;
    margin-top: 90rpx; // 为顶部导航栏腾出空间
    padding-right: 20rpx; // 增加右侧padding到20rpx,避免内容被滚动条遮挡

    // 只在聊天记录区域显示滚动条
    ::-webkit-scrollbar {
      width: 6rpx;
      height: 0 !important;
      -webkit-appearance: none;
      background: transparent;
    }

    ::-webkit-scrollbar-thumb {
      border-radius: 3rpx;
      background-color: rgba(0, 0, 0, 0.2);
    }
    // background-color: orange;
    .chat-body {
      display: flex;
      flex-direction: column;
      padding-top: 23rpx;
      padding-bottom: 100rpx;

      // background-color:skyblue;

      .self {
        justify-content: flex-end;
      }
      .item {
        display: flex;
        padding: 23rpx 30rpx;
        // background-color: greenyellow;

        &.system {
          justify-content: center;
        }

        .right {
          background-color: $chatContentbgc;
        }
        .left {
          background-color: #ffffff;
        }
        .system {
          background-color: #f0f0f0;
          color: #666666;
          text-align: center;
        }
        // 聊天消息的三角形
        .right::after {
          position: absolute;
          display: inline-block;
          content: '';
          width: 0;
          height: 0;
          left: 100%;
          top: 10px;
          border: 12rpx solid transparent;
          border-left: 12rpx solid $chatContentbgc;
        }

        .left::after {
          position: absolute;
          display: inline-block;
          content: '';
          width: 0;
          height: 0;
          top: 10px;
          right: 100%;
          border: 12rpx solid transparent;
          border-right: 12rpx solid #ffffff;
        }

        .content {
          position: relative;
          max-width: 486rpx;
          border-radius: 8rpx;
          word-wrap: break-word;
          padding: 24rpx 24rpx;
          margin: 0 24rpx;
          border-radius: 5px;
          font-size: 32rpx;
          font-family: PingFang SC;
          font-weight: 500;
          color: #333333;
          line-height: 42rpx;
        }

        .reply-btn {
          margin-top: 8rpx;
          font-size: 24rpx;
          color: #4f7df5;
          text-align: right;
          padding-right: 8rpx;
        }

        .avatar {
          display: flex;
          justify-content: center;
          width: 78rpx;
          height: 78rpx;
          background: $sendBtnbgc;
          border-radius: 50rpx;
          overflow: hidden;

          image {
            align-self: center;
          }
        }
      }
    }
  }

  /* 底部聊天发送栏 */
  .chat-bottom {
    width: 100%;
    height: 100rpx;
    background: #f4f5f7;
    transition: all 0.1s ease;
    position: fixed;
    bottom: 0;
    left: 0;
    z-index: 1;

    .send-msg {
      display: flex;
      align-items: center;
      padding: 16rpx 30rpx;
      width: 100%;
      min-height: 100rpx;
      background: #fff;
      transition: all 0.1s ease;
      z-index: 1;
    }

    .uni-textarea {
      padding-bottom: 0rpx;
      .textarea-container {
        display: flex;
        align-items: center;
      }
      .embed-btn.left-btn {
        margin-right: 10rpx;
        width: 120rpx;
        height: 60rpx;
        background: #f1f1f1;
        border-radius: 30rpx;
        font-size: 24rpx;
        color: #333333;
        display: flex;
        align-items: center;
        justify-content: center;
        border: none;
      }
      textarea {
        width: 417rpx;
        min-height: 60rpx;
        max-height: 500rpx;
        background: #f1f1f1;
        border-radius: 30rpx;
        font-size: 28rpx;
        font-family: PingFang SC;
        color: #333333;
        line-height: 60rpx;
        padding: 5rpx 8rpx;
        text-indent: 30rpx;
      }
    }

    .send-btn {
      display: flex;
      align-items: center;
      justify-content: center;
      margin-bottom: 0rpx;
      margin-left: 25rpx;
      width: 120rpx;
      height: 60rpx;
      background: #ed5a65;
      border-radius: 30rpx;
      font-size: 24rpx;
      font-family: PingFang SC;
      font-weight: 700;
      color: #ffffff;
      line-height: 24rpx;
      z-index: 1;
      outline: 2rpx solid #ffffff;
      box-shadow: 0 2rpx 10rpx rgba(237, 90, 101, 0.5);
    }
  }
}
</style>

Asp.net Core 部分,自然是创建一个 SignalR 的一个 Hub

using CacheManager.Core;
using FreeWorking.Business.App;
using FreeWorking.Domain;
using FreeWorking.Domain.Attribute;
using FreeWorking.Domain.Entities.App;
using FreeWorking.Domain.Enums;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;

namespace FreeWorking.App.Api.SignalR
{
    [AuthorizeRoles(RoleEnum.JobSeeker, RoleEnum.Recruiter)]
    public class ChatHub : Hub
    {
        private readonly ChatMessageService _chatMessageService;
        private readonly ContactRequestService _contactRequestService;
        private readonly IFreeWorkingDatabase _database;
        private readonly ICacheManager<object> _cache;
        private readonly ContactedRelationshipService _contactedRelationshipService;
        private readonly NotContactedRelationshipService _notContactedRelationshipService;

        public ChatHub(ChatMessageService chatMessageService, ContactRequestService contactRequestService, IFreeWorkingDatabase database, ICacheManager<object> cache, ContactedRelationshipService contactedRelationshipService, NotContactedRelationshipService notContactedRelationshipService)
        {
            _chatMessageService = chatMessageService;
            _contactRequestService = contactRequestService;
            _database = database;
            _cache = cache;
            _contactedRelationshipService = contactedRelationshipService;
            _notContactedRelationshipService = notContactedRelationshipService;
            OnlineUsers.Initialize(cache); // 初始化在线用户管理的缓存
        }

        // 发送交换联系方式请求
        public async Task<bool> ExchangeContactRequest(long receiverId)
        {
            var senderId = Context.UserIdentifier!;
            var findSender = await _database.Set<UserEntity>().FirstOrDefaultAsync(u => u.Id == receiverId);
            var senderRole = findSender!.CurrentRole;

            // 获取用户信息
            var senderUser = await _database.Set<UserEntity>().FirstOrDefaultAsync(u => u.Id.ToString() == senderId);
            var receiverUser = await _database.Set<UserEntity>().FirstOrDefaultAsync(u => u.Id == receiverId);

            // 检查黑名单关系
            if (!await CheckBlacklistRelationship(senderUser, receiverUser))
            {
                return false;
            }

            // 检查是否已经发送过请求
            var existingRequest = await _contactRequestService.SingleExpressAsync(t =>
                ((t.RecruiterId.ToString() == senderId && t.SeekerId == receiverId) ||
                 (t.RecruiterId == receiverId && t.SeekerId.ToString() == senderId)) &&
                t.ApprovalStatus == ApprovalStatusEnum.Pending &&
                t.RequestItem == RequestItemTypeEnum.ContactInfo);

            if (existingRequest != null)
            {
                await Clients.Caller.SendAsync("ContactRequestAlreadySent");
                return false;
            }

            // 创建新的联系方式请求
            var contactRequest = new ContactRequestEntity
            {
                RecruiterId = senderRole == RoleEnum.Recruiter ? long.Parse(senderId) : receiverId,
                SeekerId = senderRole == RoleEnum.JobSeeker ? long.Parse(senderId) : receiverId,
                RequestItem = RequestItemTypeEnum.ContactInfo,
                InitiatorRole = (RoleEnum)senderRole!,
                RequestTime = DateTime.UtcNow,
                ApprovalStatus = ApprovalStatusEnum.Pending
            };

            await _contactRequestService.CreateAsync(contactRequest);

            // 检查接收方是否在线
            if (OnlineUsers.IsUserOnline(receiverId.ToString()))
            {
                await Clients.User(receiverId.ToString()).SendAsync("ReceiveContactRequest", senderId);
            }

            return true;
        }

        // 同意交换联系方式请求
        public async Task ApproveContactRequest(string senderId)
        {
            var receiverId = Context.UserIdentifier!;
            //var receiverRole = Context.User.FindFirst(ClaimTypes.Role)?.Value == RoleEnum.Recruiter.ToString() ? RoleEnum.Recruiter : RoleEnum.JobSeeker;

            // 查找对应的请求
            var contactRequest = await _contactRequestService.SingleExpressAsync(t =>
                ((t.RecruiterId.ToString() == senderId && t.SeekerId.ToString() == receiverId) ||
                 (t.RecruiterId.ToString() == receiverId && t.SeekerId.ToString() == senderId)) &&
                t.ApprovalStatus == ApprovalStatusEnum.Pending &&
                t.RequestItem == RequestItemTypeEnum.ContactInfo);

            if (contactRequest == null)
            {
                return;
            }

            // 更新请求状态
            contactRequest.ApprovalStatus = ApprovalStatusEnum.Approved;
            contactRequest.ProcessTime = DateTime.UtcNow;
            await _contactRequestService.UpdateAsync(contactRequest);

            // 获取双方联系方式
            var receiverInfo = await GetUserContactInfo(receiverId);
            var senderInfo = await GetUserContactInfo(senderId);

            // 通知发送方请求已被同意,并发送接收方的联系方式
            if (OnlineUsers.IsUserOnline(senderId))
            {
                await Clients.User(senderId).SendAsync("ContactRequestApproved", receiverId, receiverInfo);
            }

            // 通知接收方(当前用户),并发送发送方的联系方式
            await Clients.Caller.SendAsync("ContactRequestApproved", senderId, senderInfo);

            // 更新双方关系状态为已交换联系方式
            long jobSeekerId = contactRequest.SeekerId;
            long employerId = contactRequest.RecruiterId;
            await _contactedRelationshipService.UpdateRelationshipStatusToContactExchangedAsync(jobSeekerId, employerId);

        }

        // 拒绝交换联系方式请求
        public async Task RejectContactRequest(string senderId)
        {
            var receiverId = Context.UserIdentifier!;

            // 查找对应的请求
            var contactRequest = await _contactRequestService.SingleExpressAsync(t =>
                ((t.RecruiterId.ToString() == senderId && t.SeekerId.ToString() == receiverId) ||
                 (t.RecruiterId.ToString() == receiverId && t.SeekerId.ToString() == senderId)) &&
                t.ApprovalStatus == ApprovalStatusEnum.Pending &&
                t.RequestItem == RequestItemTypeEnum.ContactInfo);

            if (contactRequest == null)
            {
                return;
            }

            // 更新请求状态
            contactRequest.ApprovalStatus = ApprovalStatusEnum.Rejected;
            contactRequest.ProcessTime = DateTime.UtcNow;
            await _contactRequestService.UpdateAsync(contactRequest);

            // 通知发送方请求已被拒绝
            if (OnlineUsers.IsUserOnline(senderId))
            {
                await Clients.User(senderId).SendAsync("ContactRequestRejected", receiverId);
            }
        }

        // 获取用户联系方式信息
        private async Task<string> GetUserContactInfo(string userId)
        {
            // 从数据库中获取用户信息
            var user = await _database.Set<UserEntity>().FirstOrDefaultAsync(u => u.Id.ToString() == userId);
            if (user == null)
            {
                return "未找到用户信息";
            }

            // 构建联系方式信息
            var contactInfo = new List<string>();
            if (!string.IsNullOrEmpty(user.PhoneNumber))
            {
                contactInfo.Add($"电话: {user.PhoneNumber}");
            }
            if (!string.IsNullOrEmpty(user.Email))
            {
                contactInfo.Add($"邮箱: {user.Email}");
            }

            // 如果没有联系方式,返回默认消息
            return contactInfo.Any() ? string.Join(",", contactInfo) : "未设置联系方式";
        }

        // 发送私聊消息(含防骚扰机制和黑名单检查)
        public async Task<bool> SendPrivateMessage(string receiverId, string message)
        {
            var senderId = Context.UserIdentifier!;
            var sessionKey = $"{senderId}_{receiverId}";

            // 获取用户信息
            var senderUser = await _database.Set<UserEntity>().FirstOrDefaultAsync(u => u.Id.ToString() == senderId);
            var receiverUser = await _database.Set<UserEntity>().FirstOrDefaultAsync(u => u.Id.ToString() == receiverId);

            // 检查黑名单关系
            if (!await CheckBlacklistRelationship(senderUser, receiverUser))
            {
                return false;
            }

            // 获取消息计数
            var (messageSendCount, messageReplyCount) = await GetMessageCounts(senderId, receiverId);

            // 如果对方已回复,删除未联系关系记录
            //if (messageReplyCount > 0 && senderUser != null && receiverUser != null)
            //{
            //    await DeleteNotContactedRelationship(senderUser, receiverUser);
            //}

            // 防骚扰检查
            if (!await CheckAntiHarassment(messageSendCount, messageReplyCount))
            {
                return false;
            }

            // 保存并发送消息
            return await SaveAndSendMessage(senderId, receiverId, message, sessionKey);
        }

        // 检查黑名单关系
        private async Task<bool> CheckBlacklistRelationship(UserEntity? senderUser, UserEntity? receiverUser)
        {
            if (senderUser == null || receiverUser == null)
            {
                return true; // 用户不存在,不进行检查
            }

            // 判断双方角色,确定查询条件
            if (senderUser.CurrentRole == RoleEnum.JobSeeker && receiverUser.CurrentRole == RoleEnum.Recruiter)
            {
                // 求职者向招聘者发送消息
                // 检查未联系关系表
                var notContactedRelationship = await _database.Set<NotContactedRelationshipEntity>()
                    .FirstOrDefaultAsync(r => r.JobSeekerId == senderUser.Id && r.RecruiterId == receiverUser.Id 
                    && r.Removed == false);

                if (notContactedRelationship != null && notContactedRelationship.JobSeekerToRecruiterRelation == NotContactedRelationStatusEnum.Rejected)
                {
                    //您已将对方加入黑名单,无法发送消息
                    await Clients.Caller.SendAsync("BlacklistMessageBlocked", 1);
                    return false;
                }

                if (notContactedRelationship != null && notContactedRelationship.RecruiterToJobSeekerRelation == NotContactedRelationStatusEnum.Rejected)
                {
                    //对方已将您加入黑名单,无法发送消息
                    await Clients.Caller.SendAsync("BlacklistMessageBlocked", 2);
                    return false;
                }

                // 检查已联系关系表
                var contactedRelationship = await _database.Set<ContactedRelationshipEntity>()
                    .FirstOrDefaultAsync(r => r.JobSeekerId == senderUser.Id && r.EmployerId == receiverUser.Id);

                if (contactedRelationship != null && contactedRelationship.JobSeekerRelationStatus == ContactedRelationStatusEnum.Blocked)
                {
                    //您已将对方加入黑名单,无法发送消息
                    await Clients.Caller.SendAsync("BlacklistMessageBlocked", 1);
                    return false;
                }

                if (contactedRelationship != null && contactedRelationship.EmployerRelationStatus == ContactedRelationStatusEnum.Blocked)
                {
                    //对方已将您加入黑名单,无法发送消息
                    await Clients.Caller.SendAsync("BlacklistMessageBlocked", 2);
                    return false;
                }
            }
            else if (senderUser.CurrentRole == RoleEnum.Recruiter && receiverUser.CurrentRole == RoleEnum.JobSeeker)
            {
                // 招聘者向求职者发送消息
                // 检查未联系关系表
                var notContactedRelationship = await _database.Set<NotContactedRelationshipEntity>()
                    .FirstOrDefaultAsync(r => r.RecruiterId == senderUser.Id && r.JobSeekerId == receiverUser.Id 
                    && r.Removed == false);

                if (notContactedRelationship != null && notContactedRelationship.RecruiterToJobSeekerRelation == NotContactedRelationStatusEnum.Rejected)
                {
                    await Clients.Caller.SendAsync("BlacklistMessageBlocked", "您已将对方加入黑名单,无法发送消息");
                    return false;
                }

                if (notContactedRelationship != null && notContactedRelationship.JobSeekerToRecruiterRelation == NotContactedRelationStatusEnum.Rejected)
                {
                    await Clients.Caller.SendAsync("BlacklistMessageBlocked", "对方已将您加入黑名单,无法发送消息");
                    return false;
                }

                // 检查已联系关系表
                var contactedRelationship = await _database.Set<ContactedRelationshipEntity>()
                    .FirstOrDefaultAsync(r => r.EmployerId == senderUser.Id && r.JobSeekerId == receiverUser.Id);

                if (contactedRelationship != null && contactedRelationship.EmployerRelationStatus == ContactedRelationStatusEnum.Blocked)
                {
                    await Clients.Caller.SendAsync("BlacklistMessageBlocked", "您已将对方加入黑名单,无法发送消息");
                    return false;
                }

                if (contactedRelationship != null && contactedRelationship.JobSeekerRelationStatus == ContactedRelationStatusEnum.Blocked)
                {
                    await Clients.Caller.SendAsync("BlacklistMessageBlocked", "对方已将您加入黑名单,无法发送消息");
                    return false;
                }
            }

            return true;
        }

        // 获取消息计数
        private async Task<(int SendCount, int ReplyCount)> GetMessageCounts(string senderId, string receiverId)
        {
            var sendCount = await _chatMessageService.CountAsync(t =>
                t.SenderId == senderId && t.ReceiverId == receiverId);
            
            var replyCount = await _chatMessageService.CountAsync(t =>
                t.SenderId == receiverId && t.ReceiverId == senderId);
            
            return (sendCount, replyCount);
        }

        // 删除未联系关系记录
        private async Task DeleteNotContactedRelationship(UserEntity senderUser, UserEntity receiverUser)
        {
            if (senderUser.CurrentRole == RoleEnum.JobSeeker && receiverUser.CurrentRole == RoleEnum.Recruiter)
            {
                // 删除求职者与招聘者的未联系关系
                var relationship = await _database.Set<NotContactedRelationshipEntity>()
                    .FirstOrDefaultAsync(r => r.JobSeekerId == senderUser.Id && r.RecruiterId == receiverUser.Id 
                    && r.Removed == false);
                
                if (relationship != null)
                {
                    await _notContactedRelationshipService.RemoveAsync(relationship);
                }
            }
            else if (senderUser.CurrentRole == RoleEnum.Recruiter && receiverUser.CurrentRole == RoleEnum.JobSeeker)
            {
                // 删除招聘者与求职者的未联系关系
                var relationship = await _database.Set<NotContactedRelationshipEntity>()
                    .FirstOrDefaultAsync(r => r.RecruiterId == senderUser.Id && r.JobSeekerId == receiverUser.Id 
                    && r.Removed == false);
                
                if (relationship != null)
                {
                    await _notContactedRelationshipService.RemoveAsync(relationship);
                }
            }
        }

        // 防骚扰检查
        private async Task<bool> CheckAntiHarassment(int sendCount, int replyCount)
        {
            if (sendCount > 0 && replyCount == 0)
            {
                await Clients.Caller.SendAsync("Blocked", "请等待对方回复后再发送新消息");
                return false;
            }
            return true;
        }

        // 保存并发送消息
        private async Task<bool> SaveAndSendMessage(string senderId, string receiverId, string message, string sessionKey)
        {
            // 保存消息(含15天过期时间)
            var chatMessage = new ChatMessageEntity
            {
                SenderId = senderId,
                ReceiverId = receiverId,
                Content = message,
                SentTime = DateTime.UtcNow,
                ExpireTime = DateTime.UtcNow.AddDays(15),
                IsDelivered = false
            };
            await _chatMessageService.CreateAsync(chatMessage);

            // 标记为等待回复状态
            _cache.Put(sessionKey, receiverId);

            // 检查接收方在线状态
            if (OnlineUsers.IsUserOnline(receiverId))
            {
                await Clients.User(receiverId).SendAsync("ReceiveMessage", senderId, message);
                chatMessage.IsDelivered = true;
                await _chatMessageService.UpdateAsync(chatMessage);
            }
            return true;
        }

        // 用户连接时处理离线消息和历史消息
        public override async Task OnConnectedAsync()
        {
            var userId = Context.UserIdentifier!;
            OnlineUsers.AddUser(userId);

            // 获取并按时间顺序发送历史消息和未送达消息
            // 1. 获取所有未送达消息
            var undeliveredMessages = await _chatMessageService.ListExpressAsync(m =>
                (m.ReceiverId == userId || m.SenderId == userId) && m.IsDelivered == false
            );

            // 2. 获取所有已送达消息(用于按对话分组)
            var deliveredMessages = await _chatMessageService.ListExpressAsync(m =>
                (m.ReceiverId == userId || m.SenderId == userId) && m.IsDelivered == true
            );

            // 3. 合并所有消息
            var allMessages = new List<ChatMessageEntity>();
            if (deliveredMessages != null)
                allMessages.AddRange(deliveredMessages);
            if (undeliveredMessages != null)
                allMessages.AddRange(undeliveredMessages);

            // 4. 按对话ID分组(对话ID由两个用户ID组成,按字母顺序排序确保一致性)
            var conversationGroups = allMessages
                .GroupBy(m =>
                {
                    var ids = new[] { m.SenderId, m.ReceiverId };
                    Array.Sort(ids);
                    return $"{ids[0]}_{ids[1]}";
                })
                .ToList();

            // 5. 对每个对话的消息按时间顺序排序并发送
            foreach (var group in conversationGroups)
            {
                var sortedMessages = group.OrderBy(m => m.SentTime).ToList();

                // 只发送每个对话的最近30条消息
                //if (sortedMessages.Count > 30)
                //{
                //    sortedMessages = sortedMessages.Skip(sortedMessages.Count - 30).ToList();
                //}

                foreach (var msg in sortedMessages)
                {
                    await Clients.Caller.SendAsync("ReceiveMessage", msg.SenderId, msg.Content);
                    // 标记未送达消息为已送达
                    if (!msg.IsDelivered)
                    {
                        msg.IsDelivered = true;
                        await _chatMessageService.UpdateAsync(msg);
                    }
                }
            }

            await base.OnConnectedAsync();
        }

        // 用户断开连接处理
        public override async Task OnDisconnectedAsync(Exception? exception)
        {
            var userId = Context.UserIdentifier!;
            OnlineUsers.RemoveUser(userId);
            await base.OnDisconnectedAsync(exception);
        }
    }

    // 在线用户管理(使用缓存)
    public static class OnlineUsers
    {
        private static ICacheManager<object> _cache;

        // 设置缓存管理器
        public static void Initialize(ICacheManager<object> cache)
        {
            _cache = cache;
        }

        private static string GetUserOnlineKey(string userId) => $"online_user:{userId}";

        public static void AddUser(string userId)
        {
            var cacheItem = new CacheItem<object>(
                GetUserOnlineKey(userId),
                true,
                ExpirationMode.Absolute,
                TimeSpan.FromMinutes(30)
            );
            _cache.Put(cacheItem); // 设置30分钟过期
        }

        public static void RemoveUser(string userId) =>
            _cache.Remove(GetUserOnlineKey(userId));

        public static bool IsUserOnline(string userId) =>
            _cache.Get<bool?>(GetUserOnlineKey(userId)) ?? false;
    }
}


网站公告

今日签到

点亮在社区的每一天
去签到