Files
nl-blogs/server/repositories/user_access_log_repository.go
2026-01-20 15:23:37 +08:00

212 lines
6.7 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"
"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
}
// 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
}