init: web_hgdjc_server

This commit is contained in:
2026-11-07 10:46:18 +08:00
commit b2ff6ac8ff
707 changed files with 147645 additions and 0 deletions
+113
View File
@@ -0,0 +1,113 @@
package srv_videoinfo
import (
"time"
"videoweb/pub/models/videoinformationmod"
"videoweb/pub/pubdef"
)
type VideoInfoReq struct {
Id *int64 `json:"id"` // Id
Title *string `json:"title"` // 标题
CreatorId *int64 `json:"creatorId"` // 作者Id
Status *videoinformationmod.InformationSatus `json:"status"` // 帖子上架状态
CategoryId *int64 `json:"categoryId"` // 分类Id
TopicId *int64 `json:"topicId"` // 专题Id
Tag *int64 `json:"tag" ` // 标签
IsHot *bool `json:"isHot"` // 是否热门
MediaType videoinformationmod.MediaType `json:"mediaType"` // 资源类型
CreatorName *string `json:"creatorName"` // 作者名称
Keyworld *string `json:"keyworld"` // 关键字
VideoTaskId *string `json:"videoTaskId"` // 视频任务id
VideoFileId *string `json:"videoFileId"` // 切片id(切片列表的ID)
IsTop *bool `json:"isTop" bson:"isTop"` // 是否置顶
IsH265 *bool `json:"isH265"` // 是否支持 H265
Sort int64 `json:"sort"` // 排序方式 0=创建时间 1=上架时间 2=更新时间 3=排序值 4=播放次数
pubdef.Page
StartTime *string `json:"startTime"` // 创建时间-开始
EndTime *string `json:"endTime"` // 创建时间-截止
OptionAccount *string `json:"optionAccount"` // 最后操作人
}
type VideoInfoResp struct {
Total int64 `json:"total"`
List videoinformationmod.VideoInformationList `json:"list"`
}
type AddVideoReq struct {
FileIds []string `json:"fileId"` // 资源id
}
type VideoInfoUpdate struct {
Title *string `json:"title,omitempty" bson:"title,omitempty"` // 帖子标题
Slug *string `json:"slug,omitempty" bson:"slug,omitempty"` // slug
AddedTime *time.Time `json:"addedTime,omitempty" bson:"addedTime,omitempty"`
CreatorId *int64 `json:"creatorId,omitempty" bson:"creatorId,omitempty"` // 作者Id
Keyworld *[]string `bson:"keyworld,omitempty" json:"keyworld"` //
MediaType *videoinformationmod.MediaType `json:"mediaType,omitempty" bson:"mediaType,omitempty"` // 资源类型
FirstTag *string `bson:"firstTag,omitempty" json:"firstTag,omitempty"` // 一级标签
SecondTags *[]string `bson:"secondTags,omitempty" json:"secondTags,omitempty"` // 二级标签标签
CoverImg *string `bson:"coverImg,omitempty" json:"coverImg,omitempty"` // 封面
UnencryptedImage *string `bson:"unencryptedImage,omitempty" json:"unencryptedImage"` // 不加密封面
Desc *string `bson:"desc,omitempty" json:"desc,omitempty"` // 详细描述
Remark *string `bson:"remark,omitempty" json:"remark,omitempty"` // 备注
Nodes *[]videoinformationmod.Node `json:"nodes,omitempty" bson:"nodes,omitempty"` // 节点
Status *videoinformationmod.InformationSatus `json:"status,omitempty" bson:"status,omitempty"` // 帖子上架状态
Sort *int64 `json:"sort,omitempty" bson:"sort,omitempty"` // 排序
Cover *string `json:"cover,omitempty" bson:"cover,omitempty"` // 视频封面
Height *int64 `json:"height,omitempty" bson:"height,omitempty"` // 分辨率高(视频才会存在)
Width *int64 `json:"width,omitempty" bson:"width,omitempty"` // 分辨率宽(视频才会存在)
VideoTime *int32 `bson:"videoTime,omitempty" json:"videoTime,omitempty"` // 影片长度
CategoryIds []int64 `json:"categoryIds" bson:"categoryIds"` // 分类ID集合,可为空;不传或空数组表示清空
TopicIds *[]int64 `json:"topicIds,omitempty" bson:"topicIds,omitempty"` // 专题ID
Tags *[]int64 `json:"tags,omitempty" bson:"tags,omitempty"` // 标签
IsHot *bool `json:"isHot,omitempty" bson:"isHot,omitempty"` // 是否热门
UpdatedAt *time.Time `json:"updatedAt,omitempty" bson:"updatedAt,omitempty"` // 更新时间
CreatorName *string `bson:"creatorName,omitempty"` // 作者名
IsTop *bool `json:"isTop,omitempty" bson:"isTop,omitempty"` // 是否置顶
Video *videoinformationmod.VideoInfo `bson:"video,omitempty"` // 视频
IsReIndexNow *bool `json:"isReIndexNow" bson:"isReIndexNow"` // 是否重新提交索引
IndexNow *int64 `bson:"indexNow,omitempty"`
Content *string `json:"content,omitempty" bson:"content,omitempty"`
SecondUpdatedAt *time.Time `json:"secondUpdatedAt,omitempty" bson:"secondUpdatedAt,omitempty"` // 二次修改时间
}
type VideoInfoUpdateReq struct {
Id int64 `json:"id" binding:"required"`
Param VideoInfoUpdate `json:"param" bson:"param"` // appId,唯一id
}
type VideoInfoDelReq struct {
Id int64 `json:"id" binding:"required"`
}
type AddVideoCollectionReq struct {
Title string `json:"title,omitempty" bson:"title,omitempty"` // 帖子标题
Slug string `json:"slug,omitempty" bson:"slug,omitempty"` // slug
Keyworld []string `bson:"keyworld,omitempty" json:"keyworld,omitempty"` // 关键字
FirstTag string `bson:"firstTag,omitempty" json:"firstTag,omitempty"` // 一级标签
SecondTags []string `bson:"secondTags,omitempty" json:"secondTags,omitempty"` // 二级标签标签
CoverImg string `bson:"coverImg,omitempty" json:"coverImg,omitempty"` // 封面
Desc string `bson:"desc,omitempty" json:"desc,omitempty"` // 详细描述
Remark string `bson:"remark,omitempty" json:"remark,omitempty"` // 备注
Tags []int64 `json:"tags,omitempty" bson:"tags,omitempty"` // 标签
CategoryIds []int64 `json:"categoryIds" bson:"categoryIds"` // 分类ID集合,可为空;不传或空数组表示清空
}
type SetFakeWatchTimesReq struct {
Ids []int64 `json:"ids"` // 视频ID集合,支持单个或批量
WatchTimesMin int64 `json:"watchTimesMin"` // 假观看次数随机最小值
WatchTimesMax int64 `json:"watchTimesMax"` // 假观看次数随机最大值
}
type SetFakeCountResp struct {
MatchedCount int64 `json:"matchedCount"` // 匹配到的视频数量
ModifiedCount int64 `json:"modifiedCount"` // 实际修改的视频数量
}
// SetVideoCategoryReq 单/批量设置视频分类请求
type SetVideoCategoryReq struct {
Ids []int64 `json:"ids"` // 视频ID集合,传1个表示单条设置,传多个表示批量设置
CategoryIds []int64 `json:"categoryIds"` // 分类ID集合,可为空;不传或空数组表示清空
}
+160
View File
@@ -0,0 +1,160 @@
package srv_videoinfo
import (
"context"
"fmt"
"strings"
"videoweb/pub/common/laosiji"
"videoweb/pub/common/log"
"videoweb/pub/common/srvdef"
"videoweb/pub/common/utils"
"videoweb/pub/models/videoinformationmod"
"videoweb/web/webg"
"go.mongodb.org/mongo-driver/bson"
)
// TranscodeVideo 手动提交主视频 H265 转码。
func TranscodeVideo(ctx context.Context, videoId int64) *srvdef.SrvErr {
info, serr := videoinformationmod.FindOneVideoInformation(ctx, bson.M{
videoinformationmod.FN.VideoInformation.Id: videoId,
})
if serr != nil {
return serr
}
src := strings.TrimSpace(info.Video.Url)
if src == "" {
return srvdef.New(srvdef.Err, "", "该视频没有可转码的源地址")
}
if laosiji.LaosijiClient == nil || laosiji.LaosijiDomains == nil {
return srvdef.New(srvdef.Err, "", "转码服务未就绪")
}
if info.HevcStatus == videoinformationmod.HevcStatusPending {
return srvdef.New(srvdef.Err, "", "转码请求已提交,正在转码中,请勿重复提交")
}
if err := SubmitTranscodeVideo(ctx, videoId, src, info.Video.FileId); err != nil {
return srvdef.New(srvdef.Err, "", "提交转码失败:"+err.Error())
}
return nil
}
func buildTranscodeSourceURL(videoUrl string) string {
name := strings.TrimSpace(videoUrl)
if name == "" {
return ""
}
if !strings.Contains(name, ".m3u8") {
name += ".m3u8"
}
host := strings.TrimSpace(webg.Conf.WebM3u8Host)
if host == "" {
return ""
}
return utils.JoinUrlPath(host, "/api/web/m3u8/"+name)
}
// SubmitTranscodeVideo 提交主视频转码任务。
func SubmitTranscodeVideo(ctx context.Context, videoId int64, src, fileId string) error {
fileURL := buildTranscodeSourceURL(src)
if fileURL == "" {
return fmt.Errorf("empty transcode source url, videoUrl:%s host:%s", src, webg.Conf.WebM3u8Host)
}
sd, err := laosiji.LaosijiDomains.Get(ctx)
if err != nil {
return err
}
result, err := laosiji.LaosijiClient.SubmitTranscoding(ctx, fileURL, fileId, *sd, videoId)
if err != nil {
return err
}
return applyTranscodeResult(ctx, videoId, result)
}
// RefreshTranscodeVideo 查询转码任务状态并回写。
func RefreshTranscodeVideo(ctx context.Context, videoId int64, src, fileId string) error {
fileURL := buildTranscodeSourceURL(src)
if fileURL == "" {
return fmt.Errorf("empty transcode source url, videoUrl:%s host:%s", src, webg.Conf.WebM3u8Host)
}
sd, err := laosiji.LaosijiDomains.Get(ctx)
if err != nil {
return err
}
result, found, err := laosiji.LaosijiClient.QueryTranscoding(ctx, fileURL, fileId, *sd)
if err != nil {
return err
}
if !found {
return nil
}
return applyTranscodeResult(ctx, videoId, result)
}
func applyTranscodeResult(ctx context.Context, videoId int64, result laosiji.TranscodeResult) error {
log.Infof("==applyTranscodeResult===> result", log.Any("videoId", videoId), log.Any("result", result))
var update bson.M
switch {
case result.Done:
meta := toVideoMetaInfo(result.HevcMeta)
if meta == nil {
meta = &videoinformationmod.VideoMetaInfo{}
}
h265Url := normalizeH265UrlForSave(result.HevcURL)
meta.M3u8URL = h265Url
set := bson.M{
videoinformationmod.FN.VideoInformation.HevcStatus: videoinformationmod.HevcStatusSuccess,
videoinformationmod.FN.VideoInformation.HevcMetaInfo: meta,
videoinformationmod.FN.VideoInformation.Video.H265Url: h265Url,
}
if result.PreviewVtt != "" {
set[videoinformationmod.FN.VideoInformation.PreviewVtt] = result.PreviewVtt
}
if result.AudioFile != "" {
set[videoinformationmod.FN.VideoInformation.AudioFile] = result.AudioFile
}
if result.SmartCover != "" {
set[videoinformationmod.FN.VideoInformation.SmartCover] = result.SmartCover
}
update = bson.M{"$set": set}
case result.Failed:
update = bson.M{
"$set": bson.M{videoinformationmod.FN.VideoInformation.HevcStatus: videoinformationmod.HevcStatusFailed},
"$inc": bson.M{videoinformationmod.FN.VideoInformation.HevcFailCount: 1},
}
default:
update = bson.M{"$set": bson.M{videoinformationmod.FN.VideoInformation.HevcStatus: videoinformationmod.HevcStatusPending}}
}
if serr := videoinformationmod.UpdateOneVideoInformation(ctx, bson.M{
videoinformationmod.FN.VideoInformation.Id: videoId,
}, update); serr != nil {
return fmt.Errorf("update video transcode status: %v", serr)
}
return nil
}
func normalizeH265UrlForSave(url string) string {
url = strings.TrimSpace(url)
return strings.TrimPrefix(url, "/")
}
func toVideoMetaInfo(m *laosiji.MetaInfo) *videoinformationmod.VideoMetaInfo {
if m == nil {
return nil
}
return &videoinformationmod.VideoMetaInfo{
Codec: m.Codec,
Profile: m.Profile,
Width: m.Width,
Height: m.Height,
Fps: m.Fps,
VideoBitrate: m.VideoBitrate,
PixFmt: m.PixFmt,
AudioCodec: m.AudioCodec,
AudioBitrate: m.AudioBitrate,
AudioSampleRate: m.AudioSampleRate,
AudioChannels: m.AudioChannels,
Bitrate: m.Bitrate,
Size: m.Size,
Duration: m.Duration,
}
}
+232
View File
@@ -0,0 +1,232 @@
package srv_videoinfo
import (
"context"
"strings"
"time"
"videoweb/pub/common/log"
"videoweb/pub/common/srvdef"
"videoweb/pub/models/playletchaptermod"
"videoweb/pub/models/videocreatermod"
"videoweb/pub/models/videoinformationmod"
"videoweb/pub/mongos"
"videoweb/pub/storage/proxy"
"videoweb/web/service/srv_videoslug"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
)
func IsPlayletMedia(ctx context.Context, media proxy.MediaResp) bool {
return playletchaptermod.IsLinkedMedia(ctx, media.IsPlaylet, media.PlayletKeys()...)
}
func SyncOneVideo(ctx context.Context, media proxy.MediaResp) *srvdef.SrvErr {
if IsPlayletMedia(ctx, media) {
log.Info("VideoSync skip playlet media", log.Any("mediaId", media.Id.Hex()), log.Any("mediaSourceId", media.MediaSourceId.Hex()))
return nil
}
filter := bson.M{
"$or": []bson.M{
{videoinformationmod.FN.VideoInformation.Video.FileId: media.Id.Hex()},
{videoinformationmod.FN.VideoInformation.SyncId: media.Id.Hex()},
},
}
videoInfo, serr := videoinformationmod.FindOneVideoInformation(ctx, filter)
if serr != nil {
// 新加入的视频
if serr.Code == srvdef.ErrNoData {
videoInfo = media.ToVideoInfo()
if videoInfo == nil {
log.Error("VideoSync FindOneVideoInformation", log.Any("MediaId", media.Id))
return srvdef.New(srvdef.ErrNoData, "videoInfo is nil")
}
videoInfo.IndexNow = 0
if len(media.Publish) > 0 {
creater, serr := videocreatermod.FindOneVideoCreaterInfo(ctx, bson.M{
videocreatermod.FN.VideoCreaterInfo.Name: media.Publish,
})
if serr != nil && serr.Code != srvdef.ErrNoData {
log.Error("VideoSync.FindOneVideoCreaterInfo", log.Any("media", media), log.E(serr))
return serr
}
if creater == nil {
createrId := mongos.Lid.VideoCreator.Get()
creater = &videocreatermod.VideoCreaterInfo{
Id: createrId,
Name: media.Publish,
Status: true,
Sort: 0,
UpdatedAt: time.Now(),
CreatedAt: time.Now(),
}
if slugErr := srv_videoslug.EnsureCreater(ctx, creater, 0); slugErr != nil {
return slugErr
}
serr = videocreatermod.InsertOneVideoCreaterInfo(ctx, creater)
if serr != nil {
log.Error("VideoSync.InsertOneVideoCreaterInfo", log.Any("media", media), log.E(serr))
return serr
}
}
videoInfo.CreatorId = creater.Id
videoInfo.CreatorName = creater.Name
}
if slugErr := srv_videoslug.EnsureVideo(ctx, videoInfo, 0); slugErr != nil {
return slugErr
}
serr = videoinformationmod.InsertOneVideoInformation(ctx, videoInfo)
if serr != nil {
log.Error("VideoSync InsertOneVideoInformation", log.Any("videoInfo", videoInfo), log.E(serr))
return serr
}
} else {
log.Error("VideoSync FindOneVideoInformation", log.Any("filter", filter), log.E(serr))
return serr
}
} else {
// 更新资源
nodes := make([]videoinformationmod.Node, 0)
if videoInfo != nil {
// 更新节点里面图片水印 以本地视频列表的节点为主
for _, node := range videoInfo.Nodes {
isFind := false
if len(node.NodeId) > 0 {
for _, v := range media.Nodes {
if node.NodeId == v.NodeId {
isFind = true
node.Images = v.Images
nodes = append(nodes, node)
break
}
}
}
if !isFind {
nodes = append(nodes, node)
}
}
} else {
nodes = media.Nodes
}
setDoc := bson.M{
videoinformationmod.FN.VideoInformation.Nodes: nodes,
videoinformationmod.FN.VideoInformation.Keyworld: media.Keyworld,
videoinformationmod.FN.VideoInformation.FirstTag: media.FirstTag,
videoinformationmod.FN.VideoInformation.SecondTags: media.SecondTags,
videoinformationmod.FN.VideoInformation.CoverImg: media.CoverImg,
videoinformationmod.FN.VideoInformation.UnencryptedImage: media.UnencryptedImage,
}
if len(media.FieldNameFs) > 0 {
video := videoinformationmod.VideoInfo{
TaskId: media.TaskID,
FileId: media.Id.Hex(),
Url: media.FieldNameFs,
Cover: media.CoverImg,
Height: media.Height,
Width: media.Width,
VideoTime: media.VideoTime,
UnencryptedPreMp4: media.UnencryptedPreMp4,
PreMp4FileNameFs: media.PreMp4FileNameFs,
}
if !strings.HasSuffix(video.Url, ".m3u8") {
video.Url += ".m3u8"
}
setDoc[videoinformationmod.FN.VideoInformation.Video.TaskId] = video.TaskId
setDoc[videoinformationmod.FN.VideoInformation.Video.FileId] = video.FileId
setDoc[videoinformationmod.FN.VideoInformation.Video.Url] = video.Url
setDoc[videoinformationmod.FN.VideoInformation.Video.Cover] = video.Cover
setDoc[videoinformationmod.FN.VideoInformation.Video.Height] = video.Height
setDoc[videoinformationmod.FN.VideoInformation.Video.Width] = video.Width
setDoc[videoinformationmod.FN.VideoInformation.Video.VideoTime] = video.VideoTime
setDoc[videoinformationmod.FN.VideoInformation.Video.UnencryptedPreMp4] = video.UnencryptedPreMp4
}
if strings.TrimSpace(videoInfo.Slug) == "" {
if slugErr := srv_videoslug.EnsureVideo(ctx, videoInfo, videoInfo.Id); slugErr == nil && strings.TrimSpace(videoInfo.Slug) != "" {
setDoc[videoinformationmod.FN.VideoInformation.Slug] = videoInfo.Slug
}
}
serr = videoinformationmod.UpdateOneVideoInformation(ctx, bson.M{videoinformationmod.FN.VideoInformation.Id: videoInfo.Id}, bson.M{"$set": setDoc})
if serr != nil {
return serr
}
}
_ = syncNodeVideo(ctx, media)
return nil
}
// SyncUnencryptedOne 仅补齐单条资源的 UnencryptedImage / UnencryptedPreMp4 字段。
// 源数据对应字段为空时跳过该字段(避免覆盖本地已有值);本地无对应资源时直接返回。
// 幂等:可重复执行,用于历史数据补齐。
func SyncUnencryptedOne(ctx context.Context, media proxy.MediaResp) *srvdef.SrvErr {
if media.UnencryptedImage == "" && media.UnencryptedPreMp4 == "" {
return nil
}
filter := bson.M{videoinformationmod.FN.VideoInformation.Video.FileId: media.Id.Hex()}
videoInfo, serr := videoinformationmod.FindOneVideoInformation(ctx, filter)
if serr != nil {
return serr
}
setDoc := bson.M{}
if media.UnencryptedImage != "" {
setDoc[videoinformationmod.FN.VideoInformation.UnencryptedImage] = media.UnencryptedImage
}
// 仅当源端有主视频文件时,才补 Video.UnencryptedPreMp4(避免向空的 Video 子结构写字段)
if media.UnencryptedPreMp4 != "" && len(media.FieldNameFs) > 0 {
setDoc[videoinformationmod.FN.VideoInformation.Video.UnencryptedPreMp4] = media.UnencryptedPreMp4
}
if len(setDoc) == 0 {
return nil
}
return videoinformationmod.UpdateOneVideoInformation(ctx,
bson.M{videoinformationmod.FN.VideoInformation.Id: videoInfo.Id},
bson.M{"$set": setDoc})
}
// syncNodeVideo 同步节点里的视频
func syncNodeVideo(ctx context.Context, media proxy.MediaResp) *srvdef.SrvErr {
filter := bson.M{"nodes.nodeId": media.Id.Hex()}
opts := &options.FindOptions{}
opts.SetLimit(100)
videoInfo, serr := videoinformationmod.FindManyVideoInformations(ctx, filter, opts)
if serr != nil {
log.Warn("syncNodeVideo", log.Any("filter", filter), log.E(serr))
return serr
}
if !strings.HasSuffix(media.FieldNameFs, ".m3u8") {
media.FieldNameFs += ".m3u8"
}
if len(videoInfo) > 0 {
// 更新视频路径
for _, video := range videoInfo {
if video.Nodes != nil && len(video.Nodes) > 0 {
isFind := false
for i, node := range video.Nodes {
if node.Videos != nil && len(node.Videos) > 0 {
for j, v := range node.Videos {
if v.FileId == media.Id.Hex() {
node.Videos[j].Url = media.FieldNameFs
isFind = true
break
}
}
}
if isFind {
video.Nodes[i] = node
break
}
}
if isFind {
serr = videoinformationmod.UpdateOneVideoInformation(ctx, bson.M{videoinformationmod.FN.VideoInformation.Id: video.Id}, bson.M{"$set": bson.M{
videoinformationmod.FN.VideoInformation.UpdatedAt: time.Now(),
videoinformationmod.FN.VideoInformation.Nodes: video.Nodes,
}})
if serr != nil {
log.Warn("syncNodeVideo UpdateOneVideoInformation failed", log.Any("filter", filter), log.E(serr))
continue
}
}
}
}
}
return nil
}
+450
View File
@@ -0,0 +1,450 @@
package srv_videoinfo
import (
"context"
"math/rand"
"net/http"
"time"
"videoweb/pub/common/godef"
"videoweb/pub/common/log"
"videoweb/pub/common/playletcache"
"videoweb/pub/common/srvdef"
"videoweb/pub/common/useragent"
"videoweb/pub/common/utils"
"videoweb/pub/models/videocreatermod"
"videoweb/pub/models/videoinformationmod"
"videoweb/pub/mongos"
"videoweb/pub/seo/sitemapdirty"
"videoweb/pub/storage"
"videoweb/pub/storage/proxy"
"videoweb/web/service/srv_report"
"videoweb/web/service/srv_search"
"videoweb/web/service/srv_seopush"
"videoweb/web/service/srv_videoslug"
"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"
)
// GetVideoInfos 按照参数搜索APP
func GetVideoInfos(ctx context.Context, param *VideoInfoReq) (*VideoInfoResp, *srvdef.SrvErr) {
filter := bson.M{}
filter[videoinformationmod.FN.VideoInformation.MediaType] = param.MediaType
if param.Id != nil && *param.Id > 0 {
filter[videoinformationmod.FN.VideoInformation.Id] = *param.Id
}
if param.Title != nil && len(*param.Title) > 0 {
filter[videoinformationmod.FN.VideoInformation.Title] = bson.M{"$regex": *param.Title}
}
if param.CreatorId != nil && *param.CreatorId > 0 {
filter[videoinformationmod.FN.VideoInformation.CreatorId] = *param.CreatorId
}
if param.TopicId != nil && *param.TopicId > 0 {
filter[videoinformationmod.FN.VideoInformation.TopicIds] = bson.M{"$in": []int64{*param.TopicId}}
}
if param.Status != nil {
filter[videoinformationmod.FN.VideoInformation.Status] = *param.Status
}
if param.CategoryId != nil && *param.CategoryId > 0 {
filter[videoinformationmod.FN.VideoInformation.CategoryIds] = bson.M{"$in": []int64{*param.CategoryId}}
}
if param.Tag != nil && *param.Tag > 0 {
filter[videoinformationmod.FN.VideoInformation.Tags] = bson.M{"$in": []int64{*param.Tag}}
}
if param.IsHot != nil {
filter[videoinformationmod.FN.VideoInformation.IsHot] = *param.IsHot
}
if param.CreatorName != nil && len(*param.CreatorName) > 0 {
filter[videoinformationmod.FN.VideoInformation.CreatorName] = bson.M{"$regex": *param.CreatorName}
}
if param.Keyworld != nil && len(*param.Keyworld) > 0 {
filter[videoinformationmod.FN.VideoInformation.Keyworld] = bson.M{"$in": []string{*param.Keyworld}}
}
if param.VideoTaskId != nil && len(*param.VideoTaskId) > 0 {
filter[videoinformationmod.FN.VideoInformation.Video.TaskId] = *param.VideoTaskId
}
if param.VideoFileId != nil && len(*param.VideoFileId) > 0 {
filter[videoinformationmod.FN.VideoInformation.Video.FileId] = *param.VideoFileId
}
if param.IsTop != nil {
filter[videoinformationmod.FN.VideoInformation.IsTop] = *param.IsTop
}
if param.IsH265 != nil {
if *param.IsH265 {
filter[videoinformationmod.FN.VideoInformation.HevcStatus] = videoinformationmod.HevcStatusSuccess
} else {
filter[videoinformationmod.FN.VideoInformation.HevcStatus] = bson.M{"$in": []videoinformationmod.HevcStatus{
videoinformationmod.HevcStatusNone,
videoinformationmod.HevcStatusPending,
videoinformationmod.HevcStatusFailed,
}}
}
}
if param.StartTime != nil && param.EndTime != nil {
start := utils.GetDayBeginByTime(*param.StartTime)
end := utils.GetDayEndByTime(*param.EndTime)
if end.Unix() > start.Unix() {
filter[videoinformationmod.FN.VideoInformation.CreatedAt] = bson.M{
"$gte": start, "$lte": end,
}
}
}
if param.OptionAccount != nil {
filter[videoinformationmod.FN.VideoInformation.OptionAccount] = *param.OptionAccount
}
opt := &options.FindOptions{}
resp := &VideoInfoResp{
Total: 0,
List: make(videoinformationmod.VideoInformationList, 0),
}
opt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.Id: -1})
switch param.Sort {
case 0:
opt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.CreatedAt: -1})
case 1:
opt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.AddedTime: -1})
case 2:
opt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.UpdatedAt: -1})
case 3:
opt.SetSort(bson.D{
{videoinformationmod.FN.VideoInformation.Sort, -1},
{videoinformationmod.FN.VideoInformation.AddedTime, -1},
})
case 4:
opt.SetSort(bson.D{
{videoinformationmod.FN.VideoInformation.Real.WatchTimes, -1},
{videoinformationmod.FN.VideoInformation.AddedTime, -1},
})
}
opt.SetSkip((param.PageNum - 1) * param.PageSize).SetLimit(param.PageSize)
total, serr := videoinformationmod.CountVideoInformations(ctx, filter)
if serr != nil {
return resp, serr
}
resp.Total = total
if total == 0 {
return resp, nil
}
ads, serr := videoinformationmod.FindManyVideoInformations(ctx, filter, opt)
if serr != nil {
return resp, serr
}
resp.List = ads
return resp, nil
}
// SyncVideoInfo 同步视频
func SyncVideoInfo(ctx context.Context,
req *AddVideoReq) *srvdef.SrvErr {
ids := make([]string, 0)
for _, id := range req.FileIds {
ids = append(ids, id)
}
if len(ids) == 0 {
return nil
}
queryMediaReq := &proxy.MediaQueryesReq{
Ids: ids,
}
resp, err := storage.QueryMedias(ctx, queryMediaReq, "")
if err != nil || resp.Code != http.StatusOK {
if resp != nil {
log.Warn("AddVideoInfo.QueryMedias failed", log.Any("filter", req.FileIds), log.E(err), log.Any("respCode", resp.Code))
} else {
log.Warn("AddVideoInfo.QueryMedias failed", log.Any("filter", req.FileIds), log.E(err))
}
return srvdef.New(srvdef.ErrMediaNotExsit, err.Error(), err.Error())
}
if len(resp.Data) == 0 {
return nil
}
for _, media := range resp.Data {
serr := SyncOneVideo(ctx, media)
if serr != nil {
log.Warn("SyncVideoInfo SYncOneVideo failed", log.Any("filter", req.FileIds), log.E(serr))
continue
}
}
return nil
}
// DelVideoInfo 删除指定APP
func DelVideoInfo(ctx context.Context, id int64) *srvdef.SrvErr {
adFilter := bson.M{videoinformationmod.FN.VideoInformation.Id: id}
info, _ := videoinformationmod.FindOneVideoInformation(ctx, adFilter)
//删除
count, serr := videoinformationmod.DelOneVideoInformation(ctx, adFilter)
if serr != nil {
return serr
}
if count == 0 {
return srvdef.New(srvdef.ErrNoData, "", "")
}
// 同步删除 ES 中的数据,避免搜索到已删除的视频
go srv_search.DelVideoFromES(id)
sitemapdirty.Mark(ctx)
if info != nil && info.Status == videoinformationmod.InformationSatusVerifyOK {
srv_seopush.EnqueueVideoDeleted(ctx, id)
}
return nil
}
// SetVideoFakeWatchTimes 单/批量随机设置视频假观看次数
func SetVideoFakeWatchTimes(ctx context.Context, req *SetFakeWatchTimesReq) (*SetFakeCountResp, *srvdef.SrvErr) {
if req == nil || len(req.Ids) == 0 {
return nil, srvdef.New(srvdef.ErrParamError, "ids不能为空")
}
if req.WatchTimesMin < 0 || req.WatchTimesMax < req.WatchTimesMin {
return nil, srvdef.New(srvdef.ErrParamError, "观看次数区间无效")
}
ids, err := normalizeFakeFlowIds(req.Ids)
if err != nil {
return nil, err
}
return setVideoFakeField(ctx, ids, videoinformationmod.FN.VideoInformation.Fake.WatchTimes, req.WatchTimesMin, req.WatchTimesMax)
}
// setVideoFakeField 使用BulkWrite批量设置假流量字段,避免批量请求逐条访问数据库
func setVideoFakeField(ctx context.Context, ids []int64, field string, min, max int64) (*SetFakeCountResp, *srvdef.SrvErr) {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
docs := make([]mongo.WriteModel, 0, len(ids))
for _, id := range ids {
docs = append(docs, mongo.NewUpdateOneModel().
SetFilter(bson.M{videoinformationmod.FN.VideoInformation.Id: id}).
SetUpdate(bson.M{"$set": bson.M{field: randomFakeFlowValue(r, min, max)}}))
}
result, err := videoinformationmod.BulkWriteVideoInformation(ctx, docs, options.BulkWrite().SetOrdered(false))
if err != nil {
return nil, err
}
if result.MatchedCount == 0 {
return nil, srvdef.New(srvdef.ErrNoData, "", "")
}
return &SetFakeCountResp{MatchedCount: result.MatchedCount, ModifiedCount: result.ModifiedCount}, nil
}
// normalizeFakeFlowIds 校验并去重视频ID,减少无效批量写入
func normalizeFakeFlowIds(rawIds []int64) ([]int64, *srvdef.SrvErr) {
ids := make([]int64, 0, len(rawIds))
seen := make(map[int64]struct{}, len(rawIds))
for _, id := range rawIds {
if id <= 0 {
return nil, srvdef.New(srvdef.ErrParamError, "ids包含无效id")
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
ids = append(ids, id)
}
if len(ids) == 0 {
return nil, srvdef.New(srvdef.ErrParamError, "ids不能为空")
}
return ids, nil
}
func randomFakeFlowValue(r *rand.Rand, min, max int64) int64 {
if min == max {
return min
}
return min + r.Int63n(max-min+1)
}
// UpdateVideoInfo 更新指定APP
func UpdateVideoInfo(ctx context.Context, id, userId int64, param VideoInfoUpdate, ua useragent.UA) *srvdef.SrvErr {
now := time.Now()
info, serr := videoinformationmod.FindOneVideoInformation(ctx, bson.M{videoinformationmod.FN.VideoInformation.Id: id})
if serr != nil {
return serr
}
param.UpdatedAt = &now
param.SecondUpdatedAt = &now
if param.CreatorId != nil {
name := ""
if *param.CreatorId == 0 {
param.CreatorName = &name
} else {
// 查询作者
c, serr := videocreatermod.FindOneVideoCreaterInfo(ctx, bson.M{
videocreatermod.FN.VideoCreaterInfo.Id: *param.CreatorId,
})
if serr != nil {
return serr
}
name = c.Name
param.CreatorName = &name
}
}
if param.Nodes != nil && len(*param.Nodes) > 0 {
nodes := *param.Nodes
for i, node := range nodes {
if len(node.NodeId) == 0 {
node.NodeId = primitive.NewObjectID().Hex()
nodes[i] = node
}
}
param.Nodes = &nodes
}
title := info.Title
if param.Title != nil {
title = *param.Title
}
keyworld := info.Keyworld
if param.Keyworld != nil {
keyworld = *param.Keyworld
}
slug, changed, slugErr := srv_videoslug.PrepareVideoOnUpdate(ctx, info, param.Slug, title, keyworld)
if slugErr != nil {
return slugErr
}
if changed {
param.Slug = &slug
} else {
param.Slug = nil
}
param.CategoryIds = dedupePositiveInt64Ids(param.CategoryIds)
if param.Cover != nil && len(*param.Cover) > 0 {
if len(info.Video.Url) > 0 {
info.Video.Cover = *param.Cover
param.Video = &info.Video
}
}
//if param.Status != nil && *param.Status != info.Status {
// if *param.Status == videoinformationmod.InformationSatusVerifyOK {
// param.AddedTime = &now
// }
//}
adFilter := bson.M{videoinformationmod.FN.VideoInformation.Id: id}
param.IndexNow = nil
if (param.Status != nil && *param.Status == videoinformationmod.InformationSatusVerifyOK) ||
(param.IsReIndexNow != nil && *param.IsReIndexNow) {
indexNow := int64(0)
param.IndexNow = &indexNow
}
serr = videoinformationmod.UpdateOneVideoInformation(ctx, adFilter, bson.M{"$set": param})
if serr != nil {
return serr
}
playletcache.ClearHomeCache()
// status 发生变更时实时同步 ES,避免搜索结果与 MongoDB 不一致
if param.Status != nil {
newStatus := *param.Status
go func() {
if newStatus != videoinformationmod.InformationSatusVerifyOK {
// 下架/重置状态:立即从 ES 删除
srv_search.DelVideoFromES(id)
} else {
// 上架:从 MongoDB 读取最新完整数据后写入 ES
latest, serr := videoinformationmod.FindOneVideoInformation(context.Background(), adFilter)
if serr != nil {
log.Warn("UpdateVideoInfo: read latest for ES upsert failed", log.Any("id", id), log.E(serr))
return
}
srv_search.UpsertVideoToES(latest)
}
}()
}
if param.Status != nil && info.Status != *param.Status {
if flag := srv_report.VideoStatusFlag(*param.Status); flag != 0 {
mediaCopy := *info
mediaCopy.Status = *param.Status
godef.Go(func() {
srv_report.ReportVideoStatusChangeEvent(&mediaCopy, flag, userId, ua)
})
}
}
sitemapdirty.Mark(ctx)
if param.Status != nil {
if *param.Status == videoinformationmod.InformationSatusVerifyOK &&
info.Status != videoinformationmod.InformationSatusVerifyOK {
srv_seopush.EnqueueVideoUpdated(ctx, id, *param.Status)
} else if info.Status == videoinformationmod.InformationSatusVerifyOK &&
*param.Status != videoinformationmod.InformationSatusVerifyOK {
srv_seopush.EnqueueVideoDeleted(ctx, id)
}
}
return nil
}
func AddVideoCollection(ctx context.Context, param *AddVideoCollectionReq) *srvdef.SrvErr {
info := videoinformationmod.VideoInformation{
Id: mongos.Lid.VideoInfo.Get(),
Title: param.Title,
MediaType: videoinformationmod.MediaType_Collection,
AddedTime: time.Now(),
Keyworld: param.Keyworld,
FirstTag: param.FirstTag,
SecondTags: param.SecondTags,
CoverImg: param.CoverImg,
Desc: param.Desc,
Remark: param.Remark,
Nodes: make([]videoinformationmod.Node, 0),
Status: videoinformationmod.InformationSatusWaiting,
Video: videoinformationmod.VideoInfo{},
CategoryIds: dedupePositiveInt64Ids(param.CategoryIds),
Tags: param.Tags,
Real: videoinformationmod.Flow{},
VideoCount: 0,
IndexNow: 0,
UpdatedAt: time.Now(),
CreatedAt: time.Now(),
Slug: param.Slug,
}
if serr := srv_videoslug.EnsureVideo(ctx, &info, 0); serr != nil {
return serr
}
if serr := videoinformationmod.InsertOneVideoInformation(ctx, &info); serr != nil {
return serr
}
playletcache.ClearHomeCache()
sitemapdirty.Mark(ctx)
return nil
}
// SetVideoCategory 单/批量设置视频分类
func SetVideoCategory(ctx context.Context, req *SetVideoCategoryReq) *srvdef.SrvErr {
if req == nil {
return srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
}
ids, err := normalizeFakeFlowIds(req.Ids)
if err != nil {
return err
}
if err := videoinformationmod.UpdateManyVideoInformation(ctx,
bson.M{videoinformationmod.FN.VideoInformation.Id: bson.M{"$in": ids}},
bson.M{"$set": bson.M{
videoinformationmod.FN.VideoInformation.CategoryIds: dedupePositiveInt64Ids(req.CategoryIds),
videoinformationmod.FN.VideoInformation.UpdatedAt: time.Now(),
}},
); err != nil {
return err
}
playletcache.ClearHomeCache()
return nil
}
func dedupePositiveInt64Ids(ids []int64) []int64 {
result := make([]int64, 0, len(ids))
seen := make(map[int64]struct{}, len(ids))
for _, id := range ids {
if id <= 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
result = append(result, id)
}
return result
}