236 lines
7.4 KiB
Go
236 lines
7.4 KiB
Go
package repositories
|
||
|
||
import (
|
||
"log"
|
||
"regexp"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/niangaodev/art-code/config"
|
||
"github.com/niangaodev/art-code/models"
|
||
)
|
||
|
||
// CreateUserAccessLog 创建用户访问日志
|
||
func CreateUserAccessLog(logEntry *models.UserAccessLog) error {
|
||
if logEntry.AccessTime == 0 {
|
||
logEntry.AccessTime = time.Now().Unix()
|
||
}
|
||
err := config.DB.Create(logEntry).Error
|
||
if err != nil {
|
||
log.Printf("Error creating user access log: %v", err)
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// HasVisitedThisHour checks if the visitor already has an access log for this article in the current hour.
|
||
func HasVisitedThisHour(articleID, userID uint, visitorKey string) (bool, error) {
|
||
hourStart := time.Now().Truncate(time.Hour).Unix()
|
||
hourEnd := hourStart + 3600
|
||
|
||
var count int64
|
||
query := config.DB.Model(&models.UserAccessLog{}).
|
||
Where("deleted_at = ?", 0).
|
||
Where("article_id = ?", articleID).
|
||
Where("access_time >= ? AND access_time < ?", hourStart, hourEnd)
|
||
|
||
if userID > 0 {
|
||
query = query.Where("user_id = ?", userID)
|
||
} else {
|
||
query = query.Where("visitor_key = ?", visitorKey)
|
||
}
|
||
|
||
if err := query.Count(&count).Error; err != nil {
|
||
return false, err
|
||
}
|
||
|
||
return count > 0, nil
|
||
}
|
||
|
||
// AccessStats 访问统计数据结构
|
||
type AccessStats struct {
|
||
Date string `json:"date"`
|
||
Count int `json:"count"`
|
||
}
|
||
|
||
// GetArticleAccessTrend 获取文章访问趋势
|
||
func GetArticleAccessTrend(startDate, endDate string) ([]AccessStats, error) {
|
||
query := config.DB.Model(&models.UserAccessLog{}).
|
||
Select("FROM_UNIXTIME(access_time, '%Y-%m-%d') as date, COUNT(*) as count")
|
||
|
||
if startDate != "" {
|
||
startUnix := parseDateToUnix(startDate, false)
|
||
query = query.Where("access_time >= ?", startUnix)
|
||
}
|
||
if endDate != "" {
|
||
endUnix := parseDateToUnix(endDate, true)
|
||
query = query.Where("access_time <= ?", endUnix)
|
||
}
|
||
|
||
var results []AccessStats
|
||
err := query.Group("date").
|
||
Order("date ASC").
|
||
Scan(&results).Error
|
||
|
||
return results, err
|
||
}
|
||
|
||
// GetTopArticlesByAccess 获取访问量最高的文章
|
||
func GetTopArticlesByAccess(limit int) ([]struct {
|
||
ArticleID int `json:"article_id"`
|
||
Title string `json:"title"`
|
||
Count int `json:"count"`
|
||
}, error) {
|
||
var results []struct {
|
||
ArticleID int `json:"article_id"`
|
||
Title string `json:"title"`
|
||
Count int `json:"count"`
|
||
}
|
||
err := config.DB.Model(&models.UserAccessLog{}).
|
||
Select("user_access_logs.article_id, posts.title, COUNT(*) as count").
|
||
Joins("JOIN posts ON user_access_logs.article_id = posts.id").
|
||
Group("user_access_logs.article_id, posts.title").
|
||
Order("count DESC").
|
||
Limit(limit).
|
||
Scan(&results).Error
|
||
|
||
return results, err
|
||
}
|
||
|
||
// GetPostAccessLogsFromUserAccessLogs 获取指定文章的访问记录(从 user_access_logs 表)
|
||
func GetPostAccessLogsFromUserAccessLogs(postID int, page, pageSize int) ([]models.UserAccessLog, int64, error) {
|
||
query := config.DB.Model(&models.UserAccessLog{}).
|
||
Where("deleted_at = ?", 0).
|
||
Where("article_id = ?", postID)
|
||
|
||
var total int64
|
||
if err := query.Count(&total).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
var logs []models.UserAccessLog
|
||
offset := (page - 1) * pageSize
|
||
err := query.Order("access_time DESC").
|
||
Limit(pageSize).
|
||
Offset(offset).
|
||
Find(&logs).Error
|
||
|
||
return logs, total, err
|
||
}
|
||
|
||
// GetUserRegionsFromUserAccessLogs 获取用户地域分布(使用 user_access_logs 表的 user_location 字段)
|
||
// 返回所有34个省份的数据,没有数据的省份 Count 设为 0
|
||
func GetUserRegionsFromUserAccessLogs(startDate, endDate string) ([]struct {
|
||
Region string
|
||
Count int
|
||
}, error) {
|
||
query := config.DB.Model(&models.UserAccessLog{}).
|
||
Select("COALESCE(NULLIF(user_location, ''), 'Unknown') as region, COUNT(DISTINCT user_ip) as count").
|
||
Where("deleted_at = ?", 0)
|
||
|
||
if startDate != "" {
|
||
startUnix := parseDateToUnix(startDate, false)
|
||
query = query.Where("access_time >= ?", startUnix)
|
||
}
|
||
|
||
if endDate != "" {
|
||
endUnix := parseDateToUnix(endDate, true)
|
||
query = query.Where("access_time <= ?", 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
|
||
log.Printf("Extracted province: %s from region: %s, count: %d", province, r.Region, r.Count)
|
||
} else {
|
||
log.Printf("Failed to extract province from region: %s", r.Region)
|
||
}
|
||
}
|
||
|
||
// 定义完整的省份名称列表(包含"省"、"市"、"自治区"等后缀)
|
||
allProvincesFullNames := []string{
|
||
"北京市", "天津市", "河北省", "山西省", "内蒙古自治区", "辽宁省", "吉林省", "黑龙江省",
|
||
"上海市", "江苏省", "浙江省", "安徽省", "福建省", "江西省", "山东省", "河南省",
|
||
"湖北省", "湖南省", "广东省", "广西壮族自治区", "海南省", "重庆市", "四川省", "贵州省",
|
||
"云南省", "西藏自治区", "陕西省", "甘肃省", "青海省", "宁夏回族自治区", "新疆维吾尔自治区", "台湾省", "香港特别行政区", "澳门特别行政区",
|
||
}
|
||
|
||
// 为所有省份生成数据,没有数据的设为0
|
||
var finalResults []struct {
|
||
Region string
|
||
Count int
|
||
}
|
||
for _, province := range allProvincesFullNames {
|
||
count := provinceMap[province]
|
||
finalResults = append(finalResults, struct {
|
||
Region string
|
||
Count int
|
||
}{
|
||
Region: province,
|
||
Count: count,
|
||
})
|
||
}
|
||
|
||
return finalResults, nil
|
||
}
|
||
|
||
// 所有省份列表(34个)- 用于用户访问日志统计
|
||
var allProvincesForUserAccess = []string{
|
||
"北京", "天津", "河北", "山西", "内蒙古", "辽宁", "吉林", "黑龙江",
|
||
"上海", "江苏", "浙江", "安徽", "福建", "江西", "山东", "河南",
|
||
"湖北", "湖南", "广东", "广西", "海南", "重庆", "四川", "贵州",
|
||
"云南", "西藏", "陕西", "甘肃", "青海", "宁夏", "新疆", "台湾", "香港", "澳门",
|
||
}
|
||
|
||
// extractProvinceFromRegion 从归属地字符串中提取省份信息
|
||
// 格式: 国家|省份|城市|ISP
|
||
// 例如: 中国|浙江省|杭州市|电信 -> 浙江省
|
||
// 注意:返回完整的省份名称,包括"省"、"市"、"自治区"等后缀,以匹配地图数据
|
||
func extractProvinceFromRegion(region string) string {
|
||
if region == "" || region == "Unknown" || region == "Internal" {
|
||
return "未知"
|
||
}
|
||
|
||
// 使用正则表达式验证格式: 国家|省份|城市|ISP
|
||
// 匹配格式: 至少包含"国家|省份"两部分
|
||
regionPattern := regexp.MustCompile(`^[^|]+\|[^|]+`)
|
||
if !regionPattern.MatchString(region) {
|
||
log.Printf("Invalid region format (regex validation failed): %s", region)
|
||
return "未知"
|
||
}
|
||
|
||
parts := strings.Split(region, "|")
|
||
// 格式应该是: 国家|省份|城市|ISP (至少需要2个部分)
|
||
if len(parts) < 2 {
|
||
log.Printf("Invalid region format (insufficient parts): %s, parts: %v", region, parts)
|
||
return "未知"
|
||
}
|
||
|
||
province := strings.TrimSpace(parts[1]) // 省份在索引1
|
||
if province == "" || province == "0" {
|
||
log.Printf("Invalid province in region: %s, province part: %s", region, province)
|
||
return "未知"
|
||
}
|
||
|
||
// 直接返回完整的省份名称,不移除"省"、"市"、"自治区"等后缀
|
||
// 这样可以直接匹配地图数据中的省份名称
|
||
return province
|
||
}
|