Files
nl-blogs/server/repositories/log_repository.go
2026-01-20 11:00:42 +08:00

242 lines
6.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package repositories
import (
// "log"
"fmt"
"strings"
"time"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
)
// Helper to parse date string to unix timestamp
func parseDateToUnix(dateStr string, isEnd bool) int64 {
if dateStr == "" {
return 0
}
// Try parsing with time first
t, err := time.ParseInLocation("2006-01-02 15:04", dateStr, time.Local)
if err == nil {
if isEnd {
// HH:mm:59
return t.Add(59 * time.Second).Unix()
}
return t.Unix()
}
// Try parsing just date
t, err = time.ParseInLocation("2006-01-02", dateStr, time.Local)
if err == nil {
if isEnd {
// 23:59:59
return t.Add(24*time.Hour - 1*time.Second).Unix()
}
return t.Unix()
}
return 0
}
// CreateAccessLog 创建访问日志
func CreateAccessLog(log *models.AccessLog) error {
return config.DB.Create(log).Error
}
// UVTrendData UV趋势数据
type UVTrendData struct {
Date string `json:"date"`
Count int `json:"value"`
YoY float64 `json:"yoy"`
MoM float64 `json:"mom"`
}
// GetDailyUV 获取UV趋势
func GetDailyUV(startDate, endDate string) ([]UVTrendData, error) {
// 确定日期范围
var startTime, endTime time.Time
if startDate != "" {
startUnix := parseDateToUnix(startDate, false)
startTime = time.Unix(startUnix, 0)
} else {
startTime = time.Now().AddDate(0, 0, -6)
startTime = time.Date(startTime.Year(), startTime.Month(), startTime.Day(), 0, 0, 0, 0, time.Local)
}
if endDate != "" {
endUnix := parseDateToUnix(endDate, true)
endTime = time.Unix(endUnix, 0)
} else {
endTime = time.Now()
endTime = time.Date(endTime.Year(), endTime.Month(), endTime.Day(), 23, 59, 59, 0, time.Local)
}
// 查询数据库
query := config.DB.Model(&models.UserAccessLog{}).
Select("FROM_UNIXTIME(access_time, '%Y-%m-%d') as date, COUNT(DISTINCT user_ip) as count").
Where("access_time >= ?", startTime.Unix()).
Where("access_time <= ?", endTime.Unix())
var results []UVTrendData
err := query.Group("date").
Order("date ASC").
Scan(&results).Error
if err != nil {
return nil, err
}
// 创建日期到数据的映射
resultMap := make(map[string]UVTrendData)
for _, r := range results {
resultMap[r.Date] = r
}
// 生成完整日期列表
var fullResults []UVTrendData
current := time.Date(startTime.Year(), startTime.Month(), startTime.Day(), 0, 0, 0, 0, time.Local)
endDay := time.Date(endTime.Year(), endTime.Month(), endTime.Day(), 0, 0, 0, 0, time.Local)
for !current.After(endDay) {
dateStr := current.Format("2006-01-02")
if data, exists := resultMap[dateStr]; exists {
data.YoY = 0
data.MoM = 0
fullResults = append(fullResults, data)
} else {
// 填充0值
fullResults = append(fullResults, UVTrendData{
Date: dateStr,
Count: 0,
YoY: 0,
MoM: 0,
})
}
current = current.AddDate(0, 0, 1)
}
return fullResults, nil
}
// GetAccessLogs 获取访问日志列表(支持分页和筛选)
func GetAccessLogs(page, pageSize int, postID *int) ([]models.AccessLog, int64, error) {
query := config.DB.Model(&models.AccessLog{}).
Where("deleted_at = ?", 0)
// 如果指定了文章ID筛选该文章的访问记录
if postID != nil {
// 路径格式可能是 /api/posts/{id} 或 /blog/{id} 等
// 使用 LIKE 匹配包含文章ID的路径
query = query.Where("path LIKE ?", fmt.Sprintf("%%/posts/%d%%", *postID))
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
var logs []models.AccessLog
offset := (page - 1) * pageSize
err := query.Order("created_at DESC").
Limit(pageSize).
Offset(offset).
Find(&logs).Error
return logs, total, err
}
// GetPostAccessLogs 获取指定文章的访问记录
func GetPostAccessLogs(postID int, page, pageSize int) ([]models.AccessLog, int64, error) {
return GetAccessLogs(page, pageSize, &postID)
}
// ExtractProvinceFromRegion 从归属地字符串中提取省份信息
// 格式: 国家|区域|省份|城市|ISP
// 例如: 中国|0|浙江|杭州市|联通 -> 浙江
func ExtractProvinceFromRegion(region string) string {
if region == "" || region == "Unknown" || region == "Internal" {
return "未知"
}
parts := strings.Split(region, "|")
if len(parts) >= 3 {
province := strings.TrimSpace(parts[2])
if province != "" && province != "0" {
// 移除"省"、"市"、"自治区"等后缀,统一格式
province = strings.TrimSuffix(province, "省")
province = strings.TrimSuffix(province, "市")
province = strings.TrimSuffix(province, "自治区")
province = strings.TrimSuffix(province, "特别行政区")
return province
}
}
return "未知"
}
// GetUserRegions 获取用户地域分布(使用 access_logs 表的 region 字段)
func GetUserRegions(startDate, endDate string) ([]struct {
Region string
Count int
}, error) {
query := config.DB.Model(&models.AccessLog{}).
Select("COALESCE(NULLIF(region, ''), 'Unknown') as region, COUNT(DISTINCT ip) as count").
Where("deleted_at = ?", 0)
if startDate != "" {
startUnix := parseDateToUnix(startDate, false)
query = query.Where("created_at >= ?", startUnix)
}
if endDate != "" {
endUnix := parseDateToUnix(endDate, true)
query = query.Where("created_at <= ?", endUnix)
}
var results []struct {
Region string
Count int
}
err := query.Group("region").
Order("count DESC").
Limit(50).
Scan(&results).Error
if err != nil {
return nil, err
}
// 解析归属地,提取省份信息并聚合
provinceMap := make(map[string]int)
for _, r := range results {
province := ExtractProvinceFromRegion(r.Region)
provinceMap[province] += r.Count
}
// 转换为结果格式
var finalResults []struct {
Region string
Count int
}
for province, count := range provinceMap {
finalResults = append(finalResults, struct {
Region string
Count int
}{
Region: province,
Count: count,
})
}
// 按数量排序
for i := 0; i < len(finalResults)-1; i++ {
for j := i + 1; j < len(finalResults); j++ {
if finalResults[i].Count < finalResults[j].Count {
finalResults[i], finalResults[j] = finalResults[j], finalResults[i]
}
}
}
return finalResults, nil
}