mirror of
https://github.com/woodchen-ink/random-api-go.git
synced 2025-07-18 05:42:01 +08:00
370 lines
8.6 KiB
Go
370 lines
8.6 KiB
Go
package service
|
||
|
||
import (
|
||
"log"
|
||
"net/url"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"random-api-go/database"
|
||
"random-api-go/model"
|
||
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// DomainStatsService 域名统计服务
|
||
type DomainStatsService struct {
|
||
mu sync.RWMutex
|
||
memoryStats map[string]*DomainStatsBatch // 内存中的批量统计
|
||
}
|
||
|
||
// DomainStatsBatch 批量统计数据
|
||
type DomainStatsBatch struct {
|
||
Count uint64
|
||
LastSeen time.Time
|
||
}
|
||
|
||
var (
|
||
domainStatsService *DomainStatsService
|
||
domainStatsOnce sync.Once
|
||
)
|
||
|
||
// GetDomainStatsService 获取域名统计服务实例
|
||
func GetDomainStatsService() *DomainStatsService {
|
||
domainStatsOnce.Do(func() {
|
||
domainStatsService = &DomainStatsService{
|
||
memoryStats: make(map[string]*DomainStatsBatch),
|
||
}
|
||
// 启动定期保存任务
|
||
go domainStatsService.startPeriodicSave()
|
||
})
|
||
return domainStatsService
|
||
}
|
||
|
||
// extractDomain 从Referer中提取域名
|
||
func (s *DomainStatsService) extractDomain(referer string) string {
|
||
if referer == "" {
|
||
return "direct" // 直接访问
|
||
}
|
||
|
||
parsedURL, err := url.Parse(referer)
|
||
if err != nil {
|
||
return "unknown"
|
||
}
|
||
|
||
domain := parsedURL.Hostname()
|
||
if domain == "" {
|
||
return "unknown"
|
||
}
|
||
|
||
return domain
|
||
}
|
||
|
||
// RecordRequest 记录请求(忽略静态文件和管理后台)
|
||
func (s *DomainStatsService) RecordRequest(path, referer string) {
|
||
// 忽略的路径模式
|
||
if s.shouldIgnorePath(path) {
|
||
return
|
||
}
|
||
|
||
domain := s.extractDomain(referer)
|
||
|
||
now := time.Now()
|
||
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
|
||
if batch, exists := s.memoryStats[domain]; exists {
|
||
batch.Count++
|
||
batch.LastSeen = now
|
||
} else {
|
||
s.memoryStats[domain] = &DomainStatsBatch{
|
||
Count: 1,
|
||
LastSeen: now,
|
||
}
|
||
}
|
||
}
|
||
|
||
// shouldIgnorePath 判断是否应该忽略此路径的统计
|
||
func (s *DomainStatsService) shouldIgnorePath(path string) bool {
|
||
// 忽略管理后台API
|
||
if strings.HasPrefix(path, "/api/admin/") {
|
||
return true
|
||
}
|
||
|
||
// 忽略系统API
|
||
if strings.HasPrefix(path, "/api") {
|
||
return true
|
||
}
|
||
|
||
// 忽略静态文件路径
|
||
if strings.HasPrefix(path, "/_next/") ||
|
||
strings.HasPrefix(path, "/static/") ||
|
||
strings.HasPrefix(path, "/favicon.ico") ||
|
||
s.hasFileExtension(path) {
|
||
return true
|
||
}
|
||
|
||
// 忽略前端路由
|
||
if strings.HasPrefix(path, "/admin") {
|
||
return true
|
||
}
|
||
|
||
// 忽略根路径(通常是前端首页)
|
||
if path == "/" {
|
||
return true
|
||
}
|
||
|
||
// 其他路径都统计(包括所有API端点)
|
||
return false
|
||
}
|
||
|
||
// hasFileExtension 检查路径是否包含文件扩展名
|
||
func (s *DomainStatsService) hasFileExtension(path string) bool {
|
||
// 获取路径的最后一部分
|
||
parts := strings.Split(path, "/")
|
||
if len(parts) == 0 {
|
||
return false
|
||
}
|
||
|
||
lastPart := parts[len(parts)-1]
|
||
|
||
// 检查是否包含点号且不是隐藏文件
|
||
if strings.Contains(lastPart, ".") && !strings.HasPrefix(lastPart, ".") {
|
||
// 常见的文件扩展名
|
||
commonExts := []string{
|
||
".html", ".css", ".js", ".json", ".png", ".jpg", ".jpeg",
|
||
".gif", ".svg", ".ico", ".woff", ".woff2", ".ttf", ".eot",
|
||
".txt", ".xml", ".pdf", ".zip", ".mp4", ".mp3", ".webp",
|
||
}
|
||
|
||
for _, ext := range commonExts {
|
||
if strings.HasSuffix(strings.ToLower(lastPart), ext) {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
// startPeriodicSave 启动定期保存任务(每5分钟保存一次)
|
||
func (s *DomainStatsService) startPeriodicSave() {
|
||
ticker := time.NewTicker(5 * time.Minute)
|
||
defer ticker.Stop()
|
||
|
||
// 定期清理任务(每天执行一次)
|
||
cleanupTicker := time.NewTicker(24 * time.Hour)
|
||
defer cleanupTicker.Stop()
|
||
|
||
for {
|
||
select {
|
||
case <-ticker.C:
|
||
if err := s.flushToDatabase(); err != nil {
|
||
log.Printf("Failed to flush domain stats to database: %v", err)
|
||
}
|
||
case <-cleanupTicker.C:
|
||
if err := s.CleanupOldStats(); err != nil {
|
||
log.Printf("Failed to cleanup old domain stats: %v", err)
|
||
} else {
|
||
log.Println("Domain stats cleanup completed successfully")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// flushToDatabase 将内存中的统计数据保存到数据库
|
||
func (s *DomainStatsService) flushToDatabase() error {
|
||
s.mu.Lock()
|
||
currentStats := s.memoryStats
|
||
s.memoryStats = make(map[string]*DomainStatsBatch) // 重置内存统计
|
||
s.mu.Unlock()
|
||
|
||
if len(currentStats) == 0 {
|
||
return nil
|
||
}
|
||
|
||
return database.DB.Transaction(func(tx *gorm.DB) error {
|
||
today := time.Now().Truncate(24 * time.Hour)
|
||
|
||
for domain, batch := range currentStats {
|
||
// 更新总统计
|
||
var domainStats model.DomainStats
|
||
err := tx.Where("domain = ?", domain).First(&domainStats).Error
|
||
if err == gorm.ErrRecordNotFound {
|
||
// 创建新记录
|
||
domainStats = model.DomainStats{
|
||
Domain: domain,
|
||
Count: batch.Count,
|
||
LastSeen: batch.LastSeen,
|
||
}
|
||
if err := tx.Create(&domainStats).Error; err != nil {
|
||
return err
|
||
}
|
||
} else if err != nil {
|
||
return err
|
||
} else {
|
||
// 更新现有记录
|
||
domainStats.Count += batch.Count
|
||
domainStats.LastSeen = batch.LastSeen
|
||
if err := tx.Save(&domainStats).Error; err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
// 更新每日统计
|
||
var dailyStats model.DailyDomainStats
|
||
err = tx.Where("domain = ? AND date = ?", domain, today).First(&dailyStats).Error
|
||
if err == gorm.ErrRecordNotFound {
|
||
// 创建新记录
|
||
dailyStats = model.DailyDomainStats{
|
||
Domain: domain,
|
||
Date: today,
|
||
Count: batch.Count,
|
||
}
|
||
if err := tx.Create(&dailyStats).Error; err != nil {
|
||
return err
|
||
}
|
||
} else if err != nil {
|
||
return err
|
||
} else {
|
||
// 更新现有记录
|
||
dailyStats.Count += batch.Count
|
||
if err := tx.Save(&dailyStats).Error; err != nil {
|
||
return err
|
||
}
|
||
}
|
||
}
|
||
|
||
return nil
|
||
})
|
||
}
|
||
|
||
// GetTop24HourDomains 获取24小时内访问最多的前20个域名
|
||
func (s *DomainStatsService) GetTop24HourDomains() ([]model.DomainStatsResult, error) {
|
||
yesterday := time.Now().AddDate(0, 0, -1).Truncate(24 * time.Hour)
|
||
today := time.Now().Truncate(24 * time.Hour)
|
||
|
||
// 从数据库获取数据
|
||
var dbResults []model.DomainStatsResult
|
||
err := database.DB.Model(&model.DailyDomainStats{}).
|
||
Select("domain, SUM(count) as count").
|
||
Where("date >= ? AND date <= ?", yesterday, today).
|
||
Group("domain").
|
||
Scan(&dbResults).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 合并内存数据
|
||
s.mu.RLock()
|
||
memoryStats := make(map[string]uint64)
|
||
for domain, batch := range s.memoryStats {
|
||
memoryStats[domain] = batch.Count
|
||
}
|
||
s.mu.RUnlock()
|
||
|
||
// 创建域名计数映射
|
||
domainCounts := make(map[string]uint64)
|
||
|
||
// 添加数据库数据
|
||
for _, result := range dbResults {
|
||
domainCounts[result.Domain] += result.Count
|
||
}
|
||
|
||
// 添加内存数据
|
||
for domain, count := range memoryStats {
|
||
domainCounts[domain] += count
|
||
}
|
||
|
||
// 转换为结果切片并排序
|
||
var results []model.DomainStatsResult
|
||
for domain, count := range domainCounts {
|
||
results = append(results, model.DomainStatsResult{
|
||
Domain: domain,
|
||
Count: count,
|
||
})
|
||
}
|
||
|
||
// 按访问次数降序排序
|
||
for i := 0; i < len(results)-1; i++ {
|
||
for j := i + 1; j < len(results); j++ {
|
||
if results[i].Count < results[j].Count {
|
||
results[i], results[j] = results[j], results[i]
|
||
}
|
||
}
|
||
}
|
||
|
||
// 限制返回前20个
|
||
if len(results) > 30 {
|
||
results = results[:30]
|
||
}
|
||
|
||
return results, nil
|
||
}
|
||
|
||
// GetTopTotalDomains 获取总访问最多的前20个域名
|
||
func (s *DomainStatsService) GetTopTotalDomains() ([]model.DomainStatsResult, error) {
|
||
// 从数据库获取数据
|
||
var dbResults []model.DomainStatsResult
|
||
err := database.DB.Model(&model.DomainStats{}).
|
||
Select("domain, count").
|
||
Scan(&dbResults).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 合并内存数据
|
||
s.mu.RLock()
|
||
memoryStats := make(map[string]uint64)
|
||
for domain, batch := range s.memoryStats {
|
||
memoryStats[domain] = batch.Count
|
||
}
|
||
s.mu.RUnlock()
|
||
|
||
// 创建域名计数映射
|
||
domainCounts := make(map[string]uint64)
|
||
|
||
// 添加数据库数据
|
||
for _, result := range dbResults {
|
||
domainCounts[result.Domain] += result.Count
|
||
}
|
||
|
||
// 添加内存数据
|
||
for domain, count := range memoryStats {
|
||
domainCounts[domain] += count
|
||
}
|
||
|
||
// 转换为结果切片并排序
|
||
var results []model.DomainStatsResult
|
||
for domain, count := range domainCounts {
|
||
results = append(results, model.DomainStatsResult{
|
||
Domain: domain,
|
||
Count: count,
|
||
})
|
||
}
|
||
|
||
// 按访问次数降序排序
|
||
for i := 0; i < len(results)-1; i++ {
|
||
for j := i + 1; j < len(results); j++ {
|
||
if results[i].Count < results[j].Count {
|
||
results[i], results[j] = results[j], results[i]
|
||
}
|
||
}
|
||
}
|
||
|
||
// 限制返回前20个
|
||
if len(results) > 30 {
|
||
results = results[:30]
|
||
}
|
||
|
||
return results, nil
|
||
}
|
||
|
||
// CleanupOldStats 清理旧的统计数据(保留最近30天的每日统计)
|
||
func (s *DomainStatsService) CleanupOldStats() error {
|
||
cutoffDate := time.Now().AddDate(0, 0, -30).Truncate(24 * time.Hour)
|
||
return database.DB.Where("date < ?", cutoffDate).Delete(&model.DailyDomainStats{}).Error
|
||
}
|