diff --git a/cmd/server/main.go b/cmd/server/main.go index 12006d1..10d0125 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -101,6 +101,10 @@ func initDB() *gorm.DB { &model.Attachment{}, &model.ApiRequestLog{}, &model.LoginLog{}, + &model.Moment{}, + &model.MomentLike{}, + &model.MomentComment{}, + &model.MomentNotification{}, ) if err != nil { log.Fatalf("❌ 数据库迁移失败: %v", err) @@ -122,6 +126,10 @@ func initDB() *gorm.DB { {"attachments", "附件表,记录上传的文件信息"}, {"api_request_logs", "接口请求日志表"}, {"login_logs", "登录日志表"}, + {"moments", "朋友圈动态表"}, + {"moment_likes", "动态点赞表"}, + {"moment_comments", "动态评论表"}, + {"moment_notifications", "朋友圈通知表"}, } for _, tc := range tableComments { @@ -215,6 +223,7 @@ func main() { service.InitRoomService(db) // 房间服务(聊天房间管理) service.InitAttachmentService(db) // 附件服务(文件上传、管理) service.InitLoginLogService(db) // 登录日志服务(记录登录历史) + service.InitMomentService(db) // 朋友圈服务(动态、点赞、评论) // 步骤5: 启动TURN服务器(用于WebRTC音视频通话) go turnserver.Start() @@ -373,6 +382,9 @@ func main() { authGroup.GET("/attachments", api.GetAttachmentsHandler) authGroup.GET("/attachments/:id", api.GetAttachmentHandler) authGroup.POST("/attachments/delete/:id", api.DeleteAttachmentHandler) + + // 朋友圈管理 + api.RegisterMomentRoutes(authGroup) } } diff --git a/internal/api/moment_handler.go b/internal/api/moment_handler.go new file mode 100644 index 0000000..110d8b8 --- /dev/null +++ b/internal/api/moment_handler.go @@ -0,0 +1,483 @@ +/** + * package api + * 作用:处理朋友圈相关的 HTTP 请求接口 + */ +package api + +import ( + "strconv" + "xk-websocket-v2/internal/model" + "xk-websocket-v2/internal/service" + "xk-websocket-v2/internal/utils" + + "github.com/gin-gonic/gin" +) + +// ========================================== +// 动态相关接口 +// ========================================== + +/** + * CreateMomentHandler + * 功能:发布动态 + * 路径:POST /api/moments + */ +func CreateMomentHandler(c *gin.Context) { + userID := c.GetString("user_id") + if userID == "" { + utils.Unauthorized(c, "未登录") + return + } + + var req model.CreateMomentReq + if err := c.ShouldBindJSON(&req); err != nil { + utils.BadRequest(c, "参数错误") + return + } + + // 校验内容不能为空(除非有媒体) + if req.Content == "" && len(req.MediaURLs) == 0 { + utils.BadRequest(c, "内容不能为空") + return + } + + moment, err := service.MomentSvc.CreateMoment(userID, &req) + if err != nil { + utils.InternalError(c, err.Error()) + return + } + + utils.SuccessWithData(c, moment, "发布成功") +} + +/** + * DeleteMomentHandler + * 功能:删除动态 + * 路径:DELETE /api/moments/:id + */ +func DeleteMomentHandler(c *gin.Context) { + userID := c.GetString("user_id") + if userID == "" { + utils.Unauthorized(c, "未登录") + return + } + + momentID, err := strconv.ParseUint(c.Param("id"), 10, 64) + if err != nil { + utils.BadRequest(c, "无效的动态ID") + return + } + + if err := service.MomentSvc.DeleteMoment(userID, uint(momentID)); err != nil { + utils.BadRequest(c, err.Error()) + return + } + + utils.Success(c, "删除成功") +} + +/** + * GetMomentDetailHandler + * 功能:获取动态详情 + * 路径:GET /api/moments/:id + */ +func GetMomentDetailHandler(c *gin.Context) { + userID := c.GetString("user_id") + if userID == "" { + utils.Unauthorized(c, "未登录") + return + } + + momentID, err := strconv.ParseUint(c.Param("id"), 10, 64) + if err != nil { + utils.BadRequest(c, "无效的动态ID") + return + } + + moment, err := service.MomentSvc.GetMomentDetail(userID, uint(momentID)) + if err != nil { + utils.BadRequest(c, err.Error()) + return + } + + utils.SuccessWithData(c, moment, "获取成功") +} + +/** + * GetMomentsHandler + * 功能:获取好友动态列表 + * 路径:GET /api/moments + */ +func GetMomentsHandler(c *gin.Context) { + userID := c.GetString("user_id") + if userID == "" { + utils.Unauthorized(c, "未登录") + return + } + + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + + moments, total, err := service.MomentSvc.GetFriendsMoments(userID, page, pageSize) + if err != nil { + utils.InternalError(c, err.Error()) + return + } + + // 确保返回空数组而不是null + if moments == nil { + moments = []model.Moment{} + } + + utils.SuccessWithData(c, gin.H{ + "data": moments, + "total": total, + "page": page, + "size": pageSize, + }, "获取成功") +} + +/** + * GetUserMomentsHandler + * 功能:获取指定用户的动态列表 + * 路径:GET /api/moments/user/:user_id + */ +func GetUserMomentsHandler(c *gin.Context) { + viewerID := c.GetString("user_id") + if viewerID == "" { + utils.Unauthorized(c, "未登录") + return + } + + targetUserID := c.Param("user_id") + if targetUserID == "" { + utils.BadRequest(c, "用户ID不能为空") + return + } + + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + + moments, total, err := service.MomentSvc.GetUserMoments(viewerID, targetUserID, page, pageSize) + if err != nil { + utils.InternalError(c, err.Error()) + return + } + + if moments == nil { + moments = []model.Moment{} + } + + utils.SuccessWithData(c, gin.H{ + "data": moments, + "total": total, + "page": page, + "size": pageSize, + }, "获取成功") +} + +// ========================================== +// 点赞相关接口 +// ========================================== + +/** + * LikeMomentHandler + * 功能:点赞动态 + * 路径:POST /api/moments/:id/like + */ +func LikeMomentHandler(c *gin.Context) { + userID := c.GetString("user_id") + if userID == "" { + utils.Unauthorized(c, "未登录") + return + } + + momentID, err := strconv.ParseUint(c.Param("id"), 10, 64) + if err != nil { + utils.BadRequest(c, "无效的动态ID") + return + } + + if err := service.MomentSvc.LikeMoment(userID, uint(momentID)); err != nil { + utils.BadRequest(c, err.Error()) + return + } + + utils.Success(c, "点赞成功") +} + +/** + * UnlikeMomentHandler + * 功能:取消点赞 + * 路径:DELETE /api/moments/:id/like + */ +func UnlikeMomentHandler(c *gin.Context) { + userID := c.GetString("user_id") + if userID == "" { + utils.Unauthorized(c, "未登录") + return + } + + momentID, err := strconv.ParseUint(c.Param("id"), 10, 64) + if err != nil { + utils.BadRequest(c, "无效的动态ID") + return + } + + if err := service.MomentSvc.UnlikeMoment(userID, uint(momentID)); err != nil { + utils.BadRequest(c, err.Error()) + return + } + + utils.Success(c, "取消点赞成功") +} + +/** + * GetMomentLikesHandler + * 功能:获取动态的点赞列表 + * 路径:GET /api/moments/:id/likes + */ +func GetMomentLikesHandler(c *gin.Context) { + momentID, err := strconv.ParseUint(c.Param("id"), 10, 64) + if err != nil { + utils.BadRequest(c, "无效的动态ID") + return + } + + likes, err := service.MomentSvc.GetMomentLikes(uint(momentID)) + if err != nil { + utils.InternalError(c, err.Error()) + return + } + + if likes == nil { + likes = []model.MomentLike{} + } + + utils.SuccessWithData(c, likes, "获取成功") +} + +// ========================================== +// 评论相关接口 +// ========================================== + +/** + * CreateCommentHandler + * 功能:发表评论 + * 路径:POST /api/moments/:id/comments + */ +func CreateCommentHandler(c *gin.Context) { + userID := c.GetString("user_id") + if userID == "" { + utils.Unauthorized(c, "未登录") + return + } + + momentID, err := strconv.ParseUint(c.Param("id"), 10, 64) + if err != nil { + utils.BadRequest(c, "无效的动态ID") + return + } + + var req model.CreateCommentReq + if err := c.ShouldBindJSON(&req); err != nil { + utils.BadRequest(c, "参数错误") + return + } + + if req.Content == "" { + utils.BadRequest(c, "评论内容不能为空") + return + } + + comment, err := service.MomentSvc.CreateComment(userID, uint(momentID), &req) + if err != nil { + utils.BadRequest(c, err.Error()) + return + } + + utils.SuccessWithData(c, comment, "评论成功") +} + +/** + * DeleteCommentHandler + * 功能:删除评论 + * 路径:DELETE /api/moments/comments/:id + */ +func DeleteCommentHandler(c *gin.Context) { + userID := c.GetString("user_id") + if userID == "" { + utils.Unauthorized(c, "未登录") + return + } + + commentID, err := strconv.ParseUint(c.Param("id"), 10, 64) + if err != nil { + utils.BadRequest(c, "无效的评论ID") + return + } + + if err := service.MomentSvc.DeleteComment(userID, uint(commentID)); err != nil { + utils.BadRequest(c, err.Error()) + return + } + + utils.Success(c, "删除成功") +} + +/** + * GetMomentCommentsHandler + * 功能:获取动态的评论列表 + * 路径:GET /api/moments/:id/comments + */ +func GetMomentCommentsHandler(c *gin.Context) { + momentID, err := strconv.ParseUint(c.Param("id"), 10, 64) + if err != nil { + utils.BadRequest(c, "无效的动态ID") + return + } + + comments, err := service.MomentSvc.GetMomentComments(uint(momentID)) + if err != nil { + utils.InternalError(c, err.Error()) + return + } + + if comments == nil { + comments = []model.MomentComment{} + } + + utils.SuccessWithData(c, comments, "获取成功") +} + +// ========================================== +// 通知相关接口 +// ========================================== + +/** + * GetMomentNotificationsHandler + * 功能:获取朋友圈通知列表 + * 路径:GET /api/moments/notifications + */ +func GetMomentNotificationsHandler(c *gin.Context) { + userID := c.GetString("user_id") + if userID == "" { + utils.Unauthorized(c, "未登录") + return + } + + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + + notifications, total, err := service.MomentSvc.GetNotifications(userID, page, pageSize) + if err != nil { + utils.InternalError(c, err.Error()) + return + } + + if notifications == nil { + notifications = []model.MomentNotification{} + } + + utils.SuccessWithData(c, gin.H{ + "data": notifications, + "total": total, + "page": page, + "size": pageSize, + }, "获取成功") +} + +/** + * MarkNotificationsReadHandler + * 功能:标记通知为已读 + * 路径:POST /api/moments/notifications/read + */ +func MarkNotificationsReadHandler(c *gin.Context) { + userID := c.GetString("user_id") + if userID == "" { + utils.Unauthorized(c, "未登录") + return + } + + var req struct { + IDs []uint `json:"ids"` + All bool `json:"all"` // 是否标记全部已读 + } + if err := c.ShouldBindJSON(&req); err != nil { + utils.BadRequest(c, "参数错误") + return + } + + var err error + if req.All { + err = service.MomentSvc.MarkAllNotificationsRead(userID) + } else if len(req.IDs) > 0 { + err = service.MomentSvc.MarkNotificationsRead(userID, req.IDs) + } else { + utils.BadRequest(c, "请提供通知ID或设置all为true") + return + } + + if err != nil { + utils.InternalError(c, err.Error()) + return + } + + utils.Success(c, "标记成功") +} + +/** + * GetUnreadCountHandler + * 功能:获取未读通知数量 + * 路径:GET /api/moments/notifications/unread-count + */ +func GetUnreadCountHandler(c *gin.Context) { + userID := c.GetString("user_id") + if userID == "" { + utils.Unauthorized(c, "未登录") + return + } + + count, err := service.MomentSvc.GetUnreadCount(userID) + if err != nil { + utils.InternalError(c, err.Error()) + return + } + + utils.SuccessWithData(c, gin.H{"count": count}, "获取成功") +} + +// ========================================== +// 路由注册函数 +// ========================================== + +/** + * RegisterMomentRoutes + * 功能:注册朋友圈相关路由 + */ +func RegisterMomentRoutes(r *gin.RouterGroup) { + moments := r.Group("/moments") + { + // 动态相关 + moments.POST("", CreateMomentHandler) + moments.GET("", GetMomentsHandler) + moments.GET("/:id", GetMomentDetailHandler) + moments.DELETE("/:id", DeleteMomentHandler) + moments.GET("/user/:user_id", GetUserMomentsHandler) + + // 点赞相关 + moments.POST("/:id/like", LikeMomentHandler) + moments.DELETE("/:id/like", UnlikeMomentHandler) + moments.GET("/:id/likes", GetMomentLikesHandler) + + // 评论相关 + moments.POST("/:id/comments", CreateCommentHandler) + moments.GET("/:id/comments", GetMomentCommentsHandler) + moments.DELETE("/comments/:id", DeleteCommentHandler) + + // 通知相关 + moments.GET("/notifications", GetMomentNotificationsHandler) + moments.POST("/notifications/read", MarkNotificationsReadHandler) + moments.GET("/notifications/unread-count", GetUnreadCountHandler) + } +} diff --git a/internal/model/types.go b/internal/model/types.go index 50da703..6ce7c1a 100644 --- a/internal/model/types.go +++ b/internal/model/types.go @@ -570,6 +570,224 @@ type SendCodeReq struct { Type string `json:"type" binding:"required,oneof=email sms"` } +// ========================================== +// 朋友圈相关实体 (Moments) +// ========================================== + +/** + * Moment + * 对应数据库表:moments + * 作用:朋友圈动态表 + */ +type Moment struct { + // 动态ID + ID uint `gorm:"primaryKey;comment:动态ID" json:"id"` + // 发布者ID + UserID string `gorm:"type:varchar(100);index;comment:发布者ID" json:"user_id"` + // 文字内容 + Content string `gorm:"type:text;comment:文字内容" json:"content"` + // 媒体类型: 0=纯文字 1=图片 2=视频 + MediaType int8 `gorm:"type:tinyint;default:0;comment:媒体类型" json:"media_type"` + // 媒体URL列表(JSON数组) + MediaURLs string `gorm:"type:text;comment:媒体URL列表" json:"media_urls"` + // 位置信息 + Location string `gorm:"type:varchar(255);comment:位置信息" json:"location"` + // 可见性: 0=公开 1=仅好友 2=部分好友可见 3=部分好友不可见 + Visibility int8 `gorm:"type:tinyint;default:0;comment:可见性" json:"visibility"` + // 可见/不可见用户ID列表(JSON数组) + VisibleUserIDs string `gorm:"type:text;comment:可见用户ID列表" json:"visible_user_ids"` + // @的用户ID列表(JSON数组) + MentionUserIDs string `gorm:"type:text;comment:@的用户ID列表" json:"mention_user_ids"` + // 话题标签(JSON数组) + TopicTags string `gorm:"type:varchar(500);comment:话题标签" json:"topic_tags"` + // 点赞数 + LikeCount int `gorm:"type:int;default:0;comment:点赞数" json:"like_count"` + // 评论数 + CommentCount int `gorm:"type:int;default:0;comment:评论数" json:"comment_count"` + // 是否已删除 + IsDeleted bool `gorm:"type:tinyint(1);default:0;comment:是否已删除" json:"-"` + // 创建时间 + CreatedAt time.Time `gorm:"autoCreateTime;comment:创建时间" json:"created_at"` + // 更新时间 + UpdatedAt time.Time `gorm:"autoUpdateTime;comment:更新时间" json:"updated_at"` + // 关联字段(不存储在数据库中) + User *User `gorm:"-" json:"user,omitempty"` + IsLiked bool `gorm:"-" json:"is_liked"` + Likes []MomentLike `gorm:"-" json:"likes,omitempty"` + Comments []MomentComment `gorm:"-" json:"comments,omitempty"` +} + +// TableName 指定表名 +func (Moment) TableName() string { + return "moments" +} + +/** + * MomentLike + * 对应数据库表:moment_likes + * 作用:动态点赞表 + */ +type MomentLike struct { + // 主键ID + ID uint `gorm:"primaryKey;comment:主键ID" json:"id"` + // 动态ID + MomentID uint `gorm:"type:bigint;index;comment:动态ID" json:"moment_id"` + // 点赞用户ID + UserID string `gorm:"type:varchar(100);index;comment:点赞用户ID" json:"user_id"` + // 创建时间 + CreatedAt time.Time `gorm:"autoCreateTime;comment:创建时间" json:"created_at"` + // 关联字段(不存储在数据库中) + User *User `gorm:"-" json:"user,omitempty"` +} + +// TableName 指定表名 +func (MomentLike) TableName() string { + return "moment_likes" +} + +/** + * MomentComment + * 对应数据库表:moment_comments + * 作用:动态评论表 + */ +type MomentComment struct { + // 评论ID + ID uint `gorm:"primaryKey;comment:评论ID" json:"id"` + // 动态ID + MomentID uint `gorm:"type:bigint;index;comment:动态ID" json:"moment_id"` + // 评论者ID + UserID string `gorm:"type:varchar(100);index;comment:评论者ID" json:"user_id"` + // 回复的评论ID(NULL为直接评论) + ReplyToCommentID *uint `gorm:"type:bigint;index;comment:回复的评论ID" json:"reply_to_comment_id"` + // 回复的用户ID + ReplyToUserID string `gorm:"type:varchar(100);comment:回复的用户ID" json:"reply_to_user_id"` + // 评论内容 + Content string `gorm:"type:text;comment:评论内容" json:"content"` + // 是否已删除 + IsDeleted bool `gorm:"type:tinyint(1);default:0;comment:是否已删除" json:"-"` + // 创建时间 + CreatedAt time.Time `gorm:"autoCreateTime;comment:创建时间" json:"created_at"` + // 关联字段(不存储在数据库中) + User *User `gorm:"-" json:"user,omitempty"` + ReplyToUser *User `gorm:"-" json:"reply_to_user,omitempty"` +} + +// TableName 指定表名 +func (MomentComment) TableName() string { + return "moment_comments" +} + +/** + * MomentNotification + * 对应数据库表:moment_notifications + * 作用:朋友圈通知表 + */ +type MomentNotification struct { + // 通知ID + ID uint `gorm:"primaryKey;comment:通知ID" json:"id"` + // 接收通知的用户ID + UserID string `gorm:"type:varchar(100);index;comment:接收通知的用户ID" json:"user_id"` + // 触发通知的用户ID + FromUserID string `gorm:"type:varchar(100);comment:触发通知的用户ID" json:"from_user_id"` + // 关联的动态ID + MomentID uint `gorm:"type:bigint;index;comment:关联的动态ID" json:"moment_id"` + // 通知类型: 1=点赞 2=评论 3=回复 4=@提及 + Type int8 `gorm:"type:tinyint;comment:通知类型" json:"type"` + // 关联的评论ID + CommentID *uint `gorm:"type:bigint;comment:关联的评论ID" json:"comment_id"` + // 是否已读 + IsRead bool `gorm:"type:tinyint(1);default:0;index;comment:是否已读" json:"is_read"` + // 创建时间 + CreatedAt time.Time `gorm:"autoCreateTime;index;comment:创建时间" json:"created_at"` + // 关联字段(不存储在数据库中) + FromUser *User `gorm:"-" json:"from_user,omitempty"` + Moment *Moment `gorm:"-" json:"moment,omitempty"` + Comment *MomentComment `gorm:"-" json:"comment,omitempty"` +} + +// TableName 指定表名 +func (MomentNotification) TableName() string { + return "moment_notifications" +} + +// ========================================== +// 朋友圈相关 DTO +// ========================================== + +/** + * CreateMomentReq + * 作用:发布动态请求参数 + */ +type CreateMomentReq struct { + // 文字内容 + Content string `json:"content"` + // 媒体类型: 0=纯文字 1=图片 2=视频 + MediaType int8 `json:"media_type"` + // 媒体URL列表 + MediaURLs []string `json:"media_urls"` + // 位置信息 + Location string `json:"location,omitempty"` + // 可见性: 0=公开 1=仅好友 2=部分好友可见 3=部分好友不可见 + Visibility int8 `json:"visibility"` + // 可见/不可见用户ID列表 + VisibleUserIDs []string `json:"visible_user_ids,omitempty"` + // @的用户ID列表 + MentionUserIDs []string `json:"mention_user_ids,omitempty"` + // 话题标签 + TopicTags []string `json:"topic_tags,omitempty"` +} + +/** + * CreateCommentReq + * 作用:发表评论请求参数 + */ +type CreateCommentReq struct { + // 评论内容 + Content string `json:"content" binding:"required"` + // 回复的评论ID(可选,回复评论时使用) + ReplyToCommentID *uint `json:"reply_to_comment_id,omitempty"` +} + +/** + * MomentNotifPayload + * 作用:WebSocket 推送朋友圈通知的数据结构 + */ +type MomentNotifPayload struct { + // 通知类型: "like", "comment", "reply", "mention" + Type string `json:"type"` + // 动态ID + MomentID uint `json:"moment_id"` + // 触发通知的用户 + FromUser *User `json:"from_user"` + // 评论内容(评论/回复时使用) + Content string `json:"content,omitempty"` + // 评论ID + CommentID uint `json:"comment_id,omitempty"` +} + +// 朋友圈通知类型常量 +const ( + MomentNotifTypeLike = 1 // 点赞 + MomentNotifTypeComment = 2 // 评论 + MomentNotifTypeReply = 3 // 回复 + MomentNotifTypeMention = 4 // @提及 +) + +// 动态可见性常量 +const ( + MomentVisibilityPublic = 0 // 公开 + MomentVisibilityFriendsOnly = 1 // 仅好友可见 + MomentVisibilityPartialVisible = 2 // 部分好友可见 + MomentVisibilityPartialHidden = 3 // 部分好友不可见 +) + +// 媒体类型常量 +const ( + MomentMediaTypeText = 0 // 纯文字 + MomentMediaTypeImage = 1 // 图片 + MomentMediaTypeVideo = 2 // 视频 +) + // ========================================== // 常量定义 // ========================================== diff --git a/internal/service/moment_service.go b/internal/service/moment_service.go new file mode 100644 index 0000000..d382ed2 --- /dev/null +++ b/internal/service/moment_service.go @@ -0,0 +1,769 @@ +/** + * package service + * 作用:朋友圈业务逻辑服务 + */ +package service + +import ( + "encoding/json" + "errors" + "log" + "xk-websocket-v2/internal/model" + + "gorm.io/gorm" +) + +// MomentService 朋友圈服务结构体 +type MomentService struct { + DB *gorm.DB +} + +// MomentSvc 全局单例 +var MomentSvc *MomentService + +/** + * InitMomentService + * 功能:初始化朋友圈服务 + */ +func InitMomentService(db *gorm.DB) { + MomentSvc = &MomentService{DB: db} +} + +// ========================================== +// 动态相关方法 +// ========================================== + +/** + * CreateMoment + * 功能:发布动态 + */ +func (s *MomentService) CreateMoment(userID string, req *model.CreateMomentReq) (*model.Moment, error) { + // 序列化数组字段为 JSON + mediaURLsJSON, _ := json.Marshal(req.MediaURLs) + visibleUserIDsJSON, _ := json.Marshal(req.VisibleUserIDs) + mentionUserIDsJSON, _ := json.Marshal(req.MentionUserIDs) + topicTagsJSON, _ := json.Marshal(req.TopicTags) + + moment := &model.Moment{ + UserID: userID, + Content: req.Content, + MediaType: req.MediaType, + MediaURLs: string(mediaURLsJSON), + Location: req.Location, + Visibility: req.Visibility, + VisibleUserIDs: string(visibleUserIDsJSON), + MentionUserIDs: string(mentionUserIDsJSON), + TopicTags: string(topicTagsJSON), + } + + if err := s.DB.Create(moment).Error; err != nil { + return nil, err + } + + // 处理@用户通知 + if len(req.MentionUserIDs) > 0 { + go s.createMentionNotifications(userID, moment.ID, req.MentionUserIDs) + } + + // 加载用户信息 + s.loadMomentUser(moment) + + return moment, nil +} + +/** + * DeleteMoment + * 功能:删除动态(软删除) + */ +func (s *MomentService) DeleteMoment(userID string, momentID uint) error { + result := s.DB.Model(&model.Moment{}). + Where("id = ? AND user_id = ?", momentID, userID). + Update("is_deleted", true) + + if result.RowsAffected == 0 { + return errors.New("动态不存在或无权删除") + } + return result.Error +} + +/** + * GetMomentDetail + * 功能:获取动态详情(含权限校验) + */ +func (s *MomentService) GetMomentDetail(viewerID string, momentID uint) (*model.Moment, error) { + var moment model.Moment + if err := s.DB.Where("id = ? AND is_deleted = ?", momentID, false).First(&moment).Error; err != nil { + return nil, err + } + + // 检查可见性权限 + if !s.checkVisibility(&moment, viewerID) { + return nil, errors.New("无权查看该动态") + } + + // 加载关联数据 + s.loadMomentUser(&moment) + s.loadMomentLikes(&moment, viewerID) + s.loadMomentComments(&moment) + + return &moment, nil +} + +/** + * GetFriendsMoments + * 功能:获取好友动态列表(含可见性过滤) + */ +func (s *MomentService) GetFriendsMoments(userID string, page, pageSize int) ([]model.Moment, int64, error) { + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 50 { + pageSize = 20 + } + offset := (page - 1) * pageSize + + // 获取好友ID列表 + friendIDs := s.getFriendIDs(userID) + // 加上自己的ID,可以看到自己的动态 + friendIDs = append(friendIDs, userID) + + var total int64 + var moments []model.Moment + + // 查询动态(包含公开和好友可见的) + query := s.DB.Model(&model.Moment{}). + Where("is_deleted = ?", false). + Where("user_id IN ?", friendIDs) + + // 统计总数 + query.Count(&total) + + // 分页查询 + if err := query.Order("created_at DESC"). + Offset(offset). + Limit(pageSize). + Find(&moments).Error; err != nil { + return nil, 0, err + } + + // 过滤可见性并加载关联数据 + var visibleMoments []model.Moment + for i := range moments { + if s.checkVisibility(&moments[i], userID) { + s.loadMomentUser(&moments[i]) + s.loadMomentLikes(&moments[i], userID) + s.loadMomentComments(&moments[i]) + visibleMoments = append(visibleMoments, moments[i]) + } + } + + return visibleMoments, total, nil +} + +/** + * GetUserMoments + * 功能:获取指定用户的动态列表 + */ +func (s *MomentService) GetUserMoments(viewerID, targetUserID string, page, pageSize int) ([]model.Moment, int64, error) { + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 50 { + pageSize = 20 + } + offset := (page - 1) * pageSize + + var total int64 + var moments []model.Moment + + query := s.DB.Model(&model.Moment{}). + Where("user_id = ? AND is_deleted = ?", targetUserID, false) + + query.Count(&total) + + if err := query.Order("created_at DESC"). + Offset(offset). + Limit(pageSize). + Find(&moments).Error; err != nil { + return nil, 0, err + } + + // 过滤可见性并加载关联数据 + var visibleMoments []model.Moment + for i := range moments { + if s.checkVisibility(&moments[i], viewerID) { + s.loadMomentUser(&moments[i]) + s.loadMomentLikes(&moments[i], viewerID) + s.loadMomentComments(&moments[i]) + visibleMoments = append(visibleMoments, moments[i]) + } + } + + return visibleMoments, total, nil +} + +// ========================================== +// 点赞相关方法 +// ========================================== + +/** + * LikeMoment + * 功能:点赞动态 + */ +func (s *MomentService) LikeMoment(userID string, momentID uint) error { + // 检查动态是否存在 + var moment model.Moment + if err := s.DB.Where("id = ? AND is_deleted = ?", momentID, false).First(&moment).Error; err != nil { + return errors.New("动态不存在") + } + + // 检查是否已点赞 + var existingLike model.MomentLike + if err := s.DB.Where("moment_id = ? AND user_id = ?", momentID, userID).First(&existingLike).Error; err == nil { + return errors.New("已点赞过该动态") + } + + // 开启事务 + tx := s.DB.Begin() + + // 创建点赞记录 + like := model.MomentLike{ + MomentID: momentID, + UserID: userID, + } + if err := tx.Create(&like).Error; err != nil { + tx.Rollback() + return err + } + + // 更新动态点赞数 + if err := tx.Model(&model.Moment{}).Where("id = ?", momentID). + UpdateColumn("like_count", gorm.Expr("like_count + 1")).Error; err != nil { + tx.Rollback() + return err + } + + tx.Commit() + + // 发送通知(异步) + if moment.UserID != userID { + go s.createNotification(moment.UserID, userID, momentID, model.MomentNotifTypeLike, nil) + } + + return nil +} + +/** + * UnlikeMoment + * 功能:取消点赞 + */ +func (s *MomentService) UnlikeMoment(userID string, momentID uint) error { + tx := s.DB.Begin() + + // 删除点赞记录 + result := tx.Where("moment_id = ? AND user_id = ?", momentID, userID).Delete(&model.MomentLike{}) + if result.RowsAffected == 0 { + tx.Rollback() + return errors.New("未点赞过该动态") + } + + // 更新动态点赞数 + if err := tx.Model(&model.Moment{}).Where("id = ?", momentID). + UpdateColumn("like_count", gorm.Expr("like_count - 1")).Error; err != nil { + tx.Rollback() + return err + } + + return tx.Commit().Error +} + +/** + * GetMomentLikes + * 功能:获取动态的点赞列表 + */ +func (s *MomentService) GetMomentLikes(momentID uint) ([]model.MomentLike, error) { + var likes []model.MomentLike + if err := s.DB.Where("moment_id = ?", momentID). + Order("created_at DESC"). + Find(&likes).Error; err != nil { + return nil, err + } + + // 加载用户信息 + for i := range likes { + var user model.User + if err := s.DB.Where("id = ?", likes[i].UserID).First(&user).Error; err == nil { + user.Password = "" + likes[i].User = &user + } + } + + return likes, nil +} + +// ========================================== +// 评论相关方法 +// ========================================== + +/** + * CreateComment + * 功能:发表评论/回复 + */ +func (s *MomentService) CreateComment(userID string, momentID uint, req *model.CreateCommentReq) (*model.MomentComment, error) { + // 检查动态是否存在 + var moment model.Moment + if err := s.DB.Where("id = ? AND is_deleted = ?", momentID, false).First(&moment).Error; err != nil { + return nil, errors.New("动态不存在") + } + + comment := &model.MomentComment{ + MomentID: momentID, + UserID: userID, + Content: req.Content, + } + + // 如果是回复评论 + var replyToUserID string + if req.ReplyToCommentID != nil { + var replyComment model.MomentComment + if err := s.DB.Where("id = ? AND is_deleted = ?", *req.ReplyToCommentID, false).First(&replyComment).Error; err != nil { + return nil, errors.New("回复的评论不存在") + } + comment.ReplyToCommentID = req.ReplyToCommentID + comment.ReplyToUserID = replyComment.UserID + replyToUserID = replyComment.UserID + } + + // 开启事务 + tx := s.DB.Begin() + + if err := tx.Create(comment).Error; err != nil { + tx.Rollback() + return nil, err + } + + // 更新动态评论数 + if err := tx.Model(&model.Moment{}).Where("id = ?", momentID). + UpdateColumn("comment_count", gorm.Expr("comment_count + 1")).Error; err != nil { + tx.Rollback() + return nil, err + } + + tx.Commit() + + // 发送通知(异步) + go func() { + if req.ReplyToCommentID != nil && replyToUserID != userID { + // 回复评论通知 + s.createNotification(replyToUserID, userID, momentID, model.MomentNotifTypeReply, &comment.ID) + } else if moment.UserID != userID { + // 评论动态通知 + s.createNotification(moment.UserID, userID, momentID, model.MomentNotifTypeComment, &comment.ID) + } + }() + + // 加载用户信息 + s.loadCommentUser(comment) + + return comment, nil +} + +/** + * DeleteComment + * 功能:删除评论 + */ +func (s *MomentService) DeleteComment(userID string, commentID uint) error { + // 查找评论 + var comment model.MomentComment + if err := s.DB.Where("id = ? AND is_deleted = ?", commentID, false).First(&comment).Error; err != nil { + return errors.New("评论不存在") + } + + // 检查权限(评论者或动态作者可以删除) + var moment model.Moment + s.DB.Where("id = ?", comment.MomentID).First(&moment) + + if comment.UserID != userID && moment.UserID != userID { + return errors.New("无权删除该评论") + } + + // 开启事务 + tx := s.DB.Begin() + + // 软删除评论 + if err := tx.Model(&comment).Update("is_deleted", true).Error; err != nil { + tx.Rollback() + return err + } + + // 更新动态评论数 + if err := tx.Model(&model.Moment{}).Where("id = ?", comment.MomentID). + UpdateColumn("comment_count", gorm.Expr("comment_count - 1")).Error; err != nil { + tx.Rollback() + return err + } + + return tx.Commit().Error +} + +/** + * GetMomentComments + * 功能:获取动态的评论列表 + */ +func (s *MomentService) GetMomentComments(momentID uint) ([]model.MomentComment, error) { + var comments []model.MomentComment + if err := s.DB.Where("moment_id = ? AND is_deleted = ?", momentID, false). + Order("created_at ASC"). + Find(&comments).Error; err != nil { + return nil, err + } + + // 加载用户信息 + for i := range comments { + s.loadCommentUser(&comments[i]) + } + + return comments, nil +} + +// ========================================== +// 通知相关方法 +// ========================================== + +/** + * GetNotifications + * 功能:获取通知列表 + */ +func (s *MomentService) GetNotifications(userID string, page, pageSize int) ([]model.MomentNotification, int64, error) { + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 50 { + pageSize = 20 + } + offset := (page - 1) * pageSize + + var total int64 + var notifications []model.MomentNotification + + query := s.DB.Model(&model.MomentNotification{}).Where("user_id = ?", userID) + query.Count(&total) + + if err := query.Order("created_at DESC"). + Offset(offset). + Limit(pageSize). + Find(¬ifications).Error; err != nil { + return nil, 0, err + } + + // 加载关联数据 + for i := range notifications { + s.loadNotificationData(¬ifications[i]) + } + + return notifications, total, nil +} + +/** + * MarkNotificationsRead + * 功能:标记通知为已读 + */ +func (s *MomentService) MarkNotificationsRead(userID string, ids []uint) error { + return s.DB.Model(&model.MomentNotification{}). + Where("user_id = ? AND id IN ?", userID, ids). + Update("is_read", true).Error +} + +/** + * MarkAllNotificationsRead + * 功能:标记所有通知为已读 + */ +func (s *MomentService) MarkAllNotificationsRead(userID string) error { + return s.DB.Model(&model.MomentNotification{}). + Where("user_id = ? AND is_read = ?", userID, false). + Update("is_read", true).Error +} + +/** + * GetUnreadCount + * 功能:获取未读通知数量 + */ +func (s *MomentService) GetUnreadCount(userID string) (int64, error) { + var count int64 + err := s.DB.Model(&model.MomentNotification{}). + Where("user_id = ? AND is_read = ?", userID, false). + Count(&count).Error + return count, err +} + +// ========================================== +// 内部辅助方法 +// ========================================== + +/** + * checkVisibility + * 功能:检查动态对某用户是否可见 + */ +func (s *MomentService) checkVisibility(moment *model.Moment, viewerID string) bool { + // 作者自己总是可见 + if moment.UserID == viewerID { + return true + } + + switch moment.Visibility { + case model.MomentVisibilityPublic: + // 公开,所有人可见 + return true + + case model.MomentVisibilityFriendsOnly: + // 仅好友可见 + return s.isFriend(moment.UserID, viewerID) + + case model.MomentVisibilityPartialVisible: + // 部分好友可见 + var visibleIDs []string + json.Unmarshal([]byte(moment.VisibleUserIDs), &visibleIDs) + for _, id := range visibleIDs { + if id == viewerID { + return true + } + } + return false + + case model.MomentVisibilityPartialHidden: + // 部分好友不可见 + if !s.isFriend(moment.UserID, viewerID) { + return false + } + var hiddenIDs []string + json.Unmarshal([]byte(moment.VisibleUserIDs), &hiddenIDs) + for _, id := range hiddenIDs { + if id == viewerID { + return false + } + } + return true + + default: + return false + } +} + +/** + * isFriend + * 功能:检查两个用户是否是好友关系 + */ +func (s *MomentService) isFriend(userID1, userID2 string) bool { + var count int64 + s.DB.Model(&model.UserContact{}). + Where("user_id = ? AND contact_id = ?", userID1, userID2). + Count(&count) + return count > 0 +} + +/** + * getFriendIDs + * 功能:获取用户的所有好友ID + */ +func (s *MomentService) getFriendIDs(userID string) []string { + var contacts []model.UserContact + s.DB.Where("user_id = ?", userID).Find(&contacts) + + ids := make([]string, 0, len(contacts)) + for _, c := range contacts { + ids = append(ids, c.ContactID) + } + return ids +} + +/** + * loadMomentUser + * 功能:加载动态的发布者信息 + */ +func (s *MomentService) loadMomentUser(moment *model.Moment) { + var user model.User + if err := s.DB.Where("id = ?", moment.UserID).First(&user).Error; err == nil { + user.Password = "" + moment.User = &user + } +} + +/** + * loadMomentLikes + * 功能:加载动态的点赞列表,并判断当前用户是否已点赞 + */ +func (s *MomentService) loadMomentLikes(moment *model.Moment, viewerID string) { + var likes []model.MomentLike + s.DB.Where("moment_id = ?", moment.ID). + Order("created_at DESC"). + Limit(10). // 只加载最近10个点赞 + Find(&likes) + + for i := range likes { + var user model.User + if err := s.DB.Where("id = ?", likes[i].UserID).First(&user).Error; err == nil { + user.Password = "" + likes[i].User = &user + } + if likes[i].UserID == viewerID { + moment.IsLiked = true + } + } + + moment.Likes = likes + + // 如果没有在前10个点赞中找到,再单独查询是否已点赞 + if !moment.IsLiked && viewerID != "" { + var count int64 + s.DB.Model(&model.MomentLike{}). + Where("moment_id = ? AND user_id = ?", moment.ID, viewerID). + Count(&count) + moment.IsLiked = count > 0 + } +} + +/** + * loadMomentComments + * 功能:加载动态的评论列表 + */ +func (s *MomentService) loadMomentComments(moment *model.Moment) { + var comments []model.MomentComment + s.DB.Where("moment_id = ? AND is_deleted = ?", moment.ID, false). + Order("created_at ASC"). + Find(&comments) + + for i := range comments { + s.loadCommentUser(&comments[i]) + } + + moment.Comments = comments +} + +/** + * loadCommentUser + * 功能:加载评论的用户信息 + */ +func (s *MomentService) loadCommentUser(comment *model.MomentComment) { + var user model.User + if err := s.DB.Where("id = ?", comment.UserID).First(&user).Error; err == nil { + user.Password = "" + comment.User = &user + } + + if comment.ReplyToUserID != "" { + var replyUser model.User + if err := s.DB.Where("id = ?", comment.ReplyToUserID).First(&replyUser).Error; err == nil { + replyUser.Password = "" + comment.ReplyToUser = &replyUser + } + } +} + +/** + * loadNotificationData + * 功能:加载通知的关联数据 + */ +func (s *MomentService) loadNotificationData(notif *model.MomentNotification) { + // 加载发送者信息 + var fromUser model.User + if err := s.DB.Where("id = ?", notif.FromUserID).First(&fromUser).Error; err == nil { + fromUser.Password = "" + notif.FromUser = &fromUser + } + + // 加载动态信息 + var moment model.Moment + if err := s.DB.Where("id = ?", notif.MomentID).First(&moment).Error; err == nil { + notif.Moment = &moment + } + + // 加载评论信息 + if notif.CommentID != nil { + var comment model.MomentComment + if err := s.DB.Where("id = ?", *notif.CommentID).First(&comment).Error; err == nil { + notif.Comment = &comment + } + } +} + +/** + * createNotification + * 功能:创建通知并推送 + */ +func (s *MomentService) createNotification(toUserID, fromUserID string, momentID uint, notifType int8, commentID *uint) { + notif := &model.MomentNotification{ + UserID: toUserID, + FromUserID: fromUserID, + MomentID: momentID, + Type: notifType, + CommentID: commentID, + } + + if err := s.DB.Create(notif).Error; err != nil { + log.Printf("❌ 创建朋友圈通知失败: %v", err) + return + } + + // 通过 WebSocket 推送实时通知 + s.sendRealtimeNotification(notif) +} + +/** + * createMentionNotifications + * 功能:为@的用户创建通知 + */ +func (s *MomentService) createMentionNotifications(fromUserID string, momentID uint, mentionUserIDs []string) { + for _, userID := range mentionUserIDs { + if userID != fromUserID { + s.createNotification(userID, fromUserID, momentID, model.MomentNotifTypeMention, nil) + } + } +} + +/** + * sendRealtimeNotification + * 功能:通过 WebSocket 推送实时通知 + */ +func (s *MomentService) sendRealtimeNotification(notif *model.MomentNotification) { + if ChatSvc == nil { + return + } + + // 加载关联数据 + s.loadNotificationData(notif) + + // 构建通知类型字符串 + var typeStr string + switch notif.Type { + case model.MomentNotifTypeLike: + typeStr = "like" + case model.MomentNotifTypeComment: + typeStr = "comment" + case model.MomentNotifTypeReply: + typeStr = "reply" + case model.MomentNotifTypeMention: + typeStr = "mention" + } + + // 构建推送消息 + payload := model.MomentNotifPayload{ + Type: typeStr, + MomentID: notif.MomentID, + FromUser: notif.FromUser, + } + + if notif.Comment != nil { + payload.Content = notif.Comment.Content + payload.CommentID = notif.Comment.ID + } + + pushMsg := model.WsPayload{ + RequestType: "moment_notification", + Data: payload, + } + + msgBytes, _ := json.Marshal(pushMsg) + ChatSvc.DispatchMessage(notif.UserID, msgBytes) + + log.Printf("📢 [朋友圈] 推送通知: To=%s Type=%s", notif.UserID, typeStr) +} diff --git a/nl_im_plus.sql b/nl_im_plus.sql index 71ab760..7ae1b2d 100644 --- a/nl_im_plus.sql +++ b/nl_im_plus.sql @@ -336,4 +336,85 @@ CREATE TABLE `verification_codes` ( -- Records of verification_codes -- ---------------------------- +-- ---------------------------- +-- Table structure for moments (朋友圈动态表) +-- ---------------------------- +DROP TABLE IF EXISTS `moments`; +CREATE TABLE `moments` ( + `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '动态ID', + `user_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '发布者ID', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '文字内容', + `media_type` tinyint NULL DEFAULT 0 COMMENT '媒体类型: 0=纯文字 1=图片 2=视频', + `media_urls` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '媒体URL列表(JSON数组)', + `location` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '位置信息', + `visibility` tinyint NULL DEFAULT 0 COMMENT '可见性: 0=公开 1=仅好友 2=部分好友可见 3=部分好友不可见', + `visible_user_ids` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '可见/不可见用户ID列表(JSON数组)', + `mention_user_ids` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '@的用户ID列表(JSON数组)', + `topic_tags` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '话题标签(JSON数组)', + `like_count` int NULL DEFAULT 0 COMMENT '点赞数', + `comment_count` int NULL DEFAULT 0 COMMENT '评论数', + `is_deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否已删除', + `created_at` datetime(3) NULL DEFAULT NULL COMMENT '创建时间', + `updated_at` datetime(3) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_moments_user_id`(`user_id` ASC) USING BTREE, + INDEX `idx_moments_created_at`(`created_at` ASC) USING BTREE, + INDEX `idx_moments_visibility`(`visibility` ASC) USING BTREE, + INDEX `idx_moments_is_deleted`(`is_deleted` ASC) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '朋友圈动态表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for moment_likes (动态点赞表) +-- ---------------------------- +DROP TABLE IF EXISTS `moment_likes`; +CREATE TABLE `moment_likes` ( + `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `moment_id` bigint UNSIGNED NOT NULL COMMENT '动态ID', + `user_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '点赞用户ID', + `created_at` datetime(3) NULL DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) 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_user_id`(`user_id` ASC) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '动态点赞表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for moment_comments (动态评论表) +-- ---------------------------- +DROP TABLE IF EXISTS `moment_comments`; +CREATE TABLE `moment_comments` ( + `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '评论ID', + `moment_id` bigint UNSIGNED NOT NULL COMMENT '动态ID', + `user_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '评论者ID', + `reply_to_comment_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '回复的评论ID(NULL为直接评论)', + `reply_to_user_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '回复的用户ID', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '评论内容', + `is_deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否已删除', + `created_at` datetime(3) NULL DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_moment_comments_moment_id`(`moment_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 +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '动态评论表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for moment_notifications (朋友圈通知表) +-- ---------------------------- +DROP TABLE IF EXISTS `moment_notifications`; +CREATE TABLE `moment_notifications` ( + `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '通知ID', + `user_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '接收通知的用户ID', + `from_user_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '触发通知的用户ID', + `moment_id` bigint UNSIGNED NOT NULL COMMENT '关联的动态ID', + `type` tinyint NOT NULL COMMENT '通知类型: 1=点赞 2=评论 3=回复 4=@提及', + `comment_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '关联的评论ID', + `is_read` tinyint(1) NULL DEFAULT 0 COMMENT '是否已读', + `created_at` datetime(3) NULL DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_moment_notifications_user_id`(`user_id` 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_moment_id`(`moment_id` ASC) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '朋友圈通知表' ROW_FORMAT = Dynamic; + SET FOREIGN_KEY_CHECKS = 1;