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 "未知" } // 所有省份列表(34个) var allProvinces = []string{ "北京", "天津", "河北", "山西", "内蒙古", "辽宁", "吉林", "黑龙江", "上海", "江苏", "浙江", "安徽", "福建", "江西", "山东", "河南", "湖北", "湖南", "广东", "广西", "海南", "重庆", "四川", "贵州", "云南", "西藏", "陕西", "甘肃", "青海", "宁夏", "新疆", "台湾", "香港", "澳门", } // GetUserRegions 获取用户地域分布(使用 access_logs 表的 region 字段) // 返回所有34个省份的数据,没有数据的省份 Count 设为 0 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) if province != "未知" { provinceMap[province] += r.Count } } // 为所有省份生成数据,没有数据的设为0 var finalResults []struct { Region string Count int } for _, province := range allProvinces { count := provinceMap[province] finalResults = append(finalResults, struct { Region string Count int }{ Region: province, Count: count, }) } return finalResults, nil }