feat(admin): 独立后台壳、站点分析与聊天监管,公开站拆出主题切换与访问埋点
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
88
backend/service/telemetry.go
Normal file
88
backend/service/telemetry.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/freefire/jiang13-bbs/model"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
var (
|
||||
vidUUIDRe = regexp.MustCompile(`(?i)^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
|
||||
)
|
||||
|
||||
// TelemetryService 轻量页面访问埋点
|
||||
type TelemetryService struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewTelemetryService(db *gorm.DB) *TelemetryService {
|
||||
return &TelemetryService{db: db}
|
||||
}
|
||||
|
||||
func todayDate() time.Time {
|
||||
now := time.Now()
|
||||
return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
}
|
||||
|
||||
func hashVid(vid string) string {
|
||||
sum := sha256.Sum256([]byte(strings.TrimSpace(strings.ToLower(vid))))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// ValidVisitorID 校验浏览器访客 ID(UUID)
|
||||
func ValidVisitorID(vid string) bool {
|
||||
return vidUUIDRe.MatchString(strings.TrimSpace(vid))
|
||||
}
|
||||
|
||||
// RecordPageView 记录一次页面浏览:PV 必增;合法 vid 首次出现当日才 UV+1
|
||||
func (s *TelemetryService) RecordPageView(vid string, loggedIn bool) error {
|
||||
day := todayDate()
|
||||
|
||||
err := s.db.Transaction(func(tx *gorm.DB) error {
|
||||
var st model.SiteDailyStats
|
||||
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("date = ?", day).First(&st).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
st = model.SiteDailyStats{Date: day}
|
||||
if err := tx.Create(&st).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// 重新锁住新建行
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("date = ?", day).First(&st).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{
|
||||
"pv": st.PV + 1,
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
if loggedIn {
|
||||
updates["logged_in_pv"] = st.LoggedInPV + 1
|
||||
}
|
||||
|
||||
if ValidVisitorID(vid) {
|
||||
vis := model.SiteDailyVisitor{Date: day, VidHash: hashVid(vid)}
|
||||
res := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&vis)
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
if res.RowsAffected > 0 {
|
||||
updates["uv"] = st.UV + 1
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Model(&model.SiteDailyStats{}).Where("date = ?", day).Updates(updates).Error
|
||||
})
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user