// 客户端管理模块 // 处理单个 WebSocket 客户端的连接、消息读取和发送 package wsClient import ( "encoding/json" "fmt" "github.com/gorilla/websocket" // WebSocket 库 "github.com/rs/zerolog/log" ) // Client represents connection to a single chatting user // Client 表示一个聊天用户的连接 // 每个 WebSocket 连接对应一个 Client 实例 type Client struct { // 客户端名称 -- 用作唯一标识,进行查找用的 (门店端: shop_{shopId}_{userId}, 买家端: customer_{customId}_{userId}) // 在消息中用于标识消息发送者 name string // a socket is the web socket for this uer // WebSocket 连接对象,用于与浏览器端进行双向通信 socket *websocket.Conn //receive is a channel to receive messages from other clients // 用于接收来自其他客户端的消息的 channel // 房间会将广播的消息发送到这个 channel receive chan []byte //room is where the client is chatting in // 客户端所在的聊天房间引用 // 用于将发送的消息转发给房间 room *Room // 新增的用户参数 userInfo UserInfo } type UserInfo struct { UserId int32 `json:"userId"` Username string `json:"username"` CustomId int32 `json:"customId"` ShopId int32 `json:"shopId"` Platform string `json:"platform"` Type string `json:"type,omitempty"` // 客户端类型 -- shop:门店端, customer:客户端 StaffId int32 `json:"staffId"` } // read 从 WebSocket 连接读取消息并转发给房间 (原作者说: Used to send messages) // 这个方法会阻塞运行直到连接断开或出现错误 func (c *Client) read() { // close the connection when we are done defer c.socket.Close() // endlessly read messages from input for { // 从 WebSocket 连接读取消息 // _ 表示忽略消息类型,msg 是消息内容,err 是错误 _, msg, err := c.socket.ReadMessage() // break if there is an error if err != nil { if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { log.Error().Err(err).Str("client", c.name).Msg("读取WebSocket消息失败") } else { log.Info().Err(err).Str("client", c.name).Msg("WebSocket连接正常关闭") } break } // 记录原始消息内容用于调试 //log.Debug().Str("client", c.name).Int("rawMessageLength", len(msg)).Str("rawMessage", string(msg)).Msg("读取到原始WebSocket消息") // 在此处处理心跳消息 if string(msg) == "*hb*" { log.Debug().Str("client", c.name).Str("msg_content", string(msg)).Msg("Received heartbeat message") // 额外验证:使用 fmt.Printf 确保字段能正确显示 // fmt.Printf("DEBUG: Heartbeat from %s with message: %s\n", c.name, string(msg)) continue } // 构造要发送的消息对象,包含用户名和消息内容 outgoing := map[string]string{ "name": c.name, // 发送者用户名 "message": string(msg), // 消息内容(转换为字符串) } // 将消息对象编码为 JSON 格式 jsMessage, err := json.Marshal(outgoing) if err != nil { log.Error().Err(err).Msg("Enconding failed!") continue } // 将编码后的消息和发送者信息一起发送到房间的 forward channel c.room.forward <- &message{ content: jsMessage, // 消息内容 sender: c, // 发送者(当前客户端) } // 房间会将这条消息分发给除发送者外的所有其他客户端(在 room.go 中 run() 方法 case msg := <-r.forward 中) } } // write 从 receive channel 读取消息并发送给客户端 // 在独立的 goroutine 中运行,处理向客户端发送消息的任务 func (c *Client) write() { defer c.socket.Close() // 循环从 receive channel 读取消息 for msg := range c.receive { err := c.socket.WriteMessage(websocket.TextMessage, msg) if err != nil { log.Error().Err(err).Str("client", c.name).Msg("写入WebSocket消息失败") return } } } // 根据房间参数生成客户端名称 // 用于生成客户端的唯一标识,用作唯一标识,进行查找用的 (门店端: shop_{shopId}_{userId}, 买家端: customer_{customId}_{userId}) func clientName(roomParams *RoomParams) string { // 首先根据 Type 来判断是门店端还是买家端 if roomParams.Type == "shop" { return fmt.Sprintf("shop_%d_%d", roomParams.ShopId, roomParams.StaffId) } else { return fmt.Sprintf("customer_%d_%d", roomParams.CustomId, roomParams.UserId) } }