mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-26 03:44:55 +08:00
update: admin ui
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
@@ -16,6 +17,7 @@ type SearchStatRepository interface {
|
||||
GetHotKeywords(days int, limit int) ([]entity.KeywordStat, error)
|
||||
GetSearchTrend(days int) ([]entity.DailySearchStat, error)
|
||||
GetKeywordTrend(keyword string, days int) ([]entity.DailySearchStat, error)
|
||||
GetSummary() (map[string]int64, error)
|
||||
}
|
||||
|
||||
// SearchStatRepositoryImpl 搜索统计Repository实现
|
||||
@@ -30,51 +32,34 @@ func NewSearchStatRepository(db *gorm.DB) SearchStatRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// RecordSearch 记录搜索
|
||||
// RecordSearch 记录搜索(每次都插入新记录)
|
||||
func (r *SearchStatRepositoryImpl) RecordSearch(keyword, ip, userAgent string) error {
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
|
||||
// 查找今天是否已有该关键词的记录
|
||||
var stat entity.SearchStat
|
||||
err := r.db.Where("keyword = ? AND date = ?", keyword, today).First(&stat).Error
|
||||
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// 创建新记录
|
||||
stat = entity.SearchStat{
|
||||
stat := entity.SearchStat{
|
||||
Keyword: keyword,
|
||||
Count: 1,
|
||||
Date: today,
|
||||
Date: time.Now(), // 可保留 date 字段,实际用 created_at 统计
|
||||
IP: ip,
|
||||
UserAgent: userAgent,
|
||||
}
|
||||
return r.db.Create(&stat).Error
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新现有记录
|
||||
stat.Count++
|
||||
stat.IP = ip
|
||||
stat.UserAgent = userAgent
|
||||
return r.db.Save(&stat).Error
|
||||
}
|
||||
|
||||
// GetDailyStats 获取每日统计
|
||||
func (r *SearchStatRepositoryImpl) GetDailyStats(days int) ([]entity.DailySearchStat, error) {
|
||||
var stats []entity.DailySearchStat
|
||||
|
||||
query := `
|
||||
query := fmt.Sprintf(`
|
||||
SELECT
|
||||
date,
|
||||
SUM(count) as total_searches,
|
||||
COUNT(DISTINCT keyword) as unique_keywords
|
||||
FROM search_stats
|
||||
WHERE date >= CURRENT_DATE - INTERVAL '? days'
|
||||
WHERE date >= CURRENT_DATE - INTERVAL '%d days'
|
||||
GROUP BY date
|
||||
ORDER BY date DESC
|
||||
`
|
||||
`, days)
|
||||
|
||||
err := r.db.Raw(query, days).Scan(&stats).Error
|
||||
err := r.db.Raw(query).Scan(&stats).Error
|
||||
return stats, err
|
||||
}
|
||||
|
||||
@@ -82,19 +67,19 @@ func (r *SearchStatRepositoryImpl) GetDailyStats(days int) ([]entity.DailySearch
|
||||
func (r *SearchStatRepositoryImpl) GetHotKeywords(days int, limit int) ([]entity.KeywordStat, error) {
|
||||
var keywords []entity.KeywordStat
|
||||
|
||||
query := `
|
||||
query := fmt.Sprintf(`
|
||||
SELECT
|
||||
keyword,
|
||||
SUM(count) as count,
|
||||
RANK() OVER (ORDER BY SUM(count) DESC) as rank
|
||||
FROM search_stats
|
||||
WHERE date >= CURRENT_DATE - INTERVAL '? days'
|
||||
WHERE date >= CURRENT_DATE - INTERVAL '%d days'
|
||||
GROUP BY keyword
|
||||
ORDER BY count DESC
|
||||
LIMIT ?
|
||||
`
|
||||
`, days)
|
||||
|
||||
err := r.db.Raw(query, days, limit).Scan(&keywords).Error
|
||||
err := r.db.Raw(query, limit).Scan(&keywords).Error
|
||||
return keywords, err
|
||||
}
|
||||
|
||||
@@ -102,18 +87,18 @@ func (r *SearchStatRepositoryImpl) GetHotKeywords(days int, limit int) ([]entity
|
||||
func (r *SearchStatRepositoryImpl) GetSearchTrend(days int) ([]entity.DailySearchStat, error) {
|
||||
var stats []entity.DailySearchStat
|
||||
|
||||
query := `
|
||||
query := fmt.Sprintf(`
|
||||
SELECT
|
||||
date,
|
||||
SUM(count) as total_searches,
|
||||
COUNT(DISTINCT keyword) as unique_keywords
|
||||
FROM search_stats
|
||||
WHERE date >= CURRENT_DATE - INTERVAL '? days'
|
||||
WHERE date >= CURRENT_DATE - INTERVAL '%d days'
|
||||
GROUP BY date
|
||||
ORDER BY date ASC
|
||||
`
|
||||
`, days)
|
||||
|
||||
err := r.db.Raw(query, days).Scan(&stats).Error
|
||||
err := r.db.Raw(query).Scan(&stats).Error
|
||||
return stats, err
|
||||
}
|
||||
|
||||
@@ -121,17 +106,54 @@ func (r *SearchStatRepositoryImpl) GetSearchTrend(days int) ([]entity.DailySearc
|
||||
func (r *SearchStatRepositoryImpl) GetKeywordTrend(keyword string, days int) ([]entity.DailySearchStat, error) {
|
||||
var stats []entity.DailySearchStat
|
||||
|
||||
query := `
|
||||
query := fmt.Sprintf(`
|
||||
SELECT
|
||||
date,
|
||||
SUM(count) as total_searches,
|
||||
COUNT(DISTINCT keyword) as unique_keywords
|
||||
FROM search_stats
|
||||
WHERE keyword = ? AND date >= CURRENT_DATE - INTERVAL '? days'
|
||||
WHERE keyword = ? AND date >= CURRENT_DATE - INTERVAL '%d days'
|
||||
GROUP BY date
|
||||
ORDER BY date ASC
|
||||
`
|
||||
`, days)
|
||||
|
||||
err := r.db.Raw(query, keyword, days).Scan(&stats).Error
|
||||
err := r.db.Raw(query, keyword).Scan(&stats).Error
|
||||
return stats, err
|
||||
}
|
||||
|
||||
// GetSummary 获取搜索统计汇总
|
||||
func (r *SearchStatRepositoryImpl) GetSummary() (map[string]int64, error) {
|
||||
var total, today, week, month, keywords int64
|
||||
now := time.Now()
|
||||
todayStr := now.Format("2006-01-02")
|
||||
weekStart := now.AddDate(0, 0, -int(now.Weekday())+1).Format("2006-01-02") // 周一
|
||||
monthStart := now.Format("2006-01") + "-01"
|
||||
|
||||
// 总搜索次数
|
||||
if err := r.db.Model(&entity.SearchStat{}).Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 今日搜索次数
|
||||
if err := r.db.Model(&entity.SearchStat{}).Where("DATE(created_at) = ?", todayStr).Count(&today).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 本周搜索次数
|
||||
if err := r.db.Model(&entity.SearchStat{}).Where("created_at >= ?", weekStart).Count(&week).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 本月搜索次数
|
||||
if err := r.db.Model(&entity.SearchStat{}).Where("created_at >= ?", monthStart).Count(&month).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 总关键词数
|
||||
if err := r.db.Model(&entity.SearchStat{}).Distinct("keyword").Count(&keywords).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]int64{
|
||||
"total": total,
|
||||
"today": today,
|
||||
"week": week,
|
||||
"month": month,
|
||||
"keywords": keywords,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -142,3 +142,13 @@ func GetKeywordTrend(c *gin.Context) {
|
||||
response := converter.ToDailySearchStatResponseList(trend)
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
// GetSearchStatsSummary 获取搜索统计汇总
|
||||
func GetSearchStatsSummary(c *gin.Context) {
|
||||
summary, err := repoManager.SearchStatRepository.GetSummary()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取搜索统计汇总失败", 500)
|
||||
return
|
||||
}
|
||||
SuccessResponse(c, summary)
|
||||
}
|
||||
|
||||
1
main.go
1
main.go
@@ -186,6 +186,7 @@ func main() {
|
||||
api.GET("/search-stats/keyword/:keyword/trend", handlers.GetKeywordTrend)
|
||||
api.POST("/search-stats", handlers.RecordSearch)
|
||||
api.POST("/search-stats/record", handlers.RecordSearch)
|
||||
api.GET("/search-stats/summary", handlers.GetSearchStatsSummary)
|
||||
|
||||
// 系统配置路由
|
||||
api.GET("/system/config", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetSystemConfig)
|
||||
|
||||
@@ -321,7 +321,8 @@ const panApi = usePanApi()
|
||||
|
||||
const { data: pansData } = await useAsyncData('pans', () => panApi.getPans())
|
||||
const pans = computed(() => {
|
||||
return (pansData.value).data.list || []
|
||||
// 统一接口格式后直接为数组
|
||||
return Array.isArray(pansData.value) ? pansData.value : (pansData.value?.list || [])
|
||||
})
|
||||
const platformOptions = computed(() => {
|
||||
return pans.value.map(pan => ({
|
||||
|
||||
@@ -116,6 +116,8 @@ definePageMeta({
|
||||
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import Chart from 'chart.js/auto'
|
||||
import { useApiFetch } from '~/composables/useApiFetch'
|
||||
import { parseApiResponse } from '~/composables/useApi'
|
||||
|
||||
const stats = ref({
|
||||
todaySearches: 0,
|
||||
@@ -143,12 +145,23 @@ const getPercentage = (count) => {
|
||||
// 加载搜索统计
|
||||
const loadSearchStats = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/search-stats')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
searchList.value = data.data || []
|
||||
// 其它统计数据赋值...
|
||||
}
|
||||
// 1. 汇总卡片
|
||||
const summary = await useApiFetch('/search-stats/summary').then(parseApiResponse)
|
||||
stats.value.todaySearches = summary.today || 0
|
||||
stats.value.weekSearches = summary.week || 0
|
||||
stats.value.monthSearches = summary.month || 0
|
||||
// 2. 热门关键词
|
||||
const hotKeywords = await useApiFetch('/search-stats/hot-keywords').then(parseApiResponse)
|
||||
stats.value.hotKeywords = hotKeywords || []
|
||||
// 3. 趋势
|
||||
const trend = await useApiFetch('/search-stats/trend').then(parseApiResponse)
|
||||
stats.value.searchTrend.days = (trend || []).map(item => item.date ? (new Date(item.date)).toLocaleDateString() : '')
|
||||
stats.value.searchTrend.values = (trend || []).map(item => item.total_searches)
|
||||
// 4. 搜索记录
|
||||
const data = await useApiFetch('/search-stats').then(parseApiResponse)
|
||||
searchList.value = data || []
|
||||
// 5. 更新图表
|
||||
setTimeout(updateChart, 100)
|
||||
} catch (error) {
|
||||
console.error('加载搜索统计失败:', error)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user