功能是模仿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;
}
}