Initial backend repository commit.
Set up project files and add .gitignore to exclude local build/runtime artifacts. Made-with: Cursor
This commit is contained in:
118
internal/model/audit_log.go
Normal file
118
internal/model/audit_log.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AuditTargetType 审核对象类型
|
||||
type AuditTargetType string
|
||||
|
||||
const (
|
||||
AuditTargetTypePost AuditTargetType = "post" // 帖子
|
||||
AuditTargetTypeComment AuditTargetType = "comment" // 评论
|
||||
AuditTargetTypeMessage AuditTargetType = "message" // 私信
|
||||
AuditTargetTypeUser AuditTargetType = "user" // 用户资料
|
||||
AuditTargetTypeImage AuditTargetType = "image" // 图片
|
||||
)
|
||||
|
||||
// AuditResult 审核结果
|
||||
type AuditResult string
|
||||
|
||||
const (
|
||||
AuditResultPass AuditResult = "pass" // 通过
|
||||
AuditResultReview AuditResult = "review" // 需人工复审
|
||||
AuditResultBlock AuditResult = "block" // 违规拦截
|
||||
AuditResultUnknown AuditResult = "unknown" // 未知
|
||||
)
|
||||
|
||||
// AuditRiskLevel 风险等级
|
||||
type AuditRiskLevel string
|
||||
|
||||
const (
|
||||
AuditRiskLevelLow AuditRiskLevel = "low" // 低风险
|
||||
AuditRiskLevelMedium AuditRiskLevel = "medium" // 中风险
|
||||
AuditRiskLevelHigh AuditRiskLevel = "high" // 高风险
|
||||
)
|
||||
|
||||
// AuditSource 审核来源
|
||||
type AuditSource string
|
||||
|
||||
const (
|
||||
AuditSourceAuto AuditSource = "auto" // 自动审核
|
||||
AuditSourceManual AuditSource = "manual" // 人工审核
|
||||
AuditSourceCallback AuditSource = "callback" // 回调审核
|
||||
)
|
||||
|
||||
// AuditLog 审核日志实体
|
||||
type AuditLog struct {
|
||||
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
|
||||
TargetType AuditTargetType `json:"target_type" gorm:"type:varchar(50);index"`
|
||||
TargetID string `json:"target_id" gorm:"type:varchar(255);index"`
|
||||
Content string `json:"content" gorm:"type:text"` // 待审核内容
|
||||
ContentType string `json:"content_type" gorm:"type:varchar(50)"` // 内容类型: text, image
|
||||
ContentURL string `json:"content_url" gorm:"type:text"` // 图片/文件URL
|
||||
AuditType string `json:"audit_type" gorm:"type:varchar(50)"` // 审核类型: porn, violence, ad, political, fraud, gamble
|
||||
Result AuditResult `json:"result" gorm:"type:varchar(50);index"`
|
||||
RiskLevel AuditRiskLevel `json:"risk_level" gorm:"type:varchar(20)"`
|
||||
Labels string `json:"labels" gorm:"type:text"` // JSON数组,标签列表
|
||||
Suggestion string `json:"suggestion" gorm:"type:varchar(50)"` // pass, review, block
|
||||
Detail string `json:"detail" gorm:"type:text"` // 详细说明
|
||||
ThirdPartyID string `json:"third_party_id" gorm:"type:varchar(255)"` // 第三方审核服务返回的ID
|
||||
Source AuditSource `json:"source" gorm:"type:varchar(20);default:auto"`
|
||||
ReviewerID string `json:"reviewer_id" gorm:"type:varchar(255)"` // 审核人ID(人工审核时使用)
|
||||
ReviewerName string `json:"reviewer_name" gorm:"type:varchar(100)"` // 审核人名称
|
||||
ReviewTime *time.Time `json:"review_time" gorm:"index"` // 审核时间
|
||||
UserID string `json:"user_id" gorm:"type:varchar(255);index"` // 内容发布者ID
|
||||
UserIP string `json:"user_ip" gorm:"type:varchar(45)"` // 用户IP
|
||||
Status string `json:"status" gorm:"type:varchar(20);default:pending"` // pending, completed, failed
|
||||
RejectReason string `json:"reject_reason" gorm:"type:text"` // 拒绝原因(人工审核时使用)
|
||||
ExtraData string `json:"extra_data" gorm:"type:text"` // 额外数据,JSON格式
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前生成UUID
|
||||
func (al *AuditLog) BeforeCreate(tx *gorm.DB) error {
|
||||
if al.ID == "" {
|
||||
al.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (AuditLog) TableName() string {
|
||||
return "audit_logs"
|
||||
}
|
||||
|
||||
// AuditLogRequest 创建审核日志请求
|
||||
type AuditLogRequest struct {
|
||||
TargetType AuditTargetType `json:"target_type" validate:"required"`
|
||||
TargetID string `json:"target_id" validate:"required"`
|
||||
Content string `json:"content"`
|
||||
ContentType string `json:"content_type"`
|
||||
ContentURL string `json:"content_url"`
|
||||
AuditType string `json:"audit_type"`
|
||||
UserID string `json:"user_id"`
|
||||
UserIP string `json:"user_ip"`
|
||||
}
|
||||
|
||||
// AuditLogListItem 审核日志列表项
|
||||
type AuditLogListItem struct {
|
||||
ID string `json:"id"`
|
||||
TargetType AuditTargetType `json:"target_type"`
|
||||
TargetID string `json:"target_id"`
|
||||
Content string `json:"content"`
|
||||
ContentType string `json:"content_type"`
|
||||
Result AuditResult `json:"result"`
|
||||
RiskLevel AuditRiskLevel `json:"risk_level"`
|
||||
Suggestion string `json:"suggestion"`
|
||||
Source AuditSource `json:"source"`
|
||||
ReviewerID string `json:"reviewer_id"`
|
||||
ReviewTime *time.Time `json:"review_time"`
|
||||
UserID string `json:"user_id"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
80
internal/model/comment.go
Normal file
80
internal/model/comment.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// CommentStatus 评论状态
|
||||
type CommentStatus string
|
||||
|
||||
const (
|
||||
CommentStatusDraft CommentStatus = "draft"
|
||||
CommentStatusPending CommentStatus = "pending"
|
||||
CommentStatusPublished CommentStatus = "published"
|
||||
CommentStatusRejected CommentStatus = "rejected"
|
||||
CommentStatusDeleted CommentStatus = "deleted"
|
||||
)
|
||||
|
||||
// Comment 评论实体
|
||||
type Comment struct {
|
||||
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
|
||||
PostID string `json:"post_id" gorm:"type:varchar(36);not null;index:idx_comments_post_parent_status_created,priority:1"`
|
||||
UserID string `json:"user_id" gorm:"type:varchar(36);index;not null"`
|
||||
ParentID *string `json:"parent_id" gorm:"type:varchar(36);index:idx_comments_post_parent_status_created,priority:2"` // 父评论 ID(支持嵌套)
|
||||
RootID *string `json:"root_id" gorm:"type:varchar(36);index:idx_comments_root_status_created,priority:1"` // 根评论 ID(用于高效查询)
|
||||
Content string `json:"content" gorm:"type:text;not null"`
|
||||
Images string `json:"images" gorm:"type:text"` // 图片URL列表,JSON数组格式
|
||||
|
||||
// 关联
|
||||
User *User `json:"-" gorm:"foreignKey:UserID"`
|
||||
Replies []*Comment `json:"-" gorm:"-"` // 子回复(手动加载,非 GORM 关联)
|
||||
|
||||
// 审核状态
|
||||
Status CommentStatus `json:"status" gorm:"type:varchar(20);default:published;index:idx_comments_post_parent_status_created,priority:3;index:idx_comments_root_status_created,priority:2"`
|
||||
|
||||
// 统计
|
||||
LikesCount int `json:"likes_count" gorm:"default:0"`
|
||||
RepliesCount int `json:"replies_count" gorm:"default:0"`
|
||||
|
||||
// 软删除
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
|
||||
// 时间戳
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index:idx_comments_post_parent_status_created,priority:4,sort:asc;index:idx_comments_root_status_created,priority:3,sort:asc"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前生成UUID
|
||||
func (c *Comment) BeforeCreate(tx *gorm.DB) error {
|
||||
if c.ID == "" {
|
||||
c.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Comment) TableName() string {
|
||||
return "comments"
|
||||
}
|
||||
|
||||
// CommentLike 评论点赞
|
||||
type CommentLike struct {
|
||||
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
|
||||
CommentID string `json:"comment_id" gorm:"type:varchar(36);index;not null"`
|
||||
UserID string `json:"user_id" gorm:"type:varchar(36);index;not null"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前生成UUID
|
||||
func (cl *CommentLike) BeforeCreate(tx *gorm.DB) error {
|
||||
if cl.ID == "" {
|
||||
cl.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (CommentLike) TableName() string {
|
||||
return "comment_likes"
|
||||
}
|
||||
68
internal/model/conversation.go
Normal file
68
internal/model/conversation.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"carrot_bbs/internal/pkg/utils"
|
||||
)
|
||||
|
||||
// ConversationType 会话类型
|
||||
type ConversationType string
|
||||
|
||||
const (
|
||||
ConversationTypePrivate ConversationType = "private" // 私聊
|
||||
ConversationTypeGroup ConversationType = "group" // 群聊
|
||||
ConversationTypeSystem ConversationType = "system" // 系统通知会话
|
||||
)
|
||||
|
||||
// Conversation 会话实体
|
||||
// 使用雪花算法ID(作为string存储)和seq机制实现消息排序和增量同步
|
||||
type Conversation struct {
|
||||
ID string `gorm:"primaryKey;size:20" json:"id"` // 雪花算法ID(转为string避免精度丢失)
|
||||
Type ConversationType `gorm:"type:varchar(20);default:'private'" json:"type"` // 会话类型
|
||||
GroupID *string `gorm:"index" json:"group_id,omitempty"` // 关联的群组ID(群聊时使用,string类型避免JS精度丢失);使用指针支持NULL值
|
||||
LastSeq int64 `gorm:"default:0" json:"last_seq"` // 最后一条消息的seq
|
||||
LastMsgTime *time.Time `json:"last_msg_time,omitempty"` // 最后消息时间
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
|
||||
// 关联 - 使用 polymorphic 模式避免外键约束问题
|
||||
Participants []ConversationParticipant `gorm:"foreignKey:ConversationID" json:"participants,omitempty"`
|
||||
Group *Group `gorm:"foreignKey:GroupID;references:ID" json:"group,omitempty"`
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前生成雪花算法ID
|
||||
func (c *Conversation) BeforeCreate(tx *gorm.DB) error {
|
||||
if c.ID == "" {
|
||||
id, err := utils.GetSnowflake().GenerateID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.ID = strconv.FormatInt(id, 10)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Conversation) TableName() string {
|
||||
return "conversations"
|
||||
}
|
||||
|
||||
// ConversationParticipant 会话参与者
|
||||
type ConversationParticipant struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ConversationID string `gorm:"not null;size:20;uniqueIndex:idx_conversation_user,priority:1;index:idx_cp_conversation_user,priority:1" json:"conversation_id"` // 雪花算法ID(string类型)
|
||||
UserID string `gorm:"column:user_id;type:varchar(50);not null;uniqueIndex:idx_conversation_user,priority:2;index:idx_cp_conversation_user,priority:2;index:idx_cp_user_hidden_pinned_updated,priority:1" json:"user_id"` // UUID格式,与JWT中user_id保持一致
|
||||
LastReadSeq int64 `gorm:"default:0" json:"last_read_seq"` // 已读到的seq位置
|
||||
Muted bool `gorm:"default:false" json:"muted"` // 是否免打扰
|
||||
IsPinned bool `gorm:"default:false;index:idx_cp_user_hidden_pinned_updated,priority:3" json:"is_pinned"` // 是否置顶会话(用户维度)
|
||||
HiddenAt *time.Time `gorm:"index:idx_cp_user_hidden_pinned_updated,priority:2" json:"hidden_at,omitempty"` // 仅自己删除会话时使用,收到新消息后自动恢复
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;index:idx_cp_user_hidden_pinned_updated,priority:4,sort:desc"`
|
||||
}
|
||||
|
||||
func (ConversationParticipant) TableName() string {
|
||||
return "conversation_participants"
|
||||
}
|
||||
94
internal/model/device_token.go
Normal file
94
internal/model/device_token.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"carrot_bbs/internal/pkg/utils"
|
||||
)
|
||||
|
||||
// DeviceType 设备类型
|
||||
type DeviceType string
|
||||
|
||||
const (
|
||||
DeviceTypeIOS DeviceType = "ios" // iOS设备
|
||||
DeviceTypeAndroid DeviceType = "android" // Android设备
|
||||
DeviceTypeWeb DeviceType = "web" // Web端
|
||||
)
|
||||
|
||||
// DeviceToken 设备Token实体
|
||||
// 用于管理用户的多设备推送Token
|
||||
type DeviceToken struct {
|
||||
ID int64 `gorm:"primaryKey;autoIncrement:false" json:"id"` // 雪花算法ID
|
||||
UserID string `gorm:"column:user_id;type:varchar(50);index;not null" json:"user_id"` // 用户ID (UUID格式)
|
||||
DeviceID string `gorm:"type:varchar(100);not null" json:"device_id"` // 设备唯一标识
|
||||
DeviceType DeviceType `gorm:"type:varchar(20);not null" json:"device_type"` // 设备类型
|
||||
PushToken string `gorm:"type:varchar(256);not null" json:"push_token"` // 推送Token(FCM/APNs等)
|
||||
IsActive bool `gorm:"default:true" json:"is_active"` // 是否活跃
|
||||
DeviceName string `gorm:"type:varchar(100)" json:"device_name,omitempty"` // 设备名称(可选)
|
||||
|
||||
// 时间戳
|
||||
LastUsedAt *time.Time `json:"last_used_at,omitempty"` // 最后使用时间
|
||||
|
||||
// 软删除
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
|
||||
// 时间戳
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前生成雪花算法ID
|
||||
func (d *DeviceToken) BeforeCreate(tx *gorm.DB) error {
|
||||
if d.ID == 0 {
|
||||
id, err := utils.GetSnowflake().GenerateID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.ID = id
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (DeviceToken) TableName() string {
|
||||
return "device_tokens"
|
||||
}
|
||||
|
||||
// UpdateLastUsed 更新最后使用时间
|
||||
func (d *DeviceToken) UpdateLastUsed() {
|
||||
now := time.Now()
|
||||
d.LastUsedAt = &now
|
||||
}
|
||||
|
||||
// Deactivate 停用设备
|
||||
func (d *DeviceToken) Deactivate() {
|
||||
d.IsActive = false
|
||||
}
|
||||
|
||||
// Activate 激活设备
|
||||
func (d *DeviceToken) Activate() {
|
||||
d.IsActive = true
|
||||
now := time.Now()
|
||||
d.LastUsedAt = &now
|
||||
}
|
||||
|
||||
// IsIOS 判断是否为iOS设备
|
||||
func (d *DeviceToken) IsIOS() bool {
|
||||
return d.DeviceType == DeviceTypeIOS
|
||||
}
|
||||
|
||||
// IsAndroid 判断是否为Android设备
|
||||
func (d *DeviceToken) IsAndroid() bool {
|
||||
return d.DeviceType == DeviceTypeAndroid
|
||||
}
|
||||
|
||||
// IsWeb 判断是否为Web端
|
||||
func (d *DeviceToken) IsWeb() bool {
|
||||
return d.DeviceType == DeviceTypeWeb
|
||||
}
|
||||
|
||||
// SupportsMobilePush 判断是否支持手机推送
|
||||
func (d *DeviceToken) SupportsMobilePush() bool {
|
||||
return d.DeviceType == DeviceTypeIOS || d.DeviceType == DeviceTypeAndroid
|
||||
}
|
||||
28
internal/model/favorite.go
Normal file
28
internal/model/favorite.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Favorite 收藏
|
||||
type Favorite struct {
|
||||
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
|
||||
PostID string `json:"post_id" gorm:"type:varchar(36);not null;index;uniqueIndex:idx_favorite_post_user,priority:1"`
|
||||
UserID string `json:"user_id" gorm:"type:varchar(36);not null;index;uniqueIndex:idx_favorite_post_user,priority:2"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前生成UUID
|
||||
func (f *Favorite) BeforeCreate(tx *gorm.DB) error {
|
||||
if f.ID == "" {
|
||||
f.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Favorite) TableName() string {
|
||||
return "favorites"
|
||||
}
|
||||
28
internal/model/follow.go
Normal file
28
internal/model/follow.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Follow 关注关系
|
||||
type Follow struct {
|
||||
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
|
||||
FollowerID string `json:"follower_id" gorm:"type:varchar(36);index;not null;uniqueIndex:idx_follower_following"` // 关注者
|
||||
FollowingID string `json:"following_id" gorm:"type:varchar(36);index;not null;uniqueIndex:idx_follower_following"` // 被关注者
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前生成UUID
|
||||
func (f *Follow) BeforeCreate(tx *gorm.DB) error {
|
||||
if f.ID == "" {
|
||||
f.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Follow) TableName() string {
|
||||
return "follows"
|
||||
}
|
||||
57
internal/model/group.go
Normal file
57
internal/model/group.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"carrot_bbs/internal/pkg/utils"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// JoinType 群组加入类型
|
||||
type JoinType int
|
||||
|
||||
const (
|
||||
JoinTypeAnyone JoinType = 0 // 允许任何人加入
|
||||
JoinTypeApproval JoinType = 1 // 需要审批
|
||||
JoinTypeForbidden JoinType = 2 // 不允许加入
|
||||
)
|
||||
|
||||
// Group 群组模型
|
||||
type Group struct {
|
||||
ID string `gorm:"primaryKey;size:20" json:"id"`
|
||||
Name string `gorm:"size:50;not null" json:"name"`
|
||||
Avatar string `gorm:"size:512" json:"avatar"`
|
||||
Description string `gorm:"size:500" json:"description"`
|
||||
OwnerID string `gorm:"type:varchar(36);not null;index" json:"owner_id"`
|
||||
MemberCount int `gorm:"default:0" json:"member_count"`
|
||||
MaxMembers int `gorm:"default:500" json:"max_members"`
|
||||
JoinType JoinType `gorm:"default:0" json:"join_type"` // 0:允许任何人加入 1:需要审批 2:不允许加入
|
||||
MuteAll bool `gorm:"default:false" json:"mute_all"` // 全员禁言
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前生成雪花算法ID
|
||||
func (g *Group) BeforeCreate(tx *gorm.DB) error {
|
||||
if g.ID == "" {
|
||||
id, err := utils.GetSnowflake().GenerateID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
g.ID = strconv.FormatInt(id, 10)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetIDInt 获取数字类型的ID(用于比较)
|
||||
func (g *Group) GetIDInt() uint64 {
|
||||
id, _ := strconv.ParseUint(g.ID, 10, 64)
|
||||
return id
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (Group) TableName() string {
|
||||
return "groups"
|
||||
}
|
||||
38
internal/model/group_announcement.go
Normal file
38
internal/model/group_announcement.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"carrot_bbs/internal/pkg/utils"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GroupAnnouncement 群公告模型
|
||||
type GroupAnnouncement struct {
|
||||
ID string `gorm:"primaryKey;size:20" json:"id"`
|
||||
GroupID string `gorm:"not null;index" json:"group_id"`
|
||||
Content string `gorm:"type:text;not null" json:"content"`
|
||||
AuthorID string `gorm:"type:varchar(36);not null" json:"author_id"`
|
||||
IsPinned bool `gorm:"default:false" json:"is_pinned"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前生成雪花算法ID
|
||||
func (ga *GroupAnnouncement) BeforeCreate(tx *gorm.DB) error {
|
||||
if ga.ID == "" {
|
||||
id, err := utils.GetSnowflake().GenerateID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ga.ID = strconv.FormatInt(id, 10)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (GroupAnnouncement) TableName() string {
|
||||
return "group_announcements"
|
||||
}
|
||||
59
internal/model/group_join_request.go
Normal file
59
internal/model/group_join_request.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"carrot_bbs/internal/pkg/utils"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type GroupJoinRequestType string
|
||||
|
||||
const (
|
||||
GroupJoinRequestTypeInvite GroupJoinRequestType = "invite"
|
||||
GroupJoinRequestTypeJoinApply GroupJoinRequestType = "join_apply"
|
||||
)
|
||||
|
||||
type GroupJoinRequestStatus string
|
||||
|
||||
const (
|
||||
GroupJoinRequestStatusPending GroupJoinRequestStatus = "pending"
|
||||
GroupJoinRequestStatusAccepted GroupJoinRequestStatus = "accepted"
|
||||
GroupJoinRequestStatusRejected GroupJoinRequestStatus = "rejected"
|
||||
GroupJoinRequestStatusCancelled GroupJoinRequestStatus = "cancelled"
|
||||
GroupJoinRequestStatusExpired GroupJoinRequestStatus = "expired"
|
||||
)
|
||||
|
||||
// GroupJoinRequest 统一保存邀请入群和主动加群申请
|
||||
type GroupJoinRequest struct {
|
||||
ID string `gorm:"primaryKey;size:20" json:"id"`
|
||||
Flag string `gorm:"size:64;uniqueIndex;not null" json:"flag"`
|
||||
GroupID string `gorm:"not null;index;index:idx_gjr_group_target_type_status_created,priority:1" json:"group_id"`
|
||||
InitiatorID string `gorm:"type:varchar(36);not null;index" json:"initiator_id"`
|
||||
TargetUserID string `gorm:"type:varchar(36);not null;index;index:idx_gjr_group_target_type_status_created,priority:2" json:"target_user_id"`
|
||||
RequestType GroupJoinRequestType `gorm:"size:20;not null;index;index:idx_gjr_group_target_type_status_created,priority:3" json:"request_type"`
|
||||
Status GroupJoinRequestStatus `gorm:"size:20;not null;index;index:idx_gjr_group_target_type_status_created,priority:4" json:"status"`
|
||||
Reason string `gorm:"size:500" json:"reason"`
|
||||
ReviewerID string `gorm:"type:varchar(36);index" json:"reviewer_id"`
|
||||
ReviewedAt *time.Time `json:"reviewed_at"`
|
||||
ExpireAt *time.Time `json:"expire_at"`
|
||||
CreatedAt time.Time `gorm:"index:idx_gjr_group_target_type_status_created,priority:5,sort:desc" json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (r *GroupJoinRequest) BeforeCreate(tx *gorm.DB) error {
|
||||
if r.ID == "" {
|
||||
id, err := utils.GetSnowflake().GenerateID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.ID = strconv.FormatInt(id, 10)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (GroupJoinRequest) TableName() string {
|
||||
return "group_join_requests"
|
||||
}
|
||||
47
internal/model/group_member.go
Normal file
47
internal/model/group_member.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"carrot_bbs/internal/pkg/utils"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GroupMember 群成员模型
|
||||
type GroupMember struct {
|
||||
ID string `gorm:"primaryKey;size:20" json:"id"`
|
||||
GroupID string `gorm:"not null;uniqueIndex:idx_group_user" json:"group_id"`
|
||||
UserID string `gorm:"type:varchar(36);not null;uniqueIndex:idx_group_user" json:"user_id"`
|
||||
Role string `gorm:"size:20;default:'member'" json:"role"` // owner, admin, member
|
||||
Nickname string `gorm:"size:50" json:"nickname"` // 群内昵称
|
||||
Muted bool `gorm:"default:false" json:"muted"` // 是否被禁言
|
||||
JoinTime time.Time `json:"join_time"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前生成雪花算法ID
|
||||
func (gm *GroupMember) BeforeCreate(tx *gorm.DB) error {
|
||||
if gm.ID == "" {
|
||||
id, err := utils.GetSnowflake().GenerateID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gm.ID = strconv.FormatInt(id, 10)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (GroupMember) TableName() string {
|
||||
return "group_members"
|
||||
}
|
||||
|
||||
// Role 常量
|
||||
const (
|
||||
GroupRoleOwner = "owner"
|
||||
GroupRoleAdmin = "admin"
|
||||
GroupRoleMember = "member"
|
||||
)
|
||||
159
internal/model/init.go
Normal file
159
internal/model/init.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
"carrot_bbs/internal/config"
|
||||
)
|
||||
|
||||
// DB 全局数据库连接
|
||||
var DB *gorm.DB
|
||||
|
||||
// InitDB 初始化数据库连接
|
||||
func InitDB(cfg *config.DatabaseConfig) error {
|
||||
var err error
|
||||
var db *gorm.DB
|
||||
gormLogger := logger.New(
|
||||
log.New(os.Stdout, "\r\n", log.LstdFlags),
|
||||
logger.Config{
|
||||
SlowThreshold: time.Duration(cfg.SlowThresholdMs) * time.Millisecond,
|
||||
LogLevel: parseGormLogLevel(cfg.LogLevel),
|
||||
IgnoreRecordNotFoundError: cfg.IgnoreRecordNotFound,
|
||||
ParameterizedQueries: cfg.ParameterizedQueries,
|
||||
Colorful: false,
|
||||
},
|
||||
)
|
||||
|
||||
// 根据数据库类型选择驱动
|
||||
switch cfg.Type {
|
||||
case "sqlite":
|
||||
db, err = gorm.Open(sqlite.Open(cfg.SQLite.Path), &gorm.Config{
|
||||
Logger: gormLogger,
|
||||
})
|
||||
case "postgres", "postgresql":
|
||||
dsn := cfg.Postgres.DSN()
|
||||
db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||
Logger: gormLogger,
|
||||
})
|
||||
default:
|
||||
// 默认使用PostgreSQL
|
||||
dsn := cfg.Postgres.DSN()
|
||||
db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||
Logger: gormLogger,
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to database: %w", err)
|
||||
}
|
||||
|
||||
DB = db
|
||||
|
||||
// 配置连接池(SQLite不支持连接池配置,跳过)
|
||||
if cfg.Type != "sqlite" {
|
||||
sqlDB, err := DB.DB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get database instance: %w", err)
|
||||
}
|
||||
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
|
||||
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
|
||||
}
|
||||
|
||||
// 自动迁移
|
||||
if err := autoMigrate(DB); err != nil {
|
||||
return fmt.Errorf("failed to auto migrate: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Database connected (%s) and migrated successfully", cfg.Type)
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseGormLogLevel(level string) logger.LogLevel {
|
||||
switch level {
|
||||
case "silent":
|
||||
return logger.Silent
|
||||
case "error":
|
||||
return logger.Error
|
||||
case "warn":
|
||||
return logger.Warn
|
||||
case "info":
|
||||
return logger.Info
|
||||
default:
|
||||
return logger.Warn
|
||||
}
|
||||
}
|
||||
|
||||
// autoMigrate 自动迁移数据库表
|
||||
func autoMigrate(db *gorm.DB) error {
|
||||
err := db.AutoMigrate(
|
||||
// 用户相关
|
||||
&User{},
|
||||
|
||||
// 帖子相关
|
||||
&Post{},
|
||||
&PostImage{},
|
||||
|
||||
// 评论相关
|
||||
&Comment{},
|
||||
&CommentLike{},
|
||||
|
||||
// 消息相关(使用雪花算法ID和seq机制)
|
||||
// 已读位置存储在 ConversationParticipant.LastReadSeq 中
|
||||
&Conversation{},
|
||||
&ConversationParticipant{},
|
||||
&Message{},
|
||||
|
||||
// 系统通知(独立表,每个用户只能看到自己的通知)
|
||||
&SystemNotification{},
|
||||
|
||||
// 通知
|
||||
&Notification{},
|
||||
|
||||
// 推送中心相关
|
||||
&PushRecord{}, // 推送记录
|
||||
&DeviceToken{}, // 设备Token
|
||||
|
||||
// 社交
|
||||
&Follow{},
|
||||
&UserBlock{},
|
||||
&PostLike{},
|
||||
&Favorite{},
|
||||
|
||||
// 投票
|
||||
&VoteOption{},
|
||||
&UserVote{},
|
||||
|
||||
// 敏感词和审核
|
||||
&SensitiveWord{},
|
||||
&AuditLog{},
|
||||
|
||||
// 群组相关
|
||||
&Group{},
|
||||
&GroupMember{},
|
||||
&GroupAnnouncement{},
|
||||
&GroupJoinRequest{},
|
||||
|
||||
// 自定义表情
|
||||
&UserSticker{},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloseDB 关闭数据库连接
|
||||
func CloseDB() {
|
||||
if sqlDB, err := DB.DB(); err == nil {
|
||||
sqlDB.Close()
|
||||
}
|
||||
}
|
||||
28
internal/model/like.go
Normal file
28
internal/model/like.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PostLike 帖子点赞
|
||||
type PostLike struct {
|
||||
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
|
||||
PostID string `json:"post_id" gorm:"type:varchar(36);not null;index;uniqueIndex:idx_post_like_user,priority:1"`
|
||||
UserID string `json:"user_id" gorm:"type:varchar(36);not null;index;uniqueIndex:idx_post_like_user,priority:2"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前生成UUID
|
||||
func (pl *PostLike) BeforeCreate(tx *gorm.DB) error {
|
||||
if pl.ID == "" {
|
||||
pl.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (PostLike) TableName() string {
|
||||
return "post_likes"
|
||||
}
|
||||
205
internal/model/message.go
Normal file
205
internal/model/message.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"carrot_bbs/internal/pkg/utils"
|
||||
)
|
||||
|
||||
// 系统消息相关常量
|
||||
const (
|
||||
// SystemSenderID 系统消息发送者ID (类似QQ 10000)
|
||||
SystemSenderID int64 = 10000
|
||||
|
||||
// SystemSenderIDStr 系统消息发送者ID字符串版本
|
||||
SystemSenderIDStr string = "10000"
|
||||
|
||||
// SystemConversationID 系统通知会话ID (string类型)
|
||||
SystemConversationID string = "9999999999"
|
||||
)
|
||||
|
||||
// ContentType 消息内容类型
|
||||
type ContentType string
|
||||
|
||||
const (
|
||||
ContentTypeText ContentType = "text"
|
||||
ContentTypeImage ContentType = "image"
|
||||
ContentTypeVideo ContentType = "video"
|
||||
ContentTypeAudio ContentType = "audio"
|
||||
ContentTypeFile ContentType = "file"
|
||||
)
|
||||
|
||||
// MessageStatus 消息状态
|
||||
type MessageStatus string
|
||||
|
||||
const (
|
||||
MessageStatusNormal MessageStatus = "normal" // 正常
|
||||
MessageStatusRecalled MessageStatus = "recalled" // 已撤回
|
||||
MessageStatusDeleted MessageStatus = "deleted" // 已删除
|
||||
)
|
||||
|
||||
// MessageCategory 消息类别
|
||||
type MessageCategory string
|
||||
|
||||
const (
|
||||
CategoryChat MessageCategory = "chat" // 普通聊天
|
||||
CategoryNotification MessageCategory = "notification" // 通知类消息
|
||||
CategoryAnnouncement MessageCategory = "announcement" // 系统公告
|
||||
CategoryMarketing MessageCategory = "marketing" // 营销消息
|
||||
)
|
||||
|
||||
// SystemMessageType 系统消息类型 (对应原NotificationType)
|
||||
type SystemMessageType string
|
||||
|
||||
const (
|
||||
// 互动通知
|
||||
SystemTypeLikePost SystemMessageType = "like_post" // 点赞帖子
|
||||
SystemTypeLikeComment SystemMessageType = "like_comment" // 点赞评论
|
||||
SystemTypeComment SystemMessageType = "comment" // 评论
|
||||
SystemTypeReply SystemMessageType = "reply" // 回复
|
||||
SystemTypeFollow SystemMessageType = "follow" // 关注
|
||||
SystemTypeMention SystemMessageType = "mention" // @提及
|
||||
|
||||
// 系统消息
|
||||
SystemTypeSystem SystemMessageType = "system" // 系统通知
|
||||
SystemTypeAnnounce SystemMessageType = "announce" // 系统公告
|
||||
)
|
||||
|
||||
// ExtraData 消息额外数据,用于存储系统消息的相关信息
|
||||
type ExtraData struct {
|
||||
// 操作者信息
|
||||
ActorID int64 `json:"actor_id,omitempty"` // 操作者ID (数字格式,兼容旧数据)
|
||||
ActorIDStr string `json:"actor_id_str,omitempty"` // 操作者ID (UUID字符串格式)
|
||||
ActorName string `json:"actor_name,omitempty"` // 操作者名称
|
||||
AvatarURL string `json:"avatar_url,omitempty"` // 操作者头像
|
||||
|
||||
// 目标信息
|
||||
TargetID int64 `json:"target_id,omitempty"` // 目标ID(帖子ID、评论ID等)
|
||||
TargetTitle string `json:"target_title,omitempty"` // 目标标题
|
||||
TargetType string `json:"target_type,omitempty"` // 目标类型(post/comment等)
|
||||
|
||||
// 其他信息
|
||||
ActionURL string `json:"action_url,omitempty"` // 跳转链接
|
||||
ActionTime string `json:"action_time,omitempty"` // 操作时间
|
||||
}
|
||||
|
||||
// Value 实现driver.Valuer接口,用于数据库存储
|
||||
func (e ExtraData) Value() (driver.Value, error) {
|
||||
return json.Marshal(e)
|
||||
}
|
||||
|
||||
// Scan 实现sql.Scanner接口,用于数据库读取
|
||||
func (e *ExtraData) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
bytes, ok := value.([]byte)
|
||||
if !ok {
|
||||
return errors.New("type assertion to []byte failed")
|
||||
}
|
||||
return json.Unmarshal(bytes, e)
|
||||
}
|
||||
|
||||
// MessageSegmentData 单个消息段的数据
|
||||
type MessageSegmentData map[string]interface{}
|
||||
|
||||
// MessageSegment 消息段
|
||||
type MessageSegment struct {
|
||||
Type string `json:"type"`
|
||||
Data MessageSegmentData `json:"data"`
|
||||
}
|
||||
|
||||
// MessageSegments 消息链类型
|
||||
type MessageSegments []MessageSegment
|
||||
|
||||
// Value 实现driver.Valuer接口,用于数据库存储
|
||||
func (s MessageSegments) Value() (driver.Value, error) {
|
||||
return json.Marshal(s)
|
||||
}
|
||||
|
||||
// Scan 实现sql.Scanner接口,用于数据库读取
|
||||
func (s *MessageSegments) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
*s = nil
|
||||
return nil
|
||||
}
|
||||
bytes, ok := value.([]byte)
|
||||
if !ok {
|
||||
return errors.New("type assertion to []byte failed")
|
||||
}
|
||||
return json.Unmarshal(bytes, s)
|
||||
}
|
||||
|
||||
// Message 消息实体
|
||||
// 使用雪花算法ID(string类型)和seq机制实现消息排序和增量同步
|
||||
type Message struct {
|
||||
ID string `gorm:"primaryKey;size:20" json:"id"` // 雪花算法ID(string类型)
|
||||
ConversationID string `gorm:"not null;size:20;index:idx_msg_conversation_seq,priority:1" json:"conversation_id"` // 会话ID(string类型)
|
||||
SenderID string `gorm:"column:sender_id;type:varchar(50);index;not null" json:"sender_id"` // 发送者ID (UUID格式)
|
||||
Seq int64 `gorm:"not null;index:idx_msg_conversation_seq,priority:2" json:"seq"` // 会话内序号,用于排序和增量同步
|
||||
Segments MessageSegments `gorm:"type:json" json:"segments"` // 消息链(结构体数组)
|
||||
ReplyToID *string `json:"reply_to_id,omitempty"` // 回复的消息ID(string类型)
|
||||
Status MessageStatus `gorm:"type:varchar(20);default:'normal'" json:"status"` // 消息状态
|
||||
|
||||
// 新增字段:消息分类和系统消息类型
|
||||
Category MessageCategory `gorm:"type:varchar(20);default:'chat'" json:"category"` // 消息分类
|
||||
SystemType SystemMessageType `gorm:"type:varchar(30)" json:"system_type,omitempty"` // 系统消息类型
|
||||
ExtraData *ExtraData `gorm:"type:json" json:"extra_data,omitempty"` // 额外数据(JSON格式)
|
||||
|
||||
// @相关字段
|
||||
MentionUsers string `gorm:"type:text" json:"mention_users"` // @的用户ID列表,JSON数组
|
||||
MentionAll bool `gorm:"default:false" json:"mention_all"` // 是否@所有人
|
||||
|
||||
// 软删除
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
|
||||
// 时间戳
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
// SenderIDStr 返回发送者ID字符串(保持兼容性)
|
||||
func (m *Message) SenderIDStr() string {
|
||||
return m.SenderID
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前生成雪花算法ID
|
||||
func (m *Message) BeforeCreate(tx *gorm.DB) error {
|
||||
if m.ID == "" {
|
||||
id, err := utils.GetSnowflake().GenerateID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.ID = strconv.FormatInt(id, 10)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Message) TableName() string {
|
||||
return "messages"
|
||||
}
|
||||
|
||||
// IsSystemMessage 判断是否为系统消息
|
||||
func (m *Message) IsSystemMessage() bool {
|
||||
return m.SenderID == SystemSenderIDStr || m.Category == CategoryNotification || m.Category == CategoryAnnouncement
|
||||
}
|
||||
|
||||
// IsInteractionNotification 判断是否为互动通知
|
||||
func (m *Message) IsInteractionNotification() bool {
|
||||
if m.Category != CategoryNotification {
|
||||
return false
|
||||
}
|
||||
switch m.SystemType {
|
||||
case SystemTypeLikePost, SystemTypeLikeComment, SystemTypeComment,
|
||||
SystemTypeReply, SystemTypeFollow, SystemTypeMention:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
19
internal/model/message_read.go
Normal file
19
internal/model/message_read.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// MessageRead 消息已读状态
|
||||
// 记录每个用户在每个会话中的已读位置
|
||||
type MessageRead struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ConversationID int64 `gorm:"uniqueIndex:idx_conversation_user;not null" json:"conversation_id"`
|
||||
UserID uint `gorm:"uniqueIndex:idx_conversation_user;not null" json:"user_id"`
|
||||
LastReadSeq int64 `gorm:"not null" json:"last_read_seq"` // 已读到的seq位置
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
func (MessageRead) TableName() string {
|
||||
return "message_reads"
|
||||
}
|
||||
53
internal/model/notification.go
Normal file
53
internal/model/notification.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// NotificationType 通知类型
|
||||
type NotificationType string
|
||||
|
||||
const (
|
||||
NotificationTypeLikePost NotificationType = "like_post"
|
||||
NotificationTypeLikeComment NotificationType = "like_comment"
|
||||
NotificationTypeComment NotificationType = "comment"
|
||||
NotificationTypeReply NotificationType = "reply"
|
||||
NotificationTypeFollow NotificationType = "follow"
|
||||
NotificationTypeMention NotificationType = "mention"
|
||||
NotificationTypeSystem NotificationType = "system"
|
||||
)
|
||||
|
||||
// Notification 通知实体
|
||||
type Notification struct {
|
||||
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
|
||||
UserID string `json:"user_id" gorm:"type:varchar(36);not null;index:idx_notifications_user_read_created,priority:1"` // 接收者
|
||||
Type NotificationType `json:"type" gorm:"type:varchar(30);not null"`
|
||||
Title string `json:"title" gorm:"type:varchar(200);not null"`
|
||||
Content string `json:"content" gorm:"type:text"`
|
||||
Data string `json:"data" gorm:"type:jsonb"` // 相关数据(JSON)
|
||||
|
||||
// 已读状态
|
||||
IsRead bool `json:"is_read" gorm:"default:false;index:idx_notifications_user_read_created,priority:2"`
|
||||
ReadAt *time.Time `json:"read_at" gorm:"type:timestamp"`
|
||||
|
||||
// 软删除
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
|
||||
// 时间戳
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index:idx_notifications_user_read_created,priority:3,sort:desc"`
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前生成UUID
|
||||
func (n *Notification) BeforeCreate(tx *gorm.DB) error {
|
||||
if n.ID == "" {
|
||||
n.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Notification) TableName() string {
|
||||
return "notifications"
|
||||
}
|
||||
100
internal/model/post.go
Normal file
100
internal/model/post.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PostStatus 帖子状态
|
||||
type PostStatus string
|
||||
|
||||
const (
|
||||
PostStatusDraft PostStatus = "draft"
|
||||
PostStatusPending PostStatus = "pending" // 待审核
|
||||
PostStatusPublished PostStatus = "published"
|
||||
PostStatusRejected PostStatus = "rejected"
|
||||
PostStatusDeleted PostStatus = "deleted"
|
||||
)
|
||||
|
||||
// Post 帖子实体
|
||||
type Post struct {
|
||||
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
|
||||
UserID string `json:"user_id" gorm:"type:varchar(36);index;index:idx_posts_user_status_created,priority:1;not null"`
|
||||
CommunityID string `json:"community_id" gorm:"type:varchar(36);index"`
|
||||
Title string `json:"title" gorm:"type:varchar(200);not null"`
|
||||
Content string `json:"content" gorm:"type:text;not null"`
|
||||
|
||||
// 关联
|
||||
// User 需要参与缓存序列化;否则列表命中缓存后会丢失作者信息,前端退化为“匿名用户”
|
||||
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
Images []PostImage `json:"images" gorm:"foreignKey:PostID"`
|
||||
|
||||
// 审核状态
|
||||
Status PostStatus `json:"status" gorm:"type:varchar(20);default:published;index:idx_posts_status_created,priority:1;index:idx_posts_user_status_created,priority:2"`
|
||||
ReviewedAt *time.Time `json:"reviewed_at" gorm:"type:timestamp"`
|
||||
ReviewedBy string `json:"reviewed_by" gorm:"type:varchar(50)"`
|
||||
RejectReason string `json:"reject_reason" gorm:"type:varchar(500)"`
|
||||
|
||||
// 统计
|
||||
LikesCount int `json:"likes_count" gorm:"column:likes_count;default:0"`
|
||||
CommentsCount int `json:"comments_count" gorm:"column:comments_count;default:0"`
|
||||
FavoritesCount int `json:"favorites_count" gorm:"column:favorites_count;default:0"`
|
||||
SharesCount int `json:"shares_count" gorm:"column:shares_count;default:0"`
|
||||
ViewsCount int `json:"views_count" gorm:"column:views_count;default:0"`
|
||||
HotScore float64 `json:"hot_score" gorm:"column:hot_score;default:0;index:idx_posts_hot_score_created,priority:1"`
|
||||
|
||||
// 置顶/锁定
|
||||
IsPinned bool `json:"is_pinned" gorm:"default:false"`
|
||||
IsLocked bool `json:"is_locked" gorm:"default:false"`
|
||||
IsDeleted bool `json:"-" gorm:"default:false"`
|
||||
|
||||
// 投票
|
||||
IsVote bool `json:"is_vote" gorm:"column:is_vote;default:false"`
|
||||
|
||||
// 软删除
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
|
||||
// 时间戳
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index:idx_posts_status_created,priority:2,sort:desc;index:idx_posts_user_status_created,priority:3,sort:desc;index:idx_posts_hot_score_created,priority:2,sort:desc"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前生成UUID
|
||||
func (p *Post) BeforeCreate(tx *gorm.DB) error {
|
||||
if p.ID == "" {
|
||||
p.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Post) TableName() string {
|
||||
return "posts"
|
||||
}
|
||||
|
||||
// PostImage 帖子图片
|
||||
type PostImage struct {
|
||||
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
|
||||
PostID string `json:"post_id" gorm:"type:varchar(36);index;not null"`
|
||||
URL string `json:"url" gorm:"type:text;not null"`
|
||||
ThumbnailURL string `json:"thumbnail_url" gorm:"type:text"`
|
||||
Width int `json:"width" gorm:"default:0"`
|
||||
Height int `json:"height" gorm:"default:0"`
|
||||
Size int64 `json:"size" gorm:"default:0"` // 文件大小(字节)
|
||||
MimeType string `json:"mime_type" gorm:"type:varchar(50)"`
|
||||
SortOrder int `json:"sort_order" gorm:"default:0"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前生成UUID
|
||||
func (pi *PostImage) BeforeCreate(tx *gorm.DB) error {
|
||||
if pi.ID == "" {
|
||||
pi.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (PostImage) TableName() string {
|
||||
return "post_images"
|
||||
}
|
||||
129
internal/model/push_record.go
Normal file
129
internal/model/push_record.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"carrot_bbs/internal/pkg/utils"
|
||||
)
|
||||
|
||||
// PushChannel 推送通道类型
|
||||
type PushChannel string
|
||||
|
||||
const (
|
||||
PushChannelWebSocket PushChannel = "websocket" // WebSocket推送
|
||||
PushChannelFCM PushChannel = "fcm" // Firebase Cloud Messaging
|
||||
PushChannelAPNs PushChannel = "apns" // Apple Push Notification service
|
||||
PushChannelHuawei PushChannel = "huawei" // 华为推送
|
||||
)
|
||||
|
||||
// PushStatus 推送状态
|
||||
type PushStatus string
|
||||
|
||||
const (
|
||||
PushStatusPending PushStatus = "pending" // 待推送
|
||||
PushStatusPushing PushStatus = "pushing" // 推送中
|
||||
PushStatusPushed PushStatus = "pushed" // 已推送(成功发送到推送服务)
|
||||
PushStatusDelivered PushStatus = "delivered" // 已送达(客户端确认)
|
||||
PushStatusFailed PushStatus = "failed" // 推送失败
|
||||
PushStatusExpired PushStatus = "expired" // 消息过期
|
||||
)
|
||||
|
||||
// PushRecord 推送记录实体
|
||||
// 用于跟踪消息的推送状态,支持多设备推送和重试机制
|
||||
type PushRecord struct {
|
||||
ID int64 `gorm:"primaryKey;autoIncrement:false" json:"id"` // 雪花算法ID
|
||||
UserID string `gorm:"column:user_id;type:varchar(50);index;not null" json:"user_id"` // 目标用户ID (UUID格式)
|
||||
MessageID string `gorm:"index;not null;size:20" json:"message_id"` // 关联的消息ID (string类型)
|
||||
PushChannel PushChannel `gorm:"type:varchar(20);not null" json:"push_channel"` // 推送通道
|
||||
PushStatus PushStatus `gorm:"type:varchar(20);not null;default:'pending'" json:"push_status"` // 推送状态
|
||||
|
||||
// 设备信息
|
||||
DeviceToken string `gorm:"type:varchar(256)" json:"device_token,omitempty"` // 设备Token(用于手机推送)
|
||||
DeviceType string `gorm:"type:varchar(20)" json:"device_type,omitempty"` // 设备类型 (ios/android/web)
|
||||
|
||||
// 重试机制
|
||||
RetryCount int `gorm:"default:0" json:"retry_count"` // 重试次数
|
||||
MaxRetry int `gorm:"default:3" json:"max_retry"` // 最大重试次数
|
||||
|
||||
// 时间戳
|
||||
PushedAt *time.Time `json:"pushed_at,omitempty"` // 推送时间
|
||||
DeliveredAt *time.Time `json:"delivered_at,omitempty"` // 送达时间
|
||||
ExpiredAt *time.Time `gorm:"index" json:"expired_at,omitempty"` // 过期时间
|
||||
|
||||
// 错误信息
|
||||
ErrorMessage string `gorm:"type:varchar(500)" json:"error_message,omitempty"` // 错误信息
|
||||
|
||||
// 软删除
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
|
||||
// 时间戳
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前生成雪花算法ID
|
||||
func (r *PushRecord) BeforeCreate(tx *gorm.DB) error {
|
||||
if r.ID == 0 {
|
||||
id, err := utils.GetSnowflake().GenerateID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.ID = id
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (PushRecord) TableName() string {
|
||||
return "push_records"
|
||||
}
|
||||
|
||||
// CanRetry 判断是否可以重试
|
||||
func (r *PushRecord) CanRetry() bool {
|
||||
return r.RetryCount < r.MaxRetry && r.PushStatus != PushStatusDelivered && r.PushStatus != PushStatusExpired
|
||||
}
|
||||
|
||||
// IsExpired 判断是否已过期
|
||||
func (r *PushRecord) IsExpired() bool {
|
||||
if r.ExpiredAt == nil {
|
||||
return false
|
||||
}
|
||||
return time.Now().After(*r.ExpiredAt)
|
||||
}
|
||||
|
||||
// MarkPushing 标记为推送中
|
||||
func (r *PushRecord) MarkPushing() {
|
||||
r.PushStatus = PushStatusPushing
|
||||
}
|
||||
|
||||
// MarkPushed 标记为已推送
|
||||
func (r *PushRecord) MarkPushed() {
|
||||
now := time.Now()
|
||||
r.PushStatus = PushStatusPushed
|
||||
r.PushedAt = &now
|
||||
}
|
||||
|
||||
// MarkDelivered 标记为已送达
|
||||
func (r *PushRecord) MarkDelivered() {
|
||||
now := time.Now()
|
||||
r.PushStatus = PushStatusDelivered
|
||||
r.DeliveredAt = &now
|
||||
}
|
||||
|
||||
// MarkFailed 标记为推送失败
|
||||
func (r *PushRecord) MarkFailed(errMsg string) {
|
||||
r.PushStatus = PushStatusFailed
|
||||
r.ErrorMessage = errMsg
|
||||
r.RetryCount++
|
||||
}
|
||||
|
||||
// MarkExpired 标记为已过期
|
||||
func (r *PushRecord) MarkExpired() {
|
||||
r.PushStatus = PushStatusExpired
|
||||
}
|
||||
|
||||
// IncrementRetry 增加重试次数
|
||||
func (r *PushRecord) IncrementRetry() {
|
||||
r.RetryCount++
|
||||
}
|
||||
77
internal/model/sensitive_word.go
Normal file
77
internal/model/sensitive_word.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SensitiveWordLevel 敏感词级别
|
||||
type SensitiveWordLevel int
|
||||
|
||||
const (
|
||||
SensitiveWordLevelLow SensitiveWordLevel = 1 // 低危
|
||||
SensitiveWordLevelMedium SensitiveWordLevel = 2 // 中危
|
||||
SensitiveWordLevelHigh SensitiveWordLevel = 3 // 高危
|
||||
)
|
||||
|
||||
// SensitiveWordCategory 敏感词分类
|
||||
type SensitiveWordCategory string
|
||||
|
||||
const (
|
||||
SensitiveWordCategoryPolitical SensitiveWordCategory = "political" // 政治
|
||||
SensitiveWordCategoryPorn SensitiveWordCategory = "porn" // 色情
|
||||
SensitiveWordCategoryViolence SensitiveWordCategory = "violence" // 暴力
|
||||
SensitiveWordCategoryAd SensitiveWordCategory = "ad" // 广告
|
||||
SensitiveWordCategoryGamble SensitiveWordCategory = "gamble" // 赌博
|
||||
SensitiveWordCategoryFraud SensitiveWordCategory = "fraud" // 诈骗
|
||||
SensitiveWordCategoryOther SensitiveWordCategory = "other" // 其他
|
||||
)
|
||||
|
||||
// SensitiveWord 敏感词实体
|
||||
type SensitiveWord struct {
|
||||
ID string `gorm:"type:varchar(36);primaryKey"`
|
||||
Word string `gorm:"type:varchar(255);uniqueIndex;not null"`
|
||||
Category SensitiveWordCategory `gorm:"type:varchar(50);index"`
|
||||
Level SensitiveWordLevel `gorm:"type:int;default:1"`
|
||||
IsActive bool `gorm:"default:true"`
|
||||
CreatedBy string `gorm:"type:varchar(255)"`
|
||||
UpdatedBy string `gorm:"type:varchar(255)"`
|
||||
Remark string `gorm:"type:text"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前生成UUID
|
||||
func (sw *SensitiveWord) BeforeCreate(tx *gorm.DB) error {
|
||||
if sw.ID == "" {
|
||||
sw.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (SensitiveWord) TableName() string {
|
||||
return "sensitive_words"
|
||||
}
|
||||
|
||||
// SensitiveWordRequest 创建/更新敏感词请求
|
||||
type SensitiveWordRequest struct {
|
||||
Word string `json:"word" validate:"required,min=1,max=255"`
|
||||
Category SensitiveWordCategory `json:"category"`
|
||||
Level SensitiveWordLevel `json:"level"`
|
||||
Remark string `json:"remark"`
|
||||
CreatedBy string `json:"-"`
|
||||
}
|
||||
|
||||
// SensitiveWordListItem 敏感词列表项(用于列表展示)
|
||||
type SensitiveWordListItem struct {
|
||||
ID string `json:"id"`
|
||||
Word string `json:"word"`
|
||||
Category SensitiveWordCategory `json:"category"`
|
||||
Level SensitiveWordLevel `json:"level"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
33
internal/model/sticker.go
Normal file
33
internal/model/sticker.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// UserSticker 用户自定义表情
|
||||
type UserSticker struct {
|
||||
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
|
||||
UserID string `json:"user_id" gorm:"type:varchar(36);not null;index:idx_user_stickers"`
|
||||
URL string `json:"url" gorm:"type:text;not null"`
|
||||
Width int `json:"width" gorm:"default:0"`
|
||||
Height int `json:"height" gorm:"default:0"`
|
||||
SortOrder int `json:"sort_order" gorm:"default:0;index:idx_user_stickers_sort"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
// TableName 表名
|
||||
func (UserSticker) TableName() string {
|
||||
return "user_stickers"
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前生成UUID
|
||||
func (s *UserSticker) BeforeCreate(tx *gorm.DB) error {
|
||||
if s.ID == "" {
|
||||
s.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
127
internal/model/system_notification.go
Normal file
127
internal/model/system_notification.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"carrot_bbs/internal/pkg/utils"
|
||||
)
|
||||
|
||||
// SystemNotificationType 系统通知类型
|
||||
type SystemNotificationType string
|
||||
|
||||
const (
|
||||
// 互动通知
|
||||
SysNotifyLikePost SystemNotificationType = "like_post" // 点赞帖子
|
||||
SysNotifyLikeComment SystemNotificationType = "like_comment" // 点赞评论
|
||||
SysNotifyComment SystemNotificationType = "comment" // 评论
|
||||
SysNotifyReply SystemNotificationType = "reply" // 回复
|
||||
SysNotifyFollow SystemNotificationType = "follow" // 关注
|
||||
SysNotifyMention SystemNotificationType = "mention" // @提及
|
||||
SysNotifyFavoritePost SystemNotificationType = "favorite_post" // 收藏帖子
|
||||
SysNotifyLikeReply SystemNotificationType = "like_reply" // 点赞回复
|
||||
|
||||
// 系统消息
|
||||
SysNotifySystem SystemNotificationType = "system" // 系统通知
|
||||
SysNotifyAnnounce SystemNotificationType = "announce" // 系统公告
|
||||
SysNotifyGroupInvite SystemNotificationType = "group_invite" // 群邀请
|
||||
SysNotifyGroupJoinApply SystemNotificationType = "group_join_apply" // 加群申请待审批
|
||||
SysNotifyGroupJoinApproved SystemNotificationType = "group_join_approved" // 加群申请通过
|
||||
SysNotifyGroupJoinRejected SystemNotificationType = "group_join_rejected" // 加群申请拒绝
|
||||
)
|
||||
|
||||
// SystemNotificationExtra 额外数据
|
||||
type SystemNotificationExtra struct {
|
||||
// 操作者信息
|
||||
ActorID int64 `json:"actor_id,omitempty"`
|
||||
ActorIDStr string `json:"actor_id_str,omitempty"`
|
||||
ActorName string `json:"actor_name,omitempty"`
|
||||
AvatarURL string `json:"avatar_url,omitempty"`
|
||||
|
||||
// 目标信息
|
||||
TargetID string `json:"target_id,omitempty"` // 改为string类型以支持UUID
|
||||
TargetTitle string `json:"target_title,omitempty"`
|
||||
TargetType string `json:"target_type,omitempty"`
|
||||
|
||||
// 其他信息
|
||||
ActionURL string `json:"action_url,omitempty"`
|
||||
ActionTime string `json:"action_time,omitempty"`
|
||||
|
||||
// 群邀请/加群申请扩展字段
|
||||
GroupID string `json:"group_id,omitempty"`
|
||||
GroupName string `json:"group_name,omitempty"`
|
||||
GroupAvatar string `json:"group_avatar,omitempty"`
|
||||
GroupDescription string `json:"group_description,omitempty"`
|
||||
Flag string `json:"flag,omitempty"`
|
||||
RequestType string `json:"request_type,omitempty"`
|
||||
RequestStatus string `json:"request_status,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
TargetUserID string `json:"target_user_id,omitempty"`
|
||||
TargetUserName string `json:"target_user_name,omitempty"`
|
||||
TargetUserAvatar string `json:"target_user_avatar,omitempty"`
|
||||
}
|
||||
|
||||
// Value 实现driver.Valuer接口
|
||||
func (e SystemNotificationExtra) Value() (driver.Value, error) {
|
||||
return json.Marshal(e)
|
||||
}
|
||||
|
||||
// Scan 实现sql.Scanner接口
|
||||
func (e *SystemNotificationExtra) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
bytes, ok := value.([]byte)
|
||||
if !ok {
|
||||
return errors.New("type assertion to []byte failed")
|
||||
}
|
||||
return json.Unmarshal(bytes, e)
|
||||
}
|
||||
|
||||
// SystemNotification 系统通知(独立表,与消息完全分离)
|
||||
// 每个用户只能看到自己的系统通知
|
||||
type SystemNotification struct {
|
||||
ID int64 `gorm:"primaryKey;autoIncrement:false" json:"id"`
|
||||
ReceiverID string `gorm:"column:receiver_id;type:varchar(50);not null;index:idx_sys_notifications_receiver_read_created,priority:1" json:"receiver_id"` // 接收者ID (UUID)
|
||||
Type SystemNotificationType `gorm:"type:varchar(30);not null" json:"type"` // 通知类型
|
||||
Title string `gorm:"type:varchar(200)" json:"title,omitempty"` // 标题
|
||||
Content string `gorm:"type:text;not null" json:"content"` // 内容
|
||||
ExtraData *SystemNotificationExtra `gorm:"type:json" json:"extra_data,omitempty"` // 额外数据
|
||||
IsRead bool `gorm:"default:false;index:idx_sys_notifications_receiver_read_created,priority:2" json:"is_read"` // 是否已读
|
||||
ReadAt *time.Time `json:"read_at,omitempty"` // 阅读时间
|
||||
|
||||
// 软删除
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
|
||||
// 时间戳
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index:idx_sys_notifications_receiver_read_created,priority:3,sort:desc"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前生成雪花算法ID
|
||||
func (n *SystemNotification) BeforeCreate(tx *gorm.DB) error {
|
||||
if n.ID == 0 {
|
||||
id, err := utils.GetSnowflake().GenerateID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n.ID = id
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (SystemNotification) TableName() string {
|
||||
return "system_notifications"
|
||||
}
|
||||
|
||||
// MarkAsRead 标记为已读
|
||||
func (n *SystemNotification) MarkAsRead() {
|
||||
now := time.Now()
|
||||
n.IsRead = true
|
||||
n.ReadAt = &now
|
||||
}
|
||||
66
internal/model/user.go
Normal file
66
internal/model/user.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// UserStatus 用户状态
|
||||
type UserStatus string
|
||||
|
||||
const (
|
||||
UserStatusActive UserStatus = "active"
|
||||
UserStatusBanned UserStatus = "banned"
|
||||
UserStatusInactive UserStatus = "inactive"
|
||||
)
|
||||
|
||||
// User 用户实体
|
||||
type User struct {
|
||||
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
|
||||
Username string `json:"username" gorm:"type:varchar(50);uniqueIndex;not null"`
|
||||
Nickname string `json:"nickname" gorm:"type:varchar(100);not null"`
|
||||
Email *string `json:"email" gorm:"type:varchar(255);uniqueIndex"`
|
||||
Phone *string `json:"phone" gorm:"type:varchar(20);uniqueIndex"`
|
||||
EmailVerified bool `json:"email_verified" gorm:"default:false"`
|
||||
PasswordHash string `json:"-" gorm:"type:varchar(255);not null"`
|
||||
Avatar string `json:"avatar" gorm:"type:text"`
|
||||
CoverURL string `json:"cover_url" gorm:"type:text"` // 头图URL
|
||||
Bio string `json:"bio" gorm:"type:text"`
|
||||
Website string `json:"website" gorm:"type:varchar(255)"`
|
||||
Location string `json:"location" gorm:"type:varchar(100)"`
|
||||
|
||||
// 实名认证信息(可选)
|
||||
RealName string `json:"real_name" gorm:"type:varchar(100)"` // 真实姓名
|
||||
IDCard string `json:"-" gorm:"type:varchar(18)"` // 身份证号(加密存储)
|
||||
IsVerified bool `json:"is_verified" gorm:"default:false"` // 是否实名认证
|
||||
VerifiedAt *time.Time `json:"verified_at" gorm:"type:timestamp"`
|
||||
|
||||
// 统计计数
|
||||
PostsCount int `json:"posts_count" gorm:"default:0"`
|
||||
FollowersCount int `json:"followers_count" gorm:"default:0"`
|
||||
FollowingCount int `json:"following_count" gorm:"default:0"`
|
||||
|
||||
// 状态
|
||||
Status UserStatus `json:"status" gorm:"type:varchar(20);default:active"`
|
||||
LastLoginAt *time.Time `json:"last_login_at" gorm:"type:timestamp"`
|
||||
LastLoginIP string `json:"last_login_ip" gorm:"type:varchar(45)"`
|
||||
|
||||
// 时间戳
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前生成UUID
|
||||
func (u *User) BeforeCreate(tx *gorm.DB) error {
|
||||
if u.ID == "" {
|
||||
u.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (User) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
27
internal/model/user_block.go
Normal file
27
internal/model/user_block.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// UserBlock 用户拉黑关系
|
||||
type UserBlock struct {
|
||||
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
|
||||
BlockerID string `json:"blocker_id" gorm:"type:varchar(36);index;not null;uniqueIndex:idx_blocker_blocked"` // 拉黑人
|
||||
BlockedID string `json:"blocked_id" gorm:"type:varchar(36);index;not null;uniqueIndex:idx_blocker_blocked"` // 被拉黑人
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
}
|
||||
|
||||
func (b *UserBlock) BeforeCreate(tx *gorm.DB) error {
|
||||
if b.ID == "" {
|
||||
b.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (UserBlock) TableName() string {
|
||||
return "user_blocks"
|
||||
}
|
||||
52
internal/model/vote.go
Normal file
52
internal/model/vote.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// VoteOption 投票选项
|
||||
type VoteOption struct {
|
||||
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
|
||||
PostID string `json:"post_id" gorm:"type:varchar(36);index:idx_vote_option_post_sort,priority:1;not null"`
|
||||
Content string `json:"content" gorm:"type:varchar(200);not null"`
|
||||
SortOrder int `json:"sort_order" gorm:"default:0;index:idx_vote_option_post_sort,priority:2"`
|
||||
VotesCount int `json:"votes_count" gorm:"default:0"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前生成UUID
|
||||
func (vo *VoteOption) BeforeCreate(tx *gorm.DB) error {
|
||||
if vo.ID == "" {
|
||||
vo.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (VoteOption) TableName() string {
|
||||
return "vote_options"
|
||||
}
|
||||
|
||||
// UserVote 用户投票记录
|
||||
type UserVote struct {
|
||||
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
|
||||
PostID string `json:"post_id" gorm:"type:varchar(36);index;uniqueIndex:idx_user_vote_post_user,priority:1;not null"`
|
||||
UserID string `json:"user_id" gorm:"type:varchar(36);index;uniqueIndex:idx_user_vote_post_user,priority:2;not null"`
|
||||
OptionID string `json:"option_id" gorm:"type:varchar(36);index;not null"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前生成UUID
|
||||
func (uv *UserVote) BeforeCreate(tx *gorm.DB) error {
|
||||
if uv.ID == "" {
|
||||
uv.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (UserVote) TableName() string {
|
||||
return "user_votes"
|
||||
}
|
||||
Reference in New Issue
Block a user