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

@ -55,6 +55,9 @@ func NewKefuServer(c *gin.Context) {
} }
} }
func AddKefuToList(kefu *User) { func AddKefuToList(kefu *User) {
Mux.Lock()
defer Mux.Unlock()
oldUser, ok := KefuList[kefu.Id] oldUser, ok := KefuList[kefu.Id]
if oldUser != nil || ok { if oldUser != nil || ok {
msg := TypeMessage{ msg := TypeMessage{
@ -69,21 +72,27 @@ func AddKefuToList(kefu *User) {
KefuList[kefu.Id] = kefu KefuList[kefu.Id] = kefu
// 广播客服上线状态给所有该客服的访客 // 广播客服上线状态给所有该客服的访客
BroadcastKefuOnlineStatus(kefu.Id, true) broadcastKefuOnlineStatusLocked(kefu.Id, true)
} }
func RemoveKefuFromList(kefuId string) { func RemoveKefuFromList(kefuId string) {
Mux.Lock()
defer Mux.Unlock()
_, ok := KefuList[kefuId] _, ok := KefuList[kefuId]
if ok { if ok {
delete(KefuList, kefuId) delete(KefuList, kefuId)
// 广播客服离线状态给所有该客服的访客 // 广播客服离线状态给所有该客服的访客
BroadcastKefuOnlineStatus(kefuId, false) broadcastKefuOnlineStatusLocked(kefuId, false)
} }
} }
// 给指定客服发消息 // 给指定客服发消息
func OneKefuMessage(toId string, str []byte) { func OneKefuMessage(toId string, str []byte) {
Mux.RLock()
kefu, ok := KefuList[toId] kefu, ok := KefuList[toId]
Mux.RUnlock()
if ok { if ok {
log.Println("OneKefuMessage lock") log.Println("OneKefuMessage lock")
kefu.Mux.Lock() kefu.Mux.Lock()
@ -93,6 +102,27 @@ func OneKefuMessage(toId string, str []byte) {
tools.Logger().Println("send_kefu_message", error, string(str)) 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) { func KefuMessage(visitorId, content string, kefuInfo models.User) {
msg := TypeMessage{ msg := TypeMessage{
Type: "message", Type: "message",
@ -120,11 +150,25 @@ func SendPingToKefuClient() {
// 收集需要删除的客服ID // 收集需要删除的客服ID
var toRemove []string var toRemove []string
for kefuId, kefu := range KefuList { Mux.RLock()
if kefu == nil { // 先遍历获取所有需要检查的客服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) toRemove = append(toRemove, kefuId)
continue continue
} }
kefu.Mux.Lock() kefu.Mux.Lock()
err := kefu.Conn.WriteMessage(websocket.TextMessage, str) err := kefu.Conn.WriteMessage(websocket.TextMessage, str)
kefu.Mux.Unlock() kefu.Mux.Unlock()

@ -212,7 +212,11 @@ func SendKefuStatusToVisitor(visitor *User, kefuId string) {
if visitor == nil || visitor.Conn == nil { if visitor == nil || visitor.Conn == nil {
return return
} }
Mux.RLock()
_, online := KefuList[kefuId] _, online := KefuList[kefuId]
Mux.RUnlock()
kefuInfo := models.FindUser(kefuId) kefuInfo := models.FindUser(kefuId)
msg := TypeMessage{ msg := TypeMessage{
Type: "kefu_status", Type: "kefu_status",
@ -227,26 +231,6 @@ func SendKefuStatusToVisitor(visitor *User, kefuId string) {
visitor.Conn.WriteMessage(websocket.TextMessage, str) 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{}) { func HandleCheckKefuStatus(conn *websocket.Conn, data interface{}) {
var kefuId string var kefuId string
switch v := data.(type) { switch v := data.(type) {
@ -262,7 +246,10 @@ func HandleCheckKefuStatus(conn *websocket.Conn, data interface{}) {
return return
} }
Mux.RLock()
_, online := KefuList[kefuId] _, online := KefuList[kefuId]
Mux.RUnlock()
kefuInfo := models.FindUser(kefuId) kefuInfo := models.FindUser(kefuId)
msg := TypeMessage{ msg := TypeMessage{
Type: "kefu_status", Type: "kefu_status",

Loading…
Cancel
Save