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
@@ -0,0 +1,182 @@
package contentmarkerser
import (
"encoding/json"
"time"
"91porn-server/common/constant/redisconst"
"91porn-server/common/log"
"91porn-server/models/v/mediamod"
"91porn-server/models/v/moduleconfmod"
"91porn-server/models/v/tagmod"
"91porn-server/models/v/vidmod"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type ModuleMarker struct {
ModuleID string `json:"moduleId"`
LatestAt *time.Time `json:"latestAt"`
}
type Response struct {
HomeLatestAt *time.Time `json:"homeLatestAt"`
Modules []ModuleMarker `json:"modules"`
}
// MarkerCacheReader is the Redis JSON subset used to share marker snapshots.
type MarkerCacheReader interface {
GetWithJson2Any(key string, val any) error
}
// MarkerCache adds atomic cache population to the marker snapshot reader.
type MarkerCache interface {
MarkerCacheReader
SetNX(key string, value interface{}, expiration time.Duration) (bool, error)
}
// GetCached returns the shared marker snapshot, loading and caching it on a miss.
func GetCached(now time.Time, cache MarkerCache) (Response, error) {
data, err := getCached(now, cache, Get)
if err != nil {
return Response{}, err
}
if latestAt, cacheHit := homeLatestAtFromCachedList(cache); cacheHit {
data.HomeLatestAt = latestAt
}
return data, nil
}
func getCached(now time.Time, cache MarkerCache, load func(time.Time) (Response, error)) (Response, error) {
if data, ok := ReadCached(cache); ok {
return data, nil
}
data, err := load(now)
if err != nil {
return Response{}, err
}
if cache != nil {
payload, marshalErr := json.Marshal(data)
if marshalErr != nil {
log.Warn("marshal content update markers failed", log.E(marshalErr))
return data, nil
}
stored, cacheErr := cache.SetNX(
redisconst.ContentUpdateMarkersCache,
string(payload),
redisconst.ContentUpdateMarkersCacheExpire,
)
if cacheErr != nil {
log.Warn("cache content update markers failed", log.E(cacheErr))
} else if !stored {
if current, ok := ReadCached(cache); ok {
return current, nil
}
}
}
return data, nil
}
// ReadCached returns only an existing marker snapshot and never queries MongoDB.
func ReadCached(cache MarkerCacheReader) (Response, bool) {
if cache == nil {
return Response{}, false
}
var data Response
if err := cache.GetWithJson2Any(redisconst.ContentUpdateMarkersCache, &data); err != nil {
return Response{}, false
}
return data, true
}
type cachedHomeLatestList struct {
Videos []struct {
ReviewAt time.Time `json:"reviewAt"`
} `json:"videos"`
}
const (
homeLatestListSortType = 1
homeLatestListPageNumber = 1
homeLatestListPageSize = 10
)
// homeLatestAtFromCachedList makes the "latest" red dot describe the same
// first-page snapshot that the client can currently read from the list cache.
func homeLatestAtFromCachedList(cache MarkerCacheReader) (*time.Time, bool) {
if cache == nil {
return nil, false
}
var data cachedHomeLatestList
key := redisconst.GetMostNewModuleVideoListKey(
homeLatestListSortType,
homeLatestListPageNumber,
homeLatestListPageSize,
)
if err := cache.GetWithJson2Any(key, &data); err != nil {
return nil, false
}
if len(data.Videos) == 0 || data.Videos[0].ReviewAt.IsZero() {
return nil, true
}
latestAt := data.Videos[0].ReviewAt
return &latestAt, true
}
func Get(now time.Time) (Response, error) {
modules, err := moduleconfmod.GetAllActiveModule(now)
if err != nil {
return Response{}, err
}
excluded, err := moduleconfmod.ExcludedVideoModuleIDs(now, false)
if err != nil {
return Response{}, err
}
baseFilter := bson.M{"status": vidmod.CheckPass, "newsType": vidmod.SP}
if len(excluded) > 0 {
baseFilter["mId"] = bson.M{"$nin": excluded}
}
if darkTag, findErr := tagmod.FindOneTagByName("暗网"); findErr == nil && !darkTag.ID.IsZero() {
baseFilter["tags"] = bson.M{"$ne": darkTag.ID}
}
homeLatestAt, err := vidmod.LatestReviewAt(baseFilter)
if err != nil {
return Response{}, err
}
moduleIDs := make([]string, 0, len(modules))
moduleObjectIDs := make([]primitive.ObjectID, 0, len(modules))
for _, module := range modules {
moduleIDs = append(moduleIDs, module.ID.Hex())
moduleObjectIDs = append(moduleObjectIDs, module.ID)
}
moduleTimes, err := vidmod.LatestReviewAtByModules(moduleIDs)
if err != nil {
return Response{}, err
}
mediaModuleTimes, err := mediamod.LatestContentAtByModules(moduleObjectIDs)
if err != nil {
return Response{}, err
}
markers := make([]ModuleMarker, 0, len(modules))
for _, module := range modules {
var latest *time.Time
if value, ok := moduleTimes[module.ID.Hex()]; ok {
valueCopy := value
latest = &valueCopy
}
if value, ok := mediaModuleTimes[module.ID.Hex()]; ok && (latest == nil || value.After(*latest)) {
valueCopy := value
latest = &valueCopy
}
markers = append(markers, ModuleMarker{ModuleID: module.ID.Hex(), LatestAt: latest})
}
return Response{
HomeLatestAt: homeLatestAt,
Modules: markers,
}, nil
}
@@ -0,0 +1,229 @@
package contentmarkerser
import (
"bytes"
"encoding/json"
"errors"
"sync"
"testing"
"time"
"91porn-server/common/constant/redisconst"
)
type markerCacheStub struct {
data Response
getErr error
setData Response
setCalls int
latestListCached bool
latestListReviewAt *time.Time
}
type markerRaceCache struct {
mu sync.Mutex
payload string
exists bool
}
func (c *markerRaceCache) GetWithJson2Any(_ string, val any) error {
c.mu.Lock()
defer c.mu.Unlock()
if !c.exists {
return errors.New("cache miss")
}
return json.Unmarshal([]byte(c.payload), val)
}
func (c *markerRaceCache) SetNX(_ string, val interface{}, _ time.Duration) (bool, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.exists {
return false, nil
}
payload, ok := val.(string)
if !ok {
return false, errors.New("cache value is not JSON text")
}
c.payload = payload
c.exists = true
return true, nil
}
func (c *markerCacheStub) GetWithJson2Any(key string, val any) error {
if key == redisconst.GetMostNewModuleVideoListKey(
homeLatestListSortType,
homeLatestListPageNumber,
homeLatestListPageSize,
) {
if !c.latestListCached {
return errors.New("latest list cache miss")
}
data := cachedHomeLatestList{}
if c.latestListReviewAt != nil {
data.Videos = append(data.Videos, struct {
ReviewAt time.Time `json:"reviewAt"`
}{ReviewAt: *c.latestListReviewAt})
}
payload, err := json.Marshal(data)
if err != nil {
return err
}
return json.Unmarshal(payload, val)
}
if c.getErr != nil {
return c.getErr
}
payload, err := json.Marshal(c.data)
if err != nil {
return err
}
return json.Unmarshal(payload, val)
}
func (c *markerCacheStub) SetNX(_ string, val interface{}, _ time.Duration) (bool, error) {
c.setCalls++
payload, ok := val.(string)
if !ok {
return false, errors.New("cache value is not JSON text")
}
if err := json.Unmarshal([]byte(payload), &c.setData); err != nil {
return false, err
}
return true, nil
}
func TestResponseOmitsTodayLatestAt(t *testing.T) {
payload, err := json.Marshal(Response{})
if err != nil {
t.Fatalf("json.Marshal(Response{}) error = %v", err)
}
if bytes.Contains(payload, []byte("todayLatestAt")) {
t.Fatalf("response still contains removed todayLatestAt field: %s", payload)
}
}
func TestGetCachedReturnsCachedMarkers(t *testing.T) {
latest := time.Date(2026, 8, 4, 11, 30, 0, 123000000, time.FixedZone("CST", 8*60*60))
cache := &markerCacheStub{data: Response{HomeLatestAt: &latest}}
got, err := GetCached(time.Now(), cache)
if err != nil {
t.Fatalf("GetCached() error = %v", err)
}
if got.HomeLatestAt == nil || !got.HomeLatestAt.Equal(latest) {
t.Fatalf("GetCached() homeLatestAt = %v, want %v", got.HomeLatestAt, latest)
}
if cache.setCalls != 0 {
t.Fatalf("GetCached() cache writes = %d, want 0", cache.setCalls)
}
}
func TestReadCachedReportsMiss(t *testing.T) {
cache := &markerCacheStub{getErr: errors.New("cache miss")}
if _, ok := ReadCached(cache); ok {
t.Fatal("ReadCached() hit = true, want false")
}
if _, ok := ReadCached(nil); ok {
t.Fatal("ReadCached(nil) hit = true, want false")
}
}
func TestGetCachedUsesHomeLatestAtFromCachedList(t *testing.T) {
markerLatest := time.Date(2026, 8, 4, 11, 30, 0, 0, time.UTC)
listLatest := markerLatest.Add(-2 * time.Minute)
cache := &markerCacheStub{
data: Response{HomeLatestAt: &markerLatest},
latestListCached: true,
latestListReviewAt: &listLatest,
}
got, err := GetCached(time.Now(), cache)
if err != nil {
t.Fatalf("GetCached() error = %v", err)
}
if got.HomeLatestAt == nil || !got.HomeLatestAt.Equal(listLatest) {
t.Fatalf("GetCached() homeLatestAt = %v, want cached list time %v", got.HomeLatestAt, listLatest)
}
}
func TestGetCachedClearsHomeLatestAtForCachedEmptyList(t *testing.T) {
markerLatest := time.Date(2026, 8, 4, 11, 30, 0, 0, time.UTC)
cache := &markerCacheStub{
data: Response{HomeLatestAt: &markerLatest},
latestListCached: true,
}
got, err := GetCached(time.Now(), cache)
if err != nil {
t.Fatalf("GetCached() error = %v", err)
}
if got.HomeLatestAt != nil {
t.Fatalf("GetCached() homeLatestAt = %v, want nil for cached empty list", got.HomeLatestAt)
}
}
func TestGetCachedLoadsAndWritesCacheBeforeReturning(t *testing.T) {
latest := time.Date(2026, 8, 4, 11, 31, 0, 0, time.UTC)
cache := &markerCacheStub{getErr: errors.New("cache miss")}
loadCalls := 0
load := func(time.Time) (Response, error) {
loadCalls++
return Response{HomeLatestAt: &latest}, nil
}
got, err := getCached(time.Now(), cache, load)
if err != nil {
t.Fatalf("getCached() error = %v", err)
}
if loadCalls != 1 {
t.Fatalf("getCached() loader calls = %d, want 1", loadCalls)
}
if cache.setCalls != 1 || cache.setData.HomeLatestAt == nil {
t.Fatalf("getCached() cache write = (%d, %v), want one marker write", cache.setCalls, cache.setData.HomeLatestAt)
}
if got.HomeLatestAt == nil || !got.HomeLatestAt.Equal(latest) {
t.Fatalf("getCached() homeLatestAt = %v, want %v", got.HomeLatestAt, latest)
}
}
func TestGetCachedConcurrentMissReturnsWinningSnapshot(t *testing.T) {
oldLatest := time.Date(2026, 8, 4, 11, 30, 0, 0, time.UTC)
newLatest := oldLatest.Add(time.Minute)
cache := &markerRaceCache{}
oldLoaderStarted := make(chan struct{})
releaseOldLoader := make(chan struct{})
type outcome struct {
data Response
err error
}
oldOutcome := make(chan outcome, 1)
go func() {
data, err := getCached(time.Now(), cache, func(time.Time) (Response, error) {
close(oldLoaderStarted)
<-releaseOldLoader
return Response{HomeLatestAt: &oldLatest}, nil
})
oldOutcome <- outcome{data: data, err: err}
}()
<-oldLoaderStarted
newData, err := getCached(time.Now(), cache, func(time.Time) (Response, error) {
return Response{HomeLatestAt: &newLatest}, nil
})
if err != nil {
t.Fatalf("new getCached() error = %v", err)
}
close(releaseOldLoader)
oldResult := <-oldOutcome
if oldResult.err != nil {
t.Fatalf("old getCached() error = %v", oldResult.err)
}
for name, data := range map[string]Response{"new": newData, "old": oldResult.data} {
if data.HomeLatestAt == nil || !data.HomeLatestAt.Equal(newLatest) {
t.Fatalf("%s getCached() homeLatestAt = %v, want winning %v", name, data.HomeLatestAt, newLatest)
}
}
}