326 lines
9.2 KiB
Go
326 lines
9.2 KiB
Go
package repositories
|
||
|
||
import (
|
||
// "log"
|
||
"fmt"
|
||
"net/url"
|
||
"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
|
||
}
|
||
|
||
// 处理URL编码格式:先尝试URL解码,然后将+替换为空格
|
||
decoded := dateStr
|
||
if decodedStr, err := url.QueryUnescape(dateStr); err == nil {
|
||
decoded = decodedStr
|
||
}
|
||
// URL编码中+可能代表空格,需要替换
|
||
decoded = strings.ReplaceAll(decoded, "+", " ")
|
||
|
||
// Try parsing with time first
|
||
t, err := time.ParseInLocation("2006-01-02 15:04", decoded, 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", decoded, 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, filter AccessLogFilter) ([]models.AccessLog, int64, error) {
|
||
query := config.DB.Model(&models.AccessLog{}).
|
||
Where("deleted_at = ?", 0)
|
||
|
||
// 如果指定了文章ID,筛选该文章的访问记录
|
||
if postID != nil {
|
||
// 路径格式可能是:
|
||
// - /api/posts/{id} (API调用)
|
||
// - /blog/{id} (前端页面访问)
|
||
// - /posts/{id} (其他可能的格式)
|
||
// 使用精确的匹配,避免匹配到其他ID(如123匹配到1234)
|
||
// 使用LIKE模式,确保ID后面是 / 或 ? 或 字符串结束
|
||
postIDStr := fmt.Sprintf("%d", *postID)
|
||
// 匹配精确的路径格式,避免部分匹配
|
||
// 例如:/api/posts/123 或 /api/posts/123/ 或 /api/posts/123?xxx
|
||
// 但不匹配 /api/posts/1234
|
||
// 使用精确的 LIKE 匹配,确保只匹配正确的文章ID
|
||
// 匹配格式:/api/posts/{id}、/api/posts/{id}/、/api/posts/{id}?xxx
|
||
// 匹配格式:/blog/{id}、/blog/{id}/、/blog/{id}?xxx
|
||
// 匹配格式:/posts/{id}、/posts/{id}/、/posts/{id}?xxx
|
||
query = query.Where(
|
||
"(path = ? OR path LIKE ? OR path LIKE ? OR "+
|
||
"path = ? OR path LIKE ? OR path LIKE ? OR "+
|
||
"path = ? OR path LIKE ? OR path LIKE ?)",
|
||
// /api/posts/{id} 格式(3个参数)
|
||
fmt.Sprintf("/api/posts/%s", postIDStr),
|
||
fmt.Sprintf("/api/posts/%s/%%", postIDStr),
|
||
fmt.Sprintf("/api/posts/%s?%%", postIDStr),
|
||
// /blog/{id} 格式(3个参数)
|
||
fmt.Sprintf("/blog/%s", postIDStr),
|
||
fmt.Sprintf("/blog/%s/%%", postIDStr),
|
||
fmt.Sprintf("/blog/%s?%%", postIDStr),
|
||
// /posts/{id} 格式(3个参数)
|
||
fmt.Sprintf("/posts/%s", postIDStr),
|
||
fmt.Sprintf("/posts/%s/%%", postIDStr),
|
||
fmt.Sprintf("/posts/%s?%%", postIDStr),
|
||
)
|
||
}
|
||
|
||
if filter.Path != "" {
|
||
query = query.Where("path LIKE ?", "%"+filter.Path+"%")
|
||
}
|
||
if filter.Region != "" {
|
||
query = query.Where("region LIKE ?", "%"+filter.Region+"%")
|
||
}
|
||
if filter.StartDate != "" {
|
||
startUnix := parseDateToUnix(filter.StartDate, false)
|
||
if startUnix > 0 {
|
||
query = query.Where("created_at >= ?", startUnix)
|
||
}
|
||
}
|
||
if filter.EndDate != "" {
|
||
endUnix := parseDateToUnix(filter.EndDate, true)
|
||
if endUnix > 0 {
|
||
query = query.Where("created_at <= ?", endUnix)
|
||
}
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
// GetPostAccessLogsFromAccessLogs 从 access_logs 表获取指定文章的访问记录(已废弃,应使用 user_access_logs)
|
||
// 保留此函数以保持向后兼容,但建议使用 user_access_log_repository.GetPostAccessLogs
|
||
func GetPostAccessLogsFromAccessLogs(postID int, page, pageSize int) ([]models.AccessLog, int64, error) {
|
||
return GetAccessLogs(page, pageSize, &postID, AccessLogFilter{})
|
||
}
|
||
|
||
// 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{
|
||
"北京", "天津", "河北", "山西", "内蒙古", "辽宁", "吉林", "黑龙江",
|
||
"上海", "江苏", "浙江", "安徽", "福建", "江西", "山东", "河南",
|
||
"湖北", "湖南", "广东", "广西", "海南", "重庆", "四川", "贵州",
|
||
"云南", "西藏", "陕西", "甘肃", "青海", "宁夏", "新疆", "台湾", "香港", "澳门",
|
||
}
|
||
|
||
// GetUserRegionsFromAccessLogs 获取用户地域分布(使用 access_logs 表的 region 字段)
|
||
// 返回所有34个省份的数据,没有数据的省份 Count 设为 0
|
||
// 注意:此函数查询的是所有访问记录,包括静态资源、API调用等
|
||
// 建议使用 GetUserRegionsFromUserAccessLogs 获取更准确的用户访问统计
|
||
func GetUserRegionsFromAccessLogs(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
|
||
}
|
||
|
||
// GetDefaultUserRegions 返回所有34个省份的默认数据(Count为0)
|
||
func GetDefaultUserRegions() []struct {
|
||
Region string
|
||
Count int
|
||
} {
|
||
var defaultResults []struct {
|
||
Region string
|
||
Count int
|
||
}
|
||
for _, province := range allProvinces {
|
||
defaultResults = append(defaultResults, struct {
|
||
Region string
|
||
Count int
|
||
}{
|
||
Region: province,
|
||
Count: 0,
|
||
})
|
||
}
|
||
return defaultResults
|
||
}
|