Files
rootandClaude Opus 5 8679200f41 Initial commit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 13:57:10 +08:00

653 lines
19 KiB
Go

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
}