Files
panel/internal/biz/website_stat.go
T
耗子andClaude Opus 4.8 443516a2cf refactor!: 迁移至 samber/do 依赖注入与三层架构
依赖注入:
- 移除 google/wire(含 wire.go/wire_gen.go 与全部 ProviderSet),
  改用 samber/do v2 单注入器 + 双入口惰性构建
- 贡献模型替代命令式注册:路由 routes:、命令 commands:、
  任务 jobs: 前缀经 internal/registry 收集与校验
- 构造函数统一 func NewXxx(i do.Injector) (T, error)

三层架构:
- 补全用例层:每个 biz.XxxRepo 配 XxxUsecase,
  service/command/job 表现层只依赖用例,不再直接引用仓储

路由与文档:
- route/http.go 按域拆为声明式 Endpoint 贡献,
  Endpoint 承载登录白名单与端点限流语义
- 调试模式下提供 OpenAPI 3.1 文档:/openapi.json 与 /docs(Scalar),
  从 validate 标签生成

目录与依赖:
- internal/http/{middleware,request,rule} 拍平至 internal/*
- CLI 命令拆至 internal/command
- 日志轮转 timberjack 换为 libtnb/logrotate
- 升级 validator/cron/sessions/gormstore/sqlite/securecookie
- 数据库迁移保持 gormigrate 不变

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 21:54:22 +08:00

334 lines
13 KiB
Go

package biz
import "time"
// WebsiteStat 网站统计(每站每天一行或每小时一行)
type WebsiteStat struct {
ID uint `gorm:"primaryKey" json:"id"`
Site string `gorm:"not null;uniqueIndex:idx_wstat_unique" json:"site"`
Date string `gorm:"not null;uniqueIndex:idx_wstat_unique;index" json:"date"`
Hour int `gorm:"not null;uniqueIndex:idx_wstat_unique;default:-1" json:"hour"` // -1=每日汇总, 0-23=小时
PV uint64 `gorm:"not null;default:0" json:"pv"`
UV uint64 `gorm:"not null;default:0" json:"uv"`
IP uint64 `gorm:"not null;default:0" json:"ip"`
Bandwidth uint64 `gorm:"not null;default:0" json:"bandwidth"`
BandwidthIn uint64 `gorm:"not null;default:0" json:"bandwidth_in"`
Requests uint64 `gorm:"not null;default:0" json:"requests"`
Errors uint64 `gorm:"not null;default:0" json:"errors"`
Spiders uint64 `gorm:"not null;default:0" json:"spiders"`
RequestTimeSum uint64 `gorm:"not null;default:0" json:"request_time_sum"`
RequestTimeCount uint64 `gorm:"not null;default:0" json:"request_time_count"`
Status2xx uint64 `gorm:"not null;default:0" json:"status_2xx"`
Status3xx uint64 `gorm:"not null;default:0" json:"status_3xx"`
Status4xx uint64 `gorm:"not null;default:0" json:"status_4xx"`
Status5xx uint64 `gorm:"not null;default:0" json:"status_5xx"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// WebsiteErrorLog 网站错误日志(400-599 状态码详情)
type WebsiteErrorLog struct {
ID uint `gorm:"primaryKey" json:"id"`
Site string `gorm:"not null;index:idx_werr_site_time" json:"site"`
URI string `gorm:"not null" json:"uri"`
Method string `gorm:"not null" json:"method"`
Status int `gorm:"not null;index" json:"status"`
IP string `gorm:"not null" json:"ip"`
UA string `gorm:"not null" json:"ua"`
Body string `gorm:"type:text" json:"body"`
CreatedAt time.Time `gorm:"index:idx_werr_site_time" json:"created_at"`
}
// WebsiteStatSpider 蜘蛛统计(site, date, spider 唯一)
type WebsiteStatSpider struct {
ID uint `gorm:"primaryKey" json:"id"`
Site string `gorm:"not null;uniqueIndex:idx_wspider_unique" json:"site"`
Date string `gorm:"not null;uniqueIndex:idx_wspider_unique;index" json:"date"`
Spider string `gorm:"not null;uniqueIndex:idx_wspider_unique" json:"spider"`
Requests uint64 `gorm:"not null;default:0" json:"requests"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// WebsiteStatClient 客户端统计(site, date, browser, os 唯一)
type WebsiteStatClient struct {
ID uint `gorm:"primaryKey" json:"id"`
Site string `gorm:"not null;uniqueIndex:idx_wclient_unique" json:"site"`
Date string `gorm:"not null;uniqueIndex:idx_wclient_unique;index" json:"date"`
Browser string `gorm:"not null;uniqueIndex:idx_wclient_unique" json:"browser"`
OS string `gorm:"not null;uniqueIndex:idx_wclient_unique" json:"os"`
Requests uint64 `gorm:"not null;default:0" json:"requests"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// WebsiteStatIP IP 统计(site, date, ip 唯一)
type WebsiteStatIP struct {
ID uint `gorm:"primaryKey" json:"id"`
Site string `gorm:"not null;uniqueIndex:idx_wip_unique" json:"site"`
Date string `gorm:"not null;uniqueIndex:idx_wip_unique;index" json:"date"`
IP string `gorm:"not null;uniqueIndex:idx_wip_unique" json:"ip"`
Country string `gorm:"not null;default:''" json:"country"`
Region string `gorm:"not null;default:''" json:"region"`
City string `gorm:"not null;default:''" json:"city"`
ISP string `gorm:"not null;default:''" json:"isp"`
Requests uint64 `gorm:"not null;default:0" json:"requests"`
Bandwidth uint64 `gorm:"not null;default:0" json:"bandwidth"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// WebsiteStatURI URI 统计(site, date, uri 唯一)
type WebsiteStatURI struct {
ID uint `gorm:"primaryKey" json:"id"`
Site string `gorm:"not null;uniqueIndex:idx_wuri_unique" json:"site"`
Date string `gorm:"not null;uniqueIndex:idx_wuri_unique;index" json:"date"`
URI string `gorm:"not null;uniqueIndex:idx_wuri_unique" json:"uri"`
Requests uint64 `gorm:"not null;default:0" json:"requests"`
Bandwidth uint64 `gorm:"not null;default:0" json:"bandwidth"`
Errors uint64 `gorm:"not null;default:0" json:"errors"`
RequestTimeSum uint64 `gorm:"not null;default:0" json:"request_time_sum"`
RequestTimeCount uint64 `gorm:"not null;default:0" json:"request_time_count"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// WebsiteStatSeries 时间序列数据点(用于 API 返回)
type WebsiteStatSeries struct {
Key string `json:"key"` // 小时 "0"-"23" 或日期 "2026-02-18"
PV uint64 `json:"pv"`
UV uint64 `json:"uv"`
IP uint64 `json:"ip"`
Bandwidth uint64 `json:"bandwidth"`
BandwidthIn uint64 `json:"bandwidth_in"`
Requests uint64 `json:"requests"`
Errors uint64 `json:"errors"`
Spiders uint64 `json:"spiders"`
RequestTimeSum uint64 `json:"request_time_sum"`
RequestTimeCount uint64 `json:"request_time_count"`
Status2xx uint64 `json:"status_2xx"`
Status3xx uint64 `json:"status_3xx"`
Status4xx uint64 `json:"status_4xx"`
Status5xx uint64 `json:"status_5xx"`
}
// WebsiteStatSpiderRank 蜘蛛排名
type WebsiteStatSpiderRank struct {
Spider string `json:"spider"`
Requests uint64 `json:"requests"`
Percent float64 `json:"percent"`
}
// WebsiteStatClientRank 客户端排名
type WebsiteStatClientRank struct {
Browser string `json:"browser"`
OS string `json:"os"`
Requests uint64 `json:"requests"`
}
// WebsiteStatIPRank IP 排名
type WebsiteStatIPRank struct {
IP string `json:"ip"`
Country string `json:"country"`
Region string `json:"region"`
City string `json:"city"`
ISP string `json:"isp"`
Requests uint64 `json:"requests"`
Bandwidth uint64 `json:"bandwidth"`
}
// WebsiteStatGeoRank 地理位置归类统计
type WebsiteStatGeoRank struct {
Country string `json:"country"`
Region string `json:"region"`
City string `json:"city"`
Requests uint64 `json:"requests"`
Bandwidth uint64 `json:"bandwidth"`
}
// WebsiteStatURIRank URI 排名
type WebsiteStatURIRank struct {
URI string `json:"uri"`
Requests uint64 `json:"requests"`
Bandwidth uint64 `json:"bandwidth"`
Errors uint64 `json:"errors"`
RequestTimeSum uint64 `json:"request_time_sum"`
RequestTimeCount uint64 `json:"request_time_count"`
}
// WebsiteStatSiteItem 网站维度汇总
type WebsiteStatSiteItem struct {
Site string `json:"site"`
PV uint64 `json:"pv"`
UV uint64 `json:"uv"`
IP uint64 `json:"ip"`
Bandwidth uint64 `json:"bandwidth"`
BandwidthIn uint64 `json:"bandwidth_in"`
Requests uint64 `json:"requests"`
Errors uint64 `json:"errors"`
Spiders uint64 `json:"spiders"`
RequestTimeSum uint64 `json:"request_time_sum"`
RequestTimeCount uint64 `json:"request_time_count"`
Status2xx uint64 `json:"status_2xx"`
Status3xx uint64 `json:"status_3xx"`
Status4xx uint64 `json:"status_4xx"`
Status5xx uint64 `json:"status_5xx"`
}
// WebsiteStatRepo 网站统计数据访问接口
type WebsiteStatRepo interface {
// Upsert 批量写入/更新统计数据(含每日汇总和小时数据)
Upsert(stats []*WebsiteStat) error
// ListByDateRange 按日期范围查询每站汇总(仅 hour=-1 的每日行)
ListByDateRange(start, end string, sites []string) ([]*WebsiteStat, error)
// DailySeries 按日分组的时间序列
DailySeries(start, end string, sites []string) ([]*WebsiteStatSeries, error)
// HourlySeries 按小时的时间序列(单天查询)
HourlySeries(date string, sites []string) ([]*WebsiteStatSeries, error)
// ClearBefore 清理过期数据
ClearBefore(date string) error
// InsertErrors 批量写入错误日志
InsertErrors(errors []*WebsiteErrorLog) error
// ClearErrorsBefore 清理过期错误日志
ClearErrorsBefore(t time.Time) error
// Clear 清空所有统计数据
Clear() error
VacuumDB() error
// 蜘蛛统计
UpsertSpiders(stats []*WebsiteStatSpider) error
TopSpiders(start, end string, sites []string, limit uint) ([]*WebsiteStatSpiderRank, error)
ClearSpidersBefore(date string) error
// 客户端统计
UpsertClients(stats []*WebsiteStatClient) error
TopClients(start, end string, sites []string, limit uint) ([]*WebsiteStatClientRank, error)
ClearClientsBefore(date string) error
// IP 统计
UpsertIPs(stats []*WebsiteStatIP) error
TopIPs(start, end string, sites []string, page, limit uint) ([]*WebsiteStatIPRank, uint, error)
TopGeos(start, end string, sites []string, groupBy string, country string, limit uint) ([]*WebsiteStatGeoRank, error)
ClearIPsBefore(date string) error
// URI 统计
UpsertURIs(stats []*WebsiteStatURI) error
TopURIs(start, end string, sites []string, page, limit uint) ([]*WebsiteStatURIRank, uint, error)
TopSlowURIs(start, end string, sites []string, threshold, page, limit uint) ([]*WebsiteStatURIRank, uint, error)
ClearURIsBefore(date string) error
// 错误日志查询
ListErrors(start, end string, sites []string, status int, page, limit uint) ([]*WebsiteErrorLog, uint, error)
// 网站维度汇总
ListSiteStats(start, end string, sites []string) ([]*WebsiteStatSiteItem, error)
}
type WebsiteStatUsecase struct {
repo WebsiteStatRepo
}
func NewWebsiteStatUsecase(repo WebsiteStatRepo) *WebsiteStatUsecase {
return &WebsiteStatUsecase{repo: repo}
}
func (uc *WebsiteStatUsecase) Upsert(stats []*WebsiteStat) error {
return uc.repo.Upsert(stats)
}
func (uc *WebsiteStatUsecase) ListByDateRange(start, end string, sites []string) ([]*WebsiteStat, error) {
return uc.repo.ListByDateRange(start, end, sites)
}
func (uc *WebsiteStatUsecase) DailySeries(start, end string, sites []string) ([]*WebsiteStatSeries, error) {
return uc.repo.DailySeries(start, end, sites)
}
func (uc *WebsiteStatUsecase) HourlySeries(date string, sites []string) ([]*WebsiteStatSeries, error) {
return uc.repo.HourlySeries(date, sites)
}
func (uc *WebsiteStatUsecase) ClearBefore(date string) error {
return uc.repo.ClearBefore(date)
}
func (uc *WebsiteStatUsecase) InsertErrors(errors []*WebsiteErrorLog) error {
return uc.repo.InsertErrors(errors)
}
func (uc *WebsiteStatUsecase) ClearErrorsBefore(t time.Time) error {
return uc.repo.ClearErrorsBefore(t)
}
func (uc *WebsiteStatUsecase) Clear() error {
return uc.repo.Clear()
}
func (uc *WebsiteStatUsecase) VacuumDB() error {
return uc.repo.VacuumDB()
}
func (uc *WebsiteStatUsecase) UpsertSpiders(stats []*WebsiteStatSpider) error {
return uc.repo.UpsertSpiders(stats)
}
func (uc *WebsiteStatUsecase) TopSpiders(start, end string, sites []string, limit uint) ([]*WebsiteStatSpiderRank, error) {
return uc.repo.TopSpiders(start, end, sites, limit)
}
func (uc *WebsiteStatUsecase) ClearSpidersBefore(date string) error {
return uc.repo.ClearSpidersBefore(date)
}
func (uc *WebsiteStatUsecase) UpsertClients(stats []*WebsiteStatClient) error {
return uc.repo.UpsertClients(stats)
}
func (uc *WebsiteStatUsecase) TopClients(start, end string, sites []string, limit uint) ([]*WebsiteStatClientRank, error) {
return uc.repo.TopClients(start, end, sites, limit)
}
func (uc *WebsiteStatUsecase) ClearClientsBefore(date string) error {
return uc.repo.ClearClientsBefore(date)
}
func (uc *WebsiteStatUsecase) UpsertIPs(stats []*WebsiteStatIP) error {
return uc.repo.UpsertIPs(stats)
}
func (uc *WebsiteStatUsecase) TopIPs(start, end string, sites []string, page, limit uint) ([]*WebsiteStatIPRank, uint, error) {
return uc.repo.TopIPs(start, end, sites, page, limit)
}
func (uc *WebsiteStatUsecase) TopGeos(start, end string, sites []string, groupBy string, country string, limit uint) ([]*WebsiteStatGeoRank, error) {
return uc.repo.TopGeos(start, end, sites, groupBy, country, limit)
}
func (uc *WebsiteStatUsecase) ClearIPsBefore(date string) error {
return uc.repo.ClearIPsBefore(date)
}
func (uc *WebsiteStatUsecase) UpsertURIs(stats []*WebsiteStatURI) error {
return uc.repo.UpsertURIs(stats)
}
func (uc *WebsiteStatUsecase) TopURIs(start, end string, sites []string, page, limit uint) ([]*WebsiteStatURIRank, uint, error) {
return uc.repo.TopURIs(start, end, sites, page, limit)
}
func (uc *WebsiteStatUsecase) TopSlowURIs(start, end string, sites []string, threshold, page, limit uint) ([]*WebsiteStatURIRank, uint, error) {
return uc.repo.TopSlowURIs(start, end, sites, threshold, page, limit)
}
func (uc *WebsiteStatUsecase) ClearURIsBefore(date string) error {
return uc.repo.ClearURIsBefore(date)
}
func (uc *WebsiteStatUsecase) ListErrors(start, end string, sites []string, status int, page, limit uint) ([]*WebsiteErrorLog, uint, error) {
return uc.repo.ListErrors(start, end, sites, status, page, limit)
}
func (uc *WebsiteStatUsecase) ListSiteStats(start, end string, sites []string) ([]*WebsiteStatSiteItem, error) {
return uc.repo.ListSiteStats(start, end, sites)
}