部分联系人和会话接口
This commit is contained in:
@@ -38,7 +38,7 @@
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"account\": \"user1@example.com\",\n \"password\": \"12345678\",\n \"remember\": true\n}"
|
||||
"raw": "{\n \"account\": \"workerqi@163.com\",\n \"password\": \"12345678\",\n \"remember\": true\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/login",
|
||||
@@ -52,9 +52,13 @@
|
||||
"script": {
|
||||
"exec": [
|
||||
"if (pm.response.code === 200) {",
|
||||
" var jsonData = pm.response.json();",
|
||||
" pm.collectionVariables.set(\"token\", jsonData.token);",
|
||||
" pm.collectionVariables.set(\"user_id\", jsonData.user.id);",
|
||||
" const body = pm.response.json();",
|
||||
" if (body.code === 0 && body.result && body.result.token) {",
|
||||
" pm.environment.set(\"token\", body.result.token);",
|
||||
" if (body.result.user && body.result.user.id) {",
|
||||
" pm.environment.set(\"user_id\", body.result.user.id);",
|
||||
" }",
|
||||
" }",
|
||||
"}"
|
||||
]
|
||||
}
|
||||
@@ -143,6 +147,103 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "会话管理",
|
||||
"item": [
|
||||
{
|
||||
"name": "获取会话列表",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/conversations",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "conversations"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "重置会话未读数",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"target_id\": \"10002\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/conversations/reset-unread",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "conversations", "reset-unread"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "更新会话标记",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"target_id\": \"10002\",\n \"is_top\": true,\n \"is_muted\": false,\n \"is_special_care\": true\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/conversations/update",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "conversations", "update"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "删除会话",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"target_id\": \"10002\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/conversations/delete",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "conversations", "delete"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "用户管理",
|
||||
"item": [
|
||||
|
||||
@@ -95,6 +95,7 @@ func initDB() *gorm.DB {
|
||||
&model.UserContact{},
|
||||
&model.ContactGroup{},
|
||||
&model.ChatRoom{},
|
||||
&model.ChatConversation{},
|
||||
&model.FriendRequest{},
|
||||
&model.VerificationCode{},
|
||||
&model.Attachment{},
|
||||
@@ -113,6 +114,7 @@ func initDB() *gorm.DB {
|
||||
{"chat_messages", "聊天消息表,持久化存储聊天记录,包括文本、多媒体和信令状态"},
|
||||
{"users", "用户基本信息表"},
|
||||
{"user_contacts", "用户联系人表,存储好友关系、分组、备注等信息"},
|
||||
{"chat_conversations", "用户会话表,记录每个用户的最近会话列表和未读信息"},
|
||||
{"contact_groups", "联系人分组表"},
|
||||
{"chat_rooms", "聊天房间表,支持点对点和群聊"},
|
||||
{"friend_requests", "好友申请表"},
|
||||
@@ -209,6 +211,7 @@ func main() {
|
||||
service.InitAuthService(db, rdb) // 认证服务(登录、注册、验证码)
|
||||
service.InitUserService(db) // 用户服务(用户信息管理)
|
||||
service.InitContactService(db) // 联系人服务(好友管理、分组管理)
|
||||
service.InitConversationService(db) // 会话服务(最近聊天列表)
|
||||
service.InitRoomService(db) // 房间服务(聊天房间管理)
|
||||
service.InitAttachmentService(db) // 附件服务(文件上传、管理)
|
||||
service.InitLoginLogService(db) // 登录日志服务(记录登录历史)
|
||||
@@ -339,6 +342,12 @@ func main() {
|
||||
authGroup.POST("/contacts/update/:id", api.UpdateContactHandler)
|
||||
authGroup.POST("/contacts/delete/:id", api.DeleteContactHandler)
|
||||
|
||||
// 会话管理
|
||||
authGroup.GET("/conversations", api.GetConversationListHandler)
|
||||
authGroup.POST("/conversations/reset-unread", api.ResetConversationUnreadHandler)
|
||||
authGroup.POST("/conversations/update", api.UpdateConversationHandler)
|
||||
authGroup.POST("/conversations/delete", api.DeleteConversationHandler)
|
||||
|
||||
// 房间管理
|
||||
authGroup.POST("/rooms", api.CreateRoomHandler)
|
||||
authGroup.GET("/rooms/:id", api.GetRoomHandler)
|
||||
|
||||
122
internal/api/conversation_handler.go
Normal file
122
internal/api/conversation_handler.go
Normal 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, "删除成功")
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
149
internal/service/conversation_service.go
Normal file
149
internal/service/conversation_service.go
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user