- 新增附件模型与上传服务,拆分头像上传/管理逻辑 - 重构用户资料接口,移除直接修改头像字段,改用专用上传接口 - 添加站点公开设置API与管理员外观配置页 - 全局替换hover背景为accent-soft,优化交互一致性 - 完善头像组件,支持图片加载失败回退与src更新重置 - 新增用户状态事件总线,解决同页头像更新不及时问题 - 调整依赖包顺序与CI依赖版本 - 重构板块列表接口,新增帖子计数 - 迁移账号设置到用户主页tab,合并重复代码
198 lines
9.2 KiB
Go
198 lines
9.2 KiB
Go
package model
|
||
|
||
import (
|
||
"time"
|
||
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// Role 用户角色
|
||
type Role string
|
||
|
||
const (
|
||
RoleUser Role = "user"
|
||
RoleAdmin Role = "admin"
|
||
)
|
||
|
||
// 内容审核状态
|
||
const (
|
||
ContentStatusPending = "pending"
|
||
ContentStatusPublished = "published"
|
||
ContentStatusRejected = "rejected"
|
||
)
|
||
|
||
// 帖子类型
|
||
const (
|
||
PostTypeNormal = "normal"
|
||
PostTypeQuestion = "question"
|
||
)
|
||
|
||
// 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"`
|
||
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:normal;index" json:"post_type"`
|
||
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"`
|
||
}
|
||
|
||
// 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" // 点赞了你的帖子
|
||
)
|
||
|
||
// 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
|
||
PostID uint `gorm:"index;not null" json:"post_id"` // 关联帖子
|
||
CommentID uint `gorm:"index" json:"comment_id"` // 关联评论(点赞时为 0)
|
||
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"`
|
||
}
|
||
|
||
// 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"`
|
||
}
|