Explorar o código

1. 创建连接时,接收参数的调整 2. 实现保存聊天人列表及自增未读消息数

shizhongqi hai 11 meses
pai
achega
673b384244
Modificáronse 5 ficheiros con 485 adicións e 33 borrados
  1. 2 1
      main.go
  2. 78 0
      score_debug.go
  3. 12 11
      wsClient/client.go
  4. 313 0
      wsClient/redis.go
  5. 80 21
      wsClient/room.go

+ 2 - 1
main.go

@@ -66,6 +66,7 @@ func main() {
 
 	// Handle all websocket connections for chat rooms dynamically
 	http.HandleFunc("/room", func(w http.ResponseWriter, r *http.Request) {
+		log.Println("--------------------------------")
 		log.Println("Received request for room")
 		// 从WebSocket子协议中预解析房间名称
 		protocols := r.Header["Sec-Websocket-Protocol"]
@@ -88,7 +89,7 @@ func main() {
             http.Error(w, "Room name required in WebSocket protocols or URL parameter", http.StatusBadRequest)
 		}
 		roomParams.RoomName = roomName
-		log.Printf("--------------- 房间名称: %s\n\n", roomName)
+		log.Printf("---- 房间名称: %s\n\n", roomName)
 
 		// 获取(或创建)指定名称的房间,并启动房间的消息处理协程
 		room := wsClient.GetRoomAndRun(roomName)

+ 78 - 0
score_debug.go

@@ -0,0 +1,78 @@
+package main
+
+import (
+	"fmt"
+	"strconv"
+	"time"
+)
+
+// 分数计算常量
+const (
+	UnreadMultiplier = 10000.0 // 未读消息数乘数,用于小数部分存储
+)
+
+// calculateScore 计算有序集合的分数,同时存储时间戳和未读消息数
+// 整数部分:Unix时间戳,小数部分:未读消息数(除以乘数)
+func calculateScore(timestamp int64, unreadCount int) float64 {
+	score := float64(timestamp) + (float64(unreadCount) / UnreadMultiplier)
+	fmt.Printf("calculateScore: timestamp=%d, unreadCount=%d, score=%f\n", timestamp, unreadCount, score)
+	return score
+}
+
+// parseScore 从分数中解析出时间戳和未读消息数
+func parseScore(score float64) (timestamp int64, unreadCount int) {
+	timestamp = int64(score) // 整数部分是时间戳
+	decimalPart := score - float64(timestamp)
+	// 使用四舍五入来处理浮点数精度问题
+	unreadCount = int(decimalPart*UnreadMultiplier + 0.5)
+	fmt.Printf("parseScore: score=%f, timestamp=%d, decimalPart=%f, unreadCount=%d\n", score, timestamp, decimalPart, unreadCount)
+	return
+}
+
+func main() {
+	fmt.Println("=== 分数计算测试 ===")
+
+	// 测试用例
+	timestamp := time.Now().Unix()
+	fmt.Printf("原始时间戳: %d\n\n", timestamp)
+
+	// 测试1: 未读消息数为0
+	fmt.Println("测试1: 未读消息数为0")
+	score1 := calculateScore(timestamp, 0)
+	ts1, unread1 := parseScore(score1)
+	fmt.Printf("结果: timestamp=%d, unreadCount=%d\n\n", ts1, unread1)
+
+	// 测试2: 未读消息数为1
+	fmt.Println("测试2: 未读消息数为1")
+	score2 := calculateScore(timestamp, 1)
+	ts2, unread2 := parseScore(score2)
+	fmt.Printf("结果: timestamp=%d, unreadCount=%d\n\n", ts2, unread2)
+
+	// 测试3: 未读消息数为5
+	fmt.Println("测试3: 未读消息数为5")
+	score3 := calculateScore(timestamp, 5)
+	ts3, unread3 := parseScore(score3)
+	fmt.Printf("结果: timestamp=%d, unreadCount=%d\n\n", ts3, unread3)
+
+	// 测试4: 未读消息数为100
+	fmt.Println("测试4: 未读消息数为100")
+	score4 := calculateScore(timestamp, 100)
+	ts4, unread4 := parseScore(score4)
+	fmt.Printf("结果: timestamp=%d, unreadCount=%d\n\n", ts4, unread4)
+
+	// 测试5: 验证小数部分的精度
+	fmt.Println("测试5: 小数部分精度验证")
+	testScore := 1703123456.123
+	fmt.Printf("测试分数: %f\n", testScore)
+	ts5, unread5 := parseScore(testScore)
+	fmt.Printf("解析结果: timestamp=%d, unreadCount=%d\n\n", ts5, unread5)
+
+	// 测试6: 字符串转换测试
+	fmt.Println("测试6: 字符串转换测试")
+	scoreStr := fmt.Sprintf("%.6f", score2)
+	fmt.Printf("分数字符串: %s\n", scoreStr)
+	if parsedScore, err := strconv.ParseFloat(scoreStr, 64); err == nil {
+		ts6, unread6 := parseScore(parsedScore)
+		fmt.Printf("字符串解析结果: timestamp=%d, unreadCount=%d\n", ts6, unread6)
+	}
+}

+ 12 - 11
wsClient/client.go

@@ -30,18 +30,19 @@ type Client struct {
 	// 客户端所在的聊天房间引用
 	// 用于将发送的消息转发给房间
 	room *Room
-	
+
 	// 新增的用户参数
 	userInfo UserInfo
 }
 
 type UserInfo struct {
-	UserId   int32 `json:"userId"`
+	UserId   int32  `json:"userId"`
 	Username string `json:"username"`
-	CustomId int32 `json:"customId"`
-	ShopId   int32 `json:"shopId"`
+	CustomId int32  `json:"customId"`
+	ShopId   int32  `json:"shopId"`
 	Platform string `json:"platform"`
-	Type string `json:"type,omitempty"` // 客户端类型 -- shop:门店端, customer:客户端
+	Type     string `json:"type,omitempty"` // 客户端类型 -- shop:门店端, customer:客户端
+	StaffId  int32  `json:"staffId"`
 }
 
 // read 从 WebSocket 连接读取消息并转发给房间 (原作者说: Used to send messages)
@@ -49,7 +50,7 @@ type UserInfo struct {
 func (c *Client) read() {
 	// close the connection when we are done
 	defer c.socket.Close()
-	
+
 	// endlessly read messages from input
 	for {
 		// 从 WebSocket 连接读取消息
@@ -62,8 +63,8 @@ func (c *Client) read() {
 
 		// 构造要发送的消息对象,包含用户名和消息内容
 		outgoing := map[string]string{
-			"name":    c.name,         // 发送者用户名
-			"message": string(msg),   // 消息内容(转换为字符串)
+			"name":    c.name,      // 发送者用户名
+			"message": string(msg), // 消息内容(转换为字符串)
 		}
 
 		// 将消息对象编码为 JSON 格式
@@ -75,9 +76,9 @@ func (c *Client) read() {
 
 		// 将编码后的消息和发送者信息一起发送到房间的 forward channel
 		c.room.forward <- &message{
-			content:   jsMessage, // 消息内容
-			sender: c,         // 发送者(当前客户端)
-		}// 房间会将这条消息分发给除发送者外的所有其他客户端(在 room.go 中 run() 方法 case msg := <-r.forward 中)
+			content: jsMessage, // 消息内容
+			sender:  c,         // 发送者(当前客户端)
+		} // 房间会将这条消息分发给除发送者外的所有其他客户端(在 room.go 中 run() 方法 case msg := <-r.forward 中)
 	}
 }
 

+ 313 - 0
wsClient/redis.go

@@ -4,7 +4,9 @@ package wsClient
 import (
 	"context"
 	"encoding/json"
+	"fmt"
 	"log"
+	"strconv"
 	"time"
 
 	"github.com/redis/go-redis/v9"
@@ -16,6 +18,20 @@ var redisClient *redis.Client
 // Redis上下文
 var ctx = context.Background()
 
+// 分数计算常量
+const (
+	UnreadMultiplier = 10000.0 // 未读消息数乘数,用于小数部分存储(增大乘数以提高精度)
+	ContactTTL       = 7 * 24 * time.Hour // 最近联系人过期时间(7天)
+)
+
+// ContactInfo 表示联系人信息
+type ContactInfo struct {
+	ContactID    int32     `json:"contact_id"`    // 联系人ID
+	Timestamp    time.Time `json:"timestamp"`     // 最后聊天时间
+	UnreadCount  int       `json:"unread_count"`  // 未读消息数
+	ContactType  string    `json:"contact_type"`  // 联系人类型(shop/customer)
+}
+
 // ChatMessage 表示存储在Redis中的聊天消息结构
 type ChatMessage struct {
 	Content   string    `json:"content"`   // 原始消息内容(JSON字符串)
@@ -97,6 +113,168 @@ func StoreMessage(roomName, username, message string) error {
 	return StoreRawMessage(roomName, msgBytes)
 }
 
+// UpdateRecentContacts 更新最近联系人列表
+// 使用浮点数分数同时存储时间戳和未读消息数
+func UpdateRecentContacts(client *Client) error {
+	if redisClient == nil {
+		log.Println("Redis客户端未初始化")
+		return nil
+	}
+
+	currentTime := time.Now().Unix()
+
+	if client.userInfo.Type == "shop" {
+		// 更新门店的联系人列表(客户)
+		key := getContactKey("shop", client.userInfo.ShopId) // shop_chats:{ShopId}
+		if key != "" {
+			// 先获取当前未读消息数
+			// currentScore, err := redisClient.ZScore(ctx, key, strconv.Itoa(int(client.userInfo.CustomId))).Result()
+			unreadCount := 0 // ---->>>>> 门店是主动方,所以未读消息数为0
+			// if err == nil {
+			// 	// 如果成员已存在,保留当前的未读消息数
+			// 	_, unreadCount = parseScore(currentScore)
+			// }
+
+			score := calculateScore(currentTime, unreadCount)
+			redisClient.ZAdd(ctx, key, redis.Z{
+				Score:  score,
+				Member: strconv.Itoa(int(client.userInfo.CustomId)),
+			})
+			// 设置过期时间
+			redisClient.Expire(ctx, key, ContactTTL)
+		}
+
+		// 更新客户的联系人列表(门店)
+		key = getContactKey("customer", client.userInfo.UserId) // customer_chats:{UserId}
+		if key != "" {
+			// 先获取当前未读消息数
+			currentScore, err := redisClient.ZScore(ctx, key, strconv.Itoa(int(client.userInfo.ShopId))).Result()
+			unreadCount := 0
+			if err == nil {
+				// 如果成员已存在,保留当前的未读消息数
+				_, unreadCount = parseScore(currentScore)
+			}
+
+			score := calculateScore(currentTime, unreadCount)
+			redisClient.ZAdd(ctx, key, redis.Z{
+				Score:  score,
+				Member: strconv.Itoa(int(client.userInfo.ShopId)),
+			})
+			// 设置过期时间
+			redisClient.Expire(ctx, key, ContactTTL)
+		}
+	}
+
+	if client.userInfo.Type == "customer" {
+		// 更新客户的联系人列表(门店)
+		key := getContactKey("customer", client.userInfo.UserId) // customer_chats:{UserId}
+		if key != "" {
+			// 先获取当前未读消息数
+			// currentScore, err := redisClient.ZScore(ctx, key, strconv.Itoa(int(client.userInfo.ShopId))).Result()
+			unreadCount := 0 // ---->>>>> 客户是主动方,所以未读消息数为0
+			// if err == nil {
+			// 	// 如果成员已存在,保留当前的未读消息数
+			// 	_, unreadCount = parseScore(currentScore)
+			// }
+
+			score := calculateScore(currentTime, unreadCount)
+			redisClient.ZAdd(ctx, key, redis.Z{
+				Score:  score,
+				Member: strconv.Itoa(int(client.userInfo.ShopId)),
+			})
+			// 设置过期时间
+			redisClient.Expire(ctx, key, ContactTTL)
+		}
+
+		// 更新门店的联系人列表(客户)
+		key = getContactKey("shop", client.userInfo.ShopId) // shop_chats:{ShopId}
+		if key != "" {
+			// 先获取当前未读消息数
+			currentScore, err := redisClient.ZScore(ctx, key, strconv.Itoa(int(client.userInfo.CustomId))).Result()
+			unreadCount := 0
+			if err == nil {
+				// 如果成员已存在,保留当前的未读消息数
+				_, unreadCount = parseScore(currentScore)
+			}
+
+			score := calculateScore(currentTime, unreadCount)
+			redisClient.ZAdd(ctx, key, redis.Z{
+				Score:  score,
+				Member: strconv.Itoa(int(client.userInfo.ShopId)),
+			})
+			// 设置过期时间
+			redisClient.Expire(ctx, key, ContactTTL)
+		}
+	}
+
+	log.Printf("最近联系人列表已更新 - client name: %s", client.name)
+	return nil
+}
+
+// 增加未读消息数
+// 为接收者(对方)的联系人列表增加未读消息数
+func IncrementUnreadCount(sender *Client, count int) error {
+	if redisClient == nil {
+		log.Println("Redis客户端未初始化")
+		return nil
+	}
+
+	// 根据发送者的类型确定接收者的类型和ID
+	var receiverType string
+	var receiverID int32
+	var senderMember string
+
+	if sender.userInfo.Type == "shop" {
+		// 门店发送消息给客户,接收者是客户
+		receiverType = "customer"
+		receiverID = sender.userInfo.UserId
+		senderMember = strconv.Itoa(int(sender.userInfo.ShopId))
+	} else if sender.userInfo.Type == "customer" {
+		// 客户发送消息给门店,接收者是门店
+		receiverType = "shop"
+		receiverID = sender.userInfo.ShopId
+		senderMember = strconv.Itoa(int(sender.userInfo.CustomId))
+	} else {
+		log.Printf("未知的客户端类型: %s", sender.userInfo.Type)
+		return nil
+	}
+
+	// 获取接收者的联系人列表键
+	key := getContactKey(receiverType, receiverID)
+	if key == "" {
+		log.Printf("无法生成联系人键 - 类型: %s, ID: %d", receiverType, receiverID)
+		return nil
+	}
+
+	// 获取当前分数
+	currentScore, err := redisClient.ZScore(ctx, key, senderMember).Result()
+	if err != nil {
+		// 如果成员不存在,使用当前时间戳和0未读消息数创建
+		currentScore = calculateScore(time.Now().Unix(), 0)
+	}
+
+	// 解析当前的分数
+	_, currentUnread := parseScore(currentScore)
+	// 增加未读消息数
+	newUnread := currentUnread + count
+	// 重新计算分数
+	timestamp := time.Now().Unix()
+	newScore := calculateScore(timestamp, newUnread)
+
+	// 更新分数
+	redisClient.ZAdd(ctx, key, redis.Z{
+		Score:  newScore,
+		Member: senderMember,
+	})
+
+	// 设置过期时间
+	redisClient.Expire(ctx, key, ContactTTL)
+
+	log.Printf("未读消息数已增加 - 接收者类型: %s, 接收者ID: %d, 发送者ID: %s, 未读数: %d, 新分数: %f",
+		receiverType, receiverID, senderMember, newUnread, newScore)
+	return nil
+}
+
 // GetRoomMessages 获取房间的历史消息
 // 返回指定数量的最新消息
 func GetRoomMessages(roomName string, count int64) ([]ChatMessage, error) {
@@ -145,3 +323,138 @@ func CloseRedis() error {
 	}
 	return nil
 }
+
+// calculateScore 计算有序集合的分数,同时存储时间戳和未读消息数
+// 整数部分:Unix时间戳,小数部分:未读消息数(除以乘数)
+func calculateScore(timestamp int64, unreadCount int) float64 {
+	return float64(timestamp) + (float64(unreadCount) / UnreadMultiplier)
+}
+
+// parseScore 从分数中解析出时间戳和未读消息数
+func parseScore(score float64) (timestamp int64, unreadCount int) {
+	timestamp = int64(score) // 整数部分是时间戳
+	decimalPart := score - float64(timestamp)
+	// 使用四舍五入来处理浮点数精度问题
+	unreadCount = int(decimalPart*UnreadMultiplier + 0.5)
+	return
+}
+
+// getContactKey 生成联系人的Redis键
+func getContactKey(userType string, userID int32) string {
+	if userType == "shop" {
+		return "shop_chats:" + strconv.Itoa(int(userID))
+	} else if userType == "customer" {
+		return "customer_chats:" + strconv.Itoa(int(userID))
+	}
+	return ""
+}
+
+// GetRecentContacts 获取用户的最近联系人列表
+// 返回按最后聊天时间排序的联系人列表(最新的在前)
+func GetRecentContacts(userType string, userID int32, limit int64) ([]ContactInfo, error) {
+	if redisClient == nil {
+		log.Println("Redis客户端未初始化")
+		return nil, nil
+	}
+
+	key := getContactKey(userType, userID)
+	if key == "" {
+		return nil, fmt.Errorf("无效的用户类型: %s", userType)
+	}
+
+	// 获取最近的联系人(按分数降序,最新的在前)
+	result, err := redisClient.ZRevRangeWithScores(ctx, key, 0, limit-1).Result()
+	if err != nil {
+		log.Printf("获取最近联系人失败: %v", err)
+		return nil, err
+	}
+
+	var contacts []ContactInfo
+	for _, item := range result {
+		contactID, err := strconv.Atoi(item.Member.(string))
+		if err != nil {
+			log.Printf("解析联系人ID失败: %v", err)
+			continue
+		}
+
+		timestamp, unreadCount := parseScore(item.Score)
+
+		// 确定联系人的类型
+		contactType := "customer"
+		if userType == "customer" {
+			contactType = "shop"
+		}
+
+		contact := ContactInfo{
+			ContactID:   int32(contactID),
+			Timestamp:   time.Unix(timestamp, 0),
+			UnreadCount: unreadCount,
+			ContactType: contactType,
+		}
+		contacts = append(contacts, contact)
+	}
+
+	return contacts, nil
+}
+
+// GetTotalUnreadCount 获取用户的总未读消息数
+func GetTotalUnreadCount(userType string, userID int32) (int, error) {
+	if redisClient == nil {
+		log.Println("Redis客户端未初始化")
+		return 0, nil
+	}
+
+	key := getContactKey(userType, userID)
+	if key == "" {
+		return 0, fmt.Errorf("无效的用户类型: %s", userType)
+	}
+
+	// 获取所有联系人的分数
+	result, err := redisClient.ZRangeWithScores(ctx, key, 0, -1).Result()
+	if err != nil {
+		log.Printf("获取联系人分数失败: %v", err)
+		return 0, err
+	}
+
+	totalUnread := 0
+	for _, item := range result {
+		_, unreadCount := parseScore(item.Score)
+		totalUnread += unreadCount
+	}
+
+	return totalUnread, nil
+}
+
+// ClearUnreadCount 清除指定联系人的未读消息数
+func ClearUnreadCount(userType string, userID int32, contactID int32) error {
+	if redisClient == nil {
+		log.Println("Redis客户端未初始化")
+		return nil
+	}
+
+	key := getContactKey(userType, userID)
+	if key == "" {
+		return fmt.Errorf("无效的用户类型: %s", userType)
+	}
+
+	// 获取当前分数
+	currentScore, err := redisClient.ZScore(ctx, key, strconv.Itoa(int(contactID))).Result()
+	if err != nil {
+		// 如果联系人不存在,不需要清理
+		return nil
+	}
+
+	// 解析当前分数,保留时间戳,清除未读消息数
+	timestamp, _ := parseScore(currentScore)
+	newScore := calculateScore(timestamp, 0)
+
+	// 更新分数
+	redisClient.ZAdd(ctx, key, redis.Z{
+		Score:  newScore,
+		Member: strconv.Itoa(int(contactID)),
+	})
+
+	log.Printf("未读消息数已清除 - 用户类型: %s, 用户ID: %d, 联系人ID: %d",
+		userType, userID, contactID)
+	return nil
+}

+ 80 - 21
wsClient/room.go

@@ -51,12 +51,13 @@ type message struct {
 // RoomParams 表示从客户端 WebSocket子协议中解析出的房间参数
 type RoomParams struct {
 	RoomName  string `json:"roomName,omitempty"`
-	UserId    int32 `json:"u_id"` 	// 用户ID: 门店客户端是 staffId, 买家客户端是 userId  ---- 未确定,可修改
+	UserId    int32 `json:"u_id"` 	// 用户ID: 只是表示买家客户端是 userId
 	CustomId  int32 `json:"c_id"`
 	ShopId    int32 `json:"s_id"`
 	Username  string `json:"u_name"`
 	Platform  string `json:"platform"`
-	Type  string `json:"type,omitempty"` // 客户端类型:shop-门店端, customer-客户端
+	Type      string `json:"type,omitempty"` // 客户端类型:shop-门店端, customer-客户端
+	StaffId   int32 `json:"staff_id"` // 只是表示门店客户端的员工Id
 }
 
 // 全局互斥锁,用于保护 rooms 字典的并发访问,确保多个 goroutine 同时访问 rooms 时的线程安全
@@ -85,27 +86,12 @@ func GetRoomAndRun(name string) *Room {
 	}
 	r := newRoom(name) // 传入房间名称
 	rooms[name] = r
+
 	// 启动房间的消息处理循环(在新的 goroutine 中)
 	go r.run()
 	return r
 }
 
-// GetRoom 获取指定名称的房间,如果不存在则创建新房间但不启动
-// 注意:这个函数只创建房间但不启动 run() 方法,可能导致房间无法处理消息
-// 建议使用 GetRoomAndRun() 替代
-func GetRoom(roomName string) *Room {
-	mu.Lock()
-	defer mu.Unlock()
-
-	if r, ok := rooms[roomName]; ok {
-		return r
-	}
-	nr := newRoom(roomName) // 传入房间名称
-	rooms[roomName] = nr
-	// 注意:这里没有调用 go nr.run(),房间不会处理消息
-	return nr
-}
-
 // run 是房间的核心消息处理循环
 // 在独立的 goroutine 中运行,使用 select 语句处理三种类型的事件
 func (r *Room) run() {
@@ -113,36 +99,100 @@ func (r *Room) run() {
 		select {
 		// 处理客户端加入事件
 		case client := <-r.join:
+			// 检查channel是否已关闭(收到nil值)
+			if client == nil {
+				return // channel已关闭,退出goroutine
+			}
 			// 将新客户端添加到房间的客户端集合中
 			r.clientConns[client] = true // 使用 true 作为值,实际上只使用 key(client 指针)
+			log.Printf("客户端 %v 加入房间 %v", client.name, r.name)
 
 		// 处理客户端离开事件
 		case client := <-r.leave:
+			// 检查channel是否已关闭(收到nil值)
+			if client == nil {
+				return // channel已关闭,退出goroutine
+			}
 			// 从房间的客户端集合中删除客户端
 			delete(r.clientConns, client)
 			// 关闭客户端的接收 channel,通知客户端的 write() 方法退出
 			close(client.receive)
+			log.Printf("客户端 %v 离开房间 %v", client.name, r.name)
+			if len(r.clientConns) == 0 {
+				log.Printf("房间 %v 没有客户端,关闭房间", r.name)
+				close(r.forward)
+				close(r.join)
+				close(r.leave)
+				delete(rooms, r.name)
+				return // 退出run() goroutine,避免继续从已关闭的channel读取数据
+			}
 
 		// 处理消息转发事件
 		case msg := <-r.forward:
+			// 检查channel是否已关闭(收到nil值)
+			if msg == nil {
+				return // channel已关闭,退出goroutine
+			}
 			// 打印消息内容与发送者信息
 			log.Printf("Received message from %v: --- %s", msg.sender, string(msg.content))
 
 			// 直接存储原始消息内容到Redis,使用房间名作为键
 			go StoreRawMessage(r.name, msg.content) // 异步存储,不阻塞消息转发
+			// 创建最近联系人列表 或 更新最近联系人列表
+			go UpdateRecentContacts(msg.sender)
 
+			online := false
 			// 将消息发送给房间中的所有客户端,但排除发送者自己
-			// 使用指针比较是最高效的方法,因为每个客户端都有唯一的内存地址
 			for client := range r.clientConns {
-				if client != msg.sender { // 高效的指针比较,排除发送者
+				if client != msg.sender && client.userInfo.Type != msg.sender.userInfo.Type { // 高效的指针比较,排除发送者,且排除同一类型的客户端
 					// 将消息发送到每个客户端的接收 channel
 					client.receive <- msg.content // 只发送消息内容,不包含发送者信息
+					online = true
 				}
 			}
+
+			// 本消息转发,目的是让客户与门店互相发消息,当有另一方不在线时,要累计未读消息数
+			if !online {
+				go IncrementUnreadCount(msg.sender, 1)
+			}
+		}
+	}
+}
+
+// 查询当前房间的客户端数量
+func (r *Room)clientCount() int {
+	return len(r.clientConns)
+}
+
+// 统计所有房间里的客户端总数量
+func GetTotalClientCount() int {
+	totalCount := 0
+	for _, room := range rooms {
+		totalCount += len(room.clientConns)
+	}
+	return totalCount
+}
+
+// 用 Client.name 来查找房间
+func GetRoomByClientName(clientName string) *Room {
+	for _, room := range rooms {
+		for client := range room.clientConns {
+			if client.name == clientName {
+				return room
+			}
 		}
 	}
+	return nil
+}
+
+// 用 Client.name 查询用户是否在线
+func IsClientOnline(clientName string) bool {
+	room := GetRoomByClientName(clientName)
+	return room != nil
 }
 
+
+// ======================================== webSocket服务相关 ========================================
 // WebSocket 相关常量定义
 const (
 	// WebSocket 连接的读写缓冲区大小(字节)
@@ -268,7 +318,15 @@ func parseProtocolPart(part string, params *RoomParams) error {
 		params.Platform = p2Data.Platform
 		params.Type = p2Data.Type
 
-	// 当有 p3 参数时,解析 p3 参数
+	case "p3":
+		// 解析平台和类型
+		var p3Data struct {
+			StaffId int32 `json:"staff_id"`
+		}
+		if err := json.Unmarshal([]byte(decodedStr), &p3Data); err != nil {
+			return fmt.Errorf("p2参数JSON解析失败: %v", err)
+		}
+		params.StaffId = p3Data.StaffId
 
 	// 当有 p4 参数时,解析 p4 参数
 
@@ -306,6 +364,7 @@ func (r *Room) HttpServe(w http.ResponseWriter, req *http.Request,roomParams *Ro
 			ShopId:   roomParams.ShopId,
 			Platform: roomParams.Platform,
 			Type: roomParams.Type,
+			StaffId: roomParams.StaffId,
 		},
 	}