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

1096 lines
37 KiB
Go
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package mediaser
import (
"91porn-server/common"
"91porn-server/common/constant"
"91porn-server/common/db"
"91porn-server/common/file"
"91porn-server/common/httputil"
"91porn-server/common/log"
"91porn-server/common/redis"
sli "91porn-server/common/slice"
"91porn-server/common/stderr"
topser "91porn-server/common/top"
topasist "91porn-server/common/top/asistant"
"91porn-server/models/commod"
"91porn-server/models/v/mediacontentmod"
"91porn-server/models/v/mediatagmod"
"91porn-server/models/v/moduleconfmod"
"91porn-server/models/v/modulesectionmod"
"91porn-server/models/v/vidmod"
"91porn-server/web/webg"
"bytes"
"errors"
"fmt"
"strings"
"time"
"go.mongodb.org/mongo-driver/mongo/options"
"91porn-server/models/v/mediamod"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type WebListReq struct {
ID *string `form:"id" bson:"id,omitempty"` // ID
Title *string `form:"title" json:"title"` // 帖子标题
Kind *int `form:"kind" bson:"kind,omitempty"` // 帖子种类 1:动漫,2:漫画
UpdateStatus *int `form:"updateStatus" bson:"updateStatus,omitempty"` // 连载状态
Type *string `form:"type" bson:"type,omitempty"` // 类型 "video":视频,"image":图片
Status *int `form:"status" bson:"status,omitempty"` // 是否开启
Permission *int `form:"permission" json:"permission"` // 观看权限
Start *time.Time `form:"start" json:"start"` // 开始时间
End *time.Time `form:"end" json:"end"` // 结束时间
Tag *string `form:"tag" json:"tag"` // 标签名称
SID *string `form:"sId" json:"sId"` // 专题ID
MID *string `form:"mId" json:"mId"` // 模块ID
MediaType *string `form:"mediaType" json:"mediaType"` // 媒体类型
MediaSubType *int `form:"mediaSubType" json:"mediaSubType"` // 媒体子类型(小说使用)0文本小说 1-有声小说
MediaCenterID *uint `form:"mediaCenterId" json:"mediaCenterId"` // 媒资库资源ID
PageNumber uint64 `form:"pageNumber" json:"pageNumber" binding:"required,min=1"` // 当前页
PageSize uint64 `form:"pageSize" json:"pageSize" binding:"required,min=1,max=100"` // 每页条数
}
type WebListRes struct {
Total int64 `json:"total"`
List []*mediamod.Media `json:"list"`
}
func (q *WebListReq) Filter() bson.M {
var query = bson.M{}
if q.Title != nil {
i := make(map[string]string)
i["$regex"] = *q.Title
i["$options"] = "i"
query["title"] = i
}
if q.Status != nil {
query["status"] = *q.Status
}
if q.Type != nil {
query["mediaType"] = *q.Type
}
if q.Permission != nil {
query["permission"] = *q.Permission
}
if q.Tag != nil {
//tag, _ := mediatagmod.FindOneTagByName(*q.Tag)
//query["tags"] = tag.ID
tagIds, _ := mediatagmod.FindTagIdsByName(*q.Tag)
if len(tagIds) > 0 {
query["tags"] = bson.M{"$in": tagIds}
}
}
if q.Start != nil && q.End != nil {
i := make(map[string]time.Time)
i["$gte"] = *q.Start
i["$lt"] = *q.End
query["createdAt"] = i
}
if q.ID != nil {
id, _ := primitive.ObjectIDFromHex(*q.ID)
query["_id"] = id
}
if q.SID != nil {
id, _ := primitive.ObjectIDFromHex(*q.SID)
query["sId"] = id
}
if q.MID != nil {
id, _ := primitive.ObjectIDFromHex(*q.MID)
query["mId"] = id
}
if q.MediaType != nil {
query["mediaType"] = *q.MediaType
}
if q.MediaSubType != nil {
query["mediaSubType"] = *q.MediaSubType
}
if q.Kind != nil {
query["kind"] = *q.Kind
}
if q.UpdateStatus != nil && *q.UpdateStatus > 0 {
query["updateStatus"] = *q.UpdateStatus
}
if q.MediaCenterID != nil {
query["mediaCenterId"] = *q.MediaCenterID
}
query["isDelete"] = false
return query
}
func (q *WebListReq) Options() *options.FindOptions {
sort := bson.M{"createdAt": -1}
if q.SID != nil {
sort = bson.M{"sectionSort": -1}
}
return options.Find().SetSkip(int64((q.PageNumber - 1) * q.PageSize)).SetLimit(int64(q.PageSize)).SetSort(sort)
}
// GetList 获取列表
func (q *WebListReq) GetList() (res *WebListRes, err error) {
var out = &WebListRes{}
count, err := mediamod.QueryAllCount(q.Filter())
if err != nil {
return nil, err
}
if count == 0 {
out.List = []*mediamod.Media{}
out.Total = 0
return out, nil
}
out.Total = count
out.List, err = mediamod.QueryAllList(q.Filter(), q.Options())
if err != nil {
log.Error(fmt.Sprintf("mediamod queryAllList error:%+v:", err))
return nil, err
}
if len(out.List) > 0 {
var tags []primitive.ObjectID
for _, m := range out.List {
if len(m.Tags) > 0 {
tags = append(tags, m.Tags...)
}
}
if len(tags) > 0 {
toMap, err := mediatagmod.FindFilterToMap(bson.M{"_id": bson.M{"$in": tags}})
if err != nil {
return out, stderr.ErrDbQueryError
}
if toMap != nil {
for i, v := range out.List {
var tagObjs []mediamod.TagDetail
if len(v.Tags) > 0 {
for _, id := range v.Tags {
if toMap[id] != nil {
tagObjs = append(tagObjs, mediamod.TagDetail{
Id: id,
Name: toMap[id].Name,
})
}
}
}
out.List[i].TagDetails = tagObjs
out.List[i].CountPurchasesRate = float32(out.List[i].CountBrowse) / float32(out.List[i].CountView+1)
}
}
}
}
return out, nil
}
// Export 导出
func (q *WebListReq) Export() (buff bytes.Buffer, code error) {
count, err := mediamod.QueryAllCount(q.Filter())
if err != nil {
return
}
if count == 0 {
return
}
step := 900
totalPages := int(count)/step + 1
list := []*mediamod.WebExportMedia{}
for page := 1; page <= totalPages; page++ {
q.PageSize = uint64(step)
q.PageNumber = uint64(page)
res, err := mediamod.QueryAllList(q.Filter(), q.Options())
if err != nil {
log.Error(fmt.Sprintf("collectionmod queryAllList error:%+v:", err))
continue
}
// 没有查到数据直接停止
if len(res) == 0 {
break
}
var tags []primitive.ObjectID
for _, m := range res {
if len(m.Tags) > 0 {
tags = append(tags, m.Tags...)
}
}
tags = sli.RemoveRepObjectID(tags)
toMap, err := mediatagmod.FindFilterToMap(bson.M{"_id": bson.M{"$in": tags}})
if err != nil {
return
}
for _, v := range res {
item := &mediamod.WebExportMedia{
ID: v.ID,
Title: v.Title,
Kind: mediamod.KindName(v.Kind),
Direction: mediamod.DirectionName(v.Direction),
TotalEpisode: v.TotalEpisode,
UpdateStatus: mediamod.UpdateStatusName(v.UpdateStatus),
Status: mediamod.StatusName(v.Status),
ModuleName: v.ModuleName,
SectionName: v.SectionName,
MediaType: mediamod.MediaTypeName(v.MediaType),
FreeTime: v.FreeTime,
Price: v.Price,
CountBrowse: v.CountBrowse,
CountCollect: v.CountCollect,
CountComment: v.CountComment,
CountLike: v.CountLike,
CountPurchases: v.CountPurchases,
CountView: v.CountView,
Permission: mediamod.PermissionName(v.Permission),
PermissionIconHide: v.PermissionIconHide,
Summary: v.Summary,
UpdatedAct: v.UpdatedAct,
CreatedAt: v.CreatedAt.Format("2006-01-02 15:04:05"),
UpdateTime: v.UpdateTime.Format("2006-01-02 15:04:05"),
}
tagNameList := []string{}
for _, tagId := range v.Tags {
tag, ok := toMap[tagId]
if !ok {
continue
}
tagNameList = append(tagNameList, tag.Name)
}
item.Tags = strings.Join(tagNameList, ",")
list = append(list, item)
}
fmt.Println(len(list))
}
excelFile := file.WriteStruct2Xlsx("动漫列表", list)
defer excelFile.Close()
_ = excelFile.DeleteSheet("Sheet1")
_, _ = excelFile.WriteTo(&buff)
return buff, nil
}
type WebFindMediaReq struct {
ID string `form:"id" bson:"id,omitempty"` // ID
}
type WebFindMediaRes struct {
MediaTitle string `json:"mediaTitle" bson:"mediaTitle"` // 媒体标题
MediaType string `json:"mediaType" bson:"mediaType"` // 媒体类型
IsExist bool `json:"isExist" bson:"isExist"` // 是否存在
}
func (q *WebFindMediaReq) Filter() bson.M {
var query = bson.M{}
id, _ := primitive.ObjectIDFromHex(q.ID)
query["_id"] = id
query["isDelete"] = false
return query
}
// GetMediaByID 获取列表
func (q *WebFindMediaReq) GetMediaByID() (res *WebFindMediaRes, err error) {
var out = &WebFindMediaRes{}
data, err := mediamod.QueryMediaByID(q.Filter())
if err != nil {
log.Error(fmt.Sprintf("mediamod QueryMediaByID error:%+v:", err))
return nil, err
}
if data == nil || data.ID.IsZero() {
return nil, errors.New("media data is null")
}
if !data.ID.IsZero() {
out.IsExist = true
out.MediaTitle = data.Title
out.MediaType = data.MediaType
} else {
out.IsExist = false
}
return out, nil
}
type WebCreateReq struct {
SortCode int `json:"sortCode" form:"sortCode"` // 排序
Status int `json:"status" form:"status"` // 是否开启
Title string `json:"title" form:"title"` // 标题
HorizontalCover string `json:"horizontalCover" form:"horizontalCover"` // 横版封面
VerticalCover string `json:"verticalCover" form:"verticalCover"` // 竖版封面
Tags []primitive.ObjectID `json:"tags" form:"tags"` // 标签
Summary string `json:"summary" form:"summary"` // 简介
TotalEpisode int `json:"totalEpisode" form:"totalEpisode"` // 总集数
UpdateStatus int `json:"updateStatus" bson:"updateStatus"` // 更新状态
MediaType string `json:"mediaType" form:"mediaType"` // 媒体类型
MediaSubType mediamod.MediaSubType `json:"mediaSubType" form:"mediaSubType"` // 子类型 ,暂时只有小说使用 0-默认文本小说 1-有声小说
Kind int `json:"kind" form:"kind"` // 种类 (1、哩番,2、3D,3、同人动画,4、单行本,5、连载漫画,6、韩漫 )
PermissionIconHide bool `json:"permissionIconHide" form:"permissionIconHide"` // 售卖类型标识隐藏
Permission int `json:"permission" form:"permission"` // 收听权限 0:会员 1:金币购买 2:免费
Price int64 `json:"price" form:"price"` // 价格
Direction int64 `json:"direction" form:"direction"` // 排版方向 0横 1竖(冗余字段)
FreeTime int `json:"freeTime" form:"freeTime"` // 免费试看时长 单位 s
FreeEpisode int `json:"freeEpisode" form:"freeEpisode"` // 免费集数(前几集免费)
Choice primitive.ObjectID `json:"choice" form:"choice"` // 精选
ChoiceSort int `json:"choiceSort" form:"choiceSort"` // 精选专题排序
IsActive bool `json:"isActive" form:"isActive"` // 是否激活
CountComment int64 `json:"countComment" form:"countComment"` // 评论数
CountPurchases int64 `json:"countPurchases" form:"countPurchases"` // 购买数
Style int `json:"style" form:"style"` // 对应五种风格背景颜色
CountBrowse int64 `json:"countBrowse" form:"countBrowse"` // 浏览数
CountCollect int64 `json:"countCollect" form:"countCollect"` // 收藏数
CountLike int64 `json:"countLike" form:"countLike"` // 喜欢数
CountView int64 `json:"countView" form:"countView"` // 展现数
Number int64 `json:"number" form:"number"` // 编号
MediaSize int `form:"mediaSize" json:"mediaSize"` // 媒体大小
SourceID string `form:"sourceID" json:"sourceID"` // 资源ID
SourceURL string `form:"sourceURL" json:"sourceURL"` // 资源链接
AudioUrl string `json:"audioUrl" bson:"audioUrl"` // 有声小说地址
Author string `json:"author" bson:"author"` // 作者
Md5 string `form:"md5" json:"md5"` // md5
EpisodeNumber int `json:"episodeNumber" bson:"episodeNumber"` // 第几集
EpisodeNumberName string `form:"episodeNumberName" json:"episodeNumberName"` // 章节名称
ListenPermission int `json:"listenPermission" bson:"listenPermission"` // 收听权限 0:会员 1:金币购买 2:免费
SeriesCover []string `form:"seriesCover" json:"seriesCover"` // 封面
Filename string `form:"filename" json:"filename"` // 文件名称
PlayTime uint `form:"playTime" json:"playTime"` // 时长
Cover string `form:"cover" json:"cover"` // 子集的封面
}
func (p *WebCreateReq) Validate() error {
if p.MediaType != mediamod.MediaTypeDrama {
return nil
}
p.Title = strings.TrimSpace(p.Title)
p.HorizontalCover = strings.TrimSpace(p.HorizontalCover)
p.VerticalCover = strings.TrimSpace(p.VerticalCover)
if p.Title == "" || p.HorizontalCover == "" || p.VerticalCover == "" {
return errors.New("短剧标题、横版封面和竖版封面不能为空")
}
if p.TotalEpisode < 1 {
return errors.New("短剧总集数必须大于0")
}
if p.UpdateStatus != mediamod.Update && p.UpdateStatus != mediamod.Finish {
return errors.New("短剧更新状态只支持1或2")
}
if p.Status != 0 {
return errors.New("短剧创建后请先添加有效首集再上架")
}
if p.Kind != mediamod.Theater {
return errors.New("短剧kind必须为8")
}
if p.Permission != 1 {
return errors.New("短剧主记录permission必须为1")
}
if p.SortCode < 0 {
return errors.New("短剧推荐排序不能小于0")
}
return nil
}
// Create 发布数据
func (p *WebCreateReq) Create(manager string) (code stderr.Code, vid primitive.ObjectID) {
now := time.Now()
if p.MediaType == mediamod.MediaTypeVideo {
vid, _ = primitive.ObjectIDFromHex(p.SourceID)
} else {
vid = primitive.NewObjectID()
}
if p.UpdateStatus == 0 {
p.UpdateStatus = 2
}
data := mediamod.Media{
SortCode: p.SortCode,
Status: p.Status,
Title: p.Title,
HorizontalCover: p.HorizontalCover,
VerticalCover: p.VerticalCover,
Tags: p.Tags,
Summary: p.Summary,
TotalEpisode: p.TotalEpisode,
UpdateStatus: p.UpdateStatus,
MediaType: p.MediaType,
MediaSubType: p.MediaSubType,
Kind: p.Kind,
PermissionIconHide: p.PermissionIconHide,
Permission: p.Permission,
Price: p.Price,
Direction: p.Direction,
Style: p.Style,
FreeTime: p.FreeTime,
FreeEpisode: p.FreeEpisode,
Choice: p.Choice,
ChoiceSort: p.ChoiceSort,
CountComment: p.CountComment,
CountPurchases: 0,
CountBrowse: 0,
CountCollect: 0,
CountLike: 0,
CountView: 0,
Number: 0,
IsDelete: false,
UpdatedAct: manager,
UpdateTime: now,
CreatedAt: now,
}
if p.MediaType == mediamod.MediaTypeImage {
data.DefaultContent = mediamod.DefaultContent{
Id: vid,
Cover: p.Cover,
Name: p.EpisodeNumberName,
UrlSet: p.SeriesCover,
}
}
resultID := vid
err := webg.VideoDB.Trans(func(t *db.MongoTool) error {
// 创建数据
mediaId, err := mediamod.Insert(t, data)
if err != nil {
log.Error(fmt.Sprintf("SubmitBase mediamod.InsertMedia err:%v", err))
return stderr.ErrDbInsertError
}
if p.MediaType == mediamod.MediaTypeDrama {
resultID = mediaId
return nil
}
mc := mediacontentmod.MediaContent{
ID: vid,
MediaID: mediaId,
Cover: p.Cover,
MediaType: p.MediaType,
EpisodeNumber: p.EpisodeNumber,
Name: p.EpisodeNumberName,
ListenPermission: p.ListenPermission,
Price: p.Price,
IsActive: p.IsActive,
}
// 视频
if p.MediaType == mediamod.MediaTypeVideo {
mc.Md5 = p.Md5
mc.VideoUrl = p.SourceURL
mc.Status = 0
}
// 音频
if p.MediaType == mediamod.MediaTypeText {
mc.Md5 = p.Md5
mc.AudioUrl = p.AudioUrl
mc.Author = p.Author
mc.Status = 1
}
// 图集
if p.MediaType == mediamod.MediaTypeImage {
mc.UrlSet = p.SeriesCover
}
// 媒体内容新增
if _, err := mediacontentmod.Insert(t, mc); err != nil {
log.Error(fmt.Sprintf("SubmitBase mediacontentmod Insert err:%v", err))
return stderr.ErrDbInsertError
}
return nil
})
if err != nil {
log.Error(fmt.Sprintf("Media SubmitBase Trans fail error:%+v:", err))
return stderr.Failure, vid
}
return stderr.Success, resultID
}
// DeleteManyByTTL 删除视频 被删除的视屏归属为officialUID,未审核或审核未通过的视屏通过TTL删除
func DeleteManyByTTL(ids ...primitive.ObjectID) (code stderr.Code, data interface{}) {
delSetDoc := (&vidmod.SetDoc{}).SetPublisherID(constant.DeleteUID).SetDeleteAt(time.Now()).SetStatus(vidmod.CheckFailure)
_, err := vidmod.ModifyMany(ids, *delSetDoc)
if err != nil {
return stderr.ErrDbUpdateError, nil
}
return stderr.Success, nil
}
// SendBaseInfo2AWs 上传基本文件信息到aws
func SendBaseInfo2AWs(req mediamod.SendBaseInfo2AWsReq) (code stderr.Code) {
tags := []string{}
if len(req.Tags) > 0 {
for _, t := range req.Tags {
tags = append(tags, t.Hex())
}
}
info := vidmod.BaseVid4Aws{
ID: req.SourceID,
Tags: tags,
CheckSum: req.Md5,
Title: req.Title,
Actors: []string{},
AddedTime: time.Now().Format("2006-01-02"),
PlayTime: uint64(req.PlayTime),
Size: req.MediaSize,
Filename: req.Filename,
Via: "q1",
Type: "sp",
}
params, _ := common.ToJsonM(info)
respBody := commod.Resp{}
if _, err := httputil.DefaultClientPostJsonWithResp(&respBody, webg.Conf.URL.FileInfoUrl, nil, params); err != nil {
log.Error("SendBaseInfo2AWs error", log.Any("req", req), log.Any("err", err))
return stderr.Failure
}
if respBody.Code != stderr.Success {
log.Error("SendBaseInfo2AWs status error", log.Any("req", req), log.Any("respBody.Code", respBody.Code))
return stderr.Failure
}
return stderr.Success
}
type WebUpdateReq struct {
ID primitive.ObjectID `json:"id" binding:"required"`
SortCode *int `json:"sortCode"` // 排序
MediaSubType *mediamod.MediaSubType `json:"mediaSubType" form:"mediaSubType"` // 子类型 ,暂时只有小说使用 0-默认文本小说 1-有声小说
Status *int `json:"status"` // 是否开启 1:上架,2:下架
Title *string `json:"title"` // 标题
HorizontalCover *string `json:"horizontalCover"` // 横版封面
VerticalCover *string `json:"verticalCover"` // 竖版封面
Tags *[]primitive.ObjectID `json:"tags"` // 标签
Summary *string `json:"summary"` // 简介
TotalEpisode *int `json:"totalEpisode"` // 总集数
UpdateStatus *int `json:"updateStatus" bson:"updateStatus"` // 更新状态
Kind *int `json:"kind"` // 种类(1、哩番,2、3D,3、同人动画,4、单行本,5、连载漫画,6、韩漫)
Style *int `json:"style" form:"style"` // 对应五种风格背景颜色
PermissionIconHide *bool `json:"permissionIconHide"` // 售卖类型标识隐藏
Permission *int `json:"permission"` // 收听权限 0:会员 1:金币购买 2:免费
Price *int64 `json:"price"` // 价格
Direction *int64 `json:"direction"` // 排版方向 0横 1竖(冗余字段)
SellType *int `json:"sellType"` // 商品售卖类型(0单集售卖 1整部售卖)
FreeTime *int `json:"freeTime"` // 免费试看时长 单位 s
FreeEpisode *int `json:"freeEpisode" form:"freeEpisode"` // 免费集数(前几集免费)
SectionSort *int `json:"sectionSort" bson:"sectionSort"` // 专题排序
Choice *primitive.ObjectID `json:"choice"` // 精选
ChoiceSort *int `json:"choiceSort"` // 精选专题排序
IsActive *bool `json:"isActive"` // 是否激活
IsRemove *bool `json:"isRemove"` // 是否移出专题
CountComment *int64 `json:"countComment"` // 评论数
CountPurchases *int64 `json:"countPurchases"` // 购买数
CountBrowse *int64 `json:"countBrowse"` // 浏览数
CountCollect *int64 `json:"countCollect"` // 收藏数
CountLike *int64 `json:"countLike"` // 喜欢数
CountView *int64 `json:"countView"` // 展现数
Number *int64 `json:"number"` // 编号
}
func (p *WebUpdateReq) mergedDramaMedia(current mediamod.Media) mediamod.Media {
next := current
if p.SortCode != nil {
next.SortCode = *p.SortCode
}
if p.Status != nil {
next.Status = *p.Status
}
if p.Title != nil {
next.Title = strings.TrimSpace(*p.Title)
}
if p.HorizontalCover != nil {
next.HorizontalCover = strings.TrimSpace(*p.HorizontalCover)
}
if p.VerticalCover != nil {
next.VerticalCover = strings.TrimSpace(*p.VerticalCover)
}
if p.TotalEpisode != nil {
next.TotalEpisode = *p.TotalEpisode
}
if p.UpdateStatus != nil {
next.UpdateStatus = *p.UpdateStatus
}
if p.Kind != nil {
next.Kind = *p.Kind
}
if p.Permission != nil {
next.Permission = *p.Permission
}
return next
}
func validateDramaMediaFields(media mediamod.Media, requireHorizontalCover bool) error {
if strings.TrimSpace(media.Title) == "" || strings.TrimSpace(media.VerticalCover) == "" {
return errors.New("短剧标题和竖版封面不能为空")
}
if requireHorizontalCover && strings.TrimSpace(media.HorizontalCover) == "" {
return errors.New("短剧横版封面不能为空")
}
if media.TotalEpisode < 1 {
return errors.New("短剧总集数必须大于0")
}
if media.UpdateStatus != mediamod.Update && media.UpdateStatus != mediamod.Finish {
return errors.New("短剧更新状态只支持1或2")
}
if media.Status != 0 && media.Status != 1 {
return errors.New("短剧状态只支持0或1")
}
if media.Kind != mediamod.Theater {
return errors.New("短剧kind必须为8")
}
if media.Permission != 1 {
return errors.New("短剧主记录permission必须为1")
}
if media.SortCode < 0 {
return errors.New("短剧推荐排序不能小于0")
}
return nil
}
func validateDramaMediaForPublish(media mediamod.Media, first mediacontentmod.MediaContent) error {
if err := validateDramaMediaFields(media, true); err != nil {
return err
}
return validateDramaFirstEpisodeForPublish(media, first)
}
func validateDramaFirstEpisodeForPublish(media mediamod.Media, first mediacontentmod.MediaContent) error {
if first.ID.IsZero() || first.MediaID != media.ID || first.MediaType != mediamod.MediaTypeDrama || first.EpisodeNumber != 1 || first.IsDelete {
return errors.New("短剧第1集不存在,不能上架整部短剧")
}
if !first.IsActive {
return errors.New("短剧第1集未上架,不能上架整部短剧")
}
if first.Status != 1 {
return errors.New("短剧第1集仍在转码,不能上架整部短剧")
}
if strings.TrimSpace(first.VideoUrl) == "" && strings.TrimSpace(first.H265Url) == "" {
return errors.New("短剧第1集没有播放资源,不能上架整部短剧")
}
return nil
}
func (p *WebUpdateReq) validateDramaUpdate(current mediamod.Media) error {
if current.MediaType != mediamod.MediaTypeDrama {
return nil
}
next := p.mergedDramaMedia(current)
if err := validateDramaMediaFields(next, false); err != nil {
return err
}
if next.Status != 1 {
return nil
}
first, err := mediacontentmod.GetInfoByCond(bson.M{
"mediaId": current.ID,
"mediaType": mediamod.MediaTypeDrama,
"episodeNumber": 1,
"isDelete": false,
})
if err != nil {
return errors.New("短剧第1集不存在,不能上架整部短剧")
}
return validateDramaFirstEpisodeForPublish(next, first)
}
// 钻石榜
const (
diamondRanking = "DiamondRanking:%v"
)
// Update 更新数据
func (p *WebUpdateReq) Update(manager string) error {
media, err := mediamod.GetInfo(p.ID)
if err != nil {
return err
}
if err = p.validateDramaUpdate(media); err != nil {
return err
}
data := make(map[string]interface{})
if p.MediaSubType != nil {
data["mediaSubType"] = *p.MediaSubType
}
if p.SortCode != nil {
data["sortCode"] = *p.SortCode
}
if p.Status != nil {
data["status"] = *p.Status
if media.Status != 1 && *p.Status == 1 {
data["latestPublishedAt"] = time.Now()
}
}
if p.UpdateStatus != nil {
data["updateStatus"] = *p.UpdateStatus
if *p.UpdateStatus == 0 {
data["updateStatus"] = 2
}
}
if p.Title != nil {
data["title"] = strings.TrimSpace(*p.Title)
}
if p.HorizontalCover != nil {
data["horizontalCover"] = strings.TrimSpace(*p.HorizontalCover)
}
if p.VerticalCover != nil {
data["verticalCover"] = strings.TrimSpace(*p.VerticalCover)
}
if p.Tags != nil {
data["tags"] = *p.Tags
}
if p.SectionSort != nil {
data["sectionSort"] = *p.SectionSort
}
if p.Summary != nil {
data["summary"] = *p.Summary
}
if p.TotalEpisode != nil {
data["totalEpisode"] = *p.TotalEpisode
}
if p.Kind != nil {
data["kind"] = *p.Kind
}
if p.PermissionIconHide != nil {
data["permissionIconHide"] = *p.PermissionIconHide
}
if p.Style != nil {
data["style"] = *p.Style
}
if p.Permission != nil {
data["permission"] = *p.Permission
}
if p.Price != nil {
data["price"] = *p.Price
}
if p.Direction != nil {
data["direction"] = *p.Direction
}
if p.SellType != nil {
data["sellType"] = *p.SellType
}
if p.FreeTime != nil {
data["freeTime"] = *p.FreeTime
}
if p.FreeEpisode != nil {
data["freeEpisode"] = *p.FreeEpisode
}
if p.Choice != nil {
data["choice"] = *p.Choice
}
if p.ChoiceSort != nil {
data["choiceSort"] = *p.ChoiceSort
}
if p.CountComment != nil {
data["countComment"] = *p.CountComment
}
if p.CountPurchases != nil {
data["countPurchases"] = *p.CountPurchases
}
if p.CountBrowse != nil {
data["countBrowse"] = *p.CountBrowse
}
if p.CountCollect != nil {
data["countCollect"] = *p.CountCollect
}
if p.CountLike != nil {
data["countLike"] = *p.CountLike
}
if p.CountView != nil {
data["countView"] = *p.CountView
}
if p.Number != nil {
data["number"] = *p.Number
}
if p.IsRemove != nil {
data["mId"] = primitive.ObjectID{}
data["sId"] = primitive.ObjectID{}
}
data["updatedAct"] = manager
data["updateTime"] = time.Now()
if _, err = mediamod.UpdateByID(nil, p.ID, data); err != nil {
return err
}
if p.Status != nil && *p.Status != 1 {
// 将它从排行榜上移除
topasist.Remove(topser.TypeMedia(media.MediaType), media.ID.Hex())
key := fmt.Sprintf(diamondRanking, media.MediaType)
redis.Handler.ZRem(key, media.ID.Hex())
}
return nil
}
type WebBatchUpdateReq struct {
IDS []string `json:"ids" binding:"required"` // IDS
SID *primitive.ObjectID `json:"sId" bson:"sId,omitempty"` // 专题ID
IsActive *bool `json:"isActive" bson:"isActive,omitempty"` // 上下架
Price *int64 `json:"price" bson:"price"` // 价格
Permission *int `json:"permission" bson:"permission"` // 收听权限 0:会员 1:金币购买 2:免费
Tags *[]primitive.ObjectID `json:"tags"` // 标签
FreeEpisode *int `json:"freeEpisode" form:"freeEpisode"` // 免费集数(前几集免费)
}
func (p *WebBatchUpdateReq) Filter() []primitive.ObjectID {
ids, _ := p.ValidatedIDs()
return ids
}
func (p *WebBatchUpdateReq) ValidatedIDs() ([]primitive.ObjectID, error) {
if len(p.IDS) == 0 || len(p.IDS) > 100 {
return nil, errors.New("ids数量必须在1到100之间")
}
ids := make([]primitive.ObjectID, 0, len(p.IDS))
seen := make(map[primitive.ObjectID]struct{}, len(p.IDS))
for _, raw := range p.IDS {
id, err := primitive.ObjectIDFromHex(raw)
if err != nil || id.IsZero() {
return nil, errors.New("ids包含无效的媒体ID")
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
ids = append(ids, id)
}
return ids, nil
}
func loadBatchMedia(ids []primitive.ObjectID) ([]*mediamod.Media, error) {
list, err := mediamod.QueryAllList(bson.M{
"_id": bson.M{"$in": ids},
"isDelete": false,
}, options.Find().SetLimit(int64(len(ids))))
if err != nil {
return nil, err
}
if len(list) != len(ids) {
return nil, errors.New("部分媒体不存在或已删除")
}
return list, nil
}
func validateDramaBatchPublish(medias []*mediamod.Media) error {
dramaIDs := make([]primitive.ObjectID, 0, len(medias))
dramaByID := make(map[primitive.ObjectID]mediamod.Media)
for _, media := range medias {
if media.MediaType != mediamod.MediaTypeDrama {
continue
}
dramaIDs = append(dramaIDs, media.ID)
dramaByID[media.ID] = *media
}
if len(dramaIDs) == 0 {
return nil
}
firstList, err := mediacontentmod.QueryAllList(bson.M{
"mediaId": bson.M{"$in": dramaIDs},
"mediaType": mediamod.MediaTypeDrama,
"episodeNumber": 1,
"isDelete": false,
}, options.Find().SetLimit(int64(len(dramaIDs))))
if err != nil {
return err
}
firstByMediaID := make(map[primitive.ObjectID]mediacontentmod.MediaContent, len(firstList))
for _, first := range firstList {
firstByMediaID[first.MediaID] = *first
}
for _, id := range dramaIDs {
media := dramaByID[id]
media.Status = 1
if err := validateDramaMediaForPublish(media, firstByMediaID[id]); err != nil {
return fmt.Errorf("短剧%s%w", media.Title, err)
}
}
return nil
}
func (p *WebBatchUpdateReq) validateDramaBatchFields(medias []*mediamod.Media) error {
if p.Price == nil && p.Permission == nil && p.FreeEpisode == nil {
return nil
}
for _, media := range medias {
if media.MediaType == mediamod.MediaTypeDrama {
return errors.New("短剧价格、权限和免费集数必须在剧集管理中配置")
}
}
return nil
}
// Update 更新数据
func (p *WebBatchUpdateReq) Update(manager string) error {
ids, err := p.ValidatedIDs()
if err != nil {
return err
}
medias, err := loadBatchMedia(ids)
if err != nil {
return err
}
if err = p.validateDramaBatchFields(medias); err != nil {
return err
}
if p.IsActive != nil && *p.IsActive {
if err = validateDramaBatchPublish(medias); err != nil {
return err
}
}
nonDramaIDs := make([]primitive.ObjectID, 0, len(medias))
for _, media := range medias {
if media.MediaType != mediamod.MediaTypeDrama {
nonDramaIDs = append(nonDramaIDs, media.ID)
}
}
data := make(map[string]interface{})
if p.SID != nil {
section, err := modulesectionmod.GetBySectionByID(*p.SID)
if err != nil {
return err
}
if section.ID.IsZero() {
return errors.New("section is null")
}
moduleConf, err := moduleconfmod.FindOneById(section.SubModuleID)
if err != nil {
return err
}
if moduleConf.ID.IsZero() {
return errors.New("module conf is null")
}
data["mId"] = section.SubModuleID
data["sId"] = section.ID
data["sectionName"] = section.SectionName
data["moduleName"] = moduleConf.ModuleName
}
if p.Price != nil {
data["price"] = *p.Price
}
if p.Permission != nil {
data["permission"] = *p.Permission
}
if p.Tags != nil {
data["tags"] = *p.Tags
}
if p.FreeEpisode != nil {
data["freeEpisode"] = *p.FreeEpisode
}
data["updatedAct"] = manager
data["updateTime"] = time.Now()
err = webg.VideoDB.Trans(func(t *db.MongoTool) error {
if p.IsActive != nil {
// 短剧的剧集上下架由 media_content/batch/update 单独控制;
// 其他媒体保持历史整部上下架行为。
if len(nonDramaIDs) > 0 {
filter := bson.M{"mediaId": bson.M{"$in": nonDramaIDs}}
updateData := make(map[string]interface{})
updateData["isActive"] = *p.IsActive
updateData["updatedAct"] = manager
updateData["updateTime"] = time.Now()
_, err := mediacontentmod.UpdateByCond(t, filter, updateData)
if err != nil {
log.Error(fmt.Sprintf("mediacontentmod UpdateByCond error:%+v:", err))
return err
}
}
if *p.IsActive {
data["status"] = 1
data["latestPublishedAt"] = time.Now()
} else {
data["status"] = 0
}
}
// 更新数据
if _, err := mediamod.UpdateByIDS(t, ids, data); err != nil {
log.Error(fmt.Sprintf("mediamod UpdateByIDS error:%+v:", err))
return err
}
return nil
})
if err != nil {
log.Error(fmt.Sprintf("Media update Trans fail error:%+v:", err))
return err
}
if p.IsActive != nil && *p.IsActive == false {
// 批量下架
common.Go(func() {
for _, id := range p.IDS {
objId, _ := primitive.ObjectIDFromHex(id)
media, err := mediamod.GetInfo(objId)
if err != nil {
continue
}
// 将它从排行榜上移除
topasist.Remove(topser.TypeMedia(media.MediaType), media.ID.Hex())
key := fmt.Sprintf(diamondRanking, media.MediaType)
redis.Handler.ZRem(key, media.ID.Hex())
}
})
}
return nil
}
type WebDeleteReq struct {
ID primitive.ObjectID `json:"id" binding:"required"`
}
// Delete 删除数据
func (p *WebDeleteReq) Delete() error {
media, err := mediamod.GetInfo(p.ID)
if err != nil {
return err
}
err = webg.VideoDB.Trans(func(t *db.MongoTool) error {
if err := mediamod.DeleteByID(t, p.ID); err != nil {
return err
}
if media.MediaType == mediamod.MediaTypeDrama {
return mediacontentmod.DeleteByMediaId(t, p.ID)
}
return nil
})
if err != nil {
return err
}
// 将它从排行榜上移除
topasist.Remove(topser.TypeMedia(media.MediaType), media.ID.Hex())
key := fmt.Sprintf(diamondRanking, media.MediaType)
redis.Handler.ZRem(key, media.ID.Hex())
return nil
}
type WebBatchDeleteReq struct {
Ids []primitive.ObjectID `json:"ids" binding:"required"`
}
// Delete 删除数据
func (p *WebBatchDeleteReq) Delete() error {
if len(p.Ids) == 0 || len(p.Ids) > 100 {
return errors.New("ids数量必须在1到100之间")
}
medias := make([]mediamod.Media, 0, len(p.Ids))
for _, id := range p.Ids {
media, err := mediamod.GetInfo(id)
if err != nil {
return err
}
medias = append(medias, media)
}
err := webg.VideoDB.Trans(func(t *db.MongoTool) error {
for _, media := range medias {
if err := mediamod.DeleteByID(t, media.ID); err != nil {
return err
}
if media.MediaType == mediamod.MediaTypeDrama {
if err := mediacontentmod.DeleteByMediaId(t, media.ID); err != nil {
return err
}
}
}
return nil
})
if err != nil {
return err
}
for _, media := range medias {
// 将它从排行榜上移除
topasist.Remove(topser.TypeMedia(media.MediaType), media.ID.Hex())
key := fmt.Sprintf(diamondRanking, media.MediaType)
redis.Handler.ZRem(key, media.ID.Hex())
}
return nil
}