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
+150
View File
@@ -0,0 +1,150 @@
package scenebannermod
import (
"fmt"
"time"
"91porn-server/common/db"
"91porn-server/models"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
var mdb *db.MongoDB
func Init() {
mdb = db.Init(models.SceneBanner)
// 旧版本通过 scene_1 唯一索引限制每个场景只能配置一条 Banner。
// 多图轮播需要先移除该索引,已有 Banner 数据不受影响。
if err := coll().DropIndexIfExists("scene_1"); err != nil {
panic(fmt.Sprintf("%s model drop legacy index err ==>[%+v]", models.SceneBanner, err))
}
if _, err := coll().CreateIndex([]mongo.IndexModel{
{
Keys: bson.D{
{Key: "scene", Value: 1},
{Key: "sort", Value: -1},
{Key: "updatedAt", Value: -1},
},
},
{
Keys: bson.D{
{Key: "scene", Value: 1},
{Key: "enable", Value: 1},
{Key: "startAt", Value: 1},
{Key: "endAt", Value: 1},
},
},
}); err != nil {
panic(fmt.Sprintf("%s model set index err ==>[%+v]", models.SceneBanner, err))
}
}
func coll() *db.MongoTool {
return mdb.Coll(models.SceneBanner)
}
func Insert(p *SceneBanner) error {
p.Normalize()
if err := p.Validate(); err != nil {
return err
}
now := time.Now()
p.ID = primitive.NewObjectID()
p.CreatedAt = now
p.UpdatedAt = now
_, err := coll().InsertOne(p)
return err
}
func Update(p *SceneBanner) error {
if p.ID.IsZero() {
return fmt.Errorf("id is required")
}
p.Normalize()
if err := p.Validate(); err != nil {
return err
}
p.UpdatedAt = time.Now()
set := bson.M{
"scene": p.Scene,
"imageUrl": p.ImageURL,
"mediaType": p.MediaType,
"linkType": p.LinkType,
"linkValue": p.LinkValue,
"sort": p.Sort,
"enable": p.Enable,
"updatedAt": p.UpdatedAt,
}
update := bson.M{"$set": set}
unset := bson.M{}
if p.StartAt == nil {
unset["startAt"] = ""
} else {
set["startAt"] = *p.StartAt
}
if p.EndAt == nil {
unset["endAt"] = ""
} else {
set["endAt"] = *p.EndAt
}
if len(unset) > 0 {
update["$unset"] = unset
}
result, err := coll().UpdateOne(bson.M{"_id": p.ID}, update)
if err != nil {
return err
}
if result.MatchedCount == 0 {
return mongo.ErrNoDocuments
}
return nil
}
func Delete(id primitive.ObjectID) error {
if id.IsZero() {
return fmt.Errorf("id is required")
}
_, err := coll().DeleteById(id)
return err
}
func List(scene string) ([]SceneBanner, error) {
filter := bson.M{}
if scene != "" {
filter["scene"] = scene
}
var list []SceneBanner
err := coll().Find(&list, filter, bannerFindOptions())
return list, err
}
func FindActive(scene string, now time.Time) ([]SceneBanner, error) {
filter := bson.M{
"scene": scene,
"enable": true,
"$and": bson.A{
bson.M{"$or": bson.A{
bson.M{"startAt": bson.M{"$exists": false}},
bson.M{"startAt": nil},
bson.M{"startAt": bson.M{"$lte": now}},
}},
bson.M{"$or": bson.A{
bson.M{"endAt": bson.M{"$exists": false}},
bson.M{"endAt": nil},
bson.M{"endAt": bson.M{"$gt": now}},
}},
},
}
var list []SceneBanner
err := coll().Find(&list, filter, bannerFindOptions())
return list, err
}
func bannerFindOptions() *options.FindOptions {
return options.Find().
SetSort(bson.D{{Key: "sort", Value: -1}, {Key: "updatedAt", Value: -1}})
}
+85
View File
@@ -0,0 +1,85 @@
package scenebannermod
import (
"fmt"
"strings"
"time"
"go.mongodb.org/mongo-driver/bson/primitive"
)
const (
SceneCommentTop = "COMMENT_TOP"
MediaTypeImage = "IMAGE"
MediaTypeGIF = "GIF"
LinkTypeInternal = "INTERNAL"
LinkTypeExternal = "EXTERNAL"
LinkTypeNone = "NONE"
)
type SceneBanner struct {
ID primitive.ObjectID `json:"id" bson:"_id,omitempty"`
Scene string `json:"scene" bson:"scene"`
ImageURL string `json:"imageUrl" bson:"imageUrl"`
MediaType string `json:"mediaType" bson:"mediaType"`
LinkType string `json:"linkType" bson:"linkType"`
LinkValue string `json:"linkValue" bson:"linkValue"`
Sort int `json:"sort" bson:"sort"`
Enable bool `json:"enable" bson:"enable"`
StartAt *time.Time `json:"startAt,omitempty" bson:"startAt,omitempty"`
EndAt *time.Time `json:"endAt,omitempty" bson:"endAt,omitempty"`
CreatedAt time.Time `json:"createdAt" bson:"createdAt"`
UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"`
}
func ValidScene(scene string) bool {
return scene == SceneCommentTop
}
func (p *SceneBanner) Normalize() {
p.Scene = strings.TrimSpace(strings.ToUpper(p.Scene))
p.ImageURL = strings.TrimSpace(p.ImageURL)
p.MediaType = strings.TrimSpace(strings.ToUpper(p.MediaType))
p.LinkType = strings.TrimSpace(strings.ToUpper(p.LinkType))
p.LinkValue = strings.TrimSpace(p.LinkValue)
}
func (p SceneBanner) Validate() error {
if !ValidScene(p.Scene) {
return fmt.Errorf("unsupported scene: %s", p.Scene)
}
if p.ImageURL == "" {
return fmt.Errorf("imageUrl is required")
}
if p.MediaType != MediaTypeImage && p.MediaType != MediaTypeGIF {
return fmt.Errorf("unsupported mediaType: %s", p.MediaType)
}
switch p.LinkType {
case LinkTypeInternal, LinkTypeExternal:
if p.LinkValue == "" {
return fmt.Errorf("linkValue is required for %s", p.LinkType)
}
case LinkTypeNone:
if p.LinkValue != "" {
return fmt.Errorf("linkValue must be empty when linkType is NONE")
}
default:
return fmt.Errorf("unsupported linkType: %s", p.LinkType)
}
if p.StartAt != nil && p.EndAt != nil && !p.EndAt.After(*p.StartAt) {
return fmt.Errorf("endAt must be later than startAt")
}
return nil
}
func (p SceneBanner) ActiveAt(now time.Time) bool {
if !p.Enable {
return false
}
if p.StartAt != nil && p.StartAt.After(now) {
return false
}
return p.EndAt == nil || p.EndAt.After(now)
}
+47
View File
@@ -0,0 +1,47 @@
package scenebannermod
import (
"reflect"
"testing"
"time"
"go.mongodb.org/mongo-driver/bson"
)
func TestSceneBannerValidate(t *testing.T) {
banner := SceneBanner{
Scene: "comment_top",
ImageURL: "https://example.com/banner.gif",
MediaType: "gif",
LinkType: "internal",
LinkValue: "video://detail?id=1",
}
banner.Normalize()
if err := banner.Validate(); err != nil {
t.Fatal(err)
}
if banner.Scene != SceneCommentTop || banner.MediaType != MediaTypeGIF {
t.Fatalf("unexpected normalized banner: %+v", banner)
}
}
func TestSceneBannerActiveAt(t *testing.T) {
now := time.Now()
start := now.Add(-time.Minute)
end := now.Add(time.Minute)
banner := SceneBanner{Enable: true, StartAt: &start, EndAt: &end}
if !banner.ActiveAt(now) {
t.Fatal("banner should be active")
}
}
func TestBannerFindOptionsSupportsCarousel(t *testing.T) {
opts := bannerFindOptions()
if opts.Limit != nil {
t.Fatalf("carousel query must not limit result count: %d", *opts.Limit)
}
wantSort := bson.D{{Key: "sort", Value: -1}, {Key: "updatedAt", Value: -1}}
if !reflect.DeepEqual(opts.Sort, wantSort) {
t.Fatalf("unexpected carousel sort: %#v", opts.Sort)
}
}