feat: 站点访问统计与可配置侧边栏组件

- 新增访问统计:visitstats 服务与查询、visit_capture 中间件、爬虫/来源识别、管理端 analytics 页面
- 新增侧边栏:sidebar 服务与 handler、管理端 sidebar 配置页、前端侧边栏组件
- 新增 widget 嵌入代码生成(widgetCode)与 widgetApi
- 遥测逻辑迁移至 handler 层,删除 service/telemetry
- env 示例补充 Docker 部署可信代理 IP 说明
This commit is contained in:
2026-09-24 18:53:45 +08:00
parent 3e55b5d230
commit 7b327bc8cc
60 changed files with 6539 additions and 572 deletions

View File

@@ -58,10 +58,10 @@ func InitDB(dsn string) error {
if err := db.AutoMigrate(
&TemporaryUpload{}, &ModuleConfig{}, &SettingsAudit{}, &ActionCounter{}, &MailTask{}, &EmailChallenge{}, &StoredObject{},
&User{}, &Board{}, &Post{}, &Comment{}, &CommentEditHistory{}, &RefreshToken{}, &Like{}, &Notification{}, &Checkin{},
&Announcement{}, &SitePage{}, &SiteSetting{}, &SiteDailyStats{}, &SiteDailyVisitor{}, &Attachment{}, &UserBoard{}, &LoginLog{},
&Announcement{}, &SitePage{}, &SiteSetting{}, &SiteDailyStats{}, &SiteDailyVisitor{}, &VisitEvent{}, &Attachment{}, &UserBoard{}, &LoginLog{},
&ChatRoom{}, &ChatRoomMember{}, &ChatMessage{},
&PointLedger{}, &PostContentUnlock{}, &PostAttachment{}, &PostAttachmentUnlock{},
&PostPollVote{}, &PostLotteryEntry{}, &Ad{},
&PostPollVote{}, &PostLotteryEntry{}, &Ad{}, &SidebarWidget{},
&ImportRecord{},
); err != nil {
return fmt.Errorf("自动迁移失败: %w", err)

View File

@@ -455,6 +455,19 @@ type SiteSetting struct {
UpdatedAt time.Time `json:"updated_at"`
}
// SidebarWidget 自定义侧栏工具(站长 HTML+CSS+JS 代码块,前台原样渲染执行)
// Enabled 不加 gorm default 标签:规避布尔零值省略陷阱,创建时由代码显式赋值
type SidebarWidget struct {
ID uint `gorm:"primarykey" json:"id"`
Name string `gorm:"size:64;not null" json:"name"`
Html string `gorm:"type:text" json:"html"`
Css string `gorm:"type:text" json:"css"`
Js string `gorm:"type:text" json:"js"`
Enabled bool `json:"enabled"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// SiteDailyStats 站点日维度访问量(轻量 PV/UV)
type SiteDailyStats struct {
Date time.Time `gorm:"type:date;primaryKey" json:"date"`
@@ -470,6 +483,31 @@ type SiteDailyVisitor struct {
VidHash string `gorm:"size:64;primaryKey" json:"-"`
}
// 访问事件类型:pageview 人类浏览(计 PV/UV);bot 爬虫;probe 异常/错误响应
const (
VisitKindPageview = "pageview"
VisitKindBot = "bot"
VisitKindProbe = "probe"
)
// VisitEvent 单次访问事件(内存攒批落库,保留期滚动清理)
type VisitEvent struct {
ID uint64 `gorm:"primaryKey" json:"id"`
CreatedAt time.Time `gorm:"index:idx_visit_created" json:"created_at"`
Kind string `gorm:"size:16;index:idx_visit_kind_time,priority:1;not null;default:'pageview'" json:"kind"`
Path string `gorm:"size:512;not null;default:''" json:"path"`
RefHost string `gorm:"size:128;not null;default:''" json:"ref_host"`
RefKind string `gorm:"size:16;not null;default:'direct'" json:"ref_kind"` // direct|internal|search|social|external
VidHash string `gorm:"size:64;not null;default:''" json:"-"`
UserID uint `gorm:"not null;default:0" json:"user_id"`
IP string `gorm:"size:45;not null;default:''" json:"ip"`
Device string `gorm:"size:16;not null;default:''" json:"device"` // computer|mobile|tablet
OS string `gorm:"size:32;not null;default:''" json:"os"`
Browser string `gorm:"size:32;not null;default:''" json:"browser"`
Bot string `gorm:"size:64;not null;default:''" json:"bot"` // 非空 = 爬虫名
Status int `gorm:"not null;default:0" json:"status"` // probe 的响应状态码
}
// 附件类型
const (
AttachmentKindAvatar = "avatar" // 用户头像(正方形 WebP)