package model import ( "time" "gorm.io/gorm" ) // Role 用户角色(等级递增:普通用户 < 板块管理员 < 管理员 < 超级管理员 < 站长) type Role string const ( RoleUser Role = "user" // 普通用户:发帖/评论需审核 RoleBoardAdmin Role = "board_admin" // 板块管理员:仅管理被授权板块的帖子与评论 RoleAdmin Role = "admin" // 管理员:公告 + 全站帖子/评论审核 RoleSuperAdmin Role = "super_admin" // 超级管理员:全站后台(用户/公告/设置/内容),但不可操作站长 RoleOwner Role = "owner" // 站长:最高权限,全站唯一,任何人(含自己)不可改角色/封禁 ) // RoleLevel 角色等级,用于层级比较;未知角色按普通用户处理 func RoleLevel(r Role) int { switch r { case RoleOwner: return 100 case RoleSuperAdmin: return 80 case RoleAdmin: return 50 case RoleBoardAdmin: return 30 default: return 0 } } // IsStaff 是否为任一管理角色(板块管理员及以上) func IsStaff(r Role) bool { return RoleLevel(r) >= RoleLevel(RoleBoardAdmin) } // ValidRole 角色枚举校验 func ValidRole(r Role) bool { switch r { case RoleUser, RoleBoardAdmin, RoleAdmin, RoleSuperAdmin, RoleOwner: return true } return false } // 内容审核状态 const ( ContentStatusPending = "pending" ContentStatusPublished = "published" ContentStatusRejected = "rejected" ) // 帖子类型(discussion 为默认;兼容旧值 normal → discussion) const ( PostTypeDiscussion = "discussion" // 讨论(默认) PostTypeQuestion = "question" // 问答 PostTypePoll = "poll" // 投票 PostTypeBounty = "bounty" // 悬赏 PostTypeLottery = "lottery" // 抽奖 PostTypeNormal = "normal" // 旧值,读取时归一为 discussion ) // ValidPostType 发帖类型白名单 func ValidPostType(t string) bool { switch t { case PostTypeDiscussion, PostTypeQuestion, PostTypePoll, PostTypeBounty, PostTypeLottery: return true } return false } // NormalizePostType 归一化类型(空/旧 normal → discussion) func NormalizePostType(t string) string { if t == "" || t == PostTypeNormal { return PostTypeDiscussion } if ValidPostType(t) { return t } return PostTypeDiscussion } // 正文可见性 const ( ContentAccessPublic = "public" // 公开 ContentAccessLogin = "login" // 登录可见 ContentAccessReply = "reply" // 回复可见 ContentAccessPoints = "points" // 积分购买可见 ) // ValidContentAccess 可见性白名单 func ValidContentAccess(a string) bool { switch a { case ContentAccessPublic, ContentAccessLogin, ContentAccessReply, ContentAccessPoints: return true } return false } // NormalizeContentAccess 归一化可见性 func NormalizeContentAccess(a string) string { if a == "" { return ContentAccessPublic } if ValidContentAccess(a) { return a } return ContentAccessPublic } // User 用户表 type User struct { ID uint `gorm:"primaryKey" json:"id"` Username string `gorm:"uniqueIndex;size:128;not null" json:"username"` Email string `gorm:"index;size:128;default:''" json:"-"` Password string `gorm:"size:128;not null" json:"-"` Nickname string `gorm:"size:64" json:"nickname"` Avatar string `gorm:"size:512" json:"avatar"` Signature string `gorm:"size:255;default:''" json:"signature"` // 个性签名 Role Role `gorm:"size:16;default:user" json:"role"` Banned bool `gorm:"default:false" json:"banned"` Points int `gorm:"not null;default:0" json:"points"` // 可用积分余额 TokenVersion int `gorm:"default:0" json:"-"` // token 版本号,改密码/封禁时递增使旧 JWT 失效 LastSeenAt *time.Time `gorm:"index" json:"-"` // 最近活跃时间(在线统计,限频更新) CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } // RefreshToken 刷新令牌表(支持服务端撤销、一次性轮转与盗用检测) // - TokenHash: token 的 SHA-256,数据库不存明文 // - TokenCipher: token 的 AES-GCM 密文,仅存在于"当前有效"行;轮转后旧行立即抹除。 // 保留它是为了在轮转宽限期内把【同一个】新 token 返回给并发重放请求(见 service 层) // - RotatedTo/RotatedAt: 轮转链,用于宽限重放判定与盗用(吊销后重放)杀全家族 type RefreshToken struct { ID uint `gorm:"primaryKey" json:"id"` UserID uint `gorm:"index;not null" json:"user_id"` TokenHash string `gorm:"uniqueIndex;size:64;not null" json:"-"` TokenCipher string `gorm:"size:512;default:''" json:"-"` ExpiresAt time.Time `gorm:"index;not null" json:"expires_at"` Revoked bool `gorm:"default:false;index" json:"revoked"` RotatedTo uint `gorm:"index;default:0" json:"-"` RotatedAt *time.Time `gorm:"index" json:"-"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // Board 论坛板块 type Board struct { ID uint `gorm:"primaryKey" json:"id"` Name string `gorm:"size:64;not null" json:"name"` Description string `gorm:"size:512" json:"description"` Icon string `gorm:"size:64;default:''" json:"icon"` ColorIndex int `gorm:"default:-1" json:"color_index"` SortOrder int `gorm:"default:0" json:"sort_order"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } // Post 帖子 type Post struct { ID uint `gorm:"primaryKey" json:"id"` BoardID uint `gorm:"index;not null" json:"board_id"` UserID uint `gorm:"index;not null" json:"user_id"` Title string `gorm:"size:256;not null" json:"title"` Content string `gorm:"type:text;not null" json:"content"` Tags string `gorm:"size:256" json:"tags"` PostType string `gorm:"size:16;default:discussion;index" json:"post_type"` ContentAccess string `gorm:"size:16;default:public;index" json:"content_access"` // public|login|reply|points AccessPoints int `gorm:"not null;default:0" json:"access_points"` // points 可见时所需积分 TypeMeta string `gorm:"type:text;default:''" json:"type_meta"` // 类型扩展 JSON(投票/悬赏/抽奖壳) Pinned int `gorm:"default:0" json:"pinned"` Recommended bool `gorm:"default:false;index" json:"recommended"` Status string `gorm:"size:16;default:published;index" json:"status"` LikeCount int `gorm:"default:0" json:"like_count"` ViewCount int `gorm:"default:0" json:"view_count"` CommentCount int `gorm:"default:0" json:"comment_count"` Liked bool `gorm:"-" json:"liked"` // 当前用户是否已点赞(展示字段,不入库) CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` Board Board `gorm:"foreignKey:BoardID" json:"board,omitempty"` User User `gorm:"foreignKey:UserID" json:"user,omitempty"` } // PointLedger 积分流水(余额以 User.Points 为准,本表可审计) type PointLedger struct { ID uint `gorm:"primaryKey" json:"id"` UserID uint `gorm:"index;not null" json:"user_id"` Delta int `gorm:"not null" json:"delta"` // 正入负出 Balance int `gorm:"not null" json:"balance"` // 变动后余额 Reason string `gorm:"size:32;not null;index" json:"reason"` RefType string `gorm:"size:32;not null;default:''" json:"ref_type"` RefID uint `gorm:"not null;default:0" json:"ref_id"` Note string `gorm:"size:256;not null;default:''" json:"note"` CreatedAt time.Time `gorm:"index" json:"created_at"` } // 积分流水原因 const ( PointReasonCheckin = "checkin" PointReasonUnlockPost = "unlock_post" PointReasonDownloadFile = "download_file" PointReasonMigrateCheckin = "migrate_checkin" PointReasonBountyEscrow = "bounty_escrow" PointReasonBountyRefund = "bounty_refund" PointReasonBountyAward = "bounty_award" ) // PostPollVote 投票记录(多选时同一用户多行) type PostPollVote struct { ID uint `gorm:"primaryKey" json:"id"` PostID uint `gorm:"uniqueIndex:idx_poll_vote;not null" json:"post_id"` UserID uint `gorm:"uniqueIndex:idx_poll_vote;not null" json:"user_id"` OptionIndex int `gorm:"uniqueIndex:idx_poll_vote;not null" json:"option_index"` CreatedAt time.Time `json:"created_at"` } // PostLotteryEntry 抽奖报名 type PostLotteryEntry struct { ID uint `gorm:"primaryKey" json:"id"` PostID uint `gorm:"uniqueIndex:idx_lottery_entry;not null" json:"post_id"` UserID uint `gorm:"uniqueIndex:idx_lottery_entry;not null" json:"user_id"` CreatedAt time.Time `json:"created_at"` } // PostContentUnlock 积分购买正文解锁记录 type PostContentUnlock struct { ID uint `gorm:"primaryKey" json:"id"` PostID uint `gorm:"uniqueIndex:idx_post_unlock_user;not null" json:"post_id"` UserID uint `gorm:"uniqueIndex:idx_post_unlock_user;not null" json:"user_id"` Points int `gorm:"not null;default:0" json:"points"` CreatedAt time.Time `json:"created_at"` } // PostAttachment 帖子文件附件(不走公开静态目录,经 API 鉴权下载) type PostAttachment struct { ID uint `gorm:"primaryKey" json:"id"` PostID uint `gorm:"index;not null;default:0" json:"post_id"` // 0=草稿未绑定 UserID uint `gorm:"index;not null" json:"user_id"` Name string `gorm:"size:256;not null" json:"name"` // 原始文件名 StoredName string `gorm:"size:64;not null" json:"-"` // 磁盘文件名 MIME string `gorm:"size:128;not null;default:application/octet-stream" json:"mime"` Size int `gorm:"not null;default:0" json:"size"` PricePoints int `gorm:"not null;default:0" json:"price_points"` // 0=免费 DownloadCount int `gorm:"not null;default:0" json:"download_count"` CreatedAt time.Time `json:"created_at"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } // PostAttachmentUnlock 积分附件下载解锁(按附件计费一次) type PostAttachmentUnlock struct { ID uint `gorm:"primaryKey" json:"id"` AttachmentID uint `gorm:"uniqueIndex:idx_att_unlock_user;not null" json:"attachment_id"` UserID uint `gorm:"uniqueIndex:idx_att_unlock_user;not null" json:"user_id"` Points int `gorm:"not null;default:0" json:"points"` CreatedAt time.Time `json:"created_at"` } // Comment 评论(主评论 = 楼层,ParentID 为空;子评论挂 root_id 对应楼层下) type Comment struct { ID uint `gorm:"primaryKey" json:"id"` PostID uint `gorm:"index;not null" json:"post_id"` UserID uint `gorm:"index;not null" json:"user_id"` ParentID *uint `gorm:"index" json:"parent_id"` // 父评论 ID,NULL = 主评论(楼层) RootID *uint `gorm:"index" json:"root_id"` // 所属楼层(顶层主评论)ID,子评论必填 Depth int `gorm:"not null;default:0" json:"depth"` // 层级:主评论 0,子评论 = 父 + 1 Content string `gorm:"type:text;not null" json:"content"` Status string `gorm:"size:16;default:published;index" json:"status"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` User User `gorm:"foreignKey:UserID" json:"user,omitempty"` } // Like 点赞记录(联合唯一索引防止重复点赞) type Like struct { ID uint `gorm:"primaryKey" json:"id"` PostID uint `gorm:"uniqueIndex:idx_post_user;not null" json:"post_id"` UserID uint `gorm:"uniqueIndex:idx_post_user;not null" json:"user_id"` CreatedAt time.Time `json:"created_at"` } // Checkin 每日签到记录(用户+日期联合唯一,防止重复签到) type Checkin struct { ID uint `gorm:"primaryKey" json:"id"` UserID uint `gorm:"uniqueIndex:idx_checkin_user_date;not null" json:"user_id"` CheckinDate time.Time `gorm:"type:date;uniqueIndex:idx_checkin_user_date;not null" json:"checkin_date"` Points int `gorm:"not null;default:5" json:"points"` // 当日所得积分 CreatedAt time.Time `json:"created_at"` } // 通知类型 const ( NotificationTypeComment = "comment" // 评论了你的帖子 NotificationTypeReply = "reply" // 回复了你的评论 NotificationTypeLike = "like" // 点赞了你的帖子 NotificationTypeApproved = "approved" // 内容审核通过 NotificationTypeRejected = "rejected" // 内容审核未通过 NotificationTypeMention = "mention" // 群聊中 @ 了你(PostID=0,用 RoomID/MessageID) NotificationTypePendingReview = "pending_review" // 有新内容待审核(发给可审者) ) // Notification 站内通知 type Notification struct { ID uint `gorm:"primaryKey" json:"id"` UserID uint `gorm:"index;not null" json:"user_id"` // 接收通知的用户 ActorID uint `gorm:"not null" json:"actor_id"` // 触发通知的用户 Type string `gorm:"size:16;not null;index" json:"type"` // comment | like | mention ... PostID uint `gorm:"index;not null;default:0" json:"post_id"` // 关联帖子(mention 类为 0) CommentID uint `gorm:"index" json:"comment_id"` // 关联评论(点赞时为 0) RoomID uint `gorm:"index;not null;default:0" json:"room_id"` // 关联群聊(mention) MessageID uint `gorm:"not null;default:0" json:"message_id"` // 关联群聊消息(mention) Content string `gorm:"size:256" json:"content"` // 内容预览 IsRead bool `gorm:"default:false;index" json:"is_read"` CreatedAt time.Time `json:"created_at"` Actor User `gorm:"foreignKey:ActorID" json:"actor,omitempty"` Post Post `gorm:"foreignKey:PostID" json:"post,omitempty"` Room *ChatRoom `gorm:"foreignKey:RoomID;constraint:-" json:"room,omitempty"` } // Announcement 站点公告(后台文章管理:支持草稿、标签与标签预设色) type Announcement struct { ID uint `gorm:"primaryKey" json:"id"` Title string `gorm:"size:200;not null" json:"title"` Content string `gorm:"type:text;not null" json:"content"` Tag string `gorm:"size:32;not null;default:公告" json:"tag"` TagColor string `gorm:"size:16;not null;default:blue" json:"tag_color"` // blue/green/orange/red/purple/gray Published bool `gorm:"not null;index" json:"published"` // 显式写入 false;不可用 default:true,否则草稿零值会被 GORM 省略而落成已发布 CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } // SiteSetting 站点级键值设置(如历史在线峰值 peak_online) type SiteSetting struct { Key string `gorm:"primaryKey;size:64" json:"key"` Value string `gorm:"size:255;not null;default:''" json:"value"` UpdatedAt time.Time `json:"updated_at"` } // 附件类型 const ( AttachmentKindAvatar = "avatar" // 用户头像(正方形 WebP) AttachmentKindImage = "image" // 通用图片(帖子插图等,媒体库统一管理) ) // Attachment 用户上传的附件(当前仅头像,全部为 WebP) type Attachment struct { ID uint `gorm:"primaryKey" json:"id"` UserID uint `gorm:"index:idx_attachment_user_kind,priority:1;not null" json:"user_id"` Kind string `gorm:"size:16;index:idx_attachment_user_kind,priority:2;not null" json:"kind"` URL string `gorm:"size:512;not null" json:"url"` MIME string `gorm:"size:32;not null;default:image/webp" json:"mime"` Size int `gorm:"not null;default:0" json:"size"` // 裁剪后文件字节数(大小限制以此为准) Width int `gorm:"not null;default:0" json:"width"` Height int `gorm:"not null;default:0" json:"height"` CreatedAt time.Time `gorm:"index" json:"created_at"` } // UserBoard 板块管理员的板块授权(多对多;仅 role=board_admin 的行生效) type UserBoard struct { ID uint `gorm:"primaryKey" json:"id"` UserID uint `gorm:"uniqueIndex:idx_user_board;not null" json:"user_id"` BoardID uint `gorm:"uniqueIndex:idx_user_board;not null" json:"board_id"` CreatedAt time.Time `json:"created_at"` User User `gorm:"foreignKey:UserID" json:"-"` Board Board `gorm:"foreignKey:BoardID" json:"board,omitempty"` } // LoginLog 登录历史(成功与失败都记录;失败时用户名可能不存在,UserID 为 0) type LoginLog struct { ID uint `gorm:"primaryKey" json:"id"` UserID uint `gorm:"index;not null;default:0" json:"user_id"` Username string `gorm:"size:128;index;not null;default:''" json:"username"` IP string `gorm:"size:45;not null;default:''" json:"ip"` // IPv4/IPv6 最长 45 字符 UserAgent string `gorm:"size:512;not null;default:''" json:"user_agent"` Success bool `gorm:"index;not null;default:false" json:"success"` CreatedAt time.Time `gorm:"index" json:"created_at"` } // 群聊成员角色 const ( ChatRoleOwner = "owner" // 群主(建群者,每群唯一):改名/解散/踢人/禁言 ChatRoleMember = "member" // 普通成员 ) // ChatRoom 会话房间:群聊或一对一私聊(RoomType);IsDefault 为全站大厅 type ChatRoom struct { ID uint `gorm:"primaryKey" json:"id"` Name string `gorm:"size:64;not null;default:''" json:"name"` Description string `gorm:"size:256;not null;default:''" json:"description"` OwnerID uint `gorm:"index;not null" json:"owner_id"` RoomType string `gorm:"size:16;not null;default:group;index" json:"room_type"` // group | direct DirectKey string `gorm:"size:64;not null;default:'';index" json:"-"` // 私聊双方 ID 排序键 "min:max" IsPrivate bool `gorm:"not null;default:false;index" json:"is_private"` IsDefault bool `gorm:"not null;default:false;index" json:"is_default"` MemberCount int `gorm:"not null;default:0" json:"member_count"` LastMessageID uint `gorm:"index;not null;default:0" json:"last_message_id"` // 最后一条消息(列表排序/未读基准) CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` Owner User `gorm:"foreignKey:OwnerID" json:"owner,omitempty"` LastMessage *ChatMessage `gorm:"foreignKey:LastMessageID;constraint:-" json:"last_message,omitempty"` } const ( ChatRoomTypeGroup = "group" ChatRoomTypeDirect = "direct" ) // ChatRoomMember 群成员(房间+用户联合唯一;LastReadMessageID 为未读对账水位) type ChatRoomMember struct { ID uint `gorm:"primaryKey" json:"id"` RoomID uint `gorm:"uniqueIndex:idx_chat_member;index;not null" json:"room_id"` UserID uint `gorm:"uniqueIndex:idx_chat_member;not null" json:"user_id"` Role string `gorm:"size:16;not null;default:member" json:"role"` LastReadMessageID uint `gorm:"not null;default:0" json:"last_read_message_id"` Muted bool `gorm:"not null;default:false" json:"muted"` // 被群主禁言 PinnedAt *time.Time `json:"pinned_at,omitempty"` // 该用户个人置顶时间;大厅强制置顶不依赖此字段 CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` User User `gorm:"foreignKey:UserID" json:"user,omitempty"` } // ChatMessage 群聊消息(先落库再经 WS 广播,WS 不承担可靠投递;撤回保留行供审计) type ChatMessage struct { ID uint `gorm:"primaryKey" json:"id"` RoomID uint `gorm:"index:idx_chat_room_created,priority:1;not null" json:"room_id"` SenderID uint `gorm:"index;not null" json:"sender_id"` Content string `gorm:"type:varchar(2000);not null" json:"content"` ReplyToID uint `gorm:"not null;default:0;index" json:"reply_to_id"` // 引用的消息 ID,0 表示无引用 ReplySnap string `gorm:"size:512;not null;default:''" json:"reply_snap"` // 引用快照「昵称: 摘要」,原文撤回后仍可展示 MentionIDs string `gorm:"size:512;not null;default:''" json:"-"` // 被@用户 ID 逗号分隔(落通知用) RecalledAt *time.Time `json:"recalled_at,omitempty"` RecalledBy uint `gorm:"not null;default:0" json:"recalled_by"` CreatedAt time.Time `gorm:"index:idx_chat_room_created,priority:2" json:"created_at"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` Sender User `gorm:"foreignKey:SenderID" json:"sender,omitempty"` // RecalledBy=0 时不建 FK;Preload 仅用于已撤回消息展示撤回者昵称 Recaller *User `gorm:"foreignKey:RecalledBy;constraint:-" json:"recaller,omitempty"` }