完善角色与消息权限:收紧板管内容处置,彻底删除仅站长,并统一管理删帖/评弹窗。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -81,8 +81,9 @@ func fillDaySeries(start, end time.Time, got map[string]int64) []DayCount {
|
||||
return out
|
||||
}
|
||||
|
||||
// Overview 返回近 N 日访问与内容趋势
|
||||
func (s *AnalyticsService) Overview(rangeQ string) (*AnalyticsOverview, error) {
|
||||
// Overview 返回近 N 日访问与内容趋势。
|
||||
// scoped=true 时按 boardIDs 过滤内容类指标,并隐藏全站 PV/UV/注册。
|
||||
func (s *AnalyticsService) Overview(rangeQ string, boardIDs []uint, scoped bool) (*AnalyticsOverview, error) {
|
||||
days := parseRangeDays(rangeQ)
|
||||
now := time.Now()
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
@@ -97,22 +98,38 @@ func (s *AnalyticsService) Overview(rangeQ string) (*AnalyticsOverview, error) {
|
||||
TopViewed: make([]TopViewPost, 0),
|
||||
}
|
||||
|
||||
var stats []model.SiteDailyStats
|
||||
if err := s.db.Where("date >= ? AND date <= ?", start, end).
|
||||
Order("date ASC").Find(&stats).Error; err != nil {
|
||||
return nil, err
|
||||
if scoped && len(boardIDs) == 0 {
|
||||
for d := start; !d.After(end); d = d.AddDate(0, 0, 1) {
|
||||
out.Visits = append(out.Visits, VisitDay{Date: dateKey(d)})
|
||||
}
|
||||
out.PostsSeries = fillDaySeries(start, end, nil)
|
||||
out.CommentsSeries = fillDaySeries(start, end, nil)
|
||||
out.RegistersSeries = fillDaySeries(start, end, nil)
|
||||
return out, nil
|
||||
}
|
||||
visitMap := map[string]model.SiteDailyStats{}
|
||||
for _, st := range stats {
|
||||
visitMap[dateKey(st.Date)] = st
|
||||
}
|
||||
for d := start; !d.After(end); d = d.AddDate(0, 0, 1) {
|
||||
k := dateKey(d)
|
||||
st := visitMap[k]
|
||||
out.Visits = append(out.Visits, VisitDay{Date: k, PV: st.PV, UV: st.UV})
|
||||
if k == dateKey(today) {
|
||||
out.TodayPV = st.PV
|
||||
out.TodayUV = st.UV
|
||||
|
||||
if !scoped {
|
||||
var stats []model.SiteDailyStats
|
||||
if err := s.db.Where("date >= ? AND date <= ?", start, end).
|
||||
Order("date ASC").Find(&stats).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
visitMap := map[string]model.SiteDailyStats{}
|
||||
for _, st := range stats {
|
||||
visitMap[dateKey(st.Date)] = st
|
||||
}
|
||||
for d := start; !d.After(end); d = d.AddDate(0, 0, 1) {
|
||||
k := dateKey(d)
|
||||
st := visitMap[k]
|
||||
out.Visits = append(out.Visits, VisitDay{Date: k, PV: st.PV, UV: st.UV})
|
||||
if k == dateKey(today) {
|
||||
out.TodayPV = st.PV
|
||||
out.TodayUV = st.UV
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for d := start; !d.After(end); d = d.AddDate(0, 0, 1) {
|
||||
out.Visits = append(out.Visits, VisitDay{Date: dateKey(d)})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,38 +149,56 @@ func (s *AnalyticsService) Overview(rangeQ string) (*AnalyticsOverview, error) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
boardFilter := ""
|
||||
var postArgs []interface{}
|
||||
postArgs = append(postArgs, model.ContentStatusPublished, start, end.AddDate(0, 0, 1))
|
||||
if scoped {
|
||||
boardFilter = " AND board_id IN ?"
|
||||
postArgs = append(postArgs, boardIDs)
|
||||
}
|
||||
postMap, err := scanDayCounts(`
|
||||
SELECT to_char(created_at, 'YYYY-MM-DD') AS day, count(*) AS cnt
|
||||
FROM posts
|
||||
WHERE deleted_at IS NULL AND status = ? AND created_at >= ? AND created_at < ?
|
||||
WHERE deleted_at IS NULL AND status = ? AND created_at >= ? AND created_at < ?`+boardFilter+`
|
||||
GROUP BY day ORDER BY day`,
|
||||
model.ContentStatusPublished, start, end.AddDate(0, 0, 1))
|
||||
postArgs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out.PostsSeries = fillDaySeries(start, end, postMap)
|
||||
|
||||
var commentArgs []interface{}
|
||||
commentArgs = append(commentArgs, model.ContentStatusPublished, start, end.AddDate(0, 0, 1))
|
||||
commentBoard := ""
|
||||
if scoped {
|
||||
commentBoard = " AND post_id IN (SELECT id FROM posts WHERE board_id IN ?)"
|
||||
commentArgs = append(commentArgs, boardIDs)
|
||||
}
|
||||
commentMap, err := scanDayCounts(`
|
||||
SELECT to_char(created_at, 'YYYY-MM-DD') AS day, count(*) AS cnt
|
||||
FROM comments
|
||||
WHERE deleted_at IS NULL AND status = ? AND created_at >= ? AND created_at < ?
|
||||
WHERE deleted_at IS NULL AND status = ? AND created_at >= ? AND created_at < ?`+commentBoard+`
|
||||
GROUP BY day ORDER BY day`,
|
||||
model.ContentStatusPublished, start, end.AddDate(0, 0, 1))
|
||||
commentArgs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out.CommentsSeries = fillDaySeries(start, end, commentMap)
|
||||
|
||||
regMap, err := scanDayCounts(`
|
||||
SELECT to_char(created_at, 'YYYY-MM-DD') AS day, count(*) AS cnt
|
||||
FROM users
|
||||
WHERE deleted_at IS NULL AND created_at >= ? AND created_at < ?
|
||||
GROUP BY day ORDER BY day`,
|
||||
start, end.AddDate(0, 0, 1))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if !scoped {
|
||||
regMap, err := scanDayCounts(`
|
||||
SELECT to_char(created_at, 'YYYY-MM-DD') AS day, count(*) AS cnt
|
||||
FROM users
|
||||
WHERE deleted_at IS NULL AND created_at >= ? AND created_at < ?
|
||||
GROUP BY day ORDER BY day`,
|
||||
start, end.AddDate(0, 0, 1))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out.RegistersSeries = fillDaySeries(start, end, regMap)
|
||||
} else {
|
||||
out.RegistersSeries = fillDaySeries(start, end, nil)
|
||||
}
|
||||
out.RegistersSeries = fillDaySeries(start, end, regMap)
|
||||
|
||||
type boardRow struct {
|
||||
ID uint
|
||||
@@ -171,13 +206,18 @@ func (s *AnalyticsService) Overview(rangeQ string) (*AnalyticsOverview, error) {
|
||||
PostCount int64
|
||||
}
|
||||
var boards []boardRow
|
||||
if err := s.db.Raw(`
|
||||
boardSQL := `
|
||||
SELECT b.id, b.name, count(p.id) AS post_count
|
||||
FROM boards b
|
||||
LEFT JOIN posts p ON p.board_id = b.id AND p.deleted_at IS NULL AND p.status = ?
|
||||
WHERE b.deleted_at IS NULL
|
||||
GROUP BY b.id, b.name
|
||||
ORDER BY post_count DESC`, model.ContentStatusPublished).Scan(&boards).Error; err != nil {
|
||||
WHERE b.deleted_at IS NULL`
|
||||
boardArgs := []interface{}{model.ContentStatusPublished}
|
||||
if scoped {
|
||||
boardSQL += ` AND b.id IN ?`
|
||||
boardArgs = append(boardArgs, boardIDs)
|
||||
}
|
||||
boardSQL += ` GROUP BY b.id, b.name ORDER BY post_count DESC`
|
||||
if err := s.db.Raw(boardSQL, boardArgs...).Scan(&boards).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, b := range boards {
|
||||
@@ -192,11 +232,14 @@ func (s *AnalyticsService) Overview(rangeQ string) (*AnalyticsOverview, error) {
|
||||
PostType string
|
||||
Cnt int64
|
||||
}
|
||||
var types []typeRow
|
||||
if err := s.db.Model(&model.Post{}).
|
||||
typeQ := s.db.Model(&model.Post{}).
|
||||
Select("post_type, count(*) as cnt").
|
||||
Where("deleted_at IS NULL AND status = ?", model.ContentStatusPublished).
|
||||
Group("post_type").Scan(&types).Error; err != nil {
|
||||
Where("deleted_at IS NULL AND status = ?", model.ContentStatusPublished)
|
||||
if scoped {
|
||||
typeQ = typeQ.Where("board_id IN ?", boardIDs)
|
||||
}
|
||||
var types []typeRow
|
||||
if err := typeQ.Group("post_type").Scan(&types).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, t := range types {
|
||||
@@ -208,10 +251,13 @@ func (s *AnalyticsService) Overview(rangeQ string) (*AnalyticsOverview, error) {
|
||||
})
|
||||
}
|
||||
|
||||
topQ := s.db.Select("id, title, view_count, board_id").
|
||||
Where("deleted_at IS NULL AND status = ?", model.ContentStatusPublished)
|
||||
if scoped {
|
||||
topQ = topQ.Where("board_id IN ?", boardIDs)
|
||||
}
|
||||
var tops []model.Post
|
||||
if err := s.db.Select("id, title, view_count, board_id").
|
||||
Where("deleted_at IS NULL AND status = ?", model.ContentStatusPublished).
|
||||
Order("view_count DESC").Limit(8).Find(&tops).Error; err != nil {
|
||||
if err := topQ.Order("view_count DESC").Limit(8).Find(&tops).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, p := range tops {
|
||||
|
||||
Reference in New Issue
Block a user