|
|
@@ -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
|
|
|
+}
|