redis.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. // Redis模块 - 负责Redis连接管理和消息存储
  2. package wsClient
  3. import (
  4. "context"
  5. "encoding/json"
  6. "fmt"
  7. "log"
  8. "os"
  9. "strconv"
  10. "time"
  11. "github.com/redis/go-redis/v9"
  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.Printf("Redis连接失败: %v", err)
  66. return err
  67. }
  68. log.Printf("Redis连接成功: %s", pong)
  69. return nil
  70. }
  71. // StoreRawMessage 将原始消息内容存储到Redis中
  72. // 使用房间名作为Redis列表的键,存储消息历史记录
  73. func StoreRawMessage(roomName string, rawMessage []byte) error {
  74. if redisClient == nil {
  75. log.Println("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.Printf("消息序列化失败: %v", err)
  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.Printf("消息存储到Redis失败: %v", err)
  96. return err
  97. }
  98. // 设置过期时间(可选)- 7天后自动删除
  99. redisClient.Expire(ctx, key, 7*24*time.Hour)
  100. log.Printf("消息已存储到Redis - 房间: %s\n\n", roomName)
  101. return nil
  102. }
  103. // StoreMessage 将消息存储到Redis中(保留旧接口以兼容)
  104. // 使用房间名作为Redis列表的键,存储消息历史记录
  105. func StoreMessage(roomName, username, message string) error {
  106. if redisClient == nil {
  107. log.Println("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.Println("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.Printf("最近联系人列表已更新 - client name: %s", client.name)
  203. return nil
  204. }
  205. // 增加未读消息数
  206. // 为接收者(对方)的联系人列表增加未读消息数
  207. func IncrementUnreadCount(sender *Client, count int) error {
  208. if redisClient == nil {
  209. log.Println("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.Printf("未知的客户端类型: %s", sender.userInfo.Type)
  228. return nil
  229. }
  230. // 获取接收者的联系人列表键
  231. key := getContactKey(receiverType, receiverID)
  232. if key == "" {
  233. log.Printf("无法生成联系人键 - 类型: %s, ID: %d", receiverType, receiverID)
  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",
  257. receiverType, receiverID, senderMember, newUnread, newScore)
  258. return nil
  259. }
  260. // GetRoomMessages 获取房间的历史消息
  261. // 返回指定数量的最新消息
  262. func GetRoomMessages(roomName string, count int64) ([]ChatMessage, error) {
  263. if redisClient == nil {
  264. log.Println("Redis客户端未初始化")
  265. return nil, nil
  266. }
  267. key := "room_messages:" + roomName
  268. // 获取列表中的最新消息(从右端开始,即最新的消息)
  269. // -count 表示从列表末尾开始的count个元素
  270. result, err := redisClient.LRange(ctx, key, -count, -1).Result()
  271. if err != nil {
  272. log.Printf("从Redis获取消息失败: %v", err)
  273. return nil, err
  274. }
  275. var messages []ChatMessage
  276. for _, msgStr := range result {
  277. var msg ChatMessage
  278. if err := json.Unmarshal([]byte(msgStr), &msg); err != nil {
  279. log.Printf("消息反序列化失败: %v", err)
  280. continue
  281. }
  282. messages = append(messages, msg)
  283. }
  284. return messages, nil
  285. }
  286. // GetRoomMessageCount 获取房间的消息总数
  287. func GetRoomMessageCount(roomName string) (int64, error) {
  288. if redisClient == nil {
  289. return 0, nil
  290. }
  291. key := "room_messages:" + roomName
  292. return redisClient.LLen(ctx, key).Result()
  293. }
  294. // CloseRedis 关闭Redis连接
  295. func CloseRedis() error {
  296. if redisClient != nil {
  297. return redisClient.Close()
  298. }
  299. return nil
  300. }
  301. // calculateScore 计算有序集合的分数,同时存储时间戳和未读消息数
  302. // 整数部分:Unix时间戳,小数部分:未读消息数(除以乘数)
  303. func calculateScore(timestamp int64, unreadCount int) float64 {
  304. return float64(timestamp) + (float64(unreadCount) / UnreadMultiplier)
  305. }
  306. // parseScore 从分数中解析出时间戳和未读消息数
  307. func parseScore(score float64) (timestamp int64, unreadCount int) {
  308. timestamp = int64(score) // 整数部分是时间戳
  309. decimalPart := score - float64(timestamp)
  310. // 使用四舍五入来处理浮点数精度问题
  311. unreadCount = int(decimalPart*UnreadMultiplier + 0.5)
  312. return
  313. }
  314. /**
  315. * 生成联系人的Redis键
  316. * @param userType 用户类型
  317. * @param id 用户ID -- shopId 或 userId
  318. * @return 联系人的Redis键
  319. */
  320. func getContactKey(userType string, id int32) string {
  321. if userType == UserTypeShop {
  322. return "shop_chats:" + strconv.Itoa(int(id))
  323. } else if userType == UserTypeCustomer {
  324. return "customer_chats:" + strconv.Itoa(int(id))
  325. }
  326. return ""
  327. }
  328. // GetRecentContacts 获取用户的最近联系人列表
  329. // 返回按最后聊天时间排序的联系人列表(最新的在前)
  330. func GetRecentContacts(userType string, userID int32, limit int64) ([]ContactInfo, error) {
  331. if redisClient == nil {
  332. log.Println("Redis客户端未初始化")
  333. return nil, nil
  334. }
  335. key := getContactKey(userType, userID)
  336. if key == "" {
  337. return nil, fmt.Errorf("无效的用户类型: %s", userType)
  338. }
  339. // 获取最近的联系人(按分数降序,最新的在前)
  340. result, err := redisClient.ZRevRangeWithScores(ctx, key, 0, limit-1).Result()
  341. if err != nil {
  342. log.Printf("获取最近联系人失败: %v", err)
  343. return nil, err
  344. }
  345. var contacts []ContactInfo
  346. for _, item := range result {
  347. contactID, err := strconv.Atoi(item.Member.(string))
  348. if err != nil {
  349. log.Printf("解析联系人ID失败: %v", err)
  350. continue
  351. }
  352. timestamp, unreadCount := parseScore(item.Score)
  353. // 确定联系人的类型
  354. contactType := UserTypeCustomer
  355. if userType == UserTypeCustomer {
  356. contactType = UserTypeShop
  357. }
  358. contact := ContactInfo{
  359. ContactID: int32(contactID),
  360. Timestamp: time.Unix(timestamp, 0),
  361. UnreadCount: unreadCount,
  362. ContactType: contactType,
  363. }
  364. contacts = append(contacts, contact)
  365. }
  366. return contacts, nil
  367. }
  368. // GetTotalUnreadCount 获取用户的总未读消息数
  369. func GetTotalUnreadCount(userType string, userID int32) (int, error) {
  370. if redisClient == nil {
  371. log.Println("Redis客户端未初始化")
  372. return 0, nil
  373. }
  374. key := getContactKey(userType, userID)
  375. if key == "" {
  376. return 0, fmt.Errorf("无效的用户类型: %s", userType)
  377. }
  378. // 获取所有联系人的分数
  379. result, err := redisClient.ZRangeWithScores(ctx, key, 0, -1).Result()
  380. if err != nil {
  381. log.Printf("获取联系人分数失败: %v", err)
  382. return 0, err
  383. }
  384. totalUnread := 0
  385. for _, item := range result {
  386. _, unreadCount := parseScore(item.Score)
  387. totalUnread += unreadCount
  388. }
  389. return totalUnread, nil
  390. }
  391. /**
  392. * 清除指定联系人的未读消息数
  393. * @param client 客户端
  394. * @return 错误
  395. */
  396. func ClearUnreadCount(client *Client) error {
  397. id := client.userInfo.ShopId
  398. contactID := client.userInfo.CustomId
  399. if client.userInfo.Type == UserTypeCustomer {
  400. id = client.userInfo.UserId
  401. contactID = client.userInfo.ShopId
  402. }
  403. if redisClient == nil {
  404. log.Println("Redis客户端未初始化")
  405. return fmt.Errorf("redis client is not initialized")
  406. }
  407. key := getContactKey(client.userInfo.Type, id)
  408. if key == "" {
  409. log.Printf("无效的用户类型: %s", client.userInfo.Type)
  410. return fmt.Errorf("invalid user type: %s", client.userInfo.Type)
  411. }
  412. contactIDStr := strconv.Itoa(int(contactID))
  413. // 获取当前分数
  414. currentScore, err := redisClient.ZScore(ctx, key, contactIDStr).Result()
  415. if err != nil {
  416. if err == redis.Nil {
  417. // 如果联系人不存在,这并非一个错误,只是无需清理。
  418. log.Printf("Contact not found, no unread count to clear - UserType: %s, UserID: %d, ContactID: %d",
  419. client.userInfo.Type, id, contactID)
  420. return nil
  421. }
  422. // 对于其他类型的错误(如连接问题),则返回错误。
  423. log.Printf("failed to get score for contact - UserType: %s, UserID: %d, ContactID: %d, Error: %v",
  424. client.userInfo.Type, id, contactID, err)
  425. return err
  426. }
  427. // 解析当前分数,保留时间戳,清除未读消息数
  428. timestamp, _ := parseScore(currentScore)
  429. newScore := calculateScore(timestamp, 0)
  430. // 更新分数
  431. if err := redisClient.ZAdd(ctx, key, redis.Z{
  432. Score: newScore,
  433. Member: contactIDStr,
  434. }).Err(); err != nil {
  435. log.Printf("failed to clear unread count - UserType: %s, UserID: %d, ContactID: %d, Error: %v",
  436. client.userInfo.Type, id, contactID, err)
  437. return err
  438. }
  439. log.Printf("未读消息数已清除 - 用户类型: %s, 用户ID: %d, 联系人ID: %d",
  440. client.userInfo.Type, id, contactID)
  441. return nil
  442. }