完善角色与消息权限:收紧板管内容处置,彻底删除仅站长,并统一管理删帖/评弹窗。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-20 14:30:08 +08:00
parent 1f2e4b2a33
commit c0462a3500
37 changed files with 1340 additions and 472 deletions

View File

@@ -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 {