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

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

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

TypeScript 复制代码
<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

cs 复制代码
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;
    }
}
相关推荐
Jackson@ML10 分钟前
用Visual Studio Code最新版开发C#应用程序
ide·vscode·c#
小月鸭12 分钟前
如何理解HTML语义化
前端·html
jump68035 分钟前
url输入到网页展示会发生什么?
前端
诸葛韩信39 分钟前
我们需要了解的Web Workers
前端
brzhang44 分钟前
我觉得可以试试 TOON —— 一个为 LLM 而生的极致压缩数据格式
前端·后端·架构
yivifu1 小时前
JavaScript Selection API详解
java·前端·javascript
这儿有一堆花1 小时前
告别 Class 组件:拥抱 React Hooks 带来的函数式新范式
前端·javascript·react.js
她说彩礼65万1 小时前
C# 代理模式
开发语言·c#·代理模式
十二春秋1 小时前
场景模拟:基础路由配置
前端
六月的可乐1 小时前
实战干货-Vue实现AI聊天助手全流程解析
前端·vue.js·ai编程