首次提交:姜十三论坛

后端 Go+Gin:认证/CSRF/限流、板块、帖子、评论、点赞、通知、用户资料、置顶推荐;前端 Next.js 16:发帖/编辑/删除、搜索、分页、点赞、通知中心、设置;基础设施 docker-compose 与配置模板;添加 .gitignore 与专有许可证(保留所有权利)
This commit is contained in:
2026-09-12 01:11:05 +08:00
commit 0c2b0d2d6a
62 changed files with 8289 additions and 0 deletions

63
backend/model/db.go Normal file
View File

@@ -0,0 +1,63 @@
package model
import (
"fmt"
"log"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// DB 全局数据库实例
var DB *gorm.DB
// InitDB 连接 PostgreSQL 并自动迁移
func InitDB(dsn string) error {
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Warn),
})
if err != nil {
return fmt.Errorf("连接 PostgreSQL 失败: %w", err)
}
if err := db.AutoMigrate(
&User{}, &Board{}, &Post{}, &Comment{}, &RefreshToken{}, &Like{}, &Notification{},
); err != nil {
return fmt.Errorf("自动迁移失败: %w", err)
}
DB = db
seedDefaultBoards(db)
log.Println("[model] PostgreSQL 数据库初始化完成")
return nil
}
// PingDB 检测数据库连接
func PingDB() error {
if DB == nil {
return fmt.Errorf("数据库未初始化")
}
sqlDB, err := DB.DB()
if err != nil {
return err
}
return sqlDB.Ping()
}
// seedDefaultBoards 写入默认板块
func seedDefaultBoards(db *gorm.DB) {
defaults := []Board{
{Name: "综合讨论", Description: "什么都可以聊", Icon: "message-circle", SortOrder: 1},
{Name: "技术分享", Description: "分享技术心得与问题", Icon: "code", SortOrder: 2},
{Name: "问答求助", Description: "提问与解答", Icon: "help-circle", SortOrder: 3},
{Name: "闲聊灌水", Description: "轻松闲聊", Icon: "coffee", SortOrder: 4},
}
for _, b := range defaults {
var count int64
db.Model(&Board{}).Where("name = ?", b.Name).Count(&count)
if count == 0 {
_ = db.Create(&b).Error
}
}
}

136
backend/model/models.go Normal file
View File

@@ -0,0 +1,136 @@
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"`
Role Role `gorm:"size:16;default:user" json:"role"`
Banned bool `gorm:"default:false" json:"banned"`
TokenVersion int `gorm:"default:0" json:"-"` // token 版本号,改密码/封禁时递增使旧 JWT 失效
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// RefreshToken 刷新令牌表(支持服务端撤销与轮转)
type RefreshToken struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"index;not null" json:"user_id"`
Token string `gorm:"uniqueIndex;size:128;not null" json:"-"`
ExpiresAt time.Time `gorm:"index;not null" json:"expires_at"`
Revoked bool `gorm:"default:false" json:"revoked"`
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 评论
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"`
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"`
}
// 通知类型
const (
NotificationTypeComment = "comment" // 评论了你的帖子
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"`
}