package officialwebsiteser import ( "91porn-server/models/v/tagmod" "91porn-server/models/v/vidmod" "math/rand/v2" "time" "91porn-server/common/stderr" officialwebsitemod "91porn-server/models/v/officialWebsitemod" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" ) // ───────────────────────────────────────── // Service functions // ───────────────────────────────────────── func CheckVideoExistByVid(vid []primitive.ObjectID) ([]officialwebsitemod.Video, error) { filter := bson.M{"_id": bson.M{"$in": vid}} mod := &officialwebsitemod.Video{} videos, err := mod.FindMany(filter) return videos, err } func ListVideo(req *VideoListReq) (int64, []officialwebsitemod.Video, error) { filter, err := req.Query() if err != nil { return 0, nil, err } mod := &officialwebsitemod.Video{} total, err := mod.Count(filter) if err != nil { return 0, nil, err } list, err := mod.FindMany(filter, req.FindOptions()) if err != nil { return 0, nil, err } return total, list, nil } // resolveVideoSeoSlug 处理 seoSlug:手填则校验格式并查重(冲突报错);为空则由标题生成雏形并追加后缀去重。 func resolveVideoSeoSlug(provided, title string, excludeID primitive.ObjectID) (string, error) { exists := func(slug string) (bool, error) { filter := officialwebsitemod.M{"seoSlug": slug} if !excludeID.IsZero() { filter["_id"] = officialwebsitemod.M{"$ne": excludeID} } n, err := (&officialwebsitemod.Video{}).Count(filter) return n > 0, err } if provided != "" { if !ValidateSeoSlug(provided) { return "", stderr.ErrParamError } used, err := exists(provided) if err != nil { return "", err } if used { return "", stderr.ErrParamError } return provided, nil } return UniqueSeoSlug(GenerateSeoSlug(title), exists) } func CreateVideo(req *ModifyVideoReq) (officialwebsitemod.Video, error) { data, err := req.toMod() if err != nil { return officialwebsitemod.Video{}, err } // SEO slug:手填优先(校验唯一),为空则由标题生成雏形并去重 if data.SeoSlug, err = resolveVideoSeoSlug(data.SeoSlug, data.Title, primitive.NilObjectID); err != nil { return officialwebsitemod.Video{}, err } if err := data.Create(); err != nil { return officialwebsitemod.Video{}, err } return data, nil } func UpdateVideo(req *ModifyVideoReq) (resp CountResp, err error) { if req == nil || req.ID == "" { return } id, parseErr := primitive.ObjectIDFromHex(req.ID) if parseErr != nil || isNilObjectID(id) { err = stderr.ErrParamError return } mod := &officialwebsitemod.Video{} if err = mod.FindOne(officialwebsitemod.M{"_id": id}); err != nil { return } // 若提交了 seoSlug,先校验/去重(排除自身),回写后由 toM 落库 if req.SeoSlug != nil { var slug string if slug, err = resolveVideoSeoSlug(*req.SeoSlug, mod.Title, id); err != nil { return } *req.SeoSlug = slug } var update officialwebsitemod.M if update, err = req.toM(); err != nil || update == nil { return } resp.Count, err = mod.Update(officialwebsitemod.M{"_id": id}, update) return } func DeleteVideo(req *DeleteVideoReq) (data DeleteVideoResp, err error) { if req == nil { err = stderr.CodeEmptyData return } var id primitive.ObjectID if id, err = primitive.ObjectIDFromHex(req.ID); err != nil { return } if isNilObjectID(id) { err = stderr.ErrParamError return } mod := &officialwebsitemod.Video{} data.Count, err = mod.Delete(officialwebsitemod.M{"_id": id}) return } func BatchUpdateVideoOwner(req *BatchUpdateVideoOwnerReq) (data BatchUpdateVideoOwnerResp, err error) { if req == nil { err = stderr.ErrParamError return } // Check owner is exist var ownerID primitive.ObjectID ownerID, err = primitive.ObjectIDFromHex(req.OwnerID) if err != nil { return } var heroMod = &officialwebsitemod.Hero{} err = heroMod.FindOne(officialwebsitemod.M{"_id": ownerID}) if err != nil { return } if isNilObjectID(heroMod.ID) { err = stderr.OfficialWebsiteHeroExist return } var update = bson.M{"$set": bson.M{"heroId": ownerID}} // Check album is exist, if exist then add into update if req.AlbumID != "" { var albumID primitive.ObjectID albumID, err = primitive.ObjectIDFromHex(req.AlbumID) if err != nil { return } var album = &officialwebsitemod.Album{} err = album.FindOne(officialwebsitemod.M{"_id": albumID}) if err != nil { return } update = bson.M{"$set": bson.M{"heroId": ownerID, "albumId": albumID}} } var videoIDs []primitive.ObjectID for i := range req.VideoIDs { videoID, parseErr := primitive.ObjectIDFromHex(req.VideoIDs[i]) err = parseErr if err != nil { return } videoIDs = append(videoIDs, videoID) } var videoMod = &officialwebsitemod.Video{} filter := bson.M{"_id": bson.M{"$in": videoIDs}} data.Count, err = videoMod.UpdateMany(filter, update) return } // ───────────────────────────────────────── // Batch (Modify) types // ───────────────────────────────────────── type BatchUpdateVideoOwnerReq struct { VideoIDs []string `json:"video_ids"` OwnerID string `json:"owner_id"` AlbumID string `json:"album_id"` } type BatchUpdateVideoOwnerResp struct { Count int64 `json:"count"` } // ───────────────────────────────────────── // Mutation (Modify) types // ───────────────────────────────────────── type ModifyVideoReq struct { ID string `json:"id" form:"id"` AlbumID *string `json:"albumId" form:"albumId"` HeroID *string `json:"heroId" form:"heroId"` Title *string `json:"title"` Description *string `json:"description"` Cover *string `json:"cover"` Url *string `json:"url"` SeoSlug *string `json:"seoSlug"` Tags *[]officialwebsitemod.Tag `json:"tags"` IsHot *bool `json:"isHot"` Sort *int64 `json:"sort"` IsActive *bool `json:"isActive"` WatchCount *int64 `json:"watchCount"` LikeCount *int64 `json:"likeCount"` CommentCount *int64 `json:"commentCount"` CollectCount *int64 `json:"collectCount"` } // ───────────────────────────────────────── // Delete types // ───────────────────────────────────────── type DeleteVideoReq struct { ID string `json:"id" form:"id"` } type DeleteVideoResp struct { Count int64 `json:"count"` } // ───────────────────────────────────────── // toMod — ModifyVideoReq → model struct // ───────────────────────────────────────── func (req *ModifyVideoReq) toMod() (officialwebsitemod.Video, error) { if req == nil { return officialwebsitemod.Video{}, nil } data := officialwebsitemod.Video{ Title: stringOrZero(req.Title), Description: stringOrZero(req.Description), Cover: stringOrZero(req.Cover), Url: stringOrZero(req.Url), IsHot: boolOrZero(req.IsHot), SortModel: officialwebsitemod.SortModel{ Sort: int64OrZero(req.Sort), IsActive: boolOrZero(req.IsActive), }, WatchCount: int64OrZero(req.WatchCount), LikeCount: int64OrZero(req.LikeCount), CommentCount: int64OrZero(req.CommentCount), CollectCount: int64OrZero(req.CollectCount), } if req.AlbumID != nil { if *req.AlbumID != "" { albumID, err := primitive.ObjectIDFromHex(*req.AlbumID) if err != nil || isNilObjectID(albumID) { return officialwebsitemod.Video{}, stderr.ErrParamError } data.AlbumID = albumID } } if req.HeroID != nil { if *req.HeroID != "" { heroID, err := primitive.ObjectIDFromHex(*req.HeroID) if err != nil || isNilObjectID(heroID) { return officialwebsitemod.Video{}, stderr.ErrParamError } data.HeroID = heroID } } if req.Tags != nil { data.Tags = *req.Tags } if req.SeoSlug != nil { data.SeoSlug = *req.SeoSlug } return data, nil } // ───────────────────────────────────────── // toM — ModifyVideoReq → officialwebsitemod.M // ───────────────────────────────────────── func (req *ModifyVideoReq) toM() (officialwebsitemod.M, error) { if req == nil { return nil, stderr.CodeEmptyData } set := officialwebsitemod.M{} if req.AlbumID != nil { if *req.AlbumID != "" { albumID, err := primitive.ObjectIDFromHex(*req.AlbumID) if err != nil || isNilObjectID(albumID) { return nil, stderr.ErrParamError } set["albumId"] = albumID } } if req.HeroID != nil { if *req.HeroID != "" { heroID, err := primitive.ObjectIDFromHex(*req.HeroID) if err != nil || isNilObjectID(heroID) { return nil, stderr.ErrParamError } set["heroId"] = heroID } } if req.Title != nil { set["title"] = *req.Title } if req.Description != nil { set["description"] = *req.Description } if req.Cover != nil { set["cover"] = *req.Cover } if req.Url != nil { set["url"] = *req.Url } if req.Tags != nil { set["tags"] = *req.Tags } if req.SeoSlug != nil && *req.SeoSlug != "" { set["seoSlug"] = *req.SeoSlug } if req.IsHot != nil { set["isHot"] = *req.IsHot } if req.Sort != nil { set["sort"] = *req.Sort } if req.IsActive != nil { set["isActive"] = *req.IsActive } if req.WatchCount != nil { set["watchCount"] = *req.WatchCount } if req.LikeCount != nil { set["likeCount"] = *req.LikeCount } if req.CommentCount != nil { set["commentCount"] = *req.CommentCount } if req.CollectCount != nil { set["collectCount"] = *req.CollectCount } if len(set) == 0 { return nil, nil } set["updatedAt"] = time.Now().UTC() return officialwebsitemod.M{"$set": set}, nil } func BatchImportVideo(req *BatchImportVideoReq) (resp BatchImportVideoResp, err error) { if req == nil || len(req.VideoIds) == 0 { err = stderr.ErrParamError return } objIds := make([]primitive.ObjectID, 0, len(req.VideoIds)) videoIDSet := make(map[primitive.ObjectID]struct{}, len(req.VideoIds)) for _, videoId := range req.VideoIds { var id primitive.ObjectID id, err = primitive.ObjectIDFromHex(videoId) if err != nil { return } if _, exists := videoIDSet[id]; exists { continue } videoIDSet[id] = struct{}{} objIds = append(objIds, id) } if len(objIds) == 0 { return } // 1) Filter out videos that already exist in official website video table by `_id`. var videoMod = &officialwebsitemod.Video{} officialVideos, findVideoErr := videoMod.FindMany(officialwebsitemod.M{"_id": bson.M{"$in": objIds}}) if findVideoErr != nil { err = findVideoErr return } existedVideoMap := make(map[primitive.ObjectID]struct{}, len(officialVideos)) for _, v := range officialVideos { existedVideoMap[v.ID] = struct{}{} } needImportVideoIDs := make([]primitive.ObjectID, 0, len(objIds)) for _, id := range objIds { if _, exists := existedVideoMap[id]; exists { continue } needImportVideoIDs = append(needImportVideoIDs, id) } if len(needImportVideoIDs) == 0 { return } mats := []vidmod.Matcher{ (&vidmod.IDInMatch{IDs: needImportVideoIDs}).New(), } list, err := vidmod.FindMany(mats...) if err != nil { return } if len(list) == 0 { return } var insertVideos []officialwebsitemod.Video allTagIDs := make([]primitive.ObjectID, 0) allTagIDSet := make(map[primitive.ObjectID]struct{}) for _, video := range list { insertVideos = append(insertVideos, officialwebsitemod.Video{ ID: video.ID, Title: video.Title, Description: video.Content, Cover: video.Cover, Url: video.SourceURL, IsHot: false, WatchCount: int64(video.FakePlayCount+video.PlayCount) * rand.Int64N(10), LikeCount: int64(video.FakeLikeCount+video.LikeCount) * rand.Int64N(10), CommentCount: int64(video.FakeCommentCount+video.CommentCount) * rand.Int64N(10), CollectCount: int64(video.CollectCount) * rand.Int64N(10), SortModel: officialwebsitemod.SortModel{ IsActive: true, }, BaseModel: officialwebsitemod.BaseModel{ CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), }, }) for _, tag := range video.Tags { if _, exists := allTagIDSet[tag]; exists { continue } allTagIDSet[tag] = struct{}{} allTagIDs = append(allTagIDs, tag) } } if len(insertVideos) == 0 { return } // 2) Filter out tags that already exist in official website tag table by `_id`. var tagMod = &officialwebsitemod.Tag{} existedTagMap := map[primitive.ObjectID]struct{}{} existedOfficialTagInfoMap := map[primitive.ObjectID]officialwebsitemod.Tag{} if len(allTagIDs) > 0 { officialWebsiteTags, findTagErr := tagMod.FindMany(officialwebsitemod.M{ "_id": bson.M{"$in": allTagIDs}, "type": officialwebsitemod.OfficialWebsiteTagTypeVideo, }) if findTagErr != nil { err = findTagErr return } existedTagMap = make(map[primitive.ObjectID]struct{}, len(officialWebsiteTags)) for _, t := range officialWebsiteTags { existedTagMap[t.ID] = struct{}{} existedOfficialTagInfoMap[t.ID] = t } } needImportTagIDs := make([]primitive.ObjectID, 0, len(allTagIDs)) for _, tagID := range allTagIDs { if _, exists := existedTagMap[tagID]; exists { continue } needImportTagIDs = append(needImportTagIDs, tagID) } tagsMap, err := tagmod.FindTagsMapByIDS(needImportTagIDs) if err != nil { return } var insertTags []officialwebsitemod.Tag for _, tagID := range needImportTagIDs { tag, ok := tagsMap[tagID] if !ok { continue } insertTags = append(insertTags, officialwebsitemod.Tag{ ID: tag.ID, Name: tag.TagName, Type: officialwebsitemod.OfficialWebsiteTagTypeVideo, IsHot: len(tag.HotMark) > 0, SortModel: officialwebsitemod.SortModel{ IsActive: true, }, BaseModel: officialwebsitemod.BaseModel{ CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), }, }) } // Fill video tags from tag info map to keep video docs self-contained. for i := range insertVideos { srcVideo := list[i] videoTags := make([]officialwebsitemod.Tag, 0, len(srcVideo.Tags)) for _, tagID := range srcVideo.Tags { if oldTag, exists := existedOfficialTagInfoMap[tagID]; exists { videoTags = append(videoTags, officialwebsitemod.Tag{ ID: oldTag.ID, Name: oldTag.Name, Type: officialwebsitemod.OfficialWebsiteTagTypeVideo, IsHot: oldTag.IsHot, SortModel: officialwebsitemod.SortModel{ Sort: oldTag.Sort, IsActive: true, }, }) continue } tag, ok := tagsMap[tagID] if !ok { continue } videoTags = append(videoTags, officialwebsitemod.Tag{ ID: tag.ID, Name: tag.TagName, Type: officialwebsitemod.OfficialWebsiteTagTypeVideo, IsHot: len(tag.HotMark) > 0, SortModel: officialwebsitemod.SortModel{ IsActive: true, }, }) } insertVideos[i].Tags = videoTags } // 3) Insert filtered videos and tags into official website tables separately. var ids []primitive.ObjectID ids, err = videoMod.InsertMany(insertVideos) if err != nil { return } for i := range insertTags { if createErr := insertTags[i].Create(); createErr != nil { err = createErr return } } resp.Count = int64(len(ids)) return } // ───────────────────────────────────────── // Batch Import types // ───────────────────────────────────────── type BatchImportVideoReq struct { VideoIds []string `json:"videoIds"` } type BatchImportVideoResp struct { Count int64 `json:"count"` } // ───────────────────────────────────────── // List types // ───────────────────────────────────────── type VideoListReq struct { PageReq ID *string `form:"id"` IDs []string `form:"ids"` PrimiteveIDs []primitive.ObjectID `form:"primitiveIds"` AlbumID *string `form:"albumId"` HeroID *string `form:"heroID"` Title *string `form:"title"` Description *string `form:"description"` Cover *string `form:"cover"` Url *string `form:"url"` Tags *string `form:"tags"` IsHot *bool `form:"isHot"` Sort *int64 `form:"sort"` IsActive *bool `form:"isActive"` CreatedAt *time.Time `form:"createdAt"` UpdatedAt *time.Time `form:"updatedAt"` DeletedAt *time.Time `form:"deletedAt"` } func (r *VideoListReq) Query() (officialwebsitemod.M, error) { filter := officialwebsitemod.M{} if err := parseObjectIDFilter(filter, "_id", r.ID); err != nil { return nil, err } if len(r.IDs) > 0 { var objIDs []primitive.ObjectID for _, id := range r.IDs { objID, err := primitive.ObjectIDFromHex(id) if err != nil { return nil, err } objIDs = append(objIDs, objID) } filter["_id"] = bson.M{"$in": objIDs} } if len(r.PrimiteveIDs) > 0 { filter["_id"] = bson.M{"$in": r.PrimiteveIDs} } if err := parseObjectIDFilter(filter, "albumId", r.AlbumID); err != nil { return nil, err } if err := parseObjectIDFilter(filter, "heroId", r.HeroID); err != nil { return nil, err } if r.Title != nil { filter["title"] = *r.Title } if r.Description != nil { filter["description"] = *r.Description } if r.Cover != nil { filter["cover"] = *r.Cover } if r.Url != nil { filter["url"] = *r.Url } if err := parseJSONFilter(filter, "tags", r.Tags, &[]officialwebsitemod.Tag{}); err != nil { return nil, err } if r.IsHot != nil { filter["isHot"] = *r.IsHot } if r.Sort != nil { filter["sort"] = *r.Sort } if r.IsActive != nil { filter["isActive"] = *r.IsActive } if r.CreatedAt != nil { filter["createdAt"] = *r.CreatedAt } if r.UpdatedAt != nil { filter["updatedAt"] = *r.UpdatedAt } if r.DeletedAt != nil { filter["deletedAt"] = *r.DeletedAt } return filter, nil }