部分联系人和会话接口

This commit is contained in:
2025-12-04 08:51:40 +08:00
parent f61d567c2a
commit 10a90b262d
6 changed files with 451 additions and 12 deletions

View File

@@ -0,0 +1,122 @@
/**
* package api
* 作用:会话列表(最近聊天)相关 API 处理器
*/
package api
import (
"xk-websocket-v2/internal/service"
"xk-websocket-v2/internal/utils"
"github.com/gin-gonic/gin"
)
/**
* GetConversationListHandler
* 功能:获取当前用户的会话列表
* 路径GET /api/conversations
*/
func GetConversationListHandler(c *gin.Context) {
userID, _ := c.Get("user_id")
list, err := service.ConversationSvc.GetConversations(userID.(string))
if err != nil {
utils.InternalError(c, "查询失败")
return
}
utils.SuccessWithData(c, list, "获取成功")
}
/**
* ResetConversationUnreadHandler
* 功能:重置会话未读数
* 路径POST /api/conversations/reset-unread
*/
func ResetConversationUnreadHandler(c *gin.Context) {
userID, _ := c.Get("user_id")
var req struct {
TargetID string `json:"target_id" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
utils.BadRequest(c, "参数错误")
return
}
if err := service.ConversationSvc.ResetUnread(userID.(string), req.TargetID); err != nil {
utils.InternalError(c, "操作失败")
return
}
utils.Success(c, "已重置未读")
}
/**
* UpdateConversationHandler
* 功能:更新会话标记(置顶、免打扰、特别关心)
* 路径POST /api/conversations/update
*/
func UpdateConversationHandler(c *gin.Context) {
userID, _ := c.Get("user_id")
var req struct {
TargetID string `json:"target_id" binding:"required"`
IsTop *bool `json:"is_top"`
IsMuted *bool `json:"is_muted"`
IsSpecialCare *bool `json:"is_special_care"`
}
if err := c.ShouldBindJSON(&req); err != nil {
utils.BadRequest(c, "参数错误")
return
}
updates := make(map[string]interface{})
if req.IsTop != nil {
updates["is_top"] = *req.IsTop
}
if req.IsMuted != nil {
updates["is_muted"] = *req.IsMuted
}
if req.IsSpecialCare != nil {
updates["is_special_care"] = *req.IsSpecialCare
}
if len(updates) == 0 {
utils.BadRequest(c, "没有可更新的字段")
return
}
if err := service.ConversationSvc.UpdateConversationFlags(userID.(string), req.TargetID, updates); err != nil {
utils.InternalError(c, "更新失败")
return
}
utils.Success(c, "更新成功")
}
/**
* DeleteConversationHandler
* 功能:删除会话
* 路径POST /api/conversations/delete
*/
func DeleteConversationHandler(c *gin.Context) {
userID, _ := c.Get("user_id")
var req struct {
TargetID string `json:"target_id" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
utils.BadRequest(c, "参数错误")
return
}
if err := service.ConversationSvc.DeleteConversation(userID.(string), req.TargetID); err != nil {
utils.InternalError(c, "删除失败")
return
}
utils.Success(c, "删除成功")
}

View File

@@ -183,6 +183,10 @@ type UserContact struct {
IsTop bool `gorm:"type:tinyint(1);default:0;comment:是否置顶" json:"is_top"`
// 是否免打扰
IsMuted bool `gorm:"type:tinyint(1);default:0;comment:是否免打扰" json:"is_muted"`
// 是否特别关心(高优先级提醒,类似 QQ 特别关心)
IsSpecialCare bool `gorm:"type:tinyint(1);default:0;comment:是否特别关心" json:"is_special_care"`
// 是否拉黑(加入黑名单,不再接收消息/出现在默认列表中)
IsBlocked bool `gorm:"type:tinyint(1);default:0;comment:是否拉黑" json:"is_blocked"`
// 最后聊天时间
LastChatTime *time.Time `gorm:"type:datetime;comment:最后聊天时间" json:"last_chat_time"`
// 最后一条消息
@@ -380,6 +384,45 @@ func (ApiRequestLog) TableName() string {
return "api_request_logs"
}
/**
* ChatConversation
* 对应数据库表chat_conversations
* 作用:用户级别的最近会话列表,支持置顶、免打扰和未读计数等功能
*/
type ChatConversation struct {
// 主键ID
ID uint `gorm:"primaryKey;comment:主键ID" json:"id"`
// 所属用户ID当前登录用户
UserID string `gorm:"type:varchar(100);index;comment:用户ID" json:"user_id"`
// 目标ID好友ID或群ID
TargetID string `gorm:"type:varchar(100);index;comment:目标ID(好友或群)" json:"target_id"`
// 房间ID与 chat_messages / chat_rooms 中的 room_id 一致
RoomID string `gorm:"type:varchar(100);index;comment:房间ID" json:"room_id"`
// 会话类型1-私聊 2-群聊
Type int `gorm:"type:int;comment:会话类型(1=私聊,2=群聊)" json:"type"`
// 是否置顶
IsTop bool `gorm:"type:tinyint(1);default:0;comment:是否置顶" json:"is_top"`
// 是否免打扰
IsMuted bool `gorm:"type:tinyint(1);default:0;comment:是否免打扰" json:"is_muted"`
// 是否特别关心(用于在前端高亮显示)
IsSpecialCare bool `gorm:"type:tinyint(1);default:0;comment:是否特别关心" json:"is_special_care"`
// 未读消息数
UnreadCount int `gorm:"type:int;default:0;comment:未读消息数" json:"unread_count"`
// 最后一条消息内容(用于列表摘要)
LastMessage string `gorm:"type:varchar(500);comment:最后一条消息内容" json:"last_message"`
// 最后消息时间
LastTime time.Time `gorm:"type:datetime;index;comment:最后消息时间" json:"last_time"`
// 创建时间
CreatedAt time.Time `gorm:"autoCreateTime;comment:创建时间" json:"created_at"`
// 更新时间
UpdatedAt time.Time `gorm:"autoUpdateTime;comment:更新时间" json:"updated_at"`
}
// TableName 指定表名和注释
func (ChatConversation) TableName() string {
return "chat_conversations"
}
/**
* LoginLog
* 对应数据库表login_logs

View File

@@ -141,7 +141,22 @@ func (s *ChatService) HandleUserMessage(senderClient *manager.Client, req *model
log.Printf("❌ 消息持久化失败: %v", err)
}
// 4. 构建推送消息体 (DTO)
// 4. 更新会话列表(发送方与接收方)
if ConversationSvc != nil {
// 当前实现仅支持点对点私聊
senderID := senderClient.UserID
receiverID := req.ReceiverUserID
// 对发送方更新会话(不增加未读)
if senderID != "" && receiverID != "" {
// sender 视角 target 为 receiver
_ = ConversationSvc.UpsertConversationOnMessage(senderID, receiverID, req.RoomID, msg, true)
// receiver 视角 target 为 sender
_ = ConversationSvc.UpsertConversationOnMessage(receiverID, senderID, req.RoomID, msg, false)
}
}
// 5. 构建推送消息体 (DTO)
// 将持久化后的完整对象推给前端
pushMsg := model.WsPayload{
RequestType: "receive_message",

View File

@@ -0,0 +1,149 @@
/**
* package service
* 作用:会话列表(最近聊天)管理服务
*/
package service
import (
"time"
"xk-websocket-v2/internal/model"
"gorm.io/gorm"
)
// ConversationService 会话服务结构体
type ConversationService struct {
DB *gorm.DB
}
// ConversationSvc 全局单例
var ConversationSvc *ConversationService
// InitConversationService 初始化会话服务
func InitConversationService(db *gorm.DB) {
ConversationSvc = &ConversationService{DB: db}
}
/**
* GetConversations
* 功能:获取用户的最近会话列表
*/
func (s *ConversationService) GetConversations(userID string) ([]model.ChatConversation, error) {
var list []model.ChatConversation
err := s.DB.Where("user_id = ?", userID).
Order("is_top DESC, last_time DESC, id DESC").
Find(&list).Error
return list, err
}
/**
* UpsertConversationOnMessage
* 功能:在发送/接收消息时更新会话记录
* @param userID 会话所属用户
* @param targetID 好友ID或群ID
* @param roomID 房间ID
* @param msg 已持久化的消息
* @param isSender 是否为发送方(发送方通常不增加未读数)
*/
func (s *ConversationService) UpsertConversationOnMessage(userID, targetID, roomID string, msg model.ChatMessage, isSender bool) error {
if s == nil {
return nil
}
var conv model.ChatConversation
tx := s.DB.Where("user_id = ? AND target_id = ?", userID, targetID).First(&conv)
now := time.Now()
// 计算摘要
summary := buildMessageSummary(msg)
if tx.Error != nil {
if tx.Error == gorm.ErrRecordNotFound {
// 新建会话
conv = model.ChatConversation{
UserID: userID,
TargetID: targetID,
RoomID: roomID,
Type: 1, // 目前只支持私聊,后续可扩展群聊
LastMessage: summary,
LastTime: now,
}
// 接收方增加未读
if !isSender {
conv.UnreadCount = 1
}
return s.DB.Create(&conv).Error
}
return tx.Error
}
// 已存在会话则更新
updates := map[string]interface{}{
"room_id": roomID,
"last_message": summary,
"last_time": now,
}
if !isSender {
updates["unread_count"] = conv.UnreadCount + 1
}
return s.DB.Model(&model.ChatConversation{}).
Where("id = ?", conv.ID).
Updates(updates).Error
}
/**
* ResetUnread
* 功能:清空某个目标的未读消息数
*/
func (s *ConversationService) ResetUnread(userID, targetID string) error {
return s.DB.Model(&model.ChatConversation{}).
Where("user_id = ? AND target_id = ?", userID, targetID).
Update("unread_count", 0).Error
}
/**
* UpdateConversationFlags
* 功能:更新会话标记(置顶、免打扰、特别关心等)
*/
func (s *ConversationService) UpdateConversationFlags(userID, targetID string, updates map[string]interface{}) error {
return s.DB.Model(&model.ChatConversation{}).
Where("user_id = ? AND target_id = ?", userID, targetID).
Updates(updates).Error
}
/**
* DeleteConversation
* 功能:删除单个会话记录(不删除聊天记录)
*/
func (s *ConversationService) DeleteConversation(userID, targetID string) error {
return s.DB.Where("user_id = ? AND target_id = ?", userID, targetID).
Delete(&model.ChatConversation{}).Error
}
// buildMessageSummary 根据消息类型构建会话摘要
func buildMessageSummary(msg model.ChatMessage) string {
switch msg.MessageType {
case 1:
return "[图片]"
case 2:
return "[语音]"
case 3:
return "[视频]"
case 6, 7:
return "[通话]"
case 8:
return "[文件]"
default:
if msg.Content == "" {
return "[空消息]"
}
if len([]rune(msg.Content)) > 50 {
return string([]rune(msg.Content)[:50]) + "..."
}
return msg.Content
}
}