redis.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. // Redis模块 - 负责Redis连接管理和消息存储
  2. package wsClient
  3. import (
  4. "context"
  5. "encoding/json"
  6. "fmt"
  7. "os"
  8. "strconv"
  9. "time"
  10. "github.com/redis/go-redis/v9"
  11. "github.com/rs/zerolog/log"
  12. )
  13. // Redis客户端实例,全局变量
  14. var redisClient *redis.Client
  15. // Redis上下文
  16. var ctx = context.Background()
  17. // 分数计算常量
  18. const (
  19. UnreadMultiplier = 10000.0 // 未读消息数乘数,用于小数部分存储(增大乘数以提高精度)
  20. ContactTTL = 7 * 24 * time.Hour // 最近联系人过期时间(7天)
  21. // 门店用户类型
  22. UserTypeShop = "shop"
  23. // 顾客用户类型
  24. UserTypeCustomer = "customer"
  25. )
  26. // ContactInfo 表示联系人信息
  27. type ContactInfo struct {
  28. ContactID int32 `json:"contact_id"` // 联系人ID
  29. Timestamp time.Time `json:"timestamp"` // 最后聊天时间
  30. UnreadCount int `json:"unread_count"` // 未读消息数
  31. ContactType string `json:"contact_type"` // 联系人类型(shop/customer)
  32. }
  33. // 从环境变量初始化Redis连接
  34. func InitializeRedisFromEnv() error {
  35. // 从环境变量获取Redis配置,如果没有设置则使用默认值
  36. redisAddr := os.Getenv("REDIS_ADDR")
  37. if redisAddr == "" {
  38. redisAddr = "localhost:6379" // 默认Redis地址
  39. }
  40. redisPassword := os.Getenv("REDIS_PASSWORD") // 默认为空
  41. redisDB := 0 // 默认数据库
  42. if dbStr := os.Getenv("REDIS_DB"); dbStr != "" {
  43. if db, err := strconv.Atoi(dbStr); err == nil {
  44. redisDB = db
  45. }
  46. }
  47. return InitRedis(redisAddr, redisPassword, redisDB)
  48. }
  49. // ChatMessage 表示存储在Redis中的聊天消息结构
  50. type ChatMessage struct {
  51. Content string `json:"content"` // 原始消息内容(JSON字符串)
  52. Timestamp time.Time `json:"timestamp"` // 存储时间戳
  53. RoomName string `json:"room_name"` // 房间名称
  54. }
  55. // InitRedis 初始化Redis连接
  56. func InitRedis(addr, password string, db int) error {
  57. redisClient = redis.NewClient(&redis.Options{
  58. Addr: addr, // Redis服务器地址 (例如: "localhost:6379")
  59. Password: password, // Redis密码 (如果没有密码则为空字符串)
  60. DB: db, // Redis数据库编号 (0-15)
  61. })
  62. // 测试Redis连接
  63. pong, err := redisClient.Ping(ctx).Result()
  64. if err != nil {
  65. log.Error().Err(err).Msg("Redis连接失败")
  66. return err
  67. }
  68. log.Info().Str("pong", pong).Msg("Redis连接成功")
  69. return nil
  70. }
  71. // StoreRawMessage 将原始消息内容存储到Redis中
  72. // 使用房间名作为Redis列表的键,存储消息历史记录
  73. func StoreRawMessage(roomName string, rawMessage []byte) error {
  74. if redisClient == nil {
  75. log.Warn().Msg("Redis客户端未初始化")
  76. return nil
  77. }
  78. // 创建消息对象,包含原始消息内容和时间戳
  79. chatMsg := ChatMessage{
  80. Content: string(rawMessage), // 直接存储原始JSON字符串
  81. Timestamp: time.Now(),
  82. RoomName: roomName,
  83. }
  84. // 将消息序列化为JSON
  85. msgBytes, err := json.Marshal(chatMsg)
  86. if err != nil {
  87. log.Error().Err(err).Msg("消息序列化失败")
  88. return err
  89. }
  90. // 使用房间名作为Redis键,将消息添加到列表的右端
  91. // 键格式: "room_messages:{房间名}"
  92. key := "room_messages:" + roomName
  93. err = redisClient.RPush(ctx, key, msgBytes).Err()
  94. if err != nil {
  95. log.Error().Err(err).Str("key", key).Msg("消息存储到Redis失败")
  96. return err
  97. }
  98. // 设置过期时间(可选)- 7天后自动删除
  99. redisClient.Expire(ctx, key, 7*24*time.Hour)
  100. log.Debug().Str("roomName", roomName).Msg("消息已存储到Redis")
  101. return nil
  102. }
  103. // StoreMessage 将消息存储到Redis中(保留旧接口以兼容)
  104. // 使用房间名作为Redis列表的键,存储消息历史记录
  105. func StoreMessage(roomName, username, message string) error {
  106. if redisClient == nil {
  107. log.Warn().Msg("Redis客户端未初始化")
  108. return nil
  109. }
  110. // 构建与新格式兼容的消息结构
  111. msgContent := map[string]interface{}{
  112. "name": username,
  113. "message": message,
  114. }
  115. msgBytes, _ := json.Marshal(msgContent)
  116. return StoreRawMessage(roomName, msgBytes)
  117. }
  118. // UpdateRecentContacts 更新最近联系人列表
  119. // 使用浮点数分数同时存储时间戳和未读消息数
  120. func UpdateRecentContacts(client *Client) error {
  121. if redisClient == nil {
  122. log.Warn().Msg("Redis客户端未初始化")
  123. return nil
  124. }
  125. currentTime := time.Now().Unix()
  126. if client.userInfo.Type == UserTypeShop {
  127. // 更新门店的联系人列表(客户)
  128. key := getContactKey(UserTypeShop, client.userInfo.ShopId) // shop_chats:{ShopId}
  129. if key != "" {
  130. // 先获取当前未读消息数
  131. // currentScore, err := redisClient.ZScore(ctx, key, strconv.Itoa(int(client.userInfo.CustomId))).Result()
  132. unreadCount := 0 // ---->>>>> 门店是主动方,所以未读消息数为0
  133. // if err == nil {
  134. // // 如果成员已存在,保留当前的未读消息数
  135. // _, unreadCount = parseScore(currentScore)
  136. // }
  137. score := calculateScore(currentTime, unreadCount)
  138. redisClient.ZAdd(ctx, key, redis.Z{
  139. Score: score,
  140. Member: strconv.Itoa(int(client.userInfo.CustomId)),
  141. })
  142. // 设置过期时间
  143. redisClient.Expire(ctx, key, ContactTTL)
  144. }
  145. // 更新客户的联系人列表(门店)
  146. key = getContactKey(UserTypeCustomer, client.userInfo.UserId) // customer_chats:{UserId}
  147. if key != "" {
  148. // 先获取当前未读消息数
  149. currentScore, err := redisClient.ZScore(ctx, key, strconv.Itoa(int(client.userInfo.ShopId))).Result()
  150. unreadCount := 0
  151. if err == nil {
  152. // 如果成员已存在,保留当前的未读消息数
  153. _, unreadCount = parseScore(currentScore)
  154. }
  155. score := calculateScore(currentTime, unreadCount)
  156. redisClient.ZAdd(ctx, key, redis.Z{
  157. Score: score,
  158. Member: strconv.Itoa(int(client.userInfo.ShopId)),
  159. })
  160. // 设置过期时间
  161. redisClient.Expire(ctx, key, ContactTTL)
  162. }
  163. }
  164. if client.userInfo.Type == UserTypeCustomer {
  165. // 更新客户的联系人列表(门店)
  166. key := getContactKey(UserTypeCustomer, client.userInfo.UserId) // customer_chats:{UserId}
  167. if key != "" {
  168. // 先获取当前未读消息数
  169. // currentScore, err := redisClient.ZScore(ctx, key, strconv.Itoa(int(client.userInfo.ShopId))).Result()
  170. unreadCount := 0 // ---->>>>> 客户是主动方,所以未读消息数为0
  171. // if err == nil {
  172. // // 如果成员已存在,保留当前的未读消息数
  173. // _, unreadCount = parseScore(currentScore)
  174. // }
  175. score := calculateScore(currentTime, unreadCount)
  176. redisClient.ZAdd(ctx, key, redis.Z{
  177. Score: score,
  178. Member: strconv.Itoa(int(client.userInfo.ShopId)),
  179. })
  180. // 设置过期时间
  181. redisClient.Expire(ctx, key, ContactTTL)
  182. }
  183. // 更新门店的联系人列表(客户)
  184. key = getContactKey(UserTypeShop, client.userInfo.ShopId) // shop_chats:{ShopId}
  185. if key != "" {
  186. // 先获取当前未读消息数
  187. currentScore, err := redisClient.ZScore(ctx, key, strconv.Itoa(int(client.userInfo.CustomId))).Result()
  188. unreadCount := 0
  189. if err == nil {
  190. // 如果成员已存在,保留当前的未读消息数
  191. _, unreadCount = parseScore(currentScore)
  192. }
  193. score := calculateScore(currentTime, unreadCount)
  194. redisClient.ZAdd(ctx, key, redis.Z{
  195. Score: score,
  196. Member: strconv.Itoa(int(client.userInfo.CustomId)),
  197. })
  198. // 设置过期时间
  199. redisClient.Expire(ctx, key, ContactTTL)
  200. }
  201. }
  202. log.Debug().Str("clientName", client.name).Msg("最近联系人列表已更新")
  203. return nil
  204. }
  205. // 增加未读消息数
  206. // 为接收者(对方)的联系人列表增加未读消息数
  207. func IncrementUnreadCount(sender *Client, count int) error {
  208. if redisClient == nil {
  209. log.Warn().Msg("Redis客户端未初始化")
  210. return nil
  211. }
  212. // 根据发送者的类型确定接收者的类型和ID
  213. var receiverType string
  214. var receiverID int32
  215. var senderMember string
  216. if sender.userInfo.Type == UserTypeShop {
  217. // 门店发送消息给客户,接收者是客户
  218. receiverType = UserTypeCustomer
  219. receiverID = sender.userInfo.UserId
  220. senderMember = strconv.Itoa(int(sender.userInfo.ShopId))
  221. } else if sender.userInfo.Type == UserTypeCustomer {
  222. // 客户发送消息给门店,接收者是门店
  223. receiverType = UserTypeShop
  224. receiverID = sender.userInfo.ShopId
  225. senderMember = strconv.Itoa(int(sender.userInfo.CustomId))
  226. } else {
  227. log.Warn().Str("clientType", sender.userInfo.Type).Msg("未知的客户端类型")
  228. return nil
  229. }
  230. // 获取接收者的联系人列表键
  231. key := getContactKey(receiverType, receiverID)
  232. if key == "" {
  233. log.Error().Str("receiverType", receiverType).Int32("receiverID", receiverID).Msg("无法生成联系人键")
  234. return nil
  235. }
  236. // 获取当前分数
  237. currentScore, err := redisClient.ZScore(ctx, key, senderMember).Result()
  238. if err != nil {
  239. // 如果成员不存在,使用当前时间戳和0未读消息数创建
  240. currentScore = calculateScore(time.Now().Unix(), 0)
  241. }
  242. // 解析当前的分数
  243. _, currentUnread := parseScore(currentScore)
  244. // 增加未读消息数
  245. newUnread := currentUnread + count
  246. // 重新计算分数
  247. timestamp := time.Now().Unix()
  248. newScore := calculateScore(timestamp, newUnread)
  249. // 更新分数
  250. redisClient.ZAdd(ctx, key, redis.Z{
  251. Score: newScore,
  252. Member: senderMember,
  253. })
  254. // 设置过期时间
  255. redisClient.Expire(ctx, key, ContactTTL)
  256. // log.Printf("未读消息数已增加 - 接收者类型: %s, 接收者ID: %d, 发送者ID: %s, 未读数: %d, 新分数: %f", receiverType, receiverID, senderMember, newUnread, newScore)
  257. log.Debug().
  258. Str("receiverType", receiverType).
  259. Int32("receiverID", receiverID).
  260. Str("senderMember", senderMember).
  261. Int("newUnread", newUnread).
  262. Float64("newScore", newScore).
  263. Msg("未读消息数已增加")
  264. return nil
  265. }
  266. // GetRoomMessages 获取房间的历史消息
  267. // 返回指定数量的最新消息
  268. func GetRoomMessages(roomName string, count int64) ([]ChatMessage, error) {
  269. if redisClient == nil {
  270. log.Warn().Msg("Redis客户端未初始化")
  271. return nil, nil
  272. }
  273. key := "room_messages:" + roomName
  274. // 获取列表中的最新消息(从右端开始,即最新的消息)
  275. // -count 表示从列表末尾开始的count个元素
  276. result, err := redisClient.LRange(ctx, key, -count, -1).Result()
  277. if err != nil {
  278. //log.Printf("从Redis获取消息失败: %v", err)
  279. log.Error().Err(err).Str("key", key).Msg("从Redis获取消息失败")
  280. return nil, err
  281. }
  282. var messages []ChatMessage
  283. for _, msgStr := range result {
  284. var msg ChatMessage
  285. if err := json.Unmarshal([]byte(msgStr), &msg); err != nil {
  286. // log.Printf("消息反序列化失败: %v", err)
  287. log.Error().Err(err).Str("msg", msgStr).Msg("消息反序列化失败")
  288. continue
  289. }
  290. messages = append(messages, msg)
  291. }
  292. return messages, nil
  293. }
  294. // GetRoomMessageCount 获取房间的消息总数
  295. func GetRoomMessageCount(roomName string) (int64, error) {
  296. if redisClient == nil {
  297. return 0, nil
  298. }
  299. key := "room_messages:" + roomName
  300. return redisClient.LLen(ctx, key).Result()
  301. }
  302. // CloseRedis 关闭Redis连接
  303. func CloseRedis() error {
  304. if redisClient != nil {
  305. return redisClient.Close()
  306. }
  307. return nil
  308. }
  309. // calculateScore 计算有序集合的分数,同时存储时间戳和未读消息数
  310. // 整数部分:Unix时间戳,小数部分:未读消息数(除以乘数)
  311. func calculateScore(timestamp int64, unreadCount int) float64 {
  312. return float64(timestamp) + (float64(unreadCount) / UnreadMultiplier)
  313. }
  314. // parseScore 从分数中解析出时间戳和未读消息数
  315. func parseScore(score float64) (timestamp int64, unreadCount int) {
  316. timestamp = int64(score) // 整数部分是时间戳
  317. decimalPart := score - float64(timestamp)
  318. // 使用四舍五入来处理浮点数精度问题
  319. unreadCount = int(decimalPart*UnreadMultiplier + 0.5)
  320. return
  321. }
  322. // 生成发送者和接收者的信息
  323. // @param sender 发送者
  324. // @param count 未读消息数
  325. // @return 接收者类型, 接收者ID, 未读消息数
  326. func GeneHaveSendInfo(sender *Client, count int) (string, int32, int32, int) {
  327. if sender.userInfo.Type == UserTypeShop { // 门店发送消息给 客户
  328. return UserTypeCustomer, sender.userInfo.ShopId, sender.userInfo.UserId, count
  329. } else if sender.userInfo.Type == UserTypeCustomer { // 客户发送消息给 门店
  330. return UserTypeShop, sender.userInfo.ShopId, sender.userInfo.UserId, count
  331. }
  332. return "", 0, 0, 0
  333. }
  334. /**
  335. * 生成联系人的Redis键
  336. * @param userType 用户类型
  337. * @param id 用户ID -- shopId 或 userId
  338. * @return 联系人的Redis键
  339. */
  340. func getContactKey(userType string, id int32) string {
  341. if userType == UserTypeShop {
  342. return "shop_chats:" + strconv.Itoa(int(id))
  343. } else if userType == UserTypeCustomer {
  344. return "customer_chats:" + strconv.Itoa(int(id))
  345. }
  346. return ""
  347. }
  348. // GetRecentContacts 获取用户的最近联系人列表
  349. // 返回按最后聊天时间排序的联系人列表(最新的在前)
  350. func GetRecentContacts(userType string, userID int32, limit int64) ([]ContactInfo, error) {
  351. if redisClient == nil {
  352. log.Warn().Msg("Redis客户端未初始化")
  353. return nil, nil
  354. }
  355. key := getContactKey(userType, userID)
  356. if key == "" {
  357. return nil, fmt.Errorf("无效的用户类型: %s", userType)
  358. }
  359. // 获取最近的联系人(按分数降序,最新的在前)
  360. result, err := redisClient.ZRevRangeWithScores(ctx, key, 0, limit-1).Result()
  361. if err != nil {
  362. log.Error().Err(err).Str("key", key).Msg("获取最近联系人失败")
  363. return nil, err
  364. }
  365. var contacts []ContactInfo
  366. for _, item := range result {
  367. contactID, err := strconv.Atoi(item.Member.(string))
  368. if err != nil {
  369. // log.Printf("解析联系人ID失败: %v", err)
  370. log.Error().Err(err).Any("member", item.Member).Msg("解析联系人ID失败")
  371. continue
  372. }
  373. timestamp, unreadCount := parseScore(item.Score)
  374. // 确定联系人的类型
  375. contactType := UserTypeCustomer
  376. if userType == UserTypeCustomer {
  377. contactType = UserTypeShop
  378. }
  379. contact := ContactInfo{
  380. ContactID: int32(contactID),
  381. Timestamp: time.Unix(timestamp, 0),
  382. UnreadCount: unreadCount,
  383. ContactType: contactType,
  384. }
  385. contacts = append(contacts, contact)
  386. }
  387. return contacts, nil
  388. }
  389. // GetTotalUnreadCount 获取用户的总未读消息数
  390. func GetTotalUnreadCount(userType string, userID int32) (int, error) {
  391. if redisClient == nil {
  392. log.Warn().Msg("Redis客户端未初始化")
  393. return 0, nil
  394. }
  395. key := getContactKey(userType, userID)
  396. if key == "" {
  397. return 0, fmt.Errorf("无效的用户类型: %s", userType)
  398. }
  399. // 获取所有联系人的分数
  400. result, err := redisClient.ZRangeWithScores(ctx, key, 0, -1).Result()
  401. if err != nil {
  402. log.Error().Err(err).Str("key", key).Msg("获取联系人分数失败")
  403. return 0, err
  404. }
  405. totalUnread := 0
  406. for _, item := range result {
  407. _, unreadCount := parseScore(item.Score)
  408. totalUnread += unreadCount
  409. }
  410. return totalUnread, nil
  411. }
  412. /**
  413. * 清除指定联系人的未读消息数
  414. * @param client 客户端
  415. * @return 错误
  416. */
  417. func ClearUnreadCount(client *Client) error {
  418. id := client.userInfo.ShopId
  419. contactID := client.userInfo.CustomId
  420. if client.userInfo.Type == UserTypeCustomer {
  421. id = client.userInfo.UserId
  422. contactID = client.userInfo.ShopId
  423. }
  424. if redisClient == nil {
  425. log.Warn().Msg("Redis客户端未初始化")
  426. return fmt.Errorf("redis client is not initialized")
  427. }
  428. key := getContactKey(client.userInfo.Type, id)
  429. if key == "" {
  430. log.Error().Str("userType", client.userInfo.Type).Msg("无效的用户类型")
  431. return fmt.Errorf("invalid user type: %s", client.userInfo.Type)
  432. }
  433. contactIDStr := strconv.Itoa(int(contactID))
  434. // 获取当前分数
  435. currentScore, err := redisClient.ZScore(ctx, key, contactIDStr).Result()
  436. if err != nil {
  437. if err == redis.Nil {
  438. // 如果联系人不存在,这并非一个错误,只是无需清理。
  439. // log.Printf("Contact not found, no unread count to clear - UserType: %s, UserID: %d, ContactID: %d", client.userInfo.Type, id, contactID)
  440. log.Debug().
  441. Str("userType", client.userInfo.Type).
  442. Int32("userID", id).
  443. Int32("contactID", contactID).
  444. Msg("Contact not found, no unread count to clear")
  445. return nil
  446. }
  447. // 对于其他类型的错误(如连接问题),则返回错误。
  448. // log.Printf("failed to get score for contact - UserType: %s, UserID: %d, ContactID: %d, Error: %v",client.userInfo.Type, id, contactID, err)
  449. log.Error().
  450. Err(err).
  451. Str("userType", client.userInfo.Type).
  452. Int32("userID", id).
  453. Int32("contactID", contactID).
  454. Msg("failed to get score for contact")
  455. return err
  456. }
  457. // 解析当前分数,保留时间戳,清除未读消息数
  458. timestamp, _ := parseScore(currentScore)
  459. newScore := calculateScore(timestamp, 0)
  460. // 更新分数
  461. if err := redisClient.ZAdd(ctx, key, redis.Z{
  462. Score: newScore,
  463. Member: contactIDStr,
  464. }).Err(); err != nil {
  465. log.Error().
  466. Err(err).
  467. Str("userType", client.userInfo.Type).
  468. Int32("userID", id).
  469. Int32("contactID", contactID).
  470. Msg("failed to clear unread count")
  471. return err
  472. }
  473. // log.Printf("未读消息数已清除 - 用户类型: %s, 用户ID: %d, 联系人ID: %d", client.userInfo.Type, id, contactID)
  474. log.Info().
  475. Str("userType", client.userInfo.Type).
  476. Int32("userID", id).
  477. Int32("contactID", contactID).
  478. Msg("未读消息数已清除")
  479. return nil
  480. }