直播后台:新增按 IP 发言管控与在线会话视图。
支持全体禁言、单 IP 禁言/解禁与同 IP 本地限频,同时在开播页展示在线会话明细并补充实时优先码率策略,兼顾实时性与并发承载。 Made-with: Cursor
This commit is contained in:
225
server/pkg/weblive/moderation.go
Normal file
225
server/pkg/weblive/moderation.go
Normal file
@@ -0,0 +1,225 @@
|
||||
package weblive
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
modMu sync.RWMutex
|
||||
muteAll bool
|
||||
mutedIP = make(map[string]bool)
|
||||
ipWindow = make(map[string][]int64) // 每个 IP 最近窗口内的发送时间戳(ms)
|
||||
onlineMap = make(map[string]*onlineSession)
|
||||
seq uint64
|
||||
)
|
||||
|
||||
type ModerationSnapshot struct {
|
||||
MuteAll bool `json:"mute_all"`
|
||||
MutedIPs []string `json:"muted_ips"`
|
||||
OnlineIPs []IPOnlineItem `json:"online_ips"`
|
||||
OnlineUsers []OnlineUserItem `json:"online_users"`
|
||||
RateLimit struct {
|
||||
WindowMs int `json:"window_ms"`
|
||||
MaxHits int `json:"max_hits"`
|
||||
} `json:"rate_limit"`
|
||||
}
|
||||
|
||||
type IPOnlineItem struct {
|
||||
IP string `json:"ip"`
|
||||
Count int `json:"count"`
|
||||
Muted bool `json:"muted"`
|
||||
}
|
||||
|
||||
type onlineSession struct {
|
||||
ID string
|
||||
IP string
|
||||
Username string
|
||||
Channel string
|
||||
Connected time.Time
|
||||
LastAt time.Time
|
||||
}
|
||||
|
||||
type OnlineUserItem struct {
|
||||
ID string `json:"id"`
|
||||
IP string `json:"ip"`
|
||||
Username string `json:"username"`
|
||||
Channel string `json:"channel"`
|
||||
ConnectedAt string `json:"connected_at"`
|
||||
OnlineSec int64 `json:"online_sec"`
|
||||
IdleSec int64 `json:"idle_sec"`
|
||||
Muted bool `json:"muted"`
|
||||
}
|
||||
|
||||
func SetMuteAll(enabled bool) {
|
||||
modMu.Lock()
|
||||
muteAll = enabled
|
||||
modMu.Unlock()
|
||||
}
|
||||
|
||||
func SetIPMuted(ip string, enabled bool) {
|
||||
if ip == "" {
|
||||
return
|
||||
}
|
||||
modMu.Lock()
|
||||
if enabled {
|
||||
mutedIP[ip] = true
|
||||
} else {
|
||||
delete(mutedIP, ip)
|
||||
}
|
||||
modMu.Unlock()
|
||||
}
|
||||
|
||||
const (
|
||||
ipSendWindowMs = 3000
|
||||
ipSendMaxHits = 10
|
||||
)
|
||||
|
||||
func IsIPMuted(ip string) bool {
|
||||
modMu.RLock()
|
||||
defer modMu.RUnlock()
|
||||
return mutedIP[ip]
|
||||
}
|
||||
|
||||
func IsMutedForIP(ip string) bool {
|
||||
modMu.RLock()
|
||||
defer modMu.RUnlock()
|
||||
if muteAll {
|
||||
return true
|
||||
}
|
||||
return mutedIP[ip]
|
||||
}
|
||||
|
||||
func ModerationStateSnapshot() ModerationSnapshot {
|
||||
modMu.RLock()
|
||||
muteAllNow := muteAll
|
||||
muted := make([]string, 0, len(mutedIP))
|
||||
for ip := range mutedIP {
|
||||
muted = append(muted, ip)
|
||||
}
|
||||
modMu.RUnlock()
|
||||
sort.Strings(muted)
|
||||
|
||||
counts := onlineIPCountsLocked()
|
||||
online := make([]IPOnlineItem, 0, len(counts))
|
||||
for ip, n := range counts {
|
||||
online = append(online, IPOnlineItem{IP: ip, Count: n, Muted: IsIPMuted(ip)})
|
||||
}
|
||||
sort.Slice(online, func(i, j int) bool {
|
||||
if online[i].Count == online[j].Count {
|
||||
return online[i].IP < online[j].IP
|
||||
}
|
||||
return online[i].Count > online[j].Count
|
||||
})
|
||||
users := onlineUsersLocked()
|
||||
out := ModerationSnapshot{MuteAll: muteAllNow, MutedIPs: muted, OnlineIPs: online, OnlineUsers: users}
|
||||
out.RateLimit.WindowMs = ipSendWindowMs
|
||||
out.RateLimit.MaxHits = ipSendMaxHits
|
||||
return out
|
||||
}
|
||||
|
||||
// AllowSendByIP 本地内存限频(同 IP 先本地判定,避免刷爆)
|
||||
func AllowSendByIP(ip string) bool {
|
||||
if ip == "" {
|
||||
return true
|
||||
}
|
||||
now := time.Now().UnixMilli()
|
||||
cut := now - ipSendWindowMs
|
||||
modMu.Lock()
|
||||
defer modMu.Unlock()
|
||||
arr := ipWindow[ip]
|
||||
if len(arr) > 0 {
|
||||
k := 0
|
||||
for _, ts := range arr {
|
||||
if ts >= cut {
|
||||
arr[k] = ts
|
||||
k++
|
||||
}
|
||||
}
|
||||
arr = arr[:k]
|
||||
}
|
||||
if len(arr) >= ipSendMaxHits {
|
||||
ipWindow[ip] = arr
|
||||
return false
|
||||
}
|
||||
arr = append(arr, now)
|
||||
if len(arr) > ipSendMaxHits*4 {
|
||||
arr = arr[len(arr)-ipSendMaxHits*2:]
|
||||
}
|
||||
ipWindow[ip] = arr
|
||||
return true
|
||||
}
|
||||
|
||||
func RegisterOnlineSession(channel, ip, username string) string {
|
||||
now := time.Now()
|
||||
id := fmt.Sprintf("%s-%d", channel, atomic.AddUint64(&seq, 1))
|
||||
modMu.Lock()
|
||||
onlineMap[id] = &onlineSession{
|
||||
ID: id,
|
||||
IP: ip,
|
||||
Username: username,
|
||||
Channel: channel,
|
||||
Connected: now,
|
||||
LastAt: now,
|
||||
}
|
||||
modMu.Unlock()
|
||||
return id
|
||||
}
|
||||
|
||||
func TouchOnlineSession(id string) {
|
||||
if id == "" {
|
||||
return
|
||||
}
|
||||
modMu.Lock()
|
||||
if s := onlineMap[id]; s != nil {
|
||||
s.LastAt = time.Now()
|
||||
}
|
||||
modMu.Unlock()
|
||||
}
|
||||
|
||||
func UnregisterOnlineSession(id string) {
|
||||
if id == "" {
|
||||
return
|
||||
}
|
||||
modMu.Lock()
|
||||
delete(onlineMap, id)
|
||||
modMu.Unlock()
|
||||
}
|
||||
|
||||
func onlineIPCountsLocked() map[string]int {
|
||||
out := make(map[string]int)
|
||||
for _, s := range onlineMap {
|
||||
if s == nil || s.IP == "" {
|
||||
continue
|
||||
}
|
||||
out[s.IP]++
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func onlineUsersLocked() []OnlineUserItem {
|
||||
now := time.Now()
|
||||
out := make([]OnlineUserItem, 0, len(onlineMap))
|
||||
for _, s := range onlineMap {
|
||||
if s == nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, OnlineUserItem{
|
||||
ID: s.ID,
|
||||
IP: s.IP,
|
||||
Username: s.Username,
|
||||
Channel: s.Channel,
|
||||
ConnectedAt: s.Connected.Format(time.RFC3339),
|
||||
OnlineSec: int64(now.Sub(s.Connected).Seconds()),
|
||||
IdleSec: int64(now.Sub(s.LastAt).Seconds()),
|
||||
Muted: mutedIP[s.IP] || muteAll,
|
||||
})
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
return out[i].OnlineSec > out[j].OnlineSec
|
||||
})
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user