Initial commit

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 13:57:10 +08:00
co-authored by Claude Opus 5
commit 8679200f41
1897 changed files with 257900 additions and 0 deletions
+334
View File
@@ -0,0 +1,334 @@
package moduleconfmod
import (
"errors"
"fmt"
"sort"
"sync"
"time"
"91porn-server/common/log"
"go.mongodb.org/mongo-driver/bson/primitive"
"golang.org/x/sync/singleflight"
)
const (
moduleSnapshotTTL = 15 * time.Second
moduleSnapshotRetryDelay = time.Second
)
var errModuleSnapshotInvalidated = errors.New("module configuration snapshot invalidated while loading")
type moduleSnapshotLoader func() ([]ModuleConf, error)
// moduleSnapshot is immutable after construction. Every accessor returns
// copies so callers cannot mutate data shared by concurrent requests.
type moduleSnapshot struct {
modules []ModuleConf
byID map[primitive.ObjectID]int
}
func newModuleSnapshot(modules []ModuleConf) *moduleSnapshot {
copied := cloneModuleConfs(modules)
byID := make(map[primitive.ObjectID]int, len(copied))
for i := range copied {
byID[copied[i].ID] = i
}
return &moduleSnapshot{
modules: copied,
byID: byID,
}
}
func (s *moduleSnapshot) all() []ModuleConf {
if s == nil {
return nil
}
return cloneModuleConfs(s.modules)
}
func (s *moduleSnapshot) findByID(id primitive.ObjectID) (ModuleConf, bool) {
if s == nil {
return ModuleConf{}, false
}
index, ok := s.byID[id]
if !ok {
return ModuleConf{}, false
}
return cloneModuleConf(s.modules[index]), true
}
func (s *moduleSnapshot) findByIDs(ids []primitive.ObjectID) []ModuleConf {
if s == nil || len(ids) == 0 {
return nil
}
wanted := make(map[primitive.ObjectID]struct{}, len(ids))
for _, id := range ids {
wanted[id] = struct{}{}
}
modules := make([]ModuleConf, 0, len(wanted))
for i := range s.modules {
if _, ok := wanted[s.modules[i].ID]; ok {
modules = append(modules, cloneModuleConf(s.modules[i]))
}
}
return modules
}
func (s *moduleSnapshot) findByModuleName(moduleName string) []ModuleConf {
modules := make([]ModuleConf, 0)
for i := range s.modules {
if s.modules[i].ModuleName == moduleName {
modules = append(modules, cloneModuleConf(s.modules[i]))
}
}
return modules
}
func (s *moduleSnapshot) findByType(moduleType int) []ModuleConf {
modules := make([]ModuleConf, 0)
for i := range s.modules {
if s.modules[i].Type == moduleType {
modules = append(modules, cloneModuleConf(s.modules[i]))
}
}
return modules
}
func (s *moduleSnapshot) enabled() []ModuleConf {
modules := make([]ModuleConf, 0)
for i := range s.modules {
if s.modules[i].Status == 1 && s.modules[i].DeletedAt == nil {
modules = append(modules, cloneModuleConf(s.modules[i]))
}
}
sortModuleConfs(modules)
return modules
}
func (s *moduleSnapshot) activeAt(now time.Time) []ModuleConf {
modules := make([]ModuleConf, 0)
for i := range s.modules {
if s.modules[i].IsActiveAt(now) {
modules = append(modules, cloneModuleConf(s.modules[i]))
}
}
sortModuleConfs(modules)
return modules
}
func (s *moduleSnapshot) excludedVideoModuleIDs(now time.Time, recommend bool) []string {
ids := make([]string, 0)
for i := range s.modules {
module := s.modules[i]
excludedByScene := module.ExcludeLatest
if recommend {
excludedByScene = module.ExcludeRecommend
}
if excludedByScene || (module.SearchOnlyWhenInactive && !module.IsActiveAt(now)) {
ids = append(ids, module.ID.Hex())
}
}
return ids
}
func (s *moduleSnapshot) excludedSearchModuleIDs() []string {
ids := make([]string, 0)
for i := range s.modules {
if s.modules[i].ExcludeSearch {
ids = append(ids, s.modules[i].ID.Hex())
}
}
return ids
}
func (s *moduleSnapshot) blockedOutsideSearchModuleIDs(moduleIDs []primitive.ObjectID, now time.Time) map[string]struct{} {
blocked := make(map[string]struct{})
seen := make(map[primitive.ObjectID]struct{}, len(moduleIDs))
for _, moduleID := range moduleIDs {
if moduleID.IsZero() {
continue
}
if _, ok := seen[moduleID]; ok {
continue
}
seen[moduleID] = struct{}{}
index, ok := s.byID[moduleID]
if !ok {
continue
}
module := s.modules[index]
if module.SearchOnlyWhenInactive && !module.IsActiveAt(now) {
blocked[module.ID.Hex()] = struct{}{}
}
}
return blocked
}
type moduleSnapshotCache struct {
mu sync.RWMutex
loadGroup singleflight.Group
loader moduleSnapshotLoader
now func() time.Time
ttl time.Duration
retryDelay time.Duration
snapshot *moduleSnapshot
refreshAt time.Time
generation uint64
}
func newModuleSnapshotCache(
loader moduleSnapshotLoader,
now func() time.Time,
ttl time.Duration,
retryDelay time.Duration,
) *moduleSnapshotCache {
if now == nil {
now = time.Now
}
if ttl <= 0 {
ttl = moduleSnapshotTTL
}
if retryDelay <= 0 {
retryDelay = moduleSnapshotRetryDelay
}
return &moduleSnapshotCache{
loader: loader,
now: now,
ttl: ttl,
retryDelay: retryDelay,
}
}
func (c *moduleSnapshotCache) get() (*moduleSnapshot, error) {
for {
now := c.now()
c.mu.RLock()
if c.snapshot != nil && now.Before(c.refreshAt) {
snapshot := c.snapshot
c.mu.RUnlock()
return snapshot, nil
}
generation := c.generation
c.mu.RUnlock()
key := fmt.Sprintf("module-snapshot-%d", generation)
value, err, _ := c.loadGroup.Do(key, func() (interface{}, error) {
return c.load(generation)
})
if errors.Is(err, errModuleSnapshotInvalidated) {
continue
}
if err != nil {
return nil, err
}
// A write may invalidate the cache after load() installs the snapshot
// but before singleflight returns it to this reader.
c.mu.RLock()
currentGeneration := c.generation
c.mu.RUnlock()
if currentGeneration != generation {
continue
}
return value.(*moduleSnapshot), nil
}
}
func (c *moduleSnapshotCache) load(generation uint64) (*moduleSnapshot, error) {
now := c.now()
c.mu.RLock()
if generation != c.generation {
c.mu.RUnlock()
return nil, errModuleSnapshotInvalidated
}
if c.snapshot != nil && now.Before(c.refreshAt) {
snapshot := c.snapshot
c.mu.RUnlock()
return snapshot, nil
}
c.mu.RUnlock()
modules, err := c.loader()
loadedAt := c.now()
c.mu.Lock()
if generation != c.generation {
c.mu.Unlock()
return nil, errModuleSnapshotInvalidated
}
if err != nil {
if c.snapshot == nil {
c.mu.Unlock()
return nil, err
}
// Avoid retrying Mongo on every request while still keeping retries
// frequent enough for a transient outage to recover quickly.
c.refreshAt = loadedAt.Add(c.retryDelay)
snapshot := c.snapshot
c.mu.Unlock()
log.Warn("module configuration snapshot refresh failed; using last-known-good data", log.E(err))
return snapshot, nil
}
c.snapshot = newModuleSnapshot(modules)
c.refreshAt = loadedAt.Add(c.ttl)
snapshot := c.snapshot
c.mu.Unlock()
return snapshot, nil
}
// invalidate forces the next reader to reload. The previous snapshot remains
// available as last-known-good if the refresh fails.
func (c *moduleSnapshotCache) invalidate() {
c.mu.Lock()
c.generation++
c.refreshAt = time.Time{}
c.mu.Unlock()
}
// reset is used when the package is rebound to a new Mongo client.
func (c *moduleSnapshotCache) reset() {
c.mu.Lock()
c.generation++
c.snapshot = nil
c.refreshAt = time.Time{}
c.mu.Unlock()
}
func cloneModuleConfs(modules []ModuleConf) []ModuleConf {
if modules == nil {
return nil
}
copied := make([]ModuleConf, len(modules))
for i := range modules {
copied[i] = cloneModuleConf(modules[i])
}
return copied
}
func cloneModuleConf(module ModuleConf) ModuleConf {
copied := module
copied.OnlineAt = cloneTime(module.OnlineAt)
copied.OfflineAt = cloneTime(module.OfflineAt)
copied.DeletedAt = cloneTime(module.DeletedAt)
if module.HaiJiaoStyle.SortRules != nil {
copied.HaiJiaoStyle.SortRules = append([]SortItem(nil), module.HaiJiaoStyle.SortRules...)
}
return copied
}
func cloneTime(value *time.Time) *time.Time {
if value == nil {
return nil
}
copied := *value
return &copied
}
func sortModuleConfs(modules []ModuleConf) {
sort.SliceStable(modules, func(i, j int) bool {
return modules[i].SortNum < modules[j].SortNum
})
}