feat: 管理端网站监控,浏览量写入独立 monitor.db

请求日志按日 JSONL;page_views 不进主库,避免统计数据撑大 jiang13.db。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-31 07:55:45 +08:00
parent 16b6a18096
commit 429956c594
54 changed files with 5504 additions and 146 deletions

View File

@@ -13,25 +13,36 @@ import (
var DB *gorm.DB
// InitDB 初始化 SQLite 并自动迁移
func InitDB(dbPath string) error {
// MonitorDB 网站监控独立库page_views与主库 jiang13.db 分离以免撑大备份
var MonitorDB *gorm.DB
func openSQLite(dbPath string) (*gorm.DB, error) {
dir := filepath.Dir(dbPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("创建数据库目录失败: %w", err)
return nil, 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)
return nil, fmt.Errorf("连接 SQLite 失败: %w", err)
}
sqlDB, err := db.DB()
if err != nil {
return err
return nil, err
}
sqlDB.SetMaxOpenConns(1)
return db, nil
}
// InitDB 初始化主库 SQLite 并自动迁移(不含 page_views
func InitDB(dbPath string) error {
db, err := openSQLite(dbPath)
if err != nil {
return err
}
if err := db.AutoMigrate(
&User{}, &Board{}, &Post{}, &Comment{},
@@ -60,6 +71,20 @@ func InitDB(dbPath string) error {
return nil
}
// InitMonitorDB 打开独立监控库page_views 只写此处)
func InitMonitorDB(monitorDBPath string) error {
db, err := openSQLite(monitorDBPath)
if err != nil {
return fmt.Errorf("监控库初始化失败: %w", err)
}
if err := db.AutoMigrate(&PageView{}); err != nil {
return fmt.Errorf("监控库迁移失败: %w", err)
}
MonitorDB = db
log.Println("[model] 监控库初始化完成:", monitorDBPath)
return nil
}
// PingDB 检测数据库连接是否可用(供健康检查使用)
func PingDB() error {
if DB == nil {

View File

@@ -481,3 +481,20 @@ type CommunityInstance struct {
FirstSeenAt time.Time `json:"first_seen_at"`
LastSeenAt time.Time `gorm:"index" json:"last_seen_at"`
}
// PageView 前台路由浏览量(第一方 SPA 信标;存独立 monitor.db请求日志走 jsonl
type PageView struct {
ID uint `gorm:"primaryKey" json:"id"`
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
Path string `gorm:"size:512;index" json:"path"`
Referrer string `gorm:"size:512" json:"referrer"`
IP string `gorm:"size:64;index" json:"ip"` // 完整客户端 IP
UA string `gorm:"size:512" json:"ua"`
Country string `gorm:"size:8;index" json:"country"`
Region string `gorm:"size:64" json:"region"`
RegionISO string `gorm:"size:16;index" json:"region_iso"`
City string `gorm:"size:64;index" json:"city"`
ASN uint `gorm:"index" json:"asn"`
ASOrg string `gorm:"size:128" json:"as_org"`
IsBot bool `gorm:"default:false;index" json:"is_bot"`
}

54
model/monitor_db_test.go Normal file
View File

@@ -0,0 +1,54 @@
package model
import (
"path/filepath"
"testing"
"gorm.io/gorm"
)
func TestInitMonitorDBUsesSeparateFile(t *testing.T) {
dir := t.TempDir()
mainPath := filepath.Join(dir, "jiang13.db")
monPath := filepath.Join(dir, "monitor.db")
prevDB, prevMon := DB, MonitorDB
t.Cleanup(func() {
closeGorm(MonitorDB)
closeGorm(DB)
DB, MonitorDB = prevDB, prevMon
})
if err := InitDB(mainPath); err != nil {
t.Fatal(err)
}
if err := InitMonitorDB(monPath); err != nil {
t.Fatal(err)
}
if tableExists(DB, "page_views") {
t.Fatal("主库不应创建 page_views")
}
if !tableExists(MonitorDB, "page_views") {
t.Fatal("监控库缺少 page_views")
}
}
func tableExists(db *gorm.DB, name string) bool {
if db == nil {
return false
}
var n int
_ = db.Raw("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?", name).Scan(&n).Error
return n > 0
}
func closeGorm(db *gorm.DB) {
if db == nil {
return
}
sqlDB, err := db.DB()
if err != nil {
return
}
_ = sqlDB.Close()
}