修复了一些问题

This commit is contained in:
李琦
2026-07-08 08:18:58 +08:00
parent bb7891706b
commit 3417856607
19 changed files with 615 additions and 778 deletions

View File

@@ -106,6 +106,8 @@ func initDB() *gorm.DB {
&model.MomentLike{}, &model.MomentLike{},
&model.MomentComment{}, &model.MomentComment{},
&model.MomentNotification{}, &model.MomentNotification{},
&model.MessageReadReceipt{},
&model.UserSetting{},
) )
if err != nil { if err != nil {
log.Fatalf("❌ 数据库迁移失败: %v", err) log.Fatalf("❌ 数据库迁移失败: %v", err)
@@ -131,6 +133,8 @@ func initDB() *gorm.DB {
{"moment_likes", "动态点赞表"}, {"moment_likes", "动态点赞表"},
{"moment_comments", "动态评论表"}, {"moment_comments", "动态评论表"},
{"moment_notifications", "朋友圈通知表"}, {"moment_notifications", "朋友圈通知表"},
{"message_read_receipts", "消息已读回执表"},
{"user_settings", "用户个性化设置表"},
} }
for _, tc := range tableComments { for _, tc := range tableComments {
@@ -197,6 +201,15 @@ func main() {
db := initDB() // 连接MySQL数据库 db := initDB() // 连接MySQL数据库
rdb := initRedis() // 连接Redis rdb := initRedis() // 连接Redis
// 初始化 ip2region 离线 IP 库
ip2regionPath := viper.GetString("ip2region.db_path")
if ip2regionPath == "" {
ip2regionPath = "./ip2region.xdb"
}
if err := utils.InitIP2Region(ip2regionPath); err != nil {
log.Printf("⚠️ ip2region 初始化失败: %v", err)
}
// 步骤2: 启动WebSocket工作池用于处理WebSocket消息 // 步骤2: 启动WebSocket工作池用于处理WebSocket消息
ws.StartWorkerPool() ws.StartWorkerPool()
defer ws.StopWorkerPool() // 程序退出时关闭工作池 defer ws.StopWorkerPool() // 程序退出时关闭工作池
@@ -283,7 +296,7 @@ func main() {
clientID := fmt.Sprintf("%s-%d", viper.GetString("app.node_id"), time.Now().UnixNano()) clientID := fmt.Sprintf("%s-%d", viper.GetString("app.node_id"), time.Now().UnixNano())
// 步骤5: 创建客户端对象,包含连接和发送队列 // 步骤5: 创建客户端对象,包含连接和发送队列
client := &manager.Client{ID: clientID, Conn: conn, SendQueue: make(chan []byte, 256)} client := &manager.Client{ID: clientID, RemoteIP: c.ClientIP(), Conn: conn, SendQueue: make(chan []byte, 256)}
// 步骤6: 注册客户端到管理器 // 步骤6: 注册客户端到管理器
manager.Manager.Register(client) manager.Manager.Register(client)
@@ -344,6 +357,7 @@ func main() {
// 用户管理 // 用户管理
authGroup.GET("/user/my-info", api.GetMyInfoHandler) authGroup.GET("/user/my-info", api.GetMyInfoHandler)
authGroup.GET("/user/list", api.GetUserListHandler) authGroup.GET("/user/list", api.GetUserListHandler)
authGroup.GET("/user/:id", api.GetUserByIDHandler)
authGroup.POST("/user/create", api.CreateUserHandler) authGroup.POST("/user/create", api.CreateUserHandler)
authGroup.POST("/user/update", api.UpdateUserHandler) authGroup.POST("/user/update", api.UpdateUserHandler)
authGroup.POST("/user/delete", api.DeleteUserHandler) authGroup.POST("/user/delete", api.DeleteUserHandler)
@@ -363,6 +377,15 @@ func main() {
authGroup.POST("/contacts/update/:id", api.UpdateContactHandler) authGroup.POST("/contacts/update/:id", api.UpdateContactHandler)
authGroup.POST("/contacts/delete/:id", api.DeleteContactHandler) authGroup.POST("/contacts/delete/:id", api.DeleteContactHandler)
// 消息撤回、登出、已读回执
authGroup.POST("/messages/recall", api.RecallMessageHandler)
authGroup.POST("/messages/read-receipts", api.MarkMessagesReadHandler)
authGroup.POST("/logout", api.LogoutHandler)
// 用户设置
authGroup.GET("/user/settings", api.GetUserSettingsHandler)
authGroup.POST("/user/settings", api.UpdateUserSettingsHandler)
// 会话管理 // 会话管理
authGroup.GET("/conversations/by-room/:room_id", api.GetConversationByRoomHandler) authGroup.GET("/conversations/by-room/:room_id", api.GetConversationByRoomHandler)
authGroup.GET("/conversations", api.GetConversationListHandler) authGroup.GET("/conversations", api.GetConversationListHandler)
@@ -389,7 +412,10 @@ func main() {
authGroup.POST("/groups/:room_id/announcement", api.UpdateGroupAnnouncementHandler) authGroup.POST("/groups/:room_id/announcement", api.UpdateGroupAnnouncementHandler)
authGroup.POST("/groups/:room_id/members/:user_id/mute", api.MuteGroupMemberHandler) authGroup.POST("/groups/:room_id/members/:user_id/mute", api.MuteGroupMemberHandler)
authGroup.GET("/groups/:room_id/members/:user_id/mute", api.GetMemberMuteStatusHandler) authGroup.GET("/groups/:room_id/members/:user_id/mute", api.GetMemberMuteStatusHandler)
authGroup.GET("/groups/:room_id/settings", api.GetGroupSettingsHandler)
authGroup.POST("/groups/:room_id/settings", api.UpdateGroupSettingsHandler)
authGroup.GET("/group-notifications", api.GetGroupNotificationsHandler) authGroup.GET("/group-notifications", api.GetGroupNotificationsHandler)
authGroup.GET("/calls/history", api.GetCallHistoryHandler)
// 附件管理 // 附件管理
authGroup.POST("/attachments/upload", api.UploadAttachmentHandler) authGroup.POST("/attachments/upload", api.UploadAttachmentHandler)

1
go.mod
View File

@@ -44,6 +44,7 @@ require (
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20260630140118-a99989343ebd // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect

2
go.sum
View File

@@ -71,6 +71,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20260630140118-a99989343ebd h1:KWmIx5ok5cv0xfiMn6PajeITT/P4NcMV8FdjglAp0s8=
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20260630140118-a99989343ebd/go.mod h1:sj5LMpsqB4IWdwIrcmmBJM6m+rW/uOQLSGUPhKkqdh8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=

View File

@@ -25,7 +25,7 @@ func LoginHandler(c *gin.Context) {
} }
// 获取客户端IP // 获取客户端IP
ip := getClientIP(c) ip := utils.GetClientIP(c)
// 调用认证服务登录 // 调用认证服务登录
user, err := service.AuthSvc.Login(req.Account, req.Password) user, err := service.AuthSvc.Login(req.Account, req.Password)
@@ -57,24 +57,6 @@ func LoginHandler(c *gin.Context) {
}, "登录成功") }, "登录成功")
} }
// getClientIP 获取客户端IP
func getClientIP(c *gin.Context) string {
// 优先从X-Forwarded-For获取
ip := c.GetHeader("X-Forwarded-For")
if ip != "" {
return ip
}
// 从X-Real-IP获取
ip = c.GetHeader("X-Real-IP")
if ip != "" {
return ip
}
// 从RemoteAddr获取
return c.ClientIP()
}
/** /**
* RegisterHandler * RegisterHandler
* 功能:用户注册 * 功能:用户注册
@@ -222,4 +204,3 @@ func SendSmsCodeHandler(c *gin.Context) {
"code": code, // 仅开发环境,生产环境应移除 "code": code, // 仅开发环境,生产环境应移除
}, "验证码已发送") }, "验证码已发送")
} }

View File

@@ -310,6 +310,7 @@ func UpdateContactHandler(c *gin.Context) {
GroupID uint `json:"group_id"` GroupID uint `json:"group_id"`
IsTop *bool `json:"is_top"` IsTop *bool `json:"is_top"`
IsMuted *bool `json:"is_muted"` IsMuted *bool `json:"is_muted"`
IsBlocked *bool `json:"is_blocked"`
} }
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
@@ -330,6 +331,9 @@ func UpdateContactHandler(c *gin.Context) {
if req.IsMuted != nil { if req.IsMuted != nil {
updates["is_muted"] = *req.IsMuted updates["is_muted"] = *req.IsMuted
} }
if req.IsBlocked != nil {
updates["is_blocked"] = *req.IsBlocked
}
if err := service.ContactSvc.UpdateContact(userID.(string), contactID, updates); err != nil { if err := service.ContactSvc.UpdateContact(userID.(string), contactID, updates); err != nil {
utils.BadRequest(c, "更新失败: "+err.Error()) utils.BadRequest(c, "更新失败: "+err.Error())

View File

@@ -56,14 +56,19 @@ func SendHandler(c *gin.Context) {
// 4. 构造临时的发送者客户端对象 // 4. 构造临时的发送者客户端对象
// 这个对象将传递给 Service 层,用于识别消息来源 // 这个对象将传递给 Service 层,用于识别消息来源
mockClient := &manager.Client{ mockClient := &manager.Client{
UserID: senderID, UserID: senderID,
ID: clientID, ID: clientID,
RemoteIP: utils.GetClientIP(c),
} }
// 5. 调用核心业务逻辑处理消息 // 5. 调用核心业务逻辑处理消息,返回持久化后的消息体供前端对齐 ID
service.ChatSvc.HandleUserMessage(mockClient, &req) savedMsg := service.ChatSvc.HandleUserMessage(mockClient, &req)
// 6. 返回成功响应 // 6. 返回成功响应(含服务端消息 ID避免前端重复展示
if savedMsg != nil {
utils.SuccessWithData(c, savedMsg, "消息已发送")
return
}
utils.Success(c, "消息已发送") utils.Success(c, "消息已发送")
} }
@@ -169,6 +174,54 @@ func HistoryHandler(c *gin.Context) {
}, "获取成功") }, "获取成功")
} }
/**
* RecallMessageHandler
* 功能撤回消息2 分钟内,仅发送者可撤回)
* 路径POST /api/messages/recall
*/
func RecallMessageHandler(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
utils.Unauthorized(c, "未认证")
return
}
var req struct {
MessageID uint `json:"message_id" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
utils.BadRequest(c, "参数错误")
return
}
var msg model.ChatMessage
if err := service.ChatSvc.DB.First(&msg, req.MessageID).Error; err != nil {
utils.NotFound(c, "消息不存在")
return
}
if msg.SenderUserID != userID.(string) {
utils.Forbidden(c, "无权撤回该消息")
return
}
if time.Since(msg.CreatedAt) > 2*time.Minute {
utils.BadRequest(c, "超过2分钟无法撤回")
return
}
msg.MessageType = model.MessageTypeSystem
msg.Content = "撤回了一条消息"
if err := service.ChatSvc.DB.Save(&msg).Error; err != nil {
utils.InternalError(c, "撤回失败")
return
}
service.ChatSvc.BroadcastChatMessage(msg)
utils.SuccessWithData(c, msg, "撤回成功")
}
/** /**
* SyncMessagesHandler * SyncMessagesHandler
* 功能:全量同步消息 (V1 兼容)。 * 功能:全量同步消息 (V1 兼容)。
@@ -178,7 +231,6 @@ func SyncMessagesHandler(c *gin.Context) {
HistoryHandler(c) HistoryHandler(c)
} }
// ========================================== // ==========================================
// 系统与 WebRTC 接口 // 系统与 WebRTC 接口
// ========================================== // ==========================================

View File

@@ -934,3 +934,93 @@ func GetMemberMuteStatusHandler(c *gin.Context) {
"muted_until": mutedUntil, "muted_until": mutedUntil,
}, "获取成功") }, "获取成功")
} }
/**
* GetGroupSettingsHandler
* 功能:获取群设置(名称、头像、公告、邀请权限等)
* 路径GET /api/groups/:room_id/settings
*/
func GetGroupSettingsHandler(c *gin.Context) {
roomID := c.Param("room_id")
room, err := service.RoomSvc.GetRoom(roomID)
if err != nil {
utils.NotFound(c, "群聊不存在")
return
}
utils.SuccessWithData(c, gin.H{
"name": room.RoomName,
"avatar": room.RoomAvatar,
"description": room.Announcement,
"invite_permission": 0,
"created_at": room.CreatedAt,
"updated_at": room.UpdatedAt,
}, "获取成功")
}
/**
* UpdateGroupSettingsHandler
* 功能:更新群设置
* 路径POST /api/groups/:room_id/settings
*/
func UpdateGroupSettingsHandler(c *gin.Context) {
userID, _ := c.Get("user_id")
roomID := c.Param("room_id")
ok, err := service.RoomSvc.IsGroupAdminOrOwner(roomID, userID.(string))
if err != nil || !ok {
utils.Forbidden(c, "无权限修改群设置")
return
}
var req struct {
Name string `json:"name"`
Avatar string `json:"avatar"`
Description string `json:"description"`
InvitePermission *int `json:"invite_permission"`
}
if err := c.ShouldBindJSON(&req); err != nil {
utils.BadRequest(c, "参数错误")
return
}
updates := map[string]interface{}{}
if req.Name != "" {
updates["room_name"] = req.Name
}
if req.Avatar != "" {
updates["room_avatar"] = req.Avatar
}
if req.Description != "" {
updates["announcement"] = req.Description
}
if len(updates) == 0 {
utils.BadRequest(c, "没有可更新的字段")
return
}
if err := service.RoomSvc.DB.Model(&model.ChatRoom{}).Where("room_id = ?", roomID).Updates(updates).Error; err != nil {
utils.InternalError(c, "更新失败")
return
}
GetGroupSettingsHandler(c)
}
/**
* GetCallHistoryHandler
* 功能:获取当前用户的通话记录(基于 call_id 消息)
* 路径GET /api/calls/history
*/
func GetCallHistoryHandler(c *gin.Context) {
userID, _ := c.Get("user_id")
var messages []model.ChatMessage
err := service.ChatSvc.DB.
Where("(sender_user_id = ? OR receiver_user_id = ?) AND call_id <> '' AND call_id IS NOT NULL", userID, userID).
Order("created_at DESC").
Limit(50).
Find(&messages).Error
if err != nil {
utils.InternalError(c, "查询失败")
return
}
utils.SuccessWithData(c, messages, "获取成功")
}

View File

@@ -0,0 +1,126 @@
/**
* package api
* 作用:用户设置与消息已读回执 HTTP 接口
*/
package api
import (
"encoding/json"
"time"
"xk-websocket-v2/internal/model"
"xk-websocket-v2/internal/service"
"xk-websocket-v2/internal/utils"
"github.com/gin-gonic/gin"
)
/**
* GetUserSettingsHandler
* 功能:获取当前用户的全部设置
*/
func GetUserSettingsHandler(c *gin.Context) {
userID, ok := c.Get("user_id")
if !ok {
utils.Unauthorized(c, "未认证")
return
}
var rows []model.UserSetting
if err := service.ChatSvc.DB.Where("user_id = ?", userID.(string)).Find(&rows).Error; err != nil {
utils.InternalError(c, "查询失败")
return
}
result := make(map[string]string, len(rows))
for _, row := range rows {
result[row.SettingKey] = row.SettingValue
}
utils.SuccessWithData(c, result, "获取成功")
}
/**
* UpdateUserSettingsHandler
* 功能:批量更新用户设置
*/
func UpdateUserSettingsHandler(c *gin.Context) {
userID, ok := c.Get("user_id")
if !ok {
utils.Unauthorized(c, "未认证")
return
}
var req struct {
Settings map[string]string `json:"settings" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
utils.BadRequest(c, "参数错误")
return
}
uid := userID.(string)
now := time.Now()
for key, val := range req.Settings {
setting := model.UserSetting{
UserID: uid,
SettingKey: key,
SettingValue: val,
UpdatedAt: now,
}
service.ChatSvc.DB.Save(&setting)
}
GetUserSettingsHandler(c)
}
/**
* MarkMessagesReadHandler
* 功能:批量标记消息已读
*/
func MarkMessagesReadHandler(c *gin.Context) {
userID, ok := c.Get("user_id")
if !ok {
utils.Unauthorized(c, "未认证")
return
}
var req struct {
MessageIDs []uint `json:"message_ids" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
utils.BadRequest(c, "参数错误")
return
}
uid := userID.(string)
now := time.Now()
readIDs := make([]uint, 0, len(req.MessageIDs))
for _, mid := range req.MessageIDs {
receipt := model.MessageReadReceipt{
MessageID: mid,
UserID: uid,
ReadAt: now,
}
service.ChatSvc.DB.Where("message_id = ? AND user_id = ?", mid, uid).
Assign(receipt).FirstOrCreate(&receipt)
readIDs = append(readIDs, mid)
// 通知消息发送者(已读回执)
var msg model.ChatMessage
if err := service.ChatSvc.DB.First(&msg, mid).Error; err == nil && msg.SenderUserID != "" && msg.SenderUserID != uid {
payload := model.WsPayload{
RequestType: "messages_read",
Data: map[string]interface{}{
"message_ids": []uint{mid},
"reader_id": uid,
"room_id": msg.RoomID,
},
}
if bytes, err := json.Marshal(payload); err == nil {
service.ChatSvc.DispatchMessage(msg.SenderUserID, bytes)
}
}
}
utils.SuccessWithData(c, gin.H{"message_ids": readIDs}, "已标记已读")
}

View File

@@ -40,6 +40,27 @@ func GetMyInfoHandler(c *gin.Context) {
utils.SuccessWithData(c, user, "获取成功") utils.SuccessWithData(c, user, "获取成功")
} }
/**
* GetUserByIDHandler
* 功能:根据用户 ID 获取公开资料
* 路径GET /api/user/:id
*/
func GetUserByIDHandler(c *gin.Context) {
targetID := c.Param("id")
if targetID == "" {
utils.BadRequest(c, "用户ID不能为空")
return
}
user, err := service.UserSvc.GetUserByID(targetID)
if err != nil {
utils.NotFound(c, "用户不存在")
return
}
user.Password = ""
utils.SuccessWithData(c, user, "获取成功")
}
/** /**
* GetUserListHandler * GetUserListHandler
* 功能:获取用户列表 * 功能:获取用户列表

View File

@@ -20,6 +20,7 @@ import (
type Client struct { type Client struct {
ID string // 客户端唯一标识 (格式: NodeID-Timestamp) ID string // 客户端唯一标识 (格式: NodeID-Timestamp)
UserID string // 绑定的用户ID (未绑定时为空) UserID string // 绑定的用户ID (未绑定时为空)
RemoteIP string // WebSocket 连接来源 IP
Conn *websocket.Conn // 底层 WebSocket 连接对象 Conn *websocket.Conn // 底层 WebSocket 连接对象
SendQueue chan []byte // 发送缓冲队列 (防止网络阻塞导致写协程卡死) SendQueue chan []byte // 发送缓冲队列 (防止网络阻塞导致写协程卡死)
} }

View File

@@ -145,7 +145,7 @@ func (m *RequestLogMiddleware) Handler() gin.HandlerFunc {
} }
// 获取请求IP // 获取请求IP
ip := getClientIP(c) ip := utils.GetClientIP(c)
// 本地IP不记录 // 本地IP不记录
if utils.GetIPLocation(ip) == "本地" { if utils.GetIPLocation(ip) == "本地" {
@@ -253,28 +253,6 @@ func (m *RequestLogMiddleware) logRequest(c *gin.Context, ip, userID string, req
m.DB.Create(&log) m.DB.Create(&log)
} }
// getClientIP 获取客户端IP
func getClientIP(c *gin.Context) string {
// 优先从X-Forwarded-For获取
ip := c.GetHeader("X-Forwarded-For")
if ip != "" {
// X-Forwarded-For可能包含多个IP取第一个
ips := strings.Split(ip, ",")
if len(ips) > 0 {
return strings.TrimSpace(ips[0])
}
}
// 从X-Real-IP获取
ip = c.GetHeader("X-Real-IP")
if ip != "" {
return ip
}
// 从RemoteAddr获取
return c.ClientIP()
}
// responseWriter 响应写入器(用于捕获响应内容) // responseWriter 响应写入器(用于捕获响应内容)
type responseWriter struct { type responseWriter struct {
gin.ResponseWriter gin.ResponseWriter

View File

@@ -23,6 +23,10 @@ type ChatMessage struct {
RoomID string `gorm:"type:varchar(100);index;comment:房间ID" json:"room_id"` RoomID string `gorm:"type:varchar(100);index;comment:房间ID" json:"room_id"`
// 发送者用户ID // 发送者用户ID
SenderUserID string `gorm:"type:varchar(100);index;comment:发送者用户ID" json:"sender_user_id"` SenderUserID string `gorm:"type:varchar(100);index;comment:发送者用户ID" json:"sender_user_id"`
// 发送者 IP
SenderIP string `gorm:"type:varchar(50);comment:发送者IP" json:"sender_ip"`
// IP 归属地
IPLocation string `gorm:"type:varchar(255);comment:IP归属地" json:"ip_location"`
// 接收者用户ID // 接收者用户ID
ReceiverUserID string `gorm:"type:varchar(100);index;comment:接收者用户ID" json:"receiver_user_id"` ReceiverUserID string `gorm:"type:varchar(100);index;comment:接收者用户ID" json:"receiver_user_id"`
// 消息类型 ( // 0:文本 1:图片 2:语音 3:视频 8:文件 6:信令) // 消息类型 ( // 0:文本 1:图片 2:语音 3:视频 8:文件 6:信令)
@@ -798,6 +802,36 @@ const (
MomentMediaTypeVideo = 2 // 视频 MomentMediaTypeVideo = 2 // 视频
) )
// ==========================================
// 已读回执与用户设置
// ==========================================
/**
* MessageReadReceipt
* 作用:消息已读回执表
*/
type MessageReadReceipt struct {
ID uint `json:"id" gorm:"primaryKey"`
MessageID uint `json:"message_id" gorm:"index;not null;uniqueIndex:uk_msg_user,priority:1"`
UserID string `json:"user_id" gorm:"size:100;index;not null;uniqueIndex:uk_msg_user,priority:2"`
ReadAt time.Time `json:"read_at" gorm:"not null"`
}
func (MessageReadReceipt) TableName() string { return "message_read_receipts" }
/**
* UserSetting
* 作用:用户个性化设置键值表
*/
type UserSetting struct {
UserID string `json:"user_id" gorm:"size:100;primaryKey"`
SettingKey string `json:"setting_key" gorm:"size:50;primaryKey"`
SettingValue string `json:"setting_value" gorm:"type:text"`
UpdatedAt time.Time `json:"updated_at"`
}
func (UserSetting) TableName() string { return "user_settings" }
// ========================================== // ==========================================
// 常量定义 // 常量定义
// ========================================== // ==========================================

View File

@@ -19,6 +19,7 @@ import (
"xk-websocket-v2/internal/manager" "xk-websocket-v2/internal/manager"
"xk-websocket-v2/internal/model" "xk-websocket-v2/internal/model"
"xk-websocket-v2/internal/turnserver" "xk-websocket-v2/internal/turnserver"
"xk-websocket-v2/internal/utils"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
"github.com/spf13/viper" "github.com/spf13/viper"
@@ -92,8 +93,9 @@ func (s *ChatService) IsUserOnline(userID string) bool {
* 功能处理用户发来的消息入口函数。包含多端同步、URL 抓取、持久化和转发逻辑。 * 功能处理用户发来的消息入口函数。包含多端同步、URL 抓取、持久化和转发逻辑。
* @param senderClient 发送消息的客户端(用于识别来源设备) * @param senderClient 发送消息的客户端(用于识别来源设备)
* @param req 消息请求体 * @param req 消息请求体
* @returns 持久化后的消息(信令/群通知/被拦截时返回 nil
*/ */
func (s *ChatService) HandleUserMessage(senderClient *manager.Client, req *model.SendMessageReq) { func (s *ChatService) HandleUserMessage(senderClient *manager.Client, req *model.SendMessageReq) *model.ChatMessage {
// 1. [关键] 拦截通话信令:处理多端同步逻辑 // 1. [关键] 拦截通话信令:处理多端同步逻辑
if req.CallStatus == "accepted" { if req.CallStatus == "accepted" {
s.NotifyOtherDevices(senderClient.UserID, senderClient.ID, req.CallID) s.NotifyOtherDevices(senderClient.UserID, senderClient.ID, req.CallID)
@@ -128,7 +130,7 @@ func (s *ChatService) HandleUserMessage(senderClient *manager.Client, req *model
// 验证群成员身份 // 验证群成员身份
memberIDs, mErr := RoomSvc.GetRoomMembers(req.RoomID) memberIDs, mErr := RoomSvc.GetRoomMembers(req.RoomID)
if mErr != nil { if mErr != nil {
return return nil
} }
isMember := false isMember := false
@@ -150,7 +152,7 @@ func (s *ChatService) HandleUserMessage(senderClient *manager.Client, req *model
} }
errorBytes, _ := json.Marshal(errorMsg) errorBytes, _ := json.Marshal(errorMsg)
s.DispatchMessage(senderClient.UserID, errorBytes) s.DispatchMessage(senderClient.UserID, errorBytes)
return return nil
} }
} }
} }
@@ -160,9 +162,27 @@ func (s *ChatService) HandleUserMessage(senderClient *manager.Client, req *model
receiverUserID = req.RoomID receiverUserID = req.RoomID
} }
// 私聊:接收方已拉黑发送方则拒绝
if !isGroupMessage && req.ReceiverUserID != "" && ContactSvc != nil {
if ContactSvc.IsBlocked(req.ReceiverUserID, senderClient.UserID) {
errorMsg := model.WsPayload{
RequestType: "error",
Data: map[string]interface{}{
"message": "消息已发出,但被对方拒收",
"code": "BLOCKED",
},
}
errorBytes, _ := json.Marshal(errorMsg)
s.DispatchMessage(senderClient.UserID, errorBytes)
return nil
}
}
msg := model.ChatMessage{ msg := model.ChatMessage{
RoomID: req.RoomID, RoomID: req.RoomID,
SenderUserID: senderClient.UserID, SenderUserID: senderClient.UserID,
SenderIP: senderClient.RemoteIP,
IPLocation: utils.GetIPLocation(senderClient.RemoteIP),
ReceiverUserID: receiverUserID, ReceiverUserID: receiverUserID,
MessageType: req.MessageType, MessageType: req.MessageType,
Content: req.Content, Content: req.Content,
@@ -225,11 +245,12 @@ func (s *ChatService) HandleUserMessage(senderClient *manager.Client, req *model
// 处理@通知 // 处理@通知
s.handleMentionNotification(senderClient.UserID, req.RoomID, extraData, msg) s.handleMentionNotification(senderClient.UserID, req.RoomID, extraData, msg)
return return &msg
} }
} }
} }
s.DispatchMessage(req.ReceiverUserID, msgBytes) s.DispatchMessage(req.ReceiverUserID, msgBytes)
return &msg
} else { } else {
// ========================================== // ==========================================
@@ -275,6 +296,7 @@ func (s *ChatService) HandleUserMessage(senderClient *manager.Client, req *model
log.Printf("📡 [WebRTC] 定向信令: To=%s Action=%s", req.ReceiverUserID, req.CallStatus) log.Printf("📡 [WebRTC] 定向信令: To=%s Action=%s", req.ReceiverUserID, req.CallStatus)
} }
} }
return nil
} }
// 辅助:正则提取第一个 URL // 辅助:正则提取第一个 URL
@@ -381,6 +403,39 @@ func (s *ChatService) NotifyOtherDevices(userID, currentClientID, callID string)
} }
} }
/**
* BroadcastChatMessage
* 功能:将聊天消息推送给房间成员或私聊对方(含发送者其他设备)
*/
func (s *ChatService) BroadcastChatMessage(msg model.ChatMessage) {
pushMsg := model.WsPayload{
RequestType: "receive_message",
Data: msg,
}
msgBytes, _ := json.Marshal(pushMsg)
if msg.RoomID != "" {
room, err := RoomSvc.GetRoom(msg.RoomID)
if err == nil && room.RoomType == "group" {
memberIDs, mErr := RoomSvc.GetRoomMembers(msg.RoomID)
if mErr == nil {
for _, uid := range memberIDs {
if uid != msg.SenderUserID {
s.DispatchMessage(uid, msgBytes)
}
}
s.DispatchMessage(msg.SenderUserID, msgBytes)
return
}
}
}
if msg.ReceiverUserID != "" {
s.DispatchMessage(msg.ReceiverUserID, msgBytes)
}
s.DispatchMessage(msg.SenderUserID, msgBytes)
}
/** /**
* DispatchMessage * DispatchMessage
* 功能:将消息路由到目标用户(无论其在哪个节点)。 * 功能:将消息路由到目标用户(无论其在哪个节点)。

View File

@@ -524,8 +524,54 @@ func (s *ContactService) GetUserDetailWithFriendStatus(currentUserID, targetUser
} }
return map[string]interface{}{ return map[string]interface{}{
"is_friend": isFriend, "is_friend": isFriend,
"contact": contact, "contact": contact,
"user": user, "user": user,
"common_groups": s.GetCommonGroups(currentUserID, targetUserID),
}, nil }, nil
} }
/**
* GetCommonGroups
* 功能:获取两人共同加入的群聊列表
*/
func (s *ContactService) GetCommonGroups(userID, contactID string) []model.ChatRoom {
var myRooms []model.RoomMember
if err := s.DB.Where("user_id = ?", userID).Find(&myRooms).Error; err != nil {
return nil
}
roomSet := make(map[string]bool, len(myRooms))
for _, m := range myRooms {
roomSet[m.RoomID] = true
}
var theirRooms []model.RoomMember
if err := s.DB.Where("user_id = ?", contactID).Find(&theirRooms).Error; err != nil {
return nil
}
commonIDs := make([]string, 0)
for _, m := range theirRooms {
if roomSet[m.RoomID] {
commonIDs = append(commonIDs, m.RoomID)
}
}
if len(commonIDs) == 0 {
return []model.ChatRoom{}
}
var rooms []model.ChatRoom
_ = s.DB.Where("room_id IN ? AND room_type = ?", commonIDs, "group").Find(&rooms).Error
return rooms
}
/**
* IsBlocked
* 功能:检查 owner 是否拉黑了 targetowner 对 target 设置了 is_blocked
*/
func (s *ContactService) IsBlocked(ownerID, targetID string) bool {
var contact model.UserContact
err := s.DB.Where("user_id = ? AND contact_id = ? AND is_blocked = ?", ownerID, targetID, true).
First(&contact).Error
return err == nil
}

View File

@@ -0,0 +1,28 @@
/**
* package utils
* 作用:从 HTTP 请求中提取客户端真实 IP
*/
package utils
import (
"strings"
"github.com/gin-gonic/gin"
)
/**
* GetClientIP
* 功能:优先从代理头获取,否则使用 Gin ClientIP
*/
func GetClientIP(c *gin.Context) string {
ip := c.GetHeader("X-Forwarded-For")
if ip != "" {
parts := strings.Split(ip, ",")
return strings.TrimSpace(parts[0])
}
ip = c.GetHeader("X-Real-IP")
if ip != "" {
return ip
}
return c.ClientIP()
}

View File

@@ -1,163 +1,102 @@
/** /**
* package utils * package utils
* 作用IP归属地查询工具 * 作用IP 归属地查询(基于本地 ip2region.xdb 离线库)
* 说明使用第三方API查询IP归属地信息
*/ */
package utils package utils
import ( import (
"encoding/json" "log"
"fmt"
"io"
"net/http"
"strings" "strings"
"time" "sync"
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
) )
// IPLocationInfo IP归属地信息
type IPLocationInfo struct {
Country string `json:"country"` // 国家
Region string `json:"region"` // 省份
City string `json:"city"` // 城市
ISP string `json:"isp"` // 运营商
FullLocation string `json:"full_location"` // 完整归属地
}
// 缓存结构(简单内存缓存)
var ( var (
ipCache = make(map[string]*IPLocationInfo) ipSearcher *xdb.Searcher
cacheTimeout = 24 * time.Hour ipSearcherOnce sync.Once
ipSearcherErr error
ipCache sync.Map
) )
/**
* InitIP2Region
* 功能:加载 ip2region.xdb服务启动时调用一次
*/
func InitIP2Region(dbPath string) error {
ipSearcherOnce.Do(func() {
ipSearcher, ipSearcherErr = xdb.NewWithFileOnly(xdb.IPv4, dbPath)
if ipSearcherErr != nil {
log.Printf("❌ [IP2Region] 加载失败: %v", ipSearcherErr)
return
}
log.Printf("✅ [IP2Region] 已加载: %s", dbPath)
})
return ipSearcherErr
}
/** /**
* GetIPLocation * GetIPLocation
* 功能:获取IP归属地信息 * 功能:查询 IP 归属地,内网/本地返回「本地」
* @param ip IP地址
* @returns 归属地信息字符串
*/ */
func GetIPLocation(ip string) string { func GetIPLocation(ip string) string {
// 排除本地IP
if isLocalIP(ip) { if isLocalIP(ip) {
return "本地" return "本地"
} }
if ip == "" {
// 检查缓存 return "未知"
if info, ok := ipCache[ip]; ok {
return info.FullLocation
} }
// 查询IP归属地 if cached, ok := ipCache.Load(ip); ok {
info := queryIPLocation(ip) return cached.(string)
if info != nil {
// 构建完整归属地字符串
parts := []string{}
if info.Country != "" {
parts = append(parts, info.Country)
}
if info.Region != "" {
parts = append(parts, info.Region)
}
if info.City != "" {
parts = append(parts, info.City)
}
if info.ISP != "" {
parts = append(parts, info.ISP)
}
if len(parts) > 0 {
info.FullLocation = strings.Join(parts, " ")
} else {
info.FullLocation = "未知"
}
// 存入缓存
ipCache[ip] = info
return info.FullLocation
} }
return "未知" if ipSearcher == nil {
return "未知"
}
region, err := ipSearcher.Search(ip)
if err != nil || region == "" {
return "未知"
}
// 格式: 国家|区域|省份|城市|ISP
parts := strings.Split(region, "|")
labels := []string{}
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" && p != "0" {
labels = append(labels, p)
}
}
result := "未知"
if len(labels) > 0 {
result = strings.Join(labels, " ")
}
ipCache.Store(ip, result)
return result
} }
/** /**
* isLocalIP * isLocalIP
* 功能判断是否为本地IP * 功能:判断是否为本地/内网 IP
*/ */
func isLocalIP(ip string) bool { func isLocalIP(ip string) bool {
// 本地回环地址
if ip == "127.0.0.1" || ip == "localhost" || ip == "::1" { if ip == "127.0.0.1" || ip == "localhost" || ip == "::1" {
return true return true
} }
if strings.HasPrefix(ip, "192.168.") || strings.HasPrefix(ip, "10.") {
// 内网地址 return true
if strings.HasPrefix(ip, "192.168.") || }
strings.HasPrefix(ip, "10.") || if strings.HasPrefix(ip, "172.16.") || strings.HasPrefix(ip, "172.17.") ||
strings.HasPrefix(ip, "172.16.") || strings.HasPrefix(ip, "172.18.") || strings.HasPrefix(ip, "172.19.") ||
strings.HasPrefix(ip, "172.17.") || strings.HasPrefix(ip, "172.20.") || strings.HasPrefix(ip, "172.21.") ||
strings.HasPrefix(ip, "172.18.") || strings.HasPrefix(ip, "172.22.") || strings.HasPrefix(ip, "172.23.") ||
strings.HasPrefix(ip, "172.19.") || strings.HasPrefix(ip, "172.24.") || strings.HasPrefix(ip, "172.25.") ||
strings.HasPrefix(ip, "172.20.") || strings.HasPrefix(ip, "172.26.") || strings.HasPrefix(ip, "172.27.") ||
strings.HasPrefix(ip, "172.21.") || strings.HasPrefix(ip, "172.28.") || strings.HasPrefix(ip, "172.29.") ||
strings.HasPrefix(ip, "172.22.") || strings.HasPrefix(ip, "172.30.") || strings.HasPrefix(ip, "172.31.") {
strings.HasPrefix(ip, "172.23.") ||
strings.HasPrefix(ip, "172.24.") ||
strings.HasPrefix(ip, "172.25.") ||
strings.HasPrefix(ip, "172.26.") ||
strings.HasPrefix(ip, "172.27.") ||
strings.HasPrefix(ip, "172.28.") ||
strings.HasPrefix(ip, "172.29.") ||
strings.HasPrefix(ip, "172.30.") ||
strings.HasPrefix(ip, "172.31.") {
return true return true
} }
return false return false
} }
/**
* queryIPLocation
* 功能查询IP归属地使用ip-api.com免费API
*/
func queryIPLocation(ip string) *IPLocationInfo {
// 使用ip-api.com免费API限制每分钟45次请求
url := fmt.Sprintf("http://ip-api.com/json/%s?lang=zh-CN&fields=status,message,country,regionName,city,isp", ip)
client := &http.Client{
Timeout: 3 * time.Second,
}
resp, err := client.Get(url)
if err != nil {
return nil
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil
}
var result struct {
Status string `json:"status"`
Message string `json:"message"`
Country string `json:"country"`
Region string `json:"regionName"`
City string `json:"city"`
ISP string `json:"isp"`
}
if err := json.Unmarshal(body, &result); err != nil {
return nil
}
if result.Status != "success" {
return nil
}
return &IPLocationInfo{
Country: result.Country,
Region: result.Region,
City: result.City,
ISP: result.ISP,
}
}

BIN
ip2region.xdb Normal file

Binary file not shown.

View File

@@ -1,17 +1,17 @@
/* /*
Navicat Premium Dump SQL Navicat Premium Dump SQL
Source Server : 新Mysql服务器 Source Server : 开发环境-本地
Source Server Type : MySQL Source Server Type : MySQL
Source Server Version : 80407 (8.4.7) Source Server Version : 80408 (8.4.8)
Source Host : 101.43.12.11:3306 Source Host : localhost:3306
Source Schema : nl_im_plus Source Schema : nl_im_plus
Target Server Type : MySQL Target Server Type : MySQL
Target Server Version : 80407 (8.4.7) Target Server Version : 80408 (8.4.8)
File Encoding : 65001 File Encoding : 65001
Date: 08/12/2025 14:10:48 Date: 07/07/2026 16:44:26
*/ */
SET NAMES utf8mb4; SET NAMES utf8mb4;
@@ -39,11 +39,7 @@ CREATE TABLE `api_request_logs` (
INDEX `idx_api_request_logs_user_id`(`user_id` ASC) USING BTREE, INDEX `idx_api_request_logs_user_id`(`user_id` ASC) USING BTREE,
INDEX `idx_api_request_logs_response_code`(`response_code` ASC) USING BTREE, INDEX `idx_api_request_logs_response_code`(`response_code` ASC) USING BTREE,
INDEX `idx_api_request_logs_request_time`(`request_time` ASC) USING BTREE INDEX `idx_api_request_logs_request_time`(`request_time` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '接口请求日志表' ROW_FORMAT = DYNAMIC; ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '接口请求日志表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of api_request_logs
-- ----------------------------
-- ---------------------------- -- ----------------------------
-- Table structure for attachments -- Table structure for attachments
@@ -65,18 +61,7 @@ CREATE TABLE `attachments` (
PRIMARY KEY (`id`) USING BTREE, PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_attachments_uploader_id`(`uploader_id` ASC) USING BTREE, INDEX `idx_attachments_uploader_id`(`uploader_id` ASC) USING BTREE,
INDEX `idx_attachments_file_type`(`file_type` ASC) USING BTREE INDEX `idx_attachments_file_type`(`file_type` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '附件表,记录上传的文件信息' ROW_FORMAT = DYNAMIC; ) ENGINE = InnoDB AUTO_INCREMENT = 17 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '附件表,记录上传的文件信息' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of attachments
-- ----------------------------
INSERT INTO `attachments` VALUES (1, '10001', 'app-logo.png', 'image', 50300, 'uploads\\images\\1765155994172139000_139000.png', '/uploads/images/1765155994172139000_139000.png', 'image/png', 0, 0, 0, '2025-12-08 09:06:34.394');
INSERT INTO `attachments` VALUES (2, '10002', 'acf44708-e9f2-45c3-8399-16237ff619b4.jpg', 'image', 80342, 'uploads\\images\\1765159407925763500_763500.jpg', '/uploads/images/1765159407925763500_763500.jpg', 'image/jpeg', 0, 0, 0, '2025-12-08 10:03:27.937');
INSERT INTO `attachments` VALUES (3, '10002', 'acf44708-e9f2-45c3-8399-16237ff619b4.png', 'image', 1106410, 'uploads\\images\\1765159407978118500_118500.png', '/uploads/images/1765159407978118500_118500.png', 'image/png', 0, 0, 0, '2025-12-08 10:03:27.991');
INSERT INTO `attachments` VALUES (4, '10002', 'app-logo.png', 'image', 50300, 'uploads\\images\\1765159408031331600_331600.png', '/uploads/images/1765159408031331600_331600.png', 'image/png', 0, 0, 0, '2025-12-08 10:03:28.045');
INSERT INTO `attachments` VALUES (5, '10002', 'qd.png', 'image', 83764, 'uploads\\images\\1765159408083277400_277400.png', '/uploads/images/1765159408083277400_277400.png', 'image/png', 0, 0, 0, '2025-12-08 10:03:28.094');
INSERT INTO `attachments` VALUES (6, '10002', 'weibo_login.mp4', 'video', 20268266, 'uploads\\videos\\1765159423732615800_615800.mp4', '/uploads/videos/1765159423732615800_615800.mp4', 'video/mp4', 0, 0, 0, '2025-12-08 10:03:43.753');
INSERT INTO `attachments` VALUES (7, '10001', 'app-logo.png', 'image', 50300, 'uploads\\images\\1765170659549540900_540900.png', '/uploads/images/1765170659549540900_540900.png', 'image/png', 0, 0, 0, '2025-12-08 13:10:59.734');
-- ---------------------------- -- ----------------------------
-- Table structure for chat_conversations -- Table structure for chat_conversations
@@ -101,15 +86,7 @@ CREATE TABLE `chat_conversations` (
INDEX `idx_chat_conversations_target_id`(`target_id` ASC) USING BTREE, INDEX `idx_chat_conversations_target_id`(`target_id` ASC) USING BTREE,
INDEX `idx_chat_conversations_room_id`(`room_id` ASC) USING BTREE, INDEX `idx_chat_conversations_room_id`(`room_id` ASC) USING BTREE,
INDEX `idx_chat_conversations_last_time`(`last_time` ASC) USING BTREE INDEX `idx_chat_conversations_last_time`(`last_time` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户会话表,记录每个用户的最近会话列表和未读信息' ROW_FORMAT = DYNAMIC; ) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户会话表,记录每个用户的最近会话列表和未读信息' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of chat_conversations
-- ----------------------------
INSERT INTO `chat_conversations` VALUES (1, '10002', '10001', '256224828233748480', 1, 0, 0, 0, 0, '我已经通过了你的好友申请,开始和我聊天吧~', '2025-12-08 09:06:00', '2025-12-08 09:05:59.742', '2025-12-08 11:30:28.569');
INSERT INTO `chat_conversations` VALUES (2, '10001', '10002', '256224828233748480', 1, 0, 0, 0, 0, '我已经通过了你的好友申请,开始和我聊天吧~', '2025-12-08 09:06:00', '2025-12-08 09:05:59.813', '2025-12-08 09:06:00.018');
INSERT INTO `chat_conversations` VALUES (3, '10001', 'group_256244427461365760', 'group_256244427461365760', 2, 0, 0, 0, 0, '李四222', '2025-12-08 13:54:45', '2025-12-08 10:23:52.545', '2025-12-08 13:54:47.473');
INSERT INTO `chat_conversations` VALUES (4, '10002', 'group_256244427461365760', 'group_256244427461365760', 2, 0, 0, 0, 0, '李四222', '2025-12-08 13:54:45', '2025-12-08 10:23:52.545', '2025-12-08 13:54:44.991');
-- ---------------------------- -- ----------------------------
-- Table structure for chat_messages -- Table structure for chat_messages
@@ -119,6 +96,8 @@ CREATE TABLE `chat_messages` (
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '消息唯一标识ID', `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '消息唯一标识ID',
`room_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '房间ID', `room_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '房间ID',
`sender_user_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '发送者用户ID', `sender_user_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '发送者用户ID',
`sender_ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '发送者IP',
`ip_location` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'IP归属地',
`receiver_user_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '接收者用户ID', `receiver_user_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '接收者用户ID',
`message_type` bigint NULL DEFAULT NULL COMMENT '消息类型', `message_type` bigint NULL DEFAULT NULL COMMENT '消息类型',
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '消息内容', `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '消息内容',
@@ -132,17 +111,7 @@ CREATE TABLE `chat_messages` (
INDEX `idx_chat_messages_sender_user_id`(`sender_user_id` ASC) USING BTREE, INDEX `idx_chat_messages_sender_user_id`(`sender_user_id` ASC) USING BTREE,
INDEX `idx_chat_messages_receiver_user_id`(`receiver_user_id` ASC) USING BTREE, INDEX `idx_chat_messages_receiver_user_id`(`receiver_user_id` ASC) USING BTREE,
INDEX `idx_chat_messages_call_id`(`call_id` ASC) USING BTREE INDEX `idx_chat_messages_call_id`(`call_id` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '聊天消息表,持久化存储聊天记录,包括文本、多媒体和信令状态' ROW_FORMAT = DYNAMIC; ) ENGINE = InnoDB AUTO_INCREMENT = 104 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '聊天消息表,持久化存储聊天记录,包括文本、多媒体和信令状态' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of chat_messages
-- ----------------------------
INSERT INTO `chat_messages` VALUES (1, '256224828233748480', '10001', '10002', 0, '我已经通过了你的好友申请,开始和我聊天吧~', '', 0, '', '', '2025-12-08 09:05:59.882');
INSERT INTO `chat_messages` VALUES (2, 'group_256244427461365760', '10001', 'group_256244427461365760', 4, '张三 邀请你加入了群聊', '{\"creator\":\"10001\",\"room_id\":\"group_256244427461365760\",\"type\":\"group_created\"}', 0, '', '', '2025-12-08 10:23:52.740');
INSERT INTO `chat_messages` VALUES (3, 'group_256244427461365760', '10001', 'group_256244427461365760', 0, '@李四 你好', '{\"mention_user_ids\":[\"10002\"]}', 0, '', '', '2025-12-08 11:27:32.014');
INSERT INTO `chat_messages` VALUES (4, 'group_256244427461365760', '10002', 'group_256244427461365760', 0, '.', '{}', 0, '', '', '2025-12-08 13:54:30.347');
INSERT INTO `chat_messages` VALUES (5, 'group_256244427461365760', '10002', 'group_256244427461365760', 0, '123', '{}', 0, '', '', '2025-12-08 13:54:35.594');
INSERT INTO `chat_messages` VALUES (6, 'group_256244427461365760', '10002', 'group_256244427461365760', 0, '222', '{}', 0, '', '', '2025-12-08 13:54:44.646');
-- ---------------------------- -- ----------------------------
-- Table structure for chat_rooms -- Table structure for chat_rooms
@@ -161,18 +130,13 @@ CREATE TABLE `chat_rooms` (
`announcement` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '群公告', `announcement` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '群公告',
`created_at` datetime(3) NULL DEFAULT NULL COMMENT '创建时间', `created_at` datetime(3) NULL DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime(3) NULL DEFAULT NULL COMMENT '更新时间', `updated_at` datetime(3) NULL DEFAULT NULL COMMENT '更新时间',
`members` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '成员列表JSON',
PRIMARY KEY (`room_id`) USING BTREE, PRIMARY KEY (`room_id`) USING BTREE,
INDEX `idx_chat_rooms_room_type`(`room_type` ASC) USING BTREE, INDEX `idx_chat_rooms_room_type`(`room_type` ASC) USING BTREE,
INDEX `idx_chat_rooms_owner_id`(`owner_id` ASC) USING BTREE, INDEX `idx_chat_rooms_owner_id`(`owner_id` ASC) USING BTREE,
INDEX `idx_chat_rooms_creator_id`(`creator_id` ASC) USING BTREE, INDEX `idx_chat_rooms_creator_id`(`creator_id` ASC) USING BTREE,
INDEX `idx_chat_rooms_last_message_time`(`last_message_time` ASC) USING BTREE INDEX `idx_chat_rooms_last_message_time`(`last_message_time` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '聊天房间表,支持点对点和群聊' ROW_FORMAT = DYNAMIC; ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '聊天房间表,支持点对点和群聊' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of chat_rooms
-- ----------------------------
INSERT INTO `chat_rooms` VALUES ('256224828233748480', 'p2p', '', '', '0', '', '10001', NULL, '', '', '2025-12-08 09:05:59.479', '2025-12-08 09:05:59.479');
INSERT INTO `chat_rooms` VALUES ('group_256244427461365760', 'group', '1', '1', '10001', '', '10001', NULL, '', '你好', '2025-12-08 10:23:52.329', '2025-12-08 13:54:08.971');
-- ---------------------------- -- ----------------------------
-- Table structure for contact_groups -- Table structure for contact_groups
@@ -186,11 +150,7 @@ CREATE TABLE `contact_groups` (
`created_at` datetime(3) NULL DEFAULT NULL COMMENT '创建时间', `created_at` datetime(3) NULL DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE, PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_contact_groups_user_id`(`user_id` ASC) USING BTREE INDEX `idx_contact_groups_user_id`(`user_id` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '联系人分组表' ROW_FORMAT = DYNAMIC; ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '联系人分组表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of contact_groups
-- ----------------------------
-- ---------------------------- -- ----------------------------
-- Table structure for friend_requests -- Table structure for friend_requests
@@ -208,12 +168,7 @@ CREATE TABLE `friend_requests` (
INDEX `idx_friend_requests_from_user_id`(`from_user_id` ASC) USING BTREE, INDEX `idx_friend_requests_from_user_id`(`from_user_id` ASC) USING BTREE,
INDEX `idx_friend_requests_to_user_id`(`to_user_id` ASC) USING BTREE, INDEX `idx_friend_requests_to_user_id`(`to_user_id` ASC) USING BTREE,
INDEX `idx_friend_requests_status`(`status` ASC) USING BTREE INDEX `idx_friend_requests_status`(`status` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '好友申请表' ROW_FORMAT = DYNAMIC; ) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '好友申请表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of friend_requests
-- ----------------------------
INSERT INTO `friend_requests` VALUES (1, '10002', '10001', '你好,我是张三', 'accepted', '2025-12-08 09:05:54.580', '2025-12-08 09:05:59.446');
-- ---------------------------- -- ----------------------------
-- Table structure for login_logs -- Table structure for login_logs
@@ -233,13 +188,7 @@ CREATE TABLE `login_logs` (
INDEX `idx_login_logs_ip`(`ip` ASC) USING BTREE, INDEX `idx_login_logs_ip`(`ip` ASC) USING BTREE,
INDEX `idx_login_logs_success`(`success` ASC) USING BTREE, INDEX `idx_login_logs_success`(`success` ASC) USING BTREE,
INDEX `idx_login_logs_login_time`(`login_time` ASC) USING BTREE INDEX `idx_login_logs_login_time`(`login_time` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '登录日志表' ROW_FORMAT = DYNAMIC; ) ENGINE = InnoDB AUTO_INCREMENT = 20 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '登录日志表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of login_logs
-- ----------------------------
INSERT INTO `login_logs` VALUES (1, 'user002@example.com', '10002', '本地', '::1', 1, '2025-12-08 13:10:40');
INSERT INTO `login_logs` VALUES (2, 'workerqi@163.com', '10001', '本地', '::1', 1, '2025-12-08 13:10:48');
-- ---------------------------- -- ----------------------------
-- Table structure for moment_comments -- Table structure for moment_comments
@@ -259,15 +208,7 @@ CREATE TABLE `moment_comments` (
INDEX `idx_moment_comments_user_id`(`user_id` ASC) USING BTREE, INDEX `idx_moment_comments_user_id`(`user_id` ASC) USING BTREE,
INDEX `idx_moment_comments_reply_to`(`reply_to_comment_id` ASC) USING BTREE, INDEX `idx_moment_comments_reply_to`(`reply_to_comment_id` ASC) USING BTREE,
INDEX `idx_moment_comments_reply_to_comment_id`(`reply_to_comment_id` ASC) USING BTREE INDEX `idx_moment_comments_reply_to_comment_id`(`reply_to_comment_id` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '动态评论表' ROW_FORMAT = DYNAMIC; ) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '动态评论表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of moment_comments
-- ----------------------------
INSERT INTO `moment_comments` VALUES (1, 1, '10002', NULL, '', '', 0, '2025-12-08 09:51:33.512');
INSERT INTO `moment_comments` VALUES (2, 2, '10001', NULL, '', '你好', 0, '2025-12-08 09:52:13.529');
INSERT INTO `moment_comments` VALUES (3, 5, '10001', NULL, '', '确实不错', 0, '2025-12-08 11:31:23.850');
INSERT INTO `moment_comments` VALUES (4, 2, '10002', 2, '10001', '嗯嗯', 0, '2025-12-08 12:53:31.946');
-- ---------------------------- -- ----------------------------
-- Table structure for moment_likes -- Table structure for moment_likes
@@ -282,18 +223,7 @@ CREATE TABLE `moment_likes` (
UNIQUE INDEX `uk_moment_user`(`moment_id` ASC, `user_id` ASC) USING BTREE, UNIQUE INDEX `uk_moment_user`(`moment_id` ASC, `user_id` ASC) USING BTREE,
INDEX `idx_moment_likes_moment_id`(`moment_id` ASC) USING BTREE, INDEX `idx_moment_likes_moment_id`(`moment_id` ASC) USING BTREE,
INDEX `idx_moment_likes_user_id`(`user_id` ASC) USING BTREE INDEX `idx_moment_likes_user_id`(`user_id` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '动态点赞表' ROW_FORMAT = DYNAMIC; ) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '动态点赞表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of moment_likes
-- ----------------------------
INSERT INTO `moment_likes` VALUES (1, 1, '10002', '2025-12-08 09:06:47.565');
INSERT INTO `moment_likes` VALUES (2, 2, '10001', '2025-12-08 09:52:07.061');
INSERT INTO `moment_likes` VALUES (3, 4, '10002', '2025-12-08 10:03:53.559');
INSERT INTO `moment_likes` VALUES (4, 4, '10001', '2025-12-08 10:04:06.564');
INSERT INTO `moment_likes` VALUES (5, 5, '10001', '2025-12-08 11:31:16.553');
INSERT INTO `moment_likes` VALUES (6, 2, '10002', '2025-12-08 13:10:29.289');
INSERT INTO `moment_likes` VALUES (7, 7, '10002', '2025-12-08 13:45:53.852');
-- ---------------------------- -- ----------------------------
-- Table structure for moment_notifications -- Table structure for moment_notifications
@@ -313,23 +243,7 @@ CREATE TABLE `moment_notifications` (
INDEX `idx_moment_notifications_is_read`(`is_read` ASC) USING BTREE, INDEX `idx_moment_notifications_is_read`(`is_read` ASC) USING BTREE,
INDEX `idx_moment_notifications_created_at`(`created_at` ASC) USING BTREE, INDEX `idx_moment_notifications_created_at`(`created_at` ASC) USING BTREE,
INDEX `idx_moment_notifications_moment_id`(`moment_id` ASC) USING BTREE INDEX `idx_moment_notifications_moment_id`(`moment_id` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 13 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '朋友圈通知表' ROW_FORMAT = DYNAMIC; ) ENGINE = InnoDB AUTO_INCREMENT = 23 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '朋友圈通知表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of moment_notifications
-- ----------------------------
INSERT INTO `moment_notifications` VALUES (1, '10001', '10002', 1, 1, NULL, 0, '2025-12-08 09:06:47.673');
INSERT INTO `moment_notifications` VALUES (2, '10001', '10002', 1, 2, 1, 0, '2025-12-08 09:51:33.620');
INSERT INTO `moment_notifications` VALUES (3, '10002', '10001', 2, 1, NULL, 1, '2025-12-08 09:52:07.164');
INSERT INTO `moment_notifications` VALUES (4, '10002', '10001', 2, 2, 2, 1, '2025-12-08 09:52:13.624');
INSERT INTO `moment_notifications` VALUES (5, '10002', '10001', 4, 1, NULL, 1, '2025-12-08 10:04:06.659');
INSERT INTO `moment_notifications` VALUES (6, '10001', '10002', 5, 4, NULL, 0, '2025-12-08 11:31:11.609');
INSERT INTO `moment_notifications` VALUES (7, '10002', '10001', 5, 1, NULL, 1, '2025-12-08 11:31:16.642');
INSERT INTO `moment_notifications` VALUES (8, '10002', '10001', 5, 2, 3, 1, '2025-12-08 11:31:23.936');
INSERT INTO `moment_notifications` VALUES (9, '10001', '10002', 2, 3, 4, 0, '2025-12-08 12:53:32.069');
INSERT INTO `moment_notifications` VALUES (10, '10002', '10001', 6, 4, NULL, 0, '2025-12-08 13:45:09.606');
INSERT INTO `moment_notifications` VALUES (11, '10002', '10001', 7, 4, NULL, 0, '2025-12-08 13:45:33.703');
INSERT INTO `moment_notifications` VALUES (12, '10001', '10002', 7, 1, NULL, 0, '2025-12-08 13:45:53.961');
-- ---------------------------- -- ----------------------------
-- Table structure for moments -- Table structure for moments
@@ -356,18 +270,7 @@ CREATE TABLE `moments` (
INDEX `idx_moments_created_at`(`created_at` ASC) USING BTREE, INDEX `idx_moments_created_at`(`created_at` ASC) USING BTREE,
INDEX `idx_moments_visibility`(`visibility` ASC) USING BTREE, INDEX `idx_moments_visibility`(`visibility` ASC) USING BTREE,
INDEX `idx_moments_is_deleted`(`is_deleted` ASC) USING BTREE INDEX `idx_moments_is_deleted`(`is_deleted` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '朋友圈动态表' ROW_FORMAT = DYNAMIC; ) ENGINE = InnoDB AUTO_INCREMENT = 13 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '朋友圈动态表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of moments
-- ----------------------------
INSERT INTO `moments` VALUES (1, '10001', '大家好,这个是我第一条动态~', 1, '[\"/uploads/images/1765155994172139000_139000.png\"]', '', 0, 'null', 'null', 'null', 1, 1, 0, '2025-12-08 09:06:34.699', '2025-12-08 09:06:34.699');
INSERT INTO `moments` VALUES (2, '10002', '这个是我第一个朋友圈~', 0, '[]', '', 0, 'null', 'null', 'null', 2, 2, 0, '2025-12-08 09:52:02.097', '2025-12-08 09:52:02.097');
INSERT INTO `moments` VALUES (3, '10002', '很开心', 1, '[\"/uploads/images/1765159407925763500_763500.jpg\",\"/uploads/images/1765159407978118500_118500.png\",\"/uploads/images/1765159408031331600_331600.png\",\"/uploads/images/1765159408083277400_277400.png\"]', '', 0, 'null', 'null', 'null', 0, 0, 0, '2025-12-08 10:03:28.142', '2025-12-08 10:03:28.142');
INSERT INTO `moments` VALUES (4, '10002', '哦豁', 2, '[\"/uploads/videos/1765159423732615800_615800.mp4\"]', '', 0, 'null', 'null', 'null', 2, 0, 0, '2025-12-08 10:03:43.810', '2025-12-08 10:03:43.810');
INSERT INTO `moments` VALUES (5, '10002', '今天天气真好 @张三 ', 0, '[]', '', 0, 'null', '[\"10001\"]', 'null', 1, 1, 0, '2025-12-08 11:31:11.549', '2025-12-08 11:31:11.549');
INSERT INTO `moments` VALUES (6, '10001', '... @李四 ', 0, '[]', '', 0, 'null', '[\"10002\"]', 'null', 0, 0, 0, '2025-12-08 13:45:09.525', '2025-12-08 13:45:09.525');
INSERT INTO `moments` VALUES (7, '10001', '@李四 ', 0, '[]', '', 0, 'null', '[\"10002\"]', 'null', 1, 0, 0, '2025-12-08 13:45:33.502', '2025-12-08 13:45:33.502');
-- ---------------------------- -- ----------------------------
-- Table structure for room_members -- Table structure for room_members
@@ -381,15 +284,7 @@ CREATE TABLE `room_members` (
`muted_until` datetime NULL DEFAULT NULL COMMENT '禁言到期时间', `muted_until` datetime NULL DEFAULT NULL COMMENT '禁言到期时间',
PRIMARY KEY (`room_id`, `user_id`) USING BTREE, PRIMARY KEY (`room_id`, `user_id`) USING BTREE,
INDEX `idx_room_members_user_id`(`user_id` ASC) USING BTREE INDEX `idx_room_members_user_id`(`user_id` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '房间成员表,存储房间与用户的关联关系' ROW_FORMAT = DYNAMIC; ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '房间成员表,存储房间与用户的关联关系' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of room_members
-- ----------------------------
INSERT INTO `room_members` VALUES ('256224828233748480', '10001', 0, '2025-12-08 09:05:59.562', NULL);
INSERT INTO `room_members` VALUES ('256224828233748480', '10002', 0, '2025-12-08 09:05:59.523', NULL);
INSERT INTO `room_members` VALUES ('group_256244427461365760', '10001', 2, '2025-12-08 10:23:52.375', NULL);
INSERT INTO `room_members` VALUES ('group_256244427461365760', '10002', 1, '2025-12-08 10:23:52.351', NULL);
-- ---------------------------- -- ----------------------------
-- Table structure for user_contacts -- Table structure for user_contacts
@@ -416,13 +311,7 @@ CREATE TABLE `user_contacts` (
INDEX `idx_user_contacts_contact_id`(`contact_id` ASC) USING BTREE, INDEX `idx_user_contacts_contact_id`(`contact_id` ASC) USING BTREE,
INDEX `idx_user_contacts_room_id`(`room_id` ASC) USING BTREE, INDEX `idx_user_contacts_room_id`(`room_id` ASC) USING BTREE,
INDEX `idx_user_contacts_group_id`(`group_id` ASC) USING BTREE INDEX `idx_user_contacts_group_id`(`group_id` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户联系人表,存储好友关系、分组、备注等信息' ROW_FORMAT = DYNAMIC; ) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户联系人表,存储好友关系、分组、备注等信息' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user_contacts
-- ----------------------------
INSERT INTO `user_contacts` VALUES (1, '10002', '10001', '256224828233748480', '', 0, 0, 0, 0, 0, NULL, '', 0, '2025-12-08 09:05:59.632', '2025-12-08 09:05:59.632');
INSERT INTO `user_contacts` VALUES (2, '10001', '10002', '256224828233748480', '', 0, 0, 0, 0, 0, NULL, '', 0, '2025-12-08 09:05:59.662', '2025-12-08 09:05:59.662');
-- ---------------------------- -- ----------------------------
-- Table structure for users -- Table structure for users
@@ -442,31 +331,7 @@ CREATE TABLE `users` (
PRIMARY KEY (`id`) USING BTREE, PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `idx_users_email`(`email` ASC) USING BTREE, UNIQUE INDEX `idx_users_email`(`email` ASC) USING BTREE,
UNIQUE INDEX `idx_users_phone`(`phone` ASC) USING BTREE UNIQUE INDEX `idx_users_phone`(`phone` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户基本信息表' ROW_FORMAT = DYNAMIC; ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户基本信息表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of users
-- ----------------------------
INSERT INTO `users` VALUES ('10001', 'workerqi@163.com', '13800138001', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '张三', '/uploads/images/1765170659549540900_540900.png', 'Golang 开发工程师', '北京', '2025-12-03 09:53:49.000', '2025-12-08 13:10:59.809');
INSERT INTO `users` VALUES ('10002', 'user002@example.com', '13800138002', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '李四', '', '前端开发工程师', '上海', '2025-12-03 09:53:49.000', '2025-12-03 09:53:49.000');
INSERT INTO `users` VALUES ('10003', 'user003@example.com', '13800138003', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '王五', '', '产品经理', '深圳', '2025-12-03 09:53:49.000', '2025-12-03 09:53:49.000');
INSERT INTO `users` VALUES ('10004', 'user004@example.com', '13800138004', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '赵六', '', 'UI设计师', '广州', '2025-12-03 09:53:49.000', '2025-12-03 09:53:49.000');
INSERT INTO `users` VALUES ('10005', 'user005@example.com', '13800138005', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '钱七', '', '测试工程师', '杭州', '2025-12-03 09:53:49.000', '2025-12-03 09:53:49.000');
INSERT INTO `users` VALUES ('10006', 'user006@example.com', '13800138006', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '孙八', '', '运维工程师', '成都', '2025-12-03 09:53:49.000', '2025-12-03 09:53:49.000');
INSERT INTO `users` VALUES ('10007', 'user007@example.com', '13800138007', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '周九', '', '数据分析师', '武汉', '2025-12-03 09:53:49.000', '2025-12-03 09:53:49.000');
INSERT INTO `users` VALUES ('10008', 'user008@example.com', '13800138008', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '吴十', '', '项目经理', '西安', '2025-12-03 09:53:49.000', '2025-12-03 09:53:49.000');
INSERT INTO `users` VALUES ('10009', 'user009@example.com', '13800138009', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '郑十一', '', '架构师', '南京', '2025-12-03 09:53:49.000', '2025-12-03 09:53:49.000');
INSERT INTO `users` VALUES ('10010', 'user010@example.com', '13800138010', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '王十二', '', '技术总监', '苏州', '2025-12-03 09:53:49.000', '2025-12-03 09:53:49.000');
INSERT INTO `users` VALUES ('10011', 'user011@example.com', '13800138011', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '冯十三', '', 'HR经理', '天津', '2025-12-03 09:53:49.000', '2025-12-03 09:53:49.000');
INSERT INTO `users` VALUES ('10012', 'user012@example.com', '13800138012', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '陈十四', '', '财务经理', '重庆', '2025-12-03 09:53:49.000', '2025-12-03 09:53:49.000');
INSERT INTO `users` VALUES ('10013', 'user013@example.com', '13800138013', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '褚十五', '', '市场经理', '长沙', '2025-12-03 09:53:49.000', '2025-12-03 09:53:49.000');
INSERT INTO `users` VALUES ('10014', 'user014@example.com', '13800138014', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '卫十六', '', '销售经理', '郑州', '2025-12-03 09:53:49.000', '2025-12-03 09:53:49.000');
INSERT INTO `users` VALUES ('10015', 'user015@example.com', '13800138015', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '蒋十七', '', '客服主管', '济南', '2025-12-03 09:53:49.000', '2025-12-03 09:53:49.000');
INSERT INTO `users` VALUES ('10016', 'user016@example.com', '13800138016', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '沈十八', '', '运营专员', '青岛', '2025-12-03 09:53:49.000', '2025-12-03 09:53:49.000');
INSERT INTO `users` VALUES ('10017', 'user017@example.com', '13800138017', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '韩十九', '', '内容编辑', '大连', '2025-12-03 09:53:49.000', '2025-12-03 09:53:49.000');
INSERT INTO `users` VALUES ('10018', 'user018@example.com', '13800138018', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '杨二十', '', '商务拓展', '厦门', '2025-12-03 09:53:49.000', '2025-12-03 09:53:49.000');
INSERT INTO `users` VALUES ('10019', 'user019@example.com', '13800138019', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '朱二一', '', '法务专员', '福州', '2025-12-03 09:53:49.000', '2025-12-03 09:53:49.000');
INSERT INTO `users` VALUES ('10020', 'user020@example.com', '13800138020', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '秦二二', '', '行政助理', '合肥', '2025-12-03 09:53:49.000', '2025-12-03 09:53:49.000');
-- ---------------------------- -- ----------------------------
-- Table structure for verification_codes -- Table structure for verification_codes
@@ -482,10 +347,32 @@ CREATE TABLE `verification_codes` (
PRIMARY KEY (`id`) USING BTREE, PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_verification_codes_target`(`target` ASC) USING BTREE, INDEX `idx_verification_codes_target`(`target` ASC) USING BTREE,
INDEX `idx_verification_codes_expires_at`(`expires_at` ASC) USING BTREE INDEX `idx_verification_codes_expires_at`(`expires_at` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '验证码表' ROW_FORMAT = DYNAMIC; ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '验证码表' ROW_FORMAT = Dynamic;
-- ---------------------------- -- ----------------------------
-- Records of verification_codes -- Table structure for message_read_receipts
-- ---------------------------- -- ----------------------------
DROP TABLE IF EXISTS `message_read_receipts`;
CREATE TABLE `message_read_receipts` (
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`message_id` bigint UNSIGNED NOT NULL COMMENT '消息ID',
`user_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '已读用户ID',
`read_at` datetime(3) NOT NULL COMMENT '已读时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uk_msg_user`(`message_id` ASC, `user_id` ASC) USING BTREE,
INDEX `idx_message_read_receipts_user_id`(`user_id` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '消息已读回执表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for user_settings
-- ----------------------------
DROP TABLE IF EXISTS `user_settings`;
CREATE TABLE `user_settings` (
`user_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户ID',
`setting_key` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '设置键',
`setting_value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '设置值',
`updated_at` datetime(3) NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`user_id`, `setting_key`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户个性化设置表' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1; SET FOREIGN_KEY_CHECKS = 1;

File diff suppressed because one or more lines are too long