浏览器登录改为 DB sessions(可吊销);敏感词与 OIDC PEM 入 settings; 落地安装向导、注册发帖与 /admin 仪表盘/板块/审核/设置。 Co-authored-by: Cursor <cursoragent@cursor.com>
178 lines
6.0 KiB
Go
178 lines
6.0 KiB
Go
package models
|
||
|
||
import (
|
||
"fmt"
|
||
"log"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/glebarez/sqlite"
|
||
"gorm.io/driver/mysql"
|
||
"gorm.io/driver/postgres"
|
||
"gorm.io/gorm"
|
||
"gorm.io/gorm/logger"
|
||
)
|
||
|
||
var DB *gorm.DB
|
||
|
||
// DatabaseConfig 与 config.DatabaseConfig 对齐的精简结构(避免 models→config 循环依赖)
|
||
type DatabaseConfig struct {
|
||
Type string
|
||
DSN string
|
||
SQLitePath string
|
||
MaxOpenConns int
|
||
MaxIdleConns int
|
||
ConnMaxLifetimeSec int
|
||
}
|
||
|
||
// InitDB 按方言初始化数据库并自动迁移
|
||
func InitDB(cfg DatabaseConfig) error {
|
||
typ := strings.ToLower(strings.TrimSpace(cfg.Type))
|
||
if typ == "" {
|
||
typ = "sqlite"
|
||
}
|
||
|
||
dialector, err := openDialector(typ, cfg)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
db, err := gorm.Open(dialector, &gorm.Config{
|
||
Logger: logger.Default.LogMode(logger.Warn),
|
||
})
|
||
if err != nil {
|
||
return fmt.Errorf("连接数据库失败 (%s): %w — 请检查 JIANG13_DB_TYPE / JIANG13_DB_DSN", typ, err)
|
||
}
|
||
|
||
sqlDB, err := db.DB()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if cfg.MaxOpenConns > 0 {
|
||
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
|
||
}
|
||
if cfg.MaxIdleConns > 0 {
|
||
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
|
||
}
|
||
if cfg.ConnMaxLifetimeSec > 0 {
|
||
sqlDB.SetConnMaxLifetime(time.Duration(cfg.ConnMaxLifetimeSec) * time.Second)
|
||
}
|
||
|
||
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{},
|
||
&Session{},
|
||
); 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.Printf("[models] 数据库初始化完成 type=%s", typ)
|
||
return nil
|
||
}
|
||
|
||
func openDialector(typ string, cfg DatabaseConfig) (gorm.Dialector, error) {
|
||
switch typ {
|
||
case "sqlite", "sqlite3":
|
||
path := cfg.SQLitePath
|
||
if path == "" {
|
||
path = cfg.DSN
|
||
}
|
||
if path == "" {
|
||
return nil, fmt.Errorf("sqlite 需要文件路径")
|
||
}
|
||
if path != ":memory:" {
|
||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||
return nil, fmt.Errorf("创建数据库目录失败: %w", err)
|
||
}
|
||
}
|
||
return sqlite.Open(path), nil
|
||
case "postgres", "postgresql", "pg":
|
||
if cfg.DSN == "" {
|
||
return nil, fmt.Errorf("postgres 需要 JIANG13_DB_DSN 或 HOST/USER/NAME")
|
||
}
|
||
return postgres.Open(cfg.DSN), nil
|
||
case "mysql", "mariadb":
|
||
if cfg.DSN == "" {
|
||
return nil, fmt.Errorf("mysql 需要 JIANG13_DB_DSN 或 HOST/USER/NAME")
|
||
}
|
||
return mysql.Open(cfg.DSN), nil
|
||
default:
|
||
return nil, fmt.Errorf("不支持的数据库类型 %q", typ)
|
||
}
|
||
}
|
||
|
||
// DialectorName 当前驱动名(sqlite/postgres/mysql)
|
||
func DialectorName() string {
|
||
if DB == nil {
|
||
return ""
|
||
}
|
||
return DB.Dialector.Name()
|
||
}
|
||
|
||
// 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
|
||
}
|
||
}
|
||
}
|