fix(websocket): 修复并发访问KefuList导致的竞态问题

添加互斥锁保护KefuList的读写操作,重构广播客服状态函数为内部锁安全版本
优化客服心跳检测逻辑,避免遍历时持有锁
为离线留言表单添加过渡动画效果
pull/57/head
CGW406 5 months ago
parent 6a19942798
commit 6d34450b83

@ -15,6 +15,15 @@
<link rel="stylesheet" href="/static/css/common.css?v=sdsderfrgfgdfdf" />
<link rel="stylesheet" href="/static/css/icono.min.css" />
<link rel="stylesheet" href="/static/css/icon/iconfont.css?v=fgjlgfda"/>
<style>
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s ease, transform 0.3s ease;
}
.fade-enter, .fade-leave-to {
opacity: 0;
transform: translateY(10px);
}
</style>
</head>
<body class="visitorBody">
<div id="app" class="chatCenter">
@ -29,71 +38,73 @@
</div>
</div>
<!-- 离线留言表单 - 当客服不在线时显示 -->
<div class="chatEntBox" v-if="showOfflineMessageForm">
<div class="offlineMessageContainer">
<div class="offlineMessageTitle">
<h3><i class="el-icon-chat-dot-round"></i> 客服离线,请留言</h3>
<p>我们的客服目前不在线,请填写以下表单,客服上线后会尽快回复您。</p>
</div>
<el-form :model="offlineMessageForm" :rules="offlineMessageRules" ref="offlineMessageForm" label-width="80px" class="offlineMessageForm">
<el-form-item label="您的姓名" prop="visitor_name">
<el-input v-model="offlineMessageForm.visitor_name" placeholder="请输入您的姓名"></el-input>
</el-form-item>
<el-form-item label="联系邮箱" prop="visitor_email">
<el-input v-model="offlineMessageForm.visitor_email" placeholder="请输入您的邮箱,方便我们回复"></el-input>
</el-form-item>
<el-form-item label="联系电话" prop="visitor_phone">
<el-input v-model="offlineMessageForm.visitor_phone" placeholder="请输入您的电话(选填)"></el-input>
</el-form-item>
<el-form-item label="留言主题" prop="subject">
<el-input v-model="offlineMessageForm.subject" placeholder="请输入留言主题"></el-input>
</el-form-item>
<!-- 过渡动画包裹层 -->
<transition name="fade">
<!-- 离线留言表单 - 当客服不在线时显示 -->
<div class="chatEntBox" v-if="showOfflineMessageForm">
<div class="offlineMessageContainer">
<div class="offlineMessageTitle">
<h3><i class="el-icon-chat-dot-round"></i> 客服离线,请留言</h3>
<p>我们的客服目前不在线,请填写以下表单,客服上线后会尽快回复您。</p>
</div>
<el-form-item label="留言内容" prop="content">
<el-input
type="textarea"
:rows="5"
v-model="offlineMessageForm.content"
placeholder="请输入您的留言内容...">
</el-input>
</el-form-item>
<el-form :model="offlineMessageForm" :rules="offlineMessageRules" ref="offlineMessageForm" label-width="80px" class="offlineMessageForm">
<el-form-item label="您的姓名" prop="visitor_name">
<el-input v-model="offlineMessageForm.visitor_name" placeholder="请输入您的姓名"></el-input>
</el-form-item>
<el-form-item label="联系邮箱" prop="visitor_email">
<el-input v-model="offlineMessageForm.visitor_email" placeholder="请输入您的邮箱,方便我们回复"></el-input>
</el-form-item>
<el-form-item label="联系电话" prop="visitor_phone">
<el-input v-model="offlineMessageForm.visitor_phone" placeholder="请输入您的电话(选填)"></el-input>
</el-form-item>
<el-form-item label="留言主题" prop="subject">
<el-input v-model="offlineMessageForm.subject" placeholder="请输入留言主题"></el-input>
</el-form-item>
<el-form-item label="留言内容" prop="content">
<el-input
type="textarea"
:rows="5"
v-model="offlineMessageForm.content"
placeholder="请输入您的留言内容...">
</el-input>
</el-form-item>
<el-form-item>
<el-button
type="primary"
:loading="offlineMessageSubmitting"
@click="submitOfflineMessage"
:disabled="offlineMessageSubmitting">
提交留言
</el-button>
<el-button @click="checkKefuStatusByWs">
尝试重新连接
</el-button>
</el-form-item>
</el-form>
<el-form-item>
<el-button
type="primary"
:loading="offlineMessageSubmitting"
@click="submitOfflineMessage"
:disabled="offlineMessageSubmitting">
提交留言
<div class="offlineMessageSuccess" v-if="offlineMessageSubmitted">
<el-alert
title="留言提交成功!"
type="success"
:closable="false"
show-icon
description="感谢您的留言,客服上线后会尽快与您联系。">
</el-alert>
<el-button type="primary" plain @click="resetOfflineMessageForm" style="margin-top: 15px;">
继续留言
</el-button>
<el-button @click="tryReconnect">
尝试重新连接
</el-button>
</el-form-item>
</el-form>
<div class="offlineMessageSuccess" v-if="offlineMessageSubmitted">
<el-alert
title="留言提交成功!"
type="success"
:closable="false"
show-icon
description="感谢您的留言,客服上线后会尽快与您联系。">
</el-alert>
<el-button type="primary" plain @click="resetOfflineMessageForm" style="margin-top: 15px;">
继续留言
</el-button>
</div>
</div>
</div>
</div>
<!-- 正常聊天界面 - 当客服在线时显示 -->
<div class="chatEntBox" v-else>
<!-- 正常聊天界面 - 当客服在线时显示 -->
<div class="chatEntBox" v-else>
<div class="chatContext chatVisitorPage">
<div class="chatBox">
<div class="chatNotice" v-on:click="getHistoryMessage" v-show="showLoadMore">
@ -143,6 +154,7 @@
<el-button type="primary" size="mini" class="visitorEditorBtn" :disabled="sendDisabled||messageContent==''" v-on:click="chatToUser();showIconBtns=false">Send</el-button>
</div>
</div>
</transition>
<div class="chatArticle">
<h3 class="hotQuestionTitle">Announcements</h3>
<div class="allNotice" v-html><{kefuInfo.allNotice}></div>
@ -739,7 +751,7 @@
};
this.$refs.offlineMessageForm.resetFields();
},
tryReconnect:function() {
checkKefuStatusByWs:function() {
let _this = this;
_this.$message({
message: '正在尝试重新连接...',

@ -55,6 +55,9 @@ func NewKefuServer(c *gin.Context) {
}
}
func AddKefuToList(kefu *User) {
Mux.Lock()
defer Mux.Unlock()
oldUser, ok := KefuList[kefu.Id]
if oldUser != nil || ok {
msg := TypeMessage{
@ -69,21 +72,27 @@ func AddKefuToList(kefu *User) {
KefuList[kefu.Id] = kefu
// 广播客服上线状态给所有该客服的访客
BroadcastKefuOnlineStatus(kefu.Id, true)
broadcastKefuOnlineStatusLocked(kefu.Id, true)
}
func RemoveKefuFromList(kefuId string) {
Mux.Lock()
defer Mux.Unlock()
_, ok := KefuList[kefuId]
if ok {
delete(KefuList, kefuId)
// 广播客服离线状态给所有该客服的访客
BroadcastKefuOnlineStatus(kefuId, false)
broadcastKefuOnlineStatusLocked(kefuId, false)
}
}
// 给指定客服发消息
func OneKefuMessage(toId string, str []byte) {
Mux.RLock()
kefu, ok := KefuList[toId]
Mux.RUnlock()
if ok {
log.Println("OneKefuMessage lock")
kefu.Mux.Lock()
@ -93,6 +102,27 @@ func OneKefuMessage(toId string, str []byte) {
tools.Logger().Println("send_kefu_message", error, string(str))
}
}
// 内部函数:在持有锁的情况下广播客服状态
func broadcastKefuOnlineStatusLocked(kefuId string, isOnline bool) {
kefuInfo := models.FindUser(kefuId)
msg := TypeMessage{
Type: "kefu_status_change",
Data: map[string]interface{}{
"kefu_id": kefuId,
"is_online": isOnline,
"nickname": kefuInfo.Nickname,
"avator": kefuInfo.Avator,
},
}
str, _ := json.Marshal(msg)
for _, visitor := range ClientList {
if visitor.To_id == kefuId && visitor.Conn != nil {
visitor.Conn.WriteMessage(websocket.TextMessage, str)
}
}
}
func KefuMessage(visitorId, content string, kefuInfo models.User) {
msg := TypeMessage{
Type: "message",
@ -120,11 +150,25 @@ func SendPingToKefuClient() {
// 收集需要删除的客服ID
var toRemove []string
for kefuId, kefu := range KefuList {
if kefu == nil {
Mux.RLock()
// 先遍历获取所有需要检查的客服ID
var kefuIds []string
for kefuId := range KefuList {
kefuIds = append(kefuIds, kefuId)
}
Mux.RUnlock()
// 遍历检查每个客服连接(遍历过程中不持有锁,避免死锁)
for _, kefuId := range kefuIds {
Mux.RLock()
kefu, ok := KefuList[kefuId]
Mux.RUnlock()
if !ok || kefu == nil {
toRemove = append(toRemove, kefuId)
continue
}
kefu.Mux.Lock()
err := kefu.Conn.WriteMessage(websocket.TextMessage, str)
kefu.Mux.Unlock()

@ -212,7 +212,11 @@ func SendKefuStatusToVisitor(visitor *User, kefuId string) {
if visitor == nil || visitor.Conn == nil {
return
}
Mux.RLock()
_, online := KefuList[kefuId]
Mux.RUnlock()
kefuInfo := models.FindUser(kefuId)
msg := TypeMessage{
Type: "kefu_status",
@ -227,26 +231,6 @@ func SendKefuStatusToVisitor(visitor *User, kefuId string) {
visitor.Conn.WriteMessage(websocket.TextMessage, str)
}
func BroadcastKefuOnlineStatus(kefuId string, isOnline bool) {
kefuInfo := models.FindUser(kefuId)
msg := TypeMessage{
Type: "kefu_status_change",
Data: map[string]interface{}{
"kefu_id": kefuId,
"is_online": isOnline,
"nickname": kefuInfo.Nickname,
"avator": kefuInfo.Avator,
},
}
str, _ := json.Marshal(msg)
for _, visitor := range ClientList {
if visitor.To_id == kefuId && visitor.Conn != nil {
visitor.Conn.WriteMessage(websocket.TextMessage, str)
}
}
}
func HandleCheckKefuStatus(conn *websocket.Conn, data interface{}) {
var kefuId string
switch v := data.(type) {
@ -262,7 +246,10 @@ func HandleCheckKefuStatus(conn *websocket.Conn, data interface{}) {
return
}
Mux.RLock()
_, online := KefuList[kefuId]
Mux.RUnlock()
kefuInfo := models.FindUser(kefuId)
msg := TypeMessage{
Type: "kefu_status",

Loading…
Cancel
Save