refactor: Gitea 式目录改组,移除本分支 SPA 与杂项产物

将 model/service/handler/middleware 迁至 models/services/routers/api/modules/auth,并删除 frontend、embed_static、scripts 及误入库缓存/二进制。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-29 03:54:56 +08:00
parent 1414c71dec
commit 9fe299a45f
449 changed files with 0 additions and 52779 deletions

111
models/db.go Normal file
View File

@@ -0,0 +1,111 @@
package model
import (
"fmt"
"log"
"os"
"path/filepath"
"github.com/glebarez/sqlite" // 纯 Go支持 CGO_ENABLED=0 交叉编译
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var DB *gorm.DB
// InitDB 初始化 SQLite 并自动迁移
func InitDB(dbPath string) error {
dir := filepath.Dir(dbPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("创建数据库目录失败: %w", err)
}
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
Logger: logger.Default.LogMode(logger.Warn),
})
if err != nil {
return fmt.Errorf("连接 SQLite 失败: %w", err)
}
sqlDB, err := db.DB()
if err != nil {
return err
}
sqlDB.SetMaxOpenConns(1)
if err := db.AutoMigrate(
&User{}, &Board{}, &Post{}, &Comment{},
&PostLike{}, &CommentLike{}, &PostFavorite{}, &PostRevision{}, &CommentRevision{}, &ForumSetting{},
&OAuthClient{}, &OAuthAuthCode{},
&GiteaRepo{},
&PrivateMessage{}, &PostReport{}, &FriendLinkApply{},
&Media{},
&PointLedger{}, &CheckIn{}, &LotteryDraw{}, &PostContentUnlock{},
&BadgeDef{}, &UserBadge{},
&SitePage{}, &Poll{}, &PollOption{}, &PollVote{}, &PostLotteryWinner{},
); err != nil {
return fmt.Errorf("自动迁移失败: %w", err)
}
// 存量数据默认视为已公开,避免升级后内容全部进入待审
_ = db.Model(&Post{}).Where("status = '' OR status IS NULL").Update("status", ContentStatusPublished).Error
_ = db.Model(&Comment{}).Where("status = '' OR status IS NULL").Update("status", ContentStatusPublished).Error
_ = db.Model(&Post{}).Where("post_type = '' OR post_type IS NULL").Update("post_type", PostTypeNormal).Error
DB = db
seedDefaultBadges(db)
backfillUserExp(db)
log.Println("[model] SQLite 数据库初始化完成:", dbPath)
return nil
}
// PingDB 检测数据库连接是否可用(供健康检查使用)
func PingDB() error {
if DB == nil {
return fmt.Errorf("数据库未初始化")
}
sqlDB, err := DB.DB()
if err != nil {
return err
}
return sqlDB.Ping()
}
// seedDefaultBadges 写入内置自动徽章(已存在则跳过)
func seedDefaultBadges(db *gorm.DB) {
defs := []BadgeDef{
{Code: "tenure_30", Name: "初来乍到", Description: "注册满 30 天", Icon: "calendar", Kind: BadgeKindAuto, Metric: BadgeMetricTenureDays, Threshold: 30, SortOrder: 10, Enabled: true},
{Code: "tenure_365", Name: "资深居民", Description: "注册满 365 天", Icon: "calendar-heart", Kind: BadgeKindAuto, Metric: BadgeMetricTenureDays, Threshold: 365, SortOrder: 20, Enabled: true},
{Code: "likes_10", Name: "小有人气", Description: "帖子获赞累计 10", Icon: "heart", Kind: BadgeKindAuto, Metric: BadgeMetricLikesReceived, Threshold: 10, SortOrder: 30, Enabled: true},
{Code: "likes_100", Name: "人气作者", Description: "帖子获赞累计 100", Icon: "heart-handshake", Kind: BadgeKindAuto, Metric: BadgeMetricLikesReceived, Threshold: 100, SortOrder: 40, Enabled: true},
{Code: "likes_1000", Name: "人气巨星", Description: "帖子获赞累计 1000", Icon: "flame", Kind: BadgeKindAuto, Metric: BadgeMetricLikesReceived, Threshold: 1000, SortOrder: 50, Enabled: true},
{Code: "income_100", Name: "小有进账", Description: "创作分成累计 100 积分", Icon: "coins", Kind: BadgeKindAuto, Metric: BadgeMetricCreatorIncome, Threshold: 100, SortOrder: 60, Enabled: true},
{Code: "income_1000", Name: "创作达人", Description: "创作分成累计 1000 积分", Icon: "gem", Kind: BadgeKindAuto, Metric: BadgeMetricCreatorIncome, Threshold: 1000, SortOrder: 70, Enabled: true},
}
for _, d := range defs {
var n int64
db.Model(&BadgeDef{}).Where("code = ?", d.Code).Count(&n)
if n == 0 {
_ = db.Create(&d).Error
}
}
}
// backfillUserExp 对 Exp 仍为 0 的用户按存量公开内容粗算经验(仅补一次量级)
func backfillUserExp(db *gorm.DB) {
var users []User
if err := db.Select("id", "exp").Where("exp = 0").Find(&users).Error; err != nil {
return
}
for _, u := range users {
var posts, comments int64
var likeSum int64
_ = db.Model(&Post{}).Where("user_id = ? AND status = ?", u.ID, ContentStatusPublished).Count(&posts).Error
_ = db.Model(&Comment{}).Where("user_id = ? AND status = ?", u.ID, ContentStatusPublished).Count(&comments).Error
_ = db.Model(&Post{}).Select("COALESCE(SUM(like_count), 0)").Where("user_id = ? AND status = ?", u.ID, ContentStatusPublished).Scan(&likeSum).Error
exp := int(posts)*10 + int(comments)*2 + int(likeSum)
if exp > 0 {
_ = db.Model(&User{}).Where("id = ? AND exp = 0", u.ID).Update("exp", exp).Error
}
}
}

27
models/gitea.go Normal file
View File

@@ -0,0 +1,27 @@
package model
import "time"
// GiteaRepo 从 Gitea 同步的公开仓库缓存(侧栏 /projects 读取)
type GiteaRepo struct {
ID uint `gorm:"primaryKey" json:"id"`
GiteaID int64 `gorm:"uniqueIndex;not null" json:"gitea_id"`
OwnerLogin string `gorm:"size:128;not null;index" json:"owner_login"`
Name string `gorm:"size:255;not null" json:"name"`
FullName string `gorm:"size:512;not null" json:"full_name"`
Description string `gorm:"size:2048;default:''" json:"description"`
HTMLURL string `gorm:"size:1024;not null" json:"html_url"`
Language string `gorm:"size:64;default:''" json:"language"`
StarsCount int `gorm:"default:0" json:"stars_count"`
ForksCount int `gorm:"default:0" json:"forks_count"`
Private bool `gorm:"default:false" json:"private"`
UpdatedAtRemote *time.Time `json:"updated_at_remote"`
ForumUserID *uint `gorm:"index" json:"forum_user_id"`
SyncedAt time.Time `json:"synced_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (GiteaRepo) TableName() string {
return "gitea_repos"
}

34
models/level.go Normal file
View File

@@ -0,0 +1,34 @@
package model
// LevelThresholds 各等级所需最低 Exp下标 0 对应 Lv1
var LevelThresholds = []int{0, 20, 50, 100, 200, 400, 800, 1500, 3000, 5000}
// LevelFromExp 由经验推导等级110
func LevelFromExp(exp int) int {
if exp < 0 {
exp = 0
}
level := 1
for i, th := range LevelThresholds {
if exp >= th {
level = i + 1
}
}
return level
}
// ExpForLevel 某等级的门槛 Exp超出范围则钳制
func ExpForLevel(level int) int {
if level < 1 {
level = 1
}
if level > len(LevelThresholds) {
level = len(LevelThresholds)
}
return LevelThresholds[level-1]
}
// MaxLevel 最高等级
func MaxLevel() int {
return len(LevelThresholds)
}

467
models/models.go Normal file
View File

@@ -0,0 +1,467 @@
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" // 问答(未解决 / 已解决)
PostTypePoll = "poll" // 投票
PostTypeBounty = "bounty" // 悬赏
PostTypeLottery = "lottery" // 抽奖
)
// 悬赏状态
const (
BountyStatusOpen = "open"
BountyStatusAwarded = "awarded"
BountyStatusRefunded = "refunded"
)
// 帖内抽奖状态
const (
PostLotteryStatusOpen = "open"
PostLotteryStatusDrawn = "drawn"
)
// User 用户表
// Email / Password / LastLogin* / LastAccessAt 默认不随帖子等嵌套 User 序列化;
// 个人中心与后台列表请用 UserSelf / UserAdmin。
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"`
Signature string `gorm:"size:512;default:''" json:"signature"` // 个人签名
Avatar string `gorm:"size:512" json:"avatar"` // 兼容 CDN / S3 较长绝对 URL
Role Role `gorm:"size:16;default:user" json:"role"`
Verified bool `gorm:"default:false;index" json:"verified"` // 站长认证:免审发帖/评论
Exp int `gorm:"default:0" json:"exp"` // 经验(不可消费)
Points int `gorm:"default:0" json:"points"` // 可用积分
CreatorIncomeTotal int `gorm:"default:0" json:"creator_income_total"` // 累计创作分成
Banned bool `gorm:"default:false" json:"banned"`
BannedAt *time.Time `json:"banned_at,omitempty"`
LastLoginAt *time.Time `json:"-"`
LastLoginIP string `gorm:"size:45;default:''" json:"-"` // 兼容 IPv6
LastAccessAt *time.Time `json:"-"` // 最近一次带鉴权的访问(与登录分开)
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
// 仅序列化展示用,不落库
Level int `gorm:"-" json:"level"`
Badges []UserBadgeView `gorm:"-" json:"badges,omitempty"`
}
// AfterFind 填充展示用等级
func (u *User) AfterFind(tx *gorm.DB) (err error) {
u.Level = LevelFromExp(u.Exp)
return nil
}
// SkipsModeration 站长或认证用户发帖/评论免审
func (u *User) SkipsModeration() bool {
return u != nil && (u.Role == RoleAdmin || u.Verified)
}
// 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"` // -1 表示按 id 自动取色
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"`
ContentPlain string `gorm:"type:text" json:"-"` // 正文纯文本,供搜索索引
Tags string `gorm:"size:256" json:"tags"`
PostType string `gorm:"size:16;default:normal;index" json:"post_type"` // normal|question|poll|bounty|lottery
QuestionResolved bool `gorm:"default:false;index" json:"question_resolved"` // 仅 question 有意义
BountyPoints int `gorm:"default:0" json:"bounty_points"` // 悬赏积分(仅 bounty
BountyStatus string `gorm:"size:16;default:'';index" json:"bounty_status"` // open|awarded|refunded
BountyCommentID uint `gorm:"default:0" json:"bounty_comment_id"` // 采纳的评论
LotteryWinnerCount int `gorm:"default:1" json:"lottery_winner_count"` // 抽奖人数(仅 lottery
LotteryStatus string `gorm:"size:16;default:'';index" json:"lottery_status"` // open|drawn
Pinned bool `gorm:"default:false" json:"pinned"` // 全局置顶
BoardPinned bool `gorm:"default:false" json:"board_pinned"` // 板块内置顶
Featured bool `gorm:"default:false;index" json:"featured"` // 精华帖
EditLocked bool `gorm:"default:false" json:"edit_locked"`
CommentsLocked bool `gorm:"default:false" json:"comments_locked"` // 禁止评论(结贴)
Status string `gorm:"size:16;default:published;index" json:"status"` // pending|published|rejected
LikeCount int `gorm:"default:0" json:"like_count"`
ViewCount int `gorm:"default:0" json:"view_count"`
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"`
Comments []Comment `gorm:"foreignKey:PostID" json:"comments,omitempty"`
}
// PostRevision 帖子编辑历史(每次修改前保存旧版本)
type PostRevision struct {
ID uint `gorm:"primaryKey" json:"id"`
PostID uint `gorm:"index;not null" json:"post_id"`
EditorID uint `gorm:"index;not null" json:"editor_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"`
CreatedAt time.Time `json:"created_at"`
Editor User `gorm:"foreignKey:EditorID" json:"editor,omitempty"`
}
// CommentRevision 评论编辑历史(每次修改前保存旧版本,管理员可查看)
type CommentRevision struct {
ID uint `gorm:"primaryKey" json:"id"`
CommentID uint `gorm:"index;not null" json:"comment_id"`
EditorID uint `gorm:"index;not null" json:"editor_id"`
Content string `gorm:"type:text;not null" json:"content"`
CreatedAt time.Time `json:"created_at"`
Editor User `gorm:"foreignKey:EditorID" json:"editor,omitempty"`
}
// ForumSetting 论坛全局设置(键值对)
type ForumSetting struct {
Key string `gorm:"primaryKey;size:64" json:"key"`
Value string `gorm:"size:2048" json:"value"`
}
// Comment 楼层评论
type Comment struct {
ID uint `gorm:"primaryKey" json:"id"`
PostID uint `gorm:"index;not null" json:"post_id"`
UserID uint `gorm:"index" json:"user_id"` // 0 表示游客
Floor int `gorm:"not null" json:"floor"`
Content string `gorm:"type:text;not null" json:"content"`
ReplyTo *uint `gorm:"index" json:"reply_to,omitempty"`
GuestNick string `gorm:"size:64" json:"guest_nick,omitempty"`
GuestEmail string `gorm:"size:128" json:"guest_email,omitempty"`
GuestURL string `gorm:"size:256" json:"guest_url,omitempty"`
IsPrivate bool `gorm:"default:false" json:"is_private"`
Status string `gorm:"size:16;default:published;index" json:"status"` // pending|published|rejected
LikeCount int `gorm:"default:0" json:"like_count"`
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"`
Post Post `gorm:"foreignKey:PostID" json:"post,omitempty"`
ReplyTarget *Comment `gorm:"-" json:"reply_target,omitempty"`
// ThreadParentID 嵌套展示用父评论(父评论不可见时回挂到最近可见祖先)
ThreadParentID *uint `gorm:"-" json:"thread_parent_id,omitempty"`
ContentHidden bool `gorm:"-" json:"content_hidden"`
Liked bool `gorm:"-" json:"liked"`
}
// PostLike 帖子点赞
type PostLike 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"`
}
// CommentLike 评论点赞
type CommentLike struct {
ID uint `gorm:"primaryKey" json:"id"`
CommentID uint `gorm:"uniqueIndex:idx_comment_user;not null" json:"comment_id"`
UserID uint `gorm:"uniqueIndex:idx_comment_user;not null" json:"user_id"`
CreatedAt time.Time `json:"created_at"`
}
// PostFavorite 帖子收藏
type PostFavorite struct {
ID uint `gorm:"primaryKey" json:"id"`
PostID uint `gorm:"uniqueIndex:idx_fav_post_user;not null" json:"post_id"`
UserID uint `gorm:"uniqueIndex:idx_fav_post_user;not null" json:"user_id"`
CreatedAt time.Time `json:"created_at"`
Post Post `gorm:"foreignKey:PostID" json:"post,omitempty"`
}
// 私信类型
const (
MessageKindUser = "user" // 用户互发
MessageKindSystem = "system" // 系统通知
MessageKindReject = "reject" // 帖子被拒/下架
MessageKindReportResult = "report_result" // 举报处理结果
MessageKindReply = "reply" // 帖子/评论被回复
MessageKindMention = "mention" // 被 @提及
MessageKindModeration = "moderation" // 新内容待审核(通知管理员)
)
// PrivateMessage 站内私信
type PrivateMessage struct {
ID uint `gorm:"primaryKey" json:"id"`
FromUserID uint `gorm:"index;not null" json:"from_user_id"` // 0 表示系统
ToUserID uint `gorm:"index;not null" json:"to_user_id"`
Subject string `gorm:"size:256;not null" json:"subject"`
Content string `gorm:"type:text;not null" json:"content"`
Kind string `gorm:"size:32;default:user;index" json:"kind"`
RelatedPostID *uint `gorm:"index" json:"related_post_id,omitempty"`
RelatedReportID *uint `gorm:"index" json:"related_report_id,omitempty"`
IsRead bool `gorm:"default:false;index" json:"is_read"`
CreatedAt time.Time `json:"created_at"`
FromUser User `gorm:"foreignKey:FromUserID" json:"from_user,omitempty"`
ToUser User `gorm:"foreignKey:ToUserID" json:"to_user,omitempty"`
}
// 举报状态
const (
ReportStatusPending = "pending"
ReportStatusResolved = "resolved"
ReportStatusDismissed = "dismissed"
)
// 举报原因
const (
ReportReasonSpam = "spam"
ReportReasonAbuse = "abuse"
ReportReasonIllegal = "illegal"
ReportReasonIrrelevant = "irrelevant"
ReportReasonOther = "other"
)
// 友链申请状态
const (
FriendLinkApplyStatusPending = "pending"
FriendLinkApplyStatusApproved = "approved"
FriendLinkApplyStatusRejected = "rejected"
)
// FriendLinkApply 友情链接申请
type FriendLinkApply struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"index;not null" json:"user_id"`
Name string `gorm:"size:32;not null" json:"name"`
URL string `gorm:"size:512;not null" json:"url"`
Description string `gorm:"size:200;default:''" json:"description,omitempty"`
Logo string `gorm:"size:512;default:''" json:"logo"`
ReciprocalPageURL string `gorm:"size:512;default:''" json:"reciprocal_page_url"`
LinkOnHomepage bool `gorm:"default:true" json:"link_on_homepage"`
ReciprocalVerified bool `gorm:"default:false" json:"reciprocal_verified"`
ReciprocalCheckNote string `gorm:"size:256;default:''" json:"reciprocal_check_note,omitempty"`
ReciprocalCheckedAt *time.Time `json:"reciprocal_checked_at,omitempty"`
Status string `gorm:"size:16;default:pending;index" json:"status"`
ReviewNote string `gorm:"size:256;default:''" json:"review_note,omitempty"`
ReviewedAt *time.Time `json:"reviewed_at,omitempty"`
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"`
}
// PostReport 帖子/评论举报CommentID 有值时为评论举报)
type PostReport struct {
ID uint `gorm:"primaryKey" json:"id"`
PostID uint `gorm:"index;not null" json:"post_id"`
CommentID *uint `gorm:"index" json:"comment_id,omitempty"`
ReporterID uint `gorm:"index;not null" json:"reporter_id"`
Reason string `gorm:"size:32;not null" json:"reason"`
Detail string `gorm:"size:1000" json:"detail"`
Status string `gorm:"size:16;default:pending;index" json:"status"`
HandlerID *uint `gorm:"index" json:"handler_id,omitempty"`
HandleNote string `gorm:"size:1000" json:"handle_note"`
CreatedAt time.Time `json:"created_at"`
HandledAt *time.Time `json:"handled_at,omitempty"`
Post Post `gorm:"foreignKey:PostID" json:"post,omitempty"`
Comment *Comment `gorm:"foreignKey:CommentID" json:"comment,omitempty"`
Reporter User `gorm:"foreignKey:ReporterID" json:"reporter,omitempty"`
Handler *User `gorm:"foreignKey:HandlerID" json:"handler,omitempty"`
}
// Media 上传媒体索引(真实文件在本地 uploads 或 S3本表供后台列表与统计
type Media struct {
ID uint `gorm:"primaryKey" json:"id"`
Category string `gorm:"size:32;index;not null" json:"category"` // avatars|posts|site
Name string `gorm:"size:255;not null" json:"name"`
URL string `gorm:"size:512;uniqueIndex;not null" json:"url"`
Size int64 `gorm:"not null;default:0" json:"size"`
ContentType string `gorm:"size:64;default:''" json:"content_type"`
StorageType string `gorm:"size:16;index;default:local" json:"storage_type"` // local|s3
UserID *uint `gorm:"index" json:"user_id,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// 积分流水原因
const (
PointReasonCheckIn = "check_in"
PointReasonLottery = "lottery"
PointReasonUnlockSpend = "unlock_spend"
PointReasonCreatorIncome = "creator_income"
PointReasonAdminAdjust = "admin_adjust"
PointReasonBountyEscrow = "bounty_escrow"
PointReasonBountyAward = "bounty_award"
PointReasonBountyRefund = "bounty_refund"
)
// PointLedger 积分流水
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;index;not null" json:"reason"`
RefType string `gorm:"size:32;default:''" json:"ref_type"`
RefID uint `gorm:"default:0" json:"ref_id"`
Note string `gorm:"size:256;default:''" json:"note"`
CreatedAt time.Time `json:"created_at"`
}
// CheckIn 每日签到(用户+自然日唯一)
type CheckIn struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"uniqueIndex:idx_checkin_user_day;not null" json:"user_id"`
Day string `gorm:"uniqueIndex:idx_checkin_user_day;size:10;not null" json:"day"` // YYYY-MM-DD
Points int `gorm:"not null" json:"points"`
Streak int `gorm:"default:1" json:"streak"`
CreatedAt time.Time `json:"created_at"`
}
// LotteryDraw 每日抽奖记录
type LotteryDraw struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"uniqueIndex:idx_lottery_user_day;not null" json:"user_id"`
Day string `gorm:"uniqueIndex:idx_lottery_user_day;size:10;not null" json:"day"`
Points int `gorm:"not null" json:"points"` // 抽中积分(可为 0
CreatedAt time.Time `json:"created_at"`
}
// PostContentUnlock 帖子积分解锁记录
type PostContentUnlock struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"uniqueIndex:idx_unlock_user_post_block;not null" json:"user_id"`
PostID uint `gorm:"uniqueIndex:idx_unlock_user_post_block;index;not null" json:"post_id"`
BlockKey string `gorm:"uniqueIndex:idx_unlock_user_post_block;size:64;not null" json:"block_key"`
Cost int `gorm:"not null" json:"cost"`
CreatedAt time.Time `json:"created_at"`
}
// SitePage 自定义单页(关于我们、版规等)
type SitePage struct {
ID uint `gorm:"primaryKey" json:"id"`
Title string `gorm:"size:128;not null" json:"title"`
Slug string `gorm:"uniqueIndex;size:64;not null" json:"slug"`
Content string `gorm:"type:text;not null" json:"content"`
Published bool `gorm:"default:false;index" json:"published"`
SortOrder int `gorm:"default:0" json:"sort_order"`
ShowInFooter bool `gorm:"default:false" json:"show_in_footer"`
ShowInNav bool `gorm:"default:false" json:"show_in_nav"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// Poll 投票帖配置
type Poll struct {
ID uint `gorm:"primaryKey" json:"id"`
PostID uint `gorm:"uniqueIndex;not null" json:"post_id"`
Multi bool `gorm:"default:false" json:"multi"`
MaxChoices int `gorm:"default:1" json:"max_choices"`
Closed bool `gorm:"default:false;index" json:"closed"`
EndsAt *time.Time `gorm:"index" json:"ends_at,omitempty"`
}
// PollOption 投票选项
type PollOption struct {
ID uint `gorm:"primaryKey" json:"id"`
PostID uint `gorm:"index;not null" json:"post_id"`
Text string `gorm:"size:64;not null" json:"text"`
SortOrder int `gorm:"default:0" json:"sort_order"`
VoteCount int `gorm:"default:0" json:"vote_count"`
}
// PollVote 投票记录
type PollVote struct {
ID uint `gorm:"primaryKey" json:"id"`
PostID uint `gorm:"uniqueIndex:idx_poll_vote_multi;index;not null" json:"post_id"`
OptionID uint `gorm:"uniqueIndex:idx_poll_vote_multi;index;not null" json:"option_id"`
UserID uint `gorm:"uniqueIndex:idx_poll_vote_multi;index;not null" json:"user_id"`
}
// PostLotteryWinner 帖内抽奖中奖记录
type PostLotteryWinner 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"`
CommentID uint `gorm:"default:0" json:"comment_id"`
CreatedAt time.Time `json:"created_at"`
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
}
// 徽章类型
const (
BadgeKindAuto = "auto"
BadgeKindLimited = "limited"
)
// 自动徽章指标
const (
BadgeMetricTenureDays = "tenure_days"
BadgeMetricLikesReceived = "likes_received"
BadgeMetricCreatorIncome = "creator_income"
)
// BadgeDef 徽章定义
type BadgeDef struct {
ID uint `gorm:"primaryKey" json:"id"`
Code string `gorm:"uniqueIndex;size:64;not null" json:"code"`
Name string `gorm:"size:64;not null" json:"name"`
Description string `gorm:"size:256;default:''" json:"description"`
Icon string `gorm:"size:64;default:''" json:"icon"` // lucide / 固定 key
Kind string `gorm:"size:16;index;not null" json:"kind"` // auto|limited
Metric string `gorm:"size:32;default:''" json:"metric"`
Threshold int `gorm:"default:0" json:"threshold"`
SortOrder int `gorm:"default:0" json:"sort_order"`
Enabled bool `gorm:"default:true;index" json:"enabled"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// UserBadge 用户已获徽章
type UserBadge struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"uniqueIndex:idx_user_badge;not null" json:"user_id"`
BadgeID uint `gorm:"uniqueIndex:idx_user_badge;index;not null" json:"badge_id"`
AwardedAt time.Time `json:"awarded_at"`
AwardedBy uint `gorm:"default:0" json:"awarded_by"` // 0=系统
Badge BadgeDef `gorm:"foreignKey:BadgeID" json:"badge,omitempty"`
}

31
models/oauth.go Normal file
View File

@@ -0,0 +1,31 @@
package model
import "time"
// OAuthClient OIDC/OAuth2 客户端应用(密钥仅存 bcrypt 哈希)
type OAuthClient struct {
ID uint `gorm:"primaryKey" json:"id"`
ClientID string `gorm:"uniqueIndex;size:128;not null" json:"client_id"`
ClientSecretHash string `gorm:"size:128;not null" json:"-"`
Name string `gorm:"size:128;not null" json:"name"`
RedirectURIs string `gorm:"size:2048;not null" json:"redirect_uris"`
Enabled bool `gorm:"default:true" json:"enabled"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// OAuthAuthCode 一次性授权码OIDC Authorization Code Flow
type OAuthAuthCode struct {
ID uint `gorm:"primaryKey"`
Code string `gorm:"uniqueIndex;size:64;not null"`
ClientID string `gorm:"size:128;not null;index"`
UserID uint `gorm:"not null;index"`
RedirectURI string `gorm:"size:512;not null"`
Scope string `gorm:"size:256;default:''"`
Nonce string `gorm:"size:128;default:''"`
CodeChallenge string `gorm:"size:128;default:''"`
CodeChallengeMethod string `gorm:"size:16;default:''"`
ExpiresAt time.Time `gorm:"not null;index"`
Used bool `gorm:"default:false"`
CreatedAt time.Time
}

149
models/user_view.go Normal file
View File

@@ -0,0 +1,149 @@
package model
import "time"
// UserBadgeView 对外展示的徽章摘要
type UserBadgeView struct {
Code string `json:"code"`
Name string `json:"name"`
Description string `json:"description"`
Icon string `json:"icon"`
Kind string `json:"kind"`
}
// UserPublic 公开用户主页视图(无邮箱与登录信息)
type UserPublic struct {
ID uint `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Signature string `json:"signature"`
Avatar string `json:"avatar"`
Role Role `json:"role"`
Verified bool `json:"verified"`
Exp int `json:"exp"`
Level int `json:"level"`
CreatorIncomeTotal int `json:"creator_income_total"`
Badges []UserBadgeView `json:"badges,omitempty"`
Banned bool `json:"banned"`
BannedAt *time.Time `json:"banned_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
// UserSelf 当前登录用户视图(含邮箱,不含登录 IP
type UserSelf struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Nickname string `json:"nickname"`
Signature string `json:"signature"`
Avatar string `json:"avatar"`
Role Role `json:"role"`
Verified bool `json:"verified"`
Exp int `json:"exp"`
Level int `json:"level"`
Points int `json:"points"`
CreatorIncomeTotal int `json:"creator_income_total"`
Badges []UserBadgeView `json:"badges,omitempty"`
Banned bool `json:"banned"`
BannedAt *time.Time `json:"banned_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// UserAdmin 后台用户管理视图(含邮箱、上次登录与最近访问)
type UserAdmin struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Nickname string `json:"nickname"`
Signature string `json:"signature"`
Avatar string `json:"avatar"`
Role Role `json:"role"`
Verified bool `json:"verified"`
Exp int `json:"exp"`
Level int `json:"level"`
Points int `json:"points"`
CreatorIncomeTotal int `json:"creator_income_total"`
Banned bool `json:"banned"`
BannedAt *time.Time `json:"banned_at,omitempty"`
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
LastLoginIP string `json:"last_login_ip,omitempty"`
LastAccessAt *time.Time `json:"last_access_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ToPublic 转为公开主页视图
func (u *User) ToPublic() UserPublic {
return UserPublic{
ID: u.ID,
Username: u.Username,
Nickname: u.Nickname,
Signature: u.Signature,
Avatar: u.Avatar,
Role: u.Role,
Verified: u.Verified,
Exp: u.Exp,
Level: LevelFromExp(u.Exp),
CreatorIncomeTotal: u.CreatorIncomeTotal,
Banned: u.Banned,
BannedAt: u.BannedAt,
CreatedAt: u.CreatedAt,
}
}
// ToSelf 转为个人中心 /api/me 视图
func (u *User) ToSelf() UserSelf {
return UserSelf{
ID: u.ID,
Username: u.Username,
Email: u.Email,
Nickname: u.Nickname,
Signature: u.Signature,
Avatar: u.Avatar,
Role: u.Role,
Verified: u.Verified,
Exp: u.Exp,
Level: LevelFromExp(u.Exp),
Points: u.Points,
CreatorIncomeTotal: u.CreatorIncomeTotal,
Banned: u.Banned,
BannedAt: u.BannedAt,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
}
}
// ToAdmin 转为后台用户列表视图
func (u *User) ToAdmin() UserAdmin {
return UserAdmin{
ID: u.ID,
Username: u.Username,
Email: u.Email,
Nickname: u.Nickname,
Signature: u.Signature,
Avatar: u.Avatar,
Role: u.Role,
Verified: u.Verified,
Exp: u.Exp,
Level: LevelFromExp(u.Exp),
Points: u.Points,
CreatorIncomeTotal: u.CreatorIncomeTotal,
Banned: u.Banned,
BannedAt: u.BannedAt,
LastLoginAt: u.LastLoginAt,
LastLoginIP: u.LastLoginIP,
LastAccessAt: u.LastAccessAt,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
}
}
// UsersToAdmin 批量转换后台用户列表
func UsersToAdmin(users []User) []UserAdmin {
out := make([]UserAdmin, 0, len(users))
for i := range users {
out = append(out, users[i].ToAdmin())
}
return out
}