242 lines
6.1 KiB
Go
242 lines
6.1 KiB
Go
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
|
||
}
|