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,6 +38,8 @@
</div>
</div>
<!-- 过渡动画包裹层 -->
<transition name="fade">
<!-- 离线留言表单 - 当客服不在线时显示 -->
<div class="chatEntBox" v-if="showOfflineMessageForm">
<div class="offlineMessageContainer">
@ -71,7 +82,7 @@
:disabled="offlineMessageSubmitting">
提交留言
</el-button>
<el-button @click="tryReconnect">
<el-button @click="checkKefuStatusByWs">
尝试重新连接
</el-button>
</el-form-item>
@ -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