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:
2026-03-09 21:28:58 +08:00
commit 4d8f2ec997
102 changed files with 25022 additions and 0 deletions

118
internal/model/audit_log.go Normal file
View 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
View 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"
}

View 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"` // 雪花算法IDstring类型
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"
}

View 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"` // 推送TokenFCM/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
}

View 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
View 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
View 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"
}

View 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"
}

View 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"
}

View 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
View 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
View 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
View 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 消息实体
// 使用雪花算法IDstring类型和seq机制实现消息排序和增量同步
type Message struct {
ID string `gorm:"primaryKey;size:20" json:"id"` // 雪花算法IDstring类型
ConversationID string `gorm:"not null;size:20;index:idx_msg_conversation_seq,priority:1" json:"conversation_id"` // 会话IDstring类型
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"` // 回复的消息IDstring类型
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
}
}

View 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"
}

View 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
View 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"
}

View 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++
}

View 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
View 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
}

View 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
View 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"
}

View 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
View 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"
}