package pg import ( "context" "fmt" "sort" "strconv" "time" "github.com/samber/lo" v1 "github.com/chaitin/panda-wiki/api/stat/v1" "github.com/chaitin/panda-wiki/domain" "github.com/chaitin/panda-wiki/utils" ) func (r *StatRepository) GetConversationCountOneHour(ctx context.Context, kbID string) (int64, error) { var conversationCount int64 if err := r.db.WithContext(ctx). Model(&domain.Conversation{}). Where("kb_id = ?", kbID). Where("created_at >= ? AND created_at < ?", utils.GetTimeHourOffset(-1), utils.GetTimeHourOffset(0)). Count(&conversationCount).Error; err != nil { return conversationCount, err } return conversationCount, nil } func (r *StatRepository) GetStatPageOneHour(ctx context.Context, kbID string) (*domain.StatPageHour, error) { var statPageHour domain.StatPageHour err := r.db.WithContext(ctx).Table("stat_pages"). Select(` COUNT(DISTINCT ip) as ip_count, COUNT(DISTINCT session_id) as session_count, COUNT(*) as page_visit_count `). Where("created_at >= ? AND created_at < ?", utils.GetTimeHourOffset(-1), utils.GetTimeHourOffset(0)). Where("kb_id = ?", kbID). Find(&statPageHour).Error if err != nil { return nil, err } return &statPageHour, nil } func (r *StatRepository) GetGeCountOneHour(ctx context.Context, kbID string) (map[string]int64, error) { key := fmt.Sprintf("geo:%s:%s", kbID, time.Now().Add(-time.Duration(1)*time.Hour).Format("2006-01-02-15")) values, err := r.cache.HGetAll(ctx, key).Result() if err != nil { return nil, err } geoCount := make(map[string]int64) for field, value := range values { valueInt, err := strconv.ParseInt(value, 10, 64) if err != nil { return nil, fmt.Errorf("parse geo count failed: %w", err) } geoCount[field] += valueInt } return geoCount, nil } func (r *StatRepository) GetConversationDistributionOneHour(ctx context.Context, kbID string) (map[string]int64, error) { var cds []domain.ConversationDistribution if err := r.db.WithContext(ctx). Model(&domain.Conversation{}). Select("apps.type as app_type", "COUNT(*) as count"). Joins("left join apps on apps.id=conversations.app_id"). Where("conversations.kb_id = ?", kbID). Where("conversations.created_at >= ? AND conversations.created_at < ?", utils.GetTimeHourOffset(-1), utils.GetTimeHourOffset(0)). Group("apps.type"). Find(&cds).Error; err != nil { return nil, err } if len(cds) == 0 { return make(map[string]int64), nil } dcCount := lo.SliceToMap(cds, func(cd domain.ConversationDistribution) (string, int64) { return strconv.Itoa(int(cd.AppType)), cd.Count }) return dcCount, nil } func (r *StatRepository) GetHotRefererHostOneHour(ctx context.Context, kbID string) (map[string]int64, error) { var hotRefererHosts []*domain.HotRefererHost if err := r.db.WithContext(ctx).Model(&domain.StatPage{}). Where("kb_id = ?", kbID). Where("created_at >= ? AND created_at < ?", utils.GetTimeHourOffset(-1), utils.GetTimeHourOffset(0)). Group("referer_host"). Select("referer_host, COUNT(*) as count"). Order("count DESC"). Limit(10). Find(&hotRefererHosts).Error; err != nil { return nil, err } if len(hotRefererHosts) == 0 { return make(map[string]int64), nil } refererHostCount := lo.SliceToMap(hotRefererHosts, func(item *domain.HotRefererHost) (string, int64) { return item.RefererHost, item.Count }) return refererHostCount, nil } func (r *StatRepository) GetHotRefererHostsByHour(ctx context.Context, kbID string, startHour int64) (map[string]int64, error) { // 查询实时数据 var hotRefererHosts []*domain.HotRefererHost if err := r.db.WithContext(ctx).Model(&domain.StatPage{}). Where("kb_id = ?", kbID). Where("referer_host != '' "). Where("created_at > ?", utils.GetTimeHourOffset(-24)). Group("referer_host"). Select("referer_host, COUNT(*) as count"). Order("count DESC"). Limit(10). Find(&hotRefererHosts).Error; err != nil { return nil, err } // 查询小时统计表中的聚合数据 statPageHours := make([]domain.StatPageHour, 0) if err := r.db.WithContext(ctx).Model(&domain.StatPageHour{}). Select("hot_referer_host"). Where("kb_id = ?", kbID). Where("hour >= ? and hour < ?", utils.GetTimeHourOffset(-startHour), utils.GetTimeHourOffset(-24)). Find(&statPageHours).Error; err != nil { return nil, err } // 聚合小时统计数据 refererHostCountMap := make(map[string]int64) for i := range statPageHours { for k, v := range statPageHours[i].HotRefererHost { refererHostCountMap[k] += v } } // 合并实时数据和聚合数据 finalRefererHostCount := make(map[string]int64) for _, item := range hotRefererHosts { finalRefererHostCount[item.RefererHost] = item.Count } for host, count := range refererHostCountMap { if host != "" { finalRefererHostCount[host] += count } } return finalRefererHostCount, nil } func (r *StatRepository) CreateStatPageHour(ctx context.Context, statPageHour *domain.StatPageHour) error { return r.db.WithContext(ctx).Create(statPageHour).Error } // CheckStatPageHourExists 检查指定时间和知识库的小时统计数据是否已存在 func (r *StatRepository) CheckStatPageHourExists(ctx context.Context, kbID string, hour time.Time) (bool, error) { var count int64 err := r.db.WithContext(ctx).Model(&domain.StatPageHour{}). Where("kb_id = ? AND hour = ?", kbID, hour). Count(&count).Error if err != nil { return false, err } return count > 0, nil } // CleanupOldHourlyStats 清理90天前的小时统计数据 func (r *StatRepository) CleanupOldHourlyStats(ctx context.Context) error { return r.db.WithContext(ctx).Model(&domain.StatPageHour{}). Where("hour < NOW() - INTERVAL '90 days'"). Delete(&domain.StatPageHour{}).Error } func (r *StatRepository) GetHotPagesOneHour(ctx context.Context, kbID string) (map[string]int64, error) { var hotPages []*domain.HotPage if err := r.db.WithContext(ctx).Model(&domain.StatPage{}). Where("kb_id = ?", kbID). Where("node_id != '' "). Where("scene = ?", domain.StatPageSceneNodeDetail). Where("created_at >= ? AND created_at < ?", utils.GetTimeHourOffset(-1), utils.GetTimeHourOffset(0)). Group("node_id"). Select("node_id, COUNT(*) as count"). Order("count DESC"). Find(&hotPages).Error; err != nil { return nil, err } if len(hotPages) == 0 { return make(map[string]int64), nil } refererHostCount := lo.SliceToMap(hotPages, func(item *domain.HotPage) (string, int64) { return item.NodeID, item.Count }) return refererHostCount, nil } func (r *StatRepository) GetHotPagesByHour(ctx context.Context, kbID string, startHour int64) (map[string]int64, error) { // 查询小时统计表中的聚合数据 counts := make(map[string]int64) hotPageMaps := make([]domain.MapStrInt64, 0) if err := r.db.WithContext(ctx).Model(&domain.StatPageHour{}). Where("kb_id = ?", kbID). Where("hot_page != '{}'"). Where("hour >= ? and hour < ?", utils.GetTimeHourOffset(-startHour), utils.GetTimeHourOffset(-24)). Pluck("hot_page", &hotPageMaps).Error; err != nil { return nil, err } for i := range hotPageMaps { for k, v := range hotPageMaps[i] { counts[k] += v } } return counts, nil } func (r *StatRepository) GetHotBrowsersOneHour(ctx context.Context, kbID string) (map[string]int64, error) { var browserCount []domain.BrowserCount query := r.db.WithContext(ctx).Model(&domain.StatPage{}). Where("kb_id = ?", kbID). Where("created_at >= ? AND created_at < ?", utils.GetTimeHourOffset(-1), utils.GetTimeHourOffset(0)). Group("browser_name"). Select("browser_name as name, COUNT(*) as count") if err := query.Order("count DESC").Limit(10).Find(&browserCount).Error; err != nil { return nil, err } if len(browserCount) == 0 { return make(map[string]int64), nil } refererHostCount := lo.SliceToMap(browserCount, func(item domain.BrowserCount) (string, int64) { return item.Name, item.Count }) return refererHostCount, nil } func (r *StatRepository) GetHotOSOneHour(ctx context.Context, kbID string) (map[string]int64, error) { var osCount []domain.BrowserCount query := r.db.WithContext(ctx).Model(&domain.StatPage{}). Where("kb_id = ?", kbID). Where("created_at >= ? AND created_at < ?", utils.GetTimeHourOffset(-1), utils.GetTimeHourOffset(0)). Group("browser_os"). Select("browser_os as name, COUNT(*) as count") if err := query.Order("count DESC").Limit(10).Find(&osCount).Error; err != nil { return nil, err } if len(osCount) == 0 { return make(map[string]int64), nil } refererOSCount := lo.SliceToMap(osCount, func(item domain.BrowserCount) (string, int64) { return item.Name, item.Count }) return refererOSCount, nil } func (r *StatRepository) GetStatPageCountByHour(ctx context.Context, kbID string, startHour int64) (*v1.StatCountResp, error) { var count v1.StatCountResp if err := r.db.WithContext(ctx).Model(&domain.StatPageHour{}). Select("SUM(ip_count) as ip_count, SUM(session_count) as session_count, SUM(page_visit_count) as page_visit_count, SUM(conversation_count) as conversation_count"). Where("kb_id = ?", kbID). Where("hour >= ? and hour < ?", utils.GetTimeHourOffset(-startHour), utils.GetTimeHourOffset(-24)). Scan(&count).Error; err != nil { return nil, err } return &count, nil } func (r *StatRepository) GetHotBrowsersByHour(ctx context.Context, kbID string, startHour int64) (*domain.HotBrowser, error) { var browserCount []domain.BrowserCount query := r.db.WithContext(ctx).Model(&domain.StatPage{}). Where("kb_id = ?", kbID). Where("created_at > ?", utils.GetTimeHourOffset(-24)). Where("browser_name != '' "). Group("browser_name"). Select("browser_name as name, COUNT(*) as count") if err := query.Order("count DESC").Find(&browserCount).Error; err != nil { return nil, err } var osCount []domain.BrowserCount query = r.db.WithContext(ctx).Model(&domain.StatPage{}). Where("kb_id = ?", kbID). Where("created_at > ?", utils.GetTimeHourOffset(-24)). Where("browser_os != '' "). Group("browser_os"). Select("browser_os as name, COUNT(*) as count") if err := query.Order("count DESC").Find(&osCount).Error; err != nil { return nil, err } statPageHours := make([]domain.StatPageHour, 0) if err := r.db.WithContext(ctx).Model(&domain.StatPageHour{}). Select("hot_os, hot_browser"). Where("kb_id = ?", kbID). Where("hour >= ? and hour < ?", utils.GetTimeHourOffset(-startHour), utils.GetTimeHourOffset(-24)). Find(&statPageHours).Error; err != nil { return nil, err } hourBrowserCountMap := make(domain.MapStrInt64) hourOSCountMap := make(domain.MapStrInt64) for i := range statPageHours { for k, v := range statPageHours[i].HotOS { if k != "" { hourOSCountMap[k] += v } } for k, v := range statPageHours[i].HotBrowser { if k != "" { hourBrowserCountMap[k] += v } } } for i := range browserCount { hourBrowserCountMap[browserCount[i].Name] += browserCount[i].Count } for i := range osCount { hourOSCountMap[osCount[i].Name] += osCount[i].Count } browserCount = lo.MapToSlice(hourBrowserCountMap, func(k string, v int64) domain.BrowserCount { return domain.BrowserCount{ Name: k, Count: v, } }) osCount = lo.MapToSlice(hourOSCountMap, func(k string, v int64) domain.BrowserCount { return domain.BrowserCount{ Name: k, Count: v, } }) // Sort browserCount by count in descending order and take top 10 sort.Slice(browserCount, func(i, j int) bool { return browserCount[i].Count > browserCount[j].Count }) if len(browserCount) > 10 { browserCount = browserCount[:10] } // Sort osCount by count in descending order and take top 10 sort.Slice(osCount, func(i, j int) bool { return osCount[i].Count > osCount[j].Count }) if len(osCount) > 10 { osCount = osCount[:10] } return &domain.HotBrowser{ Browser: browserCount, OS: osCount, }, nil }