Initial commit: web_hghl_server
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"videoweb/web/service/srv_ipblack"
|
||||
"videoweb/web/service/srv_mediaresource"
|
||||
"videoweb/web/service/srv_search"
|
||||
"videoweb/web/skd"
|
||||
)
|
||||
|
||||
func Start() {
|
||||
skd.Start()
|
||||
srv_ipblack.Init()
|
||||
srv_mediaresource.Start()
|
||||
srv_search.Start()
|
||||
}
|
||||
|
||||
func Stop() {
|
||||
skd.Stop()
|
||||
srv_mediaresource.Stop()
|
||||
srv_search.Stop()
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package srv_acc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/mwacc"
|
||||
"videoweb/pub/models/oplog"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
func GetOpLogs(ctx context.Context, logSearch *OpLogSearch) (
|
||||
int64, OpLogInfoList, *srvdef.SrvErr) {
|
||||
//log.Info("GetOpLogs Test 1")
|
||||
filter, findOption := constructFilterAndOpts(logSearch)
|
||||
total, serr := oplog.CountOpLogs(ctx, filter)
|
||||
if serr != nil {
|
||||
return 0, nil, serr
|
||||
}
|
||||
list := make(OpLogInfoList, 0)
|
||||
if total == 0 {
|
||||
return 0, list, nil
|
||||
}
|
||||
//log.Info("GetOpLogs Test 2", log.Any("filter", filter), log.Any("findOption", findOption))
|
||||
opLogs, serr := oplog.FindManyOpLogs(ctx, filter, findOption)
|
||||
if serr != nil {
|
||||
return 0, nil, serr
|
||||
}
|
||||
//log.Info("GetOpLogs Test 3")
|
||||
actIds := make([]int64, 0)
|
||||
for _, v := range opLogs {
|
||||
actIds = append(actIds, v.ActId)
|
||||
}
|
||||
|
||||
accFilter := bson.M{mwacc.FN.MwAcc.Id: bson.M{"$in": actIds}}
|
||||
accs, serr := mwacc.FindManyAccByFilter(ctx, accFilter)
|
||||
if serr != nil {
|
||||
return 0, nil, serr
|
||||
}
|
||||
|
||||
//log.Info("GetOpLogs Test 4")
|
||||
for _, opLog := range opLogs {
|
||||
olInfo := &OpLogInfo{
|
||||
Path: opLog.Path,
|
||||
ActId: opLog.ActId,
|
||||
Action: opLog.Action,
|
||||
Param: opLog.RawData,
|
||||
CreatedAt: opLog.CreatedAt,
|
||||
}
|
||||
for _, acc := range accs {
|
||||
if acc.Id == olInfo.ActId {
|
||||
olInfo.ActName = acc.UserName
|
||||
break
|
||||
}
|
||||
}
|
||||
list = append(list, olInfo)
|
||||
}
|
||||
//log.Info("GetOpLogs Test 5")
|
||||
return total, list, nil
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package srv_acc
|
||||
|
||||
import "time"
|
||||
|
||||
type OpLogInfo struct {
|
||||
Path string `json:"path" bson:"path"` // 请求接口
|
||||
ActId int64 `json:"actId" bson:"actId"` // 操作人Id
|
||||
Action string `json:"action" bson:"action"` // 操作目的
|
||||
Param string `json:"param" bson:"rawData"` // 操作内容
|
||||
CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 操作时间
|
||||
ActName string `json:"actName" bson:"actName"` // 操作人
|
||||
}
|
||||
|
||||
type OpLogInfoList []*OpLogInfo
|
||||
|
||||
type OpLogSearch struct {
|
||||
PageNum int64 `json:"pageNum"` // 页码
|
||||
PageSize int64 `json:"pageSize"` //
|
||||
Path *string `json:"path"` // 请求接口
|
||||
Action *string `json:"action"` // 操作目的
|
||||
ActId *int64 `json:"actId"` // 操作人Id
|
||||
StartTime *time.Time `json:"startTime"` // 开始时间
|
||||
EndTime *time.Time `json:"endTime"` // 开始时间
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package srv_acc
|
||||
|
||||
import (
|
||||
"videoweb/pub/models/oplog"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// constructFilterAndOpts 根据搜索参数构建查询filter和options
|
||||
func constructFilterAndOpts(param *OpLogSearch) (
|
||||
bson.M, *options.FindOptions) {
|
||||
|
||||
filter := bson.M{}
|
||||
if param.Path != nil && *param.Path != "" {
|
||||
filter[oplog.FN.OpLog.Path] = bson.M{"$regex": param.Path}
|
||||
}
|
||||
if param.Action != nil && *param.Action != "" {
|
||||
filter[oplog.FN.OpLog.Action] = param.Action
|
||||
}
|
||||
if param.ActId != nil && *param.ActId > 0 {
|
||||
filter[oplog.FN.OpLog.ActId] = param.ActId
|
||||
}
|
||||
if param.StartTime != nil && param.EndTime != nil {
|
||||
filter[oplog.FN.OpLog.CreatedAt] = bson.M{"$gte": param.StartTime, "$lt": param.EndTime}
|
||||
}
|
||||
sort := bson.M{oplog.FN.OpLog.CreatedAt: -1}
|
||||
opt := &options.FindOptions{}
|
||||
opt.SetSort(sort)
|
||||
skip := (param.PageNum - 1) * param.PageSize
|
||||
opt.SetSkip(skip)
|
||||
opt.SetLimit(param.PageSize)
|
||||
return filter, opt
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package srv_actress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/slugx"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/actress"
|
||||
"videoweb/pub/mongos"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func ActressList(ctx context.Context, req *ActressListReq) (*ActressListResp, *srvdef.SrvErr) {
|
||||
filter := bson.M{}
|
||||
if req.Id != nil && *req.Id > 0 {
|
||||
filter[actress.FN.Actress.Id] = *req.Id
|
||||
}
|
||||
if req.TranslationCategoryId != nil && *req.TranslationCategoryId > 0 {
|
||||
filter[actress.FN.Actress.TranslationCategoryId] = *req.TranslationCategoryId
|
||||
}
|
||||
if req.Region != nil && len(*req.Region) > 0 {
|
||||
filter[actress.FN.Actress.Region] = bson.M{"$regex": *req.Region}
|
||||
}
|
||||
if req.Status != nil {
|
||||
filter[actress.FN.Actress.Status] = *req.Status
|
||||
}
|
||||
if req.Alias != nil && *req.Alias != "" {
|
||||
filter[actress.FN.Actress.Alias] = bson.M{"$regex": *req.Alias}
|
||||
}
|
||||
count, err := actress.CountActresss(ctx, filter)
|
||||
if err != nil {
|
||||
log.Warn("ActressList CountActresss", log.Any("filter", filter), log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
resp := &ActressListResp{
|
||||
Total: 0,
|
||||
List: make([]*actress.Actress, 0),
|
||||
}
|
||||
if count == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
opts := &options.FindOptions{}
|
||||
opts.SetLimit(req.PageSize)
|
||||
opts.SetSkip((req.PageNum - 1) * req.PageSize)
|
||||
sort := bson.M{actress.FN.Actress.CreatedAt: -1}
|
||||
switch req.SortType {
|
||||
case 1:
|
||||
sort = bson.M{actress.FN.Actress.Score: -1}
|
||||
case 2:
|
||||
sort = bson.M{actress.FN.Actress.Rank: -1}
|
||||
case 3:
|
||||
sort = bson.M{actress.FN.Actress.Works: -1}
|
||||
case 4:
|
||||
sort = bson.M{actress.FN.Actress.Real.WatchTimes: -1}
|
||||
case 5:
|
||||
sort = bson.M{actress.FN.Actress.HotValue: -1, actress.FN.Actress.Id: -1}
|
||||
}
|
||||
opts.SetSort(sort)
|
||||
list, err := actress.FindManyActresss(ctx, filter, opts)
|
||||
if err != nil {
|
||||
log.Warn("ActressList FindManyActresss", log.Any("filter", filter), log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
resp.Total = count
|
||||
resp.List = list
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func UpdateActress(ctx context.Context, req *ActressUpdateReq) *srvdef.SrvErr {
|
||||
current, err := actress.FindOneActress(ctx, bson.M{actress.FN.Actress.Id: req.Id})
|
||||
if err != nil {
|
||||
log.Warn("UpdateActress", log.Any("id", req.Id), log.E(err))
|
||||
return err
|
||||
}
|
||||
alias := current.Alias
|
||||
if req.Param.Alias != nil {
|
||||
alias = *req.Param.Alias
|
||||
}
|
||||
keywords := current.SeoInfo.Keyworld
|
||||
if req.Param.SeoInfo != nil {
|
||||
keywords = req.Param.SeoInfo.Keyworld
|
||||
}
|
||||
exists := func(s string) bool {
|
||||
doc, _ := actress.FindOneActress(ctx, slugx.Filter(actress.FN.Actress.Slug, actress.FN.Actress.Id, s, req.Id))
|
||||
return doc != nil
|
||||
}
|
||||
slug, changed, serr := slugx.PrepareOnUpdate(current.Slug, req.Param.Slug,
|
||||
utils.SlugSourceFromKeywordsOrTitle(keywords, alias), exists)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
if changed {
|
||||
req.Param.Slug = &slug
|
||||
} else {
|
||||
req.Param.Slug = nil
|
||||
}
|
||||
now := time.Now()
|
||||
req.Param.UpdatedAt = &now
|
||||
return actress.UpdateOneActress(ctx, bson.M{actress.FN.Actress.Id: req.Id}, bson.M{"$set": req.Param})
|
||||
}
|
||||
|
||||
func AddActress(ctx context.Context, req *ActressAddReq) *srvdef.SrvErr {
|
||||
if len(req.Alias) == 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "alias 不能为空", "alias 不能为空")
|
||||
}
|
||||
_, err := actress.FindOneActress(ctx, bson.M{
|
||||
actress.FN.Actress.Alias: req.Alias,
|
||||
})
|
||||
if err != nil && err.Code != srvdef.ErrNoData {
|
||||
log.Warn("AddActress", log.Any("req", req), log.E(err))
|
||||
return err
|
||||
}
|
||||
if err == nil {
|
||||
log.Warn("AddActress Re", log.Any("req", req))
|
||||
return srvdef.New(srvdef.RepeatActress)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
info := &actress.Actress{
|
||||
Id: mongos.Lid.Actress.Get(),
|
||||
Alias: req.Alias,
|
||||
Slug: req.Slug,
|
||||
SeoInfo: req.SeoInfo,
|
||||
Avatar: req.Avatar,
|
||||
Cover: req.Cover,
|
||||
Desc: req.Desc,
|
||||
TranslationCategoryId: req.TranslationCategoryId,
|
||||
Meas: req.Meas,
|
||||
Office: req.Office,
|
||||
DebutDate: req.DebutDate,
|
||||
BornDate: req.BornDate,
|
||||
Region: req.Region,
|
||||
Score: req.Score,
|
||||
Rank: req.Rank,
|
||||
HotValue: req.HotValue,
|
||||
Status: req.Status,
|
||||
Works: 0,
|
||||
Real: pubdef.Flow{},
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
exists := func(s string) bool {
|
||||
doc, _ := actress.FindOneActress(ctx, slugx.Filter(actress.FN.Actress.Slug, actress.FN.Actress.Id, s, 0))
|
||||
return doc != nil
|
||||
}
|
||||
if serr := slugx.Ensure(&info.Slug, utils.SlugSourceFromKeywordsOrTitle(info.SeoInfo.Keyworld, info.Alias), exists); serr != nil {
|
||||
return serr
|
||||
}
|
||||
return actress.InsertOneActress(ctx, info)
|
||||
}
|
||||
|
||||
func DeleteActress(ctx context.Context, req *ActressDelReq) *srvdef.SrvErr {
|
||||
_, err := actress.FindOneActress(ctx, bson.M{actress.FN.Actress.Id: req.Id})
|
||||
if err != nil {
|
||||
log.Warn("DeleteActress", log.Any("id", req.Id), log.E(err))
|
||||
return err
|
||||
}
|
||||
_, err = actress.DelOneActress(ctx, bson.M{actress.FN.Actress.Id: req.Id})
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package srv_actress
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/pub/models/actress"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
type ActressListReq struct {
|
||||
pubdef.Page
|
||||
Id *int64 `form:"id" json:"id"` // Id
|
||||
Alias *string `form:"alias" json:"alias"` // 别名
|
||||
TranslationCategoryId *int64 `form:"translationCategoryId" json:"translationCategoryId"` // 关联导航分类id
|
||||
Region *string `form:"region" json:"region"` // 地区/国家
|
||||
Status *bool `form:"status" json:"status" ` // 是否启用
|
||||
SortType int64 `form:"sortType" json:"sortType"` // 排序方式 0=创建时间 1=评分 2=排序值 3=作品数 4=总观看次数 5=热度值
|
||||
}
|
||||
type ActressListResp struct {
|
||||
Total int64 `json:"total"`
|
||||
List []*actress.Actress `json:"list"`
|
||||
}
|
||||
|
||||
type ActressUpdateReq struct {
|
||||
Id int64 `json:"id,omitempty" binding:"required"`
|
||||
Param ActressUpdateParam `json:"param" binding:"required"`
|
||||
}
|
||||
|
||||
type ActressUpdateParam struct {
|
||||
Alias *string `bson:"alias,omitempty" json:"alias"` // 别名
|
||||
Slug *string `json:"slug" bson:"slug,omitempty"` // slug
|
||||
Avatar *string `bson:"avatar,omitempty" json:"avatar"` // 头像
|
||||
Cover *string `bson:"cover,omitempty" json:"cover"` // 封面大图
|
||||
Desc *string `bson:"desc,omitempty" json:"desc"` // 详细描述
|
||||
TranslationCategoryId *int64 `bson:"translationCategoryId,omitempty" json:"translationCategoryId"` // 关联导航分类id
|
||||
Meas *actress.Meas `bson:"meas,omitempty" json:"meas"` // 三围
|
||||
Office *string `json:"office" bson:"office,omitempty"` // 事务所
|
||||
DebutDate *time.Time `bson:"debutDate,omitempty" json:"debutDate" ` // 出道日期
|
||||
BornDate *time.Time `bson:"bornDate,omitempty" json:"bornDate"` // 出生日期
|
||||
Region *string `json:"region" bson:"region,omitempty"` // 地区/国家
|
||||
Score *string `json:"score" bson:"score,omitempty"` // 评分
|
||||
Rank *int64 `json:"rank" bson:"rank,omitempty"` // 排序
|
||||
HotValue *int64 `json:"hotValue" bson:"hotValue,omitempty"` // 热度值
|
||||
Status *bool `json:"status" bson:"status,omitempty"` // 是否启用
|
||||
SeoInfo *pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
|
||||
UpdatedAt *time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` // 更新时间
|
||||
}
|
||||
|
||||
type ActressAddReq struct {
|
||||
Alias string `bson:"alias" json:"alias"` // 别名
|
||||
Slug string `json:"slug" bson:"slug"` // slug,不传则按 SEO 关键词或别名自动生成
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
|
||||
Avatar string `bson:"avatar" json:"avatar"` // 头像
|
||||
Cover string `bson:"cover" json:"cover"` // 封面大图
|
||||
Desc string `bson:"desc" json:"desc"` // 详细描述
|
||||
TranslationCategoryId int64 `bson:"translationCategoryId" json:"translationCategoryId"` // 关联导航分类id
|
||||
Meas actress.Meas `bson:"meas" json:"meas"` // 三围
|
||||
Office string `json:"office" bson:"office"` // 事务所
|
||||
DebutDate time.Time `bson:"debutDate" json:"debutDate" ` // 出道日期
|
||||
BornDate time.Time `bson:"bornDate" json:"bornDate"` // 出生日期
|
||||
Region string `json:"region" bson:"region"` // 地区/国家
|
||||
Score string `json:"score" bson:"score"` // 评分
|
||||
Rank int64 `json:"rank" bson:"rank"` // 排序
|
||||
HotValue int64 `json:"hotValue" bson:"hotValue"` // 热度值
|
||||
Status bool `json:"status" bson:"status"` // 是否启用
|
||||
}
|
||||
|
||||
type ActressDelReq struct {
|
||||
Id int64 `json:"id"`
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package srv_announcement
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"videoweb/pub/common/playletcache"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/announcementmod"
|
||||
"videoweb/pub/mongos"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func GetAnnouncementList(ctx context.Context, req *GetAnnouncementListReq) (*GetAnnouncementListResp, *srvdef.SrvErr) {
|
||||
filter := bson.M{}
|
||||
if req.Title != nil && *req.Title != "" {
|
||||
filter[announcementmod.FN.Announcement.Title] = bson.M{"$regex": *req.Title, "$options": "i"}
|
||||
}
|
||||
if req.Status != nil {
|
||||
filter[announcementmod.FN.Announcement.Status] = *req.Status
|
||||
}
|
||||
|
||||
count, serr := announcementmod.CountAnnouncement(ctx, filter)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
if count == 0 {
|
||||
return &GetAnnouncementListResp{
|
||||
Total: 0,
|
||||
List: make(announcementmod.AnnouncementList, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
opts := options.FindOptions{}
|
||||
opts.SetSort(bson.D{
|
||||
{Key: announcementmod.FN.Announcement.Sort, Value: -1},
|
||||
{Key: announcementmod.FN.Announcement.CreatedAt, Value: -1},
|
||||
})
|
||||
opts.SetLimit(req.PageSize)
|
||||
opts.SetSkip((req.PageNum - 1) * req.PageSize)
|
||||
|
||||
list, serr := announcementmod.FindManyAnnouncement(ctx, filter, &opts)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
|
||||
return &GetAnnouncementListResp{
|
||||
Total: count,
|
||||
List: list,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func AddAnnouncement(ctx context.Context, req *AddAnnouncementReq) *srvdef.SrvErr {
|
||||
now := time.Now()
|
||||
status := true
|
||||
if req.Status != nil {
|
||||
status = *req.Status
|
||||
}
|
||||
doc := &announcementmod.Announcement{
|
||||
Id: mongos.Lid.Announcement.Get(),
|
||||
Title: req.Title,
|
||||
Content: req.Content,
|
||||
Sort: req.Sort,
|
||||
Status: status,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if err := announcementmod.InsertOneAnnouncement(ctx, doc); err != nil {
|
||||
return err
|
||||
}
|
||||
playletcache.ClearHomeCache()
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateAnnouncement(ctx context.Context, req *UpdateAnnouncementReq) *srvdef.SrvErr {
|
||||
filter := bson.M{announcementmod.FN.Announcement.Id: req.Id}
|
||||
updateFields := bson.M{
|
||||
announcementmod.FN.Announcement.UpdatedAt: time.Now(),
|
||||
}
|
||||
if req.Param.Title != nil {
|
||||
updateFields[announcementmod.FN.Announcement.Title] = *req.Param.Title
|
||||
}
|
||||
if req.Param.Content != nil {
|
||||
updateFields[announcementmod.FN.Announcement.Content] = *req.Param.Content
|
||||
}
|
||||
if req.Param.Sort != nil {
|
||||
updateFields[announcementmod.FN.Announcement.Sort] = *req.Param.Sort
|
||||
}
|
||||
if req.Param.Status != nil {
|
||||
updateFields[announcementmod.FN.Announcement.Status] = *req.Param.Status
|
||||
}
|
||||
if err := announcementmod.UpdateOneAnnouncement(ctx, filter, bson.M{"$set": updateFields}); err != nil {
|
||||
return err
|
||||
}
|
||||
playletcache.ClearHomeCache()
|
||||
return nil
|
||||
}
|
||||
|
||||
func DelAnnouncement(ctx context.Context, req *DelAnnouncementReq) *srvdef.SrvErr {
|
||||
filter := bson.M{announcementmod.FN.Announcement.Id: req.Id}
|
||||
count, serr := announcementmod.DelOneAnnouncement(ctx, filter)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
if count == 0 {
|
||||
return srvdef.New(srvdef.ErrAnnouncementNotExsit, "", "")
|
||||
}
|
||||
playletcache.ClearHomeCache()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package srv_announcement
|
||||
|
||||
import (
|
||||
"videoweb/pub/models/announcementmod"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
type GetAnnouncementListReq struct {
|
||||
Title *string `json:"title"` // 标题,模糊搜索
|
||||
Status *bool `json:"status"` // 是否启用
|
||||
pubdef.Page
|
||||
}
|
||||
|
||||
type GetAnnouncementListResp struct {
|
||||
Total int64 `json:"total"`
|
||||
List announcementmod.AnnouncementList `json:"list"`
|
||||
}
|
||||
|
||||
type AddAnnouncementReq struct {
|
||||
Title string `json:"title" binding:"required"` // 标题
|
||||
Content string `json:"content"` // 内容
|
||||
Sort int64 `json:"sort"` // 排序,越大越靠前
|
||||
Status *bool `json:"status"` // 是否启用
|
||||
}
|
||||
|
||||
type UpdateAnnouncementReq struct {
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
Param UpdateAnnouncementParam `json:"param"`
|
||||
}
|
||||
|
||||
type UpdateAnnouncementParam struct {
|
||||
Title *string `json:"title"`
|
||||
Content *string `json:"content"`
|
||||
Sort *int64 `json:"sort"`
|
||||
Status *bool `json:"status"`
|
||||
}
|
||||
|
||||
type DelAnnouncementReq struct {
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package srv_app
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
type AppCategoryInfo struct {
|
||||
Id int64 `json:"id" bson:"_id,omitempty"` // Id
|
||||
Name string `json:"name" bson:"name"` // 分类名称
|
||||
Title string `json:"title" bson:"title"` // APP显示
|
||||
Desc string `json:"desc" bson:"desc"` // 描述
|
||||
Rank int64 `json:"rank" bson:"rank"` // 排序
|
||||
}
|
||||
|
||||
type AppCreatorInfo struct {
|
||||
Id int64 `json:"id" bson:"_id,omitempty"`
|
||||
Name string `json:"name" bson:"name"` // 帖子标题
|
||||
Sort int64 `json:"sort" bson:"sort"` // 排序
|
||||
}
|
||||
|
||||
type AppTagInfo struct {
|
||||
Id int64 `json:"id" bson:"_id,omitempty"`
|
||||
Title string `json:"title" bson:"title"` // 帖子标题
|
||||
Sort int64 `json:"sort" bson:"sort"` // 排序
|
||||
}
|
||||
|
||||
// AppVideoInfoSyncReq APP数据同步请求
|
||||
type AppVideoInfoSyncReq struct {
|
||||
AppId int64 `json:"appId" bson:"appId"`
|
||||
LastSyncTime time.Time `json:"lastSyncTime" bson:"lastSyncTime"` // 最后同步时间
|
||||
PageSize int64 `json:"pageSize" bson:"pageSize"` // 页大小
|
||||
}
|
||||
|
||||
type AppVideoCollectionSyncReq struct {
|
||||
AppId int64 `json:"appId" bson:"appId"`
|
||||
CollectionId []int64 `json:"collectionId" bson:"collectionId"`
|
||||
}
|
||||
|
||||
type AppVideoCollectionSyncInfo struct {
|
||||
CollectionID int64 `json:"collectionId" bson:"collectionId"` // 合集ID
|
||||
VideoId int64 `json:"videoId" bson:"videoId"` // 视频ID
|
||||
Rank int64 `json:"rank" bson:"rank"` // 排序
|
||||
}
|
||||
|
||||
type AppVideoSyncInfo struct {
|
||||
Id int64 `json:"id" bson:"_id,omitempty"`
|
||||
Title string `json:"title" bson:"title"` // 帖子标题
|
||||
MediaType videoinformationmod.MediaType `json:"mediaType" bson:"mediaType"` // 资源类型
|
||||
AddedTime time.Time `json:"addedTime" bson:"addedTime"` // 帖子上架时间
|
||||
CreatorId int64 `json:"creatorId" bson:"creatorId"` // 作者Id
|
||||
CreatorName string `json:"creatorName" bson:"creatorName"` // 作者名称
|
||||
Keyworld []string `bson:"keyworld,omitempty" json:"keyworld"` // 关键字
|
||||
FirstTag string `bson:"firstTag,omitempty" json:"firstTag"` // 一级标签
|
||||
SecondTags []string `bson:"secondTags,omitempty" json:"secondTags"` // 二级标签标签
|
||||
CoverImg string `bson:"coverImg,omitempty" json:"coverImg"` // 封面
|
||||
Desc string `bson:"desc,omitempty" json:"desc"` // 详细描述
|
||||
Remark string `bson:"remark,omitempty" json:"remark"` // 备注
|
||||
Nodes []videoinformationmod.Node `json:"nodes" bson:"nodes"` // 节点
|
||||
Status pubdef.InformationStatus `json:"status" bson:"status"` // 帖子上架状态
|
||||
Video videoinformationmod.VideoInfo `json:"video" bson:"video"` // 视频
|
||||
CategoryIds []int64 `json:"categoryIds" bson:"categoryIds"` // 分类Id
|
||||
Tags []int64 `json:"tags" bson:"tags"` // 标签
|
||||
IsHot bool `json:"isHot" bson:"isHot"` // 是否热门
|
||||
Sort int64 `json:"sort" bson:"sort"` // 排序
|
||||
Real pubdef.Flow `json:"real" bson:"real"` // 真实流量
|
||||
VideoCount int64 `json:"videoCount" bson:"videoCount"` // 合集视频数量
|
||||
UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
|
||||
CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package srv_app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/contenttagmod"
|
||||
"videoweb/pub/models/videocategorymod"
|
||||
"videoweb/pub/models/videocollectionmediamod"
|
||||
"videoweb/pub/models/videocreatermod"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
)
|
||||
|
||||
func AppVideoInfoSync(ctx context.Context, req *AppVideoInfoSyncReq) ([]*videoinformationmod.VideoInformation, *srvdef.SrvErr) {
|
||||
filter := bson.M{
|
||||
videoinformationmod.FN.VideoInformation.UpdatedAt: bson.M{
|
||||
"$gt": req.LastSyncTime},
|
||||
}
|
||||
opts := &options.FindOptions{}
|
||||
opts.SetSort(bson.M{videoinformationmod.FN.VideoInformation.UpdatedAt: 1})
|
||||
opts.SetLimit(req.PageSize)
|
||||
list, err := videoinformationmod.FindManyVideoInformations(ctx, filter, opts)
|
||||
if err != nil {
|
||||
log.Error("FindManyVideoInformations Failed", log.Any("err", err))
|
||||
return nil, err
|
||||
}
|
||||
return list, err
|
||||
}
|
||||
|
||||
func AppVideoCollectionSync(ctx context.Context, req *AppVideoCollectionSyncReq) ([]*AppVideoCollectionSyncInfo, *srvdef.SrvErr) {
|
||||
filter := bson.M{
|
||||
videocollectionmediamod.FN.VideoCollectionMediaModel.CollectionID: bson.M{"$in": req.CollectionId},
|
||||
}
|
||||
list, err := videocollectionmediamod.FindManyVideoCollectionMediaModels(ctx, filter)
|
||||
if err != nil {
|
||||
log.Error("FindManyVideoCollectionMediaModels Failed", log.Any("err", err))
|
||||
return nil, err
|
||||
}
|
||||
rsl := make([]*AppVideoCollectionSyncInfo, 0)
|
||||
for _, v := range list {
|
||||
rsl = append(rsl, &AppVideoCollectionSyncInfo{
|
||||
CollectionID: v.CollectionID,
|
||||
VideoId: v.VideoId,
|
||||
Rank: v.Rank,
|
||||
})
|
||||
}
|
||||
return rsl, nil
|
||||
}
|
||||
|
||||
func AppCategoryList(ctx context.Context) ([]AppCategoryInfo, *srvdef.SrvErr) {
|
||||
filter := bson.M{
|
||||
videocategorymod.FN.VideoCategory.Status: true,
|
||||
}
|
||||
opts := &options.FindOptions{}
|
||||
opts.SetSort(bson.M{videocategorymod.FN.VideoCategory.Rank: -1})
|
||||
list, err := videocategorymod.FindManyVideoCategorys(ctx, filter, opts)
|
||||
if err != nil {
|
||||
log.Error("FindManyVideoCategorys Failed", log.Any("err", err))
|
||||
return nil, err
|
||||
}
|
||||
resp := make([]AppCategoryInfo, 0)
|
||||
for _, v := range list {
|
||||
resp = append(resp, AppCategoryInfo{
|
||||
Id: v.Id,
|
||||
Name: v.Name,
|
||||
Title: v.Title,
|
||||
Desc: v.Desc,
|
||||
Rank: v.Rank,
|
||||
})
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func AppCreatorList(ctx context.Context) ([]AppCreatorInfo, *srvdef.SrvErr) {
|
||||
filter := bson.M{
|
||||
videocreatermod.FN.VideoCreaterInfo.Status: true,
|
||||
}
|
||||
opts := &options.FindOptions{}
|
||||
opts.SetSort(bson.M{
|
||||
videocreatermod.FN.VideoCreaterInfo.Sort: -1,
|
||||
})
|
||||
list, err := videocreatermod.FindManyVideoCreaterInfos(ctx, filter, opts)
|
||||
if err != nil {
|
||||
log.Error("FindManyVideoCreaterInfos Failed", log.Any("err", err))
|
||||
return nil, err
|
||||
}
|
||||
resp := make([]AppCreatorInfo, 0)
|
||||
for _, v := range list {
|
||||
resp = append(resp, AppCreatorInfo{
|
||||
Id: v.Id,
|
||||
Name: v.Name,
|
||||
Sort: v.Sort,
|
||||
})
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func AppTagList(ctx context.Context) ([]AppTagInfo, *srvdef.SrvErr) {
|
||||
filter := bson.M{
|
||||
contenttagmod.FN.ContentTag.Status: true,
|
||||
contenttagmod.FN.ContentTag.Types: contenttagmod.ContentTagTypeVideo,
|
||||
}
|
||||
opts := &options.FindOptions{}
|
||||
opts.SetSort(bson.D{
|
||||
{Key: contenttagmod.FN.ContentTag.Rank, Value: -1},
|
||||
{Key: contenttagmod.FN.ContentTag.Id, Value: -1},
|
||||
})
|
||||
list, err := contenttagmod.FindManyContentTag(ctx, filter, opts)
|
||||
if err != nil {
|
||||
log.Error("FindManyContentTag Failed", log.Any("err", err))
|
||||
return nil, err
|
||||
}
|
||||
resp := make([]AppTagInfo, 0)
|
||||
for _, v := range list {
|
||||
resp = append(resp, AppTagInfo{
|
||||
Id: v.Id,
|
||||
Title: v.Name,
|
||||
Sort: v.Rank,
|
||||
})
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package srv_cfg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/cfg"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
// GetSystemConfig 得到系统配置
|
||||
func GetSystemConfig(ctx context.Context) (*cfg.SysCfg, *srvdef.SrvErr) {
|
||||
filter := bson.M{
|
||||
cfg.FN.SysCfg.Id: 1, //固定id
|
||||
}
|
||||
sysCfg, serr := cfg.FindOneSysCfg(ctx, filter)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
return sysCfg, nil
|
||||
}
|
||||
|
||||
// emptyWatermark 水印字段为空时也写入空字符串,避免 omitempty 跳过导致旧值残留
|
||||
func emptyWatermark(v *string) *string {
|
||||
empty := ""
|
||||
if v == nil || strings.TrimSpace(*v) == "" {
|
||||
return &empty
|
||||
}
|
||||
s := strings.TrimSpace(*v)
|
||||
return &s
|
||||
}
|
||||
|
||||
func applyEmptyWatermarkFields(info *SysCfgUpdate) {
|
||||
if info == nil {
|
||||
return
|
||||
}
|
||||
info.WatermarkImage = emptyWatermark(info.WatermarkImage)
|
||||
info.WatermarkMedia = emptyWatermark(info.WatermarkMedia)
|
||||
info.WatermarkMediaTop = emptyWatermark(info.WatermarkMediaTop)
|
||||
info.WatermarkMediaTopRight = emptyWatermark(info.WatermarkMediaTopRight)
|
||||
info.WatermarkMediaBottomLeft = emptyWatermark(info.WatermarkMediaBottomLeft)
|
||||
}
|
||||
|
||||
// UpdateSystemConfig 更新系统配置
|
||||
func UpdateSystemConfig(ctx context.Context, info *SysCfgUpdate) *srvdef.SrvErr {
|
||||
applyEmptyWatermarkFields(info)
|
||||
filter := bson.M{
|
||||
cfg.FN.SysCfg.Id: 1, //固定id
|
||||
}
|
||||
fmt.Println("filter", filter)
|
||||
updateDoc := bson.M{"$set": info}
|
||||
serr := cfg.UpdateOneSysCfg(ctx, filter, updateDoc)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetAppKey 单独更新媒资库 appKey,并刷新内存缓存。
|
||||
func SetAppKey(ctx context.Context, appKey string) *srvdef.SrvErr {
|
||||
appKey = strings.TrimSpace(appKey)
|
||||
filter := bson.M{
|
||||
cfg.FN.SysCfg.Id: 1,
|
||||
}
|
||||
updateDoc := bson.M{"$set": bson.M{
|
||||
cfg.FN.SysCfg.AppKey: appKey,
|
||||
}}
|
||||
if serr := cfg.UpdateOneSysCfg(ctx, filter, updateDoc); serr != nil {
|
||||
return serr
|
||||
}
|
||||
cfg.CacheAppKey(appKey)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package srv_cfg
|
||||
|
||||
import (
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type CDNNode struct {
|
||||
Url *string `json:"url" bson:"url,omitempty"`
|
||||
Ratio *int `json:"ratio" bson:"ratio,omitempty"`
|
||||
}
|
||||
|
||||
type SysCfgUpdate struct {
|
||||
UnencryptedCdn *string `json:"unencryptedCdn" bson:"unencryptedCdn,omitempty"` // 不加密资源使用的cdn
|
||||
SitemapDefaultImages *[]string `json:"sitemapDefaultImages" bson:"sitemapDefaultImages,omitempty"` // Sitemap里面默认封面图
|
||||
PdfAddr *string `json:"pdfAddr" bson:"pdfAddr"` // 回家的路PDF
|
||||
ImgAppCdn *string `json:"imgAppCdn" bson:"imgAppCdn"` // 图片CDN(app使用)
|
||||
ImgAppUsCdn *string `json:"imgAppUsCdn" bson:"imgAppUsCdn"` // 图片CDN(海外app使用)
|
||||
ImgWebCdn *string `json:"imgWebCdn" bson:"imgWebCdn"` // 图片CDN(web使用)
|
||||
MediaWebCdn *string `json:"mediaWebCdn" bson:"mediaWebCdn"` // 视频CDN(web使用)
|
||||
MediaAppCdn *string `json:"mediaAppCdn" bson:"mediaAppCdn"` // 视频CDN(app使用)
|
||||
MediaAppUsCdn *string `json:"mediaAppUsCdn" bson:"mediaAppUsCdn"` // 视频CDN(海外app使用)
|
||||
WatermarkImage *string `json:"watermarkImage" bson:"watermarkImage"` // 图片水印文件
|
||||
WatermarkMedia *string `json:"watermarkMedia" bson:"watermarkMedia"` // 视频水印文件(底部右边)
|
||||
WatermarkMediaTop *string `json:"watermarkMediaTop" bson:"watermarkMediaTop"` // 视频水印文件(顶部左边)
|
||||
WatermarkMediaTopRight *string `json:"watermarkMediaTopRight" bson:"watermarkMediaTopRight"` // 视频水印文件(顶部右边)
|
||||
WatermarkMediaBottomLeft *string `json:"watermarkMediaBottomLeft" bson:"watermarkMediaBottomLeft"` // 视频水印文件(底部左边)
|
||||
Mp4Addr *string `json:"mp4Addr" bson:"mp4Addr"` // mp4地址(web使用)
|
||||
NewUrlAddr *string `json:"newUrlAddr" bson:"newUrlAddr"` //最新地址
|
||||
NewUrlAddr1 *string `json:"newUrlAddr1" bson:"newUrlAddr1"` //最新地址(备用地址1)
|
||||
NewUrlAddr2 *string `json:"newUrlAddr2" bson:"newUrlAddr2"` //最新地址(备用地址2)
|
||||
ForeverUrlAddr *string `json:"foreverUrlAddr" bson:"foreverUrlAddr"` //永久地址
|
||||
GmailAddr *string `json:"gmailAddr" bson:"gmailAddr"` //邮箱地址
|
||||
AppAddr *string `json:"appAddr" bson:"appAddr"` //APP下载地址(落地页)
|
||||
DownloadVideoUrl *string `json:"downloadVideoUrl" bson:"downloadVideoUrl"` // 下载视频URL
|
||||
WeiChart *string `json:"weiChart" bson:"weiChart"` //微信群
|
||||
QQ *string `json:"qq" bson:"qq"` //qq群
|
||||
TgGroup *string `json:"tgGroup" bson:"tgGroup"` //tg群
|
||||
PotaoGroup *string `json:"potaoGroup" bson:"potaoGroup"` //土豆群
|
||||
GitAddr *string `json:"gitAddr" bson:"gitAddr"` //git地址
|
||||
TwitterAddr *string `json:"twitterAddr" bson:"twitterAddr"` //推特地址
|
||||
Telegram *string `json:"telegram" bson:"telegram"` //商务tg
|
||||
TelegramAdd *string `json:"telegramAdd" bson:"telegramAdd"` //加入我们tg
|
||||
TranslationAddr *string `json:"translationAddr" bson:"translationAddr"` //应用导航
|
||||
//SEO
|
||||
WebTitle *string `json:"webTitle" bson:"webTitle"` // 网站SEO标题
|
||||
WebKeyworld *[]string `json:"webKeyworld" bson:"webKeyworld"` // 网站SEO关键字
|
||||
WebDescription *string `json:"webDescription" bson:"webDescription"` // 网站SEO描述
|
||||
WebSiteName *string `json:"webSiteName" bson:"webSiteName"` // 网站SEO网站名称
|
||||
WebAddr *string `json:"webAddr" bson:"webAddr"` // 网站SEO网站地址
|
||||
DHNewUrlAddr1 *string `json:"dhNewUrlAddr1" bson:"dhNewUrlAddr1"` // 导航页-线路一
|
||||
DHNewUrlAddr2 *string `json:"dhNewUrlAddr2" bson:"dhNewUrlAddr2"` // 导航页-线路2
|
||||
DHNewUrlAddr3 *string `json:"dhNewUrlAddr3" bson:"dhNewUrlAddr3"` // 导航页-线路3
|
||||
DHTitle *string `json:"dhTitle" bson:"dhTitle"` // 导航SEO标题
|
||||
DHKeyworld *[]string `json:"dhKeyworld" bson:"dhKeyworld"` // 导航SEO关键字
|
||||
DHDescription *string `json:"dhDescription" bson:"dhDescription"` // 导航SEO描述
|
||||
DHSiteName *string `json:"dhSiteName" bson:"dhSiteName"` // 导航SEO网站名称
|
||||
ChannelCode *string `json:"channelCode" bson:"channelCode"` // 渠道码
|
||||
EnableH265 *bool `json:"enableH265" bson:"enableH265"` // 是否允许客户端使用 H265 地址播放
|
||||
M3U8DayRateLimitMax *int64 `json:"m3u8DayRateLimitMax" bson:"m3u8DayRateLimitMax"` // 每 IP 每天最多不同正片数,<=0 使用默认 80
|
||||
|
||||
// Acc2UserCDN *string `json:"acc2UserCDN" bson:"acc2UserCDN"` // up主视频播放CDN
|
||||
// InviteAddr *string `json:"inviteAddr" bson:"inviteAddr"` // 用户邀请地址
|
||||
// TelegramContact *string `json:"telegramContact" bson:"telegramContact"` // Telegram联系方式
|
||||
// PotatoContact *string `json:"potatoContact" bson:"potatoContact"` // Potato联系方式
|
||||
// DriverGroup *string `json:"driverGroup" bson:"driverGroup"` // 开车群
|
||||
// CoinPrice *int64 `json:"coinPrice" bson:"coinPrice"` // 金币单价,单位分
|
||||
// OfficialEmail *string `json:"officialEmail" bson:"officialEmail"` // 官方邮箱
|
||||
// HeaderM3U8 *string `json:"headerM3U8" bson:"headerM3U8"` // 片头m3u8
|
||||
// AppCenter *string `json:"appCenter" bson:"appCenter"` // 应用中心
|
||||
// PayGuidPageUrl *string `json:"payGuidPageUrl" bson:"payGuidPageUrl"` // 支付引导页面地址
|
||||
// ApiLimit *cfg.ApiCallLimit `json:"apiLimit" bson:"apiLimit"` // 访问接口和频率参数限制(每10秒)
|
||||
// H5ShareUrl *string `json:"h5ShareUrl" bson:"h5ShareUrl"` // H5分享域名
|
||||
// OnlineNum *int64 `json:"onlineNum" bson:"onlineNum"` // 在线人数
|
||||
// Mp4Url *string `json:"mp4Url" bson:"mp4Url"` // MP4域名
|
||||
// HotScoreConf *cfg.HotScoreConf `json:"hotScoreConf" bson:"hotScoreConf"` // 视频热度值配置
|
||||
// MinimumDiscount *int64 `json:"minimumDiscount" bson:"minimumDiscount"` // 用户最低折扣
|
||||
// AndroidGuestKey *string `json:"androdiGuestkey" bson:"androdiGuestkey"` // android注册签名
|
||||
// H5GuestKey *string `json:"h5GuestKey" bson:"h5GuestKey"` // h5注册签名
|
||||
// CustomerServiceHost *string `json:"customerServiceHost" bson:"customerServiceHost"` // 客服Host
|
||||
// AdsType *cfg.AdsTypeConfig `json:"adsType" bson:"adsType"` // 广告模式
|
||||
// FloatingWindow *FloatingWindow `json:"floatingWindow" bson:"floatingWindow"` // 悬浮窗
|
||||
// IsLoliBlur *bool `json:"isLoliBlur" bson:"isLoliBlur,omitempty"` // 是否开启站群蒙版
|
||||
// ChargeGiftChannel *string `json:"chargeGiftChannel" bson:"chargeGiftChannel"` // 充值赠送金币的渠道
|
||||
// ChargeGiftGoldNumber *int64 `json:"chargeGiftGoldNumber" bson:"chargeGiftGoldNumber"` // 充值赠送金币数量
|
||||
// AdminTG *string `json:"adminTG" bson:"adminTG"` // 管理员tg
|
||||
// Theme *cfg.Theme `json:"theme" bson:"theme"`
|
||||
}
|
||||
type SysCfgMapParam struct {
|
||||
Key string `json:"key" bson:"key"` //Key
|
||||
StringVal string `json:"stringVal" bson:"stringVal"` //字符串的值
|
||||
IntVal int64 `json:"intVal" bson:"intVal"` //int64的值
|
||||
Desc string `json:"desc" bson:"desc"` //配置描述
|
||||
}
|
||||
|
||||
type UpdateSysCfgMapReq struct {
|
||||
Id primitive.ObjectID `json:"id" bson:"_id"` //ID
|
||||
Param *SysCfgMapParam `json:"param" bson:"param"`
|
||||
}
|
||||
|
||||
type DeleteSysCfgMapReq struct {
|
||||
Id primitive.ObjectID `json:"id" bson:"_id"` //ID
|
||||
}
|
||||
|
||||
type GetSysCfgMapReq struct {
|
||||
Key *string `json:"key" bson:"key"`
|
||||
Desc *string `json:"desc" bson:"desc"`
|
||||
}
|
||||
|
||||
type FloatingWindow struct {
|
||||
Enable bool `json:"enable" bson:"enable"`
|
||||
Image string `json:"image" bson:"image"` //浮窗图片
|
||||
Url string `json:"url" bson:"url"` //浮窗URL
|
||||
ImageClose string `json:"imageClose" bson:"imageClose"` //浮窗图片
|
||||
}
|
||||
@@ -0,0 +1,417 @@
|
||||
package srv_commend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
"videoweb/pub/common/enum/redisconst"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/commentlikemod"
|
||||
"videoweb/pub/models/commentmod"
|
||||
"videoweb/pub/models/playletchaptermod"
|
||||
"videoweb/pub/models/playletmod"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
"videoweb/pub/mongos"
|
||||
"videoweb/pub/pubdef"
|
||||
"videoweb/pub/redis"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func SearchComandList(ctx context.Context, param *CommendSearch) (*CommendSearchResp, *srvdef.SrvErr) {
|
||||
|
||||
filter := bson.M{}
|
||||
opt := &options.FindOptions{}
|
||||
|
||||
if param.ObjectId != nil {
|
||||
filter[commentmod.FN.Comment.ObjectId] = *param.ObjectId
|
||||
}
|
||||
if param.ObjectType != nil {
|
||||
filter[commentmod.FN.Comment.ObjectType] = *param.ObjectType
|
||||
}
|
||||
if param.Level != nil {
|
||||
filter[commentmod.FN.Comment.Level] = *param.Level
|
||||
}
|
||||
|
||||
if param.UserName != nil {
|
||||
filter[commentmod.FN.Comment.UserName] = bson.M{"$regex": *param.UserName}
|
||||
}
|
||||
if param.Text != nil {
|
||||
filter[commentmod.FN.Comment.Text] = bson.M{"$regex": *param.Text}
|
||||
}
|
||||
|
||||
if param.ReviewStatus != nil {
|
||||
filter[commentmod.FN.Comment.ReviewStatus] = *param.ReviewStatus
|
||||
}
|
||||
|
||||
sort := bson.D{
|
||||
{commentmod.FN.Comment.TopRank, -1},
|
||||
{commentmod.FN.Comment.CreatedAt, -1},
|
||||
}
|
||||
|
||||
opt.SetSort(sort)
|
||||
skip := (param.PageNum - 1) * param.PageSize
|
||||
opt.SetSkip(skip)
|
||||
opt.SetLimit(param.PageSize)
|
||||
|
||||
total, serr := commentmod.CountComments(ctx, filter)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
if total == 0 {
|
||||
return &CommendSearchResp{}, nil
|
||||
}
|
||||
|
||||
commends, serr := commentmod.FindManyComment(ctx, filter, opt)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
|
||||
resp := &CommendSearchResp{
|
||||
Total: total,
|
||||
List: make([]*PostCommentInfo, 0),
|
||||
}
|
||||
|
||||
for _, commendInfo := range commends {
|
||||
tmp := &PostCommentInfo{
|
||||
Id: commendInfo.Id,
|
||||
ObjectId: commendInfo.ObjectId,
|
||||
ObjectName: commendInfo.ObjectName,
|
||||
UserName: commendInfo.UserName,
|
||||
Level: commendInfo.Level,
|
||||
ParentsId: commendInfo.ParentsId,
|
||||
ReplyUserName: commendInfo.ReplyUserName,
|
||||
Text: commendInfo.Text,
|
||||
ReviewStatus: commendInfo.ReviewStatus,
|
||||
UpdatedAt: commendInfo.UpdatedAt,
|
||||
Ip: commendInfo.Ip,
|
||||
CreatedAt: commendInfo.CreatedAt,
|
||||
ObjectType: commendInfo.ObjectType,
|
||||
Likes: commendInfo.CountLike,
|
||||
}
|
||||
resp.List = append(resp.List, tmp)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// DelCommend 删除一条评论
|
||||
func DelCommend(ctx context.Context, commendId int64) *srvdef.SrvErr {
|
||||
commendInfo, serr := commentmod.FindOneComment(ctx, bson.M{commentmod.FN.Comment.Id: commendId})
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
_, serr = commentmod.DelOneComment(ctx, bson.M{commentmod.FN.Comment.Id: commendId})
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
if serr = commentlikemod.DeleteByCommentIds(ctx, []int64{commendId}); serr != nil {
|
||||
return serr
|
||||
}
|
||||
clearCommentListCacheByComments(commentmod.CommentModelList{commendInfo})
|
||||
|
||||
if err := refreshParentReplyCounts(ctx, commentmod.CommentModelList{commendInfo}); err != nil {
|
||||
return err
|
||||
}
|
||||
return updateCommentCountByComments(ctx, commentmod.CommentModelList{commendInfo})
|
||||
}
|
||||
|
||||
// DelCommend 删除用户所有评论
|
||||
func DelUserCommend(ctx context.Context, ip string) *srvdef.SrvErr {
|
||||
commendList, serr := commentmod.FindManyComment(ctx, bson.M{commentmod.FN.Comment.Ip: ip})
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
_, serr = commentmod.DelManyComment(ctx, bson.M{commentmod.FN.Comment.Ip: ip})
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
if serr = commentlikemod.DeleteByCommentIds(ctx, commentIdsFromModels(commendList)); serr != nil {
|
||||
return serr
|
||||
}
|
||||
clearCommentListCacheByComments(commendList)
|
||||
|
||||
if err := refreshParentReplyCounts(ctx, commendList); err != nil {
|
||||
return err
|
||||
}
|
||||
return updateCommentCountByComments(ctx, commendList)
|
||||
}
|
||||
|
||||
func commentIdsFromModels(comments commentmod.CommentModelList) []int64 {
|
||||
ids := make([]int64, 0, len(comments))
|
||||
for _, comment := range comments {
|
||||
if comment != nil {
|
||||
ids = append(ids, comment.Id)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// AddPostComment 发布评论
|
||||
func AddPostComment(ctx context.Context, req *CommendReplay) *srvdef.SrvErr {
|
||||
now := time.Now()
|
||||
if req.Score > 10 {
|
||||
log.Error("AddPostComment ErrParamError", log.Any("Score", req.Score))
|
||||
return srvdef.New(srvdef.ErrParamError, "", "")
|
||||
}
|
||||
|
||||
// 2. 生成评论相关信息
|
||||
commentInfo := &commentmod.CommentModel{
|
||||
Id: mongos.Lid.Comment.Get(),
|
||||
ObjectId: req.ObjectId,
|
||||
ObjectType: pubdef.CommentObjectTypeVideo,
|
||||
ObjectName: pubdef.CommentObjectTypeName(pubdef.CommentObjectTypeVideo),
|
||||
UserName: req.UserName,
|
||||
ParentsId: req.ParentsId,
|
||||
Text: req.Text,
|
||||
Level: 1,
|
||||
UpdatedAt: now,
|
||||
CreatedAt: now,
|
||||
}
|
||||
|
||||
if commentInfo.ParentsId > 0 {
|
||||
commentInfo.Level = 2
|
||||
} else {
|
||||
commentInfo.Level = 1
|
||||
}
|
||||
commentInfo.ReviewStatus = 1 // 管理端的默认审核通过
|
||||
|
||||
// 3. 插入数据库
|
||||
serr := commentmod.InsertOneComment(ctx, commentInfo)
|
||||
if serr != nil {
|
||||
log.Error("AddPostComment InsertOnePostComment", log.Any("req", req), log.Any("err", serr))
|
||||
return serr
|
||||
}
|
||||
clearCommentListCacheByComments(commentmod.CommentModelList{commentInfo})
|
||||
|
||||
go func() {
|
||||
_ = updateCommentCountByComments(ctx, commentmod.CommentModelList{commentInfo})
|
||||
|
||||
// 4. 修改回复条数
|
||||
if req.ParentsId > 0 {
|
||||
filter := bson.M{
|
||||
commentmod.FN.Comment.Id: req.ParentsId,
|
||||
}
|
||||
updateDoc := bson.M{
|
||||
"$inc": bson.M{
|
||||
commentmod.FN.Comment.CommentCount: 1,
|
||||
},
|
||||
}
|
||||
serr = commentmod.UpdateOneComment(ctx, filter, updateDoc)
|
||||
if serr != nil {
|
||||
log.Error("AddPostComment UpdateOnePostComment", log.Any("req", req), log.Any("err", serr))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// 置顶评论
|
||||
func TopPostComment(ctx context.Context, id int64) *srvdef.SrvErr {
|
||||
filter := bson.M{
|
||||
commentmod.FN.Comment.Id: id,
|
||||
}
|
||||
comment, serr := commentmod.FindOneComment(ctx, filter)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
//获取置顶的一条评论
|
||||
topRankComment, serr := getTopRankComment(ctx, comment.ObjectId)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
if topRankComment.Id == id && topRankComment.TopRank != 0 {
|
||||
return nil
|
||||
}
|
||||
serr = commentmod.UpdateOneComment(ctx, filter, bson.M{
|
||||
"$set": bson.M{
|
||||
commentmod.FN.Comment.TopRank: topRankComment.TopRank + 1,
|
||||
},
|
||||
})
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
clearCommentListCacheByComments(commentmod.CommentModelList{comment})
|
||||
return nil
|
||||
}
|
||||
|
||||
func getTopRankComment(ctx context.Context, objectId int64) (*commentmod.CommentModel, *srvdef.SrvErr) {
|
||||
filter := bson.M{
|
||||
commentmod.FN.Comment.ObjectId: objectId,
|
||||
}
|
||||
opts := options.FindOne().SetSort(bson.M{
|
||||
commentmod.FN.Comment.TopRank: -1,
|
||||
})
|
||||
comment, serr := commentmod.FindOneComment(ctx, filter, opts)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
return comment, nil
|
||||
}
|
||||
|
||||
func ReviewComment(ctx context.Context, id int64, reviewStatus int) *srvdef.SrvErr {
|
||||
comment, err := commentmod.FindOneComment(ctx, bson.M{commentmod.FN.Comment.Id: id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updateDoc := bson.M{
|
||||
commentmod.FN.Comment.ReviewStatus: reviewStatus,
|
||||
}
|
||||
filter := bson.M{
|
||||
commentmod.FN.Comment.Id: id,
|
||||
}
|
||||
serr := commentmod.UpdateOneComment(ctx, filter, bson.M{"$set": updateDoc})
|
||||
if serr != nil {
|
||||
log.Error("审核评论失败", log.Any("id", id), log.Any("reviewStatus", reviewStatus), log.Any("err", serr))
|
||||
return serr
|
||||
}
|
||||
|
||||
clearCommentListCacheByComments(commentmod.CommentModelList{comment})
|
||||
if comment.ReviewStatus != reviewStatus {
|
||||
if err := refreshParentReplyCounts(ctx, commentmod.CommentModelList{comment}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return updateCommentCountByComments(ctx, commentmod.CommentModelList{comment})
|
||||
}
|
||||
|
||||
// ReviewCommendBatch 批量更新
|
||||
func ReviewCommendBatch(ctx context.Context, commendIds *[]int64, reviewStatus int) *srvdef.SrvErr {
|
||||
if commendIds == nil || len(*commendIds) == 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "", "")
|
||||
}
|
||||
commentList, err := commentmod.FindManyComment(ctx, bson.M{
|
||||
commentmod.FN.Comment.Id: bson.M{"$in": *commendIds},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updateDoc := bson.M{
|
||||
commentmod.FN.Comment.ReviewStatus: reviewStatus,
|
||||
}
|
||||
filter := bson.M{commentmod.FN.Comment.Id: bson.M{"$in": commendIds}}
|
||||
serr := commentmod.UpdateManyComment(ctx, filter, bson.M{"$set": updateDoc})
|
||||
if serr != nil {
|
||||
log.Error("批量审核评论失败", log.Any("commendId", *commendIds), log.Any("reviewStatus", reviewStatus), log.Any("err", serr))
|
||||
return serr
|
||||
}
|
||||
|
||||
clearCommentListCacheByComments(commentList)
|
||||
if err := refreshParentReplyCounts(ctx, commentList); err != nil {
|
||||
return err
|
||||
}
|
||||
return updateCommentCountByComments(ctx, commentList)
|
||||
}
|
||||
|
||||
// refreshParentReplyCounts 按已审核的直接回复重算父评论回复数
|
||||
func refreshParentReplyCounts(ctx context.Context, comments commentmod.CommentModelList) *srvdef.SrvErr {
|
||||
seen := make(map[int64]struct{}, len(comments))
|
||||
for _, comment := range comments {
|
||||
if comment == nil || comment.ParentsId <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[comment.ParentsId]; ok {
|
||||
continue
|
||||
}
|
||||
seen[comment.ParentsId] = struct{}{}
|
||||
count, err := commentmod.CountComments(ctx, bson.M{
|
||||
commentmod.FN.Comment.ParentsId: comment.ParentsId,
|
||||
commentmod.FN.Comment.ReviewStatus: 1,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := commentmod.UpdateOneComment(ctx,
|
||||
bson.M{commentmod.FN.Comment.Id: comment.ParentsId},
|
||||
bson.M{"$set": bson.M{commentmod.FN.Comment.CommentCount: count}}); err != nil && err.Code != srvdef.ErrNoData {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// clearCommentListCacheByComments 按对象ID和对象类型精确清理评论列表缓存
|
||||
func clearCommentListCacheByComments(commentList commentmod.CommentModelList) {
|
||||
seen := make(map[string]struct{}, len(commentList))
|
||||
for _, comment := range commentList {
|
||||
if comment == nil {
|
||||
continue
|
||||
}
|
||||
key := commentCacheUniqueKey(comment.ObjectId, comment.ObjectType)
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
cacheKey := redisconst.CommentListCacheKey(comment.ObjectId, comment.ObjectType)
|
||||
if err := redis.Del(cacheKey); err != nil {
|
||||
log.Warn("清理评论列表缓存失败", log.Any("cacheKey", cacheKey), log.E(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// commentCacheUniqueKey 生成对象维度去重key,避免批量审核时重复清理同一对象缓存
|
||||
func commentCacheUniqueKey(objectId int64, objectType pubdef.CommentObjectType) string {
|
||||
return strconv.FormatInt(objectId, 10) + "_" + strconv.FormatInt(int64(objectType), 10)
|
||||
}
|
||||
|
||||
// UpdateCommentCount 评论审核通过/删除后,同步更新对应媒体内容的真实评论数
|
||||
// objectType: 0=视频 4=短剧 11=短剧剧集
|
||||
func UpdateCommentCount(ctx context.Context, commendIds []int64) *srvdef.SrvErr {
|
||||
commentList, _ := commentmod.FindManyComment(ctx, bson.M{
|
||||
commentmod.FN.Comment.Id: bson.M{"$in": commendIds},
|
||||
})
|
||||
return updateCommentCountByComments(ctx, commentList)
|
||||
}
|
||||
|
||||
func updateCommentCountByComments(ctx context.Context, commentList commentmod.CommentModelList) *srvdef.SrvErr {
|
||||
seen := make(map[string]struct{}, len(commentList))
|
||||
for _, comment := range commentList {
|
||||
if comment == nil {
|
||||
continue
|
||||
}
|
||||
key := commentCacheUniqueKey(comment.ObjectId, comment.ObjectType)
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
count, countErr := commentmod.CountComments(ctx, bson.M{
|
||||
commentmod.FN.Comment.ObjectId: comment.ObjectId,
|
||||
commentmod.FN.Comment.ObjectType: comment.ObjectType,
|
||||
commentmod.FN.Comment.ReviewStatus: 1,
|
||||
})
|
||||
if countErr != nil {
|
||||
return countErr
|
||||
}
|
||||
var serr *srvdef.SrvErr
|
||||
switch comment.ObjectType {
|
||||
case pubdef.CommentObjectTypeVideo:
|
||||
serr = videoinformationmod.UpdateOneVideoInformation(ctx,
|
||||
bson.M{videoinformationmod.FN.VideoInformation.Id: comment.ObjectId},
|
||||
bson.M{"$set": bson.M{videoinformationmod.FN.VideoInformation.Real.Comments: count}},
|
||||
)
|
||||
case pubdef.CommentObjectTypePlaylet:
|
||||
serr = playletmod.UpdateOnePlaylet(ctx,
|
||||
bson.M{playletmod.FN.Playlet.Id: comment.ObjectId},
|
||||
bson.M{"$set": bson.M{playletmod.FN.Playlet.CountComment: count}},
|
||||
)
|
||||
case pubdef.CommentObjectTypePlayletChapter:
|
||||
serr = playletchaptermod.UpdateOnePlayletChapter(ctx,
|
||||
bson.M{playletchaptermod.FN.PlayletChapter.Id: comment.ObjectId},
|
||||
bson.M{"$set": bson.M{playletchaptermod.FN.PlayletChapter.CountComment: count}},
|
||||
)
|
||||
}
|
||||
if serr != nil {
|
||||
log.Error("UpdateCommentCount 更新评论数失败",
|
||||
log.Any("objectId", comment.ObjectId),
|
||||
log.Any("objectType", comment.ObjectType),
|
||||
log.Any("err", serr),
|
||||
)
|
||||
return serr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package srv_commend
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
// CommendSearch 查询评论
|
||||
type CommendSearch struct {
|
||||
ObjectId *int64 `json:"objectId" bson:"objectId"` // 对象ID
|
||||
ObjectType *pubdef.CommentObjectType `json:"objectType" bson:"objectType"` // 对象类型 0=视频 4=短剧 11=短剧剧集
|
||||
UserName *string `json:"userName" bson:"userName"` // 用户名称
|
||||
Level *int64 `json:"level" bson:"level"` // 评论级别
|
||||
Text *string `json:"text" bson:"text"` // 评论内容
|
||||
ReviewStatus *int `json:"reviewStatus"`
|
||||
PageNum int64 `json:"pageNum"`
|
||||
PageSize int64 `json:"pageSize"`
|
||||
}
|
||||
|
||||
type PostCommentInfo struct {
|
||||
Id int64 `json:"id" bson:"_id,omitempty"` // 评论ID
|
||||
Ip string `json:"ip" bson:"ip"` // 评论IP
|
||||
ObjectType pubdef.CommentObjectType `json:"objectType" bson:"objectType"` // 对象类型 0=视频 4=短剧 11=短剧剧集
|
||||
ObjectId int64 `json:"objectId" bson:"objectId"` // 对象ID
|
||||
ObjectName string `json:"objectName" bson:"objectName"` // 对象类型名称 视频/短剧
|
||||
UserName string `json:"userName" bson:"userName"` // 用户名称
|
||||
Level int64 `json:"level" bson:"level"` // 评论级别
|
||||
ParentsId int64 `json:"parentsId" bson:"parentsId"` // 上级评论ID
|
||||
ReplyUserName string `json:"replyUserName" bson:"replyUserName"` // 回复名称
|
||||
Text string `json:"text" bson:"text"` // 评论内容
|
||||
ReviewStatus int `json:"reviewStatus" bson:"reviewStatus"` // 审核状态 0 未审核 1审核成功 2 审核失败
|
||||
UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
|
||||
CreatedAt time.Time `bson:"createdAt" json:"createdAt"` // 创建时间
|
||||
TopRank int64 `json:"topRank" bson:"topRank"` // 置顶排序
|
||||
Likes int64 `json:"likes" bson:"likes"` // 点赞数
|
||||
Status pubdef.UserStatus `json:"status" bson:"status"` // 评论用户的状态
|
||||
}
|
||||
|
||||
type CommendSearchResp struct {
|
||||
Total int64 `json:"total"`
|
||||
List []*PostCommentInfo `json:"list"`
|
||||
}
|
||||
|
||||
type CommendDelete struct {
|
||||
Id *int64 `json:"id" bson:"_id" binding:"required"` // Id
|
||||
}
|
||||
|
||||
type CommendReplay struct {
|
||||
ObjectId int64 `json:"objectId"` // 对象ID
|
||||
UserName string `json:"userName"` // 用户名
|
||||
ParentsId int64 `json:"parentsId"` // 评论ID
|
||||
Text string `json:"text"` // 评论内容
|
||||
Score int64 `json:"score"` // 评分
|
||||
}
|
||||
|
||||
type CommendUpdate struct {
|
||||
Score *int64 `json:"score" bson:"score,omitempty"` // 评分
|
||||
Likes *int64 `json:"likes" bson:"likes,omitempty"` // 点赞数量
|
||||
}
|
||||
|
||||
type CommendUpdateReq struct {
|
||||
Param CommendUpdate `json:"param" bson:"param"` // 参数
|
||||
Id int64 `json:"id" bson:"_id,omitempty"` // 评论ID
|
||||
}
|
||||
|
||||
type CommendTopReq struct {
|
||||
Id int64 `json:"id" bson:"_id,omitempty"` // 评论ID
|
||||
}
|
||||
|
||||
type CommendReviewReq struct {
|
||||
Id int64 `json:"id"`
|
||||
ReviewStatus int `json:"reviewStatus"`
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package srv_contactus
|
||||
|
||||
import (
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/contactusmod"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func GetContactusList(ctx *gin.Context, req *GetContactusListReq) (resp *GetContactusListResp, err *srvdef.SrvErr) {
|
||||
filter := bson.M{}
|
||||
if req.CategoryName != nil && len(*req.CategoryName) > 0 {
|
||||
filter[contactusmod.FN.ContactUsModel.CategoryName] = *req.CategoryName
|
||||
}
|
||||
if req.Ip != nil && len(*req.Ip) > 0 {
|
||||
filter[contactusmod.FN.ContactUsModel.Ip] = *req.Ip
|
||||
}
|
||||
if req.Name != nil && len(*req.Name) > 0 {
|
||||
filter[contactusmod.FN.ContactUsModel.Name] = *req.Name
|
||||
}
|
||||
if req.Email != nil && len(*req.Email) > 0 {
|
||||
filter[contactusmod.FN.ContactUsModel.Email] = *req.Email
|
||||
}
|
||||
var total int64
|
||||
total, err = contactusmod.CountContactUsModels(ctx, filter)
|
||||
if err != nil {
|
||||
log.Warn("GetContactusList.CountContactUsModels", log.Any("filter", filter), log.E(err))
|
||||
return
|
||||
}
|
||||
resp = &GetContactusListResp{
|
||||
Total: 0,
|
||||
List: make(contactusmod.ContactUsModelList, 0),
|
||||
}
|
||||
if total == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
opts := &options.FindOptions{}
|
||||
opts.SetSort(bson.M{contactusmod.FN.ContactUsModel.CreatedAt: -1})
|
||||
opts.SetLimit(req.PageSize).SetSkip((req.PageNum - 1) * req.PageSize)
|
||||
list, err := contactusmod.FindManyContactUsModel(ctx, filter, opts)
|
||||
if err != nil {
|
||||
log.Warn("GetContactusList.FindManyContactUsModels", log.Any("filter", filter), log.E(err))
|
||||
return
|
||||
}
|
||||
resp.List = list
|
||||
return resp, nil
|
||||
}
|
||||
func DelContactus(ctx *gin.Context, id primitive.ObjectID) *srvdef.SrvErr {
|
||||
if id.IsZero() {
|
||||
return srvdef.New(srvdef.ErrParamError)
|
||||
}
|
||||
_, err := contactusmod.DelOneContactUsModel(ctx, bson.M{contactusmod.FN.ContactUsModel.Id: id})
|
||||
return err
|
||||
}
|
||||
|
||||
func UpdateContactus(ctx *gin.Context, id primitive.ObjectID, status int64) *srvdef.SrvErr {
|
||||
if id.IsZero() {
|
||||
return srvdef.New(srvdef.ErrParamError)
|
||||
}
|
||||
err := contactusmod.UpdateOneContactUsModel(ctx, bson.M{contactusmod.FN.ContactUsModel.Id: id}, bson.M{
|
||||
"$set": bson.M{contactusmod.FN.ContactUsModel.Status: status},
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package srv_contactus
|
||||
|
||||
import (
|
||||
"videoweb/pub/models/contactusmod"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type GetContactusListReq struct {
|
||||
pubdef.Page
|
||||
Ip *string `form:"ip" json:"ip"` // ip
|
||||
Name *string `form:"name" json:"name"` // 姓名
|
||||
Email *string `form:"email" json:"email"` // 邮箱
|
||||
CategoryName *string `form:"categoryName" json:"categoryName"` // 分类
|
||||
}
|
||||
|
||||
type GetContactusListResp struct {
|
||||
Total int64 `json:"total"`
|
||||
List contactusmod.ContactUsModelList `json:"list"`
|
||||
}
|
||||
|
||||
type DelOneContactusReq struct {
|
||||
Id primitive.ObjectID `form:"id" json:"id"`
|
||||
}
|
||||
|
||||
type UpdateContactusReq struct {
|
||||
Id primitive.ObjectID `form:"id" json:"id"`
|
||||
Status int64 `form:"status" json:"status"`
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
package srv_contentexcel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const queryBatchSize = 500
|
||||
|
||||
var importAddedTimeStart = time.Date(2025, 1, 1, 0, 0, 0, 0, time.Local)
|
||||
|
||||
func randomImportAddedTime(r *rand.Rand) time.Time {
|
||||
now := time.Now()
|
||||
if !now.After(importAddedTimeStart) {
|
||||
return now
|
||||
}
|
||||
if r == nil {
|
||||
r = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
}
|
||||
delta := now.UnixNano() - importAddedTimeStart.UnixNano()
|
||||
if delta <= 0 {
|
||||
return now
|
||||
}
|
||||
return importAddedTimeStart.Add(time.Duration(r.Int63n(delta + 1)))
|
||||
}
|
||||
|
||||
// Export 按条件导出视频 SEO 字段为 Excel。
|
||||
func Export(ctx context.Context, req *ExportReq) ([]byte, string, *srvdef.SrvErr) {
|
||||
contentType, serr := normalizeContentType(req.Type)
|
||||
if serr != nil {
|
||||
return nil, "", serr
|
||||
}
|
||||
|
||||
dateFilter, serr := buildTimeRangeFilter(req.StartTime, req.EndTime)
|
||||
if serr != nil {
|
||||
return nil, "", serr
|
||||
}
|
||||
|
||||
ids, serr := normalizeExportIDs(req.Ids)
|
||||
if serr != nil {
|
||||
return nil, "", serr
|
||||
}
|
||||
|
||||
var rows []ContentRow
|
||||
var err error
|
||||
switch contentType {
|
||||
case ContentTypeVideo:
|
||||
rows, err = queryVideoRows(ctx, dateFilter, req.Status, ids)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, "", srvdef.New(srvdef.ExcelExportErr, err.Error())
|
||||
}
|
||||
|
||||
data, err := buildWorkbook(rows)
|
||||
if err != nil {
|
||||
return nil, "", srvdef.New(srvdef.ExcelExportErr, err.Error())
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("%s_content_%s.xlsx", contentType, time.Now().Format("20060102150405"))
|
||||
return data, filename, nil
|
||||
}
|
||||
|
||||
// Import 读取 Excel 并按 ID 批量更新标题、描述、关键词。
|
||||
func Import(ctx context.Context, contentType string, reader io.Reader) (*ImportResult, *srvdef.SrvErr) {
|
||||
normalizedType, serr := normalizeContentType(contentType)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "读取上传文件失败")
|
||||
}
|
||||
|
||||
rows, err := parseWorkbook(data)
|
||||
if err != nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, err.Error())
|
||||
}
|
||||
|
||||
result := &ImportResult{Total: len(rows)}
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
for _, row := range rows {
|
||||
var updateErr *srvdef.SrvErr
|
||||
switch normalizedType {
|
||||
case ContentTypeVideo:
|
||||
updateErr = updateVideoRow(ctx, row, r)
|
||||
}
|
||||
if updateErr != nil {
|
||||
result.Failed++
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("ID=%d: %s", row.ID, updateErr.Tips))
|
||||
continue
|
||||
}
|
||||
result.Success++
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func normalizeContentType(raw string) (string, *srvdef.SrvErr) {
|
||||
switch stringsTrimLower(raw) {
|
||||
case ContentTypeVideo:
|
||||
return stringsTrimLower(raw), nil
|
||||
default:
|
||||
return "", srvdef.New(srvdef.ErrParamError, "type 仅支持 video")
|
||||
}
|
||||
}
|
||||
|
||||
func stringsTrimLower(s string) string {
|
||||
return strings.ToLower(strings.TrimSpace(s))
|
||||
}
|
||||
|
||||
func buildTimeRangeFilter(startTime, endTime string) (bson.M, *srvdef.SrvErr) {
|
||||
filter := bson.M{}
|
||||
start, err := parseOptionalTime(startTime, false)
|
||||
if err != nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "startTime 格式错误,支持 2006-01-02 或 2006-01-02 15:04:05")
|
||||
}
|
||||
end, err := parseOptionalTime(endTime, true)
|
||||
if err != nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "endTime 格式错误,支持 2006-01-02 或 2006-01-02 15:04:05")
|
||||
}
|
||||
if start != nil && end != nil && start.After(*end) {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "startTime 不能晚于 endTime")
|
||||
}
|
||||
if start != nil {
|
||||
filter["$gte"] = *start
|
||||
}
|
||||
if end != nil {
|
||||
filter["$lte"] = *end
|
||||
}
|
||||
if len(filter) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
func parseOptionalTime(raw string, endOfDay bool) (*time.Time, error) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return nil, nil
|
||||
}
|
||||
layouts := []string{
|
||||
"2006-01-02 15:04:05",
|
||||
"2006-01-02T15:04:05",
|
||||
time.RFC3339,
|
||||
}
|
||||
for _, layout := range layouts {
|
||||
if t, err := time.ParseInLocation(layout, raw, time.Local); err == nil {
|
||||
return &t, nil
|
||||
}
|
||||
}
|
||||
if t, err := time.ParseInLocation("2006-01-02", raw, time.Local); err == nil {
|
||||
if endOfDay {
|
||||
end := time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 0, t.Location())
|
||||
return &end, nil
|
||||
}
|
||||
return &t, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid time: %s", raw)
|
||||
}
|
||||
|
||||
func applyTimeFilter(base bson.M, field string, rangeFilter bson.M) bson.M {
|
||||
if len(rangeFilter) == 0 {
|
||||
return base
|
||||
}
|
||||
base[field] = rangeFilter
|
||||
return base
|
||||
}
|
||||
|
||||
func applyStatusFilter(base bson.M, field string, status *pubdef.InformationStatus) bson.M {
|
||||
if status == nil {
|
||||
return base
|
||||
}
|
||||
base[field] = *status
|
||||
return base
|
||||
}
|
||||
|
||||
func applyIDsFilter(base bson.M, field string, ids []int64) bson.M {
|
||||
if len(ids) == 0 {
|
||||
return base
|
||||
}
|
||||
base[field] = bson.M{"$in": ids}
|
||||
return base
|
||||
}
|
||||
|
||||
// ParseExportIDs 解析导出接口的 ids 查询参数,支持 ids=1&ids=2 或 ids=1,2,3。
|
||||
func ParseExportIDs(raw []string) ([]int64, *srvdef.SrvErr) {
|
||||
if len(raw) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ids := make([]int64, 0, len(raw))
|
||||
seen := make(map[int64]struct{}, len(raw))
|
||||
for _, item := range raw {
|
||||
item = strings.TrimSpace(item)
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
for _, part := range strings.Split(item, ",") {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
id, err := strconv.ParseInt(part, 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "ids 格式错误,需为正整数")
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func normalizeExportIDs(raw []int64) ([]int64, *srvdef.SrvErr) {
|
||||
if len(raw) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ids := make([]int64, 0, len(raw))
|
||||
seen := make(map[int64]struct{}, len(raw))
|
||||
for _, id := range raw {
|
||||
if id <= 0 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "ids 格式错误,需为正整数")
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func queryVideoRows(ctx context.Context, dateFilter bson.M, status *pubdef.InformationStatus, ids []int64) ([]ContentRow, error) {
|
||||
filter := applyIDsFilter(applyStatusFilter(applyTimeFilter(bson.M{
|
||||
videoinformationmod.FN.VideoInformation.MediaType: bson.M{
|
||||
"$in": []videoinformationmod.MediaType{
|
||||
videoinformationmod.MediaType_Video,
|
||||
videoinformationmod.MediaType_AV,
|
||||
},
|
||||
},
|
||||
}, videoinformationmod.FN.VideoInformation.CreatedAt, dateFilter),
|
||||
videoinformationmod.FN.VideoInformation.Status, status),
|
||||
videoinformationmod.FN.VideoInformation.Id, ids)
|
||||
|
||||
return queryRowsInBatches(ctx, func(skip int64) ([]ContentRow, bool, error) {
|
||||
opt := options.Find().
|
||||
SetSkip(skip).
|
||||
SetLimit(queryBatchSize).
|
||||
SetSort(bson.M{videoinformationmod.FN.VideoInformation.Id: 1}).
|
||||
SetProjection(bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Id: 1,
|
||||
videoinformationmod.FN.VideoInformation.Title: 1,
|
||||
videoinformationmod.FN.VideoInformation.Desc: 1,
|
||||
videoinformationmod.FN.VideoInformation.Keyworld: 1,
|
||||
})
|
||||
|
||||
list, serr := videoinformationmod.FindManyVideoInformations(ctx, filter, opt)
|
||||
if serr != nil {
|
||||
return nil, false, fmt.Errorf("%s", serr.Tips)
|
||||
}
|
||||
rows := make([]ContentRow, 0, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
rows = append(rows, ContentRow{
|
||||
ID: item.Id,
|
||||
Title: item.Title,
|
||||
Desc: item.Desc,
|
||||
Keyworld: item.Keyworld,
|
||||
})
|
||||
}
|
||||
return rows, int64(len(list)) == queryBatchSize, nil
|
||||
})
|
||||
}
|
||||
|
||||
func queryRowsInBatches(ctx context.Context, fetch func(skip int64) ([]ContentRow, bool, error)) ([]ContentRow, error) {
|
||||
all := make([]ContentRow, 0)
|
||||
var skip int64
|
||||
for {
|
||||
rows, hasMore, err := fetch(skip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
all = append(all, rows...)
|
||||
if !hasMore {
|
||||
break
|
||||
}
|
||||
skip += queryBatchSize
|
||||
}
|
||||
return all, nil
|
||||
}
|
||||
|
||||
func updateVideoRow(ctx context.Context, row ContentRow, r *rand.Rand) *srvdef.SrvErr {
|
||||
now := time.Now()
|
||||
addedTime := randomImportAddedTime(r)
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Title: row.Title,
|
||||
videoinformationmod.FN.VideoInformation.Desc: row.Desc,
|
||||
videoinformationmod.FN.VideoInformation.Keyworld: row.Keyworld,
|
||||
videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
|
||||
videoinformationmod.FN.VideoInformation.AddedTime: addedTime,
|
||||
videoinformationmod.FN.VideoInformation.UpdatedAt: now,
|
||||
},
|
||||
}
|
||||
return videoinformationmod.UpdateOneVideoInformation(ctx, bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Id: row.ID,
|
||||
}, update)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package srv_contentexcel
|
||||
|
||||
import "videoweb/pub/pubdef"
|
||||
|
||||
const (
|
||||
ContentTypeVideo = "video"
|
||||
)
|
||||
|
||||
// ExportReq 导出查询参数。
|
||||
type ExportReq struct {
|
||||
Type string `form:"type" binding:"required"` // video
|
||||
StartTime string `form:"startTime"` // 可选,按 createdAt 筛选,格式 2006-01-02 或 2006-01-02 15:04:05
|
||||
EndTime string `form:"endTime"` // 可选,按 createdAt 筛选
|
||||
Status *pubdef.InformationStatus `form:"status"` // 可选,0初始化 1上架 2下架,不传查全部
|
||||
Ids []int64 // 可选,按 ID 筛选,支持 ids=1&ids=2 或 ids=1,2,3
|
||||
}
|
||||
|
||||
// ImportResult 导入结果。
|
||||
type ImportResult struct {
|
||||
Total int `json:"total"`
|
||||
Success int `json:"success"`
|
||||
Failed int `json:"failed"`
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
// ContentRow Excel 单行数据。
|
||||
type ContentRow struct {
|
||||
ID int64
|
||||
Title string
|
||||
Desc string
|
||||
Keyworld []string
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package srv_contentexcel
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
colID = "ID"
|
||||
colTitle = "标题"
|
||||
colDesc = "描述"
|
||||
colKeyworld = "关键词"
|
||||
colRedLineRemark = "备注(红线-建议下架)"
|
||||
sheetName = "Sheet1"
|
||||
excelHeaders = 4
|
||||
colRedLineRemarkIdx = 4
|
||||
)
|
||||
|
||||
var headerAliases = map[string]int{
|
||||
"id": 0, colID: 0,
|
||||
"title": 1, colTitle: 1,
|
||||
"desc": 2, "description": 2, colDesc: 2,
|
||||
"keyworld": 3, "keywords": 3, "keyword": 3, colKeyworld: 3,
|
||||
colRedLineRemark: colRedLineRemarkIdx,
|
||||
}
|
||||
|
||||
func buildWorkbook(rows []ContentRow) ([]byte, error) {
|
||||
f := excelize.NewFile()
|
||||
defer f.Close()
|
||||
|
||||
if err := f.SetSheetRow(sheetName, "A1", &[]string{colID, colTitle, colDesc, colKeyworld}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, row := range rows {
|
||||
cell, err := excelize.CoordinatesToCellName(1, i+2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := f.SetSheetRow(sheetName, cell, &[]any{
|
||||
row.ID,
|
||||
row.Title,
|
||||
row.Desc,
|
||||
strings.Join(row.Keyworld, ","),
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := f.Write(&buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func parseWorkbook(data []byte) ([]ContentRow, error) {
|
||||
f, err := excelize.OpenReader(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("打开 Excel 失败: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
sheets := f.GetSheetList()
|
||||
if len(sheets) == 0 {
|
||||
return nil, fmt.Errorf("Excel 无工作表")
|
||||
}
|
||||
|
||||
rows, err := f.GetRows(sheets[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取 Excel 失败: %w", err)
|
||||
}
|
||||
if len(rows) <= 1 {
|
||||
return nil, fmt.Errorf("Excel 无数据行")
|
||||
}
|
||||
|
||||
colIndex := mapHeaderColumns(rows[0])
|
||||
if len(colIndex) < excelHeaders {
|
||||
return nil, fmt.Errorf("Excel 表头需包含: ID、标题、描述、关键词")
|
||||
}
|
||||
|
||||
result := make([]ContentRow, 0, len(rows)-1)
|
||||
for lineNo, row := range rows[1:] {
|
||||
contentRow, ok, err := parseDataRow(row, colIndex, lineNo+2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
result = append(result, contentRow)
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return nil, fmt.Errorf("Excel 无有效数据行")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func mapHeaderColumns(header []string) map[int]int {
|
||||
index := make(map[int]int, excelHeaders)
|
||||
for i, name := range header {
|
||||
key := strings.ToLower(strings.TrimSpace(name))
|
||||
if col, ok := headerAliases[key]; ok {
|
||||
index[col] = i
|
||||
}
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
func parseDataRow(row []string, colIndex map[int]int, lineNo int) (ContentRow, bool, error) {
|
||||
if remarkIdx, ok := colIndex[colRedLineRemarkIdx]; ok {
|
||||
if cellValue(row, remarkIdx) != "" {
|
||||
return ContentRow{}, false, nil
|
||||
}
|
||||
}
|
||||
|
||||
idStr := cellValue(row, colIndex[0])
|
||||
if idStr == "" {
|
||||
return ContentRow{}, false, nil
|
||||
}
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
return ContentRow{}, false, fmt.Errorf("第 %d 行 ID 无效: %q", lineNo, idStr)
|
||||
}
|
||||
|
||||
return ContentRow{
|
||||
ID: id,
|
||||
Title: cellValue(row, colIndex[1]),
|
||||
Desc: cellValue(row, colIndex[2]),
|
||||
Keyworld: parseKeywords(cellValue(row, colIndex[3])),
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
func cellValue(row []string, index int) string {
|
||||
if index < 0 || index >= len(row) {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(row[index])
|
||||
}
|
||||
|
||||
func parseKeywords(raw string) []string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
raw = strings.NewReplacer(",", ",", ";", ",", ";", ",").Replace(raw)
|
||||
parts := strings.Split(raw, ",")
|
||||
keywords := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part != "" {
|
||||
keywords = append(keywords, part)
|
||||
}
|
||||
}
|
||||
return keywords
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package srv_contentexcel
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
func TestBuildTimeRangeFilter(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
filter, err := buildTimeRangeFilter("2026-01-01", "2026-01-31")
|
||||
if err != nil {
|
||||
t.Fatalf("buildTimeRangeFilter() err = %v", err)
|
||||
}
|
||||
if filter["$gte"] == nil || filter["$lte"] == nil {
|
||||
t.Fatalf("buildTimeRangeFilter() = %#v, want range filter", filter)
|
||||
}
|
||||
|
||||
empty, err := buildTimeRangeFilter("", "")
|
||||
if err != nil {
|
||||
t.Fatalf("buildTimeRangeFilter() empty err = %v", err)
|
||||
}
|
||||
if len(empty) != 0 {
|
||||
t.Fatalf("buildTimeRangeFilter() empty = %#v, want nil/empty", empty)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyStatusFilter(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
base := bson.M{"a": 1}
|
||||
if got := applyStatusFilter(base, "status", nil); len(got) != 1 {
|
||||
t.Fatalf("nil status should not add filter: %#v", got)
|
||||
}
|
||||
|
||||
status := pubdef.InformationStatusVerifyOK
|
||||
got := applyStatusFilter(bson.M{}, videoinformationmod.FN.VideoInformation.Status, &status)
|
||||
if got[videoinformationmod.FN.VideoInformation.Status] != pubdef.InformationStatusVerifyOK {
|
||||
t.Fatalf("applyStatusFilter() status = %#v", got[videoinformationmod.FN.VideoInformation.Status])
|
||||
}
|
||||
|
||||
start := time.Date(2026, 1, 1, 0, 0, 0, 0, time.Local)
|
||||
end := time.Date(2026, 1, 31, 23, 59, 59, 0, time.Local)
|
||||
dateFilter := bson.M{"$gte": start, "$lte": end}
|
||||
videoFilter := applyIDsFilter(applyStatusFilter(
|
||||
applyTimeFilter(bson.M{}, videoinformationmod.FN.VideoInformation.CreatedAt, dateFilter),
|
||||
videoinformationmod.FN.VideoInformation.Status, &status,
|
||||
), videoinformationmod.FN.VideoInformation.Id, []int64{1001, 1002})
|
||||
if videoFilter[videoinformationmod.FN.VideoInformation.CreatedAt] == nil {
|
||||
t.Fatal("expected createdAt filter")
|
||||
}
|
||||
if videoFilter[videoinformationmod.FN.VideoInformation.Status] != pubdef.InformationStatusVerifyOK {
|
||||
t.Fatal("expected status filter")
|
||||
}
|
||||
if videoFilter[videoinformationmod.FN.VideoInformation.Id] == nil {
|
||||
t.Fatal("expected id filter")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseExportIDs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ids, err := ParseExportIDs([]string{"1001,1002", "1003"})
|
||||
if err != nil {
|
||||
t.Fatalf("ParseExportIDs() err = %v", err)
|
||||
}
|
||||
if len(ids) != 3 || ids[0] != 1001 || ids[1] != 1002 || ids[2] != 1003 {
|
||||
t.Fatalf("ParseExportIDs() = %v", ids)
|
||||
}
|
||||
|
||||
dup, err := ParseExportIDs([]string{"1001", "1001,1002"})
|
||||
if err != nil {
|
||||
t.Fatalf("ParseExportIDs() dup err = %v", err)
|
||||
}
|
||||
if len(dup) != 2 {
|
||||
t.Fatalf("ParseExportIDs() dup = %v, want 2 unique ids", dup)
|
||||
}
|
||||
|
||||
if _, err := ParseExportIDs([]string{"abc"}); err == nil {
|
||||
t.Fatal("ParseExportIDs() invalid id should fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseKeywords(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := parseKeywords("a,b, c;d")
|
||||
want := []string{"a", "b", "c", "d"}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("parseKeywords() = %v, want %v", got, want)
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("parseKeywords()[%d] = %q, want %q", i, got[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapHeaderColumns(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
index := mapHeaderColumns([]string{"ID", "标题", "描述", "关键词"})
|
||||
for i := 0; i < excelHeaders; i++ {
|
||||
if _, ok := index[i]; !ok {
|
||||
t.Fatalf("missing column index %d", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseWorkbook(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rows := []ContentRow{{
|
||||
ID: 1001,
|
||||
Title: "标题A",
|
||||
Desc: "描述A",
|
||||
Keyworld: []string{"k1", "k2"},
|
||||
}}
|
||||
data, err := buildWorkbook(rows)
|
||||
if err != nil {
|
||||
t.Fatalf("buildWorkbook() err = %v", err)
|
||||
}
|
||||
|
||||
parsed, err := parseWorkbook(data)
|
||||
if err != nil {
|
||||
t.Fatalf("parseWorkbook() err = %v", err)
|
||||
}
|
||||
if len(parsed) != 1 {
|
||||
t.Fatalf("parseWorkbook() len = %d, want 1", len(parsed))
|
||||
}
|
||||
if parsed[0].ID != 1001 || parsed[0].Title != "标题A" {
|
||||
t.Fatalf("parseWorkbook() = %+v", parsed[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseWorkbookSkipRedLineRemark(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := excelize.NewFile()
|
||||
defer f.Close()
|
||||
|
||||
if err := f.SetSheetRow(sheetName, "A1", &[]string{colID, colTitle, colDesc, colKeyworld, colRedLineRemark}); err != nil {
|
||||
t.Fatalf("SetSheetRow header err = %v", err)
|
||||
}
|
||||
if err := f.SetSheetRow(sheetName, "A2", &[]any{1001, "标题A", "描述A", "k1", "建议下架"}); err != nil {
|
||||
t.Fatalf("SetSheetRow row1 err = %v", err)
|
||||
}
|
||||
if err := f.SetSheetRow(sheetName, "A3", &[]any{1002, "标题B", "描述B", "k2", ""}); err != nil {
|
||||
t.Fatalf("SetSheetRow row2 err = %v", err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := f.Write(&buf); err != nil {
|
||||
t.Fatalf("Write() err = %v", err)
|
||||
}
|
||||
|
||||
parsed, err := parseWorkbook(buf.Bytes())
|
||||
if err != nil {
|
||||
t.Fatalf("parseWorkbook() err = %v", err)
|
||||
}
|
||||
if len(parsed) != 1 {
|
||||
t.Fatalf("parseWorkbook() len = %d, want 1", len(parsed))
|
||||
}
|
||||
if parsed[0].ID != 1002 {
|
||||
t.Fatalf("parseWorkbook() ID = %d, want 1002", parsed[0].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandomImportAddedTime(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := rand.New(rand.NewSource(1))
|
||||
for i := 0; i < 20; i++ {
|
||||
got := randomImportAddedTime(r)
|
||||
if got.Before(importAddedTimeStart) {
|
||||
t.Fatalf("randomImportAddedTime() = %v, before %v", got, importAddedTimeStart)
|
||||
}
|
||||
if got.After(time.Now()) {
|
||||
t.Fatalf("randomImportAddedTime() = %v, after now", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeContentType(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if _, err := normalizeContentType("video"); err != nil {
|
||||
t.Fatalf("video should be valid: %v", err)
|
||||
}
|
||||
if _, err := normalizeContentType("post"); err == nil {
|
||||
t.Fatal("post should be invalid")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,592 @@
|
||||
package srv_contenttag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/response"
|
||||
"videoweb/pub/common/slugx"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/contenttagmod"
|
||||
"videoweb/pub/models/mwacc"
|
||||
"videoweb/pub/pubdef"
|
||||
"videoweb/web/api/proto"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultContentTagPageSize int64 = 10
|
||||
maxContentTagPageSize int64 = 100
|
||||
maxContentTagOptionSize int64 = 200
|
||||
)
|
||||
|
||||
var contentTagWebProjection = bson.D{
|
||||
{Key: contenttagmod.FN.ContentTag.Id, Value: 1},
|
||||
{Key: contenttagmod.FN.ContentTag.Name, Value: 1},
|
||||
{Key: contenttagmod.FN.ContentTag.Slug, Value: 1},
|
||||
{Key: contenttagmod.FN.ContentTag.Types, Value: 1},
|
||||
{Key: contenttagmod.FN.ContentTag.Status, Value: 1},
|
||||
{Key: contenttagmod.FN.ContentTag.Rank, Value: 1},
|
||||
{Key: contenttagmod.FN.ContentTag.VideoCount, Value: 1},
|
||||
{Key: contenttagmod.FN.ContentTag.PlayletCount, Value: 1},
|
||||
{Key: contenttagmod.FN.ContentTag.SeoInfo.Title, Value: 1},
|
||||
{Key: contenttagmod.FN.ContentTag.SeoInfo.Description, Value: 1},
|
||||
{Key: contenttagmod.FN.ContentTag.SeoInfo.Keyworld, Value: 1},
|
||||
{Key: contenttagmod.FN.ContentTag.CreatedById, Value: 1},
|
||||
{Key: contenttagmod.FN.ContentTag.CreatedByName, Value: 1},
|
||||
{Key: contenttagmod.FN.ContentTag.UpdatedByName, Value: 1},
|
||||
{Key: contenttagmod.FN.ContentTag.CreatedAt, Value: 1},
|
||||
{Key: contenttagmod.FN.ContentTag.UpdatedAt, Value: 1},
|
||||
}
|
||||
|
||||
// ContentTagBindChange 保存一次内容标签变更前后的标签ID集合
|
||||
type ContentTagBindChange struct {
|
||||
OldTagIds []int64
|
||||
NewTagIds []int64
|
||||
}
|
||||
|
||||
// ListContentTag 分页查询统一标签列表
|
||||
func ListContentTag(ctx context.Context, req *proto.ContentTagListReq) (*proto.ContentTagListPageResp, *srvdef.SrvErr) {
|
||||
if req == nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||||
}
|
||||
filter := bson.M{}
|
||||
if req.Id != nil && *req.Id > 0 {
|
||||
filter[contenttagmod.FN.ContentTag.Id] = *req.Id
|
||||
}
|
||||
if req.Status != nil {
|
||||
filter[contenttagmod.FN.ContentTag.Status] = *req.Status
|
||||
}
|
||||
if req.Type != nil {
|
||||
if !isValidContentTagType(*req.Type) {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "标签类型无效", "")
|
||||
}
|
||||
filter[contenttagmod.FN.ContentTag.Types] = *req.Type
|
||||
}
|
||||
if name := strings.TrimSpace(req.Name); name != "" {
|
||||
filter[contenttagmod.FN.ContentTag.Name] = bson.M{"$regex": regexp.QuoteMeta(name)}
|
||||
}
|
||||
pageNum, pageSize := normalizeContentTagPage(req.PageNum, req.PageSize)
|
||||
totalCount, err := contenttagmod.CountContentTag(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := &proto.ContentTagListPageResp{
|
||||
BasePageResp: response.BasePageResp{
|
||||
TotalCount: totalCount,
|
||||
PageSize: int(pageSize),
|
||||
PageNum: int(pageNum),
|
||||
List: make([]*proto.ContentTagListResp, 0),
|
||||
},
|
||||
}
|
||||
if totalCount == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
list, err := contenttagmod.FindManyContentTag(ctx, filter, options.Find().
|
||||
SetProjection(contentTagWebProjection).
|
||||
SetSort(bson.D{
|
||||
{Key: contenttagmod.FN.ContentTag.Rank, Value: -1},
|
||||
{Key: contenttagmod.FN.ContentTag.UpdatedAt, Value: -1},
|
||||
{Key: contenttagmod.FN.ContentTag.Id, Value: -1},
|
||||
}).
|
||||
SetSkip((pageNum-1)*pageSize).
|
||||
SetLimit(pageSize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]*proto.ContentTagListResp, 0, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, contentTagListItem(item))
|
||||
}
|
||||
resp.List = items
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// AddContentTag 新增统一标签
|
||||
func AddContentTag(ctx context.Context, req *proto.ContentTagAddReq, operatorId int64) (*proto.ContentTagAddResp, *srvdef.SrvErr) {
|
||||
if req == nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||||
}
|
||||
name := strings.TrimSpace(req.Name)
|
||||
if name == "" {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "标签名称不能为空", "")
|
||||
}
|
||||
if req.Status == nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "状态不能为空", "")
|
||||
}
|
||||
types, err := normalizeContentTagTypes(req.Types)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
operatorId, operatorName := resolveContentTagOperator(ctx, operatorId)
|
||||
doc := &contenttagmod.ContentTag{
|
||||
Name: name,
|
||||
Slug: req.Slug,
|
||||
Types: types,
|
||||
Status: *req.Status,
|
||||
Rank: req.Rank,
|
||||
VideoCount: 0,
|
||||
PlayletCount: 0,
|
||||
SeoInfo: normalizeContentTagSeoValue(req.SeoInfo),
|
||||
CreatedById: operatorId,
|
||||
CreatedByName: operatorName,
|
||||
UpdatedByName: operatorName,
|
||||
}
|
||||
exists := func(s string) bool {
|
||||
item, _ := contenttagmod.FindOneContentTag(ctx, slugx.Filter(
|
||||
contenttagmod.FN.ContentTag.Slug, contenttagmod.FN.ContentTag.Id, s, 0))
|
||||
return item != nil
|
||||
}
|
||||
if serr := slugx.Ensure(&doc.Slug, utils.SlugSourceFromKeywordsOrTitle(doc.SeoInfo.Keyworld, name), exists); serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
if err := contenttagmod.InsertOneContentTag(ctx, doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &proto.ContentTagAddResp{Id: doc.Id}, nil
|
||||
}
|
||||
|
||||
// UpdateContentTag 更新统一标签
|
||||
func UpdateContentTag(ctx context.Context, req *proto.ContentTagUpdateReq, operatorId int64) *srvdef.SrvErr {
|
||||
if req == nil || req.Id <= 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "标签ID无效", "")
|
||||
}
|
||||
current, err := contenttagmod.FindOneContentTag(ctx, bson.M{
|
||||
contenttagmod.FN.ContentTag.Id: req.Id,
|
||||
}, options.FindOne().SetProjection(contentTagWebProjection))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nextTypes := append([]contenttagmod.ContentTagType{}, current.Types...)
|
||||
nextVideoCount := current.VideoCount
|
||||
nextPlayletCount := current.PlayletCount
|
||||
setDoc := bson.M{contenttagmod.FN.ContentTag.UpdatedAt: time.Now()}
|
||||
if req.Name != nil {
|
||||
name := strings.TrimSpace(*req.Name)
|
||||
if name == "" {
|
||||
return srvdef.New(srvdef.ErrParamError, "标签名称不能为空", "")
|
||||
}
|
||||
setDoc[contenttagmod.FN.ContentTag.Name] = name
|
||||
}
|
||||
if req.Types != nil {
|
||||
types, err := normalizeContentTagTypes(*req.Types)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nextTypes = types
|
||||
setDoc[contenttagmod.FN.ContentTag.Types] = types
|
||||
}
|
||||
if req.Status != nil {
|
||||
setDoc[contenttagmod.FN.ContentTag.Status] = *req.Status
|
||||
}
|
||||
if req.Rank != nil {
|
||||
setDoc[contenttagmod.FN.ContentTag.Rank] = *req.Rank
|
||||
}
|
||||
if req.SeoInfo != nil {
|
||||
setDoc["seoInfo"] = normalizeContentTagSeoValue(*req.SeoInfo)
|
||||
}
|
||||
name := current.Name
|
||||
if req.Name != nil {
|
||||
name = strings.TrimSpace(*req.Name)
|
||||
}
|
||||
keywords := current.SeoInfo.Keyworld
|
||||
if req.SeoInfo != nil {
|
||||
keywords = normalizeContentTagSeoValue(*req.SeoInfo).Keyworld
|
||||
}
|
||||
exists := func(s string) bool {
|
||||
item, _ := contenttagmod.FindOneContentTag(ctx, slugx.Filter(
|
||||
contenttagmod.FN.ContentTag.Slug, contenttagmod.FN.ContentTag.Id, s, req.Id))
|
||||
return item != nil
|
||||
}
|
||||
slug, changed, serr := slugx.PrepareOnUpdate(current.Slug, req.Slug,
|
||||
utils.SlugSourceFromKeywordsOrTitle(keywords, name), exists)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
if changed {
|
||||
setDoc[contenttagmod.FN.ContentTag.Slug] = slug
|
||||
}
|
||||
if err := validateContentTagCounts(nextTypes, nextVideoCount, nextPlayletCount); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(setDoc) <= 1 {
|
||||
return srvdef.New(srvdef.ErrParamError, "没有可更新字段", "")
|
||||
}
|
||||
_, operatorName := resolveContentTagOperator(ctx, operatorId)
|
||||
setDoc[contenttagmod.FN.ContentTag.UpdatedByName] = operatorName
|
||||
return contenttagmod.UpdateOneContentTag(ctx,
|
||||
bson.M{contenttagmod.FN.ContentTag.Id: req.Id},
|
||||
bson.M{"$set": setDoc},
|
||||
)
|
||||
}
|
||||
|
||||
// DelContentTag 删除统一标签,存在任一类型绑定数量时禁止删除
|
||||
func DelContentTag(ctx context.Context, req *proto.ContentTagDelReq) *srvdef.SrvErr {
|
||||
if req == nil || req.Id <= 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "标签ID无效", "")
|
||||
}
|
||||
tag, err := contenttagmod.FindOneContentTag(ctx, bson.M{
|
||||
contenttagmod.FN.ContentTag.Id: req.Id,
|
||||
}, options.FindOne().SetProjection(contentTagWebProjection))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.VideoCount > 0 || tag.PlayletCount > 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "标签已被内容绑定,无法删除", "")
|
||||
}
|
||||
count, err := contenttagmod.DelOneContentTag(ctx, bson.M{contenttagmod.FN.ContentTag.Id: req.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return srvdef.New(srvdef.ErrNoData, "", "")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListContentTagOption 查询启用的统一标签下拉列表,传type时按支持类型过滤
|
||||
func ListContentTagOption(ctx context.Context, req *proto.ContentTagOptionReq) (*proto.ContentTagOptionResp, *srvdef.SrvErr) {
|
||||
if req == nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||||
}
|
||||
filter := bson.M{
|
||||
contenttagmod.FN.ContentTag.Status: true,
|
||||
}
|
||||
if req.Type != nil {
|
||||
if !isValidContentTagType(*req.Type) {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "标签类型无效", "")
|
||||
}
|
||||
filter[contenttagmod.FN.ContentTag.Types] = *req.Type
|
||||
}
|
||||
list, err := contenttagmod.FindManyContentTag(ctx, filter, options.Find().
|
||||
SetProjection(contentTagWebProjection).
|
||||
SetSort(bson.D{
|
||||
{Key: contenttagmod.FN.ContentTag.Rank, Value: -1},
|
||||
{Key: contenttagmod.FN.ContentTag.Id, Value: -1},
|
||||
}).
|
||||
SetLimit(maxContentTagOptionSize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := &proto.ContentTagOptionResp{List: make([]*proto.ContentTagOptionItem, 0, len(list))}
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
resp.List = append(resp.List, &proto.ContentTagOptionItem{
|
||||
Id: item.Id,
|
||||
Name: item.Name,
|
||||
Types: append([]contenttagmod.ContentTagType{}, item.Types...),
|
||||
TypeNames: contentTagTypeNames(item.Types),
|
||||
Rank: item.Rank,
|
||||
BindCount: contentTagOptionBindCount(item, req.Type),
|
||||
})
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// NormalizeContentTagIds 校验标签ID并去重,保留原有顺序便于内容详情按后台提交顺序展示
|
||||
func NormalizeContentTagIds(tagIds []int64) ([]int64, *srvdef.SrvErr) {
|
||||
result := normalizeContentTagIdsForDiff(tagIds)
|
||||
if len(result) != len(tagIds) {
|
||||
for _, tagId := range tagIds {
|
||||
if tagId <= 0 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "标签ID无效", "")
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ValidateContentTags 校验统一标签存在、启用,并且支持当前内容类型
|
||||
func ValidateContentTags(ctx context.Context, tagIds []int64, tagType contenttagmod.ContentTagType) *srvdef.SrvErr {
|
||||
ids, err := NormalizeContentTagIds(tagIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
if !isValidContentTagType(tagType) {
|
||||
return srvdef.New(srvdef.ErrParamError, "标签类型无效", "")
|
||||
}
|
||||
count, err := contenttagmod.CountContentTag(ctx, bson.M{
|
||||
contenttagmod.FN.ContentTag.Id: bson.M{"$in": ids},
|
||||
contenttagmod.FN.ContentTag.Status: true,
|
||||
contenttagmod.FN.ContentTag.Types: tagType,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count != int64(len(ids)) {
|
||||
return srvdef.New(srvdef.ErrParamError, "标签不存在、未启用或不支持当前内容类型", "")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SyncContentTagBindCount 按绑定差异增减统一标签对应类型的绑定数量
|
||||
func SyncContentTagBindCount(ctx context.Context, oldTagIds, newTagIds []int64, tagType contenttagmod.ContentTagType) *srvdef.SrvErr {
|
||||
return SyncContentTagBindCountBatch(ctx, []ContentTagBindChange{
|
||||
{OldTagIds: oldTagIds, NewTagIds: newTagIds},
|
||||
}, tagType)
|
||||
}
|
||||
|
||||
// SyncContentTagBindCountBatch 聚合多条内容标签变更后批量增减统一标签对应类型的绑定数量
|
||||
func SyncContentTagBindCountBatch(ctx context.Context, changes []ContentTagBindChange, tagType contenttagmod.ContentTagType) *srvdef.SrvErr {
|
||||
if !isValidContentTagType(tagType) {
|
||||
return srvdef.New(srvdef.ErrParamError, "标签类型无效", "")
|
||||
}
|
||||
field, err := contentTagCountField(tagType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
deltaMap := make(map[int64]int64)
|
||||
for _, change := range changes {
|
||||
addIds, removeIds := diffContentTagIds(
|
||||
normalizeContentTagIdsForDiff(change.OldTagIds),
|
||||
normalizeContentTagIdsForDiff(change.NewTagIds),
|
||||
)
|
||||
for _, tagId := range addIds {
|
||||
deltaMap[tagId]++
|
||||
}
|
||||
for _, tagId := range removeIds {
|
||||
deltaMap[tagId]--
|
||||
}
|
||||
}
|
||||
if len(deltaMap) == 0 {
|
||||
return nil
|
||||
}
|
||||
now := time.Now()
|
||||
docs := make([]mongo.WriteModel, 0, len(deltaMap))
|
||||
for tagId, delta := range deltaMap {
|
||||
if delta == 0 {
|
||||
continue
|
||||
}
|
||||
docs = append(docs, mongo.NewUpdateOneModel().
|
||||
SetFilter(bson.M{contenttagmod.FN.ContentTag.Id: tagId}).
|
||||
SetUpdate(bson.A{
|
||||
bson.M{"$set": bson.M{
|
||||
field: bson.M{"$max": bson.A{
|
||||
0,
|
||||
bson.M{"$add": bson.A{
|
||||
bson.M{"$ifNull": bson.A{"$" + field, 0}},
|
||||
delta,
|
||||
}},
|
||||
}},
|
||||
contenttagmod.FN.ContentTag.UpdatedAt: now,
|
||||
}},
|
||||
}))
|
||||
}
|
||||
if len(docs) == 0 {
|
||||
return nil
|
||||
}
|
||||
_, err = contenttagmod.BulkWriteContentTag(ctx, docs, options.BulkWrite().SetOrdered(false))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// normalizeContentTagPage 规范分页参数,限制单页数量避免大分页拖慢查询
|
||||
func normalizeContentTagPage(pageNum, pageSize int64) (int64, int64) {
|
||||
if pageNum <= 0 {
|
||||
pageNum = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = defaultContentTagPageSize
|
||||
}
|
||||
if pageSize > maxContentTagPageSize {
|
||||
pageSize = maxContentTagPageSize
|
||||
}
|
||||
return pageNum, pageSize
|
||||
}
|
||||
|
||||
func normalizeContentTagIdsForDiff(tagIds []int64) []int64 {
|
||||
result := make([]int64, 0, len(tagIds))
|
||||
seen := make(map[int64]struct{}, len(tagIds))
|
||||
for _, tagId := range tagIds {
|
||||
if tagId <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[tagId]; ok {
|
||||
continue
|
||||
}
|
||||
seen[tagId] = struct{}{}
|
||||
result = append(result, tagId)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func diffContentTagIds(oldTagIds, newTagIds []int64) ([]int64, []int64) {
|
||||
oldMap := make(map[int64]struct{}, len(oldTagIds))
|
||||
newMap := make(map[int64]struct{}, len(newTagIds))
|
||||
for _, tagId := range oldTagIds {
|
||||
oldMap[tagId] = struct{}{}
|
||||
}
|
||||
for _, tagId := range newTagIds {
|
||||
newMap[tagId] = struct{}{}
|
||||
}
|
||||
addIds := make([]int64, 0)
|
||||
removeIds := make([]int64, 0)
|
||||
for _, tagId := range newTagIds {
|
||||
if _, ok := oldMap[tagId]; !ok {
|
||||
addIds = append(addIds, tagId)
|
||||
}
|
||||
}
|
||||
for _, tagId := range oldTagIds {
|
||||
if _, ok := newMap[tagId]; !ok {
|
||||
removeIds = append(removeIds, tagId)
|
||||
}
|
||||
}
|
||||
return addIds, removeIds
|
||||
}
|
||||
|
||||
func contentTagCountField(tagType contenttagmod.ContentTagType) (string, *srvdef.SrvErr) {
|
||||
switch tagType {
|
||||
case contenttagmod.ContentTagTypeVideo:
|
||||
return contenttagmod.FN.ContentTag.VideoCount, nil
|
||||
case contenttagmod.ContentTagTypePlaylet:
|
||||
return contenttagmod.FN.ContentTag.PlayletCount, nil
|
||||
default:
|
||||
return "", srvdef.New(srvdef.ErrParamError, "标签类型无效", "")
|
||||
}
|
||||
}
|
||||
|
||||
func contentTagBindCount(item *contenttagmod.ContentTag, tagType contenttagmod.ContentTagType) int64 {
|
||||
switch tagType {
|
||||
case contenttagmod.ContentTagTypeVideo:
|
||||
return item.VideoCount
|
||||
case contenttagmod.ContentTagTypePlaylet:
|
||||
return item.PlayletCount
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func contentTagOptionBindCount(item *contenttagmod.ContentTag, tagType *contenttagmod.ContentTagType) int64 {
|
||||
if tagType == nil {
|
||||
return 0
|
||||
}
|
||||
return contentTagBindCount(item, *tagType)
|
||||
}
|
||||
|
||||
// normalizeContentTagTypes 去重并校验统一标签支持的内容类型
|
||||
func normalizeContentTagTypes(types []contenttagmod.ContentTagType) ([]contenttagmod.ContentTagType, *srvdef.SrvErr) {
|
||||
if len(types) == 0 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "标签类型不能为空", "")
|
||||
}
|
||||
seen := make(map[contenttagmod.ContentTagType]struct{}, len(types))
|
||||
result := make([]contenttagmod.ContentTagType, 0, len(types))
|
||||
for _, item := range types {
|
||||
if !isValidContentTagType(item) {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "标签类型无效", "")
|
||||
}
|
||||
if _, ok := seen[item]; ok {
|
||||
continue
|
||||
}
|
||||
seen[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i] < result[j]
|
||||
})
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// isValidContentTagType 判断统一标签类型是否为视频、短剧之一
|
||||
func isValidContentTagType(tagType contenttagmod.ContentTagType) bool {
|
||||
return tagType == contenttagmod.ContentTagTypeVideo ||
|
||||
tagType == contenttagmod.ContentTagTypePlaylet
|
||||
}
|
||||
|
||||
// validateContentTagCounts 校验各类型绑定数量非负且数量字段和支持类型保持一致
|
||||
func validateContentTagCounts(types []contenttagmod.ContentTagType, videoCount, playletCount int64) *srvdef.SrvErr {
|
||||
if videoCount < 0 || playletCount < 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "绑定数量不能小于0", "")
|
||||
}
|
||||
typeMap := make(map[contenttagmod.ContentTagType]struct{}, len(types))
|
||||
for _, item := range types {
|
||||
typeMap[item] = struct{}{}
|
||||
}
|
||||
if videoCount > 0 && !hasContentTagType(typeMap, contenttagmod.ContentTagTypeVideo) {
|
||||
return srvdef.New(srvdef.ErrParamError, "存在视频绑定数量时必须支持视频类型", "")
|
||||
}
|
||||
if playletCount > 0 && !hasContentTagType(typeMap, contenttagmod.ContentTagTypePlaylet) {
|
||||
return srvdef.New(srvdef.ErrParamError, "存在短剧绑定数量时必须支持短剧类型", "")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasContentTagType(typeMap map[contenttagmod.ContentTagType]struct{}, tagType contenttagmod.ContentTagType) bool {
|
||||
_, ok := typeMap[tagType]
|
||||
return ok
|
||||
}
|
||||
|
||||
// contentTagListItem 将统一标签模型转换为后台列表响应
|
||||
func contentTagListItem(item *contenttagmod.ContentTag) *proto.ContentTagListResp {
|
||||
return &proto.ContentTagListResp{
|
||||
Id: item.Id,
|
||||
Name: item.Name,
|
||||
Slug: item.Slug,
|
||||
Types: append([]contenttagmod.ContentTagType{}, item.Types...),
|
||||
TypeNames: contentTagTypeNames(item.Types),
|
||||
Status: item.Status,
|
||||
Rank: item.Rank,
|
||||
VideoCount: item.VideoCount,
|
||||
PlayletCount: item.PlayletCount,
|
||||
SeoInfo: normalizeContentTagSeoValue(item.SeoInfo),
|
||||
CreatedById: item.CreatedById,
|
||||
CreatedByName: item.CreatedByName,
|
||||
UpdatedByName: item.UpdatedByName,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// contentTagTypeNames 将统一标签类型转换为后台展示名称
|
||||
func contentTagTypeNames(types []contenttagmod.ContentTagType) []string {
|
||||
names := make([]string, 0, len(types))
|
||||
for _, item := range types {
|
||||
switch item {
|
||||
case contenttagmod.ContentTagTypeVideo:
|
||||
names = append(names, "视频")
|
||||
case contenttagmod.ContentTagTypePlaylet:
|
||||
names = append(names, "短剧")
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// normalizeContentTagSeoValue 规整 SEO 字段,避免空切片返回 null
|
||||
func normalizeContentTagSeoValue(seo pubdef.SeoValue) pubdef.SeoValue {
|
||||
if seo.Keyworld == nil {
|
||||
seo.Keyworld = make([]string, 0)
|
||||
}
|
||||
return seo
|
||||
}
|
||||
|
||||
// resolveContentTagOperator 从后台账号表解析当前操作人名称
|
||||
func resolveContentTagOperator(ctx context.Context, operatorId int64) (int64, string) {
|
||||
if operatorId <= 0 {
|
||||
return 0, ""
|
||||
}
|
||||
acc, err := mwacc.FindOneAccByFilter(ctx, bson.M{
|
||||
mwacc.FN.MwAcc.Id: operatorId,
|
||||
}, options.FindOne().SetProjection(bson.D{
|
||||
{Key: mwacc.FN.MwAcc.Id, Value: 1},
|
||||
{Key: mwacc.FN.MwAcc.UserName, Value: 1},
|
||||
}))
|
||||
if err != nil || acc == nil {
|
||||
return operatorId, ""
|
||||
}
|
||||
return acc.Id, acc.UserName
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package srv_detailnav
|
||||
|
||||
import (
|
||||
"videoweb/pub/models/detailnavmod"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
type GetDetailNavListReq struct {
|
||||
pubdef.Page
|
||||
}
|
||||
|
||||
type GetDetailNavListResp struct {
|
||||
Total int64 `json:"total"`
|
||||
List detailnavmod.DetailNavModelList `json:"list"`
|
||||
}
|
||||
|
||||
type AddDetailNavReq struct {
|
||||
Name string `json:"name" binding:"required"` // 导航名称
|
||||
Url string `json:"url" binding:"required"` // 导航链接
|
||||
Sort int64 `json:"sort"` // 排序
|
||||
Status *bool `json:"status"` // 是否启用
|
||||
}
|
||||
|
||||
type UpdateDetailNavReq struct {
|
||||
Id string `json:"id" binding:"required"`
|
||||
Param UpdateDetailNavParam `json:"param"`
|
||||
}
|
||||
|
||||
type UpdateDetailNavParam struct {
|
||||
Name *string `json:"name" bson:"name,omitempty"`
|
||||
Url *string `json:"url" bson:"url,omitempty"`
|
||||
Sort *int64 `json:"sort" bson:"sort,omitempty"`
|
||||
Status *bool `json:"status" bson:"status,omitempty"`
|
||||
}
|
||||
|
||||
type DelDetailNavReq struct {
|
||||
Id string `json:"id" binding:"required"`
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package srv_detailnav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/detailnavmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// GetDetailNavList 获取详情页导航列表
|
||||
func GetDetailNavList(ctx context.Context, req *GetDetailNavListReq) (*GetDetailNavListResp, *srvdef.SrvErr) {
|
||||
filter := bson.M{}
|
||||
|
||||
count, serr := detailnavmod.CountDetailNav(ctx, filter)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
if count == 0 {
|
||||
return &GetDetailNavListResp{
|
||||
Total: 0,
|
||||
List: make(detailnavmod.DetailNavModelList, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
opts := options.FindOptions{}
|
||||
opts.SetSort(bson.D{
|
||||
{Key: detailnavmod.FN.DetailNavModel.Sort, Value: 1},
|
||||
{Key: detailnavmod.FN.DetailNavModel.CreatedAt, Value: -1},
|
||||
})
|
||||
opts.SetLimit(req.PageSize)
|
||||
opts.SetSkip((req.PageNum - 1) * req.PageSize)
|
||||
|
||||
list, serr := detailnavmod.FindManyDetailNav(ctx, filter, &opts)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
|
||||
return &GetDetailNavListResp{
|
||||
Total: count,
|
||||
List: list,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AddDetailNav 添加详情页导航
|
||||
func AddDetailNav(ctx context.Context, req *AddDetailNavReq) *srvdef.SrvErr {
|
||||
now := time.Now()
|
||||
status := true
|
||||
if req.Status != nil {
|
||||
status = *req.Status
|
||||
}
|
||||
doc := &detailnavmod.DetailNavModel{
|
||||
ID: primitive.NewObjectID(),
|
||||
Name: req.Name,
|
||||
Url: req.Url,
|
||||
Sort: req.Sort,
|
||||
Status: status,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
return detailnavmod.InsertOneDetailNav(ctx, doc)
|
||||
}
|
||||
|
||||
// UpdateDetailNav 更新详情页导航
|
||||
func UpdateDetailNav(ctx context.Context, req *UpdateDetailNavReq) *srvdef.SrvErr {
|
||||
objId, err := primitive.ObjectIDFromHex(req.Id)
|
||||
if err != nil {
|
||||
return srvdef.New(srvdef.ErrParamError, "", "")
|
||||
}
|
||||
|
||||
filter := bson.M{detailnavmod.FN.DetailNavModel.ID: objId}
|
||||
updateFields := bson.M{
|
||||
detailnavmod.FN.DetailNavModel.UpdatedAt: time.Now(),
|
||||
}
|
||||
if req.Param.Name != nil {
|
||||
updateFields[detailnavmod.FN.DetailNavModel.Name] = *req.Param.Name
|
||||
}
|
||||
if req.Param.Url != nil {
|
||||
updateFields[detailnavmod.FN.DetailNavModel.Url] = *req.Param.Url
|
||||
}
|
||||
if req.Param.Sort != nil {
|
||||
updateFields[detailnavmod.FN.DetailNavModel.Sort] = *req.Param.Sort
|
||||
}
|
||||
if req.Param.Status != nil {
|
||||
updateFields[detailnavmod.FN.DetailNavModel.Status] = *req.Param.Status
|
||||
}
|
||||
|
||||
return detailnavmod.UpdateOneDetailNav(ctx, filter, bson.M{"$set": updateFields})
|
||||
}
|
||||
|
||||
// DelDetailNav 删除详情页导航
|
||||
func DelDetailNav(ctx context.Context, req *DelDetailNavReq) *srvdef.SrvErr {
|
||||
objId, err := primitive.ObjectIDFromHex(req.Id)
|
||||
if err != nil {
|
||||
return srvdef.New(srvdef.ErrParamError, "", "")
|
||||
}
|
||||
|
||||
filter := bson.M{detailnavmod.FN.DetailNavModel.ID: objId}
|
||||
count, serr := detailnavmod.DelOneDetailNav(ctx, filter)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
if count == 0 {
|
||||
return srvdef.New(srvdef.ErrNoData, "", "")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
package srv_homebanner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/response"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/homebannermod"
|
||||
"videoweb/pub/models/mwacc"
|
||||
"videoweb/web/api/proto"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultHomeBannerPageSize int64 = 10
|
||||
maxHomeBannerPageSize int64 = 100
|
||||
)
|
||||
|
||||
var homeBannerWebProjection = bson.D{
|
||||
{Key: homebannermod.FN.HomeBanner.Id, Value: 1},
|
||||
{Key: homebannermod.FN.HomeBanner.Name, Value: 1},
|
||||
{Key: homebannermod.FN.HomeBanner.Image, Value: 1},
|
||||
{Key: homebannermod.FN.HomeBanner.VerticalImage, Value: 1},
|
||||
{Key: homebannermod.FN.HomeBanner.Url, Value: 1},
|
||||
{Key: homebannermod.FN.HomeBanner.Rank, Value: 1},
|
||||
{Key: homebannermod.FN.HomeBanner.Status, Value: 1},
|
||||
{Key: homebannermod.FN.HomeBanner.CreatedById, Value: 1},
|
||||
{Key: homebannermod.FN.HomeBanner.CreatedByName, Value: 1},
|
||||
{Key: homebannermod.FN.HomeBanner.UpdatedByName, Value: 1},
|
||||
{Key: homebannermod.FN.HomeBanner.CreatedAt, Value: 1},
|
||||
{Key: homebannermod.FN.HomeBanner.UpdatedAt, Value: 1},
|
||||
}
|
||||
|
||||
// ListHomeBanner 分页查询首页 banner 列表
|
||||
func ListHomeBanner(ctx context.Context, req *proto.HomeBannerListReq) (*proto.HomeBannerListPageResp, *srvdef.SrvErr) {
|
||||
if req == nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||||
}
|
||||
filter := bson.M{}
|
||||
if req.Id != nil && *req.Id > 0 {
|
||||
filter[homebannermod.FN.HomeBanner.Id] = *req.Id
|
||||
}
|
||||
if req.Status != nil {
|
||||
filter[homebannermod.FN.HomeBanner.Status] = *req.Status
|
||||
}
|
||||
if name := strings.TrimSpace(req.Name); name != "" {
|
||||
filter[homebannermod.FN.HomeBanner.Name] = bson.M{"$regex": regexp.QuoteMeta(name)}
|
||||
}
|
||||
pageNum, pageSize := normalizeHomeBannerPage(req.PageNum, req.PageSize)
|
||||
totalCount, err := homebannermod.CountHomeBanner(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := &proto.HomeBannerListPageResp{
|
||||
BasePageResp: response.BasePageResp{
|
||||
TotalCount: totalCount,
|
||||
PageSize: int(pageSize),
|
||||
PageNum: int(pageNum),
|
||||
List: make([]*proto.HomeBannerListResp, 0),
|
||||
},
|
||||
}
|
||||
if totalCount == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
list, err := homebannermod.FindManyHomeBanner(ctx, filter, options.Find().
|
||||
SetProjection(homeBannerWebProjection).
|
||||
SetSort(bson.D{
|
||||
{Key: homebannermod.FN.HomeBanner.Rank, Value: -1},
|
||||
{Key: homebannermod.FN.HomeBanner.UpdatedAt, Value: -1},
|
||||
{Key: homebannermod.FN.HomeBanner.Id, Value: -1},
|
||||
}).
|
||||
SetSkip((pageNum-1)*pageSize).
|
||||
SetLimit(pageSize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]*proto.HomeBannerListResp, 0, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, &proto.HomeBannerListResp{
|
||||
Id: item.Id,
|
||||
Name: item.Name,
|
||||
Image: item.Image,
|
||||
VerticalImage: item.VerticalImage,
|
||||
Url: item.Url,
|
||||
Rank: item.Rank,
|
||||
Status: item.Status,
|
||||
CreatedById: item.CreatedById,
|
||||
CreatedByName: item.CreatedByName,
|
||||
UpdatedByName: item.UpdatedByName,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
})
|
||||
}
|
||||
resp.List = items
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// AddHomeBanner 新增首页 banner
|
||||
func AddHomeBanner(ctx context.Context, req *proto.HomeBannerAddReq, operatorId int64) (*proto.HomeBannerAddResp, *srvdef.SrvErr) {
|
||||
if req == nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||||
}
|
||||
name := strings.TrimSpace(req.Name)
|
||||
image := strings.TrimSpace(req.Image)
|
||||
if name == "" {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "名称不能为空", "")
|
||||
}
|
||||
if image == "" {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "图片不能为空", "")
|
||||
}
|
||||
if req.Status == nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "状态不能为空", "")
|
||||
}
|
||||
operatorId, operatorName := resolveHomeBannerOperator(ctx, operatorId)
|
||||
doc := &homebannermod.HomeBanner{
|
||||
Name: name,
|
||||
Image: image,
|
||||
VerticalImage: strings.TrimSpace(req.VerticalImage),
|
||||
Url: strings.TrimSpace(req.Url),
|
||||
Rank: req.Rank,
|
||||
Status: *req.Status,
|
||||
CreatedById: operatorId,
|
||||
CreatedByName: operatorName,
|
||||
UpdatedByName: operatorName,
|
||||
}
|
||||
if err := homebannermod.InsertOneHomeBanner(ctx, doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &proto.HomeBannerAddResp{Id: doc.Id}, nil
|
||||
}
|
||||
|
||||
// UpdateHomeBanner 更新首页 banner
|
||||
func UpdateHomeBanner(ctx context.Context, req *proto.HomeBannerUpdateReq, operatorId int64) *srvdef.SrvErr {
|
||||
if req == nil || req.Id <= 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "首页banner ID无效", "")
|
||||
}
|
||||
setDoc := bson.M{homebannermod.FN.HomeBanner.UpdatedAt: time.Now()}
|
||||
if req.Name != nil {
|
||||
name := strings.TrimSpace(*req.Name)
|
||||
if name == "" {
|
||||
return srvdef.New(srvdef.ErrParamError, "名称不能为空", "")
|
||||
}
|
||||
setDoc[homebannermod.FN.HomeBanner.Name] = name
|
||||
}
|
||||
if req.Image != nil {
|
||||
image := strings.TrimSpace(*req.Image)
|
||||
if image == "" {
|
||||
return srvdef.New(srvdef.ErrParamError, "图片不能为空", "")
|
||||
}
|
||||
setDoc[homebannermod.FN.HomeBanner.Image] = image
|
||||
}
|
||||
if req.VerticalImage != nil {
|
||||
setDoc[homebannermod.FN.HomeBanner.VerticalImage] = strings.TrimSpace(*req.VerticalImage)
|
||||
}
|
||||
if req.Url != nil {
|
||||
setDoc[homebannermod.FN.HomeBanner.Url] = strings.TrimSpace(*req.Url)
|
||||
}
|
||||
if req.Rank != nil {
|
||||
setDoc[homebannermod.FN.HomeBanner.Rank] = *req.Rank
|
||||
}
|
||||
if req.Status != nil {
|
||||
setDoc[homebannermod.FN.HomeBanner.Status] = *req.Status
|
||||
}
|
||||
if len(setDoc) <= 1 {
|
||||
return srvdef.New(srvdef.ErrParamError, "没有可更新字段", "")
|
||||
}
|
||||
_, operatorName := resolveHomeBannerOperator(ctx, operatorId)
|
||||
setDoc[homebannermod.FN.HomeBanner.UpdatedByName] = operatorName
|
||||
return homebannermod.UpdateOneHomeBanner(ctx,
|
||||
bson.M{homebannermod.FN.HomeBanner.Id: req.Id},
|
||||
bson.M{"$set": setDoc},
|
||||
)
|
||||
}
|
||||
|
||||
// DelHomeBanner 删除首页 banner
|
||||
func DelHomeBanner(ctx context.Context, req *proto.HomeBannerDelReq) *srvdef.SrvErr {
|
||||
if req == nil || req.Id <= 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "首页banner ID无效", "")
|
||||
}
|
||||
count, err := homebannermod.DelOneHomeBanner(ctx, bson.M{homebannermod.FN.HomeBanner.Id: req.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return srvdef.New(srvdef.ErrNoData, "", "")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// normalizeHomeBannerPage 规范分页参数,限制单页数量避免大分页拖慢查询
|
||||
func normalizeHomeBannerPage(pageNum, pageSize int64) (int64, int64) {
|
||||
if pageNum <= 0 {
|
||||
pageNum = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = defaultHomeBannerPageSize
|
||||
}
|
||||
if pageSize > maxHomeBannerPageSize {
|
||||
pageSize = maxHomeBannerPageSize
|
||||
}
|
||||
return pageNum, pageSize
|
||||
}
|
||||
|
||||
// resolveHomeBannerOperator 从后台账号表解析当前操作人名称
|
||||
func resolveHomeBannerOperator(ctx context.Context, operatorId int64) (int64, string) {
|
||||
if operatorId <= 0 {
|
||||
return 0, ""
|
||||
}
|
||||
acc, err := mwacc.FindOneAccByFilter(ctx, bson.M{
|
||||
mwacc.FN.MwAcc.Id: operatorId,
|
||||
}, options.FindOne().SetProjection(bson.D{
|
||||
{Key: mwacc.FN.MwAcc.Id, Value: 1},
|
||||
{Key: mwacc.FN.MwAcc.UserName, Value: 1},
|
||||
}))
|
||||
if err != nil || acc == nil {
|
||||
return operatorId, ""
|
||||
}
|
||||
return acc.Id, acc.UserName
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package srv_hotnav
|
||||
|
||||
import (
|
||||
"videoweb/pub/models/hotnavmod"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
type GetHotNavListReq struct {
|
||||
pubdef.Page
|
||||
}
|
||||
|
||||
type GetHotNavListResp struct {
|
||||
Total int64 `json:"total"`
|
||||
List hotnavmod.HotNavList `json:"list"`
|
||||
}
|
||||
|
||||
type AddHotNavReq struct {
|
||||
Title string `json:"title" binding:"required"` // 标题
|
||||
Intro string `json:"intro"` // 介绍
|
||||
Url string `json:"url" binding:"required"` // 链接
|
||||
Icon string `json:"icon"` // 图标
|
||||
Sort int64 `json:"sort"` // 排序
|
||||
Status *bool `json:"status"` // 是否启用
|
||||
}
|
||||
|
||||
type UpdateHotNavReq struct {
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
Param UpdateHotNavParam `json:"param"`
|
||||
}
|
||||
|
||||
type UpdateHotNavParam struct {
|
||||
Title *string `json:"title"`
|
||||
Intro *string `json:"intro"`
|
||||
Url *string `json:"url"`
|
||||
Icon *string `json:"icon"`
|
||||
Sort *int64 `json:"sort"`
|
||||
Status *bool `json:"status"`
|
||||
}
|
||||
|
||||
type DelHotNavReq struct {
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package srv_hotnav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/hotnavmod"
|
||||
"videoweb/pub/mongos"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func GetHotNavList(ctx context.Context, req *GetHotNavListReq) (*GetHotNavListResp, *srvdef.SrvErr) {
|
||||
filter := bson.M{}
|
||||
count, serr := hotnavmod.CountHotNav(ctx, filter)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
if count == 0 {
|
||||
return &GetHotNavListResp{
|
||||
Total: 0,
|
||||
List: make(hotnavmod.HotNavList, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
opts := options.FindOptions{}
|
||||
opts.SetSort(bson.D{
|
||||
{Key: hotnavmod.FN.HotNav.Sort, Value: -1},
|
||||
{Key: hotnavmod.FN.HotNav.CreatedAt, Value: -1},
|
||||
})
|
||||
opts.SetLimit(req.PageSize)
|
||||
opts.SetSkip((req.PageNum - 1) * req.PageSize)
|
||||
list, serr := hotnavmod.FindManyHotNav(ctx, filter, &opts)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
return &GetHotNavListResp{
|
||||
Total: count,
|
||||
List: list,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func AddHotNav(ctx context.Context, req *AddHotNavReq) *srvdef.SrvErr {
|
||||
now := time.Now()
|
||||
status := true
|
||||
if req.Status != nil {
|
||||
status = *req.Status
|
||||
}
|
||||
doc := &hotnavmod.HotNav{
|
||||
Id: mongos.Lid.HotNav.Get(),
|
||||
Title: req.Title,
|
||||
Intro: req.Intro,
|
||||
Url: req.Url,
|
||||
Icon: req.Icon,
|
||||
Sort: req.Sort,
|
||||
Status: status,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
return hotnavmod.InsertOneHotNav(ctx, doc)
|
||||
}
|
||||
|
||||
func UpdateHotNav(ctx context.Context, req *UpdateHotNavReq) *srvdef.SrvErr {
|
||||
filter := bson.M{hotnavmod.FN.HotNav.Id: req.Id}
|
||||
updateFields := bson.M{
|
||||
hotnavmod.FN.HotNav.UpdatedAt: time.Now(),
|
||||
}
|
||||
if req.Param.Title != nil {
|
||||
updateFields[hotnavmod.FN.HotNav.Title] = *req.Param.Title
|
||||
}
|
||||
if req.Param.Intro != nil {
|
||||
updateFields[hotnavmod.FN.HotNav.Intro] = *req.Param.Intro
|
||||
}
|
||||
if req.Param.Url != nil {
|
||||
updateFields[hotnavmod.FN.HotNav.Url] = *req.Param.Url
|
||||
}
|
||||
if req.Param.Icon != nil {
|
||||
updateFields[hotnavmod.FN.HotNav.Icon] = *req.Param.Icon
|
||||
}
|
||||
if req.Param.Sort != nil {
|
||||
updateFields[hotnavmod.FN.HotNav.Sort] = *req.Param.Sort
|
||||
}
|
||||
if req.Param.Status != nil {
|
||||
updateFields[hotnavmod.FN.HotNav.Status] = *req.Param.Status
|
||||
}
|
||||
return hotnavmod.UpdateOneHotNav(ctx, filter, bson.M{"$set": updateFields})
|
||||
}
|
||||
|
||||
func DelHotNav(ctx context.Context, req *DelHotNavReq) *srvdef.SrvErr {
|
||||
filter := bson.M{hotnavmod.FN.HotNav.Id: req.Id}
|
||||
count, serr := hotnavmod.DelOneHotNav(ctx, filter)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
if count == 0 {
|
||||
return srvdef.New(srvdef.ErrNoData, "", "")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package srv_ipblack
|
||||
|
||||
import (
|
||||
"videoweb/pub/models/ipblackmod"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
type GetIPBlackListReq struct {
|
||||
pubdef.Page
|
||||
IP *string `json:"ip"`
|
||||
}
|
||||
|
||||
type GetIPBlackListResp struct {
|
||||
Total int64 `json:"total"`
|
||||
List []*ipblackmod.IPBlackModel `json:"list"`
|
||||
}
|
||||
|
||||
type AddIPBlackListReq struct {
|
||||
Ip string `json:"ip"`
|
||||
Desc string `json:"desc" bson:"desc"` // 备注
|
||||
}
|
||||
|
||||
type DelIpBlackListReq struct {
|
||||
Ip string `json:"ip"`
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package srv_ipblack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"sync"
|
||||
"time"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/ipblackmod"
|
||||
)
|
||||
|
||||
var (
|
||||
ipBlackMap map[string]struct{}
|
||||
lock sync.RWMutex
|
||||
)
|
||||
|
||||
func Init() {
|
||||
ctx := context.Background()
|
||||
list, err := ipblackmod.FindManyIPBlackModel(ctx, bson.M{})
|
||||
if err != nil {
|
||||
log.Error("ipBlackInit FindManyIPBlackModel error", log.E(err))
|
||||
return
|
||||
}
|
||||
tempMaps := make(map[string]struct{})
|
||||
for _, v := range list {
|
||||
tempMaps[v.Ip] = struct{}{}
|
||||
}
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
ipBlackMap = tempMaps
|
||||
}
|
||||
|
||||
func CheckIp(ip string) bool {
|
||||
lock.RLock()
|
||||
defer lock.RUnlock()
|
||||
if _, ok := ipBlackMap[ip]; ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GetIpBlackList(ctx context.Context, req *GetIPBlackListReq) (*GetIPBlackListResp, *srvdef.SrvErr) {
|
||||
filter := bson.M{}
|
||||
if req.IP != nil && len(*req.IP) > 0 {
|
||||
filter[ipblackmod.FN.IPBlackModel.Ip] = req.IP
|
||||
}
|
||||
count, err := ipblackmod.CountIPBlackModel(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if count == 0 {
|
||||
return &GetIPBlackListResp{
|
||||
Total: 0,
|
||||
List: make([]*ipblackmod.IPBlackModel, 0),
|
||||
}, nil
|
||||
}
|
||||
opts := options.FindOptions{}
|
||||
opts.SetSort(bson.M{ipblackmod.FN.IPBlackModel.CreatedAt: -1})
|
||||
opts.SetLimit(req.PageSize)
|
||||
opts.SetSkip((req.PageNum - 1) * req.PageSize)
|
||||
list, err := ipblackmod.FindManyIPBlackModel(ctx, filter, &opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := &GetIPBlackListResp{
|
||||
Total: count,
|
||||
List: list,
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func AddIpBlack(ctx context.Context, req *AddIPBlackListReq) *srvdef.SrvErr {
|
||||
err := ipblackmod.InsertOneIPBlackModel(ctx, &ipblackmod.IPBlackModel{
|
||||
ID: primitive.NewObjectID(),
|
||||
Ip: req.Ip,
|
||||
Desc: req.Desc,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
if err == nil {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
ipBlackMap[req.Ip] = struct{}{}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func DelIpBlack(ctx context.Context, req *DelIpBlackListReq) *srvdef.SrvErr {
|
||||
_, err := ipblackmod.DelOneIPBlackModel(ctx, bson.M{
|
||||
ipblackmod.FN.IPBlackModel.Ip: req.Ip,
|
||||
})
|
||||
if err != nil {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
delete(ipBlackMap, req.Ip)
|
||||
ipBlackMap[req.Ip] = struct{}{}
|
||||
}
|
||||
return err
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,203 @@
|
||||
package srv_mediaresource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/models/playletmod"
|
||||
"videoweb/pub/pubdef"
|
||||
"videoweb/pub/redis"
|
||||
|
||||
goredis "github.com/go-redis/redis"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const (
|
||||
// PlayletCoverCompressCronSpec 历史空压缩封面扫描频率:每5分钟投递一批
|
||||
PlayletCoverCompressCronSpec = "0 */5 * * * ?"
|
||||
|
||||
playletCoverCompressQueueKeyFmt = "playlet:cover:compress:queue:%d"
|
||||
playletCoverCompressLockKeyFmt = "playlet:cover:compress:lock:%d:%d"
|
||||
playletCoverCompressFailKeyFmt = "playlet:cover:compress:fail:v2:%d:%d"
|
||||
playletCoverCompressLockTTL = 10 * time.Minute
|
||||
playletCoverCompressFailTTL = 10 * time.Minute
|
||||
playletCoverCompressPopWait = 5 * time.Second
|
||||
playletCoverCompressRetryWait = time.Second
|
||||
playletCoverCompressScanSize = int64(200)
|
||||
playletCoverCompressBatchSize = 50
|
||||
)
|
||||
|
||||
func playletCoverCompressQueueKey() string {
|
||||
return fmt.Sprintf(playletCoverCompressQueueKeyFmt, pubdef.AppId)
|
||||
}
|
||||
|
||||
func playletCoverCompressLockKey(playletId int64) string {
|
||||
return fmt.Sprintf(playletCoverCompressLockKeyFmt, pubdef.AppId, playletId)
|
||||
}
|
||||
|
||||
func playletCoverCompressFailKey(playletId int64) string {
|
||||
return fmt.Sprintf(playletCoverCompressFailKeyFmt, pubdef.AppId, playletId)
|
||||
}
|
||||
|
||||
type playletCoverCompressJob struct {
|
||||
PlayletId int64 `json:"playletId"`
|
||||
CreateTime int64 `json:"createTime"`
|
||||
}
|
||||
|
||||
// EnqueuePlayletCoverCompressJobs 扫描压缩封面为空的短剧并投递后台任务
|
||||
func EnqueuePlayletCoverCompressJobs() {
|
||||
ctx := context.Background()
|
||||
enqueued := 0
|
||||
scanned := 0
|
||||
var lastId int64
|
||||
for enqueued < playletCoverCompressBatchSize {
|
||||
filter := emptyCompressedCoverFilter()
|
||||
if lastId > 0 {
|
||||
filter[playletmod.FN.Playlet.Id] = bson.M{"$gt": lastId}
|
||||
}
|
||||
opts := options.Find().
|
||||
SetProjection(bson.D{
|
||||
{Key: playletmod.FN.Playlet.Id, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.Cover, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.CompressedCover, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.Status, Value: 1},
|
||||
}).
|
||||
SetSort(bson.D{{Key: playletmod.FN.Playlet.Id, Value: 1}}).
|
||||
SetLimit(playletCoverCompressScanSize)
|
||||
list, err := playletmod.FindManyPlaylet(ctx, filter, opts)
|
||||
if err != nil {
|
||||
log.Error("扫描待压缩短剧封面失败", log.E(err))
|
||||
return
|
||||
}
|
||||
if len(list) == 0 {
|
||||
break
|
||||
}
|
||||
scanned += len(list)
|
||||
for _, item := range list {
|
||||
if item == nil || item.Id <= 0 {
|
||||
continue
|
||||
}
|
||||
lastId = item.Id
|
||||
if enqueued >= playletCoverCompressBatchSize {
|
||||
break
|
||||
}
|
||||
if has, _ := redis.Exists(playletCoverCompressFailKey(item.Id)); has {
|
||||
continue
|
||||
}
|
||||
if err := pushPlayletCoverCompressJob(ctx, item.Id); err != nil {
|
||||
log.Error("投递短剧封面压缩任务失败", log.Any("playletId", item.Id), log.E(err))
|
||||
continue
|
||||
}
|
||||
enqueued++
|
||||
}
|
||||
if int64(len(list)) < playletCoverCompressScanSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
log.Info("短剧压缩封面任务扫描完成", log.Any("scanCount", scanned), log.Any("enqueued", enqueued))
|
||||
}
|
||||
|
||||
func emptyCompressedCoverFilter() bson.M {
|
||||
return bson.M{
|
||||
playletmod.FN.Playlet.Cover: bson.M{"$nin": []interface{}{"", nil}},
|
||||
"$or": []bson.M{
|
||||
{playletmod.FN.Playlet.CompressedCover: bson.M{"$exists": false}},
|
||||
{playletmod.FN.Playlet.CompressedCover: ""},
|
||||
{playletmod.FN.Playlet.CompressedCover: nil},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func pushPlayletCoverCompressJob(ctx context.Context, playletId int64) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
payload, err := json.Marshal(playletCoverCompressJob{
|
||||
PlayletId: playletId,
|
||||
CreateTime: time.Now().Unix(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return redis.RPush(playletCoverCompressQueueKey(), string(payload))
|
||||
}
|
||||
|
||||
func runPlayletCoverCompressWorker(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
result, err := redis.Client().BLPop(playletCoverCompressPopWait, playletCoverCompressQueueKey()).Result()
|
||||
if err != nil {
|
||||
if err == goredis.Nil {
|
||||
continue
|
||||
}
|
||||
log.Error("消费短剧封面压缩任务失败", log.E(err))
|
||||
time.Sleep(playletCoverCompressRetryWait)
|
||||
continue
|
||||
}
|
||||
if len(result) < 2 {
|
||||
log.Error("短剧封面压缩任务格式异常", log.Any("result", result))
|
||||
continue
|
||||
}
|
||||
handlePlayletCoverCompressPayload(ctx, result[1])
|
||||
}
|
||||
}
|
||||
|
||||
func handlePlayletCoverCompressPayload(ctx context.Context, payload string) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Error("处理短剧封面压缩任务异常", log.Any("err", err), log.Any("payload", payload))
|
||||
}
|
||||
}()
|
||||
var job playletCoverCompressJob
|
||||
if err := json.Unmarshal([]byte(payload), &job); err != nil {
|
||||
log.Error("解析短剧封面压缩任务失败", log.Any("payload", payload), log.E(err))
|
||||
return
|
||||
}
|
||||
if job.PlayletId <= 0 {
|
||||
return
|
||||
}
|
||||
locked, err := redis.Setnx_NewOK(playletCoverCompressLockKey(job.PlayletId), true, playletCoverCompressLockTTL)
|
||||
if err != nil {
|
||||
log.Error("获取短剧封面压缩锁失败", log.Any("playletId", job.PlayletId), log.E(err))
|
||||
return
|
||||
}
|
||||
if !locked {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := redis.Del(playletCoverCompressLockKey(job.PlayletId)); err != nil {
|
||||
log.Error("释放短剧封面压缩锁失败", log.Any("playletId", job.PlayletId), log.E(err))
|
||||
}
|
||||
}()
|
||||
playlet, serr := playletmod.FindOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: job.PlayletId}, options.FindOne().SetProjection(bson.D{
|
||||
{Key: playletmod.FN.Playlet.Id, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.Cover, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.CompressedCover, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.Status, Value: 1},
|
||||
}))
|
||||
if serr != nil || playlet == nil {
|
||||
if serr != nil {
|
||||
log.Warn("查询待压缩短剧失败", log.Any("playletId", job.PlayletId), log.E(serr))
|
||||
}
|
||||
return
|
||||
}
|
||||
if FillPlayletCompressedCover(ctx, playlet) {
|
||||
log.Info("历史短剧压缩封面完成", log.Any("playletId", playlet.Id))
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(playlet.CompressedCover) == "" && strings.TrimSpace(playlet.Cover) != "" {
|
||||
if err := redis.Set(playletCoverCompressFailKey(playlet.Id), "1", playletCoverCompressFailTTL); err != nil {
|
||||
log.Warn("记录短剧封面压缩失败冷却失败", log.Any("playletId", playlet.Id), log.E(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package srv_mediaresource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/imgutil"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/netutil"
|
||||
"videoweb/pub/models/cfg"
|
||||
"videoweb/pub/models/playletmod"
|
||||
"videoweb/pub/storage/proxy"
|
||||
"videoweb/web/service/srv_search"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
// compressPlayletCoverOrEmpty 下载原封面并按 media Resize 规则处理。
|
||||
// 原图未超过 60KB,或原图是 GIF 时,直接返回原封面地址;其它格式超过阈值则压缩为 JPEG 后上传。
|
||||
// 失败返回空字符串,不阻断入库。
|
||||
func compressPlayletCoverOrEmpty(cover string) string {
|
||||
cover = strings.TrimSpace(cover)
|
||||
if cover == "" {
|
||||
return ""
|
||||
}
|
||||
pathStr, err := uploadCompressedPlayletCover(cover)
|
||||
if err != nil {
|
||||
log.Warn("压缩短剧封面失败", log.Any("cover", cover), log.E(err))
|
||||
return ""
|
||||
}
|
||||
return pathStr
|
||||
}
|
||||
|
||||
// FillPlayletCompressedCover 为指定短剧补写压缩封面,已有值则跳过。
|
||||
func FillPlayletCompressedCover(ctx context.Context, playlet *playletmod.Playlet) bool {
|
||||
if playlet == nil || playlet.Id <= 0 {
|
||||
return false
|
||||
}
|
||||
if strings.TrimSpace(playlet.CompressedCover) != "" {
|
||||
return false
|
||||
}
|
||||
cover := strings.TrimSpace(playlet.Cover)
|
||||
if cover == "" {
|
||||
return false
|
||||
}
|
||||
pathStr := compressPlayletCoverOrEmpty(cover)
|
||||
if pathStr == "" {
|
||||
return false
|
||||
}
|
||||
now := time.Now()
|
||||
if err := playletmod.UpdateOnePlaylet(ctx,
|
||||
bson.M{
|
||||
playletmod.FN.Playlet.Id: playlet.Id,
|
||||
"$or": []bson.M{
|
||||
{playletmod.FN.Playlet.CompressedCover: bson.M{"$exists": false}},
|
||||
{playletmod.FN.Playlet.CompressedCover: ""},
|
||||
{playletmod.FN.Playlet.CompressedCover: nil},
|
||||
},
|
||||
},
|
||||
bson.M{"$set": bson.M{
|
||||
playletmod.FN.Playlet.CompressedCover: pathStr,
|
||||
playletmod.FN.Playlet.UpdatedAt: now,
|
||||
}},
|
||||
); err != nil {
|
||||
log.Warn("回写短剧压缩封面失败", log.Any("playletId", playlet.Id), log.E(err))
|
||||
return false
|
||||
}
|
||||
playlet.CompressedCover = pathStr
|
||||
latest, serr := playletmod.FindOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: playlet.Id})
|
||||
if serr == nil && latest != nil && latest.Status == playletmod.StatusUp {
|
||||
if syncErr := srv_search.SyncPlayletStatusToES(ctx, []int64{playlet.Id}, latest.Status); syncErr != nil {
|
||||
log.Warn("压缩封面后同步ES失败", log.Any("playletId", playlet.Id), log.E(syncErr))
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func uploadCompressedPlayletCover(cover string) (string, error) {
|
||||
downloadURL, err := resolvePlayletCoverURL(cover)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
downloadURL, err = encodeCoverDownloadURL(downloadURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
data, err := downloadPlayletCover(downloadURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
data = imgutil.DecryptImage(data)
|
||||
if !imgutil.NeedCompress(data) {
|
||||
return cover, nil
|
||||
}
|
||||
fileName := imgutil.SafeCoverFileName(downloadURL)
|
||||
compressed, outName, err := imgutil.CompressCover(data, fileName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
pathStr, err := proxy.UploadImg(outName, compressed, "")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("上传压缩封面失败: %w", err)
|
||||
}
|
||||
if strings.TrimSpace(pathStr) == "" {
|
||||
return "", fmt.Errorf("上传压缩封面返回空路径")
|
||||
}
|
||||
return pathStr, nil
|
||||
}
|
||||
|
||||
func resolvePlayletCoverURL(cover string) (string, error) {
|
||||
if strings.HasPrefix(cover, "http://") || strings.HasPrefix(cover, "https://") {
|
||||
return cover, nil
|
||||
}
|
||||
sysCfg, serr := cfg.FindOneSysCfg(context.Background(), bson.M{cfg.FN.SysCfg.Id: 1})
|
||||
if serr != nil {
|
||||
return "", fmt.Errorf("查询图片CDN失败: %s", serr.Error())
|
||||
}
|
||||
imageDomain := ""
|
||||
if sysCfg != nil {
|
||||
imageDomain = strings.TrimSpace(sysCfg.ImgWebCdn)
|
||||
}
|
||||
if imageDomain == "" {
|
||||
return "", fmt.Errorf("图片CDN未配置,无法下载相对路径封面")
|
||||
}
|
||||
return netutil.MergeURL(imageDomain, cover), nil
|
||||
}
|
||||
|
||||
// encodeCoverDownloadURL 把路径里的中文等非 ASCII 段转成百分号编码,避免下载失败或二次拼接异常。
|
||||
func encodeCoverDownloadURL(raw string) (string, error) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("解析封面地址失败: %w", err)
|
||||
}
|
||||
if u.Scheme == "" || u.Host == "" {
|
||||
return "", fmt.Errorf("封面地址无效")
|
||||
}
|
||||
escaped := u.EscapedPath()
|
||||
if escaped != "" {
|
||||
if dec, err := url.PathUnescape(escaped); err == nil {
|
||||
u.Path = dec
|
||||
u.RawPath = ""
|
||||
}
|
||||
}
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func downloadPlayletCover(rawURL string) ([]byte, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, rawURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("下载封面失败: %w", err)
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; xjsp-playlet-cover/1.0)")
|
||||
req.Header.Set("Accept", "image/*")
|
||||
|
||||
cli := &http.Client{Timeout: 90 * time.Second}
|
||||
resp, err := cli.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("下载封面失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("下载封面失败,状态码 %d", resp.StatusCode)
|
||||
}
|
||||
data, err := io.ReadAll(io.LimitReader(resp.Body, 20<<20))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("下载封面失败: %w", err)
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil, fmt.Errorf("下载封面内容为空")
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package srv_mediaresource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/pubdef"
|
||||
"videoweb/pub/redis"
|
||||
|
||||
goredis "github.com/go-redis/redis"
|
||||
)
|
||||
|
||||
const (
|
||||
playletResourceImportQueueKeyFmt = "playlet:resource:import:queue:%d"
|
||||
playletResourceImportLockKeyFmt = "playlet:resource:import:lock:%d:%d"
|
||||
playletResourceImportLockTTL = 10 * time.Minute
|
||||
playletResourceImportPopWait = 5 * time.Second
|
||||
playletResourceImportRetryWait = time.Second
|
||||
)
|
||||
|
||||
type playletResourceImportJob struct {
|
||||
Ids []uint `json:"ids"`
|
||||
OperatorId int64 `json:"operatorId"`
|
||||
OperatorName string `json:"operatorName"`
|
||||
CreateTime int64 `json:"createTime"`
|
||||
}
|
||||
|
||||
var playletResourceImportWorker = struct {
|
||||
mu sync.Mutex
|
||||
cancel context.CancelFunc
|
||||
}{}
|
||||
|
||||
func playletResourceImportQueueKey() string {
|
||||
return fmt.Sprintf(playletResourceImportQueueKeyFmt, pubdef.AppId)
|
||||
}
|
||||
|
||||
func playletResourceImportLockKey(mediaId uint) string {
|
||||
return fmt.Sprintf(playletResourceImportLockKeyFmt, pubdef.AppId, mediaId)
|
||||
}
|
||||
|
||||
// Start 启动短剧媒资导入消费者
|
||||
func Start() {
|
||||
playletResourceImportWorker.mu.Lock()
|
||||
defer playletResourceImportWorker.mu.Unlock()
|
||||
if playletResourceImportWorker.cancel != nil {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
playletResourceImportWorker.cancel = cancel
|
||||
go runPlayletResourceImportWorker(ctx)
|
||||
go runPlayletCoverCompressWorker(ctx)
|
||||
go runPlayletSerialSyncWorker(ctx)
|
||||
go func() {
|
||||
time.Sleep(10 * time.Second)
|
||||
EnqueuePlayletCoverCompressJobs()
|
||||
}()
|
||||
log.Info("短剧媒资后台消费者已启动")
|
||||
}
|
||||
|
||||
// Stop 停止短剧媒资导入消费者
|
||||
func Stop() {
|
||||
playletResourceImportWorker.mu.Lock()
|
||||
defer playletResourceImportWorker.mu.Unlock()
|
||||
if playletResourceImportWorker.cancel == nil {
|
||||
return
|
||||
}
|
||||
playletResourceImportWorker.cancel()
|
||||
playletResourceImportWorker.cancel = nil
|
||||
log.Info("短剧媒资后台消费者已停止")
|
||||
}
|
||||
|
||||
// pushPlayletResourceImportJob 投递短剧媒资导入任务
|
||||
func pushPlayletResourceImportJob(ctx context.Context, ids []uint, operatorId int64, operatorName string) *srvdef.SrvErr {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Error("投递短剧媒资导入任务取消", log.E(ctx.Err()))
|
||||
return srvdef.New(srvdef.Failure, "提交同步任务失败", "提交同步任务失败")
|
||||
default:
|
||||
}
|
||||
job := playletResourceImportJob{
|
||||
Ids: ids,
|
||||
OperatorId: operatorId,
|
||||
OperatorName: operatorName,
|
||||
CreateTime: time.Now().Unix(),
|
||||
}
|
||||
payload, err := json.Marshal(job)
|
||||
if err != nil {
|
||||
log.Error("序列化短剧媒资导入任务失败", log.Any("ids", ids), log.E(err))
|
||||
return srvdef.New(srvdef.Failure, "提交同步任务失败", "提交同步任务失败")
|
||||
}
|
||||
if err = redis.RPush(playletResourceImportQueueKey(), string(payload)); err != nil {
|
||||
log.Error("投递短剧媒资导入任务失败", log.Any("ids", ids), log.E(err))
|
||||
return srvdef.New(srvdef.Failure, "提交同步任务失败", "提交同步任务失败")
|
||||
}
|
||||
log.Info("投递短剧媒资导入任务成功", log.Any("ids", ids), log.Any("count", len(ids)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// runPlayletResourceImportWorker 循环消费短剧媒资导入队列
|
||||
func runPlayletResourceImportWorker(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
result, err := redis.Client().BLPop(playletResourceImportPopWait, playletResourceImportQueueKey()).Result()
|
||||
if err != nil {
|
||||
if err == goredis.Nil {
|
||||
continue
|
||||
}
|
||||
log.Error("消费短剧媒资导入任务失败", log.E(err))
|
||||
time.Sleep(playletResourceImportRetryWait)
|
||||
continue
|
||||
}
|
||||
if len(result) < 2 {
|
||||
log.Error("短剧媒资导入任务格式异常", log.Any("result", result))
|
||||
continue
|
||||
}
|
||||
handlePlayletResourceImportPayload(ctx, result[1])
|
||||
}
|
||||
}
|
||||
|
||||
// handlePlayletResourceImportPayload 处理单个短剧媒资导入任务
|
||||
func handlePlayletResourceImportPayload(ctx context.Context, payload string) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Error("处理短剧媒资导入任务异常", log.Any("err", err), log.Any("payload", payload))
|
||||
}
|
||||
}()
|
||||
var job playletResourceImportJob
|
||||
if err := json.Unmarshal([]byte(payload), &job); err != nil {
|
||||
log.Error("解析短剧媒资导入任务失败", log.Any("payload", payload), log.E(err))
|
||||
return
|
||||
}
|
||||
ids := dedupePlayletResourceIds(job.Ids)
|
||||
if len(ids) == 0 {
|
||||
log.Error("短剧媒资导入任务没有有效媒资ID", log.Any("payload", payload))
|
||||
return
|
||||
}
|
||||
log.Info("开始处理短剧媒资导入任务",
|
||||
log.Any("ids", ids),
|
||||
log.Any("count", len(ids)),
|
||||
log.Any("createTime", job.CreateTime),
|
||||
log.Any("operatorId", job.OperatorId),
|
||||
log.Any("operatorName", job.OperatorName))
|
||||
resp := importPlayletResourceBatch(ctx, ids, job.OperatorId, job.OperatorName)
|
||||
log.Info("短剧媒资导入任务处理完成",
|
||||
log.Any("ids", ids),
|
||||
log.Any("successCount", len(resp.Success)),
|
||||
log.Any("failedCount", len(resp.Failed)),
|
||||
log.Any("failed", resp.Failed))
|
||||
}
|
||||
|
||||
// lockPlayletResourceImportMedia 锁定单条媒资导入
|
||||
func lockPlayletResourceImportMedia(mediaId uint) (bool, error) {
|
||||
return redis.Setnx_NewOK(playletResourceImportLockKey(mediaId), true, playletResourceImportLockTTL)
|
||||
}
|
||||
|
||||
// unlockPlayletResourceImportMedia 释放单条媒资导入锁
|
||||
func unlockPlayletResourceImportMedia(mediaId uint) {
|
||||
if err := redis.Del(playletResourceImportLockKey(mediaId)); err != nil {
|
||||
log.Error("释放短剧媒资导入锁失败", log.Any("mediaId", mediaId), log.E(err))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package srv_mediaresource
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/httputil"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/web/api/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
playletMediaListErrMsg = "短剧媒资列表查询失败"
|
||||
playletMediaSyncErrMsg = "短剧媒资同步失败"
|
||||
playletMediaDeletedCode = 7003
|
||||
)
|
||||
|
||||
// playletMediaCenterService 短剧媒资中心客户端
|
||||
type playletMediaCenterService struct {
|
||||
url string // 媒资中心接口域名
|
||||
proId string // 媒资中心商户号
|
||||
secret string // 媒资中心签名密钥,当前接口按源项目逻辑暂不签名
|
||||
}
|
||||
|
||||
// playletMediaCenterConfig 创建短剧媒资中心客户端所需配置
|
||||
type playletMediaCenterConfig struct {
|
||||
Url string // 媒资中心接口域名
|
||||
ProId string // 媒资中心商户号
|
||||
Secret string // 媒资中心签名密钥
|
||||
}
|
||||
|
||||
// playletMediaCenterMsg 媒资中心统一响应结构
|
||||
type playletMediaCenterMsg struct {
|
||||
Code int `json:"code"` // 媒资中心业务码,0 表示成功
|
||||
Msg string `json:"msg"` // 媒资中心业务消息
|
||||
Data json.RawMessage `json:"data"` // 媒资中心业务数据,校验 code 成功后再解析
|
||||
}
|
||||
|
||||
// playletMediaPagination 媒资中心列表分页信息
|
||||
type playletMediaPagination struct {
|
||||
IsLastPage bool `json:"is_last_page"` // 是否最后一页
|
||||
Page uint `json:"page"` // 当前页码
|
||||
Size uint `json:"size"` // 每页数量
|
||||
Count uint `json:"count"` // 总数量
|
||||
}
|
||||
|
||||
// playletMediaListReq 媒资中心 /services/media/list 查询参数
|
||||
type playletMediaListReq struct {
|
||||
MediaType int `json:"media_type"` // 媒体类型:1-动漫 2-漫画 4-漫画短剧 5-短剧 6-普通短剧
|
||||
Id *uint `json:"id"` // 媒资中心资源ID
|
||||
VloggerId *uint `json:"vlogger_id"` // UP主ID
|
||||
IsUp *int `json:"is_up"` // 是否UP主资源:0-否 1-是
|
||||
Title *string `json:"title"` // 标题筛选
|
||||
Tag *string `json:"tag"` // 标签筛选
|
||||
SyncType int `json:"sync_type"` // 同步类型:1-全量 2-增量
|
||||
Page uint `json:"page"` // 页码
|
||||
Size uint `json:"size"` // 每页数量
|
||||
}
|
||||
|
||||
// playletMediaListResp 媒资中心列表响应 data 节点
|
||||
type playletMediaListResp struct {
|
||||
Data []*proto.PlayletResourceListItem `json:"data"` // 短剧媒资列表
|
||||
Pagination playletMediaPagination `json:"pagination"` // 分页信息
|
||||
}
|
||||
|
||||
// playletMediaContent 媒资中心短剧章节内容
|
||||
type playletMediaContent struct {
|
||||
ID uint `json:"id"` // 媒资内容ID
|
||||
SaleCoin uint `json:"sale_coin"` // 单集售价
|
||||
VloggerId uint `json:"vlogger_id"` // UP主ID
|
||||
Cover string `json:"cover"` // 封面
|
||||
MediaId uint `json:"video_id"` // 媒资中心资源ID
|
||||
MediaType int `json:"media_type"` // 媒体类型
|
||||
EpisodeNumber int `json:"episode_number"` // 第几集
|
||||
Title string `json:"title"` // 章节名
|
||||
Text string `json:"text"` // 文本内容
|
||||
Urls string `json:"urls"` // 资源地址集
|
||||
Height uint `json:"height"` // 高
|
||||
Width uint `json:"width"` // 宽
|
||||
Ratio float64 `json:"ratio"` // 宽高比
|
||||
MediaSize uint `json:"media_size"` // 资源大小
|
||||
PlayTime uint `json:"play_time"` // 播放时长
|
||||
M3u8Url string `json:"m3u8_url"` // m3u8视频地址
|
||||
AudioUrl string `json:"audio_url"` // 音频地址
|
||||
FsResourceId string `json:"fs_resource_id"` // 文件服务资源ID
|
||||
FsResourceStatus string `json:"fs_resource_status"` // 文件服务资源状态
|
||||
HashId string `json:"hash_id"` // 资源hash
|
||||
DataVersion int8 `json:"data_version"` // 数据版本
|
||||
TerminalCdo string `json:"terminal_cdo"` // 上传者
|
||||
CreateTime int64 `json:"create_time"` // 创建时间
|
||||
UpdateTime int64 `json:"update_time"` // 更新时间
|
||||
}
|
||||
|
||||
// playletMediaSyncReq 媒资中心 /services/media/sync 查询参数
|
||||
type playletMediaSyncReq struct {
|
||||
Id uint `json:"id"` // 媒资中心资源ID
|
||||
SyncTime int64 `json:"sync_time"` // 增量同步时间,0 表示拉全量详情
|
||||
}
|
||||
|
||||
// playletMediaSyncResp 媒资中心同步响应 data 节点
|
||||
type playletMediaSyncResp struct {
|
||||
IsDelete bool `json:"-"`
|
||||
Media *proto.PlayletResourceListItem `json:"media"`
|
||||
MediaContentList []playletMediaContent `json:"media_content_list"`
|
||||
}
|
||||
|
||||
// newPlayletMediaCenterService 创建短剧媒资中心客户端
|
||||
func newPlayletMediaCenterService(c playletMediaCenterConfig) *playletMediaCenterService {
|
||||
return &playletMediaCenterService{
|
||||
url: c.Url,
|
||||
proId: c.ProId,
|
||||
secret: c.Secret,
|
||||
}
|
||||
}
|
||||
|
||||
// mediaListParams 构造媒资中心列表查询参数
|
||||
func (m *playletMediaCenterService) mediaListParams(p *playletMediaListReq) map[string]string {
|
||||
params := map[string]string{
|
||||
"media_type": strconv.Itoa(p.MediaType),
|
||||
"sync_type": strconv.Itoa(p.SyncType),
|
||||
"page": strconv.FormatUint(uint64(p.Page), 10),
|
||||
"size": strconv.FormatUint(uint64(p.Size), 10),
|
||||
"pro_id": m.proId,
|
||||
"sign": m.sign(),
|
||||
}
|
||||
if p.Id != nil {
|
||||
params["id"] = strconv.FormatUint(uint64(*p.Id), 10)
|
||||
}
|
||||
if p.VloggerId != nil {
|
||||
params["vlogger_id"] = strconv.FormatUint(uint64(*p.VloggerId), 10)
|
||||
}
|
||||
if p.IsUp != nil {
|
||||
params["is_up"] = strconv.Itoa(*p.IsUp)
|
||||
}
|
||||
if p.Title != nil {
|
||||
params["title"] = *p.Title
|
||||
}
|
||||
if p.Tag != nil {
|
||||
params["tag"] = *p.Tag
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
// mediaSyncParams 构造媒资中心同步查询参数
|
||||
func (m *playletMediaCenterService) mediaSyncParams(p *playletMediaSyncReq) map[string]string {
|
||||
return map[string]string{
|
||||
"id": strconv.FormatUint(uint64(p.Id), 10),
|
||||
"sync_time": strconv.FormatInt(p.SyncTime, 10),
|
||||
"pro_id": m.proId,
|
||||
"sign": m.sign(),
|
||||
}
|
||||
}
|
||||
|
||||
// mediaList 查询短剧媒资列表
|
||||
func (m *playletMediaCenterService) mediaList(p *playletMediaListReq) (result playletMediaListResp, err error) {
|
||||
var retMsg playletMediaCenterMsg
|
||||
params := m.mediaListParams(p)
|
||||
path := "/services/media/list"
|
||||
log.Info("查询短剧媒资列表", log.Any("params", params))
|
||||
code, err := httputil.Client().GetWithJResp(&retMsg, common.BindUrl(m.url, path), nil, params)
|
||||
if err != nil {
|
||||
log.Error("查询短剧媒资列表请求失败", log.Any("code", code), log.E(err))
|
||||
return result, errors.New(playletMediaListErrMsg)
|
||||
}
|
||||
if code != http.StatusOK {
|
||||
log.Error("查询短剧媒资列表状态码异常", log.Any("code", code))
|
||||
return result, errors.New(playletMediaListErrMsg)
|
||||
}
|
||||
if retMsg.Code != 0 {
|
||||
log.Error("查询短剧媒资列表业务码异常", log.Any("retMsg", retMsg))
|
||||
return result, errors.New(playletMediaListErrMsg)
|
||||
}
|
||||
if err = json.Unmarshal(retMsg.Data, &result); err != nil {
|
||||
log.Error("解析短剧媒资列表失败", log.E(err))
|
||||
return result, errors.New(playletMediaListErrMsg)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// mediaSync 查询短剧媒资详情和章节内容
|
||||
func (m *playletMediaCenterService) mediaSync(p *playletMediaSyncReq) (result playletMediaSyncResp, err error) {
|
||||
var retMsg playletMediaCenterMsg
|
||||
params := m.mediaSyncParams(p)
|
||||
path := "/services/media/sync"
|
||||
code, err := httputil.Client().GetWithJResp(&retMsg, common.BindUrl(m.url, path), nil, params)
|
||||
if err != nil {
|
||||
log.Error("同步短剧媒资详情请求失败", log.Any("params", params), log.Any("code", code), log.E(err))
|
||||
return result, errors.New(playletMediaSyncErrMsg)
|
||||
}
|
||||
if code != http.StatusOK {
|
||||
log.Error("同步短剧媒资详情状态码异常", log.Any("params", params), log.Any("code", code))
|
||||
return result, errors.New(playletMediaSyncErrMsg)
|
||||
}
|
||||
if retMsg.Code == playletMediaDeletedCode {
|
||||
result.IsDelete = true
|
||||
return result, nil
|
||||
}
|
||||
if retMsg.Code != 0 {
|
||||
log.Error("同步短剧媒资详情业务码异常", log.Any("params", params), log.Any("retMsg", retMsg))
|
||||
return result, errors.New(playletMediaSyncErrMsg)
|
||||
}
|
||||
if err = json.Unmarshal(retMsg.Data, &result); err != nil {
|
||||
log.Error("解析短剧媒资详情失败", log.Any("params", params), log.E(err))
|
||||
return result, errors.New(playletMediaSyncErrMsg)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// sign 生成媒资中心签名
|
||||
func (m *playletMediaCenterService) sign() string {
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
package srv_mediaresource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/playletmod"
|
||||
"videoweb/web/api/proto"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const (
|
||||
// defaultOriginalPlayletBackfillPageSize 与媒资库列表默认分页一致。
|
||||
defaultOriginalPlayletBackfillPageSize = int64(20)
|
||||
// defaultOriginalPlayletBackfillMaxPages 限制启动回填最多扫描 2000 条媒资。
|
||||
defaultOriginalPlayletBackfillMaxPages = int64(100)
|
||||
// maxOriginalPlayletBackfillMaxPages 防止误配置导致启动任务长时间扫全量。
|
||||
maxOriginalPlayletBackfillMaxPages = int64(1000)
|
||||
)
|
||||
|
||||
// originalPlayletBackfillOnce 保证每个进程只触发一次启动回填。
|
||||
var originalPlayletBackfillOnce sync.Once
|
||||
|
||||
// originalPlayletBackfillOptions 控制 CGDJ 原创短剧回填范围。
|
||||
type originalPlayletBackfillOptions struct {
|
||||
DryRun bool // 只预览不写库
|
||||
PageSize int64 // 媒资库分页大小
|
||||
MaxPages int64 // 最多扫描页数
|
||||
}
|
||||
|
||||
// originalPlayletBackfillItem 记录单部已导入短剧的回填结果。
|
||||
type originalPlayletBackfillItem struct {
|
||||
MediaCenterId int64 // 媒资库主资源 ID
|
||||
PlayletId int64 // 本地短剧 ID
|
||||
Title string // 本地短剧标题
|
||||
MediaTitle string // 媒资库原始标题
|
||||
Source string // 本次识别出的媒资来源
|
||||
Before string // 回填前媒资来源
|
||||
Result string // updated / preview / already_marked / conflict / failed
|
||||
}
|
||||
|
||||
// originalPlayletBackfillResp 汇总一次 CGDJ 回填统计。
|
||||
type originalPlayletBackfillResp struct {
|
||||
DryRun bool
|
||||
PageSize int64
|
||||
MaxPages int64
|
||||
Pages int64
|
||||
SourceTotal int64 // 媒资库返回总数
|
||||
SourceScanned int64 // 已扫描媒资条数
|
||||
SourceMatched int64 // 匹配 CGDJ 前缀的媒资条数
|
||||
Imported int64 // 已导入本地短剧库的条数
|
||||
Matched int64 // 需要写入媒资来源的条数
|
||||
Updated int64 // 实际更新成功条数
|
||||
Skipped int64 // 已有相同来源或并发冲突跳过条数
|
||||
HasMore bool // 达到 MaxPages 后仍可能存在后续数据
|
||||
List []*originalPlayletBackfillItem
|
||||
}
|
||||
|
||||
// originalPlayletBackfillStore 抽象外部依赖,方便单测覆盖媒资库和 Mongo。
|
||||
type originalPlayletBackfillStore struct {
|
||||
mediaList func(*playletMediaListReq) (playletMediaListResp, error)
|
||||
findMany func(context.Context, bson.M, *options.FindOptions) (playletmod.PlayletList, *srvdef.SrvErr)
|
||||
updateOne func(context.Context, bson.M, bson.M) *srvdef.SrvErr
|
||||
audit func(context.Context, *originalPlayletBackfillItem)
|
||||
}
|
||||
|
||||
// StartCGDJOriginalBackfillOnce 在 web 启动后异步执行一次 CGDJ 原创短剧标记回填。
|
||||
func StartCGDJOriginalBackfillOnce(ctx context.Context) {
|
||||
originalPlayletBackfillOnce.Do(func() {
|
||||
go runCGDJOriginalBackfillOnce(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
// runCGDJOriginalBackfillOnce 执行启动回填,成功后顺手触发一次昨日数据回流。
|
||||
func runCGDJOriginalBackfillOnce(ctx context.Context) {
|
||||
resp, serr := backfillCGDJOriginalPlaylets(ctx, originalPlayletBackfillOptions{DryRun: false})
|
||||
if serr != nil {
|
||||
log.Error("CGDJ 原创短剧标记启动回填失败", log.Any("err", serr), log.Any("resp", resp))
|
||||
return
|
||||
}
|
||||
log.Info("CGDJ 原创短剧标记启动回填完成",
|
||||
log.Any("pages", resp.Pages),
|
||||
log.Any("sourceTotal", resp.SourceTotal),
|
||||
log.Any("sourceScanned", resp.SourceScanned),
|
||||
log.Any("sourceMatched", resp.SourceMatched),
|
||||
log.Any("imported", resp.Imported),
|
||||
log.Any("matched", resp.Matched),
|
||||
log.Any("updated", resp.Updated),
|
||||
log.Any("skipped", resp.Skipped),
|
||||
log.Any("hasMore", resp.HasMore))
|
||||
ReportAIContentBackflowYesterday()
|
||||
}
|
||||
|
||||
// backfillCGDJOriginalPlaylets 从媒资库 CGDJ 短剧列表回填本地短剧 mediaBusiness。
|
||||
// 判断依据只使用源媒资标题前缀和 mediaCenterId,不依赖本地短剧标题,避免运营改名后漏判。
|
||||
func backfillCGDJOriginalPlaylets(ctx context.Context, opts originalPlayletBackfillOptions) (*originalPlayletBackfillResp, *srvdef.SrvErr) {
|
||||
ms := newPlayletMediaCenterFromConfig()
|
||||
return backfillCGDJOriginalPlayletsWithStore(ctx, opts, originalPlayletBackfillStore{
|
||||
mediaList: ms.mediaList,
|
||||
findMany: func(ctx context.Context, filter bson.M, opts *options.FindOptions) (playletmod.PlayletList, *srvdef.SrvErr) {
|
||||
return playletmod.FindManyPlaylet(ctx, filter, opts)
|
||||
},
|
||||
updateOne: playletmod.UpdateOnePlaylet,
|
||||
audit: func(ctx context.Context, item *originalPlayletBackfillItem) {
|
||||
log.Info("回填短剧媒资业务来源", log.Any("playletId", item.PlayletId), log.Any("mediaCenterId", item.MediaCenterId), log.Any("title", item.Title), log.Any("mediaTitle", item.MediaTitle), log.Any("mediaBusiness", item.Source))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// backfillCGDJOriginalPlayletsWithStore 按媒资库 CGDJ 搜索结果回填本地短剧媒资来源。
|
||||
func backfillCGDJOriginalPlayletsWithStore(ctx context.Context, opts originalPlayletBackfillOptions, store originalPlayletBackfillStore) (*originalPlayletBackfillResp, *srvdef.SrvErr) {
|
||||
pageSize, maxPages, serr := normalizeOriginalPlayletBackfillOptions(opts)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
resp := &originalPlayletBackfillResp{
|
||||
DryRun: opts.DryRun,
|
||||
PageSize: pageSize,
|
||||
MaxPages: maxPages,
|
||||
List: make([]*originalPlayletBackfillItem, 0),
|
||||
}
|
||||
for _, title := range playletMediaBusinessSearchTitles() {
|
||||
searchTitle := title
|
||||
for page := int64(1); page <= maxPages; page++ {
|
||||
data, err := store.mediaList(&playletMediaListReq{
|
||||
MediaType: playletResourceMediaTypeShort,
|
||||
Title: &searchTitle,
|
||||
SyncType: defaultResourceSyncType,
|
||||
Page: uint(page),
|
||||
Size: uint(pageSize),
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("查询 CGDJ 短剧媒资失败", log.Any("title", searchTitle), log.Any("page", page), log.E(err))
|
||||
return resp, srvdef.New(srvdef.Failure, "查询 CGDJ 短剧媒资失败", "查询 CGDJ 短剧媒资失败")
|
||||
}
|
||||
resp.Pages++
|
||||
if page == 1 {
|
||||
resp.SourceTotal += int64(data.Pagination.Count)
|
||||
}
|
||||
if len(data.Data) == 0 {
|
||||
break
|
||||
}
|
||||
if serr = backfillOriginalPlayletPage(ctx, data.Data, resp, store); serr != nil {
|
||||
return resp, serr
|
||||
}
|
||||
if data.Pagination.IsLastPage || int64(len(data.Data)) < pageSize {
|
||||
break
|
||||
}
|
||||
if page == maxPages {
|
||||
resp.HasMore = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// normalizeOriginalPlayletBackfillOptions 修正分页参数并限制启动扫描范围。
|
||||
func normalizeOriginalPlayletBackfillOptions(opts originalPlayletBackfillOptions) (int64, int64, *srvdef.SrvErr) {
|
||||
pageSize := opts.PageSize
|
||||
if pageSize <= 0 {
|
||||
pageSize = defaultOriginalPlayletBackfillPageSize
|
||||
}
|
||||
maxPages := opts.MaxPages
|
||||
if maxPages <= 0 {
|
||||
maxPages = defaultOriginalPlayletBackfillMaxPages
|
||||
}
|
||||
if pageSize > int64(maxResourcePageSize) {
|
||||
return 0, 0, srvdef.New(srvdef.ErrParamError, "pageSize最多100", "pageSize最多100")
|
||||
}
|
||||
if maxPages > maxOriginalPlayletBackfillMaxPages {
|
||||
return 0, 0, srvdef.New(srvdef.ErrParamError, "maxPages最多1000", "maxPages最多1000")
|
||||
}
|
||||
return pageSize, maxPages, nil
|
||||
}
|
||||
|
||||
// backfillOriginalPlayletPage 只更新已导入本地短剧库的 CGDJ 媒资。
|
||||
func backfillOriginalPlayletPage(ctx context.Context, mediaList []*proto.PlayletResourceListItem, resp *originalPlayletBackfillResp, store originalPlayletBackfillStore) *srvdef.SrvErr {
|
||||
mediaTitleByID := make(map[uint]string, len(mediaList))
|
||||
mediaBusinessByID := make(map[uint]string, len(mediaList))
|
||||
mediaIDs := make([]uint, 0, len(mediaList))
|
||||
for _, media := range mediaList {
|
||||
resp.SourceScanned++
|
||||
business := playletMediaBusiness(media)
|
||||
if business == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := mediaTitleByID[media.ID]; exists {
|
||||
continue
|
||||
}
|
||||
resp.SourceMatched++
|
||||
mediaTitleByID[media.ID] = media.Title
|
||||
mediaBusinessByID[media.ID] = business
|
||||
mediaIDs = append(mediaIDs, media.ID)
|
||||
}
|
||||
if len(mediaIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
playlets, serr := store.findMany(ctx, bson.M{
|
||||
playletmod.FN.Playlet.MediaCenterId: bson.M{"$in": mediaIDs},
|
||||
playletmod.FN.Playlet.Status: playletmod.StatusUp,
|
||||
}, options.Find().SetProjection(bson.M{
|
||||
playletmod.FN.Playlet.Id: 1,
|
||||
playletmod.FN.Playlet.MediaCenterId: 1,
|
||||
playletmod.FN.Playlet.Title: 1,
|
||||
playletmod.FN.Playlet.MediaBusiness: 1,
|
||||
}))
|
||||
if serr != nil {
|
||||
log.Error("查询已导入 CGDJ 短剧失败", log.Any("mediaIds", mediaIDs), log.Any("err", serr))
|
||||
return serr
|
||||
}
|
||||
for _, playlet := range playlets {
|
||||
if playlet == nil || playlet.MediaCenterId == 0 {
|
||||
continue
|
||||
}
|
||||
business := mediaBusinessByID[playlet.MediaCenterId]
|
||||
if business == "" {
|
||||
continue
|
||||
}
|
||||
resp.Imported++
|
||||
item := &originalPlayletBackfillItem{
|
||||
MediaCenterId: int64(playlet.MediaCenterId),
|
||||
PlayletId: playlet.Id,
|
||||
Title: playlet.Title,
|
||||
MediaTitle: mediaTitleByID[playlet.MediaCenterId],
|
||||
Source: business,
|
||||
Before: playlet.MediaBusiness,
|
||||
}
|
||||
if playlet.MediaBusiness == business {
|
||||
resp.Skipped++
|
||||
item.Result = "already_marked"
|
||||
resp.List = append(resp.List, item)
|
||||
continue
|
||||
}
|
||||
resp.Matched++
|
||||
if resp.DryRun {
|
||||
item.Result = "preview"
|
||||
resp.List = append(resp.List, item)
|
||||
continue
|
||||
}
|
||||
serr = store.updateOne(ctx, bson.M{
|
||||
playletmod.FN.Playlet.Id: playlet.Id,
|
||||
playletmod.FN.Playlet.MediaCenterId: playlet.MediaCenterId,
|
||||
playletmod.FN.Playlet.Status: playletmod.StatusUp,
|
||||
playletmod.FN.Playlet.MediaBusiness: bson.M{"$ne": business},
|
||||
}, bson.M{"$set": bson.M{
|
||||
playletmod.FN.Playlet.MediaBusiness: business,
|
||||
}})
|
||||
if serr != nil {
|
||||
if serr.Code == srvdef.ErrNoData {
|
||||
resp.Skipped++
|
||||
item.Result = "conflict"
|
||||
resp.List = append(resp.List, item)
|
||||
continue
|
||||
}
|
||||
item.Result = "failed"
|
||||
resp.List = append(resp.List, item)
|
||||
return serr
|
||||
}
|
||||
resp.Updated++
|
||||
item.Result = "updated"
|
||||
resp.List = append(resp.List, item)
|
||||
if store.audit != nil {
|
||||
store.audit(ctx, item)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,678 @@
|
||||
package srv_mediaresource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/slugx"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/mwacc"
|
||||
"videoweb/pub/models/playletchaptermod"
|
||||
"videoweb/pub/models/playletmod"
|
||||
"videoweb/web/api/proto"
|
||||
"videoweb/web/middleware/auth"
|
||||
"videoweb/web/webg"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultResourceSyncType = 1
|
||||
defaultResourcePageNum = 1
|
||||
defaultResourcePageSize = 20
|
||||
maxResourcePageSize = 100
|
||||
maxResourceImportIds = 100
|
||||
playletResourceImportMsg = "同步任务已提交,后台正在处理"
|
||||
playletHeatMinValue = 1000
|
||||
playletHeatMaxValue = 5000
|
||||
|
||||
playletResourceMediaTypeAnime = 1 // 动漫
|
||||
playletResourceMediaTypeComic = 4 // 漫画短剧
|
||||
playletResourceMediaTypeShort = 5 // 短剧
|
||||
playletResourceMediaTypeNormal = 6 // 普通短剧
|
||||
)
|
||||
|
||||
// ListPlayletResource 分页查询短剧媒资列表
|
||||
func ListPlayletResource(ctx context.Context, req *proto.PlayletResourceListReq) (*proto.PlayletResourceListResp, *srvdef.SrvErr) {
|
||||
if req == nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||||
}
|
||||
syncType := defaultResourceSyncType
|
||||
if req.SyncType != nil {
|
||||
syncType = *req.SyncType
|
||||
}
|
||||
pageNumber := req.PageNumber
|
||||
if pageNumber <= 0 {
|
||||
pageNumber = defaultResourcePageNum
|
||||
}
|
||||
pageSize := req.PageSize
|
||||
if pageSize <= 0 {
|
||||
pageSize = defaultResourcePageSize
|
||||
}
|
||||
if pageSize > maxResourcePageSize {
|
||||
pageSize = maxResourcePageSize
|
||||
}
|
||||
mediaType, err1 := resolvePlayletResourceMediaType(req.MediaType)
|
||||
if err1 != nil {
|
||||
return nil, err1
|
||||
}
|
||||
ms := newPlayletMediaCenterFromConfig()
|
||||
data, err := ms.mediaList(&playletMediaListReq{
|
||||
MediaType: mediaType,
|
||||
Title: req.Title,
|
||||
Tag: req.Tag,
|
||||
SyncType: syncType,
|
||||
Page: uint(pageNumber),
|
||||
Size: uint(pageSize),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, srvdef.New(srvdef.Failure, err.Error(), "")
|
||||
}
|
||||
if err := fillPlayletResourceAddStatus(ctx, data.Data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &proto.PlayletResourceListResp{
|
||||
List: data.Data,
|
||||
Total: data.Pagination.Count,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListPlayletResourceChapters 查询短剧媒资剧集信息
|
||||
func ListPlayletResourceChapters(ctx context.Context, req *proto.PlayletResourceChapterListReq) (*proto.PlayletResourceChapterListResp, *srvdef.SrvErr) {
|
||||
if req == nil || req.Id == 0 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "媒资ID不能为空", "")
|
||||
}
|
||||
ms := newPlayletMediaCenterFromConfig()
|
||||
result, err := ms.mediaSync(&playletMediaSyncReq{
|
||||
Id: req.Id,
|
||||
SyncTime: 0,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, srvdef.New(srvdef.Failure, err.Error(), "")
|
||||
}
|
||||
if result.IsDelete || result.Media == nil {
|
||||
return nil, srvdef.New(srvdef.ErrNoData, "媒资不存在或已删除", "")
|
||||
}
|
||||
list := make([]*proto.PlayletResourceChapterItem, 0, len(result.MediaContentList))
|
||||
for _, item := range result.MediaContentList {
|
||||
videoUrl := strings.TrimSpace(item.M3u8Url)
|
||||
if videoUrl == "" {
|
||||
videoUrl = strings.TrimSpace(item.Urls)
|
||||
}
|
||||
list = append(list, &proto.PlayletResourceChapterItem{
|
||||
Id: item.ID,
|
||||
EpisodeNumber: item.EpisodeNumber,
|
||||
Title: strings.TrimSpace(item.Title),
|
||||
Cover: item.Cover,
|
||||
VideoUrl: videoUrl,
|
||||
Height: item.Height,
|
||||
Width: item.Width,
|
||||
MediaSize: item.MediaSize,
|
||||
PlayTime: item.PlayTime,
|
||||
SaleCoin: item.SaleCoin,
|
||||
HashId: item.HashId,
|
||||
CreateTime: item.CreateTime,
|
||||
UpdateTime: item.UpdateTime,
|
||||
})
|
||||
}
|
||||
return &proto.PlayletResourceChapterListResp{
|
||||
MediaId: req.Id,
|
||||
Title: result.Media.Title,
|
||||
List: list,
|
||||
Total: len(list),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// resolvePlayletResourceMediaType 校验后台显式传入的媒资类型
|
||||
func resolvePlayletResourceMediaType(mediaType *int) (int, *srvdef.SrvErr) {
|
||||
if mediaType == nil {
|
||||
return 0, srvdef.New(srvdef.ErrParamError, "媒体类型不能为空", "媒体类型不能为空")
|
||||
}
|
||||
if !validPlayletResourceMediaType(*mediaType) {
|
||||
return 0, srvdef.New(srvdef.ErrParamError, "媒体类型无效,仅支持1-动漫、2-漫画、4-漫画短剧、5-短剧、6-普通短剧", "媒体类型无效,仅支持1-动漫、2-漫画、4-漫画短剧、5-短剧、6-普通短剧")
|
||||
}
|
||||
return *mediaType, nil
|
||||
}
|
||||
|
||||
// validPlayletResourceMediaType 媒资列表当前允许的媒体类型
|
||||
func validPlayletResourceMediaType(mediaType int) bool {
|
||||
switch mediaType {
|
||||
case playletResourceMediaTypeAnime, playletResourceMediaTypeComic, playletResourceMediaTypeShort, playletResourceMediaTypeNormal:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// validPlayletImportMediaType 允许入库的媒资类型
|
||||
func validPlayletImportMediaType(mediaType int) bool {
|
||||
switch mediaType {
|
||||
case playletResourceMediaTypeAnime, playletResourceMediaTypeComic, playletResourceMediaTypeShort, playletResourceMediaTypeNormal:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// contentTypeFromMediaType 媒资中心 media_type 映射为本项目 contentType
|
||||
func contentTypeFromMediaType(mediaType int) playletmod.ContentType {
|
||||
switch mediaType {
|
||||
case playletResourceMediaTypeAnime:
|
||||
return playletmod.ContentTypeAnime
|
||||
case playletResourceMediaTypeComic:
|
||||
return playletmod.ContentTypeComic
|
||||
case playletResourceMediaTypeNormal:
|
||||
return playletmod.ContentTypeAlbum
|
||||
default:
|
||||
return playletmod.ContentTypeTheater
|
||||
}
|
||||
}
|
||||
|
||||
// fillPlayletResourceAddStatus 回填媒资列表是否已加入后台短剧库
|
||||
func fillPlayletResourceAddStatus(ctx context.Context, list []*proto.PlayletResourceListItem) *srvdef.SrvErr {
|
||||
mediaIds := make([]uint, 0, len(list))
|
||||
seen := make(map[uint]struct{}, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil || item.ID == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[item.ID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[item.ID] = struct{}{}
|
||||
mediaIds = append(mediaIds, item.ID)
|
||||
}
|
||||
if len(mediaIds) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
playletList, err := playletmod.FindManyPlaylet(ctx, bson.M{
|
||||
playletmod.FN.Playlet.MediaCenterId: bson.M{"$in": mediaIds},
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("查询短剧媒资加入状态失败", log.Any("mediaIds", mediaIds), log.Any("err", err))
|
||||
return err
|
||||
}
|
||||
addedMap := make(map[uint]bool, len(playletList))
|
||||
for _, item := range playletList {
|
||||
if item == nil || item.MediaCenterId == 0 {
|
||||
continue
|
||||
}
|
||||
addedMap[item.MediaCenterId] = true
|
||||
}
|
||||
for _, item := range list {
|
||||
if item != nil {
|
||||
item.IsAdd = addedMap[item.ID]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ImportPlayletResource 提交短剧媒资入库任务
|
||||
func ImportPlayletResource(ctx context.Context, req *proto.PlayletResourceUseReq) (string, *srvdef.SrvErr) {
|
||||
ids, err := validatePlayletResourceImportReq(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
operatorId, operatorName := resolveImportOperator(ctx)
|
||||
if err := pushPlayletResourceImportJob(ctx, ids, operatorId, operatorName); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return playletResourceImportMsg, nil
|
||||
}
|
||||
|
||||
// importPlayletResourceBatch 批量处理短剧媒资入库任务
|
||||
func importPlayletResourceBatch(ctx context.Context, ids []uint, operatorId int64, operatorName string) *proto.PlayletResourceUseResp {
|
||||
resp := &proto.PlayletResourceUseResp{
|
||||
Success: make([]*proto.PlayletResourceUseSuccess, 0, len(ids)),
|
||||
Failed: make([]*proto.PlayletResourceUseFailed, 0),
|
||||
}
|
||||
importedMap, err := findImportedPlayletResourceMap(ctx, ids)
|
||||
if err != nil {
|
||||
for _, id := range ids {
|
||||
resp.Failed = append(resp.Failed, &proto.PlayletResourceUseFailed{
|
||||
MediaId: id,
|
||||
Reason: "查询导入状态失败",
|
||||
})
|
||||
}
|
||||
return resp
|
||||
}
|
||||
ms := newPlayletMediaCenterFromConfig()
|
||||
for _, id := range ids {
|
||||
if importedMap[id] {
|
||||
resp.Failed = append(resp.Failed, &proto.PlayletResourceUseFailed{
|
||||
MediaId: id,
|
||||
Reason: "该媒资已导入",
|
||||
})
|
||||
continue
|
||||
}
|
||||
success, reason := importSinglePlayletResource(ctx, ms, id, operatorId, operatorName)
|
||||
if reason != "" {
|
||||
resp.Failed = append(resp.Failed, &proto.PlayletResourceUseFailed{
|
||||
MediaId: id,
|
||||
Reason: reason,
|
||||
})
|
||||
continue
|
||||
}
|
||||
resp.Success = append(resp.Success, success)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// findImportedPlayletResourceMap 批量查询已经导入的媒资ID
|
||||
func findImportedPlayletResourceMap(ctx context.Context, ids []uint) (map[uint]bool, *srvdef.SrvErr) {
|
||||
result := make(map[uint]bool, len(ids))
|
||||
if len(ids) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
playletList, err := playletmod.FindManyPlaylet(ctx, bson.M{
|
||||
playletmod.FN.Playlet.MediaCenterId: bson.M{"$in": ids},
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("批量查询短剧媒资导入状态失败", log.Any("mediaIds", ids), log.Any("err", err))
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range playletList {
|
||||
if item == nil || item.MediaCenterId == 0 {
|
||||
continue
|
||||
}
|
||||
result[item.MediaCenterId] = true
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func newPlayletMediaCenterFromConfig() *playletMediaCenterService {
|
||||
return newPlayletMediaCenterService(playletMediaCenterConfig{
|
||||
Url: webg.Conf.MediaResourceDBCfg.BaseUrl,
|
||||
ProId: webg.Conf.MediaResourceDBCfg.Merchat,
|
||||
Secret: webg.Conf.MediaResourceDBCfg.Secret,
|
||||
})
|
||||
}
|
||||
|
||||
// validatePlayletResourceImportReq 校验短剧媒资入库请求
|
||||
func validatePlayletResourceImportReq(req *proto.PlayletResourceUseReq) ([]uint, *srvdef.SrvErr) {
|
||||
if req == nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
|
||||
}
|
||||
ids := dedupePlayletResourceIds(req.Ids)
|
||||
if len(ids) == 0 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "媒资ID不能为空", "")
|
||||
}
|
||||
if len(ids) > maxResourceImportIds {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "单次最多导入100条媒资", "")
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// dedupePlayletResourceIds 去重并过滤无效媒资ID
|
||||
func dedupePlayletResourceIds(ids []uint) []uint {
|
||||
result := make([]uint, 0, len(ids))
|
||||
seen := make(map[uint]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
|
||||
}
|
||||
|
||||
// importSinglePlayletResource 导入单条短剧媒资
|
||||
func importSinglePlayletResource(ctx context.Context, ms *playletMediaCenterService, mediaId uint, operatorId int64, operatorName string) (*proto.PlayletResourceUseSuccess, string) {
|
||||
locked, err := lockPlayletResourceImportMedia(mediaId)
|
||||
if err != nil {
|
||||
log.Error("获取短剧媒资导入锁失败", log.Any("mediaId", mediaId), log.E(err))
|
||||
return nil, "获取导入锁失败"
|
||||
}
|
||||
if !locked {
|
||||
return nil, "该媒资正在导入"
|
||||
}
|
||||
defer unlockPlayletResourceImportMedia(mediaId)
|
||||
|
||||
if reason := checkPlayletResourceImported(ctx, mediaId); reason != "" {
|
||||
return nil, reason
|
||||
}
|
||||
result, err := ms.mediaSync(&playletMediaSyncReq{
|
||||
Id: mediaId,
|
||||
SyncTime: 0,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("同步短剧媒资失败", log.Any("mediaId", mediaId), log.E(err))
|
||||
return nil, "同步媒资失败"
|
||||
}
|
||||
if result.IsDelete || result.Media == nil {
|
||||
return nil, "媒资不存在或已删除"
|
||||
}
|
||||
if !validPlayletImportMediaType(result.Media.MediaType) {
|
||||
return nil, "该媒资类型不支持导入短剧"
|
||||
}
|
||||
if len(result.MediaContentList) == 0 {
|
||||
return nil, "媒资章节为空"
|
||||
}
|
||||
|
||||
playlet := buildPlayletFromMedia(result.Media, operatorId, operatorName)
|
||||
playlet.MediaSyncTime = maxPlayletMediaContentUpdateTime(0, result.MediaContentList)
|
||||
if playlet.Title == "" {
|
||||
return nil, "媒资标题为空"
|
||||
}
|
||||
exists := func(s string) bool {
|
||||
item, _ := playletmod.FindOnePlaylet(ctx, slugx.Filter(playletmod.FN.Playlet.Slug, playletmod.FN.Playlet.Id, s, 0))
|
||||
return item != nil
|
||||
}
|
||||
if serr := slugx.Ensure(&playlet.Slug, utils.SlugSourceFromKeywordsOrTitle(nil, playlet.Title), exists); serr != nil {
|
||||
if serr.Tips != "" {
|
||||
return nil, serr.Tips
|
||||
}
|
||||
return nil, "slug生成失败"
|
||||
}
|
||||
// 原图继续写入 Cover;压缩成功才写 CompressedCover,失败保持空。
|
||||
playlet.CompressedCover = compressPlayletCoverOrEmpty(playlet.Cover)
|
||||
chapters := buildPlayletChaptersFromMedia(result.MediaContentList)
|
||||
if len(chapters) == 0 {
|
||||
return nil, "媒资章节为空"
|
||||
}
|
||||
if err1 := playletmod.InsertOnePlaylet(ctx, playlet); err1 != nil {
|
||||
log.Error("短剧媒资主表入库失败", log.Any("mediaId", mediaId), log.Any("err", err1))
|
||||
return nil, "主资源入库失败"
|
||||
}
|
||||
for _, chapter := range chapters {
|
||||
chapter.PlayletId = playlet.Id
|
||||
}
|
||||
if err1 := playletchaptermod.InsertManyPlayletChapter(ctx, chapters); err1 != nil {
|
||||
log.Error("短剧媒资章节批量入库失败", log.Any("mediaId", mediaId), log.Any("playletId", playlet.Id), log.Any("chapterCount", len(chapters)), log.Any("err", err1))
|
||||
rollbackImportedPlayletResource(ctx, playlet.Id, collectPlayletChapterIds(chapters))
|
||||
return nil, "章节入库失败"
|
||||
}
|
||||
return &proto.PlayletResourceUseSuccess{
|
||||
MediaId: mediaId,
|
||||
PlayletId: playlet.Id,
|
||||
}, ""
|
||||
}
|
||||
|
||||
// collectPlayletChapterIds 收集章节ID
|
||||
func collectPlayletChapterIds(chapters playletchaptermod.PlayletChapterList) []int64 {
|
||||
ids := make([]int64, 0, len(chapters))
|
||||
for _, chapter := range chapters {
|
||||
if chapter == nil || chapter.Id == 0 {
|
||||
continue
|
||||
}
|
||||
ids = append(ids, chapter.Id)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// checkPlayletResourceImported 检查媒资是否已经导入到本项目短剧表
|
||||
func checkPlayletResourceImported(ctx context.Context, mediaId uint) string {
|
||||
_, err := playletmod.FindOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.MediaCenterId: mediaId})
|
||||
if err == nil {
|
||||
return "该媒资已导入"
|
||||
}
|
||||
if err.Code != srvdef.ErrNoData {
|
||||
log.Error("查询短剧媒资导入状态失败", log.Any("mediaId", mediaId), log.Any("err", err))
|
||||
return "查询导入状态失败"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// buildPlayletFromMedia 将媒资中心主资源转换为本项目 playlet 主表模型
|
||||
func buildPlayletFromMedia(media *proto.PlayletResourceListItem, operatorId int64, operatorName string) *playletmod.Playlet {
|
||||
cover := strings.TrimSpace(media.VerticalCover)
|
||||
if cover == "" {
|
||||
cover = strings.TrimSpace(media.HorizontalCover)
|
||||
}
|
||||
return &playletmod.Playlet{
|
||||
MediaCenterId: uint(media.ID),
|
||||
MediaCreatedAt: mediaResourceTime(media.CreateTime),
|
||||
MediaUpdatedAt: mediaResourceTime(media.UpdateTime),
|
||||
Title: strings.TrimSpace(media.Title),
|
||||
Desc: strings.TrimSpace(media.Summary),
|
||||
Anchor: strings.TrimSpace(media.Anchor),
|
||||
Cover: cover,
|
||||
OldTags: oldTagNamesFromPlayletTagsText(media.TagsText),
|
||||
MediaBusiness: playletMediaBusiness(media),
|
||||
ContentType: contentTypeFromMediaType(media.MediaType),
|
||||
TotalEpisode: int64(media.TotalEpisode),
|
||||
UpdateStatus: normalizePlayletUpdateStatus(media.UpdateStatus),
|
||||
HeatValue: randomPlayletHeatValue(),
|
||||
Status: playletmod.StatusDown,
|
||||
OperatorId: operatorId,
|
||||
OperatorName: operatorName,
|
||||
}
|
||||
}
|
||||
|
||||
// playletMediaBusinessCGDJ 赤果短剧、CGDJ 标题前缀对应的媒资业务来源。
|
||||
const playletMediaBusinessCGDJ = "CGDJ"
|
||||
|
||||
// playletMediaBusinessRule 把媒资标题前缀映射成要写入的业务来源。先匹配先生效。
|
||||
type playletMediaBusinessRule struct {
|
||||
Prefix string
|
||||
Business string
|
||||
}
|
||||
|
||||
// playletMediaBusinessRules 多个前缀可以对应同一个业务来源。
|
||||
// 新增来源时在这里追加;同一来源的不同叫法共用同一个 Business。
|
||||
var playletMediaBusinessRules = []playletMediaBusinessRule{
|
||||
//{Prefix: "赤果短剧", Business: playletMediaBusinessCGDJ},
|
||||
{Prefix: "CGDJ", Business: playletMediaBusinessCGDJ},
|
||||
}
|
||||
|
||||
func playletMediaBusiness(media *proto.PlayletResourceListItem) string {
|
||||
if media == nil || media.MediaType != playletResourceMediaTypeShort {
|
||||
return ""
|
||||
}
|
||||
title := strings.ToUpper(strings.TrimSpace(media.Title))
|
||||
for _, rule := range playletMediaBusinessRules {
|
||||
prefix := strings.ToUpper(strings.TrimSpace(rule.Prefix))
|
||||
business := strings.TrimSpace(rule.Business)
|
||||
if prefix == "" || business == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(title, prefix) {
|
||||
return business
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// playletMediaBusinessSearchTitles 返回回填时用来搜索媒资库的标题前缀。
|
||||
func playletMediaBusinessSearchTitles() []string {
|
||||
seen := make(map[string]struct{}, len(playletMediaBusinessRules))
|
||||
titles := make([]string, 0, len(playletMediaBusinessRules))
|
||||
for _, rule := range playletMediaBusinessRules {
|
||||
prefix := strings.TrimSpace(rule.Prefix)
|
||||
if prefix == "" {
|
||||
continue
|
||||
}
|
||||
key := strings.ToUpper(prefix)
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
titles = append(titles, prefix)
|
||||
}
|
||||
return titles
|
||||
}
|
||||
|
||||
func oldTagNamesFromPlayletTagsText(tagsText string) []string {
|
||||
fields := strings.FieldsFunc(strings.TrimSpace(tagsText), func(r rune) bool {
|
||||
switch r {
|
||||
case ',', ',', '、', ';', ';', '/', '|':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})
|
||||
names := make([]string, 0, len(fields))
|
||||
seen := make(map[string]struct{}, len(fields))
|
||||
for _, field := range fields {
|
||||
name := strings.TrimSpace(field)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[name]; ok {
|
||||
continue
|
||||
}
|
||||
seen[name] = struct{}{}
|
||||
names = append(names, name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func randomPlayletHeatValue() int64 {
|
||||
return int64(rand.Intn(playletHeatMaxValue-playletHeatMinValue+1) + playletHeatMinValue)
|
||||
}
|
||||
|
||||
// mediaResourceTime 转换媒资中心时间戳
|
||||
func mediaResourceTime(ts int64) time.Time {
|
||||
if ts <= 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
if ts > 1e12 {
|
||||
return time.UnixMilli(ts)
|
||||
}
|
||||
return time.Unix(ts, 0)
|
||||
}
|
||||
|
||||
// buildPlayletChaptersFromMedia 将媒资中心章节资源转换为本项目 playlet_chapter 模型
|
||||
func buildPlayletChaptersFromMedia(mediaContentList []playletMediaContent) playletchaptermod.PlayletChapterList {
|
||||
chapters := make(playletchaptermod.PlayletChapterList, 0, len(mediaContentList))
|
||||
seenMediaContentIds := make(map[uint]struct{}, len(mediaContentList))
|
||||
seenEpisodes := make(map[int]struct{}, len(mediaContentList))
|
||||
seenHashIds := make(map[string]struct{}, len(mediaContentList))
|
||||
seenVideoUrls := make(map[string]struct{}, len(mediaContentList))
|
||||
for _, item := range mediaContentList {
|
||||
videoUrl := strings.TrimSpace(item.M3u8Url)
|
||||
if videoUrl == "" {
|
||||
videoUrl = strings.TrimSpace(item.Urls)
|
||||
}
|
||||
if videoUrl == "" {
|
||||
continue
|
||||
}
|
||||
if isDuplicatePlayletMediaChapter(item, videoUrl, seenMediaContentIds, seenEpisodes, seenHashIds, seenVideoUrls) {
|
||||
continue
|
||||
}
|
||||
episodeNumber := int64(item.EpisodeNumber)
|
||||
if episodeNumber <= 0 {
|
||||
episodeNumber = int64(len(chapters) + 1)
|
||||
}
|
||||
height := int64(item.Height)
|
||||
width := int64(item.Width)
|
||||
ratio := item.Ratio
|
||||
if ratio <= 0 && height > 0 {
|
||||
ratio = float64(width) / float64(height)
|
||||
}
|
||||
chapters = append(chapters, &playletchaptermod.PlayletChapter{
|
||||
MediaContentId: uint(item.ID),
|
||||
CurrentEpisode: episodeNumber,
|
||||
Title: strings.TrimSpace(item.Title),
|
||||
Hash: item.HashId,
|
||||
Height: height,
|
||||
Width: width,
|
||||
Ratio: ratio,
|
||||
VideoUrl: videoUrl,
|
||||
MediaSize: int64(item.MediaSize),
|
||||
MediaTime: int64(item.PlayTime),
|
||||
PayType: playletchaptermod.PayTypeFree,
|
||||
SingleCoins: int64(item.SaleCoin),
|
||||
WatermarkStatus: playletchaptermod.WatermarkStatusPending,
|
||||
})
|
||||
}
|
||||
return chapters
|
||||
}
|
||||
|
||||
// isDuplicatePlayletMediaChapter 判断媒资中心章节是否重复
|
||||
func isDuplicatePlayletMediaChapter(item playletMediaContent, videoUrl string, seenMediaContentIds map[uint]struct{}, seenEpisodes map[int]struct{}, seenHashIds map[string]struct{}, seenVideoUrls map[string]struct{}) bool {
|
||||
if item.ID > 0 {
|
||||
if _, ok := seenMediaContentIds[item.ID]; ok {
|
||||
log.Info("跳过重复短剧媒资章节", log.Any("mediaContentId", item.ID), log.Any("episodeNumber", item.EpisodeNumber))
|
||||
return true
|
||||
}
|
||||
seenMediaContentIds[item.ID] = struct{}{}
|
||||
return false
|
||||
}
|
||||
if item.EpisodeNumber > 0 {
|
||||
if _, ok := seenEpisodes[item.EpisodeNumber]; ok {
|
||||
log.Info("跳过重复短剧媒资章节", log.Any("episodeNumber", item.EpisodeNumber))
|
||||
return true
|
||||
}
|
||||
seenEpisodes[item.EpisodeNumber] = struct{}{}
|
||||
return false
|
||||
}
|
||||
hashId := strings.TrimSpace(item.HashId)
|
||||
if hashId != "" {
|
||||
if _, ok := seenHashIds[hashId]; ok {
|
||||
log.Info("跳过重复短剧媒资章节", log.Any("hashId", hashId))
|
||||
return true
|
||||
}
|
||||
seenHashIds[hashId] = struct{}{}
|
||||
return false
|
||||
}
|
||||
if _, ok := seenVideoUrls[videoUrl]; ok {
|
||||
log.Info("跳过重复短剧媒资章节", log.Any("videoUrl", videoUrl))
|
||||
return true
|
||||
}
|
||||
seenVideoUrls[videoUrl] = struct{}{}
|
||||
return false
|
||||
}
|
||||
|
||||
// rollbackImportedPlayletResource 回滚单条媒资入库产生的数据
|
||||
func rollbackImportedPlayletResource(ctx context.Context, playletId int64, chapterIds []int64) {
|
||||
for _, chapterId := range chapterIds {
|
||||
if _, err := playletchaptermod.DelOnePlayletChapter(ctx, bson.M{playletchaptermod.FN.PlayletChapter.Id: chapterId}); err != nil {
|
||||
log.Error("回滚短剧媒资章节失败", log.Any("playletId", playletId), log.Any("chapterId", chapterId), log.Any("err", err))
|
||||
}
|
||||
}
|
||||
if _, err := playletmod.DelOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: playletId}); err != nil {
|
||||
log.Error("回滚短剧媒资主表失败", log.Any("playletId", playletId), log.Any("err", err))
|
||||
}
|
||||
}
|
||||
|
||||
// resolveImportOperator 从后台登录态解析当前操作人
|
||||
func resolveImportOperator(ctx context.Context) (int64, string) {
|
||||
c, ok := ctx.(*gin.Context)
|
||||
if !ok || c == nil {
|
||||
return 0, ""
|
||||
}
|
||||
accId := auth.GetAccount(c)
|
||||
if accId <= 0 {
|
||||
return 0, ""
|
||||
}
|
||||
acc, err := mwacc.FindOneAccByFilter(c, bson.M{
|
||||
mwacc.FN.MwAcc.Id: accId,
|
||||
}, options.FindOne().SetProjection(bson.D{
|
||||
{Key: mwacc.FN.MwAcc.Id, Value: 1},
|
||||
{Key: mwacc.FN.MwAcc.UserName, Value: 1},
|
||||
}))
|
||||
if err != nil || acc == nil {
|
||||
return accId, ""
|
||||
}
|
||||
return acc.Id, acc.UserName
|
||||
}
|
||||
|
||||
// maxPlayletMediaContentUpdateTime 记录本次同步返回章节中的最大更新时间
|
||||
func maxPlayletMediaContentUpdateTime(syncTime int64, mediaContentList []playletMediaContent) int64 {
|
||||
result := syncTime
|
||||
for _, item := range mediaContentList {
|
||||
if item.UpdateTime > result {
|
||||
result = item.UpdateTime
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// normalizePlayletUpdateStatus 三方 update_status=0 在本项目按 2-已完结保存
|
||||
func normalizePlayletUpdateStatus(updateStatus int) int {
|
||||
if updateStatus == 0 {
|
||||
return 2
|
||||
}
|
||||
return updateStatus
|
||||
}
|
||||
@@ -0,0 +1,468 @@
|
||||
package srv_mediaresource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/playletchaptermod"
|
||||
"videoweb/pub/models/playletmod"
|
||||
"videoweb/pub/pubdef"
|
||||
"videoweb/pub/redis"
|
||||
"videoweb/web/api/proto"
|
||||
|
||||
goredis "github.com/go-redis/redis"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const (
|
||||
// PlayletSerialSyncCronSpec 连载中短剧同步扫描频率:每1小时扫描一次,扫描阶段只投递 Redis 任务
|
||||
PlayletSerialSyncCronSpec = "0 0 * * * ?"
|
||||
|
||||
// playletSerialSyncQueueKeyFmt 解耦定时扫描和三方同步调用,避免 cron 扫描过程直接阻塞在外部接口上
|
||||
playletSerialSyncQueueKeyFmt = "playlet:serial:sync:queue:%d"
|
||||
playletSerialSyncLockKeyFmt = "playlet:serial:sync:lock:%d:%d"
|
||||
playletSerialSyncLockTTL = 55 * time.Minute
|
||||
playletSerialSyncPopWait = 5 * time.Second
|
||||
playletSerialSyncRetryWait = time.Second
|
||||
playletSerialSyncBatchSize = int64(100)
|
||||
playletManualSyncMaxCount = 100
|
||||
)
|
||||
|
||||
func playletSerialSyncQueueKey() string {
|
||||
return fmt.Sprintf(playletSerialSyncQueueKeyFmt, pubdef.AppId)
|
||||
}
|
||||
|
||||
func playletSerialSyncLockKey(mediaId uint) string {
|
||||
return fmt.Sprintf(playletSerialSyncLockKeyFmt, pubdef.AppId, mediaId)
|
||||
}
|
||||
|
||||
type playletSerialSyncJob struct {
|
||||
PlayletId int64 `json:"playletId"`
|
||||
MediaCenterId uint `json:"mediaCenterId"`
|
||||
MediaSyncTime int64 `json:"mediaSyncTime"`
|
||||
CreateTime int64 `json:"createTime"`
|
||||
}
|
||||
|
||||
// EnqueueSerialPlayletSyncJobs 扫描连载中短剧并投递 Redis 同步任务
|
||||
//
|
||||
// 这里不直接请求三方媒资中心:扫描只负责发现任务,实际同步由后台消费者逐条处理
|
||||
// 可以降低定时任务执行时间,也方便 Redis 队列在三方接口短暂异常时继续缓冲任务
|
||||
func EnqueueSerialPlayletSyncJobs() {
|
||||
ctx := context.Background()
|
||||
filter := bson.M{
|
||||
playletmod.FN.Playlet.MediaCenterId: bson.M{"$gt": 0},
|
||||
playletmod.FN.Playlet.UpdateStatus: 1,
|
||||
// 原创短剧由后台维护剧集,自动同步只处理媒资导入剧
|
||||
playletmod.FN.Playlet.IsOriginal: bson.M{"$ne": true},
|
||||
}
|
||||
opts := options.Find().
|
||||
SetSort(bson.D{{Key: playletmod.FN.Playlet.UpdatedAt, Value: 1}, {Key: playletmod.FN.Playlet.Id, Value: 1}}).
|
||||
SetLimit(playletSerialSyncBatchSize)
|
||||
list, err := playletmod.FindManyPlaylet(ctx, filter, opts)
|
||||
if err != nil {
|
||||
log.Error("扫描连载中短剧失败", log.Any("err", err))
|
||||
return
|
||||
}
|
||||
enqueued := 0
|
||||
for _, item := range list {
|
||||
if item == nil || item.Id == 0 || item.MediaCenterId == 0 || item.IsOriginal {
|
||||
continue
|
||||
}
|
||||
if err := pushPlayletSerialSyncJob(ctx, item); err != nil {
|
||||
log.Error("投递连载短剧同步任务失败", log.Any("playletId", item.Id), log.Any("mediaCenterId", item.MediaCenterId), log.E(err))
|
||||
continue
|
||||
}
|
||||
enqueued++
|
||||
}
|
||||
log.Info("连载中短剧同步任务扫描完成", log.Any("scanCount", len(list)), log.Any("enqueued", enqueued))
|
||||
}
|
||||
|
||||
// pushPlayletSerialSyncJob 将单部连载短剧投递到 Redis 队列
|
||||
func pushPlayletSerialSyncJob(ctx context.Context, playlet *playletmod.Playlet) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
job := playletSerialSyncJob{
|
||||
PlayletId: playlet.Id,
|
||||
MediaCenterId: playlet.MediaCenterId,
|
||||
MediaSyncTime: playlet.MediaSyncTime,
|
||||
CreateTime: time.Now().Unix(),
|
||||
}
|
||||
payload, err := json.Marshal(job)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return redis.RPush(playletSerialSyncQueueKey(), string(payload))
|
||||
}
|
||||
|
||||
// runPlayletSerialSyncWorker 常驻消费连载短剧同步队列
|
||||
func runPlayletSerialSyncWorker(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
result, err := redis.Client().BLPop(playletSerialSyncPopWait, playletSerialSyncQueueKey()).Result()
|
||||
if err != nil {
|
||||
if err == goredis.Nil {
|
||||
continue
|
||||
}
|
||||
log.Error("消费连载短剧同步任务失败", log.E(err))
|
||||
time.Sleep(playletSerialSyncRetryWait)
|
||||
continue
|
||||
}
|
||||
if len(result) < 2 {
|
||||
log.Error("连载短剧同步任务格式异常", log.Any("result", result))
|
||||
continue
|
||||
}
|
||||
handlePlayletSerialSyncPayload(ctx, result[1])
|
||||
}
|
||||
}
|
||||
|
||||
// handlePlayletSerialSyncPayload 解析单个 Redis 任务并执行同步,panic 会被捕获避免 worker 退出
|
||||
func handlePlayletSerialSyncPayload(ctx context.Context, payload string) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Error("处理连载短剧同步任务异常", log.Any("err", err), log.Any("payload", payload))
|
||||
}
|
||||
}()
|
||||
var job playletSerialSyncJob
|
||||
if err := json.Unmarshal([]byte(payload), &job); err != nil {
|
||||
log.Error("解析连载短剧同步任务失败", log.Any("payload", payload), log.E(err))
|
||||
return
|
||||
}
|
||||
if job.PlayletId <= 0 || job.MediaCenterId == 0 {
|
||||
log.Error("连载短剧同步任务参数异常", log.Any("job", job))
|
||||
return
|
||||
}
|
||||
if _, reason := syncSerialPlaylet(ctx, job, false, false); reason != "" {
|
||||
log.Error("连载短剧同步任务失败", log.Any("job", job), log.Any("reason", reason))
|
||||
return
|
||||
}
|
||||
log.Info("连载短剧同步任务完成", log.Any("job", job))
|
||||
}
|
||||
|
||||
// SyncPlayletChapters 手动同步指定短剧的媒资剧集,支持单/批量,允许已完结短剧补集或修正状态。
|
||||
func SyncPlayletChapters(ctx context.Context, req *proto.PlayletChapterSyncReq) (*proto.PlayletChapterSyncResp, *srvdef.SrvErr) {
|
||||
ids := normalizePlayletChapterSyncIds(req)
|
||||
if len(ids) == 0 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "")
|
||||
}
|
||||
if len(ids) > playletManualSyncMaxCount {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "单次最多同步100部短剧", "")
|
||||
}
|
||||
resp := &proto.PlayletChapterSyncResp{
|
||||
Total: len(ids),
|
||||
List: make([]*proto.PlayletChapterSyncResult, 0, len(ids)),
|
||||
}
|
||||
for _, playletId := range ids {
|
||||
item := syncOnePlayletChapters(ctx, playletId)
|
||||
if item.Success {
|
||||
resp.SuccessCount++
|
||||
} else {
|
||||
resp.FailedCount++
|
||||
}
|
||||
resp.List = append(resp.List, item)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// normalizePlayletChapterSyncIds 过滤重复或无效短剧ID。
|
||||
func normalizePlayletChapterSyncIds(req *proto.PlayletChapterSyncReq) []int64 {
|
||||
if req == nil {
|
||||
return nil
|
||||
}
|
||||
result := make([]int64, 0, len(req.PlayletIds))
|
||||
seen := make(map[int64]struct{}, len(req.PlayletIds))
|
||||
for _, id := range req.PlayletIds {
|
||||
if id <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
result = append(result, id)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// syncOnePlayletChapters 同步单部短剧的媒资剧集,单部失败不影响批量里其他短剧。
|
||||
func syncOnePlayletChapters(ctx context.Context, playletId int64) *proto.PlayletChapterSyncResult {
|
||||
result := &proto.PlayletChapterSyncResult{
|
||||
PlayletId: playletId,
|
||||
}
|
||||
playlet, err := playletmod.FindOnePlaylet(ctx, bson.M{
|
||||
playletmod.FN.Playlet.Id: playletId,
|
||||
}, options.FindOne().SetProjection(bson.D{
|
||||
{Key: playletmod.FN.Playlet.Id, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.MediaCenterId, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.MediaSyncTime, Value: 1},
|
||||
}))
|
||||
if err != nil {
|
||||
result.Reason = playletChapterSyncErrReason(err)
|
||||
return result
|
||||
}
|
||||
if playlet.MediaCenterId == 0 {
|
||||
result.Reason = "非媒资导入短剧不能同步剧集"
|
||||
return result
|
||||
}
|
||||
newChapterCount, reason := syncSerialPlaylet(ctx, playletSerialSyncJob{
|
||||
PlayletId: playlet.Id,
|
||||
MediaCenterId: playlet.MediaCenterId,
|
||||
MediaSyncTime: playlet.MediaSyncTime,
|
||||
CreateTime: time.Now().Unix(),
|
||||
}, true, true)
|
||||
if reason != "" {
|
||||
result.Reason = reason
|
||||
return result
|
||||
}
|
||||
result.Success = true
|
||||
result.NewChapterCount = newChapterCount
|
||||
return result
|
||||
}
|
||||
|
||||
// playletChapterSyncErrReason 转换单部短剧同步失败原因,避免批量结果里 reason 为空。
|
||||
func playletChapterSyncErrReason(err *srvdef.SrvErr) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
if err.Code == srvdef.ErrNoData {
|
||||
return "短剧不存在"
|
||||
}
|
||||
if reason := err.GetTip(); reason != "" {
|
||||
return reason
|
||||
}
|
||||
if reason := err.GetMsg(); reason != "" {
|
||||
return reason
|
||||
}
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
// syncSerialPlaylet 更新主表集数/状态,只追加新增章节,不覆盖已有章节。
|
||||
// 手动同步使用 forceFullSync=true 全量拉取,便于恢复本地误删的旧章节;定时任务继续使用增量同步。
|
||||
func syncSerialPlaylet(ctx context.Context, job playletSerialSyncJob, allowCompleted bool, forceFullSync bool) (int, string) {
|
||||
locked, err := lockPlayletSerialSync(job.MediaCenterId)
|
||||
if err != nil {
|
||||
log.Error("获取连载短剧同步锁失败", log.Any("job", job), log.E(err))
|
||||
return 0, "获取同步锁失败"
|
||||
}
|
||||
if !locked {
|
||||
return 0, "该媒资正在同步"
|
||||
}
|
||||
defer unlockPlayletSerialSync(job.MediaCenterId)
|
||||
|
||||
playlet, serr := playletmod.FindOnePlaylet(ctx, bson.M{
|
||||
playletmod.FN.Playlet.Id: job.PlayletId,
|
||||
playletmod.FN.Playlet.MediaCenterId: job.MediaCenterId,
|
||||
})
|
||||
if serr != nil {
|
||||
if serr.Code == srvdef.ErrNoData {
|
||||
return 0, "短剧不存在"
|
||||
}
|
||||
log.Error("查询连载短剧失败", log.Any("job", job), log.Any("err", serr))
|
||||
return 0, "查询短剧失败"
|
||||
}
|
||||
if !allowCompleted && playlet.IsOriginal {
|
||||
return 0, ""
|
||||
}
|
||||
if !allowCompleted && playlet.UpdateStatus != 1 {
|
||||
return 0, ""
|
||||
}
|
||||
|
||||
ms := newPlayletMediaCenterFromConfig()
|
||||
syncTime := playlet.MediaSyncTime
|
||||
if forceFullSync {
|
||||
syncTime = 0
|
||||
}
|
||||
result, err := ms.mediaSync(&playletMediaSyncReq{
|
||||
Id: playlet.MediaCenterId,
|
||||
SyncTime: syncTime,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("同步连载短剧媒资失败", log.Any("playletId", playlet.Id), log.Any("mediaCenterId", playlet.MediaCenterId), log.E(err))
|
||||
return 0, "同步媒资失败"
|
||||
}
|
||||
if result.IsDelete {
|
||||
return 0, "媒资不存在或已删除"
|
||||
}
|
||||
if result.Media == nil {
|
||||
return 0, "媒资主资源为空"
|
||||
}
|
||||
|
||||
newSyncTime := maxPlayletMediaContentUpdateTime(playlet.MediaSyncTime, result.MediaContentList)
|
||||
if reason := updateSerialPlayletInfo(ctx, playlet, result.Media, newSyncTime); reason != "" {
|
||||
return 0, reason
|
||||
}
|
||||
if len(result.MediaContentList) == 0 {
|
||||
return 0, ""
|
||||
}
|
||||
chapters, reason := buildNewSerialPlayletChapters(ctx, playlet.Id, result.MediaContentList)
|
||||
if reason != "" {
|
||||
return 0, reason
|
||||
}
|
||||
if len(chapters) == 0 {
|
||||
return 0, ""
|
||||
}
|
||||
// 上架且连载中时,新增剧集默认支付方式取剧级设置。
|
||||
applyPlayletDefaultPayToChapters(playlet, chapters)
|
||||
if serr = playletchaptermod.InsertManyPlayletChapter(ctx, chapters); serr != nil {
|
||||
log.Error("连载短剧新增章节入库失败", log.Any("playletId", playlet.Id), log.Any("mediaCenterId", playlet.MediaCenterId), log.Any("count", len(chapters)), log.Any("err", serr))
|
||||
return 0, "新增章节入库失败"
|
||||
}
|
||||
log.Info("连载短剧新增章节成功", log.Any("playletId", playlet.Id), log.Any("mediaCenterId", playlet.MediaCenterId), log.Any("count", len(chapters)))
|
||||
return len(chapters), ""
|
||||
}
|
||||
|
||||
// updateSerialPlayletInfo 更新三方主资源里会随连载变化的字段
|
||||
func updateSerialPlayletInfo(ctx context.Context, playlet *playletmod.Playlet, media *proto.PlayletResourceListItem, mediaSyncTime int64) string {
|
||||
updateStatus := normalizePlayletUpdateStatus(media.UpdateStatus)
|
||||
setDoc := bson.M{
|
||||
playletmod.FN.Playlet.TotalEpisode: int64(media.TotalEpisode),
|
||||
playletmod.FN.Playlet.UpdateStatus: updateStatus,
|
||||
playletmod.FN.Playlet.MediaUpdatedAt: mediaResourceTime(media.UpdateTime),
|
||||
playletmod.FN.Playlet.UpdatedAt: time.Now(),
|
||||
}
|
||||
if mediaSyncTime > playlet.MediaSyncTime {
|
||||
setDoc[playletmod.FN.Playlet.MediaSyncTime] = mediaSyncTime
|
||||
}
|
||||
if business := playletMediaBusiness(media); business != "" && playlet.MediaBusiness != business {
|
||||
setDoc[playletmod.FN.Playlet.MediaBusiness] = business
|
||||
}
|
||||
if err := playletmod.UpdateOnePlaylet(ctx, bson.M{
|
||||
playletmod.FN.Playlet.Id: playlet.Id,
|
||||
}, bson.M{"$set": setDoc}); err != nil {
|
||||
log.Error("更新连载短剧主表失败", log.Any("playletId", playlet.Id), log.Any("mediaCenterId", playlet.MediaCenterId), log.Any("err", err))
|
||||
return "更新主表失败"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// buildNewSerialPlayletChapters 根据本地已存在章节做幂等过滤,只返回需要追加的新章节
|
||||
func buildNewSerialPlayletChapters(ctx context.Context, playletId int64, mediaContentList []playletMediaContent) (playletchaptermod.PlayletChapterList, string) {
|
||||
exists, err := loadExistingPlayletChapterKeys(ctx, playletId)
|
||||
if err != nil {
|
||||
return nil, "查询已有章节失败"
|
||||
}
|
||||
chapters := buildPlayletChaptersFromMedia(mediaContentList)
|
||||
result := make(playletchaptermod.PlayletChapterList, 0, len(chapters))
|
||||
for _, chapter := range chapters {
|
||||
if chapter == nil || chapterDuplicateByKeys(chapter, exists) {
|
||||
continue
|
||||
}
|
||||
chapter.PlayletId = playletId
|
||||
markExistingPlayletChapterKeys(chapter, exists)
|
||||
result = append(result, chapter)
|
||||
}
|
||||
return result, ""
|
||||
}
|
||||
|
||||
type existingPlayletChapterKeys struct {
|
||||
mediaContentIds map[uint]struct{}
|
||||
episodes map[int64]struct{}
|
||||
hashes map[string]struct{}
|
||||
videoUrls map[string]struct{}
|
||||
}
|
||||
|
||||
// loadExistingPlayletChapterKeys 加载当前短剧已入库章节的去重键,避免连载同步重复插入章节
|
||||
func loadExistingPlayletChapterKeys(ctx context.Context, playletId int64) (existingPlayletChapterKeys, error) {
|
||||
keys := existingPlayletChapterKeys{
|
||||
mediaContentIds: make(map[uint]struct{}),
|
||||
episodes: make(map[int64]struct{}),
|
||||
hashes: make(map[string]struct{}),
|
||||
videoUrls: make(map[string]struct{}),
|
||||
}
|
||||
list, serr := playletchaptermod.FindManyPlayletChapter(ctx, bson.M{playletchaptermod.FN.PlayletChapter.PlayletId: playletId})
|
||||
if serr != nil {
|
||||
return keys, fmt.Errorf("query playlet chapters failed: %+v", serr)
|
||||
}
|
||||
for _, chapter := range list {
|
||||
markExistingPlayletChapterKeys(chapter, keys)
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// chapterDuplicateByKeys 章节跨批次去重,优先用媒资章节ID,其次用集数、hash、播放地址兜底
|
||||
func chapterDuplicateByKeys(chapter *playletchaptermod.PlayletChapter, keys existingPlayletChapterKeys) bool {
|
||||
if chapter.MediaContentId > 0 {
|
||||
if _, ok := keys.mediaContentIds[chapter.MediaContentId]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if chapter.CurrentEpisode > 0 {
|
||||
if _, ok := keys.episodes[chapter.CurrentEpisode]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if chapter.Hash != "" {
|
||||
if _, ok := keys.hashes[chapter.Hash]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if chapter.VideoUrl != "" {
|
||||
if _, ok := keys.videoUrls[chapter.VideoUrl]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// markExistingPlayletChapterKeys 把已存在或本批次准备插入的章节登记到去重集合
|
||||
func markExistingPlayletChapterKeys(chapter *playletchaptermod.PlayletChapter, keys existingPlayletChapterKeys) {
|
||||
if chapter == nil {
|
||||
return
|
||||
}
|
||||
if chapter.MediaContentId > 0 {
|
||||
keys.mediaContentIds[chapter.MediaContentId] = struct{}{}
|
||||
}
|
||||
if chapter.CurrentEpisode > 0 {
|
||||
keys.episodes[chapter.CurrentEpisode] = struct{}{}
|
||||
}
|
||||
if chapter.Hash != "" {
|
||||
keys.hashes[chapter.Hash] = struct{}{}
|
||||
}
|
||||
if chapter.VideoUrl != "" {
|
||||
keys.videoUrls[chapter.VideoUrl] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// applyPlayletDefaultPayToChapters 上架且连载中时,新增章节默认沿用剧级支付设置。
|
||||
func applyPlayletDefaultPayToChapters(playlet *playletmod.Playlet, chapters playletchaptermod.PlayletChapterList) {
|
||||
if playlet == nil || len(chapters) == 0 {
|
||||
return
|
||||
}
|
||||
if playlet.Status != playletmod.StatusUp || playlet.UpdateStatus != 1 {
|
||||
return
|
||||
}
|
||||
payType := playletchaptermod.PayType(playlet.PayType)
|
||||
singleCoins := playlet.SingleCoins
|
||||
if payType != playletchaptermod.PayTypeCoin {
|
||||
singleCoins = 0
|
||||
}
|
||||
for _, chapter := range chapters {
|
||||
if chapter == nil {
|
||||
continue
|
||||
}
|
||||
chapter.PayType = payType
|
||||
chapter.SingleCoins = singleCoins
|
||||
}
|
||||
}
|
||||
|
||||
// lockPlayletSerialSync 锁定单个媒资ID,避免同一部连载短剧被多个 worker 同时同步
|
||||
func lockPlayletSerialSync(mediaId uint) (bool, error) {
|
||||
return redis.Setnx_NewOK(playletSerialSyncLockKey(mediaId), true, playletSerialSyncLockTTL)
|
||||
}
|
||||
|
||||
// unlockPlayletSerialSync 释放单个媒资ID的同步锁
|
||||
func unlockPlayletSerialSync(mediaId uint) {
|
||||
if err := redis.Del(playletSerialSyncLockKey(mediaId)); err != nil {
|
||||
log.Error("释放连载短剧同步锁失败", log.Any("mediaId", mediaId), log.E(err))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package srv_mwacc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"videoweb/pub/common/crypt"
|
||||
"videoweb/pub/common/errors"
|
||||
"videoweb/pub/models/mwacc"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
// // 删除角色
|
||||
// func AccDel(ctx context.Context, roleId int64) (ok bool, err error){
|
||||
//
|
||||
// }
|
||||
|
||||
// 更改
|
||||
func AccUp(ctx context.Context, upDoc *AccUpDoc) (ok bool, err error) {
|
||||
|
||||
c, err := mwacc.AccCountByFilter(ctx, bson.M{
|
||||
mwacc.FN.MwAcc.Acc: upDoc.Acc,
|
||||
mwacc.FN.MwAcc.Id: bson.M{
|
||||
"$ne": upDoc.Id,
|
||||
},
|
||||
})
|
||||
|
||||
if c > 0 {
|
||||
err = errors.MakeWithMsg(errors.DBInsert, "账户名已存在")
|
||||
return false, err
|
||||
}
|
||||
|
||||
set := bson.M{}
|
||||
|
||||
set = bson.M{
|
||||
mwacc.FN.MwAcc.UserName: upDoc.UserName,
|
||||
mwacc.FN.MwAcc.Acc: upDoc.Acc,
|
||||
mwacc.FN.MwAcc.Email: upDoc.Email,
|
||||
mwacc.FN.MwAcc.RoleId: upDoc.RoleId,
|
||||
mwacc.FN.MwAcc.IsEnb: upDoc.IsEnb,
|
||||
mwacc.FN.MwAcc.UpdatedAt: time.Now(),
|
||||
mwacc.FN.MwAcc.LimitIP: upDoc.LimitIP,
|
||||
}
|
||||
|
||||
err = mwacc.UpOneAccByFilter(ctx, bson.M{
|
||||
mwacc.FN.MwAcc.Id: upDoc.Id,
|
||||
}, set)
|
||||
if err != nil {
|
||||
err = errors.MakeWithErr(errors.DBUpdate, err)
|
||||
return false, err
|
||||
}
|
||||
return true, err
|
||||
}
|
||||
|
||||
// 更改账号密码
|
||||
func AccUpPwd(ctx context.Context, upDoc *AccUpPwdDoc) (ok bool, err error) {
|
||||
|
||||
set := bson.M{}
|
||||
|
||||
set = bson.M{
|
||||
mwacc.FN.MwAcc.Pwd: crypt.StrToMd5(upDoc.Pwd),
|
||||
mwacc.FN.MwAcc.UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err = mwacc.UpOneAccByFilter(ctx, bson.M{
|
||||
mwacc.FN.MwAcc.Id: upDoc.Id,
|
||||
}, set)
|
||||
if err != nil {
|
||||
err = errors.MakeWithErr(errors.DBUpdate, err)
|
||||
return false, err
|
||||
}
|
||||
mwacc.DeleteOneToken(ctx, bson.M{mwacc.FN.MwAccToken.AccId: upDoc.Id})
|
||||
return true, err
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package srv_mwacc
|
||||
|
||||
type AccUpDoc struct {
|
||||
// 账号Id
|
||||
Id int64 `json:"id,omitempty"`
|
||||
// 用户名
|
||||
UserName string `json:"user_name,omitempty"`
|
||||
// 账号
|
||||
Acc string `json:"acc,omitempty"`
|
||||
// // 密码
|
||||
// Pwd string `json:"pwd,omitempty"`
|
||||
// 邮箱
|
||||
Email string `json:"email,omitempty"`
|
||||
// 角色Id
|
||||
RoleId int64 `json:"role_id,omitempty"`
|
||||
// 使能
|
||||
IsEnb bool `json:"is_enb,omitempty"`
|
||||
// 允许访问的IP
|
||||
LimitIP []string `json:"limitIP,omitempty"`
|
||||
}
|
||||
|
||||
type ProductAccountInfo struct {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type AccUpPwdDoc struct {
|
||||
// 账号Id
|
||||
Id int64 `json:"id,omitempty"`
|
||||
// 密码
|
||||
Pwd string `json:"pwd,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package srv_mwacc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/crypt"
|
||||
"videoweb/pub/common/enum/consts"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/google"
|
||||
"videoweb/pub/models/mwacc"
|
||||
"videoweb/web/webg"
|
||||
|
||||
"github.com/skip2/go-qrcode"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
func generateToken(userID int64) (string, error) {
|
||||
return utils.GenerateUserToken(utils.UserClaims{UserID: userID, Expire: consts.AdminJwtTokenExpire}, webg.Conf.JwtKey)
|
||||
}
|
||||
|
||||
// AccSLogin 账号登录
|
||||
func AccSLogin(ctx context.Context, acc string, pwd string) (*mwacc.MwAcc, *srvdef.SrvErr) {
|
||||
var data *mwacc.MwAcc
|
||||
res, dataErr := mwacc.FindOneAccByFilter(ctx, bson.M{
|
||||
mwacc.FN.MwAcc.Acc: acc,
|
||||
})
|
||||
|
||||
if dataErr != nil || res.Id == 0 {
|
||||
return data, srvdef.New(srvdef.AccountIsNotExists, srvdef.AccountIsNotExists.Msg(), "")
|
||||
}
|
||||
|
||||
if !res.IsEnb {
|
||||
return data, srvdef.New(srvdef.AccountErrAccessForbid, srvdef.AccountErrAccessForbid.Msg(), "")
|
||||
}
|
||||
|
||||
// 通过验证,再次验证密码是否正确
|
||||
md5pwd := crypt.StrToMd5(pwd)
|
||||
if res.Pwd != md5pwd {
|
||||
return data, srvdef.New(srvdef.AccountErrPasswordError, srvdef.AccountErrPasswordError.Msg(), "")
|
||||
}
|
||||
|
||||
// slogin 仅允许角色名含“客服”的账号登录
|
||||
if !isCustomerServiceRole(ctx, res) {
|
||||
return data, srvdef.New(srvdef.ErrUserPermission, "仅客服角色可登录", "")
|
||||
}
|
||||
|
||||
getToken, err := generateToken(res.Id)
|
||||
if err != nil {
|
||||
return data, srvdef.New(srvdef.GenerateTokenFailed, srvdef.GenerateTokenFailed.Msg(), "")
|
||||
}
|
||||
_ = mwacc.UpOneAccByFilter(ctx, bson.M{
|
||||
mwacc.FN.MwAcc.Id: res.Id,
|
||||
}, bson.M{
|
||||
mwacc.FN.MwAcc.LastTime: time.Now(),
|
||||
})
|
||||
// 更新最近登录时间
|
||||
data = res
|
||||
data.Token = getToken
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// isCustomerServiceRole 判断账号角色名是否包含“客服”
|
||||
func isCustomerServiceRole(ctx context.Context, acc *mwacc.MwAcc) bool {
|
||||
if acc == nil {
|
||||
return false
|
||||
}
|
||||
roleName := acc.RoleName
|
||||
if roleName == "" && acc.RoleId > 0 {
|
||||
role, serr := mwacc.FindOneRoleByFilter(ctx, bson.M{mwacc.FN.MwRole.Id: acc.RoleId})
|
||||
if serr == nil && role != nil {
|
||||
roleName = role.Name
|
||||
}
|
||||
}
|
||||
return strings.Contains(roleName, "客服")
|
||||
}
|
||||
|
||||
// 账号登录
|
||||
func AccLogin(ctx context.Context, acc string, pwd string, code string) (*mwacc.MwAcc, *srvdef.SrvErr) {
|
||||
var data *mwacc.MwAcc
|
||||
res, dataErr := mwacc.FindOneAccByFilter(ctx, bson.M{
|
||||
mwacc.FN.MwAcc.Acc: acc,
|
||||
})
|
||||
|
||||
if dataErr != nil || res.Id == 0 {
|
||||
return data, srvdef.New(srvdef.AccountIsNotExists, srvdef.AccountIsNotExists.Msg(), "")
|
||||
}
|
||||
|
||||
if !res.IsEnb {
|
||||
return data, srvdef.New(srvdef.AccountErrAccessForbid, srvdef.AccountErrAccessForbid.Msg(), "")
|
||||
}
|
||||
|
||||
// 通过验证,再次验证密码是否正确
|
||||
md5pwd := crypt.StrToMd5(pwd)
|
||||
if res.Pwd != md5pwd {
|
||||
return data, srvdef.New(srvdef.AccountErrPasswordError, srvdef.AccountErrPasswordError.Msg(), "")
|
||||
}
|
||||
|
||||
if !res.IsBase32 {
|
||||
// 谷歌绑定二维码
|
||||
googleUrl := google.NewGoogleAuth().GetQrcode(acc, res.Base32)
|
||||
qrCodePNG, _ := qrcode.Encode(googleUrl, qrcode.Medium, 256)
|
||||
|
||||
bq := &mwacc.MwAcc{}
|
||||
bq.Qurl = qrCodePNG
|
||||
data = bq
|
||||
} else {
|
||||
// hf屏蔽
|
||||
if webg.Conf.RunMod == "prod" && (res.RoleId != 46761 && res.RoleId != 29601) {
|
||||
if code == "" {
|
||||
return data, srvdef.New(srvdef.ErrCaptcha, srvdef.ErrCaptcha.Msg(), "")
|
||||
}
|
||||
result, Verr := google.NewGoogleAuth().VerifyCode(res.Base32, code)
|
||||
if Verr != nil || !result {
|
||||
return data, srvdef.New(srvdef.ErrCaptchaInvalid, srvdef.ErrCaptchaInvalid.Msg(), "")
|
||||
}
|
||||
}
|
||||
// 通过验证,再次验证密码是否正确
|
||||
pwd := crypt.StrToMd5(pwd)
|
||||
if res.Pwd != pwd {
|
||||
return data, srvdef.New(srvdef.AccountErrPasswordError, srvdef.AccountErrPasswordError.Msg(), "")
|
||||
}
|
||||
getToken, err := generateToken(res.Id)
|
||||
if err != nil {
|
||||
return data, srvdef.New(srvdef.GenerateTokenFailed, srvdef.GenerateTokenFailed.Msg(), "")
|
||||
}
|
||||
_ = mwacc.UpOneAccByFilter(ctx, bson.M{
|
||||
mwacc.FN.MwAcc.Id: res.Id,
|
||||
}, bson.M{
|
||||
mwacc.FN.MwAcc.LastTime: time.Now(),
|
||||
})
|
||||
// 更新最近登录时间
|
||||
data = res
|
||||
data.Token = getToken
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// 账号登录
|
||||
func ProductAccountList(ctx context.Context, accId int64) ([]ProductAccountInfo, *srvdef.SrvErr) {
|
||||
resp := make([]ProductAccountInfo, 0)
|
||||
accInfo, serr := mwacc.FindOneAccByFilter(ctx, bson.M{
|
||||
mwacc.FN.MwAcc.Id: accId,
|
||||
})
|
||||
if serr != nil {
|
||||
log.Error("ProductAccountList FindOneAccByFilter", log.Any("serr", serr))
|
||||
return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
|
||||
}
|
||||
|
||||
if accInfo.RoleId == 1000 {
|
||||
resp = append(resp, ProductAccountInfo{
|
||||
Id: accInfo.Id,
|
||||
Name: accInfo.UserName,
|
||||
})
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
accList, lerr := mwacc.FindManyAccByFilter(ctx, bson.M{
|
||||
mwacc.FN.MwAcc.RoleId: 1000,
|
||||
})
|
||||
|
||||
if lerr != nil {
|
||||
log.Error("ProductAccountList FindManyAccByFilter", log.Any("serr", serr))
|
||||
return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
|
||||
}
|
||||
|
||||
for _, v := range accList {
|
||||
resp = append(resp, ProductAccountInfo{
|
||||
Id: v.Id,
|
||||
Name: v.UserName,
|
||||
})
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package srv_mwacc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
"videoweb/pub/common/errors"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/mwacc"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
// 删除角色
|
||||
func RoleDel(ctx context.Context, roleId int64) (bool, *srvdef.SrvErr) {
|
||||
|
||||
res, err := mwacc.FindOneRoleByFilter(ctx, bson.M{mwacc.FN.MwRole.Id: roleId})
|
||||
|
||||
if err != nil || res.Id == 0 {
|
||||
log.ZapLog.Error(fmt.Sprintf("search error %v", err))
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 查询记录是否有账户
|
||||
roles, err1 := mwacc.RoleAggregate(ctx,
|
||||
[]bson.M{
|
||||
{"$match": bson.M{mwacc.FN.MwRole.Id: roleId}},
|
||||
{"$limit": 1},
|
||||
{"$lookup": bson.M{
|
||||
"localField": mwacc.FN.MwRole.Id,
|
||||
"from": mwacc.TN.MwAcc,
|
||||
"foreignField": mwacc.FN.MwAcc.RoleId,
|
||||
"as": "onlyRole",
|
||||
}},
|
||||
{"$unwind": "$onlyRole"},
|
||||
{"$project": bson.M{
|
||||
mwacc.FN.MwRole.Id: 1,
|
||||
"onlyRole.roleId": 1,
|
||||
}},
|
||||
})
|
||||
|
||||
if err1 != nil {
|
||||
log.Error("角色删除失败", log.Any("roleId", roleId))
|
||||
return false, srvdef.New(srvdef.ErrDbDeleteError, "", "")
|
||||
}
|
||||
|
||||
if len(roles) > 0 {
|
||||
log.Error("角色删除失败", log.Any("roleId", roleId))
|
||||
return false, srvdef.New(srvdef.ErrDbDeleteError, "删除失败,此角色有帐户", "")
|
||||
}
|
||||
|
||||
serr := mwacc.DelRoleByFilter(ctx, bson.M{mwacc.FN.MwRole.Id: roleId})
|
||||
if serr != nil {
|
||||
return false, serr
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 获取信息
|
||||
func RoleGetInfo(ctx context.Context, roleId int64) (*mwacc.MwRole, error) {
|
||||
|
||||
rest, err := mwacc.FindOneRoleByFilter(ctx, bson.M{
|
||||
mwacc.FN.MwRole.Id: roleId,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rest, nil
|
||||
}
|
||||
|
||||
// 角色id
|
||||
|
||||
// 角色更新
|
||||
func RoleUp(ctx context.Context, id int64, name string, desc string, isEnb bool) (ok bool, err error) {
|
||||
|
||||
roleCount, err := mwacc.RoleCountByFilter(ctx, bson.M{
|
||||
mwacc.FN.MwRole.Name: name,
|
||||
mwacc.FN.MwRole.Id: bson.M{
|
||||
"$ne": id,
|
||||
},
|
||||
})
|
||||
|
||||
if roleCount > 0 {
|
||||
err = errors.MakeWithMsg(errors.DBInsert, "角色名称已存在")
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = mwacc.UpOneRoleByFilter(ctx, bson.M{
|
||||
mwacc.FN.MwRole.Id: id,
|
||||
}, bson.M{
|
||||
"$set": bson.M{
|
||||
mwacc.FN.MwRole.Name: name,
|
||||
mwacc.FN.MwRole.Desc: desc,
|
||||
mwacc.FN.MwRole.IsEnb: isEnb,
|
||||
mwacc.FN.MwRole.UpdatedAt: time.Now(),
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.ZapLog.Error(fmt.Sprintf("RoleUp error %v", err))
|
||||
err = errors.MakeWithErr(errors.DBUpdate, err)
|
||||
return
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 设置角色状态
|
||||
func RoleEnable(ctx context.Context, roleId int64, IsEnb bool) (bool, *srvdef.SrvErr) {
|
||||
|
||||
rest, err := mwacc.FindOneRoleByFilter(ctx, bson.M{
|
||||
mwacc.FN.MwRole.Id: roleId,
|
||||
})
|
||||
|
||||
if err != nil || rest.Id == 0 {
|
||||
return false, err
|
||||
}
|
||||
// count, err := mwacc.RoleAggregate(ctx, []bson.M{
|
||||
// {"$match": bson.M{
|
||||
// mwacc.FN.MwRole.Id: roleId,
|
||||
// }},
|
||||
// {"$limit": 1},
|
||||
// {"$lookup": bson.M{
|
||||
// "localField": mwacc.FN.MwRole.Id,
|
||||
// "from": mwacc.TN.MwAcc,
|
||||
// "foreignField": mwacc.FN.MwAcc.RoleId,
|
||||
// "as": "onlyRole",
|
||||
// }},
|
||||
// {"$unwind": "$onlyRole"},
|
||||
// {"$project": bson.M{
|
||||
// "roleId": 1,
|
||||
// "onlyRole.roleId": 1,
|
||||
// }},
|
||||
// })
|
||||
data, err1 := mwacc.FindManyAccByFilter(ctx, bson.M{mwacc.FN.MwAcc.RoleId: roleId})
|
||||
if err1 != nil {
|
||||
return false, err1
|
||||
}
|
||||
|
||||
if len(data) > 0 && IsEnb == false {
|
||||
return false, srvdef.New(srvdef.ErrDbUpdateError, "更新失败,此角色下拥有管理员", "")
|
||||
}
|
||||
|
||||
StatusErr := mwacc.UpOneRoleByFilter(ctx, bson.M{
|
||||
mwacc.FN.MwRole.Id: roleId,
|
||||
}, bson.M{
|
||||
"$set": bson.M{
|
||||
mwacc.FN.MwRole.IsEnb: IsEnb,
|
||||
},
|
||||
})
|
||||
|
||||
if StatusErr != nil {
|
||||
return false, StatusErr
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// ////////////////////////////////////////////////////////////////////////////
|
||||
// 数据库Router转网站Router
|
||||
|
||||
// // 角色权限列表
|
||||
// func RoleAuthList(ctx context.Context, roleId int64) (*mwacc.MwRole, error) {
|
||||
// opt := &options.FindOneOptions{}
|
||||
// opt.SetProjection(bson.M{
|
||||
// mwacc.FN.MwRole.Id: 1,
|
||||
// mwacc.FN.MwRole.Router: 1,
|
||||
// })
|
||||
|
||||
// res, err := mwacc.FindOneRoleByFilter(ctx, bson.M{mwacc.FN.MwRole.Id: roleId})
|
||||
// if err != nil {
|
||||
// return &mwacc.MwRole{}, err
|
||||
// }
|
||||
// if res.Id != 0 {
|
||||
// return res, nil
|
||||
// }
|
||||
// return nil, nil
|
||||
// }
|
||||
|
||||
func handleMenu(ctx context.Context, reqRouter []*mwacc.Router, apiMap map[string]int64) map[string]int64 {
|
||||
|
||||
if apiMap == nil {
|
||||
apiMap = make(map[string]int64)
|
||||
}
|
||||
|
||||
for _, v := range reqRouter {
|
||||
if v.Children != nil && len(v.Children) != 0 {
|
||||
childs := make([]*mwacc.Router, 0)
|
||||
for _, v := range v.Children {
|
||||
childs = append(childs, &v)
|
||||
}
|
||||
apiMap = handleMenu(ctx, childs, apiMap)
|
||||
}
|
||||
if v.ApiUrl != nil && len(v.ApiUrl) != 0 {
|
||||
for _, v := range v.ApiUrl {
|
||||
if v.Status == true {
|
||||
for _, value := range v.Url {
|
||||
apiMap[value] = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return apiMap
|
||||
}
|
||||
|
||||
// 更新权限
|
||||
func UpAuth(ctx context.Context, id int64, dataRouter []*mwacc.Router) (ok bool, err error) {
|
||||
|
||||
apiMap := handleMenu(ctx, dataRouter, nil)
|
||||
|
||||
keys := make([]string, 0)
|
||||
for k := range apiMap {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
return mwacc.UpRoleAuth(ctx, id, keys, dataRouter)
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package srv_playlet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/response"
|
||||
"videoweb/pub/common/slugx"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/playletcategorymod"
|
||||
"videoweb/pub/models/playletmod"
|
||||
"videoweb/web/api/proto"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
var playletCategoryWebProjection = bson.D{
|
||||
{Key: playletcategorymod.FN.PlayletCategory.Id, Value: 1},
|
||||
{Key: playletcategorymod.FN.PlayletCategory.Name, Value: 1},
|
||||
{Key: playletcategorymod.FN.PlayletCategory.Slug, Value: 1},
|
||||
{Key: playletcategorymod.FN.PlayletCategory.Icon, Value: 1},
|
||||
{Key: playletcategorymod.FN.PlayletCategory.Status, Value: 1},
|
||||
{Key: playletcategorymod.FN.PlayletCategory.Rank, Value: 1},
|
||||
{Key: playletcategorymod.FN.PlayletCategory.Remark, Value: 1},
|
||||
{Key: playletcategorymod.FN.PlayletCategory.SeoInfo.Title, Value: 1},
|
||||
{Key: playletcategorymod.FN.PlayletCategory.SeoInfo.Description, Value: 1},
|
||||
{Key: playletcategorymod.FN.PlayletCategory.SeoInfo.Keyworld, Value: 1},
|
||||
{Key: playletcategorymod.FN.PlayletCategory.CreatedAt, Value: 1},
|
||||
{Key: playletcategorymod.FN.PlayletCategory.UpdatedAt, Value: 1},
|
||||
}
|
||||
|
||||
// ListPlayletCategory 分页查询短剧分类列表
|
||||
func ListPlayletCategory(ctx context.Context, req *proto.PlayletCategoryListReq) (*proto.PlayletCategoryListPageResp, *srvdef.SrvErr) {
|
||||
if req == nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||||
}
|
||||
filter := bson.M{}
|
||||
if req.Id != nil && *req.Id > 0 {
|
||||
filter[playletcategorymod.FN.PlayletCategory.Id] = *req.Id
|
||||
}
|
||||
if req.Status != nil {
|
||||
filter[playletcategorymod.FN.PlayletCategory.Status] = *req.Status
|
||||
}
|
||||
if name := strings.TrimSpace(req.Name); name != "" {
|
||||
filter[playletcategorymod.FN.PlayletCategory.Name] = bson.M{"$regex": regexp.QuoteMeta(name)}
|
||||
}
|
||||
pageNum, pageSize := normalizePage(req.PageNum, req.PageSize)
|
||||
totalCount, err := playletcategorymod.CountPlayletCategory(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := &proto.PlayletCategoryListPageResp{
|
||||
BasePageResp: response.BasePageResp{
|
||||
TotalCount: totalCount,
|
||||
PageSize: int(pageSize),
|
||||
PageNum: int(pageNum),
|
||||
List: make([]*proto.PlayletCategoryListResp, 0),
|
||||
},
|
||||
}
|
||||
if totalCount == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
list, err := playletcategorymod.FindManyPlayletCategory(ctx, filter, options.Find().
|
||||
SetProjection(playletCategoryWebProjection).
|
||||
SetSort(bson.D{
|
||||
{Key: playletcategorymod.FN.PlayletCategory.Rank, Value: -1},
|
||||
{Key: playletcategorymod.FN.PlayletCategory.Id, Value: -1},
|
||||
}).
|
||||
SetSkip((pageNum-1)*pageSize).
|
||||
SetLimit(pageSize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]*proto.PlayletCategoryListResp, 0, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, &proto.PlayletCategoryListResp{
|
||||
Id: item.Id,
|
||||
Name: item.Name,
|
||||
Slug: item.Slug,
|
||||
Icon: item.Icon,
|
||||
Status: item.Status,
|
||||
Rank: item.Rank,
|
||||
Remark: item.Remark,
|
||||
SeoInfo: normalizeSeoValue(item.SeoInfo),
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
})
|
||||
}
|
||||
resp.List = items
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// AddPlayletCategory 新增短剧分类
|
||||
func AddPlayletCategory(ctx context.Context, req *proto.PlayletCategoryAddReq) (*proto.PlayletCategoryAddResp, *srvdef.SrvErr) {
|
||||
if req == nil || strings.TrimSpace(req.Name) == "" {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "分类名称不能为空", "")
|
||||
}
|
||||
doc := &playletcategorymod.PlayletCategory{
|
||||
Name: strings.TrimSpace(req.Name),
|
||||
Slug: req.Slug,
|
||||
Icon: strings.TrimSpace(req.Icon),
|
||||
Status: req.Status,
|
||||
Rank: req.Rank,
|
||||
Remark: strings.TrimSpace(req.Remark),
|
||||
SeoInfo: normalizeSeoValue(req.SeoInfo),
|
||||
}
|
||||
exists := func(s string) bool {
|
||||
item, _ := playletcategorymod.FindOnePlayletCategory(ctx, slugx.Filter(
|
||||
playletcategorymod.FN.PlayletCategory.Slug, playletcategorymod.FN.PlayletCategory.Id, s, 0))
|
||||
return item != nil
|
||||
}
|
||||
if serr := slugx.Ensure(&doc.Slug, utils.SlugSourceFromKeywordsOrTitle(doc.SeoInfo.Keyworld, doc.Name), exists); serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
if err := playletcategorymod.InsertOnePlayletCategory(ctx, doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clearAppPlayletHomeCache()
|
||||
return &proto.PlayletCategoryAddResp{Id: doc.Id}, nil
|
||||
}
|
||||
|
||||
// UpdatePlayletCategory 更新短剧分类
|
||||
func UpdatePlayletCategory(ctx context.Context, req *proto.PlayletCategoryUpdateReq) *srvdef.SrvErr {
|
||||
if req == nil || req.Id <= 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "分类ID无效", "")
|
||||
}
|
||||
current, err := playletcategorymod.FindOnePlayletCategory(ctx, bson.M{
|
||||
playletcategorymod.FN.PlayletCategory.Id: req.Id,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setDoc := bson.M{playletcategorymod.FN.PlayletCategory.UpdatedAt: time.Now()}
|
||||
if req.Name != nil {
|
||||
name := strings.TrimSpace(*req.Name)
|
||||
if name == "" {
|
||||
return srvdef.New(srvdef.ErrParamError, "分类名称不能为空", "")
|
||||
}
|
||||
setDoc[playletcategorymod.FN.PlayletCategory.Name] = name
|
||||
}
|
||||
if req.Icon != nil {
|
||||
setDoc[playletcategorymod.FN.PlayletCategory.Icon] = strings.TrimSpace(*req.Icon)
|
||||
}
|
||||
if req.Status != nil {
|
||||
setDoc[playletcategorymod.FN.PlayletCategory.Status] = *req.Status
|
||||
}
|
||||
if req.Rank != nil {
|
||||
setDoc[playletcategorymod.FN.PlayletCategory.Rank] = *req.Rank
|
||||
}
|
||||
if req.Remark != nil {
|
||||
setDoc[playletcategorymod.FN.PlayletCategory.Remark] = strings.TrimSpace(*req.Remark)
|
||||
}
|
||||
if req.SeoInfo != nil {
|
||||
setDoc["seoInfo"] = normalizeSeoValue(*req.SeoInfo)
|
||||
}
|
||||
name := current.Name
|
||||
if req.Name != nil {
|
||||
name = strings.TrimSpace(*req.Name)
|
||||
}
|
||||
keywords := current.SeoInfo.Keyworld
|
||||
if req.SeoInfo != nil {
|
||||
keywords = normalizeSeoValue(*req.SeoInfo).Keyworld
|
||||
}
|
||||
exists := func(s string) bool {
|
||||
item, _ := playletcategorymod.FindOnePlayletCategory(ctx, slugx.Filter(
|
||||
playletcategorymod.FN.PlayletCategory.Slug, playletcategorymod.FN.PlayletCategory.Id, s, req.Id))
|
||||
return item != nil
|
||||
}
|
||||
slug, changed, serr := slugx.PrepareOnUpdate(current.Slug, req.Slug,
|
||||
utils.SlugSourceFromKeywordsOrTitle(keywords, name), exists)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
if changed {
|
||||
setDoc[playletcategorymod.FN.PlayletCategory.Slug] = slug
|
||||
}
|
||||
if len(setDoc) <= 1 {
|
||||
return srvdef.New(srvdef.ErrParamError, "没有可更新字段", "")
|
||||
}
|
||||
if err := playletcategorymod.UpdateOnePlayletCategory(ctx,
|
||||
bson.M{playletcategorymod.FN.PlayletCategory.Id: req.Id},
|
||||
bson.M{"$set": setDoc},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
clearAppPlayletHomeCache()
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelPlayletCategory 删除短剧分类,被短剧引用时禁止删除
|
||||
func DelPlayletCategory(ctx context.Context, id int64) *srvdef.SrvErr {
|
||||
if id <= 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "分类ID无效", "")
|
||||
}
|
||||
usedCount, err := playletmod.CountPlaylet(ctx, bson.M{
|
||||
playletmod.FN.Playlet.CategoryIds: bson.M{"$in": []int64{id}},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if usedCount > 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "分类已被短剧引用,无法删除", "")
|
||||
}
|
||||
count, err := playletcategorymod.DelOnePlayletCategory(ctx, bson.M{playletcategorymod.FN.PlayletCategory.Id: id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return srvdef.New(srvdef.ErrNoData, "", "")
|
||||
}
|
||||
clearAppPlayletHomeCache()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package srv_playlet
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"videoweb/pub/common/playletcache"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPageSize int64 = 10
|
||||
maxPageSize int64 = 100
|
||||
)
|
||||
|
||||
func normalizePage(pageNum, pageSize int64) (int64, int64) {
|
||||
if pageNum <= 0 {
|
||||
pageNum = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = defaultPageSize
|
||||
}
|
||||
if pageSize > maxPageSize {
|
||||
pageSize = maxPageSize
|
||||
}
|
||||
return pageNum, pageSize
|
||||
}
|
||||
|
||||
// normalizeSeoValue 规范化 SEO 字段,避免 keywords 为 null
|
||||
func normalizeSeoValue(seo pubdef.SeoValue) pubdef.SeoValue {
|
||||
seo.Title = strings.TrimSpace(seo.Title)
|
||||
seo.Description = strings.TrimSpace(seo.Description)
|
||||
if seo.Keyworld == nil {
|
||||
seo.Keyworld = make([]string, 0)
|
||||
return seo
|
||||
}
|
||||
words := make([]string, 0, len(seo.Keyworld))
|
||||
for _, word := range seo.Keyworld {
|
||||
word = strings.TrimSpace(word)
|
||||
if word == "" {
|
||||
continue
|
||||
}
|
||||
words = append(words, word)
|
||||
}
|
||||
seo.Keyworld = words
|
||||
return seo
|
||||
}
|
||||
|
||||
func clearAppPlayletHomeCache() {
|
||||
playletcache.ClearHomeCache()
|
||||
}
|
||||
@@ -0,0 +1,651 @@
|
||||
package srv_playlet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/response"
|
||||
"videoweb/pub/common/slugx"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/playlethomeconfigmod"
|
||||
"videoweb/pub/models/playletthememod"
|
||||
"videoweb/pub/models/videocategorymod"
|
||||
"videoweb/web/api/proto"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// ListPlayletHomeConfig 分页查询首页配置列表
|
||||
func ListPlayletHomeConfig(ctx context.Context, req *proto.PlayletHomeConfigListReq) (*proto.PlayletHomeConfigListPageResp, *srvdef.SrvErr) {
|
||||
if req == nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||||
}
|
||||
filter, err := buildPlayletHomeConfigFilter(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pageNum, pageSize := normalizePage(req.PageNum, req.PageSize)
|
||||
totalCount, err := playlethomeconfigmod.CountPlayletHomeConfig(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := &proto.PlayletHomeConfigListPageResp{
|
||||
BasePageResp: response.BasePageResp{
|
||||
TotalCount: totalCount,
|
||||
PageSize: int(pageSize),
|
||||
PageNum: int(pageNum),
|
||||
List: make([]*proto.PlayletHomeConfigListResp, 0),
|
||||
},
|
||||
}
|
||||
if totalCount == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
list, err := playlethomeconfigmod.FindManyPlayletHomeConfig(ctx, filter, options.Find().
|
||||
SetSort(bson.D{
|
||||
{Key: playlethomeconfigmod.FN.PlayletHomeConfig.Rank, Value: -1},
|
||||
{Key: playlethomeconfigmod.FN.PlayletHomeConfig.Id, Value: -1},
|
||||
}).
|
||||
SetSkip((pageNum-1)*pageSize).
|
||||
SetLimit(pageSize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]*proto.PlayletHomeConfigListResp, 0, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, playletHomeConfigToResp(item))
|
||||
}
|
||||
resp.List = items
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ListPlayletHomeTopicOptions 分页查询未被其他启用模块占用的题材或视频分类
|
||||
func ListPlayletHomeTopicOptions(ctx context.Context, req *proto.PlayletHomeTopicOptionsReq) (*proto.PlayletHomeTopicOptionsResp, *srvdef.SrvErr) {
|
||||
if req == nil || (req.DataType != playlethomeconfigmod.DataTypeAll && req.DataType != playlethomeconfigmod.DataTypeVideo) {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "数据类型仅支持1短剧题材、2视频分类", "")
|
||||
}
|
||||
if req.ConfigId < 0 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "配置ID无效", "")
|
||||
}
|
||||
pageNum, pageSize := normalizePage(req.PageNum, req.PageSize)
|
||||
resp := &proto.PlayletHomeTopicOptionsResp{
|
||||
List: make([]*proto.PlayletHomeTopicOption, 0),
|
||||
SelectedList: make([]*proto.PlayletHomeTopicOption, 0),
|
||||
PageNum: pageNum,
|
||||
PageSize: pageSize,
|
||||
}
|
||||
var selectedIDs []int64
|
||||
if req.ConfigId > 0 {
|
||||
current, err := playlethomeconfigmod.FindOnePlayletHomeConfig(ctx, bson.M{playlethomeconfigmod.FN.PlayletHomeConfig.Id: req.ConfigId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if current.ModuleType != playlethomeconfigmod.ModuleTypeTopic || current.DataType != req.DataType {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "当前配置不是指定数据类型的专题模块", "")
|
||||
}
|
||||
selectedIDs = current.TopicIds
|
||||
}
|
||||
// 已启用的其他专题模块占用的ID不再出现在可选列表中
|
||||
configFilter := bson.M{
|
||||
playlethomeconfigmod.FN.PlayletHomeConfig.Enabled: true,
|
||||
playlethomeconfigmod.FN.PlayletHomeConfig.ModuleType: playlethomeconfigmod.ModuleTypeTopic,
|
||||
playlethomeconfigmod.FN.PlayletHomeConfig.DataType: req.DataType,
|
||||
}
|
||||
if req.ConfigId > 0 {
|
||||
configFilter[playlethomeconfigmod.FN.PlayletHomeConfig.Id] = bson.M{"$ne": req.ConfigId}
|
||||
}
|
||||
configs, err := playlethomeconfigmod.FindManyPlayletHomeConfig(ctx, configFilter,
|
||||
options.Find().SetProjection(bson.M{playlethomeconfigmod.FN.PlayletHomeConfig.TopicIds: 1}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
occupied := make(map[int64]struct{})
|
||||
for _, config := range configs {
|
||||
if config == nil {
|
||||
continue
|
||||
}
|
||||
for _, id := range config.TopicIds {
|
||||
occupied[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
skip := (pageNum - 1) * pageSize
|
||||
if req.DataType == playlethomeconfigmod.DataTypeVideo {
|
||||
filter := bson.M{videocategorymod.FN.VideoCategory.Status: true}
|
||||
if len(occupied) > 0 {
|
||||
ids := make([]int64, 0, len(occupied))
|
||||
for id := range occupied {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
filter[videocategorymod.FN.VideoCategory.Id] = bson.M{"$nin": ids}
|
||||
}
|
||||
resp.TotalCount, err = videocategorymod.CountVideoCategorys(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list, err := videocategorymod.FindManyVideoCategorys(ctx, filter, options.Find().
|
||||
SetProjection(bson.M{videocategorymod.FN.VideoCategory.Id: 1, videocategorymod.FN.VideoCategory.Name: 1, videocategorymod.FN.VideoCategory.Status: 1}).
|
||||
SetSort(bson.D{{Key: videocategorymod.FN.VideoCategory.Rank, Value: -1}, {Key: videocategorymod.FN.VideoCategory.Id, Value: -1}}).
|
||||
SetSkip(skip).SetLimit(pageSize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range list {
|
||||
if item != nil {
|
||||
resp.List = append(resp.List, &proto.PlayletHomeTopicOption{Id: item.Id, Name: item.Name, Status: item.Status})
|
||||
}
|
||||
}
|
||||
if len(selectedIDs) > 0 {
|
||||
selected, err := videocategorymod.FindManyVideoCategorys(ctx, bson.M{videocategorymod.FN.VideoCategory.Id: bson.M{"$in": selectedIDs}},
|
||||
options.Find().SetProjection(bson.M{videocategorymod.FN.VideoCategory.Id: 1, videocategorymod.FN.VideoCategory.Name: 1, videocategorymod.FN.VideoCategory.Status: 1}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
byID := make(map[int64]*proto.PlayletHomeTopicOption, len(selected))
|
||||
for _, item := range selected {
|
||||
if item != nil {
|
||||
byID[item.Id] = &proto.PlayletHomeTopicOption{Id: item.Id, Name: item.Name, Status: item.Status}
|
||||
}
|
||||
}
|
||||
for _, id := range selectedIDs {
|
||||
if item := byID[id]; item != nil {
|
||||
resp.SelectedList = append(resp.SelectedList, item)
|
||||
} else {
|
||||
resp.SelectedList = append(resp.SelectedList, &proto.PlayletHomeTopicOption{Id: id})
|
||||
}
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
filter := bson.M{playletthememod.FN.PlayletTheme.Status: true}
|
||||
if len(occupied) > 0 {
|
||||
ids := make([]int64, 0, len(occupied))
|
||||
for id := range occupied {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
filter[playletthememod.FN.PlayletTheme.Id] = bson.M{"$nin": ids}
|
||||
}
|
||||
resp.TotalCount, err = playletthememod.CountPlayletTheme(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list, err := playletthememod.FindManyPlayletTheme(ctx, filter, options.Find().
|
||||
SetProjection(bson.M{playletthememod.FN.PlayletTheme.Id: 1, playletthememod.FN.PlayletTheme.Name: 1, playletthememod.FN.PlayletTheme.Status: 1}).
|
||||
SetSort(bson.D{{Key: playletthememod.FN.PlayletTheme.Rank, Value: -1}, {Key: playletthememod.FN.PlayletTheme.Id, Value: -1}}).
|
||||
SetSkip(skip).SetLimit(pageSize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range list {
|
||||
if item != nil {
|
||||
resp.List = append(resp.List, &proto.PlayletHomeTopicOption{Id: item.Id, Name: item.Name, Status: item.Status})
|
||||
}
|
||||
}
|
||||
if len(selectedIDs) > 0 {
|
||||
selected, err := playletthememod.FindManyPlayletTheme(ctx, bson.M{playletthememod.FN.PlayletTheme.Id: bson.M{"$in": selectedIDs}},
|
||||
options.Find().SetProjection(bson.M{playletthememod.FN.PlayletTheme.Id: 1, playletthememod.FN.PlayletTheme.Name: 1, playletthememod.FN.PlayletTheme.Status: 1}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
byID := make(map[int64]*proto.PlayletHomeTopicOption, len(selected))
|
||||
for _, item := range selected {
|
||||
if item != nil {
|
||||
byID[item.Id] = &proto.PlayletHomeTopicOption{Id: item.Id, Name: item.Name, Status: item.Status}
|
||||
}
|
||||
}
|
||||
for _, id := range selectedIDs {
|
||||
if item := byID[id]; item != nil {
|
||||
resp.SelectedList = append(resp.SelectedList, item)
|
||||
} else {
|
||||
resp.SelectedList = append(resp.SelectedList, &proto.PlayletHomeTopicOption{Id: id})
|
||||
}
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// AddPlayletHomeConfig 新增首页配置
|
||||
func AddPlayletHomeConfig(ctx context.Context, req *proto.PlayletHomeConfigAddReq) (*proto.PlayletHomeConfigAddResp, *srvdef.SrvErr) {
|
||||
if req == nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||||
}
|
||||
title, style, moduleType, dataType, recommendType, limit, err := normalizePlayletHomeConfig(req.Title, req.Style, req.ModuleType, req.DataType, req.RecommendType, req.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
doc := &playlethomeconfigmod.PlayletHomeConfig{
|
||||
Title: title,
|
||||
Slug: req.Slug,
|
||||
Desc: strings.TrimSpace(req.Desc),
|
||||
Label: strings.TrimSpace(req.Label),
|
||||
Style: style,
|
||||
CardBackgroundImage: strings.TrimSpace(req.CardBackgroundImage),
|
||||
ModuleType: moduleType,
|
||||
DataType: dataType,
|
||||
RecommendType: recommendType,
|
||||
CategoryId: req.CategoryId,
|
||||
TopicIds: req.TopicIds,
|
||||
Rank: req.Rank,
|
||||
Enabled: req.Enabled,
|
||||
Limit: limit,
|
||||
}
|
||||
if err := validateHomeConfigTopics(ctx, doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
exists := func(s string) bool {
|
||||
item, _ := playlethomeconfigmod.FindOnePlayletHomeConfig(ctx, slugx.Filter(
|
||||
playlethomeconfigmod.FN.PlayletHomeConfig.Slug, playlethomeconfigmod.FN.PlayletHomeConfig.Id, s, 0))
|
||||
return item != nil
|
||||
}
|
||||
if serr := slugx.Ensure(&doc.Slug, utils.SlugSourceFromKeywordsOrTitle(nil, title), exists); serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
if err := playlethomeconfigmod.InsertOnePlayletHomeConfig(ctx, doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clearAppPlayletHomeCache()
|
||||
return &proto.PlayletHomeConfigAddResp{Id: doc.Id}, nil
|
||||
}
|
||||
|
||||
// UpdatePlayletHomeConfig 更新首页配置
|
||||
func UpdatePlayletHomeConfig(ctx context.Context, req *proto.PlayletHomeConfigUpdateReq) *srvdef.SrvErr {
|
||||
if req == nil || req.Id <= 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "id无效", "")
|
||||
}
|
||||
setDoc := bson.M{playlethomeconfigmod.FN.PlayletHomeConfig.UpdatedAt: time.Now()}
|
||||
current, err := playlethomeconfigmod.FindOnePlayletHomeConfig(ctx, bson.M{playlethomeconfigmod.FN.PlayletHomeConfig.Id: req.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Title != nil {
|
||||
title := strings.TrimSpace(*req.Title)
|
||||
if title == "" {
|
||||
return srvdef.New(srvdef.ErrParamError, "模块标题不能为空", "")
|
||||
}
|
||||
setDoc[playlethomeconfigmod.FN.PlayletHomeConfig.Title] = title
|
||||
}
|
||||
if req.Desc != nil {
|
||||
setDoc[playlethomeconfigmod.FN.PlayletHomeConfig.Desc] = strings.TrimSpace(*req.Desc)
|
||||
}
|
||||
if req.Label != nil {
|
||||
setDoc[playlethomeconfigmod.FN.PlayletHomeConfig.Label] = strings.TrimSpace(*req.Label)
|
||||
}
|
||||
if req.Style != nil {
|
||||
style, err := normalizePlayletHomeConfigStyle(*req.Style)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setDoc[playlethomeconfigmod.FN.PlayletHomeConfig.Style] = style
|
||||
}
|
||||
if req.CardBackgroundImage != nil {
|
||||
setDoc[playlethomeconfigmod.FN.PlayletHomeConfig.CardBackgroundImage] = strings.TrimSpace(*req.CardBackgroundImage)
|
||||
}
|
||||
if req.ModuleType != nil {
|
||||
moduleType, err := normalizePlayletHomeModuleType(*req.ModuleType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setDoc[playlethomeconfigmod.FN.PlayletHomeConfig.ModuleType] = moduleType
|
||||
}
|
||||
if req.DataType != nil {
|
||||
dataType, err := normalizePlayletHomeDataType(*req.DataType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setDoc[playlethomeconfigmod.FN.PlayletHomeConfig.DataType] = dataType
|
||||
}
|
||||
if req.RecommendType != nil {
|
||||
if err := validatePlayletHomeRecommendType(*req.RecommendType); err != nil {
|
||||
return err
|
||||
}
|
||||
setDoc[playlethomeconfigmod.FN.PlayletHomeConfig.RecommendType] = *req.RecommendType
|
||||
}
|
||||
if req.CategoryId != nil {
|
||||
setDoc[playlethomeconfigmod.FN.PlayletHomeConfig.CategoryId] = *req.CategoryId
|
||||
}
|
||||
if req.TopicIds != nil {
|
||||
current.TopicIds = *req.TopicIds
|
||||
}
|
||||
if req.ModuleType != nil {
|
||||
current.ModuleType = *req.ModuleType
|
||||
}
|
||||
if req.DataType != nil {
|
||||
current.DataType = *req.DataType
|
||||
}
|
||||
if req.Enabled != nil {
|
||||
current.Enabled = *req.Enabled
|
||||
}
|
||||
if req.TopicIds != nil || req.ModuleType != nil || req.DataType != nil || req.Enabled != nil {
|
||||
if err := validateHomeConfigTopics(ctx, current); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.TopicIds != nil {
|
||||
setDoc[playlethomeconfigmod.FN.PlayletHomeConfig.TopicIds] = current.TopicIds
|
||||
}
|
||||
}
|
||||
if req.Rank != nil {
|
||||
setDoc[playlethomeconfigmod.FN.PlayletHomeConfig.Rank] = *req.Rank
|
||||
}
|
||||
if req.Enabled != nil {
|
||||
setDoc[playlethomeconfigmod.FN.PlayletHomeConfig.Enabled] = *req.Enabled
|
||||
}
|
||||
if req.Limit != nil {
|
||||
setDoc[playlethomeconfigmod.FN.PlayletHomeConfig.Limit] = normalizePlayletHomeConfigLimit(*req.Limit)
|
||||
}
|
||||
title := current.Title
|
||||
if req.Title != nil {
|
||||
title = strings.TrimSpace(*req.Title)
|
||||
}
|
||||
exists := func(s string) bool {
|
||||
item, _ := playlethomeconfigmod.FindOnePlayletHomeConfig(ctx, slugx.Filter(
|
||||
playlethomeconfigmod.FN.PlayletHomeConfig.Slug, playlethomeconfigmod.FN.PlayletHomeConfig.Id, s, req.Id))
|
||||
return item != nil
|
||||
}
|
||||
slug, changed, serr := slugx.PrepareOnUpdate(current.Slug, req.Slug,
|
||||
utils.SlugSourceFromKeywordsOrTitle(nil, title), exists)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
if changed {
|
||||
setDoc[playlethomeconfigmod.FN.PlayletHomeConfig.Slug] = slug
|
||||
}
|
||||
if len(setDoc) <= 1 {
|
||||
return srvdef.New(srvdef.ErrParamError, "没有可更新字段", "")
|
||||
}
|
||||
if err := playlethomeconfigmod.UpdateOnePlayletHomeConfig(ctx,
|
||||
bson.M{playlethomeconfigmod.FN.PlayletHomeConfig.Id: req.Id},
|
||||
bson.M{"$set": setDoc},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
clearAppPlayletHomeCache()
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelPlayletHomeConfig 删除首页配置
|
||||
func DelPlayletHomeConfig(ctx context.Context, req *proto.PlayletHomeConfigDelReq) *srvdef.SrvErr {
|
||||
if req == nil || len(req.Ids) == 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "配置ID不能为空", "")
|
||||
}
|
||||
ids := make([]int64, 0, len(req.Ids))
|
||||
seen := make(map[int64]struct{}, len(req.Ids))
|
||||
for _, id := range req.Ids {
|
||||
if id <= 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "配置ID无效", "")
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
count, err := playlethomeconfigmod.DelManyPlayletHomeConfig(ctx, bson.M{
|
||||
playlethomeconfigmod.FN.PlayletHomeConfig.Id: bson.M{"$in": ids},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return srvdef.New(srvdef.ErrNoData, "", "")
|
||||
}
|
||||
clearAppPlayletHomeCache()
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildPlayletHomeConfigFilter(req *proto.PlayletHomeConfigListReq) (bson.M, *srvdef.SrvErr) {
|
||||
filter := bson.M{}
|
||||
if req.Id != nil && *req.Id > 0 {
|
||||
filter[playlethomeconfigmod.FN.PlayletHomeConfig.Id] = *req.Id
|
||||
}
|
||||
if slug := strings.TrimSpace(req.Slug); slug != "" {
|
||||
filter[playlethomeconfigmod.FN.PlayletHomeConfig.Slug] = slug
|
||||
}
|
||||
if title := strings.TrimSpace(req.Title); title != "" {
|
||||
filter[playlethomeconfigmod.FN.PlayletHomeConfig.Title] = bson.M{"$regex": regexp.QuoteMeta(title)}
|
||||
}
|
||||
if desc := strings.TrimSpace(req.Desc); desc != "" {
|
||||
filter[playlethomeconfigmod.FN.PlayletHomeConfig.Desc] = bson.M{"$regex": regexp.QuoteMeta(desc)}
|
||||
}
|
||||
if label := strings.TrimSpace(req.Label); label != "" {
|
||||
filter[playlethomeconfigmod.FN.PlayletHomeConfig.Label] = bson.M{"$regex": regexp.QuoteMeta(label)}
|
||||
}
|
||||
if style := strings.TrimSpace(req.Style); style != "" {
|
||||
normalizedStyle, err := normalizePlayletHomeConfigStyle(style)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filter[playlethomeconfigmod.FN.PlayletHomeConfig.Style] = normalizedStyle
|
||||
}
|
||||
if req.ModuleType != nil {
|
||||
moduleType, err := normalizePlayletHomeModuleType(*req.ModuleType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if moduleType == playlethomeconfigmod.ModuleTypePlaylet {
|
||||
filter[playlethomeconfigmod.FN.PlayletHomeConfig.ModuleType] = bson.M{"$in": []interface{}{nil, int64(0), playlethomeconfigmod.ModuleTypePlaylet}}
|
||||
} else {
|
||||
filter[playlethomeconfigmod.FN.PlayletHomeConfig.ModuleType] = moduleType
|
||||
}
|
||||
}
|
||||
if req.DataType != nil {
|
||||
dataType, err := normalizePlayletHomeDataType(*req.DataType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dataType == playlethomeconfigmod.DataTypeAll {
|
||||
filter[playlethomeconfigmod.FN.PlayletHomeConfig.DataType] = bson.M{"$in": []interface{}{nil, int64(0), playlethomeconfigmod.DataTypeAll}}
|
||||
} else {
|
||||
filter[playlethomeconfigmod.FN.PlayletHomeConfig.DataType] = dataType
|
||||
}
|
||||
}
|
||||
if req.RecommendType != nil {
|
||||
if err := validatePlayletHomeRecommendType(*req.RecommendType); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filter[playlethomeconfigmod.FN.PlayletHomeConfig.RecommendType] = *req.RecommendType
|
||||
}
|
||||
if req.CategoryId != nil && *req.CategoryId > 0 {
|
||||
filter[playlethomeconfigmod.FN.PlayletHomeConfig.CategoryId] = *req.CategoryId
|
||||
}
|
||||
if req.Enabled != nil {
|
||||
filter[playlethomeconfigmod.FN.PlayletHomeConfig.Enabled] = *req.Enabled
|
||||
}
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
func playletHomeConfigToResp(item *playlethomeconfigmod.PlayletHomeConfig) *proto.PlayletHomeConfigListResp {
|
||||
if item == nil {
|
||||
return nil
|
||||
}
|
||||
return &proto.PlayletHomeConfigListResp{
|
||||
Id: item.Id,
|
||||
Slug: item.Slug,
|
||||
Title: item.Title,
|
||||
Desc: item.Desc,
|
||||
Label: item.Label,
|
||||
Style: item.Style,
|
||||
CardBackgroundImage: item.CardBackgroundImage,
|
||||
ModuleType: normalizePlayletHomeModuleTypeForResp(item.ModuleType),
|
||||
DataType: normalizePlayletHomeDataTypeForResp(item.DataType),
|
||||
RecommendType: item.RecommendType,
|
||||
CategoryId: item.CategoryId,
|
||||
TopicIds: item.TopicIds,
|
||||
Rank: item.Rank,
|
||||
Enabled: item.Enabled,
|
||||
Limit: item.Limit,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// validateHomeConfigTopics 校验专题归属和已启用模块间的专题唯一性
|
||||
func validateHomeConfigTopics(ctx context.Context, doc *playlethomeconfigmod.PlayletHomeConfig) *srvdef.SrvErr {
|
||||
if doc.ModuleType != playlethomeconfigmod.ModuleTypeTopic {
|
||||
if len(doc.TopicIds) > 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "列表模块不能配置专题ID", "")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
seen := make(map[int64]struct{}, len(doc.TopicIds))
|
||||
ids := make([]int64, 0, len(doc.TopicIds))
|
||||
for _, id := range doc.TopicIds {
|
||||
if id <= 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "专题ID必须大于0", "")
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
doc.TopicIds = ids
|
||||
if !doc.Enabled {
|
||||
return nil
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "启用的专题模块必须配置专题ID", "")
|
||||
}
|
||||
if doc.DataType == playlethomeconfigmod.DataTypeVideo {
|
||||
categories, err := videocategorymod.FindManyVideoCategorys(ctx, bson.M{
|
||||
videocategorymod.FN.VideoCategory.Id: bson.M{"$in": ids},
|
||||
videocategorymod.FN.VideoCategory.Status: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(categories) != len(ids) {
|
||||
return srvdef.New(srvdef.ErrParamError, "视频分类不存在或未上架", "")
|
||||
}
|
||||
} else {
|
||||
themes, err := playletthememod.FindManyPlayletTheme(ctx, bson.M{
|
||||
playletthememod.FN.PlayletTheme.Id: bson.M{"$in": ids},
|
||||
playletthememod.FN.PlayletTheme.Status: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(themes) != len(ids) {
|
||||
return srvdef.New(srvdef.ErrParamError, "短剧题材不存在或未上架", "")
|
||||
}
|
||||
}
|
||||
configs, err := playlethomeconfigmod.FindManyPlayletHomeConfig(ctx, bson.M{
|
||||
playlethomeconfigmod.FN.PlayletHomeConfig.Enabled: true,
|
||||
playlethomeconfigmod.FN.PlayletHomeConfig.ModuleType: playlethomeconfigmod.ModuleTypeTopic,
|
||||
playlethomeconfigmod.FN.PlayletHomeConfig.DataType: doc.DataType,
|
||||
playlethomeconfigmod.FN.PlayletHomeConfig.TopicIds: bson.M{"$in": ids},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, other := range configs {
|
||||
if other != nil && other.Id != doc.Id {
|
||||
return srvdef.New(srvdef.ErrParamError, "专题已分配给其他启用的首页模块", "")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizePlayletHomeConfig(title string, style string, moduleType int64, dataType int64, recommendType int64, limit int64) (string, string, int64, int64, int64, int64, *srvdef.SrvErr) {
|
||||
title = strings.TrimSpace(title)
|
||||
if title == "" {
|
||||
return "", "", 0, 0, 0, 0, srvdef.New(srvdef.ErrParamError, "模块标题不能为空", "")
|
||||
}
|
||||
normalizedStyle, err := normalizePlayletHomeConfigStyle(style)
|
||||
if err != nil {
|
||||
return "", "", 0, 0, 0, 0, err
|
||||
}
|
||||
normalizedModuleType, err := normalizePlayletHomeModuleType(moduleType)
|
||||
if err != nil {
|
||||
return "", "", 0, 0, 0, 0, err
|
||||
}
|
||||
normalizedDataType, err := normalizePlayletHomeDataType(dataType)
|
||||
if err != nil {
|
||||
return "", "", 0, 0, 0, 0, err
|
||||
}
|
||||
if err := validatePlayletHomeRecommendType(recommendType); err != nil {
|
||||
return "", "", 0, 0, 0, 0, err
|
||||
}
|
||||
return title, normalizedStyle, normalizedModuleType, normalizedDataType, recommendType, normalizePlayletHomeConfigLimit(limit), nil
|
||||
}
|
||||
|
||||
func normalizePlayletHomeConfigStyle(style string) (string, *srvdef.SrvErr) {
|
||||
style = strings.ToUpper(strings.TrimSpace(style))
|
||||
switch style {
|
||||
case playlethomeconfigmod.StyleA,
|
||||
playlethomeconfigmod.StyleB,
|
||||
playlethomeconfigmod.StyleC,
|
||||
playlethomeconfigmod.StyleD,
|
||||
playlethomeconfigmod.StyleE,
|
||||
playlethomeconfigmod.StyleF,
|
||||
playlethomeconfigmod.StyleG,
|
||||
playlethomeconfigmod.StyleH:
|
||||
return style, nil
|
||||
default:
|
||||
return "", srvdef.New(srvdef.ErrParamError, "样式无效,仅支持A-H", "")
|
||||
}
|
||||
}
|
||||
|
||||
func validatePlayletHomeRecommendType(recommendType int64) *srvdef.SrvErr {
|
||||
switch recommendType {
|
||||
case playlethomeconfigmod.RecommendTypeUpdated,
|
||||
playlethomeconfigmod.RecommendTypeLike,
|
||||
playlethomeconfigmod.RecommendTypeBrowse,
|
||||
playlethomeconfigmod.RecommendTypeHeat,
|
||||
playlethomeconfigmod.RecommendTypeCategory:
|
||||
return nil
|
||||
default:
|
||||
return srvdef.New(srvdef.ErrParamError, "推荐类型无效,仅支持1按更新、2按点赞、3按观看、4按热度、5按分类", "")
|
||||
}
|
||||
}
|
||||
|
||||
func normalizePlayletHomeModuleType(moduleType int64) (int64, *srvdef.SrvErr) {
|
||||
if moduleType == 0 {
|
||||
return playlethomeconfigmod.ModuleTypePlaylet, nil
|
||||
}
|
||||
switch moduleType {
|
||||
case playlethomeconfigmod.ModuleTypePlaylet, playlethomeconfigmod.ModuleTypeTopic:
|
||||
return moduleType, nil
|
||||
default:
|
||||
return 0, srvdef.New(srvdef.ErrParamError, "模块类型无效,仅支持1短剧列表、2专题", "")
|
||||
}
|
||||
}
|
||||
|
||||
func normalizePlayletHomeModuleTypeForResp(moduleType int64) int64 {
|
||||
if moduleType == playlethomeconfigmod.ModuleTypeTopic {
|
||||
return playlethomeconfigmod.ModuleTypeTopic
|
||||
}
|
||||
return playlethomeconfigmod.ModuleTypePlaylet
|
||||
}
|
||||
|
||||
func normalizePlayletHomeDataType(dataType int64) (int64, *srvdef.SrvErr) {
|
||||
if dataType == 0 {
|
||||
return playlethomeconfigmod.DataTypeAll, nil
|
||||
}
|
||||
switch dataType {
|
||||
case playlethomeconfigmod.DataTypeAll, playlethomeconfigmod.DataTypeVideo:
|
||||
return dataType, nil
|
||||
default:
|
||||
return 0, srvdef.New(srvdef.ErrParamError, "数据类型无效,仅支持1全部上架短剧、2全部上架视频", "")
|
||||
}
|
||||
}
|
||||
|
||||
func normalizePlayletHomeDataTypeForResp(dataType int64) int64 {
|
||||
if dataType == playlethomeconfigmod.DataTypeVideo {
|
||||
return playlethomeconfigmod.DataTypeVideo
|
||||
}
|
||||
return playlethomeconfigmod.DataTypeAll
|
||||
}
|
||||
|
||||
func normalizePlayletHomeConfigLimit(limit int64) int64 {
|
||||
if limit <= 0 {
|
||||
return playlethomeconfigmod.DefaultLimit
|
||||
}
|
||||
if limit > playlethomeconfigmod.MaxLimit {
|
||||
return playlethomeconfigmod.MaxLimit
|
||||
}
|
||||
return limit
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,152 @@
|
||||
package srv_playlet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/response"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/playletmod"
|
||||
"videoweb/pub/models/playlettagmod"
|
||||
"videoweb/web/api/proto"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// ListPlayletTag 分页查询标签列表
|
||||
func ListPlayletTag(ctx context.Context, req *proto.PlayletTagListReq) (*proto.PlayletTagListPageResp, *srvdef.SrvErr) {
|
||||
if req == nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||||
}
|
||||
filter := bson.M{}
|
||||
if req.Id != nil && *req.Id > 0 {
|
||||
filter[playlettagmod.FN.PlayletTag.Id] = *req.Id
|
||||
}
|
||||
if req.Status != nil {
|
||||
filter[playlettagmod.FN.PlayletTag.Status] = *req.Status
|
||||
}
|
||||
if name := strings.TrimSpace(req.Name); name != "" {
|
||||
filter[playlettagmod.FN.PlayletTag.Name] = bson.M{"$regex": regexp.QuoteMeta(name)}
|
||||
}
|
||||
pageNum, pageSize := normalizePage(req.PageNum, req.PageSize)
|
||||
totalCount, err := playlettagmod.CountPlayletTag(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := &proto.PlayletTagListPageResp{
|
||||
BasePageResp: response.BasePageResp{
|
||||
TotalCount: totalCount,
|
||||
PageSize: int(pageSize),
|
||||
PageNum: int(pageNum),
|
||||
List: make([]*proto.PlayletTagListResp, 0),
|
||||
},
|
||||
}
|
||||
if totalCount == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
list, err := playlettagmod.FindManyPlayletTag(ctx, filter, options.Find().
|
||||
SetSort(bson.D{
|
||||
{Key: playlettagmod.FN.PlayletTag.Rank, Value: -1},
|
||||
{Key: playlettagmod.FN.PlayletTag.Id, Value: -1},
|
||||
}).
|
||||
SetSkip((pageNum-1)*pageSize).
|
||||
SetLimit(pageSize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]*proto.PlayletTagListResp, 0, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, &proto.PlayletTagListResp{
|
||||
Id: item.Id,
|
||||
Name: item.Name,
|
||||
Status: item.Status,
|
||||
Rank: item.Rank,
|
||||
SeoInfo: normalizeSeoValue(item.SeoInfo),
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
})
|
||||
}
|
||||
resp.List = items
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// AddPlayletTag 新增标签
|
||||
func AddPlayletTag(ctx context.Context, req *proto.PlayletTagAddReq) (*proto.PlayletTagAddResp, *srvdef.SrvErr) {
|
||||
if req == nil || strings.TrimSpace(req.Name) == "" {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "标签名称不能为空", "")
|
||||
}
|
||||
doc := &playlettagmod.PlayletTag{
|
||||
Name: strings.TrimSpace(req.Name),
|
||||
Status: req.Status,
|
||||
Rank: req.Rank,
|
||||
SeoInfo: normalizeSeoValue(req.SeoInfo),
|
||||
}
|
||||
if err := playlettagmod.InsertOnePlayletTag(ctx, doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clearAppPlayletHomeCache()
|
||||
return &proto.PlayletTagAddResp{Id: doc.Id}, nil
|
||||
}
|
||||
|
||||
// UpdatePlayletTag 更新标签
|
||||
func UpdatePlayletTag(ctx context.Context, req *proto.PlayletTagUpdateReq) *srvdef.SrvErr {
|
||||
if req == nil || req.Id <= 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "id无效", "")
|
||||
}
|
||||
setDoc := bson.M{playlettagmod.FN.PlayletTag.UpdatedAt: time.Now()}
|
||||
if req.Name != nil {
|
||||
name := strings.TrimSpace(*req.Name)
|
||||
if name == "" {
|
||||
return srvdef.New(srvdef.ErrParamError, "标签名称不能为空", "")
|
||||
}
|
||||
setDoc[playlettagmod.FN.PlayletTag.Name] = name
|
||||
}
|
||||
if req.Status != nil {
|
||||
setDoc[playlettagmod.FN.PlayletTag.Status] = *req.Status
|
||||
}
|
||||
if req.Rank != nil {
|
||||
setDoc[playlettagmod.FN.PlayletTag.Rank] = *req.Rank
|
||||
}
|
||||
if req.SeoInfo != nil {
|
||||
setDoc["seoInfo"] = normalizeSeoValue(*req.SeoInfo)
|
||||
}
|
||||
if len(setDoc) <= 1 {
|
||||
return srvdef.New(srvdef.ErrParamError, "没有可更新字段", "")
|
||||
}
|
||||
if err := playlettagmod.UpdateOnePlayletTag(ctx,
|
||||
bson.M{playlettagmod.FN.PlayletTag.Id: req.Id},
|
||||
bson.M{"$set": setDoc},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
clearAppPlayletHomeCache()
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelPlayletTag 删除标签(被短剧引用时禁止删除)
|
||||
func DelPlayletTag(ctx context.Context, id int64) *srvdef.SrvErr {
|
||||
if id <= 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "id无效", "")
|
||||
}
|
||||
usedCount, err := playletmod.CountPlaylet(ctx, bson.M{playletmod.FN.Playlet.TagIds: id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if usedCount > 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "标签已被短剧引用,无法删除", "")
|
||||
}
|
||||
count, err := playlettagmod.DelOnePlayletTag(ctx, bson.M{playlettagmod.FN.PlayletTag.Id: id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return srvdef.New(srvdef.ErrNoData, "", "")
|
||||
}
|
||||
clearAppPlayletHomeCache()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package srv_playlet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/response"
|
||||
"videoweb/pub/common/slugx"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/playletmod"
|
||||
"videoweb/pub/models/playletthememod"
|
||||
"videoweb/web/api/proto"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
var playletThemeWebProjection = bson.D{
|
||||
{Key: playletthememod.FN.PlayletTheme.Id, Value: 1},
|
||||
{Key: playletthememod.FN.PlayletTheme.Name, Value: 1},
|
||||
{Key: playletthememod.FN.PlayletTheme.Slug, Value: 1},
|
||||
{Key: playletthememod.FN.PlayletTheme.Icon, Value: 1},
|
||||
{Key: playletthememod.FN.PlayletTheme.Status, Value: 1},
|
||||
{Key: playletthememod.FN.PlayletTheme.Rank, Value: 1},
|
||||
{Key: playletthememod.FN.PlayletTheme.SeoInfo.Title, Value: 1},
|
||||
{Key: playletthememod.FN.PlayletTheme.SeoInfo.Description, Value: 1},
|
||||
{Key: playletthememod.FN.PlayletTheme.SeoInfo.Keyworld, Value: 1},
|
||||
{Key: playletthememod.FN.PlayletTheme.CreatedAt, Value: 1},
|
||||
{Key: playletthememod.FN.PlayletTheme.UpdatedAt, Value: 1},
|
||||
}
|
||||
|
||||
// ListPlayletTheme 分页查询题材列表
|
||||
func ListPlayletTheme(ctx context.Context, req *proto.PlayletThemeListReq) (*proto.PlayletThemeListPageResp, *srvdef.SrvErr) {
|
||||
if req == nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||||
}
|
||||
filter := bson.M{}
|
||||
if req.Id != nil && *req.Id > 0 {
|
||||
filter[playletthememod.FN.PlayletTheme.Id] = *req.Id
|
||||
}
|
||||
if req.Status != nil {
|
||||
filter[playletthememod.FN.PlayletTheme.Status] = *req.Status
|
||||
}
|
||||
if name := strings.TrimSpace(req.Name); name != "" {
|
||||
filter[playletthememod.FN.PlayletTheme.Name] = bson.M{"$regex": regexp.QuoteMeta(name)}
|
||||
}
|
||||
pageNum, pageSize := normalizePage(req.PageNum, req.PageSize)
|
||||
totalCount, err := playletthememod.CountPlayletTheme(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := &proto.PlayletThemeListPageResp{
|
||||
BasePageResp: response.BasePageResp{
|
||||
TotalCount: totalCount,
|
||||
PageSize: int(pageSize),
|
||||
PageNum: int(pageNum),
|
||||
List: make([]*proto.PlayletThemeListResp, 0),
|
||||
},
|
||||
}
|
||||
if totalCount == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
list, err := playletthememod.FindManyPlayletTheme(ctx, filter, options.Find().
|
||||
SetProjection(playletThemeWebProjection).
|
||||
SetSort(bson.D{
|
||||
{Key: playletthememod.FN.PlayletTheme.Rank, Value: -1},
|
||||
{Key: playletthememod.FN.PlayletTheme.Id, Value: -1},
|
||||
}).
|
||||
SetSkip((pageNum-1)*pageSize).
|
||||
SetLimit(pageSize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]*proto.PlayletThemeListResp, 0, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, &proto.PlayletThemeListResp{
|
||||
Id: item.Id,
|
||||
Name: item.Name,
|
||||
Slug: item.Slug,
|
||||
Icon: item.Icon,
|
||||
Status: item.Status,
|
||||
Rank: item.Rank,
|
||||
SeoInfo: normalizeSeoValue(item.SeoInfo),
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
})
|
||||
}
|
||||
resp.List = items
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// AddPlayletTheme 新增题材
|
||||
func AddPlayletTheme(ctx context.Context, req *proto.PlayletThemeAddReq) (*proto.PlayletThemeAddResp, *srvdef.SrvErr) {
|
||||
if req == nil || strings.TrimSpace(req.Name) == "" {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "题材名称不能为空", "")
|
||||
}
|
||||
doc := &playletthememod.PlayletTheme{
|
||||
Name: strings.TrimSpace(req.Name),
|
||||
Slug: req.Slug,
|
||||
Icon: strings.TrimSpace(req.Icon),
|
||||
Status: req.Status,
|
||||
Rank: req.Rank,
|
||||
SeoInfo: normalizeSeoValue(req.SeoInfo),
|
||||
}
|
||||
exists := func(s string) bool {
|
||||
item, _ := playletthememod.FindOnePlayletTheme(ctx, slugx.Filter(
|
||||
playletthememod.FN.PlayletTheme.Slug, playletthememod.FN.PlayletTheme.Id, s, 0))
|
||||
return item != nil
|
||||
}
|
||||
if serr := slugx.Ensure(&doc.Slug, utils.SlugSourceFromKeywordsOrTitle(doc.SeoInfo.Keyworld, doc.Name), exists); serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
if err := playletthememod.InsertOnePlayletTheme(ctx, doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clearAppPlayletHomeCache()
|
||||
return &proto.PlayletThemeAddResp{Id: doc.Id}, nil
|
||||
}
|
||||
|
||||
// UpdatePlayletTheme 更新题材
|
||||
func UpdatePlayletTheme(ctx context.Context, req *proto.PlayletThemeUpdateReq) *srvdef.SrvErr {
|
||||
if req == nil || req.Id <= 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "题材ID无效", "")
|
||||
}
|
||||
current, err := playletthememod.FindOnePlayletTheme(ctx, bson.M{
|
||||
playletthememod.FN.PlayletTheme.Id: req.Id,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setDoc := bson.M{playletthememod.FN.PlayletTheme.UpdatedAt: time.Now()}
|
||||
if req.Name != nil {
|
||||
name := strings.TrimSpace(*req.Name)
|
||||
if name == "" {
|
||||
return srvdef.New(srvdef.ErrParamError, "题材名称不能为空", "")
|
||||
}
|
||||
setDoc[playletthememod.FN.PlayletTheme.Name] = name
|
||||
}
|
||||
if req.Icon != nil {
|
||||
setDoc[playletthememod.FN.PlayletTheme.Icon] = strings.TrimSpace(*req.Icon)
|
||||
}
|
||||
if req.Status != nil {
|
||||
setDoc[playletthememod.FN.PlayletTheme.Status] = *req.Status
|
||||
}
|
||||
if req.Rank != nil {
|
||||
setDoc[playletthememod.FN.PlayletTheme.Rank] = *req.Rank
|
||||
}
|
||||
if req.SeoInfo != nil {
|
||||
setDoc["seoInfo"] = normalizeSeoValue(*req.SeoInfo)
|
||||
}
|
||||
name := current.Name
|
||||
if req.Name != nil {
|
||||
name = strings.TrimSpace(*req.Name)
|
||||
}
|
||||
keywords := current.SeoInfo.Keyworld
|
||||
if req.SeoInfo != nil {
|
||||
keywords = normalizeSeoValue(*req.SeoInfo).Keyworld
|
||||
}
|
||||
exists := func(s string) bool {
|
||||
item, _ := playletthememod.FindOnePlayletTheme(ctx, slugx.Filter(
|
||||
playletthememod.FN.PlayletTheme.Slug, playletthememod.FN.PlayletTheme.Id, s, req.Id))
|
||||
return item != nil
|
||||
}
|
||||
slug, changed, serr := slugx.PrepareOnUpdate(current.Slug, req.Slug,
|
||||
utils.SlugSourceFromKeywordsOrTitle(keywords, name), exists)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
if changed {
|
||||
setDoc[playletthememod.FN.PlayletTheme.Slug] = slug
|
||||
}
|
||||
if len(setDoc) <= 1 {
|
||||
return srvdef.New(srvdef.ErrParamError, "没有可更新字段", "")
|
||||
}
|
||||
if err := playletthememod.UpdateOnePlayletTheme(ctx,
|
||||
bson.M{playletthememod.FN.PlayletTheme.Id: req.Id},
|
||||
bson.M{"$set": setDoc},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
clearAppPlayletHomeCache()
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelPlayletTheme 删除题材,被短剧引用时禁止删除
|
||||
func DelPlayletTheme(ctx context.Context, id int64) *srvdef.SrvErr {
|
||||
if id <= 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "题材ID无效", "")
|
||||
}
|
||||
usedCount, err := playletmod.CountPlaylet(ctx, bson.M{
|
||||
playletmod.FN.Playlet.ThemeIds: bson.M{"$in": []int64{id}},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if usedCount > 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "题材已被短剧引用,无法删除", "")
|
||||
}
|
||||
count, err := playletthememod.DelOnePlayletTheme(ctx, bson.M{playletthememod.FN.PlayletTheme.Id: id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return srvdef.New(srvdef.ErrNoData, "", "")
|
||||
}
|
||||
clearAppPlayletHomeCache()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package srv_questionnaire
|
||||
|
||||
import (
|
||||
"videoweb/pub/models/questionnairemod"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
// ListReq 查询问卷列表请求
|
||||
type ListReq struct {
|
||||
pubdef.Page
|
||||
QuestionType *string `json:"questionType"` // 模糊匹配:问题类型(选填)
|
||||
Status *string `json:"status"` // 精确匹配:处理状态(选填)
|
||||
StartTime *int64 `json:"startTime"` // 创建时间范围-开始,Unix 时间戳(秒,选填)
|
||||
EndTime *int64 `json:"endTime"` // 创建时间范围-结束,Unix 时间戳(秒,选填)
|
||||
}
|
||||
|
||||
// ListResp 查询问卷列表响应
|
||||
type ListResp struct {
|
||||
Total int64 `json:"total"` // 满足条件的总条数
|
||||
List []*questionnairemod.QuestionnaireModel `json:"list"` // 当前页数据
|
||||
}
|
||||
|
||||
// HandleReq 处理问卷请求
|
||||
type HandleReq struct {
|
||||
Id string `json:"id" binding:"required"` // 问卷ID(ObjectID 字符串)
|
||||
Status string `json:"status" binding:"required"` // 目标状态:processed 已处理 / reject 拒绝
|
||||
Remark string `json:"remark"` // 备注(选填)
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package srv_questionnaire
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/questionnairemod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// List 查询问卷列表(分页 + 条件筛选)
|
||||
func List(ctx context.Context, req *ListReq) (*ListResp, *srvdef.SrvErr) {
|
||||
if req.PageNum < 1 {
|
||||
req.PageNum = 1
|
||||
}
|
||||
if req.PageSize < 1 {
|
||||
req.PageSize = 20
|
||||
}
|
||||
|
||||
filter := bson.M{}
|
||||
if req.QuestionType != nil && *req.QuestionType != "" {
|
||||
filter[questionnairemod.FN.Questionnaire.QuestionType] = bson.M{
|
||||
"$regex": *req.QuestionType,
|
||||
}
|
||||
}
|
||||
if req.Status != nil && *req.Status != "" {
|
||||
filter[questionnairemod.FN.Questionnaire.Status] = *req.Status
|
||||
}
|
||||
if req.StartTime != nil || req.EndTime != nil {
|
||||
timeRange := bson.M{}
|
||||
if req.StartTime != nil {
|
||||
timeRange["$gte"] = time.Unix(*req.StartTime, 0)
|
||||
}
|
||||
if req.EndTime != nil {
|
||||
timeRange["$lte"] = time.Unix(*req.EndTime, 0)
|
||||
}
|
||||
filter[questionnairemod.FN.Questionnaire.CreatedAt] = timeRange
|
||||
}
|
||||
|
||||
total, serr := questionnairemod.Count(ctx, filter)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
if total == 0 {
|
||||
return &ListResp{Total: 0, List: make([]*questionnairemod.QuestionnaireModel, 0)}, nil
|
||||
}
|
||||
|
||||
opt := options.Find().
|
||||
SetSort(bson.D{{Key: questionnairemod.FN.Questionnaire.CreatedAt, Value: -1}}).
|
||||
SetSkip((req.PageNum - 1) * req.PageSize).
|
||||
SetLimit(req.PageSize)
|
||||
|
||||
list, serr := questionnairemod.FindMany(ctx, filter, opt)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
|
||||
return &ListResp{Total: total, List: list}, nil
|
||||
}
|
||||
|
||||
// Handle 处理问卷(仅允许"待处理"状态的问卷被更新)
|
||||
func Handle(ctx context.Context, req *HandleReq) *srvdef.SrvErr {
|
||||
objId, err := primitive.ObjectIDFromHex(req.Id)
|
||||
if err != nil {
|
||||
return srvdef.New(srvdef.ErrParamError, "问卷ID格式错误", "")
|
||||
}
|
||||
|
||||
filter := bson.M{
|
||||
questionnairemod.FN.Questionnaire.Id: objId,
|
||||
questionnairemod.FN.Questionnaire.Status: questionnairemod.StatusPending,
|
||||
}
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
questionnairemod.FN.Questionnaire.Status: req.Status,
|
||||
questionnairemod.FN.Questionnaire.Remark: req.Remark,
|
||||
questionnairemod.FN.Questionnaire.UpdatedAt: time.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
matched, serr := questionnairemod.UpdateOne(ctx, filter, update)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
if matched == 0 {
|
||||
// 区分"不存在"与"状态不是待处理"
|
||||
_, serr2 := questionnairemod.FindOne(ctx, bson.M{questionnairemod.FN.Questionnaire.Id: objId})
|
||||
if serr2 != nil {
|
||||
return srvdef.New(srvdef.ErrNoData, "问卷不存在", "")
|
||||
}
|
||||
return srvdef.New(srvdef.Err, "该问卷已处理,不可重复操作", "该问卷已处理,不可重复操作")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package srv_report
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/useragent"
|
||||
"videoweb/pub/dataReport"
|
||||
"videoweb/pub/models/eventmod"
|
||||
"videoweb/pub/models/videocategorymod"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
// ReportVideoStatusChangeEvent 视频状态变更事件上报
|
||||
func ReportVideoStatusChangeEvent(media *videoinformationmod.VideoInformation, flag, userId int64) {
|
||||
if media == nil || media.Id == 0 || flag == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var lsjId string
|
||||
|
||||
var videoTypeID = "default"
|
||||
var videoTypeName = "默认分类"
|
||||
if media.CategoryId > 0 {
|
||||
// 复用 reportser 中的分类解析逻辑
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
|
||||
defer cancel()
|
||||
category, _ := videocategorymod.FindOneVideoCategory(ctx, bson.M{
|
||||
videocategorymod.FN.VideoCategory.Id: media.CategoryId,
|
||||
})
|
||||
if category != nil {
|
||||
videoTypeID = fmt.Sprintf("%v", category.Id)
|
||||
videoTypeName = category.Name
|
||||
}
|
||||
}
|
||||
uaInfo := useragent.UA{SysType: pubdef.ST_PC}
|
||||
event := dataReport.NewCommonField(dataReport.EventTypeVideoStatusChange, "", uaInfo, strconv.FormatInt(userId, 10), userId)
|
||||
event.Payload = dataReport.JsonRawMessage(&dataReport.VideoStatusChangeEvent{
|
||||
MediaId: lsjId,
|
||||
VideoID: fmt.Sprintf("%v", media.Id),
|
||||
VideoTitle: media.Title,
|
||||
VideoTypeID: videoTypeID,
|
||||
VideoTypeName: videoTypeName,
|
||||
Flag: dataReport.JsonNumber(flag),
|
||||
})
|
||||
b, _ := json.Marshal(event)
|
||||
if err := eventmod.InsertOneEvent(context.Background(), &eventmod.Event{
|
||||
EventId: event.EventID,
|
||||
EventType: event.Event,
|
||||
Data: string(b),
|
||||
Status: eventmod.Default,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}); err != nil {
|
||||
log.Error("ReportVideoStatusChangeEvent fail", log.Any("mediaId", media.Id), log.E(err))
|
||||
}
|
||||
}
|
||||
|
||||
// VideoStatusFlag 将媒体状态映射为上报flag
|
||||
func VideoStatusFlag(status pubdef.InformationStatus) int64 {
|
||||
switch status {
|
||||
case pubdef.InformationStatusVerifyOK:
|
||||
return 1 // 上架
|
||||
case pubdef.InformationStatusVerifyCancel:
|
||||
return 2 // 未通过
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package srv_richtextcontent
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/pub/models/richtextcontentmod"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
type GetRichTextContentListReq struct {
|
||||
Title *string `json:"title" bson:"title"` // 标题
|
||||
Status *bool `json:"status" bson:"status"` // 是否启用
|
||||
pubdef.Page
|
||||
}
|
||||
|
||||
type GetRichTextContentResp struct {
|
||||
Total int64 `json:"total" bson:"total"`
|
||||
List richtextcontentmod.RichTextContentModelList `json:"list" bson:"list"`
|
||||
}
|
||||
|
||||
type DelRichTextContentReq struct {
|
||||
Id int64 `bson:"_id,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateRichTextContentData struct {
|
||||
Id int64 `bson:"_id,omitempty" json:"_id,omitempty"`
|
||||
Title *string `json:"title" bson:"title,omitempty"` // 标题
|
||||
Content *string `json:"content" bson:"content,omitempty"` // 内容
|
||||
Location *[]string `json:"location" bson:"location,omitempty"` // 位置
|
||||
Sort *int64 `json:"sort" bson:"sort,omitempty"` // 排序值
|
||||
Status *bool `json:"status" bson:"status,omitempty"` // 是否启用
|
||||
Desc *string `json:"desc" bson:"desc,omitempty"` // 标题(app)
|
||||
SeoInfo *pubdef.SeoValue `json:"seoInfo" bson:"seoInfo,omitempty"` // Seo相关信息
|
||||
UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"`
|
||||
}
|
||||
type UpdateRichTextContentReq struct {
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
Param UpdateRichTextContentData `json:"param" bson:"param"` // appId,唯一id
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package srv_richtextcontent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/richtextcontentmod"
|
||||
"videoweb/pub/mongos"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func GetRichTextContentList(ctx context.Context, req *GetRichTextContentListReq) (*GetRichTextContentResp, *srvdef.SrvErr) {
|
||||
filter := bson.M{}
|
||||
if req.Title != nil {
|
||||
filter[richtextcontentmod.FN.RichTextContentModel.Title] = bson.M{"$regex": *req.Title, "$options": "i"}
|
||||
}
|
||||
if req.Status != nil {
|
||||
filter[richtextcontentmod.FN.RichTextContentModel.Status] = *req.Status
|
||||
}
|
||||
total, err := richtextcontentmod.CountRichTextContentModelModels(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := &GetRichTextContentResp{}
|
||||
if total == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
opts := &options.FindOptions{}
|
||||
opts.SetSort(bson.M{richtextcontentmod.FN.RichTextContentModel.UpdatedAt: -1})
|
||||
opts.SetLimit(req.PageSize)
|
||||
opts.SetSkip((req.PageNum - 1) * req.PageSize)
|
||||
list, err := richtextcontentmod.FindManyRichTextContentModelModel(ctx, filter, opts)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
resp.Total = total
|
||||
resp.List = list
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func AddRichTextContent(ctx context.Context, ac *richtextcontentmod.RichTextContentModel) *srvdef.SrvErr {
|
||||
ac.Id = mongos.Lid.HtmlText.Get()
|
||||
now := time.Now()
|
||||
ac.CreatedAt = now
|
||||
ac.UpdatedAt = now
|
||||
|
||||
return richtextcontentmod.InsertOneRichTextContentModelModel(ctx, ac)
|
||||
}
|
||||
|
||||
func UpdateRichTextContent(ctx context.Context, id int64, param *UpdateRichTextContentData) *srvdef.SrvErr {
|
||||
now := time.Now()
|
||||
param.UpdatedAt = now
|
||||
|
||||
adFilter := bson.M{richtextcontentmod.FN.RichTextContentModel.Id: id}
|
||||
updateDoc := bson.M{"$set": param}
|
||||
serr := richtextcontentmod.UpdateOneRichTextContentModelModel(ctx, adFilter, updateDoc)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DelRichTextContent(ctx context.Context, req *DelRichTextContentReq) *srvdef.SrvErr {
|
||||
_, err := richtextcontentmod.DelOneRichTextContentModelModel(ctx, bson.M{
|
||||
richtextcontentmod.FN.RichTextContentModel.Id: req.Id,
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package srv_search
|
||||
|
||||
import "strings"
|
||||
|
||||
func contentTagNamesString(tagIds []int64, tagMap map[int64]string) string {
|
||||
names := make([]string, 0, len(tagIds))
|
||||
for _, tagId := range tagIds {
|
||||
if name := tagMap[tagId]; name != "" {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
return strings.Join(names, ",")
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package srv_search
|
||||
|
||||
import (
|
||||
"videoweb/pub/mongos"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
// Param 引擎参数
|
||||
type Param struct {
|
||||
//数据库
|
||||
Coll *mongos.Collection
|
||||
//过滤器
|
||||
CollFilter bson.M
|
||||
//跟新字段
|
||||
CollFields []string
|
||||
//es索引
|
||||
EsIndex string
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package srv_search
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/esmanager"
|
||||
"videoweb/pub/models/esmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
var syncIsRuining = false
|
||||
|
||||
func StartES() {
|
||||
log.Info("srv_search:Start...")
|
||||
defer log.Info("srv_search:Start!!!")
|
||||
startSyncEs()
|
||||
_, _ = skdCron.AddFunc("0 0/17 * * * ?", func() {
|
||||
startSyncEs()
|
||||
})
|
||||
skdCron.Start()
|
||||
}
|
||||
|
||||
func startSyncEs() {
|
||||
ctx := context.Background()
|
||||
updateEsData(ctx, esmod.GetVideoIndex(), getVideoEsData)
|
||||
updateEsData(ctx, esmod.GetPlayletIndex(), getPlayletEsData)
|
||||
updateEsData(ctx, esmod.GetActressIndex(), getActressEsData)
|
||||
}
|
||||
|
||||
func StopES() {
|
||||
log.Info("srv_search:Stop...")
|
||||
defer log.Info("srv_search:Stop!!!")
|
||||
}
|
||||
|
||||
func updateEsData(ctx context.Context, esIndex string, fn func(ctx context.Context, startTime, endTime *time.Time, esIndex string) (esmanager.M, *srvdef.SrvErr)) {
|
||||
log.Info("srv_search:do...", log.Any("ESIndex", esIndex))
|
||||
defer log.Info("srv_search:do end", log.Any("EsIndex", esIndex))
|
||||
if fn == nil {
|
||||
return
|
||||
}
|
||||
esRecord, serr := esmod.GetRecordTimeByESIndex(ctx, esIndex)
|
||||
if serr != nil {
|
||||
log.Error(fmt.Sprintf("srv_search:startEngine GetRecordByESIndex failed! Index:%v,err:%+v:", esIndex, serr))
|
||||
return
|
||||
}
|
||||
isNew := false
|
||||
if esRecord == nil {
|
||||
isNew = true
|
||||
esRecord = &esmod.ESRecord{
|
||||
Id: primitive.NewObjectID(),
|
||||
EsIndex: esIndex,
|
||||
RecordTime: time.Time{},
|
||||
Timestamp: esmod.ESTimestamp{},
|
||||
UpdatedAt: time.Time{},
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
var now = time.Now()
|
||||
var esData esmanager.M
|
||||
// 跨天凌晨5点重置所有数据
|
||||
if isNew || ((now.Hour() >= 5 && esRecord.UpdatedAt.Local().Hour() < 5) || esRecord.UpdatedAt.Local().Day() < now.Day()) {
|
||||
log.Info("srv_search:updateEsData 全量同步", log.Any("index", esIndex), log.Any("isNew", isNew))
|
||||
esData, serr = fn(ctx, nil, nil, esIndex)
|
||||
if serr != nil {
|
||||
log.Warn("updateEsData fn return err", log.Any("index", esIndex), log.E(serr))
|
||||
return
|
||||
}
|
||||
log.Info("srv_search:updateEsData 全量数据拉取完成", log.Any("index", esIndex), log.Any("count", len(esData)))
|
||||
if len(esData) == 0 {
|
||||
if err := esmod.SetOneEsRecordTime(nil, esIndex, now); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
isUpdate, err := commit(esData, esIndex, time.Now())
|
||||
if err != nil {
|
||||
log.Error("Elastic sync video data RunSync commit err! ", log.E(err))
|
||||
return
|
||||
}
|
||||
log.Info("srv_search:updateEsData 全量写入ES完成", log.Any("index", esIndex), log.Any("isUpdate", isUpdate))
|
||||
if isUpdate {
|
||||
if err := esmod.SetOneEsRecordTime(nil, esIndex, now); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
timestamp := esRecord.RecordTime
|
||||
var recentMinute = utils.RecentMinute(now, esmod.FiveMinuteScale)
|
||||
log.Info("srv_search:updateEsData 增量同步", log.Any("index", esIndex),
|
||||
log.Any("from", timestamp.Format("2006-01-02 15:04:05")),
|
||||
log.Any("to", recentMinute.Format("2006-01-02 15:04:05")))
|
||||
totalIncr := 0
|
||||
for recentMinute.After(timestamp) {
|
||||
var end = timestamp.Add(esmod.FiveMinuteScale * time.Minute)
|
||||
if end.After(recentMinute) {
|
||||
end = recentMinute
|
||||
}
|
||||
esData, serr = fn(ctx, ×tamp, &end, esIndex)
|
||||
if serr != nil {
|
||||
log.Warn("updateEsData fn return err", log.Any("index", esIndex), log.E(serr))
|
||||
return
|
||||
}
|
||||
if esData != nil && len(esData) > 0 {
|
||||
totalIncr += len(esData)
|
||||
log.Info("srv_search:updateEsData 增量批次",
|
||||
log.Any("index", esIndex),
|
||||
log.Any("window_start", timestamp.Format("2006-01-02 15:04:05")),
|
||||
log.Any("window_end", end.Format("2006-01-02 15:04:05")),
|
||||
log.Any("count", len(esData)))
|
||||
isUpdate, err := commit(esData, esIndex, time.Now())
|
||||
if err != nil {
|
||||
log.Error("Elastic sync video data RunSync commit err! ", log.E(err))
|
||||
return
|
||||
}
|
||||
if isUpdate {
|
||||
if err := esmod.SetOneEsRecordTime(nil, esIndex, end); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := esmod.SetOneEsRecordTime(nil, esIndex, end); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
timestamp = end
|
||||
}
|
||||
log.Info("srv_search:updateEsData 增量同步完成", log.Any("index", esIndex), log.Any("totalCount", totalIncr))
|
||||
}
|
||||
}
|
||||
|
||||
func commit(data esmanager.M, esIndex string, syncTime time.Time) (bool, error) {
|
||||
bulkData := handleBulkCountLimit(data)
|
||||
isHaveSuccess := false
|
||||
failedBatches := make([]int, 0)
|
||||
log.Info("srv_search:commit 开始写入ES", log.Any("index", esIndex),
|
||||
log.Any("totalDocs", len(data)), log.Any("batches", len(bulkData)))
|
||||
if bulkData != nil && len(bulkData) > 0 {
|
||||
for i, doc := range bulkData {
|
||||
if len(doc) == 0 {
|
||||
continue
|
||||
}
|
||||
if err := esmanager.Instance.Bulk(esIndex, doc); err != nil {
|
||||
log.Warn(fmt.Sprintf("Elastic sync commit video data SyncDataToES failed!, err:%+v\n", err),
|
||||
log.Any("index", esIndex), log.Any("batch", i), log.Any("batchSize", len(doc)))
|
||||
failedBatches = append(failedBatches, i)
|
||||
} else {
|
||||
isHaveSuccess = true
|
||||
log.Info("srv_search:commit 批次写入成功", log.Any("index", esIndex),
|
||||
log.Any("batch", i), log.Any("batchSize", len(doc)))
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(failedBatches) > 0 {
|
||||
return isHaveSuccess, fmt.Errorf("elastic sync commit failed batches: %v", failedBatches)
|
||||
}
|
||||
if isHaveSuccess {
|
||||
if err := esmanager.Instance.Refresh(esIndex); err != nil {
|
||||
return isHaveSuccess, err
|
||||
}
|
||||
return isHaveSuccess, nil
|
||||
} else {
|
||||
return isHaveSuccess, errors.New("Elastic sync commit video data UpsertOneTrans failed!")
|
||||
}
|
||||
}
|
||||
|
||||
func handleBulkCountLimit(data esmanager.M) []esmanager.M {
|
||||
if data == nil || len(data) == 0 {
|
||||
return make([]esmanager.M, 0)
|
||||
}
|
||||
rsl := make([]esmanager.M, 0)
|
||||
rsl = append(rsl, make(esmanager.M, batchSize))
|
||||
index := 0
|
||||
for k, v := range data {
|
||||
rsl[index][k] = v
|
||||
if len(rsl[index]) >= batchSize {
|
||||
index++
|
||||
rsl = append(rsl, make(esmanager.M, batchSize))
|
||||
}
|
||||
}
|
||||
return rsl
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
package srv_search
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/esmanager"
|
||||
"videoweb/pub/models/contenttagmod"
|
||||
"videoweb/pub/models/esmod"
|
||||
"videoweb/pub/models/playletcategorymod"
|
||||
"videoweb/pub/models/playletmod"
|
||||
"videoweb/pub/models/playletthememod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const playletESPageSize int64 = 1000
|
||||
|
||||
var playletESProjection = bson.D{
|
||||
{Key: playletmod.FN.Playlet.Id, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.Status, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.IsFeatured, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.IsRecommend, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.ShowOnBanner, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.Banner, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.OnlineAt, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.CategoryIds, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.ThemeIds, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.Title, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.Desc, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.Anchor, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.Cover, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.CompressedCover, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.TagIds, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.TotalEpisode, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.UpdateStatus, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.CountBrowse, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.VirtualCountBrowse, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.HeatValue, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.VirtualHeatValue, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.CountCollect, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.CountLike, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.VirtualCountLike, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.SeoInfo.Title, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.SeoInfo.Description, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.SeoInfo.Keyworld, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.CreatedAt, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.UpdatedAt, Value: 1},
|
||||
}
|
||||
|
||||
// getPlayletEsData 查询短剧增量数据并转换为ES批量写入数据
|
||||
func getPlayletEsData(ctx context.Context, minTime, endTime *time.Time, esIndex string) (esmanager.M, *srvdef.SrvErr) {
|
||||
list, err1 := getPlayletData(ctx, minTime, endTime)
|
||||
if err1 != nil {
|
||||
return nil, err1
|
||||
}
|
||||
return playletToEsData(ctx, list)
|
||||
}
|
||||
|
||||
// SyncPlayletsToESByLatestStatus 根据短剧最新上下架状态同步ES搜索数据
|
||||
func SyncPlayletsToESByLatestStatus(ctx context.Context, ids []int64) *srvdef.SrvErr {
|
||||
ids = dedupePlayletESIds(ids)
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
list, err1 := playletmod.FindManyPlaylet(ctx, bson.M{
|
||||
playletmod.FN.Playlet.Id: bson.M{"$in": ids},
|
||||
}, options.Find().SetProjection(bson.D{
|
||||
{Key: playletmod.FN.Playlet.Id, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.Status, Value: 1},
|
||||
}))
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
foundIds := make(map[int64]struct{}, len(list))
|
||||
upIds := make([]int64, 0, len(list))
|
||||
downIds := make([]int64, 0, len(ids))
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
foundIds[item.Id] = struct{}{}
|
||||
if item.Status == playletmod.StatusUp {
|
||||
upIds = append(upIds, item.Id)
|
||||
} else {
|
||||
downIds = append(downIds, item.Id)
|
||||
}
|
||||
}
|
||||
for _, id := range ids {
|
||||
if _, ok := foundIds[id]; !ok {
|
||||
downIds = append(downIds, id)
|
||||
}
|
||||
}
|
||||
if err1 := SyncPlayletStatusToES(ctx, upIds, playletmod.StatusUp); err1 != nil {
|
||||
return err1
|
||||
}
|
||||
if err1 := SyncPlayletStatusToES(ctx, downIds, playletmod.StatusDown); err1 != nil {
|
||||
return err1
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SyncPlayletStatusToES 根据短剧上下架状态同步ES搜索数据
|
||||
func SyncPlayletStatusToES(ctx context.Context, ids []int64, status playletmod.Status) *srvdef.SrvErr {
|
||||
ids = dedupePlayletESIds(ids)
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
if esmanager.Instance == nil {
|
||||
log.Error("短剧ES同步失败,ES未初始化", log.Any("短剧ID", ids), log.Any("状态", status))
|
||||
return srvdef.New(srvdef.ErrElsOperError, "", "")
|
||||
}
|
||||
switch status {
|
||||
case playletmod.StatusUp:
|
||||
return upsertPlayletsToES(ctx, ids)
|
||||
case playletmod.StatusDown:
|
||||
return deletePlayletsFromES(ids)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func getPlayletData(ctx context.Context, minTime, endTime *time.Time) (playletmod.PlayletList, *srvdef.SrvErr) {
|
||||
pageNum := int64(1)
|
||||
result := make(playletmod.PlayletList, 0)
|
||||
filter := bson.M{
|
||||
playletmod.FN.Playlet.Status: playletmod.StatusUp,
|
||||
}
|
||||
if minTime != nil && endTime != nil {
|
||||
filter[playletmod.FN.Playlet.UpdatedAt] = bson.M{
|
||||
"$gte": minTime,
|
||||
"$lte": endTime,
|
||||
}
|
||||
}
|
||||
for {
|
||||
list, err1 := playletmod.FindManyPlaylet(ctx, filter, options.Find().
|
||||
SetProjection(playletESProjection).
|
||||
SetSort(bson.D{{Key: playletmod.FN.Playlet.UpdatedAt, Value: -1}}).
|
||||
SetSkip((pageNum-1)*playletESPageSize).
|
||||
SetLimit(playletESPageSize))
|
||||
if err1 != nil {
|
||||
return nil, err1
|
||||
}
|
||||
result = append(result, list...)
|
||||
if int64(len(list)) < playletESPageSize {
|
||||
break
|
||||
}
|
||||
pageNum++
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func playletToEsData(ctx context.Context, list playletmod.PlayletList) (esmanager.M, *srvdef.SrvErr) {
|
||||
categoryNameMap, err1 := buildPlayletESCategoryNameMap(ctx, list)
|
||||
if err1 != nil {
|
||||
return nil, err1
|
||||
}
|
||||
themeNameMap, err1 := buildPlayletESThemeNameMap(ctx, list)
|
||||
if err1 != nil {
|
||||
return nil, err1
|
||||
}
|
||||
tagNameMap, err1 := buildPlayletESTagNameMap(ctx, list)
|
||||
if err1 != nil {
|
||||
return nil, err1
|
||||
}
|
||||
result := make(esmanager.M, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
result[strconv.FormatInt(item.Id, 10)] = playletToESDoc(item, categoryNameMap, themeNameMap, tagNameMap)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// upsertPlayletsToES 批量写入已上架短剧ES文档
|
||||
func upsertPlayletsToES(ctx context.Context, ids []int64) *srvdef.SrvErr {
|
||||
list, err1 := playletmod.FindManyPlaylet(ctx, bson.M{
|
||||
playletmod.FN.Playlet.Id: bson.M{"$in": ids},
|
||||
playletmod.FN.Playlet.Status: playletmod.StatusUp,
|
||||
}, options.Find().SetProjection(playletESProjection))
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return nil
|
||||
}
|
||||
esData, err1 := playletToEsData(ctx, list)
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
if len(esData) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err1 = AddMultiDataToES(esmod.GetPlayletIndex(), esData); err1 != nil {
|
||||
log.Error("批量写入短剧ES失败", log.Any("短剧ID", ids), log.E(err1))
|
||||
return err1
|
||||
}
|
||||
log.Info("批量写入短剧ES成功", log.Any("数量", len(esData)), log.Any("短剧ID", ids))
|
||||
return nil
|
||||
}
|
||||
|
||||
// deletePlayletsFromES 批量删除短剧ES文档
|
||||
func deletePlayletsFromES(ids []int64) *srvdef.SrvErr {
|
||||
strIds := make([]string, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if id <= 0 {
|
||||
continue
|
||||
}
|
||||
strIds = append(strIds, strconv.FormatInt(id, 10))
|
||||
}
|
||||
if len(strIds) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err1 := batchDelFromES(esmod.GetPlayletIndex(), strIds); err1 != nil {
|
||||
log.Error("批量删除短剧ES失败", log.Any("短剧ID", ids), log.E(err1))
|
||||
return err1
|
||||
}
|
||||
log.Info("批量删除短剧ES成功", log.Any("数量", len(strIds)), log.Any("短剧ID", ids))
|
||||
return nil
|
||||
}
|
||||
|
||||
// playletToESDoc 转换短剧Mongo数据为ES文档
|
||||
func playletToESDoc(item *playletmod.Playlet, categoryNameMap map[int64]string, themeNameMap map[int64]string, tagNameMap map[int64]string) *esmod.ESPlaylet {
|
||||
return &esmod.ESPlaylet{
|
||||
Id: item.Id,
|
||||
Status: item.Status,
|
||||
IsFeatured: item.IsFeatured,
|
||||
IsRecommend: item.IsRecommend,
|
||||
ShowOnBanner: item.ShowOnBanner,
|
||||
Banner: item.Banner,
|
||||
OnlineAt: item.OnlineAt,
|
||||
CategoryIds: item.CategoryIds,
|
||||
CategoryNames: playletESNamesByIds(item.CategoryIds, categoryNameMap),
|
||||
ThemeIds: item.ThemeIds,
|
||||
ThemeNames: playletESNamesByIds(item.ThemeIds, themeNameMap),
|
||||
Title: item.Title,
|
||||
Desc: item.Desc,
|
||||
Anchor: item.Anchor,
|
||||
Cover: item.DisplayCover(),
|
||||
TagIds: item.TagIds,
|
||||
TagNames: playletESNamesByIds(item.TagIds, tagNameMap),
|
||||
TotalEpisode: item.TotalEpisode,
|
||||
UpdateStatus: item.UpdateStatus,
|
||||
CountBrowse: item.DisplayCountBrowse(),
|
||||
HeatValue: item.DisplayHeatValue(),
|
||||
CountCollect: item.CountCollect,
|
||||
CountLike: item.CountLike + item.VirtualCountLike,
|
||||
SeoInfo: item.SeoInfo,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// buildPlayletESCategoryNameMap 批量查询短剧分类名称
|
||||
func buildPlayletESCategoryNameMap(ctx context.Context, list playletmod.PlayletList) (map[int64]string, *srvdef.SrvErr) {
|
||||
ids := collectPlayletESIds(list, func(item *playletmod.Playlet) []int64 {
|
||||
return item.CategoryIds
|
||||
})
|
||||
result := make(map[int64]string, len(ids))
|
||||
if len(ids) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
categories, err1 := playletcategorymod.FindManyPlayletCategory(ctx, bson.M{
|
||||
playletcategorymod.FN.PlayletCategory.Id: bson.M{"$in": ids},
|
||||
}, options.Find().SetProjection(bson.D{
|
||||
{Key: playletcategorymod.FN.PlayletCategory.Id, Value: 1},
|
||||
{Key: playletcategorymod.FN.PlayletCategory.Name, Value: 1},
|
||||
}))
|
||||
if err1 != nil {
|
||||
return nil, err1
|
||||
}
|
||||
for _, category := range categories {
|
||||
if category != nil {
|
||||
result[category.Id] = category.Name
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// buildPlayletESThemeNameMap 批量查询短剧题材名称
|
||||
func buildPlayletESThemeNameMap(ctx context.Context, list playletmod.PlayletList) (map[int64]string, *srvdef.SrvErr) {
|
||||
ids := collectPlayletESIds(list, func(item *playletmod.Playlet) []int64 {
|
||||
return item.ThemeIds
|
||||
})
|
||||
result := make(map[int64]string, len(ids))
|
||||
if len(ids) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
themes, err1 := playletthememod.FindManyPlayletTheme(ctx, bson.M{
|
||||
playletthememod.FN.PlayletTheme.Id: bson.M{"$in": ids},
|
||||
}, options.Find().SetProjection(bson.D{
|
||||
{Key: playletthememod.FN.PlayletTheme.Id, Value: 1},
|
||||
{Key: playletthememod.FN.PlayletTheme.Name, Value: 1},
|
||||
}))
|
||||
if err1 != nil {
|
||||
return nil, err1
|
||||
}
|
||||
for _, theme := range themes {
|
||||
if theme != nil {
|
||||
result[theme.Id] = theme.Name
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// buildPlayletESTagNameMap 批量查询短剧标签名称
|
||||
func buildPlayletESTagNameMap(ctx context.Context, list playletmod.PlayletList) (map[int64]string, *srvdef.SrvErr) {
|
||||
ids := collectPlayletESIds(list, func(item *playletmod.Playlet) []int64 {
|
||||
return item.TagIds
|
||||
})
|
||||
result := make(map[int64]string, len(ids))
|
||||
if len(ids) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
tags, err1 := contenttagmod.GetMapTags(ctx, ids)
|
||||
if err1 != nil {
|
||||
return nil, err1
|
||||
}
|
||||
for _, tag := range tags {
|
||||
if tag != nil {
|
||||
result[tag.Id] = tag.Name
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// collectPlayletESIds 汇总并去重短剧关联ID
|
||||
func collectPlayletESIds(list playletmod.PlayletList, pick func(item *playletmod.Playlet) []int64) []int64 {
|
||||
result := make([]int64, 0)
|
||||
seen := make(map[int64]struct{})
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
for _, id := range pick(item) {
|
||||
if id <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
result = append(result, id)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func dedupePlayletESIds(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
|
||||
}
|
||||
|
||||
// playletESNamesByIds 按ID顺序返回ES文档名称集合
|
||||
func playletESNamesByIds(ids []int64, nameMap map[int64]string) []string {
|
||||
result := make([]string, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if name := nameMap[id]; name != "" {
|
||||
result = append(result, name)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package srv_search
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/esmanager"
|
||||
"videoweb/pub/models/actress"
|
||||
"videoweb/pub/models/esmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func getActressEsData(ctx context.Context, minTime, endTime *time.Time, esIndex string) (esmanager.M, *srvdef.SrvErr) {
|
||||
data, serr := getActressData(ctx, minTime, endTime)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
return actressToEsData(data), nil
|
||||
}
|
||||
func getActressData(ctx context.Context, minTime, endTime *time.Time) ([]*actress.Actress, *srvdef.SrvErr) {
|
||||
pageNumber := 1
|
||||
pageSize := 1000
|
||||
rsl := make([]*actress.Actress, 0)
|
||||
filter := bson.M{}
|
||||
if minTime != nil && endTime != nil {
|
||||
filter[actress.FN.Actress.UpdatedAt] = bson.M{
|
||||
"$gte": minTime,
|
||||
"$lte": endTime,
|
||||
}
|
||||
}
|
||||
for {
|
||||
opts := &options.FindOptions{}
|
||||
opts.SetSort(bson.M{actress.FN.Actress.UpdatedAt: -1})
|
||||
opts.SetSkip(int64(pageSize * (pageNumber - 1)))
|
||||
opts.SetLimit(int64(pageSize))
|
||||
data, serr := actress.FindManyActresss(ctx, filter, opts)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
rsl = append(rsl, data...)
|
||||
if len(data) < pageSize {
|
||||
break
|
||||
}
|
||||
pageNumber++
|
||||
}
|
||||
return rsl, nil
|
||||
}
|
||||
func actressToEsData(data []*actress.Actress) esmanager.M {
|
||||
rsl := make(map[string]interface{}, len(data))
|
||||
for _, v := range data {
|
||||
esInfo := esmod.ESActress{
|
||||
Id: v.Id,
|
||||
Alias: v.Alias,
|
||||
Avatar: v.Avatar,
|
||||
Meas: v.Meas,
|
||||
Desc: v.Desc,
|
||||
Office: v.Office,
|
||||
DebutDate: v.DebutDate,
|
||||
BornDate: v.BornDate,
|
||||
Region: v.Region,
|
||||
Score: v.Score,
|
||||
Works: v.Works,
|
||||
HotValue: v.HotValue,
|
||||
Real: v.Real,
|
||||
Status: v.Status,
|
||||
CreatedAt: v.CreatedAt,
|
||||
UpdatedAt: v.UpdatedAt,
|
||||
TranslationCategoryId: v.TranslationCategoryId,
|
||||
Rank: v.Rank,
|
||||
}
|
||||
rsl[strconv.FormatInt(v.Id, 10)] = esInfo
|
||||
}
|
||||
return rsl
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package srv_search
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/esmanager"
|
||||
"videoweb/pub/models/esmod"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func getVideoEsData(ctx context.Context, minTime, endTime *time.Time, esIndex string) (esmanager.M, *srvdef.SrvErr) {
|
||||
data, serr := getVideoData(ctx, minTime, endTime)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
return videoToEsData(data), nil
|
||||
}
|
||||
func getVideoData(ctx context.Context, minTime, endTime *time.Time) ([]*videoinformationmod.VideoInformation, *srvdef.SrvErr) {
|
||||
pageNumber := 1
|
||||
pageSize := 1000
|
||||
rsl := make([]*videoinformationmod.VideoInformation, 0)
|
||||
filter := bson.M{}
|
||||
if minTime != nil && endTime != nil {
|
||||
filter[videoinformationmod.FN.VideoInformation.UpdatedAt] = bson.M{
|
||||
"$gte": minTime,
|
||||
"$lte": endTime,
|
||||
}
|
||||
}
|
||||
for {
|
||||
opts := &options.FindOptions{}
|
||||
opts.SetSort(bson.M{videoinformationmod.FN.VideoInformation.UpdatedAt: -1})
|
||||
opts.SetSkip(int64(pageSize * (pageNumber - 1)))
|
||||
opts.SetLimit(int64(pageSize))
|
||||
data, serr := videoinformationmod.FindManyVideoInformations(ctx, filter, opts)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
rsl = append(rsl, data...)
|
||||
if len(data) < pageSize {
|
||||
break
|
||||
}
|
||||
pageNumber++
|
||||
}
|
||||
return rsl, nil
|
||||
}
|
||||
func videoToEsData(data []*videoinformationmod.VideoInformation) esmanager.M {
|
||||
rsl := make(map[string]interface{}, len(data))
|
||||
for _, v := range data {
|
||||
esInfo := esmod.ESVideo{
|
||||
Id: v.Id,
|
||||
Title: v.Title,
|
||||
MediaType: v.MediaType,
|
||||
AddedTime: v.AddedTime,
|
||||
CreatorName: v.CreatorName,
|
||||
Keyworlds: "",
|
||||
Desc: v.Desc,
|
||||
Remark: v.Remark,
|
||||
Status: v.Status,
|
||||
IsHot: v.IsHot,
|
||||
Sort: v.Sort,
|
||||
WatchTimes: v.Real.WatchTimes,
|
||||
ShowTimes: v.Real.ShowTimes,
|
||||
Comments: v.Real.Comments,
|
||||
UpdatedAt: v.UpdatedAt,
|
||||
CreatedAt: v.CreatedAt,
|
||||
Keyworld: v.Keyworld,
|
||||
CreatorId: v.CreatorId,
|
||||
FirstTag: v.FirstTag,
|
||||
SecondTags: v.SecondTags,
|
||||
CoverImg: v.CoverImg,
|
||||
Nodes: v.Nodes,
|
||||
Video: v.Video,
|
||||
Tags: v.Tags,
|
||||
Real: v.Real,
|
||||
VideoCount: v.VideoCount,
|
||||
IssuedBrand: v.IssuedBrand,
|
||||
Bango: v.Bango,
|
||||
IssuedAt: v.IssuedAt,
|
||||
}
|
||||
if v.Keyworld != nil && len(v.Keyworld) > 0 {
|
||||
esInfo.Keyworlds = strings.Join(v.Keyworld, " ")
|
||||
}
|
||||
rsl[strconv.FormatInt(v.Id, 10)] = esInfo
|
||||
}
|
||||
return rsl
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package srv_search
|
||||
|
||||
import (
|
||||
"github.com/robfig/cron/v3"
|
||||
"videoweb/pub/common/log"
|
||||
)
|
||||
|
||||
var (
|
||||
skdCron *cron.Cron
|
||||
batchSize int = 100
|
||||
opMaxDiff int32 = 10
|
||||
)
|
||||
|
||||
func Start() {
|
||||
skdCron = cron.New(cron.WithSeconds())
|
||||
log.Info("srv_search:Start...")
|
||||
defer log.Info("srv_search:Start!!!")
|
||||
go func() {
|
||||
StartES()
|
||||
}()
|
||||
}
|
||||
|
||||
func Stop() {
|
||||
log.Info("srv_search:Stop...")
|
||||
defer log.Info("srv_search:Stop!!!")
|
||||
StopES()
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
package srv_search
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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"
|
||||
"strconv"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/esmanager"
|
||||
"videoweb/pub/models/esmod"
|
||||
"videoweb/pub/mongos"
|
||||
)
|
||||
|
||||
// resetAllDocsToES 重设所有文档到数据库
|
||||
func resetAllDocsToES(ctx context.Context, param *Param) *srvdef.SrvErr {
|
||||
log.Info("srv_search:resetAllDocsToES...", log.Any("ESIndex", param.EsIndex))
|
||||
defer log.Info("srv_search:startActress!!!")
|
||||
//删除所有记录
|
||||
delAllFromES(param.EsIndex)
|
||||
|
||||
var pageNum int64 = 1
|
||||
var pageSize int64 = 200
|
||||
updSize := 0
|
||||
for {
|
||||
findOpt := constructFindOption(pageNum, pageSize)
|
||||
docs, serr := findManyFromDb(ctx, param, findOpt)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
if len(docs) == 0 {
|
||||
break
|
||||
}
|
||||
updSize = updSize + len(docs)
|
||||
updateESData, serr := sliceMapToJsonMap(docs, param)
|
||||
log.Info("srv_search:resetAllDocsToES", log.Any("updateESData", updateESData))
|
||||
if serr = batchAddToES(param.EsIndex, updateESData); serr != nil {
|
||||
return serr
|
||||
}
|
||||
pageNum++
|
||||
log.Info("resetAllToES>>>>>>>>>>>>>>>>>>", log.Any("updateSize:", updSize))
|
||||
}
|
||||
if updSize > 0 {
|
||||
if err := esmanager.Instance.Refresh(param.EsIndex); err != nil {
|
||||
log.Warn("srv_search:resetAllDocsToES Refresh failed", log.Any("ESIndex", param.EsIndex), log.E(err))
|
||||
return srvdef.New(srvdef.ErrElsOperError, "", "")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// constructMatch 构建match
|
||||
func constructMatch(param *Param) bson.M {
|
||||
updateFilter := bson.A{}
|
||||
for _, fieldName := range param.CollFields {
|
||||
filter := bson.M{"updateDescription.updatedFields." + fieldName: bson.M{"$exists": true}}
|
||||
updateFilter = append(updateFilter, filter)
|
||||
}
|
||||
match := bson.M{"$or": bson.A{
|
||||
bson.M{"operationType": bson.M{"$in": []string{"delete", "insert", "replace"}}},
|
||||
bson.M{"$and": bson.A{
|
||||
bson.M{"operationType": bson.M{"$in": []string{"update"}}},
|
||||
bson.M{"$or": updateFilter}},
|
||||
}}}
|
||||
return match
|
||||
}
|
||||
|
||||
// constructActtressOption 查找选项
|
||||
func constructFindOption(pageNum int64, pageSize int64) *options.FindOptions {
|
||||
opt := &options.FindOptions{}
|
||||
opt.SetSort(bson.M{"createdAt": -1})
|
||||
skip := (pageNum - 1) * pageSize
|
||||
opt.SetSkip(skip)
|
||||
opt.SetLimit(pageSize)
|
||||
return opt
|
||||
}
|
||||
|
||||
// AddMultiDataToES 添加一批Ex
|
||||
func AddMultiDataToES(esIndex string, esvs esmanager.M) *srvdef.SrvErr {
|
||||
if err := esmanager.Instance.Bulk(esIndex, esvs); err != nil {
|
||||
info := fmt.Sprintf("(srv_search:AddMultiDataToES) ES Bulk failed! err:%+v:", err)
|
||||
log.Warn(info)
|
||||
return srvdef.New(srvdef.ErrElsOperError, "", "")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// findManyFromDb 查找一批数据
|
||||
func findManyFromDb(ctx context.Context, param *Param,
|
||||
opts ...*options.FindOptions) ([]bson.M, *srvdef.SrvErr) {
|
||||
docs := make([]bson.M, 0)
|
||||
if err := param.Coll.Find(ctx, &docs, param.CollFilter, opts...); err != nil {
|
||||
log.Warn("srv_search:findManyFromDb db.Find failed", log.E(err), log.Any("filter", param.CollFilter))
|
||||
return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
|
||||
}
|
||||
return docs, nil
|
||||
}
|
||||
|
||||
// sliceMapToJsonMap 从map切片构建Map对象
|
||||
func sliceMapToJsonMap(docs []bson.M, param *Param) (esmanager.M, *srvdef.SrvErr) {
|
||||
esMap := make(esmanager.M)
|
||||
for _, doc := range docs {
|
||||
fieldMap := make(esmanager.M)
|
||||
for key, value := range doc {
|
||||
if utils.IncludeString(param.CollFields, key) {
|
||||
fieldMap[key] = value
|
||||
} else if key == "_id" {
|
||||
fieldMap["id"] = value
|
||||
}
|
||||
}
|
||||
if id, ok := doc["_id"].(int64); ok {
|
||||
strId := strconv.FormatInt(id, 10)
|
||||
esMap[strId] = fieldMap
|
||||
} else {
|
||||
log.Warn("srv_search:sliceMapToJsonMap Key doesn't exist", log.Any("keyId", "_id"))
|
||||
}
|
||||
}
|
||||
return esMap, nil
|
||||
}
|
||||
|
||||
// getInfoFromChangeData 从改变流中获取数据信息
|
||||
func getInfoFromChangeData(changeData bson.M, param *Param) (
|
||||
string, *esmod.ESTimestamp, string, esmanager.M, *srvdef.SrvErr) {
|
||||
operationType, ok := changeData["operationType"].(string)
|
||||
if ok == false {
|
||||
log.Error("srv_search:getInfoFromChangeData operationType type error")
|
||||
return "", nil, "", nil, srvdef.New(srvdef.ErrData, "", "")
|
||||
}
|
||||
ptimestamp, ok := changeData["clusterTime"].(primitive.Timestamp)
|
||||
if ok == false {
|
||||
log.Error("srv_search:getInfoFromChangeData clusterTime type error")
|
||||
return "", nil, "", nil, srvdef.New(srvdef.ErrData, "", "")
|
||||
}
|
||||
estimestamp := &esmod.ESTimestamp{T: ptimestamp.T, I: ptimestamp.I}
|
||||
documentKey, ok := changeData["documentKey"].(primitive.M)
|
||||
if ok == false {
|
||||
log.Error("srv_search:getInfoFromChangeData documentKey type error")
|
||||
return "", nil, "", nil, srvdef.New(srvdef.ErrData, "", "")
|
||||
}
|
||||
docKey, ok := documentKey["_id"].(int64)
|
||||
if ok == false {
|
||||
log.Error("srv_search:getInfoFromChangeData documentKey[_id] type error")
|
||||
return "", nil, "", nil, srvdef.New(srvdef.ErrData, "", "")
|
||||
}
|
||||
strId := strconv.FormatInt(docKey, 10)
|
||||
esDocMap := make(esmanager.M)
|
||||
if changeData["fullDocument"] != nil {
|
||||
docMap, ok := changeData["fullDocument"].(primitive.M)
|
||||
if ok == false {
|
||||
log.Error("srv_search:getInfoFromChangeData fullDocument type error")
|
||||
return "", nil, "", nil, srvdef.New(srvdef.ErrData, "", "")
|
||||
}
|
||||
|
||||
for key, value := range docMap {
|
||||
if utils.IncludeString(param.CollFields, key) {
|
||||
esDocMap[key] = value
|
||||
} else if key == "_id" {
|
||||
esDocMap["id"] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
return operationType, estimestamp, strId, esDocMap, nil
|
||||
}
|
||||
|
||||
// delAllFromES 删除所有数据
|
||||
func delAllFromES(esIndex string) *srvdef.SrvErr {
|
||||
query := esmanager.M{
|
||||
"query": esmanager.M{
|
||||
"match_all": esmanager.M{},
|
||||
},
|
||||
}
|
||||
if err := esmanager.Instance.DelMany([]string{esIndex}, query); err != nil {
|
||||
log.Warn(fmt.Sprintf("(srv_search:DelAllFromEs) Es DelMany failed! err:%+v:", err),
|
||||
log.Any("query", query))
|
||||
return srvdef.New(srvdef.ErrElsOperError, "", "")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// batchAddToES 添加一批数据到ES
|
||||
func batchAddToES(esIndex string, esvs esmanager.M) *srvdef.SrvErr {
|
||||
log.Info("srv_search:resetAllDocsToES", log.Any("esIndex", esIndex), log.Any("esvs", esvs))
|
||||
if err := esmanager.Instance.Bulk(esIndex, esvs); err != nil {
|
||||
info := fmt.Sprintf("(srv_search:batchAddToES) ES Bulk failed! err:%+v:", err)
|
||||
log.Warn(info)
|
||||
return srvdef.New(srvdef.ErrElsOperError, "", "")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// batchDelFromES 删除一批数据从ES
|
||||
func batchDelFromES(esIndex string, ids []string) *srvdef.SrvErr {
|
||||
query := esmanager.M{
|
||||
"query": esmanager.M{
|
||||
"bool": esmanager.M{
|
||||
"must": esmanager.A{
|
||||
{
|
||||
"ids": esmanager.M{
|
||||
"values": ids,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := esmanager.Instance.DelMany([]string{esIndex}, query); err != nil {
|
||||
info := fmt.Sprintf(
|
||||
"(srv_search:batchDelFromES) Es DelMany failed! err:%+v:", err)
|
||||
log.Warn(info)
|
||||
return srvdef.New(srvdef.ErrElsOperError, "", "")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// watchOpLog 监控表变化
|
||||
func watchOpLog(ctx context.Context, db *mongos.Collection, pipeline []bson.M,
|
||||
opt *options.ChangeStreamOptions) (*mongo.ChangeStream, *srvdef.SrvErr) {
|
||||
changeStream, err := db.WatchOpLog(ctx, pipeline, opt)
|
||||
if err != nil {
|
||||
log.Warn("srv_search:watchOpLog db.WatchOpLog failed",
|
||||
log.E(err), log.Any("pipeline", pipeline))
|
||||
return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
|
||||
}
|
||||
return changeStream, nil
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package srv_seopush
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"videoweb/pub/models/playletmod"
|
||||
"videoweb/pub/pubdef"
|
||||
"videoweb/pub/seo/sitemapdirty"
|
||||
)
|
||||
|
||||
// MarkSitemapDirty 通知 app 进程重新生成 sitemap。
|
||||
func MarkSitemapDirty(ctx context.Context) {
|
||||
sitemapdirty.Mark(ctx)
|
||||
}
|
||||
|
||||
// NotifyPlayletAfterWrite 短剧写入后:刷新 sitemap 标记。
|
||||
// 搜索引擎只在新上架(含新增即上架)或从已上架变为下架时推送,普通改内容不推。
|
||||
func NotifyPlayletAfterWrite(ctx context.Context, playletId int64, prevStatus *playletmod.Status, status playletmod.Status, onlineAt time.Time) {
|
||||
MarkSitemapDirty(ctx)
|
||||
wasUp := prevStatus != nil && *prevStatus == playletmod.StatusUp
|
||||
if status == playletmod.StatusUp {
|
||||
if !wasUp {
|
||||
EnqueueUpdated(ctx, KindPlaylet, playletId, PageVisiblePlaylet(status, onlineAt))
|
||||
}
|
||||
return
|
||||
}
|
||||
if wasUp {
|
||||
EnqueueDeleted(ctx, KindPlaylet, playletId)
|
||||
}
|
||||
}
|
||||
|
||||
// NotifyPlayletAfterStatus 批量上下架后的 SEO 通知
|
||||
func NotifyPlayletAfterStatus(ctx context.Context, ids []int64, status playletmod.Status, onlineAt time.Time) {
|
||||
MarkSitemapDirty(ctx)
|
||||
if status == playletmod.StatusUp {
|
||||
for _, id := range ids {
|
||||
EnqueueUpdated(ctx, KindPlaylet, id, PageVisiblePlaylet(status, onlineAt))
|
||||
}
|
||||
return
|
||||
}
|
||||
for _, id := range ids {
|
||||
EnqueueDeleted(ctx, KindPlaylet, id)
|
||||
}
|
||||
}
|
||||
|
||||
// NotifyInfoAfterWrite 视频写入后的 SEO 通知。
|
||||
// 搜索引擎只在新上架或从已上架变为下架时推送,普通改内容不推。
|
||||
func NotifyInfoAfterWrite(ctx context.Context, kind string, id int64, prevStatus, status pubdef.InformationStatus) {
|
||||
MarkSitemapDirty(ctx)
|
||||
wasUp := prevStatus == pubdef.InformationStatusVerifyOK
|
||||
if PageVisibleInfo(status) {
|
||||
if !wasUp {
|
||||
EnqueueUpdated(ctx, kind, id, true)
|
||||
}
|
||||
return
|
||||
}
|
||||
if wasUp {
|
||||
EnqueueDeleted(ctx, kind, id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
// Package srv_seopush 负责内容新上架/下架时向搜索引擎推送页面地址。
|
||||
// 业务回调里即时推送一次,失败的任务由 web/skd/job/seopush 兜底重试。
|
||||
package srv_seopush
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/godef"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/playletmod"
|
||||
"videoweb/pub/models/seopushmod"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
"videoweb/pub/pubdef"
|
||||
"videoweb/pub/seo/googleindex"
|
||||
seoindexnow "videoweb/pub/seo/indexnow"
|
||||
"videoweb/web/service/srv_cfg"
|
||||
"videoweb/web/webg"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const (
|
||||
KindVideo = "video"
|
||||
KindPlaylet = "playlet"
|
||||
)
|
||||
|
||||
// channelFields 把两个推送通道的字段名抽象出来,共用同一套状态更新逻辑
|
||||
type channelFields struct {
|
||||
name string
|
||||
status string
|
||||
retry string
|
||||
errMsg string
|
||||
at string
|
||||
}
|
||||
|
||||
var googleChannel = channelFields{
|
||||
name: "google",
|
||||
status: seopushmod.FN.SeoPushTask.GoogleStatus,
|
||||
retry: seopushmod.FN.SeoPushTask.GoogleRetry,
|
||||
errMsg: seopushmod.FN.SeoPushTask.GoogleErr,
|
||||
at: seopushmod.FN.SeoPushTask.GoogleAt,
|
||||
}
|
||||
|
||||
var indexNowChannel = channelFields{
|
||||
name: "indexnow",
|
||||
status: seopushmod.FN.SeoPushTask.IndexNowStatus,
|
||||
retry: seopushmod.FN.SeoPushTask.IndexNowRetry,
|
||||
errMsg: seopushmod.FN.SeoPushTask.IndexNowErr,
|
||||
at: seopushmod.FN.SeoPushTask.IndexNowAt,
|
||||
}
|
||||
|
||||
func kindPrefix(kind string) string {
|
||||
switch kind {
|
||||
case KindVideo:
|
||||
return "/video-detail"
|
||||
case KindPlaylet:
|
||||
return "/drama-detail"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// DetailURL 拼装详情页完整地址,路径与 sitemap 一致:/{prefix}/{slug},无 slug 回退 ID。
|
||||
func DetailURL(webAddr, prefix, slug string, id int64) string {
|
||||
seg := utils.PathSegment(slug, id)
|
||||
if seg == "" {
|
||||
return ""
|
||||
}
|
||||
return utils.JoinUrlPathKeepSlash(webAddr, strings.TrimRight(prefix, "/")+"/"+seg)
|
||||
}
|
||||
|
||||
// PageVisiblePlaylet 判断短剧详情页当前是否对外可访问。
|
||||
func PageVisiblePlaylet(status playletmod.Status, onlineAt time.Time) bool {
|
||||
if status != playletmod.StatusUp {
|
||||
return false
|
||||
}
|
||||
if onlineAt.IsZero() {
|
||||
return true
|
||||
}
|
||||
return !onlineAt.After(time.Now())
|
||||
}
|
||||
|
||||
// PageVisibleInfo 判断视频详情页是否已上架。
|
||||
func PageVisibleInfo(status pubdef.InformationStatus) bool {
|
||||
return status == pubdef.InformationStatusVerifyOK
|
||||
}
|
||||
|
||||
// SEOBaseURL 取站点对外地址
|
||||
func SEOBaseURL(ctx context.Context) string {
|
||||
sysCfg, serr := srv_cfg.GetSystemConfig(ctx)
|
||||
if serr != nil || sysCfg == nil {
|
||||
log.Warn("seopush: get system config failed", log.E(serr))
|
||||
return ""
|
||||
}
|
||||
base := sysCfg.SEOBaseURL()
|
||||
if base == "" {
|
||||
log.Warn("seopush: SEO base URL is empty, skip push")
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
// Enabled 业务开关是否打开(不含环境判断)。
|
||||
func Enabled() bool {
|
||||
return pubdef.Enabled()
|
||||
}
|
||||
|
||||
func canPushExternally() bool {
|
||||
return pubdef.Enabled() && webg.Conf.RunMod == "prod"
|
||||
}
|
||||
|
||||
// EnqueueUpdated 内容新上架后登记并立即推送,仅在页面已可访问时生效
|
||||
func EnqueueUpdated(ctx context.Context, kind string, id int64, visible bool) {
|
||||
if !pubdef.Enabled() || !visible {
|
||||
return
|
||||
}
|
||||
enqueue(ctx, kind, id, seopushmod.ActionUpdated, true)
|
||||
}
|
||||
|
||||
// RegisterUpdated 只登记不立即推送,供批量补推使用
|
||||
func RegisterUpdated(ctx context.Context, kind string, id int64, visible bool) {
|
||||
if !pubdef.Enabled() || !visible {
|
||||
return
|
||||
}
|
||||
enqueue(ctx, kind, id, seopushmod.ActionUpdated, false)
|
||||
}
|
||||
|
||||
// EnqueueDeleted 内容下架/删除后登记并立即推送
|
||||
func EnqueueDeleted(ctx context.Context, kind string, id int64) {
|
||||
if !pubdef.Enabled() {
|
||||
return
|
||||
}
|
||||
enqueue(ctx, kind, id, seopushmod.ActionDeleted, true)
|
||||
}
|
||||
|
||||
func enqueue(ctx context.Context, kind string, id int64, action string, pushNow bool) {
|
||||
webAddr := SEOBaseURL(ctx)
|
||||
if webAddr == "" {
|
||||
return
|
||||
}
|
||||
prefix := kindPrefix(kind)
|
||||
if prefix == "" {
|
||||
return
|
||||
}
|
||||
pageURL := DetailURL(webAddr, prefix, loadSlug(ctx, kind, id), id)
|
||||
if pageURL == "" {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
task := &seopushmod.SeoPushTask{
|
||||
Url: pageURL,
|
||||
VideoId: id,
|
||||
Action: action,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if action == seopushmod.ActionDeleted {
|
||||
_, _ = seopushmod.DelManySeoPushTask(ctx, bson.M{
|
||||
seopushmod.FN.SeoPushTask.Url: pageURL,
|
||||
seopushmod.FN.SeoPushTask.Action: seopushmod.ActionUpdated,
|
||||
})
|
||||
}
|
||||
if serr := seopushmod.UpsertPendingSeoPushTask(ctx, task); serr != nil {
|
||||
log.Warn("seopush: enqueue task failed", log.Any("url", pageURL), log.E(serr))
|
||||
return
|
||||
}
|
||||
if action == seopushmod.ActionUpdated {
|
||||
markQueued(ctx, kind, id)
|
||||
}
|
||||
if !pushNow {
|
||||
return
|
||||
}
|
||||
|
||||
godef.Go(func() {
|
||||
PushTask(context.Background(), webAddr, task)
|
||||
})
|
||||
}
|
||||
|
||||
func loadSlug(ctx context.Context, kind string, id int64) string {
|
||||
if id <= 0 {
|
||||
return ""
|
||||
}
|
||||
switch kind {
|
||||
case KindVideo:
|
||||
item, err := videoinformationmod.FindOneVideoInformation(ctx,
|
||||
bson.M{videoinformationmod.FN.VideoInformation.Id: id},
|
||||
options.FindOne().SetProjection(bson.D{{Key: videoinformationmod.FN.VideoInformation.Slug, Value: 1}}))
|
||||
if err != nil || item == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(item.Slug)
|
||||
case KindPlaylet:
|
||||
item, err := playletmod.FindOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: id},
|
||||
options.FindOne().SetProjection(bson.D{{Key: playletmod.FN.Playlet.Slug, Value: 1}}))
|
||||
if err != nil || item == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(item.Slug)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func markQueued(ctx context.Context, kind string, id int64) {
|
||||
var serr error
|
||||
switch kind {
|
||||
case KindVideo:
|
||||
serr = videoinformationmod.UpdateOneVideoInformation(ctx,
|
||||
bson.M{videoinformationmod.FN.VideoInformation.Id: id},
|
||||
bson.M{"$set": bson.M{videoinformationmod.FN.VideoInformation.IndexNow: int64(1)}})
|
||||
case KindPlaylet:
|
||||
serr = playletmod.UpdateOnePlaylet(ctx,
|
||||
bson.M{playletmod.FN.Playlet.Id: id},
|
||||
bson.M{"$set": bson.M{playletmod.FN.Playlet.IndexNow: int64(1)}})
|
||||
}
|
||||
if serr != nil {
|
||||
log.Warn("seopush: mark queued failed", log.Any("kind", kind), log.Any("id", id), log.E(serr))
|
||||
}
|
||||
}
|
||||
|
||||
// PushTask 推送任务中尚未成功的通道
|
||||
func PushTask(ctx context.Context, webAddr string, task *seopushmod.SeoPushTask) {
|
||||
if task == nil || !pubdef.Enabled() {
|
||||
return
|
||||
}
|
||||
if strings.Contains(task.Url, "?") {
|
||||
log.Warn("seopush: skip query-url task", log.Any("url", task.Url), log.Any("action", task.Action))
|
||||
return
|
||||
}
|
||||
if !canPushExternally() {
|
||||
dryRun(ctx, task)
|
||||
return
|
||||
}
|
||||
if task.GoogleStatus != seopushmod.PushStatusSuccess {
|
||||
pushGoogle(ctx, task)
|
||||
}
|
||||
if task.IndexNowStatus != seopushmod.PushStatusSuccess {
|
||||
pushIndexNow(ctx, webAddr, task)
|
||||
}
|
||||
}
|
||||
|
||||
func dryRun(ctx context.Context, task *seopushmod.SeoPushTask) {
|
||||
log.Info("seopush: dry-run, external push skipped in non-prod",
|
||||
log.Any("url", task.Url), log.Any("action", task.Action))
|
||||
if task.GoogleStatus != seopushmod.PushStatusSuccess {
|
||||
markSuccess(ctx, task, googleChannel)
|
||||
}
|
||||
if task.IndexNowStatus != seopushmod.PushStatusSuccess {
|
||||
markSuccess(ctx, task, indexNowChannel)
|
||||
}
|
||||
}
|
||||
|
||||
func pushGoogle(ctx context.Context, task *seopushmod.SeoPushTask) {
|
||||
notifyType := googleindex.TypeUpdated
|
||||
if task.Action == seopushmod.ActionDeleted {
|
||||
notifyType = googleindex.TypeDeleted
|
||||
}
|
||||
|
||||
resp, err := googleindex.Publish(ctx, task.Url, notifyType)
|
||||
if err == nil {
|
||||
markSuccess(ctx, task, googleChannel)
|
||||
log.Info("seopush=google=pushed",
|
||||
log.Any("url", task.Url),
|
||||
log.Any("type", notifyType),
|
||||
log.Any("resp", resp))
|
||||
return
|
||||
}
|
||||
if errors.Is(err, googleindex.ErrQuotaExceeded) {
|
||||
log.Warn("seopush=google=quota_exceeded",
|
||||
log.Any("url", task.Url), log.Any("resp", err.Error()))
|
||||
return
|
||||
}
|
||||
if errors.Is(err, googleindex.ErrNotEnabled) {
|
||||
log.Warn("seopush=google=not_ready", log.Any("url", task.Url))
|
||||
return
|
||||
}
|
||||
markFailed(ctx, task, googleChannel, err)
|
||||
}
|
||||
|
||||
func pushIndexNow(ctx context.Context, webAddr string, task *seopushmod.SeoPushTask) {
|
||||
if strings.TrimSpace(webAddr) == "" {
|
||||
return
|
||||
}
|
||||
config, err := seoindexnow.BuildConfig(webAddr)
|
||||
if err != nil {
|
||||
log.Warn("seopush: build indexnow config failed", log.Any("webAddr", webAddr), log.E(err))
|
||||
return
|
||||
}
|
||||
|
||||
result := seoindexnow.NewBatchIndexNowSubmitter(config).SubmitBatch([]string{task.Url})
|
||||
if result.SuccessCount > 0 {
|
||||
markSuccess(ctx, task, indexNowChannel)
|
||||
log.Info("seopush=indexnow=pushed",
|
||||
log.Any("url", task.Url),
|
||||
log.Any("statusCode", result.StatusCodes))
|
||||
return
|
||||
}
|
||||
markFailed(ctx, task, indexNowChannel, errors.New(strings.Join(result.Errors, "; ")))
|
||||
}
|
||||
|
||||
func markSuccess(ctx context.Context, task *seopushmod.SeoPushTask, ch channelFields) {
|
||||
now := time.Now()
|
||||
if serr := seopushmod.UpdateOneSeoPushTask(ctx, taskFilter(task), bson.M{
|
||||
"$set": bson.M{
|
||||
ch.status: seopushmod.PushStatusSuccess,
|
||||
ch.errMsg: "",
|
||||
ch.at: now,
|
||||
seopushmod.FN.SeoPushTask.UpdatedAt: now,
|
||||
},
|
||||
}); serr != nil {
|
||||
log.Warn("seopush: mark success failed",
|
||||
log.Any("channel", ch.name), log.Any("url", task.Url), log.E(serr))
|
||||
}
|
||||
}
|
||||
|
||||
func markFailed(ctx context.Context, task *seopushmod.SeoPushTask, ch channelFields, pushErr error) {
|
||||
now := time.Now()
|
||||
errText := ""
|
||||
if pushErr != nil {
|
||||
errText = pushErr.Error()
|
||||
}
|
||||
log.Warn("seopush: push failed",
|
||||
log.Any("channel", ch.name), log.Any("url", task.Url), log.Any("err", errText))
|
||||
|
||||
if serr := seopushmod.UpdateOneSeoPushTask(ctx, taskFilter(task), bson.M{
|
||||
"$set": bson.M{
|
||||
ch.errMsg: errText,
|
||||
ch.at: now,
|
||||
seopushmod.FN.SeoPushTask.UpdatedAt: now,
|
||||
},
|
||||
"$inc": bson.M{ch.retry: 1},
|
||||
}); serr != nil {
|
||||
log.Warn("seopush: mark failed failed",
|
||||
log.Any("channel", ch.name), log.Any("url", task.Url), log.E(serr))
|
||||
}
|
||||
}
|
||||
|
||||
func taskFilter(task *seopushmod.SeoPushTask) bson.M {
|
||||
return bson.M{
|
||||
seopushmod.FN.SeoPushTask.Url: task.Url,
|
||||
seopushmod.FN.SeoPushTask.Action: task.Action,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
package srv_slug
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/slugx"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/actress"
|
||||
"videoweb/pub/models/contenttagmod"
|
||||
"videoweb/pub/models/playletcategorymod"
|
||||
"videoweb/pub/models/playletchaptermod"
|
||||
"videoweb/pub/models/playletmod"
|
||||
"videoweb/pub/models/playletthememod"
|
||||
"videoweb/pub/models/videocategorymod"
|
||||
"videoweb/pub/models/videocreatermod"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
"videoweb/web/api/proto"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const (
|
||||
syncSlugDefaultSize = int64(500)
|
||||
syncSlugMaxSize = int64(2000)
|
||||
|
||||
TypeVideo = "video"
|
||||
TypeVideoCategory = "videoCategory"
|
||||
TypeActress = "actress"
|
||||
TypeVideoCreater = "videoCreater"
|
||||
TypePlaylet = "playlet"
|
||||
TypePlayletCategory = "playletCategory"
|
||||
TypePlayletTheme = "playletTheme"
|
||||
TypeContentTag = "contentTag"
|
||||
)
|
||||
|
||||
type slugJob struct {
|
||||
kind string
|
||||
set func(*proto.SlugSyncResp, int64)
|
||||
run func(context.Context, int64) (int64, bool, *srvdef.SrvErr)
|
||||
hasMore func(context.Context) bool
|
||||
}
|
||||
|
||||
// SyncEmptySlugs 为视频、分类、女优、作者、短剧、统一标签中空 slug 自动生成。可重复调用,hasMore 为 true 时继续请求。
|
||||
func SyncEmptySlugs(ctx context.Context, req *proto.SlugSyncReq) (*proto.SlugSyncResp, *srvdef.SrvErr) {
|
||||
maxSize := syncSlugDefaultSize
|
||||
if req != nil && req.MaxSize > 0 {
|
||||
maxSize = req.MaxSize
|
||||
}
|
||||
if maxSize > syncSlugMaxSize {
|
||||
maxSize = syncSlugMaxSize
|
||||
}
|
||||
kind := ""
|
||||
if req != nil {
|
||||
kind = strings.TrimSpace(req.Type)
|
||||
}
|
||||
|
||||
jobs := allSlugJobs()
|
||||
if kind != "" {
|
||||
job, ok := jobByKind(jobs, kind)
|
||||
if !ok {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "不支持的type", "不支持的type")
|
||||
}
|
||||
jobs = []slugJob{job}
|
||||
}
|
||||
|
||||
resp := &proto.SlugSyncResp{}
|
||||
remain := maxSize
|
||||
for i, job := range jobs {
|
||||
if remain <= 0 {
|
||||
resp.HasMore = anyHasEmpty(ctx, jobs[i:])
|
||||
resp.TotalUpdated = sumUpdated(resp)
|
||||
return resp, nil
|
||||
}
|
||||
updated, more, err := job.run(ctx, remain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
job.set(resp, updated)
|
||||
remain -= updated
|
||||
if more {
|
||||
resp.HasMore = true
|
||||
resp.TotalUpdated = sumUpdated(resp)
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
resp.TotalUpdated = sumUpdated(resp)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func allSlugJobs() []slugJob {
|
||||
return []slugJob{
|
||||
{TypeVideoCategory, func(r *proto.SlugSyncResp, n int64) { r.VideoCategoryUpdated = n }, syncVideoCategoryEmptySlugs, hasVideoCategoryEmpty},
|
||||
{TypePlayletCategory, func(r *proto.SlugSyncResp, n int64) { r.PlayletCategoryUpdated = n }, syncPlayletCategoryEmptySlugs, hasPlayletCategoryEmpty},
|
||||
{TypePlayletTheme, func(r *proto.SlugSyncResp, n int64) { r.PlayletThemeUpdated = n }, syncPlayletThemeEmptySlugs, hasPlayletThemeEmpty},
|
||||
{TypeContentTag, func(r *proto.SlugSyncResp, n int64) { r.ContentTagUpdated = n }, syncContentTagEmptySlugs, hasContentTagEmpty},
|
||||
{TypeActress, func(r *proto.SlugSyncResp, n int64) { r.ActressUpdated = n }, syncActressEmptySlugs, hasActressEmpty},
|
||||
{TypeVideoCreater, func(r *proto.SlugSyncResp, n int64) { r.VideoCreaterUpdated = n }, syncVideoCreaterEmptySlugs, hasVideoCreaterEmpty},
|
||||
{TypePlaylet, func(r *proto.SlugSyncResp, n int64) { r.PlayletUpdated = n }, syncPlayletEmptySlugs, hasPlayletEmpty},
|
||||
{TypeVideo, func(r *proto.SlugSyncResp, n int64) { r.VideoUpdated = n }, syncVideoEmptySlugs, hasVideoEmpty},
|
||||
}
|
||||
}
|
||||
|
||||
func jobByKind(jobs []slugJob, kind string) (slugJob, bool) {
|
||||
for _, job := range jobs {
|
||||
if job.kind == kind {
|
||||
return job, true
|
||||
}
|
||||
}
|
||||
return slugJob{}, false
|
||||
}
|
||||
|
||||
func anyHasEmpty(ctx context.Context, jobs []slugJob) bool {
|
||||
for _, job := range jobs {
|
||||
if job.hasMore(ctx) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func sumUpdated(resp *proto.SlugSyncResp) int64 {
|
||||
return resp.VideoUpdated + resp.VideoCategoryUpdated + resp.ActressUpdated +
|
||||
resp.VideoCreaterUpdated +
|
||||
resp.PlayletUpdated + resp.PlayletCategoryUpdated +
|
||||
resp.PlayletThemeUpdated + resp.ContentTagUpdated
|
||||
}
|
||||
|
||||
func findLimit(maxSize int64) *options.FindOptions {
|
||||
return options.Find().SetSort(bson.D{{Key: "_id", Value: 1}}).SetLimit(maxSize + 1)
|
||||
}
|
||||
|
||||
func fillEmptySlugs[T any](
|
||||
ctx context.Context,
|
||||
list []T,
|
||||
maxSize int64,
|
||||
idOf func(T) int64,
|
||||
sourceOf func(T) string,
|
||||
exists func(slug string, excludeId int64) bool,
|
||||
update func(context.Context, int64, string) *srvdef.SrvErr,
|
||||
) (int64, bool, *srvdef.SrvErr) {
|
||||
hasMore := int64(len(list)) > maxSize
|
||||
if hasMore {
|
||||
list = list[:maxSize]
|
||||
}
|
||||
var updated int64
|
||||
for _, item := range list {
|
||||
if any(item) == nil {
|
||||
continue
|
||||
}
|
||||
id := idOf(item)
|
||||
if id <= 0 {
|
||||
continue
|
||||
}
|
||||
slug := ""
|
||||
if serr := slugx.Ensure(&slug, sourceOf(item), func(s string) bool {
|
||||
return exists(s, id)
|
||||
}); serr != nil {
|
||||
return updated, hasMore, serr
|
||||
}
|
||||
if serr := update(ctx, id, slug); serr != nil {
|
||||
return updated, hasMore, serr
|
||||
}
|
||||
updated++
|
||||
}
|
||||
return updated, hasMore, nil
|
||||
}
|
||||
|
||||
func setSlug(ctx context.Context, updateOne func(context.Context, bson.M, bson.M) *srvdef.SrvErr, idField, slugField, updatedAtField string, id int64, slug string) *srvdef.SrvErr {
|
||||
setDoc := bson.M{slugField: slug}
|
||||
if updatedAtField != "" {
|
||||
setDoc[updatedAtField] = time.Now()
|
||||
}
|
||||
return updateOne(ctx, bson.M{idField: id}, bson.M{"$set": setDoc})
|
||||
}
|
||||
|
||||
func videoEmptySlugFilter(ctx context.Context) bson.M {
|
||||
filter := slugx.EmptyFilter()
|
||||
playletIDs := playletchaptermod.ListLinkedMediaIDs(ctx)
|
||||
if len(playletIDs) == 0 {
|
||||
return filter
|
||||
}
|
||||
return bson.M{
|
||||
"$and": []bson.M{
|
||||
filter,
|
||||
{
|
||||
"$nor": []bson.M{
|
||||
{videoinformationmod.FN.VideoInformation.SyncId: bson.M{"$in": playletIDs}},
|
||||
{videoinformationmod.FN.VideoInformation.Video.FileId: bson.M{"$in": playletIDs}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func hasVideoEmpty(ctx context.Context) bool {
|
||||
n, _ := videoinformationmod.CountVideoInformations(ctx, videoEmptySlugFilter(ctx))
|
||||
return n > 0
|
||||
}
|
||||
func hasVideoCategoryEmpty(ctx context.Context) bool {
|
||||
n, _ := videocategorymod.CountVideoCategorys(ctx, slugx.EmptyFilter())
|
||||
return n > 0
|
||||
}
|
||||
func hasActressEmpty(ctx context.Context) bool {
|
||||
n, _ := actress.CountActresss(ctx, slugx.EmptyFilter())
|
||||
return n > 0
|
||||
}
|
||||
func hasVideoCreaterEmpty(ctx context.Context) bool {
|
||||
n, _ := videocreatermod.CountVideoCreaterInfos(ctx, slugx.EmptyFilter())
|
||||
return n > 0
|
||||
}
|
||||
func hasPlayletEmpty(ctx context.Context) bool {
|
||||
n, _ := playletmod.CountPlaylet(ctx, slugx.EmptyFilter())
|
||||
return n > 0
|
||||
}
|
||||
func hasPlayletCategoryEmpty(ctx context.Context) bool {
|
||||
n, _ := playletcategorymod.CountPlayletCategory(ctx, slugx.EmptyFilter())
|
||||
return n > 0
|
||||
}
|
||||
func hasPlayletThemeEmpty(ctx context.Context) bool {
|
||||
n, _ := playletthememod.CountPlayletTheme(ctx, slugx.EmptyFilter())
|
||||
return n > 0
|
||||
}
|
||||
func hasContentTagEmpty(ctx context.Context) bool {
|
||||
n, _ := contenttagmod.CountContentTag(ctx, slugx.EmptyFilter())
|
||||
return n > 0
|
||||
}
|
||||
|
||||
func syncVideoEmptySlugs(ctx context.Context, maxSize int64) (int64, bool, *srvdef.SrvErr) {
|
||||
list, err := videoinformationmod.FindManyVideoInformations(ctx, videoEmptySlugFilter(ctx), findLimit(maxSize))
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
return fillEmptySlugs(ctx, []*videoinformationmod.VideoInformation(list), maxSize,
|
||||
func(item *videoinformationmod.VideoInformation) int64 { return item.Id },
|
||||
func(item *videoinformationmod.VideoInformation) string {
|
||||
return utils.SlugSourceFromKeywordsOrTitle(item.Keyworld, item.Title)
|
||||
},
|
||||
func(s string, excludeId int64) bool {
|
||||
doc, _ := videoinformationmod.FindOneVideoInformation(ctx, slugx.Filter(
|
||||
videoinformationmod.FN.VideoInformation.Slug, videoinformationmod.FN.VideoInformation.Id, s, excludeId))
|
||||
return doc != nil
|
||||
},
|
||||
func(ctx context.Context, id int64, slug string) *srvdef.SrvErr {
|
||||
return setSlug(ctx, videoinformationmod.UpdateOneVideoInformation, videoinformationmod.FN.VideoInformation.Id,
|
||||
videoinformationmod.FN.VideoInformation.Slug, videoinformationmod.FN.VideoInformation.UpdatedAt, id, slug)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func syncVideoCategoryEmptySlugs(ctx context.Context, maxSize int64) (int64, bool, *srvdef.SrvErr) {
|
||||
list, err := videocategorymod.FindManyVideoCategorys(ctx, slugx.EmptyFilter(), findLimit(maxSize))
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
return fillEmptySlugs(ctx, []*videocategorymod.VideoCategory(list), maxSize,
|
||||
func(item *videocategorymod.VideoCategory) int64 { return item.Id },
|
||||
func(item *videocategorymod.VideoCategory) string {
|
||||
return utils.SlugSourceFromKeywordsOrTitle(item.SeoInfo.Keyworld, item.Name)
|
||||
},
|
||||
func(s string, excludeId int64) bool {
|
||||
doc, _ := videocategorymod.FindOneVideoCategory(ctx, slugx.Filter(
|
||||
videocategorymod.FN.VideoCategory.Slug, videocategorymod.FN.VideoCategory.Id, s, excludeId))
|
||||
return doc != nil
|
||||
},
|
||||
func(ctx context.Context, id int64, slug string) *srvdef.SrvErr {
|
||||
return setSlug(ctx, videocategorymod.UpdateOneVideoCategory, videocategorymod.FN.VideoCategory.Id,
|
||||
videocategorymod.FN.VideoCategory.Slug, videocategorymod.FN.VideoCategory.UpdatedAt, id, slug)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func syncActressEmptySlugs(ctx context.Context, maxSize int64) (int64, bool, *srvdef.SrvErr) {
|
||||
list, err := actress.FindManyActresss(ctx, slugx.EmptyFilter(), findLimit(maxSize))
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
return fillEmptySlugs(ctx, []*actress.Actress(list), maxSize,
|
||||
func(item *actress.Actress) int64 { return item.Id },
|
||||
func(item *actress.Actress) string {
|
||||
return utils.SlugSourceFromKeywordsOrTitle(item.SeoInfo.Keyworld, item.Alias)
|
||||
},
|
||||
func(s string, excludeId int64) bool {
|
||||
doc, _ := actress.FindOneActress(ctx, slugx.Filter(actress.FN.Actress.Slug, actress.FN.Actress.Id, s, excludeId))
|
||||
return doc != nil
|
||||
},
|
||||
func(ctx context.Context, id int64, slug string) *srvdef.SrvErr {
|
||||
return setSlug(ctx, actress.UpdateOneActress, actress.FN.Actress.Id,
|
||||
actress.FN.Actress.Slug, actress.FN.Actress.UpdatedAt, id, slug)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func syncVideoCreaterEmptySlugs(ctx context.Context, maxSize int64) (int64, bool, *srvdef.SrvErr) {
|
||||
list, err := videocreatermod.FindManyVideoCreaterInfos(ctx, slugx.EmptyFilter(), findLimit(maxSize))
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
return fillEmptySlugs(ctx, []*videocreatermod.VideoCreaterInfo(list), maxSize,
|
||||
func(item *videocreatermod.VideoCreaterInfo) int64 { return item.Id },
|
||||
func(item *videocreatermod.VideoCreaterInfo) string {
|
||||
return utils.SlugSourceFromKeywordsOrTitle(item.SeoInfo.Keyworld, item.Name)
|
||||
},
|
||||
func(s string, excludeId int64) bool {
|
||||
doc, _ := videocreatermod.FindOneVideoCreaterInfo(ctx, slugx.Filter(
|
||||
videocreatermod.FN.VideoCreaterInfo.Slug, videocreatermod.FN.VideoCreaterInfo.Id, s, excludeId))
|
||||
return doc != nil
|
||||
},
|
||||
func(ctx context.Context, id int64, slug string) *srvdef.SrvErr {
|
||||
return setSlug(ctx, videocreatermod.UpdateOneVideoCreaterInfo, videocreatermod.FN.VideoCreaterInfo.Id,
|
||||
videocreatermod.FN.VideoCreaterInfo.Slug, videocreatermod.FN.VideoCreaterInfo.UpdatedAt, id, slug)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func syncPlayletEmptySlugs(ctx context.Context, maxSize int64) (int64, bool, *srvdef.SrvErr) {
|
||||
list, err := playletmod.FindManyPlaylet(ctx, slugx.EmptyFilter(), findLimit(maxSize))
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
return fillEmptySlugs(ctx, []*playletmod.Playlet(list), maxSize,
|
||||
func(item *playletmod.Playlet) int64 { return item.Id },
|
||||
func(item *playletmod.Playlet) string {
|
||||
return utils.SlugSourceFromKeywordsOrTitle(item.SeoInfo.Keyworld, item.Title)
|
||||
},
|
||||
func(s string, excludeId int64) bool {
|
||||
doc, _ := playletmod.FindOnePlaylet(ctx, slugx.Filter(playletmod.FN.Playlet.Slug, playletmod.FN.Playlet.Id, s, excludeId))
|
||||
return doc != nil
|
||||
},
|
||||
func(ctx context.Context, id int64, slug string) *srvdef.SrvErr {
|
||||
return setSlug(ctx, playletmod.UpdateOnePlaylet, playletmod.FN.Playlet.Id,
|
||||
playletmod.FN.Playlet.Slug, playletmod.FN.Playlet.UpdatedAt, id, slug)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func syncPlayletCategoryEmptySlugs(ctx context.Context, maxSize int64) (int64, bool, *srvdef.SrvErr) {
|
||||
list, err := playletcategorymod.FindManyPlayletCategory(ctx, slugx.EmptyFilter(), findLimit(maxSize))
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
return fillEmptySlugs(ctx, []*playletcategorymod.PlayletCategory(list), maxSize,
|
||||
func(item *playletcategorymod.PlayletCategory) int64 { return item.Id },
|
||||
func(item *playletcategorymod.PlayletCategory) string {
|
||||
return utils.SlugSourceFromKeywordsOrTitle(item.SeoInfo.Keyworld, item.Name)
|
||||
},
|
||||
func(s string, excludeId int64) bool {
|
||||
doc, _ := playletcategorymod.FindOnePlayletCategory(ctx, slugx.Filter(
|
||||
playletcategorymod.FN.PlayletCategory.Slug, playletcategorymod.FN.PlayletCategory.Id, s, excludeId))
|
||||
return doc != nil
|
||||
},
|
||||
func(ctx context.Context, id int64, slug string) *srvdef.SrvErr {
|
||||
return setSlug(ctx, playletcategorymod.UpdateOnePlayletCategory, playletcategorymod.FN.PlayletCategory.Id,
|
||||
playletcategorymod.FN.PlayletCategory.Slug, playletcategorymod.FN.PlayletCategory.UpdatedAt, id, slug)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func syncPlayletThemeEmptySlugs(ctx context.Context, maxSize int64) (int64, bool, *srvdef.SrvErr) {
|
||||
list, err := playletthememod.FindManyPlayletTheme(ctx, slugx.EmptyFilter(), findLimit(maxSize))
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
return fillEmptySlugs(ctx, []*playletthememod.PlayletTheme(list), maxSize,
|
||||
func(item *playletthememod.PlayletTheme) int64 { return item.Id },
|
||||
func(item *playletthememod.PlayletTheme) string {
|
||||
return utils.SlugSourceFromKeywordsOrTitle(item.SeoInfo.Keyworld, item.Name)
|
||||
},
|
||||
func(s string, excludeId int64) bool {
|
||||
doc, _ := playletthememod.FindOnePlayletTheme(ctx, slugx.Filter(
|
||||
playletthememod.FN.PlayletTheme.Slug, playletthememod.FN.PlayletTheme.Id, s, excludeId))
|
||||
return doc != nil
|
||||
},
|
||||
func(ctx context.Context, id int64, slug string) *srvdef.SrvErr {
|
||||
return setSlug(ctx, playletthememod.UpdateOnePlayletTheme, playletthememod.FN.PlayletTheme.Id,
|
||||
playletthememod.FN.PlayletTheme.Slug, playletthememod.FN.PlayletTheme.UpdatedAt, id, slug)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func syncContentTagEmptySlugs(ctx context.Context, maxSize int64) (int64, bool, *srvdef.SrvErr) {
|
||||
list, err := contenttagmod.FindManyContentTag(ctx, slugx.EmptyFilter(), findLimit(maxSize))
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
return fillEmptySlugs(ctx, []*contenttagmod.ContentTag(list), maxSize,
|
||||
func(item *contenttagmod.ContentTag) int64 { return item.Id },
|
||||
func(item *contenttagmod.ContentTag) string {
|
||||
return utils.SlugSourceFromKeywordsOrTitle(item.SeoInfo.Keyworld, item.Name)
|
||||
},
|
||||
func(s string, excludeId int64) bool {
|
||||
doc, _ := contenttagmod.FindOneContentTag(ctx, slugx.Filter(
|
||||
contenttagmod.FN.ContentTag.Slug, contenttagmod.FN.ContentTag.Id, s, excludeId))
|
||||
return doc != nil
|
||||
},
|
||||
func(ctx context.Context, id int64, slug string) *srvdef.SrvErr {
|
||||
return setSlug(ctx, contenttagmod.UpdateOneContentTag, contenttagmod.FN.ContentTag.Id,
|
||||
contenttagmod.FN.ContentTag.Slug, contenttagmod.FN.ContentTag.UpdatedAt, id, slug)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package srv_translation
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/pub/models/translationmod"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
type TranslationInfoReq struct {
|
||||
pubdef.Page
|
||||
Id int64 `json:"id" form:"id"` // 导航ID
|
||||
Name string `json:"name" form:"name"` // 导航名称
|
||||
TransCategoryId int64 `json:"transCategoryId" form:"transCategoryId"` // 导航分类ID
|
||||
}
|
||||
type TranslationInfoResp struct {
|
||||
Total int64 `json:"total"`
|
||||
List translationmod.TranslationInfoList `json:"list"`
|
||||
}
|
||||
type TranslationInfoUpdate struct {
|
||||
Name *string `json:"name" bson:"name,omitempty"` // 分类名称
|
||||
Status *bool `json:"status" bson:"status,omitempty"` // 上架状态
|
||||
Rank *int64 `json:"rank" bson:"rank,omitempty"` // 排序
|
||||
HtmlId *int64 `json:"htmlId" bson:"htmlId,omitempty"` // 富文本ID
|
||||
Link *string `json:"link" bson:"link,omitempty"` // 外部跳转
|
||||
IsFriendAds *bool `json:"isFriendAds" bson:"isFriendAds"` // 是否是友链广告位
|
||||
TransCategoryId *int64 `json:"transCategoryId" bson:"transCategoryId,omitempty"` //
|
||||
UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"`
|
||||
}
|
||||
type TranslationInfoUpdateReq struct {
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
Param TranslationInfoUpdate `json:"param" bson:"param"` // appId,唯一id
|
||||
}
|
||||
|
||||
type TranslationInfoDelReq struct {
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package srv_translation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/translationmod"
|
||||
"videoweb/pub/mongos"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// GetTranslationInfos 按照参数搜索视频标签
|
||||
func GetTranslationInfos(ctx context.Context, param *TranslationInfoReq) (*TranslationInfoResp, *srvdef.SrvErr) {
|
||||
filter := bson.M{}
|
||||
opt := &options.FindOptions{}
|
||||
resp := &TranslationInfoResp{}
|
||||
|
||||
if param.Id > 0 {
|
||||
filter[translationmod.FN.TranslationInfo.Id] = param.Id
|
||||
}
|
||||
if name := strings.TrimSpace(param.Name); name != "" {
|
||||
filter[translationmod.FN.TranslationInfo.Name] = bson.M{"$regex": name}
|
||||
}
|
||||
if param.TransCategoryId > 0 {
|
||||
filter[translationmod.FN.TranslationInfo.TransCategoryId] = param.TransCategoryId
|
||||
}
|
||||
|
||||
opt.SetSort(bson.M{translationmod.FN.TranslationInfo.Rank: -1})
|
||||
opt.SetSkip((param.PageNum - 1) * param.PageSize).SetLimit(param.PageSize)
|
||||
|
||||
total, err1 := translationmod.CountTranslationInfos(ctx, filter)
|
||||
if err1 != nil {
|
||||
return resp, err1
|
||||
}
|
||||
resp.Total = total
|
||||
if total == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
ads, err1 := translationmod.FindManyTranslationInfos(ctx, filter, opt)
|
||||
if err1 != nil {
|
||||
return resp, err1
|
||||
}
|
||||
resp.List = ads
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// AddTranslationInfo 添加一视频标签
|
||||
func AddTranslationInfo(ctx context.Context,
|
||||
ac *translationmod.TranslationInfo) *srvdef.SrvErr {
|
||||
|
||||
ac.Id = mongos.Lid.Translation.Get()
|
||||
now := time.Now()
|
||||
ac.CreatedAt = now
|
||||
ac.UpdatedAt = now
|
||||
|
||||
serr := translationmod.InsertOneTranslationInfo(ctx, ac)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelTranslationInfo 删除指定视频标签
|
||||
func DelTranslationInfo(ctx context.Context, id int64) *srvdef.SrvErr {
|
||||
adFilter := bson.M{translationmod.FN.TranslationInfo.Id: id}
|
||||
//删除
|
||||
count, serr := translationmod.DelOneTranslationInfo(ctx, adFilter)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
if count == 0 {
|
||||
return srvdef.New(srvdef.ErrNoData, "", "")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateTranslationInfo 更新指定视频标签
|
||||
func UpdateTranslationInfo(ctx context.Context, id int64, param TranslationInfoUpdate) *srvdef.SrvErr {
|
||||
now := time.Now()
|
||||
param.UpdatedAt = now
|
||||
|
||||
adFilter := bson.M{translationmod.FN.TranslationInfo.Id: id}
|
||||
updateDoc := bson.M{"$set": param}
|
||||
serr := translationmod.UpdateOneTranslationInfo(ctx, adFilter, updateDoc)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package srv_translationcategory
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/pub/models/translationcategorymod"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
type TranslationCategoryReq struct {
|
||||
pubdef.Page
|
||||
Id int64 `json:"id" form:"id"` // 导航分类ID
|
||||
Name string `json:"name" form:"name"` // 导航分类名称
|
||||
}
|
||||
type TranslationCategoryResp struct {
|
||||
Total int64 `json:"total"`
|
||||
List translationcategorymod.TranslationCategoryList `json:"list"`
|
||||
}
|
||||
type TranslationCategoryUpdate struct {
|
||||
AppId *int32 `json:"appId" bson:"appId,omitempty"` // appId,唯一id
|
||||
Name *string `json:"name" bson:"name,omitempty"` // 名称
|
||||
Icon *string `json:"icon" bson:"icon,omitempty"` // Icon
|
||||
Status *bool `json:"status" bson:"status,omitempty"` // 上架状态
|
||||
Rank *int64 `json:"rank" bson:"rank,omitempty"` //
|
||||
CategoryType *int `json:"categoryType" bson:"categoryType"` // 导航类型 1=url跳转 2=二级导航 3=底部导航
|
||||
UrlValue *string `json:"urlValue" bson:"urlValue"` // 跳转URL
|
||||
UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"`
|
||||
}
|
||||
type TranslationCategoryUpdateReq struct {
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
Param TranslationCategoryUpdate `json:"param" bson:"param"` // appId,唯一id
|
||||
}
|
||||
|
||||
type TranslationCategoryDelReq struct {
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package srv_translationcategory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/translationcategorymod"
|
||||
"videoweb/pub/mongos"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// GetTranslationCategorys 按照参数搜索视频标签
|
||||
func GetTranslationCategorys(ctx context.Context, param *TranslationCategoryReq) (*TranslationCategoryResp, *srvdef.SrvErr) {
|
||||
filter := bson.M{}
|
||||
opt := &options.FindOptions{}
|
||||
resp := &TranslationCategoryResp{}
|
||||
|
||||
if param.Id > 0 {
|
||||
filter[translationcategorymod.FN.TranslationCategory.Id] = param.Id
|
||||
}
|
||||
if name := strings.TrimSpace(param.Name); name != "" {
|
||||
filter[translationcategorymod.FN.TranslationCategory.Name] = bson.M{"$regex": name}
|
||||
}
|
||||
|
||||
opt.SetSort(bson.M{translationcategorymod.FN.TranslationCategory.Rank: -1})
|
||||
opt.SetSkip((param.PageNum - 1) * param.PageSize).SetLimit(param.PageSize)
|
||||
|
||||
total, err1 := translationcategorymod.CountTranslationCategorys(ctx, filter)
|
||||
if err1 != nil {
|
||||
return resp, err1
|
||||
}
|
||||
resp.Total = total
|
||||
if total == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
ads, err1 := translationcategorymod.FindManyTranslationCategorys(ctx, filter, opt)
|
||||
if err1 != nil {
|
||||
return resp, err1
|
||||
}
|
||||
resp.List = ads
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// AddTranslationCategory 添加一视频标签
|
||||
func AddTranslationCategory(ctx context.Context,
|
||||
ac *translationcategorymod.TranslationCategory) *srvdef.SrvErr {
|
||||
|
||||
ac.Id = mongos.Lid.TransCategory.Get()
|
||||
now := time.Now()
|
||||
ac.CreatedAt = now
|
||||
ac.UpdatedAt = now
|
||||
|
||||
serr := translationcategorymod.InsertOneTranslationCategory(ctx, ac)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelTranslationCategory 删除指定视频标签
|
||||
func DelTranslationCategory(ctx context.Context, id int64) *srvdef.SrvErr {
|
||||
adFilter := bson.M{translationcategorymod.FN.TranslationCategory.Id: id}
|
||||
//删除
|
||||
count, serr := translationcategorymod.DelOneTranslationCategory(ctx, adFilter)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
if count == 0 {
|
||||
return srvdef.New(srvdef.ErrNoData, "", "")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateTranslationCategory 更新指定视频标签
|
||||
func UpdateTranslationCategory(ctx context.Context, id int64, param TranslationCategoryUpdate) *srvdef.SrvErr {
|
||||
now := time.Now()
|
||||
param.UpdatedAt = now
|
||||
|
||||
adFilter := bson.M{translationcategorymod.FN.TranslationCategory.Id: id}
|
||||
updateDoc := bson.M{"$set": param}
|
||||
serr := translationcategorymod.UpdateOneTranslationCategory(ctx, adFilter, updateDoc)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package srv_user
|
||||
|
||||
import (
|
||||
"videoweb/pub/models/usermod"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
type UserListReq struct {
|
||||
pubdef.Page
|
||||
Id *int64 `json:"id"` // 用户ID
|
||||
NickName string `json:"nickName"` // 昵称(模糊)
|
||||
Status *pubdef.UserStatus `json:"status"` // 状态
|
||||
}
|
||||
|
||||
type UserListResp struct {
|
||||
Total int64 `json:"total"`
|
||||
List usermod.UserList `json:"list"`
|
||||
}
|
||||
|
||||
type UserUpdateReq struct {
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
Param usermod.UserUpdate `json:"param"`
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package srv_user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/usermod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// GetUsers 分页查询用户列表
|
||||
func GetUsers(ctx context.Context, req *UserListReq) (*UserListResp, *srvdef.SrvErr) {
|
||||
filter := bson.M{}
|
||||
if req.Id != nil && *req.Id > 0 {
|
||||
filter[usermod.FN.User.Id] = *req.Id
|
||||
}
|
||||
if req.NickName != "" {
|
||||
filter[usermod.FN.User.NickName] = bson.M{"$regex": req.NickName}
|
||||
}
|
||||
if req.Status != nil {
|
||||
filter[usermod.FN.User.Status] = *req.Status
|
||||
}
|
||||
|
||||
opts := options.Find().
|
||||
SetSort(bson.M{usermod.FN.User.Id: -1}).
|
||||
SetSkip((req.PageNum - 1) * req.PageSize).
|
||||
SetLimit(req.PageSize)
|
||||
|
||||
resp := &UserListResp{}
|
||||
total, serr := usermod.CountUsers(ctx, filter)
|
||||
if serr != nil {
|
||||
return resp, serr
|
||||
}
|
||||
resp.Total = total
|
||||
if total == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
list, serr := usermod.FindManyUser(ctx, filter, opts)
|
||||
if serr != nil {
|
||||
return resp, serr
|
||||
}
|
||||
resp.List = list
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// UpdateUser 更新指定用户信息
|
||||
func UpdateUser(ctx context.Context, id int64, param usermod.UserUpdate) *srvdef.SrvErr {
|
||||
now := time.Now()
|
||||
param.UpdatedAt = &now
|
||||
return usermod.UpdateOneUser(ctx,
|
||||
bson.M{usermod.FN.User.Id: id},
|
||||
bson.M{"$set": param},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package srv_videocategory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"videoweb/pub/common/playletcache"
|
||||
"videoweb/pub/common/slugx"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/contenttagmod"
|
||||
"videoweb/pub/models/videocategorymod"
|
||||
"videoweb/pub/mongos"
|
||||
"videoweb/web/service/srv_contenttag"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// GetVideoCategorys 按照参数搜索视频分类
|
||||
func GetVideoCategorys(ctx context.Context, param *VideoCategoryReq) (*VideoCategoryResp, *srvdef.SrvErr) {
|
||||
filter := bson.M{}
|
||||
if param.Id != nil && *param.Id > 0 {
|
||||
filter[videocategorymod.FN.VideoCategory.Id] = *param.Id
|
||||
}
|
||||
if param.Name != nil && *param.Name != "" {
|
||||
filter[videocategorymod.FN.VideoCategory.Name] = bson.M{"$regex": *param.Name}
|
||||
}
|
||||
if param.DisplayPosition != nil {
|
||||
filter[videocategorymod.FN.VideoCategory.DisplayPosition] = *param.DisplayPosition
|
||||
}
|
||||
if param.SortType != nil {
|
||||
filter[videocategorymod.FN.VideoCategory.SortType] = *param.SortType
|
||||
}
|
||||
opt := &options.FindOptions{}
|
||||
resp := &VideoCategoryResp{}
|
||||
|
||||
opt.SetSort(bson.M{videocategorymod.FN.VideoCategory.Rank: -1})
|
||||
opt.SetSkip((param.PageNum - 1) * param.PageSize).SetLimit(param.PageSize)
|
||||
|
||||
total, serr := videocategorymod.CountVideoCategorys(ctx, filter)
|
||||
if serr != nil {
|
||||
return resp, serr
|
||||
}
|
||||
resp.Total = total
|
||||
if total == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
ads, serr := videocategorymod.FindManyVideoCategorys(ctx, filter, opt)
|
||||
if serr != nil {
|
||||
return resp, serr
|
||||
}
|
||||
resp.List = ads
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// AddVideoCategory 添加一个视频分类
|
||||
func AddVideoCategory(ctx context.Context,
|
||||
ac *videocategorymod.VideoCategory) *srvdef.SrvErr {
|
||||
//if ac.Name != "" {
|
||||
// count, serr := videocategorymod.CountVideoCategorys(ctx, bson.M{videocategorymod.FN.VideoCategory.Name: ac.Name})
|
||||
// if serr != nil {
|
||||
// return serr
|
||||
// }
|
||||
// if count > 0 {
|
||||
// return srvdef.New(srvdef.ErrDbInputExist, "视频分类已存在")
|
||||
// }
|
||||
//}
|
||||
|
||||
ac.Id = mongos.Lid.VideoCategory.Get()
|
||||
if ac.Tags == nil {
|
||||
ac.Tags = make([]int64, 0)
|
||||
}
|
||||
tagIds, err := srv_contenttag.NormalizeContentTagIds(ac.Tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := srv_contenttag.ValidateContentTags(ctx, tagIds, contenttagmod.ContentTagTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
ac.Tags = tagIds
|
||||
exists := func(s string) bool {
|
||||
doc, _ := videocategorymod.FindOneVideoCategory(ctx, slugx.Filter(
|
||||
videocategorymod.FN.VideoCategory.Slug, videocategorymod.FN.VideoCategory.Id, s, 0))
|
||||
return doc != nil
|
||||
}
|
||||
if serr := slugx.Ensure(&ac.Slug, utils.SlugSourceFromKeywordsOrTitle(ac.SeoInfo.Keyworld, ac.Name), exists); serr != nil {
|
||||
return serr
|
||||
}
|
||||
now := time.Now()
|
||||
ac.CreatedAt = now
|
||||
ac.UpdatedAt = now
|
||||
|
||||
serr := videocategorymod.InsertOneVideoCategory(ctx, ac)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
playletcache.ClearHomeCache()
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelVideoCategory 删除指定视频分类
|
||||
func DelVideoCategory(ctx context.Context, id int64) *srvdef.SrvErr {
|
||||
adFilter := bson.M{videocategorymod.FN.VideoCategory.Id: id}
|
||||
//删除
|
||||
count, serr := videocategorymod.DelOneVideoCategory(ctx, adFilter)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
if count == 0 {
|
||||
return srvdef.New(srvdef.ErrNoData, "", "")
|
||||
}
|
||||
playletcache.ClearHomeCache()
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateVideoCategory 更新指定视频分类
|
||||
func UpdateVideoCategory(ctx context.Context, id int64, param VideoCategoryUpdate) *srvdef.SrvErr {
|
||||
now := time.Now()
|
||||
param.UpdatedAt = now
|
||||
|
||||
adFilter := bson.M{videocategorymod.FN.VideoCategory.Id: id}
|
||||
current, serr := videocategorymod.FindOneVideoCategory(ctx, adFilter)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
name := current.Name
|
||||
if param.Name != nil {
|
||||
name = *param.Name
|
||||
}
|
||||
keywords := current.SeoInfo.Keyworld
|
||||
if param.SeoInfo != nil {
|
||||
keywords = param.SeoInfo.Keyworld
|
||||
}
|
||||
exists := func(s string) bool {
|
||||
doc, _ := videocategorymod.FindOneVideoCategory(ctx, slugx.Filter(
|
||||
videocategorymod.FN.VideoCategory.Slug, videocategorymod.FN.VideoCategory.Id, s, id))
|
||||
return doc != nil
|
||||
}
|
||||
slug, changed, serr := slugx.PrepareOnUpdate(current.Slug, param.Slug,
|
||||
utils.SlugSourceFromKeywordsOrTitle(keywords, name), exists)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
if changed {
|
||||
param.Slug = &slug
|
||||
} else {
|
||||
param.Slug = nil
|
||||
}
|
||||
if param.Tags != nil {
|
||||
tagIds, err := srv_contenttag.NormalizeContentTagIds(*param.Tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := srv_contenttag.ValidateContentTags(ctx, tagIds, contenttagmod.ContentTagTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
param.Tags = &tagIds
|
||||
}
|
||||
param.IndexNow = nil
|
||||
if param.IsReIndexNow != nil && *param.IsReIndexNow {
|
||||
indexNow := int64(0)
|
||||
param.IndexNow = &indexNow
|
||||
}
|
||||
updateDoc := bson.M{"$set": param}
|
||||
serr = videocategorymod.UpdateOneVideoCategory(ctx, adFilter, updateDoc)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
playletcache.ClearHomeCache()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package srv_videocategory
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/pub/models/videocategorymod"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
type VideoCategoryReq struct {
|
||||
pubdef.Page
|
||||
Id *int64 `json:"id"` // 分类 ID
|
||||
Name *string `json:"name"` // 分类名称(模糊匹配)
|
||||
DisplayPosition *pubdef.CategoryDisplayPosition `json:"displayPosition" bson:"displayPosition"` // 显示位置 0=仅TAB 1=仅首页 2=两者都显示
|
||||
SortType *pubdef.CategorySortType `json:"sortType" bson:"sortType"` // 排序方式
|
||||
}
|
||||
type VideoCategoryResp struct {
|
||||
Total int64 `json:"total"`
|
||||
List videocategorymod.VideoCategoryList `json:"list"`
|
||||
}
|
||||
type VideoCategoryUpdate struct {
|
||||
Name *string `json:"name" bson:"name,omitempty"` // 名称
|
||||
Slug *string `json:"slug" bson:"slug,omitempty"` // slug
|
||||
Title *string `json:"title" bson:"title,omitempty"` // APP显示
|
||||
Desc *string `json:"desc" bson:"desc,omitempty"` // 描述
|
||||
DisplayPosition *pubdef.CategoryDisplayPosition `json:"displayPosition" bson:"displayPosition,omitempty"` // 显示位置 0=仅TAB 1=仅首页 2=两者都显示
|
||||
SortType *pubdef.CategorySortType `json:"sortType" bson:"sortType,omitempty"` // 排序方式
|
||||
Tags *[]int64 `json:"tags" bson:"tags,omitempty"` // 关联视频标签
|
||||
Rank *int64 `json:"rank" bson:"rank,omitempty"` //
|
||||
TranslationCategoryId *int64 `json:"translationCategoryId" bson:"translationCategoryId,omitempty"` // 导航分类
|
||||
SeoInfo *pubdef.SeoValue `json:"seoInfo" bson:"seoInfo,omitempty"` // Seo相关信息
|
||||
Status *bool `json:"status" bson:"status,omitempty"` // 上架状态
|
||||
DisplayMode *pubdef.DisplayMode `json:"displayMode" bson:"displayMode,omitempty"` // 展示方式
|
||||
UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"`
|
||||
IsReIndexNow *bool `json:"isReIndexNow" bson:"isReIndexNow,omitempty"` // 是否重新提交索引
|
||||
IndexNow *int64 `bson:"indexNow,omitempty"`
|
||||
}
|
||||
type VideoCategoryUpdateReq struct {
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
Param VideoCategoryUpdate `json:"param" bson:"param"` // appId,唯一id
|
||||
}
|
||||
|
||||
type VideoCategoryDelReq struct {
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package srv_videocollection
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
type VideoCollectionOptionMediaReq struct {
|
||||
CollectionID int64 `json:"collectionID" bson:"collectionID"` // 合集ID
|
||||
IsAdd bool `json:"isAdd" bson:"isAdd"` // 增加还是移除
|
||||
VideoIds []int64 `json:"videoIds" bson:"videoIds"` // 视频ID
|
||||
}
|
||||
|
||||
type UpdateCollectionMediaRankReq struct {
|
||||
CollectionID int64 `json:"collectionID" bson:"collectionID"` //合集ID
|
||||
VideoId int64 `json:"videoId" bson:"videoId"` // 视频ID
|
||||
Rank int64 `json:"rank" bson:"rank"` // 排序
|
||||
}
|
||||
|
||||
type GetCollectionMediaListReq struct {
|
||||
CollectionID int64 `json:"collectionID" bson:"collectionID"`
|
||||
pubdef.Page
|
||||
}
|
||||
type GetCollectionMediaListResp struct {
|
||||
Total int64 `json:"total" bson:"total"`
|
||||
List []CollectionMediaInfo `json:"list" bson:"list"`
|
||||
}
|
||||
|
||||
type CollectionMediaInfo struct {
|
||||
Id int64 `json:"id" bson:"_id,omitempty"`
|
||||
Title string `json:"title" bson:"title"` // 帖子标题
|
||||
CoverImg string `bson:"coverImg,omitempty" json:"coverImg"` // 封面
|
||||
Desc string `bson:"desc,omitempty" json:"desc"` // 详细描述
|
||||
Remark string `bson:"remark,omitempty" json:"remark"` // 备注
|
||||
Nodes []videoinformationmod.Node `json:"nodes" bson:"nodes"` // 节点
|
||||
Status pubdef.InformationStatus `json:"status" bson:"status"` // 帖子上架状态
|
||||
Video videoinformationmod.VideoInfo `json:"video" bson:"video"` // 视频
|
||||
Rank int64 `json:"rank" bson:"rank"` // 排序
|
||||
UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
|
||||
CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package srv_videocollection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/videocollectionmediamod"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func VideoCollectionOptionMedia(ctx context.Context, req *VideoCollectionOptionMediaReq) *srvdef.SrvErr {
|
||||
_, err := videoinformationmod.FindOneVideoInformation(ctx, bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Id: req.CollectionID,
|
||||
videoinformationmod.FN.VideoInformation.MediaType: videoinformationmod.MediaType_Collection,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.IsAdd {
|
||||
// 追加视频
|
||||
videos, err := videocollectionmediamod.FindManyVideoCollectionMediaModels(ctx, bson.M{
|
||||
videocollectionmediamod.FN.VideoCollectionMediaModel.CollectionID: req.CollectionID,
|
||||
videocollectionmediamod.FN.VideoCollectionMediaModel.VideoId: bson.M{"$in": req.VideoIds},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
existVideoIds := make(map[int64]struct{}, len(videos))
|
||||
for _, video := range videos {
|
||||
existVideoIds[video.VideoId] = struct{}{}
|
||||
}
|
||||
addIds := make([]int64, 0, len(req.VideoIds))
|
||||
for _, id := range req.VideoIds {
|
||||
if _, ok := existVideoIds[id]; !ok {
|
||||
addIds = append(addIds, id)
|
||||
existVideoIds[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
if len(addIds) > 0 {
|
||||
now := time.Now()
|
||||
addInfos := make(videocollectionmediamod.VideoCollectionMediaModelList, 0, len(addIds))
|
||||
for _, id := range addIds {
|
||||
addInfos = append(addInfos, &videocollectionmediamod.VideoCollectionMediaModel{
|
||||
ID: primitive.NewObjectID(),
|
||||
CollectionID: req.CollectionID,
|
||||
VideoId: id,
|
||||
Status: pubdef.InformationStatusVerifyOK,
|
||||
Rank: 0,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
})
|
||||
}
|
||||
err = videocollectionmediamod.InsertManyVideoCollectionMedia(ctx, addInfos)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 移除视频
|
||||
_, err = videocollectionmediamod.DelManyVideoCollectionMediaModel(ctx, bson.M{
|
||||
videocollectionmediamod.FN.VideoCollectionMediaModel.CollectionID: req.CollectionID,
|
||||
videocollectionmediamod.FN.VideoCollectionMediaModel.VideoId: bson.M{"$in": req.VideoIds},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
count, err := videocollectionmediamod.CountVideoCollectionMediaModels(ctx, bson.M{
|
||||
videocollectionmediamod.FN.VideoCollectionMediaModel.CollectionID: req.CollectionID,
|
||||
})
|
||||
if err == nil {
|
||||
_ = videoinformationmod.UpdateOneVideoInformation(ctx, bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Id: req.CollectionID,
|
||||
}, bson.M{"$set": bson.M{
|
||||
videoinformationmod.FN.VideoInformation.VideoCount: count,
|
||||
videoinformationmod.FN.VideoInformation.UpdatedAt: time.Now(),
|
||||
}})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateCollectionMediaRank(ctx context.Context, req *UpdateCollectionMediaRankReq) *srvdef.SrvErr {
|
||||
err := videocollectionmediamod.UpdateOneVideoCollectionMediaModel(ctx, bson.M{
|
||||
videocollectionmediamod.FN.VideoCollectionMediaModel.CollectionID: req.CollectionID,
|
||||
videocollectionmediamod.FN.VideoCollectionMediaModel.VideoId: req.VideoId,
|
||||
}, bson.M{"$set": bson.M{
|
||||
videocollectionmediamod.FN.VideoCollectionMediaModel.Rank: req.Rank,
|
||||
}})
|
||||
return err
|
||||
}
|
||||
|
||||
func GetCollectionMediaList(ctx context.Context, req *GetCollectionMediaListReq) (*GetCollectionMediaListResp, *srvdef.SrvErr) {
|
||||
_, err := videoinformationmod.FindOneVideoInformation(ctx, bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Id: req.CollectionID,
|
||||
videoinformationmod.FN.VideoInformation.MediaType: videoinformationmod.MediaType_Collection,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts := &options.FindOptions{}
|
||||
opts.SetSort(bson.M{videocollectionmediamod.FN.VideoCollectionMediaModel.Rank: 1})
|
||||
opts.SetLimit(req.PageSize)
|
||||
opts.SetSkip((req.PageNum - 1) * req.PageSize)
|
||||
total, err := videocollectionmediamod.CountVideoCollectionMediaModels(ctx, bson.M{
|
||||
videocollectionmediamod.FN.VideoCollectionMediaModel.CollectionID: req.CollectionID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if total == 0 {
|
||||
return &GetCollectionMediaListResp{
|
||||
Total: 0,
|
||||
List: make([]CollectionMediaInfo, 0),
|
||||
}, nil
|
||||
}
|
||||
videos, err := videocollectionmediamod.FindManyVideoCollectionMediaModels(ctx, bson.M{
|
||||
videocollectionmediamod.FN.VideoCollectionMediaModel.CollectionID: req.CollectionID,
|
||||
}, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
videoIds := make([]int64, 0, len(videos))
|
||||
for _, video := range videos {
|
||||
videoIds = append(videoIds, video.VideoId)
|
||||
}
|
||||
videoInfos, err := videoinformationmod.FindManyVideoInformations(ctx, bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Id: bson.M{"$in": videoIds},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
videoInfoMap := make(map[int64]*videoinformationmod.VideoInformation, len(videoInfos))
|
||||
for _, info := range videoInfos {
|
||||
videoInfoMap[info.Id] = info
|
||||
}
|
||||
infos := make([]CollectionMediaInfo, 0, len(videos))
|
||||
for _, video := range videos {
|
||||
info, ok := videoInfoMap[video.VideoId]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
infos = append(infos, CollectionMediaInfo{
|
||||
Id: video.VideoId,
|
||||
Title: info.Title,
|
||||
CoverImg: info.CoverImg,
|
||||
Desc: info.Desc,
|
||||
Remark: info.Remark,
|
||||
Nodes: info.Nodes,
|
||||
Status: info.Status,
|
||||
Video: info.Video,
|
||||
Rank: video.Rank,
|
||||
UpdatedAt: video.UpdatedAt,
|
||||
CreatedAt: video.CreatedAt,
|
||||
})
|
||||
}
|
||||
resp := &GetCollectionMediaListResp{
|
||||
Total: total,
|
||||
List: infos,
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package srv_videocreater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/slugx"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/videocreatermod"
|
||||
"videoweb/pub/mongos"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// GetVideoCreaters 按照参数搜索视频标签
|
||||
func GetVideoCreaters(ctx context.Context, param *VideoCreaterReq) (*VideoCreaterResp, *srvdef.SrvErr) {
|
||||
filter := bson.M{}
|
||||
opt := &options.FindOptions{}
|
||||
resp := &VideoCreaterResp{}
|
||||
|
||||
opt.SetSort(bson.M{videocreatermod.FN.VideoCreaterInfo.Id: 1})
|
||||
opt.SetSkip((param.PageNum - 1) * param.PageSize).SetLimit(param.PageSize)
|
||||
|
||||
total, serr := videocreatermod.CountVideoCreaterInfos(ctx, filter)
|
||||
if serr != nil {
|
||||
return resp, serr
|
||||
}
|
||||
resp.Total = total
|
||||
if total == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
ads, serr := videocreatermod.FindManyVideoCreaterInfos(ctx, filter, opt)
|
||||
if serr != nil {
|
||||
return resp, serr
|
||||
}
|
||||
resp.List = ads
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// AddVideoCreater 添加一视频标签
|
||||
func AddVideoCreater(ctx context.Context,
|
||||
ac *videocreatermod.VideoCreaterInfo) *srvdef.SrvErr {
|
||||
|
||||
ac.Id = mongos.Lid.VideoCreator.Get()
|
||||
exists := func(s string) bool {
|
||||
doc, _ := videocreatermod.FindOneVideoCreaterInfo(ctx, slugx.Filter(
|
||||
videocreatermod.FN.VideoCreaterInfo.Slug, videocreatermod.FN.VideoCreaterInfo.Id, s, 0))
|
||||
return doc != nil
|
||||
}
|
||||
if serr := slugx.Ensure(&ac.Slug, utils.SlugSourceFromKeywordsOrTitle(ac.SeoInfo.Keyworld, ac.Name), exists); serr != nil {
|
||||
return serr
|
||||
}
|
||||
now := time.Now()
|
||||
ac.CreatedAt = now
|
||||
ac.UpdatedAt = now
|
||||
|
||||
log.Info("InsertOneVideoCreaterInfo", log.Any("", *ac))
|
||||
serr := videocreatermod.InsertOneVideoCreaterInfo(ctx, ac)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelVideoCreater 删除指定视频标签
|
||||
func DelVideoCreater(ctx context.Context, id int64) *srvdef.SrvErr {
|
||||
adFilter := bson.M{videocreatermod.FN.VideoCreaterInfo.Id: id}
|
||||
//删除
|
||||
count, serr := videocreatermod.DelOneVideoCreaterInfo(ctx, adFilter)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
if count == 0 {
|
||||
return srvdef.New(srvdef.ErrNoData, "", "")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateVideoCreater 更新指定视频标签
|
||||
func UpdateVideoCreater(ctx context.Context, id int64, param VideoCreaterUpdate) *srvdef.SrvErr {
|
||||
now := time.Now()
|
||||
param.UpdatedAt = now
|
||||
|
||||
adFilter := bson.M{videocreatermod.FN.VideoCreaterInfo.Id: id}
|
||||
current, serr := videocreatermod.FindOneVideoCreaterInfo(ctx, adFilter)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
name := current.Name
|
||||
if param.Name != nil {
|
||||
name = *param.Name
|
||||
}
|
||||
keywords := current.SeoInfo.Keyworld
|
||||
if param.SeoInfo != nil {
|
||||
keywords = param.SeoInfo.Keyworld
|
||||
}
|
||||
exists := func(s string) bool {
|
||||
doc, _ := videocreatermod.FindOneVideoCreaterInfo(ctx, slugx.Filter(
|
||||
videocreatermod.FN.VideoCreaterInfo.Slug, videocreatermod.FN.VideoCreaterInfo.Id, s, id))
|
||||
return doc != nil
|
||||
}
|
||||
slug, changed, serr := slugx.PrepareOnUpdate(current.Slug, param.Slug,
|
||||
utils.SlugSourceFromKeywordsOrTitle(keywords, name), exists)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
if changed {
|
||||
param.Slug = &slug
|
||||
} else {
|
||||
param.Slug = nil
|
||||
}
|
||||
param.IndexNow = nil
|
||||
if param.IsReIndexNow != nil && *param.IsReIndexNow {
|
||||
indexNow := int64(0)
|
||||
param.IndexNow = &indexNow
|
||||
}
|
||||
updateDoc := bson.M{"$set": param}
|
||||
serr = videocreatermod.UpdateOneVideoCreaterInfo(ctx, adFilter, updateDoc)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package srv_videocreater
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/pub/models/videocreatermod"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
type VideoCreaterReq struct {
|
||||
pubdef.Page
|
||||
}
|
||||
type VideoCreaterResp struct {
|
||||
Total int64 `json:"total"`
|
||||
List videocreatermod.VideoCreaterInfoList `json:"list"`
|
||||
}
|
||||
type VideoCreaterUpdate struct {
|
||||
AppId *int32 `json:"appId" bson:"appId,omitempty"` // appId,唯一id
|
||||
Name *string `json:"name" bson:"name,omitempty"` // 名称
|
||||
Slug *string `json:"slug" bson:"slug,omitempty"` // slug
|
||||
Status *bool `json:"status" bson:"status,omitempty"` // 上架状态
|
||||
Sort *int64 `json:"sort" bson:"sort,omitempty"` // 排序
|
||||
SeoInfo *pubdef.SeoValue `json:"seoInfo" bson:"seoInfo,omitempty"` // Seo相关信息
|
||||
UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"`
|
||||
IsReIndexNow *bool `json:"isReIndexNow" bson:"isReIndexNow"` // 是否重新提交索引
|
||||
MediaTypes *[]int64 `json:"mediaTypes" bson:"mediaTypes"` // 作者关联的资源类型 1视频
|
||||
IndexNow *int64 `bson:"indexNow,omitempty" json:"-"`
|
||||
Works *int64 `json:"works" bson:"works,omitempty"` // 作品数量
|
||||
Image *string `json:"image" bson:"image,omitempty"` // 头像
|
||||
Background *string `json:"background" bson:"background,omitempty"` // 背景
|
||||
CreateType *int64 `json:"createType" bson:"createType,omitempty"` // 创作者类型 0=普通 1=官方
|
||||
Desc *string `json:"desc" bson:"desc,omitempty"` // 简介
|
||||
}
|
||||
type VideoCreaterUpdateReq struct {
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
Param VideoCreaterUpdate `json:"param" bson:"param"` // appId,唯一id
|
||||
}
|
||||
|
||||
type VideoCreaterDelReq struct {
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package srv_videoinfo
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
type VideoInfoReq struct {
|
||||
Id *int64 `json:"id"` //
|
||||
ActressIds *[]int64 `json:"actressIds" bson:"actressIds"` // 女优ID
|
||||
Title *string `json:"title"` // 标题
|
||||
Bango *string `json:"bango"` // 番号,
|
||||
CreatorId *int64 `json:"creatorId"` // 作者Id
|
||||
Status *pubdef.InformationStatus `json:"status"` // 帖子上架状态
|
||||
Tag *int64 `json:"tag" ` // 标签
|
||||
IsHot *bool `json:"isHot"` // 是否热门
|
||||
IsRecommend *bool `json:"isRecommend"` // 是否推荐 true-推荐 false-不推荐
|
||||
MediaType *videoinformationmod.MediaType `json:"mediaType"` // 资源类型
|
||||
CreatorName *string `json:"creatorName"` // 作者名称
|
||||
Keyworld *string `json:"keyworld"` // 关键字
|
||||
CategoryId *int64 `json:"categoryId"` // 分类ID
|
||||
VideoTaskId *string `json:"videoTaskId"` // 视频任务id
|
||||
IsTop *bool `json:"isTop" bson:"isTop"` // 是否置顶
|
||||
VideoFileId *string `json:"videoFileId"` // 切片id(切片列表的ID)
|
||||
Sort int64 `json:"sort"` // 排序方式 0=创建时间 1=上架时间 2=更新时间 3=排序值 4=播放次数。按分类查询且排序方式为排序值时,使用该分类的 categoryRanks 排序
|
||||
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 SyncMediaFileReq struct {
|
||||
FileIds []string `json:"fileId"` // 媒资库影片 ID
|
||||
SourceVersion string `json:"sourceVersion,omitempty"` // v1 / v2
|
||||
FirstTagMap map[string]string `json:"firstTagMap,omitempty"` // 覆盖一级标签
|
||||
MinVideoSeconds int32 `json:"minVideoSeconds,omitempty"` // 最短时长,0 表示不限制
|
||||
VideoList []SyncMediaFileItem `json:"videoList,omitempty"` // 媒资库搜索结果,优先使用
|
||||
}
|
||||
|
||||
type SyncMediaFileItem struct {
|
||||
Id string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
FieldNameFs string `json:"fieldNameFs"`
|
||||
FirstTag string `json:"firstTag"`
|
||||
SecondTags []string `json:"secondTags"`
|
||||
VideoTime int32 `json:"videoTime"`
|
||||
CoverImg string `json:"coverImg"`
|
||||
Publish string `json:"publish"`
|
||||
Bango string `json:"bango"`
|
||||
TaskID string `json:"taskId"`
|
||||
Keyworld []string `json:"keyworld"`
|
||||
Width int64 `json:"width"`
|
||||
Height int64 `json:"height"`
|
||||
}
|
||||
|
||||
type SyncMediaFileResp struct {
|
||||
Synced int `json:"synced"`
|
||||
Failed int `json:"failed"`
|
||||
}
|
||||
type VideoInfoUpdate struct {
|
||||
Title *string `json:"title,omitempty" bson:"title,omitempty"` // 帖子标题
|
||||
Slug *string `json:"slug,omitempty" bson:"slug,omitempty"` // slug
|
||||
ActressIds *[]int64 `json:"actressIds" bson:"actressIds,omitempty"` // 女优ID
|
||||
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,omitempty"` //
|
||||
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 *pubdef.InformationStatus `json:"status,omitempty" bson:"status,omitempty"` // 帖子上架状态
|
||||
Sort *int64 `json:"sort,omitempty" bson:"sort,omitempty"` // 排序
|
||||
CategoryId *int64 `json:"categoryId" bson:"categoryId,omitempty"` // 分类ID
|
||||
CategoryRanks *[]videoinformationmod.VideoCategoryRank `json:"categoryRanks" bson:"categoryRanks,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"` // 影片长度
|
||||
|
||||
Tags *[]int64 `json:"tags,omitempty" bson:"tags,omitempty"` // 标签
|
||||
IsHot *bool `json:"isHot,omitempty" bson:"isHot,omitempty"` // 是否热门
|
||||
IsRecommend *bool `json:"isRecommend,omitempty" bson:"isRecommend,omitempty"` // 是否推荐 true-推荐 false-不推荐
|
||||
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"`
|
||||
IssuedAt *time.Time `json:"issuedAt" bson:"issuedAt"` // 发行时间
|
||||
IssuedBrand *string `json:"issuedBrand" bson:"issuedBrand"` // 发行商
|
||||
Bango *string `bson:"bango,omitempty" json:"bango"` // 番号,唯一
|
||||
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"` // 帖子标题
|
||||
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"` // 标签
|
||||
}
|
||||
|
||||
type VideoAddTagsReq struct {
|
||||
VideoIds []int64 `json:"videoIds" binding:"required"` // 视频ID集合
|
||||
Tags []int64 `json:"tags" binding:"required"` // 统一标签ID集合
|
||||
}
|
||||
|
||||
type VideoSetTagsReq struct {
|
||||
Ids []int64 `json:"ids" binding:"required"` // 视频ID集合,传1个表示单条设置,传多个表示批量设置
|
||||
TagIds *[]int64 `json:"tagIds"` // 标签ID集合,可设置多个标签,传空数组表示清空标签,不传表示参数错误
|
||||
}
|
||||
|
||||
type SetFakeLikesReq struct {
|
||||
Ids []int64 `json:"ids"` // 视频ID集合,支持单个或批量
|
||||
LikesMin int64 `json:"likesMin"` // 假点赞数随机最小值,包含该值
|
||||
LikesMax int64 `json:"likesMax"` // 假点赞数随机最大值,包含该值
|
||||
}
|
||||
|
||||
type SetFakeWatchTimesReq struct {
|
||||
Ids []int64 `json:"ids"` // 视频ID集合,支持单个或批量
|
||||
WatchTimesMin int64 `json:"watchTimesMin"` // 假观看次数随机最小值,包含该值
|
||||
WatchTimesMax int64 `json:"watchTimesMax"` // 假观看次数随机最大值,包含该值
|
||||
}
|
||||
|
||||
// SetVideoHeatReq 单/批量设置视频热度
|
||||
type SetVideoHeatReq struct {
|
||||
Ids []int64 `json:"ids"` // 视频ID集合
|
||||
HeatMin int64 `json:"heatMin"` // 热度最小值
|
||||
HeatMax int64 `json:"heatMax"` // 热度最大值
|
||||
}
|
||||
|
||||
type SetRecommendReq struct {
|
||||
Ids []int64 `json:"ids"` // 视频ID集合,传1个表示单条设置,传多个表示批量设置
|
||||
IsRecommend bool `json:"isRecommend"` // 是否推荐 true-推荐 false-不推荐
|
||||
}
|
||||
|
||||
type SetFakeCountResp struct {
|
||||
MatchedCount int64 `json:"matchedCount"` // 匹配到的视频数量
|
||||
ModifiedCount int64 `json:"modifiedCount"` // 实际修改的视频数量
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package srv_videoinfo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/contenttagmod"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
type ExternalVideoListReq struct {
|
||||
pubdef.Page
|
||||
Date string `form:"date"` // 日期筛选,格式:2006-01-02
|
||||
Publisher string `form:"publisher"` // 发布人
|
||||
}
|
||||
|
||||
type ExternalVideoListItem struct {
|
||||
Date string `json:"date"`
|
||||
Publisher string `json:"publisher"`
|
||||
SiteName string `json:"siteName"`
|
||||
Title string `json:"title"`
|
||||
Keyworld []string `json:"keyworld"`
|
||||
Tags []string `json:"tags"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Url string `json:"url"`
|
||||
Content []string `json:"content"`
|
||||
UploadUserName string `json:"uploadUserName"`
|
||||
}
|
||||
|
||||
type ExternalVideoListResp struct {
|
||||
Total int64 `json:"total"`
|
||||
List []ExternalVideoListItem `json:"list"`
|
||||
}
|
||||
|
||||
// SearchExternalVideoList 按日期查询已上架视频,供外部数据接口使用。
|
||||
func SearchExternalVideoList(ctx context.Context, req *ExternalVideoListReq) (*ExternalVideoListResp, *srvdef.SrvErr) {
|
||||
cst := time.FixedZone("CST", 8*3600)
|
||||
|
||||
var dayStart, dayEnd time.Time
|
||||
if req.Date != "" {
|
||||
t, err := time.ParseInLocation("2006-01-02", req.Date, cst)
|
||||
if err == nil {
|
||||
dayStart = t
|
||||
} else {
|
||||
dayStart = time.Now().In(cst).Truncate(24 * time.Hour).Add(-24 * time.Hour)
|
||||
}
|
||||
} else {
|
||||
now := time.Now().In(cst)
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, cst)
|
||||
dayStart = today.Add(-24 * time.Hour)
|
||||
}
|
||||
dayEnd = dayStart.Add(24 * time.Hour)
|
||||
|
||||
filter := bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
|
||||
"$or": bson.A{
|
||||
bson.M{
|
||||
videoinformationmod.FN.VideoInformation.AddedTime: bson.M{
|
||||
"$gte": dayStart.UTC(),
|
||||
"$lt": dayEnd.UTC(),
|
||||
},
|
||||
},
|
||||
bson.M{
|
||||
videoinformationmod.FN.VideoInformation.SecondUpdatedAt: bson.M{
|
||||
"$gte": dayStart.UTC(),
|
||||
"$lt": dayEnd.UTC(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if req.Publisher != "" {
|
||||
filter[videoinformationmod.FN.VideoInformation.UploadUserName] = req.Publisher
|
||||
}
|
||||
|
||||
total, serr := videoinformationmod.CountVideoInformations(ctx, filter)
|
||||
if serr != nil {
|
||||
log.Warn("SearchExternalVideoList CountVideoInformations failed", log.E(serr))
|
||||
return nil, serr
|
||||
}
|
||||
|
||||
pageSize := req.Page.PageSize
|
||||
if pageSize <= 0 {
|
||||
pageSize = 1000
|
||||
}
|
||||
pageNum := req.Page.PageNum
|
||||
if pageNum <= 0 {
|
||||
pageNum = 1
|
||||
}
|
||||
opts := &options.FindOptions{}
|
||||
opts.SetLimit(pageSize)
|
||||
opts.SetSkip((pageNum - 1) * pageSize)
|
||||
opts.SetSort(bson.D{{Key: videoinformationmod.FN.VideoInformation.AddedTime, Value: -1}})
|
||||
list, serr := videoinformationmod.FindManyVideoInformations(ctx, filter, opts)
|
||||
if serr != nil {
|
||||
log.Warn("SearchExternalVideoList FindManyVideoInformations failed", log.E(serr))
|
||||
return nil, serr
|
||||
}
|
||||
|
||||
tagIdSet := make(map[int64]struct{})
|
||||
for _, v := range list {
|
||||
for _, tid := range v.Tags {
|
||||
tagIdSet[tid] = struct{}{}
|
||||
}
|
||||
}
|
||||
tagTitleMap := make(map[int64]string)
|
||||
if len(tagIdSet) > 0 {
|
||||
tagIds := make([]int64, 0, len(tagIdSet))
|
||||
for tid := range tagIdSet {
|
||||
tagIds = append(tagIds, tid)
|
||||
}
|
||||
tags, _ := contenttagmod.GetMapTags(ctx, tagIds)
|
||||
for _, t := range tags {
|
||||
tagTitleMap[t.Id] = t.Name
|
||||
}
|
||||
}
|
||||
|
||||
items := make([]ExternalVideoListItem, 0, len(list))
|
||||
for _, v := range list {
|
||||
tagTitles := make([]string, 0, len(v.Tags))
|
||||
for _, tid := range v.Tags {
|
||||
if title, ok := tagTitleMap[tid]; ok {
|
||||
tagTitles = append(tagTitles, title)
|
||||
}
|
||||
}
|
||||
var content []string
|
||||
for _, node := range v.Nodes {
|
||||
content = append(content, node.Text)
|
||||
}
|
||||
items = append(items, ExternalVideoListItem{
|
||||
Date: utils.GetDateString(v.AddedTime),
|
||||
CreatedAt: utils.GetTimeString(v.CreatedAt),
|
||||
UpdatedAt: utils.GetTimeString(v.SecondUpdatedAt),
|
||||
Publisher: v.OptionAccount,
|
||||
SiteName: pubdef.AppName,
|
||||
Title: v.Title,
|
||||
Keyworld: v.Keyworld,
|
||||
Tags: tagTitles,
|
||||
Url: fmt.Sprintf("/video/detail?id=%d", v.Id),
|
||||
Content: content,
|
||||
UploadUserName: v.UploadUserName,
|
||||
})
|
||||
}
|
||||
return &ExternalVideoListResp{Total: total, List: items}, nil
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
package srv_videoinfo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/playletcache"
|
||||
"videoweb/pub/common/slugx"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/playletchaptermod"
|
||||
"videoweb/pub/models/videocreatermod"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
"videoweb/pub/mongos"
|
||||
"videoweb/pub/storage/proxy"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func videoSyncKey(media proxy.MediaResp) string {
|
||||
if !media.MediaSourceId.IsZero() {
|
||||
return media.MediaSourceId.Hex()
|
||||
}
|
||||
return media.Id.Hex()
|
||||
}
|
||||
|
||||
func buildVideoSyncFilter(media proxy.MediaResp) bson.M {
|
||||
keys := []string{media.Id.Hex()}
|
||||
if !media.MediaSourceId.IsZero() {
|
||||
msid := media.MediaSourceId.Hex()
|
||||
if msid != keys[0] {
|
||||
keys = append(keys, msid)
|
||||
}
|
||||
}
|
||||
or := make([]bson.M, 0, len(keys)*2)
|
||||
for _, key := range keys {
|
||||
or = append(or,
|
||||
bson.M{videoinformationmod.FN.VideoInformation.Video.FileId: key},
|
||||
bson.M{videoinformationmod.FN.VideoInformation.SyncId: key},
|
||||
)
|
||||
}
|
||||
return bson.M{"$or": or}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
// 同步时不再自动写 video_taginfo,标签由后台统一标签绑定维护
|
||||
syncKey := videoSyncKey(media)
|
||||
filter := buildVideoSyncFilter(media)
|
||||
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 serr
|
||||
}
|
||||
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,
|
||||
MediaTypes: []int64{1},
|
||||
Status: true,
|
||||
Sort: 0,
|
||||
UpdatedAt: time.Now(),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
createrExists := func(s string) bool {
|
||||
doc, _ := videocreatermod.FindOneVideoCreaterInfo(ctx, slugx.Filter(
|
||||
videocreatermod.FN.VideoCreaterInfo.Slug, videocreatermod.FN.VideoCreaterInfo.Id, s, 0))
|
||||
return doc != nil
|
||||
}
|
||||
if serr = slugx.Ensure(&creater.Slug, utils.SlugSourceFromKeywordsOrTitle(nil, creater.Name), createrExists); serr != nil {
|
||||
return serr
|
||||
}
|
||||
serr = videocreatermod.InsertOneVideoCreaterInfo(ctx, creater)
|
||||
if serr != nil {
|
||||
log.Error("VideoSync.InsertOneVideoCreaterInfo", log.Any("media", media), log.E(serr))
|
||||
return serr
|
||||
}
|
||||
} else {
|
||||
isFind := false
|
||||
for _, v := range creater.MediaTypes {
|
||||
if v == 1 {
|
||||
isFind = true
|
||||
}
|
||||
}
|
||||
if !isFind {
|
||||
mediaTypes := append(creater.MediaTypes, 1)
|
||||
_ = videocreatermod.UpdateOneVideoCreaterInfo(ctx, bson.M{videocreatermod.FN.VideoCreaterInfo.Id: creater.Id}, bson.M{
|
||||
"$set": bson.M{videocreatermod.FN.VideoCreaterInfo.MediaTypes: mediaTypes},
|
||||
})
|
||||
}
|
||||
}
|
||||
videoInfo.CreatorId = creater.Id
|
||||
videoInfo.CreatorName = creater.Name
|
||||
}
|
||||
videoInfo.IndexNow = 0
|
||||
applyDefaultFakeFlow(&videoInfo.Fake, rand.New(rand.NewSource(time.Now().UnixNano())))
|
||||
exists := func(s string) bool {
|
||||
doc, _ := videoinformationmod.FindOneVideoInformation(ctx, slugx.Filter(
|
||||
videoinformationmod.FN.VideoInformation.Slug, videoinformationmod.FN.VideoInformation.Id, s, 0))
|
||||
return doc != nil
|
||||
}
|
||||
if serr = slugx.Ensure(&videoInfo.Slug, utils.SlugSourceFromKeywordsOrTitle(videoInfo.Keyworld, videoInfo.Title), exists); serr != nil {
|
||||
return serr
|
||||
}
|
||||
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 {
|
||||
mediaNodeMap := make(map[string]videoinformationmod.Node, len(media.Nodes))
|
||||
for _, node := range media.Nodes {
|
||||
if node.NodeId != "" {
|
||||
mediaNodeMap[node.NodeId] = node
|
||||
}
|
||||
}
|
||||
// 更新节点里面图片水印 以本地视频列表的节点为主
|
||||
for _, node := range videoInfo.Nodes {
|
||||
if mediaNode, ok := mediaNodeMap[node.NodeId]; ok {
|
||||
node.Images = mediaNode.Images
|
||||
}
|
||||
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.IssuedBrand: media.IssuedBrand,
|
||||
videoinformationmod.FN.VideoInformation.IssuedAt: media.IssuedAt,
|
||||
}
|
||||
for k, v := range defaultFakeFlowSetDoc(rand.New(rand.NewSource(time.Now().UnixNano()))) {
|
||||
setDoc[k] = v
|
||||
}
|
||||
setDoc[videoinformationmod.FN.VideoInformation.SyncId] = syncKey
|
||||
if len(media.FieldNameFs) > 0 {
|
||||
video := videoinformationmod.VideoInfo{
|
||||
TaskId: media.TaskID,
|
||||
FileId: syncKey,
|
||||
Url: media.FieldNameFs,
|
||||
Cover: media.CoverImg,
|
||||
Height: media.Height,
|
||||
Width: media.Width,
|
||||
VideoTime: media.VideoTime,
|
||||
}
|
||||
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
|
||||
}
|
||||
if preMp4 := strings.TrimSpace(media.PreMp4FileNameFs); preMp4 != "" {
|
||||
setDoc[videoinformationmod.FN.VideoInformation.Video.PreMp4FileNameFs] = preMp4
|
||||
}
|
||||
if unencryptedPreMp4 := strings.TrimSpace(media.UnencryptedPreMp4); unencryptedPreMp4 != "" {
|
||||
setDoc[videoinformationmod.FN.VideoInformation.Video.UnencryptedPreMp4] = unencryptedPreMp4
|
||||
}
|
||||
if videoInfo.Slug == "" {
|
||||
exists := func(s string) bool {
|
||||
doc, _ := videoinformationmod.FindOneVideoInformation(ctx, slugx.Filter(
|
||||
videoinformationmod.FN.VideoInformation.Slug, videoinformationmod.FN.VideoInformation.Id, s, videoInfo.Id))
|
||||
return doc != nil
|
||||
}
|
||||
slug := ""
|
||||
if serr = slugx.Ensure(&slug, utils.SlugSourceFromKeywordsOrTitle(media.Keyworld, videoInfo.Title), exists); serr != nil {
|
||||
return serr
|
||||
}
|
||||
setDoc[videoinformationmod.FN.VideoInformation.Slug] = slug
|
||||
}
|
||||
serr = videoinformationmod.UpdateOneVideoInformation(ctx, bson.M{videoinformationmod.FN.VideoInformation.Id: videoInfo.Id}, bson.M{"$set": setDoc})
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
}
|
||||
playletcache.ClearHomeCache()
|
||||
_ = syncNodeVideo(ctx, media)
|
||||
return nil
|
||||
}
|
||||
|
||||
// syncNodeVideo 同步节点里的视频
|
||||
func syncNodeVideo(ctx context.Context, media proxy.MediaResp) *srvdef.SrvErr {
|
||||
mediaID := videoSyncKey(media)
|
||||
nodeIDs := []string{mediaID}
|
||||
if !media.MediaSourceId.IsZero() {
|
||||
msid := media.MediaSourceId.Hex()
|
||||
if msid != mediaID {
|
||||
nodeIDs = append(nodeIDs, msid)
|
||||
}
|
||||
}
|
||||
if media.Id.Hex() != mediaID && media.Id.Hex() != "" {
|
||||
nodeIDs = append(nodeIDs, media.Id.Hex())
|
||||
}
|
||||
filter := bson.M{"nodes.nodeId": bson.M{"$in": nodeIDs}}
|
||||
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 {
|
||||
writeModels := make([]mongo.WriteModel, 0, len(videoInfo))
|
||||
now := time.Now()
|
||||
// 更新视频路径
|
||||
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 == mediaID || containsString(nodeIDs, v.FileId) {
|
||||
node.Videos[j].Url = media.FieldNameFs
|
||||
isFind = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if isFind {
|
||||
video.Nodes[i] = node
|
||||
break
|
||||
}
|
||||
}
|
||||
if isFind {
|
||||
writeModels = append(writeModels, mongo.NewUpdateOneModel().
|
||||
SetFilter(bson.M{videoinformationmod.FN.VideoInformation.Id: video.Id}).
|
||||
SetUpdate(bson.M{"$set": bson.M{
|
||||
videoinformationmod.FN.VideoInformation.UpdatedAt: now,
|
||||
videoinformationmod.FN.VideoInformation.Nodes: video.Nodes,
|
||||
}}))
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(writeModels) > 0 {
|
||||
_, serr = videoinformationmod.BulkWriteVideoInformation(ctx, writeModels, options.BulkWrite().SetOrdered(false))
|
||||
if serr != nil {
|
||||
log.Warn("syncNodeVideo BulkWriteVideoInformation failed", log.Any("filter", filter), log.E(serr))
|
||||
return serr
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func containsString(list []string, target string) bool {
|
||||
for _, item := range list {
|
||||
if item == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
package srv_videoinfo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// UnmarshalJSON 兼容 id 使用数字字符串,以及 param 里的表单写法。
|
||||
func (r *VideoInfoUpdateReq) UnmarshalJSON(data []byte) error {
|
||||
data = bytes.TrimSpace(data)
|
||||
if len(data) == 0 || bytes.Equal(data, []byte("null")) {
|
||||
return nil
|
||||
}
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
if value, ok := raw["id"]; ok {
|
||||
next, keep, err := normalizeIntRaw(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("id %w", err)
|
||||
}
|
||||
if keep && next != nil && !bytes.Equal(bytes.TrimSpace(next), []byte("null")) {
|
||||
if err := json.Unmarshal(next, &r.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if value, ok := raw["param"]; ok && !bytes.Equal(bytes.TrimSpace(value), []byte("null")) {
|
||||
if err := json.Unmarshal(value, &r.Param); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON 兼容后台表单里的空字符串、数字字符串,以及分类排序的多种写法。
|
||||
func (v *VideoInfoUpdate) UnmarshalJSON(data []byte) error {
|
||||
data = bytes.TrimSpace(data)
|
||||
if len(data) == 0 || bytes.Equal(data, []byte("null")) {
|
||||
return nil
|
||||
}
|
||||
normalized, err := normalizeVideoUpdateJSON(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
type alias VideoInfoUpdate
|
||||
var out alias
|
||||
if err := json.Unmarshal(normalized, &out); err != nil {
|
||||
return err
|
||||
}
|
||||
*v = VideoInfoUpdate(out)
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeVideoUpdateJSON(data []byte) ([]byte, error) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, key := range []string{"addedTime", "updatedAt", "issuedAt", "secondUpdatedAt"} {
|
||||
value, ok := raw[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
next, err := normalizeTimeRaw(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if next == nil {
|
||||
delete(raw, key)
|
||||
continue
|
||||
}
|
||||
raw[key] = next
|
||||
}
|
||||
for _, key := range []string{"categoryId", "sort", "creatorId", "height", "width", "videoTime", "indexNow", "mediaType", "status"} {
|
||||
value, ok := raw[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
next, keep, err := normalizeIntRaw(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s %w", key, err)
|
||||
}
|
||||
if !keep {
|
||||
continue
|
||||
}
|
||||
if next == nil {
|
||||
delete(raw, key)
|
||||
continue
|
||||
}
|
||||
raw[key] = next
|
||||
}
|
||||
for _, key := range []string{"actressIds", "tags"} {
|
||||
value, ok := raw[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
next, keep, err := normalizeIntSliceRaw(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s %w", key, err)
|
||||
}
|
||||
if !keep {
|
||||
continue
|
||||
}
|
||||
if next == nil {
|
||||
delete(raw, key)
|
||||
continue
|
||||
}
|
||||
raw[key] = next
|
||||
}
|
||||
if value, ok := raw["categoryRanks"]; ok {
|
||||
next, keep, err := normalizeCategoryRanksRaw(value, raw["categoryId"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !keep {
|
||||
delete(raw, "categoryRanks")
|
||||
} else {
|
||||
raw["categoryRanks"] = next
|
||||
}
|
||||
}
|
||||
return json.Marshal(raw)
|
||||
}
|
||||
|
||||
func normalizeTimeRaw(raw json.RawMessage) (json.RawMessage, error) {
|
||||
raw = bytes.TrimSpace(raw)
|
||||
if len(raw) == 0 || bytes.Equal(raw, []byte("null")) {
|
||||
return raw, nil
|
||||
}
|
||||
var text string
|
||||
if err := json.Unmarshal(raw, &text); err != nil {
|
||||
return nil, fmt.Errorf("时间格式错误")
|
||||
}
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" {
|
||||
return nil, nil
|
||||
}
|
||||
parsed, err := parseFlexTime(text)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(parsed)
|
||||
}
|
||||
|
||||
func parseFlexTime(text string) (time.Time, error) {
|
||||
if parsed, err := time.Parse(time.RFC3339Nano, text); err == nil {
|
||||
return parsed, nil
|
||||
}
|
||||
if parsed, err := time.Parse(time.RFC3339, text); err == nil {
|
||||
return parsed, nil
|
||||
}
|
||||
for _, layout := range []string{"2006-01-02 15:04:05", "2006-01-02T15:04:05", "2006-01-02"} {
|
||||
if parsed, err := time.ParseInLocation(layout, text, time.Local); err == nil {
|
||||
return parsed, nil
|
||||
}
|
||||
}
|
||||
return time.Time{}, fmt.Errorf("时间格式错误: %s", text)
|
||||
}
|
||||
|
||||
// normalizeIntRaw 把数字或数字字符串整理成 JSON 数字。空字符串表示删除该字段。
|
||||
func normalizeIntRaw(raw json.RawMessage) (json.RawMessage, bool, error) {
|
||||
raw = bytes.TrimSpace(raw)
|
||||
if len(raw) == 0 {
|
||||
return nil, false, nil
|
||||
}
|
||||
if bytes.Equal(raw, []byte("null")) {
|
||||
return raw, true, nil
|
||||
}
|
||||
if raw[0] != '"' {
|
||||
var number json.Number
|
||||
if err := json.Unmarshal(raw, &number); err != nil {
|
||||
return nil, false, fmt.Errorf("数字格式错误")
|
||||
}
|
||||
if _, err := number.Int64(); err != nil {
|
||||
return nil, false, fmt.Errorf("数字格式错误")
|
||||
}
|
||||
return raw, true, nil
|
||||
}
|
||||
var text string
|
||||
if err := json.Unmarshal(raw, &text); err != nil {
|
||||
return nil, false, fmt.Errorf("数字格式错误")
|
||||
}
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" {
|
||||
return nil, true, nil
|
||||
}
|
||||
value, err := strconv.ParseInt(text, 10, 64)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("数字格式错误: %s", text)
|
||||
}
|
||||
encoded, err := json.Marshal(value)
|
||||
return encoded, true, err
|
||||
}
|
||||
|
||||
func normalizeIntSliceRaw(raw json.RawMessage) (json.RawMessage, bool, error) {
|
||||
raw = bytes.TrimSpace(raw)
|
||||
if len(raw) == 0 || bytes.Equal(raw, []byte("null")) {
|
||||
return raw, true, nil
|
||||
}
|
||||
if bytes.Equal(raw, []byte(`""`)) {
|
||||
return nil, true, nil
|
||||
}
|
||||
if raw[0] != '[' {
|
||||
return nil, false, fmt.Errorf("数组格式错误")
|
||||
}
|
||||
var items []json.RawMessage
|
||||
if err := json.Unmarshal(raw, &items); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
values := make([]int64, 0, len(items))
|
||||
for _, item := range items {
|
||||
next, keep, err := normalizeIntRaw(item)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if !keep || next == nil || bytes.Equal(bytes.TrimSpace(next), []byte("null")) {
|
||||
return nil, false, fmt.Errorf("数字格式错误")
|
||||
}
|
||||
var value int64
|
||||
if err := json.Unmarshal(next, &value); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
values = append(values, value)
|
||||
}
|
||||
encoded, err := json.Marshal(values)
|
||||
return encoded, true, err
|
||||
}
|
||||
|
||||
func normalizeCategoryRanksRaw(raw, categoryIDRaw json.RawMessage) (json.RawMessage, bool, error) {
|
||||
raw = bytes.TrimSpace(raw)
|
||||
if len(raw) == 0 || bytes.Equal(raw, []byte("null")) || bytes.Equal(raw, []byte(`""`)) {
|
||||
return nil, false, nil
|
||||
}
|
||||
fallbackID := int64FromRaw(categoryIDRaw)
|
||||
if raw[0] != '[' {
|
||||
item, ok, err := normalizeCategoryRankItem(raw, fallbackID)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, false, nil
|
||||
}
|
||||
encoded, err := json.Marshal([]categoryRankJSON{item})
|
||||
return encoded, true, err
|
||||
}
|
||||
var items []json.RawMessage
|
||||
if err := json.Unmarshal(raw, &items); err != nil {
|
||||
return nil, false, fmt.Errorf("分类排序格式错误")
|
||||
}
|
||||
if len(items) == 0 {
|
||||
encoded, err := json.Marshal([]categoryRankJSON{})
|
||||
return encoded, true, err
|
||||
}
|
||||
cleaned := make([]categoryRankJSON, 0, len(items))
|
||||
for _, item := range items {
|
||||
rank, ok, err := normalizeCategoryRankItem(item, fallbackID)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
cleaned = append(cleaned, rank)
|
||||
}
|
||||
if len(cleaned) == 0 {
|
||||
return nil, false, nil
|
||||
}
|
||||
encoded, err := json.Marshal(cleaned)
|
||||
return encoded, true, err
|
||||
}
|
||||
|
||||
type categoryRankJSON struct {
|
||||
Id int64 `json:"id"`
|
||||
Rank int64 `json:"rank"`
|
||||
}
|
||||
|
||||
func normalizeCategoryRankItem(raw json.RawMessage, fallbackID int64) (categoryRankJSON, bool, error) {
|
||||
raw = bytes.TrimSpace(raw)
|
||||
if len(raw) == 0 || bytes.Equal(raw, []byte("null")) {
|
||||
return categoryRankJSON{}, false, nil
|
||||
}
|
||||
if raw[0] != '{' {
|
||||
if bytes.Equal(raw, []byte("null")) || bytes.Equal(raw, []byte(`""`)) || rawIsZero(raw) {
|
||||
return categoryRankJSON{}, false, nil
|
||||
}
|
||||
rank := int64FromRaw(raw)
|
||||
if rank == 0 {
|
||||
return categoryRankJSON{}, false, fmt.Errorf("分类排序格式错误")
|
||||
}
|
||||
if fallbackID <= 0 {
|
||||
return categoryRankJSON{}, false, fmt.Errorf("分类排序缺少分类ID")
|
||||
}
|
||||
return categoryRankJSON{Id: fallbackID, Rank: rank}, true, nil
|
||||
}
|
||||
var fields map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &fields); err != nil {
|
||||
return categoryRankJSON{}, false, fmt.Errorf("分类排序格式错误")
|
||||
}
|
||||
id := int64FromRaw(fields["id"])
|
||||
if id <= 0 {
|
||||
id = int64FromRaw(fields["categoryId"])
|
||||
}
|
||||
rank := int64FromRaw(fields["rank"])
|
||||
if id <= 0 && !rawHasValue(fields["rank"]) {
|
||||
return categoryRankJSON{}, false, nil
|
||||
}
|
||||
if id <= 0 {
|
||||
id = fallbackID
|
||||
}
|
||||
if id <= 0 {
|
||||
return categoryRankJSON{}, false, fmt.Errorf("分类排序缺少分类ID")
|
||||
}
|
||||
return categoryRankJSON{Id: id, Rank: rank}, true, nil
|
||||
}
|
||||
|
||||
func rawHasValue(raw json.RawMessage) bool {
|
||||
raw = bytes.TrimSpace(raw)
|
||||
return len(raw) > 0 && !bytes.Equal(raw, []byte("null")) && !bytes.Equal(raw, []byte(`""`))
|
||||
}
|
||||
|
||||
func int64FromRaw(raw json.RawMessage) int64 {
|
||||
next, keep, err := normalizeIntRaw(raw)
|
||||
if err != nil || !keep || next == nil || bytes.Equal(bytes.TrimSpace(next), []byte("null")) {
|
||||
return 0
|
||||
}
|
||||
var value int64
|
||||
if err := json.Unmarshal(next, &value); err != nil {
|
||||
return 0
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func rawIsZero(raw json.RawMessage) bool {
|
||||
next, keep, err := normalizeIntRaw(raw)
|
||||
return err == nil && keep && bytes.Equal(bytes.TrimSpace(next), []byte("0"))
|
||||
}
|
||||
@@ -0,0 +1,992 @@
|
||||
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/slugx"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/actress"
|
||||
"videoweb/pub/models/contenttagmod"
|
||||
"videoweb/pub/models/videocategorymod"
|
||||
"videoweb/pub/models/videocollectionmediamod"
|
||||
"videoweb/pub/models/videocreatermod"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
"videoweb/pub/mongos"
|
||||
"videoweb/pub/pubdef"
|
||||
"videoweb/pub/storage"
|
||||
"videoweb/pub/storage/proxy"
|
||||
"videoweb/web/service/srv_contenttag"
|
||||
"videoweb/web/service/srv_report"
|
||||
"videoweb/web/service/srv_seopush"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"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{}
|
||||
if param.MediaType != nil {
|
||||
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.Bango != nil && *param.Bango != "" {
|
||||
filter[videoinformationmod.FN.VideoInformation.Bango] = bson.M{"$regex": *param.Bango}
|
||||
}
|
||||
if param.CreatorId != nil && *param.CreatorId > 0 {
|
||||
filter[videoinformationmod.FN.VideoInformation.CreatorId] = *param.CreatorId
|
||||
}
|
||||
if param.Status != nil {
|
||||
filter[videoinformationmod.FN.VideoInformation.Status] = *param.Status
|
||||
}
|
||||
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.IsRecommend != nil {
|
||||
filter[videoinformationmod.FN.VideoInformation.IsRecommend] = *param.IsRecommend
|
||||
}
|
||||
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.CategoryId != nil && *param.CategoryId > 0 {
|
||||
filter[videoinformationmod.FN.VideoInformation.CategoryId] = *param.CategoryId
|
||||
}
|
||||
if param.IsTop != nil {
|
||||
filter[videoinformationmod.FN.VideoInformation.IsTop] = *param.IsTop
|
||||
}
|
||||
if param.ActressIds != nil && len(*param.ActressIds) > 0 {
|
||||
filter[videoinformationmod.FN.VideoInformation.ActressIds] = bson.M{"$in": *param.ActressIds}
|
||||
}
|
||||
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
|
||||
}
|
||||
var ads videoinformationmod.VideoInformationList
|
||||
if param.Sort == 3 && param.CategoryId != nil && *param.CategoryId > 0 {
|
||||
ads, serr = videoinformationmod.FindManyVideoByCategoryRank(ctx, filter, *param.CategoryId, (param.PageNum-1)*param.PageSize, param.PageSize, nil)
|
||||
} else {
|
||||
ads, serr = videoinformationmod.FindManyVideoInformations(ctx, filter, opt)
|
||||
}
|
||||
if serr != nil {
|
||||
return resp, serr
|
||||
}
|
||||
resp.List = ads
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// SetVideoRecommend 单/批量设置视频是否推荐
|
||||
func SetVideoRecommend(ctx context.Context, req *SetRecommendReq) *srvdef.SrvErr {
|
||||
if req == nil || len(req.Ids) == 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "ids不能为空")
|
||||
}
|
||||
ids, err := normalizeFakeFlowIds(req.Ids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = videoinformationmod.UpdateManyVideoInformation(ctx, bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Id: bson.M{"$in": ids},
|
||||
}, bson.M{
|
||||
"$set": bson.M{
|
||||
videoinformationmod.FN.VideoInformation.IsRecommend: req.IsRecommend,
|
||||
videoinformationmod.FN.VideoInformation.UpdatedAt: time.Now(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
playletcache.ClearHomeCache()
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetVideoFakeLikes 单/批量随机设置视频假点赞数
|
||||
func SetVideoFakeLikes(ctx context.Context, req *SetFakeLikesReq) (*SetFakeCountResp, *srvdef.SrvErr) {
|
||||
if req == nil || len(req.Ids) == 0 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "ids不能为空")
|
||||
}
|
||||
if req.LikesMin < 0 || req.LikesMax < req.LikesMin {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "点赞数区间无效")
|
||||
}
|
||||
ids, serr := normalizeFakeFlowIds(req.Ids)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
return setVideoFakeField(ctx, ids, videoinformationmod.FN.VideoInformation.Fake.Likes, req.LikesMin, req.LikesMax)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// SetVideoHeat 单/批量随机设置视频热度
|
||||
func SetVideoHeat(ctx context.Context, req *SetVideoHeatReq) (*SetFakeCountResp, *srvdef.SrvErr) {
|
||||
if req == nil || len(req.Ids) == 0 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "ids不能为空")
|
||||
}
|
||||
if req.HeatMin < 0 || req.HeatMax < req.HeatMin {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "热度区间无效")
|
||||
}
|
||||
ids, err := normalizeFakeFlowIds(req.Ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return setVideoFakeField(ctx, ids, videoinformationmod.FN.VideoInformation.HeatValue, req.HeatMin, req.HeatMax)
|
||||
}
|
||||
|
||||
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, "", "")
|
||||
}
|
||||
playletcache.ClearHomeCache()
|
||||
return &SetFakeCountResp{MatchedCount: result.MatchedCount, ModifiedCount: result.ModifiedCount}, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const (
|
||||
defaultFakeFlowMin = int64(1000)
|
||||
defaultFakeFlowMax = int64(10000)
|
||||
)
|
||||
|
||||
func randomDefaultFakeFlowPair(r *rand.Rand) (watchTimes, likes int64) {
|
||||
if r == nil {
|
||||
r = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
}
|
||||
likes = randomFakeFlowValue(r, defaultFakeFlowMin, defaultFakeFlowMax-1)
|
||||
watchTimes = randomFakeFlowValue(r, likes+1, defaultFakeFlowMax)
|
||||
return watchTimes, likes
|
||||
}
|
||||
|
||||
func applyDefaultFakeFlow(fake *pubdef.FakeFlow, r *rand.Rand) {
|
||||
if fake == nil {
|
||||
return
|
||||
}
|
||||
fake.WatchTimes, fake.Likes = randomDefaultFakeFlowPair(r)
|
||||
}
|
||||
|
||||
func defaultFakeFlowSetDoc(r *rand.Rand) bson.M {
|
||||
watchTimes, likes := randomDefaultFakeFlowPair(r)
|
||||
return bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Fake.WatchTimes: watchTimes,
|
||||
videoinformationmod.FN.VideoInformation.Fake.Likes: likes,
|
||||
}
|
||||
}
|
||||
|
||||
const fillZeroFakeFlowBatchSize = 500
|
||||
|
||||
func zeroFakeFlowFilter() bson.M {
|
||||
return bson.M{
|
||||
"$or": []bson.M{
|
||||
{
|
||||
"$expr": bson.M{
|
||||
"$lt": []interface{}{
|
||||
bson.M{"$add": []interface{}{"$real.watchTimes", "$fake.watchTimes"}},
|
||||
defaultFakeFlowMin,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"$expr": bson.M{
|
||||
"$lt": []interface{}{
|
||||
bson.M{"$add": []interface{}{"$real.likes", "$fake.likes"}},
|
||||
defaultFakeFlowMin,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// FillZeroVideoFakeFlow 为播放量或收藏总量小于 1000 的视频随机设置 fake 播放量和收藏数(1000-10000,播放量大于收藏量)。
|
||||
func FillZeroVideoFakeFlow(ctx context.Context) (*SetFakeCountResp, *srvdef.SrvErr) {
|
||||
filter := zeroFakeFlowFilter()
|
||||
opt := options.Find().
|
||||
SetProjection(bson.M{videoinformationmod.FN.VideoInformation.Id: 1}).
|
||||
SetBatchSize(fillZeroFakeFlowBatchSize).
|
||||
SetLimit(fillZeroFakeFlowBatchSize)
|
||||
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
resp := &SetFakeCountResp{}
|
||||
var lastID int64
|
||||
|
||||
for {
|
||||
pageFilter := filter
|
||||
if lastID > 0 {
|
||||
pageFilter = bson.M{
|
||||
"$and": []bson.M{
|
||||
filter,
|
||||
{videoinformationmod.FN.VideoInformation.Id: bson.M{"$gt": lastID}},
|
||||
},
|
||||
}
|
||||
}
|
||||
findOpt := opt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.Id: 1})
|
||||
list, serr := videoinformationmod.FindManyVideoInformations(ctx, pageFilter, findOpt)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
if len(list) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
docs := make([]mongo.WriteModel, 0, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil || item.Id <= 0 {
|
||||
continue
|
||||
}
|
||||
lastID = item.Id
|
||||
resp.MatchedCount++
|
||||
docs = append(docs, mongo.NewUpdateOneModel().
|
||||
SetFilter(bson.M{videoinformationmod.FN.VideoInformation.Id: item.Id}).
|
||||
SetUpdate(bson.M{"$set": defaultFakeFlowSetDoc(r)}))
|
||||
}
|
||||
if len(docs) == 0 {
|
||||
break
|
||||
}
|
||||
result, err := videoinformationmod.BulkWriteVideoInformation(ctx, docs, options.BulkWrite().SetOrdered(false))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.ModifiedCount += result.ModifiedCount
|
||||
if len(list) < fillZeroFakeFlowBatchSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
if resp.ModifiedCount > 0 {
|
||||
playletcache.ClearHomeCache()
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func randomFakeFlowValue(r *rand.Rand, min, max int64) int64 {
|
||||
if min == max {
|
||||
return min
|
||||
}
|
||||
return min + r.Int63n(max-min+1)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
se := SyncOneVideo(ctx, media)
|
||||
if se != nil {
|
||||
log.Warn("SyncVideoInfo SYncOneVideo failed", log.Any("filter", req.FileIds), log.E(se))
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mediaFileDomain(sourceVersion string) string {
|
||||
if sourceVersion == "v1" {
|
||||
return proxy.BucketVideoAddressV1
|
||||
}
|
||||
return proxy.BucketVideoAddressV2
|
||||
}
|
||||
|
||||
func buildMediaRespFromSyncItem(item SyncMediaFileItem) (proxy.MediaResp, error) {
|
||||
oid, err := primitive.ObjectIDFromHex(item.Id)
|
||||
if err != nil {
|
||||
return proxy.MediaResp{}, err
|
||||
}
|
||||
return proxy.MediaResp{
|
||||
Id: oid,
|
||||
MediaSourceId: oid,
|
||||
MediaType: 2,
|
||||
Title: item.Title,
|
||||
FieldNameFs: item.FieldNameFs,
|
||||
FirstTag: item.FirstTag,
|
||||
SecondTags: item.SecondTags,
|
||||
Keyworld: item.Keyworld,
|
||||
VideoTime: item.VideoTime,
|
||||
CoverImg: item.CoverImg,
|
||||
Publish: item.Publish,
|
||||
Bango: item.Bango,
|
||||
TaskID: item.TaskID,
|
||||
Width: item.Width,
|
||||
Height: item.Height,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SyncVideoFromMediaFile 从媒资库直同步到视频列表(不经过切片任务)。
|
||||
func SyncVideoFromMediaFile(ctx context.Context, req *SyncMediaFileReq) (*SyncMediaFileResp, *srvdef.SrvErr) {
|
||||
result := &SyncMediaFileResp{}
|
||||
if req == nil {
|
||||
return result, nil
|
||||
}
|
||||
minVideoSeconds := req.MinVideoSeconds
|
||||
|
||||
syncOne := func(id string, media proxy.MediaResp) {
|
||||
if media.Id.IsZero() {
|
||||
result.Failed++
|
||||
return
|
||||
}
|
||||
if minVideoSeconds > 0 && media.VideoTime > 0 && media.VideoTime <= minVideoSeconds {
|
||||
log.Info("SyncVideoFromMediaFile skip short video",
|
||||
log.Any("id", id),
|
||||
log.Any("videoTime", media.VideoTime))
|
||||
result.Failed++
|
||||
return
|
||||
}
|
||||
if len(media.FieldNameFs) == 0 {
|
||||
log.Warn("SyncVideoFromMediaFile empty fieldNameFs", log.Any("id", id))
|
||||
result.Failed++
|
||||
return
|
||||
}
|
||||
media.MediaSourceId = media.Id
|
||||
media.MediaType = 2
|
||||
if req.FirstTagMap != nil {
|
||||
if firstTag, ok := req.FirstTagMap[id]; ok && len(firstTag) > 0 {
|
||||
media.FirstTag = firstTag
|
||||
}
|
||||
}
|
||||
if se := SyncOneVideo(ctx, media); se != nil {
|
||||
log.Warn("SyncVideoFromMediaFile SyncOneVideo failed", log.Any("id", id), log.E(se))
|
||||
result.Failed++
|
||||
return
|
||||
}
|
||||
result.Synced++
|
||||
}
|
||||
|
||||
if len(req.VideoList) > 0 {
|
||||
for _, item := range req.VideoList {
|
||||
media, err := buildMediaRespFromSyncItem(item)
|
||||
if err != nil {
|
||||
log.Warn("SyncVideoFromMediaFile invalid item", log.Any("id", item.Id), log.E(err))
|
||||
result.Failed++
|
||||
continue
|
||||
}
|
||||
syncOne(item.Id, media)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
domain := mediaFileDomain(req.SourceVersion)
|
||||
for _, id := range req.FileIds {
|
||||
if len(id) == 0 {
|
||||
continue
|
||||
}
|
||||
resp, err := storage.QueryMedia(ctx, &proxy.MediaQueryeReq{Id: id}, domain)
|
||||
if err != nil || resp == nil || resp.Code != proxy.Success {
|
||||
if resp != nil {
|
||||
log.Warn("SyncVideoFromMediaFile QueryMedia failed", log.Any("id", id), log.E(err), log.Any("respCode", resp.Code))
|
||||
} else {
|
||||
log.Warn("SyncVideoFromMediaFile QueryMedia failed", log.Any("id", id), log.E(err))
|
||||
}
|
||||
result.Failed++
|
||||
continue
|
||||
}
|
||||
media := resp.Data
|
||||
if media.Id.IsZero() {
|
||||
oid, parseErr := primitive.ObjectIDFromHex(id)
|
||||
if parseErr != nil {
|
||||
log.Warn("SyncVideoFromMediaFile invalid id", log.Any("id", id), log.E(parseErr))
|
||||
result.Failed++
|
||||
continue
|
||||
}
|
||||
media.Id = oid
|
||||
}
|
||||
syncOne(id, media)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DelVideoInfo 删除指定APP
|
||||
func DelVideoInfo(ctx context.Context, id int64) *srvdef.SrvErr {
|
||||
info, err1 := videoinformationmod.FindOneVideoInformation(ctx, bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Id: id,
|
||||
}, options.FindOne().SetProjection(bson.D{
|
||||
{Key: videoinformationmod.FN.VideoInformation.Id, Value: 1},
|
||||
{Key: videoinformationmod.FN.VideoInformation.Tags, Value: 1},
|
||||
}))
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
adFilter := bson.M{videoinformationmod.FN.VideoInformation.Id: id}
|
||||
//删除
|
||||
count, serr := videoinformationmod.DelOneVideoInformation(ctx, adFilter)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
if count == 0 {
|
||||
return srvdef.New(srvdef.ErrNoData, "", "")
|
||||
}
|
||||
playletcache.ClearHomeCache()
|
||||
// 删除后检查一下合集数据
|
||||
collectionInfos, _ := videocollectionmediamod.FindManyVideoCollectionMediaModels(ctx, bson.M{
|
||||
videocollectionmediamod.FN.VideoCollectionMediaModel.VideoId: id,
|
||||
})
|
||||
if len(collectionInfos) > 0 {
|
||||
collectionIds := make([]int64, 0)
|
||||
for _, collectionInfo := range collectionInfos {
|
||||
collectionIds = append(collectionIds, collectionInfo.CollectionID)
|
||||
}
|
||||
if len(collectionIds) > 0 {
|
||||
_ = videoinformationmod.UpdateManyVideoInformation(ctx, bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Id: bson.M{"$in": collectionInfos},
|
||||
}, bson.M{"$inc": bson.M{
|
||||
videoinformationmod.FN.VideoInformation.VideoCount: -1,
|
||||
}})
|
||||
}
|
||||
}
|
||||
_, _ = videocollectionmediamod.DelManyVideoCollectionMediaModel(ctx, bson.M{
|
||||
videocollectionmediamod.FN.VideoCollectionMediaModel.VideoId: id,
|
||||
})
|
||||
if len(info.Tags) > 0 {
|
||||
oldTagIds := append([]int64{}, info.Tags...)
|
||||
godef.Go(func() {
|
||||
_ = srv_contenttag.SyncContentTagBindCount(context.Background(), oldTagIds, nil, contenttagmod.ContentTagTypeVideo)
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateVideoInfo 更新指定APP
|
||||
func UpdateVideoInfo(ctx context.Context, id, userId int64, param VideoInfoUpdate) *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.Tags != nil {
|
||||
tagIds, err1 := srv_contenttag.NormalizeContentTagIds(*param.Tags)
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
if err1 := srv_contenttag.ValidateContentTags(ctx, tagIds, contenttagmod.ContentTagTypeVideo); err1 != nil {
|
||||
return err1
|
||||
}
|
||||
param.Tags = &tagIds
|
||||
}
|
||||
if param.CategoryRanks != nil {
|
||||
seen := make(map[int64]bool, len(*param.CategoryRanks))
|
||||
for _, rank := range *param.CategoryRanks {
|
||||
if rank.Id <= 0 || seen[rank.Id] {
|
||||
return srvdef.New(srvdef.ErrParamError, "分类ID无效或重复", "分类ID无效或重复")
|
||||
}
|
||||
seen[rank.Id] = true
|
||||
if _, err := videocategorymod.FindOneVideoCategory(ctx, bson.M{videocategorymod.FN.VideoCategory.Id: rank.Id}); err != nil {
|
||||
if err.Code == srvdef.ErrNoData {
|
||||
return srvdef.New(srvdef.ErrParamError, "视频分类不存在", "视频分类不存在")
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
isChangeActress := checkActressChange(*info, param)
|
||||
isChangeTag := checkTagChange(*info, param)
|
||||
if param.ActressIds != nil && len(*param.ActressIds) > 0 {
|
||||
acts, serr := actress.FindManyActresss(ctx, bson.M{
|
||||
actress.FN.Actress.Id: bson.M{"$in": *param.ActressIds},
|
||||
})
|
||||
if serr != nil {
|
||||
log.Warn("UpdateVideoInfo FindManyActresss failed", log.Any("filter", param.ActressIds), log.E(serr))
|
||||
return serr
|
||||
}
|
||||
acIds := make([]int64, 0)
|
||||
for _, a := range acts {
|
||||
acIds = append(acIds, a.Id)
|
||||
}
|
||||
param.ActressIds = &acIds
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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 {
|
||||
isChangeActress = true
|
||||
tempActressIds := make([]int64, 0)
|
||||
if param.ActressIds != nil {
|
||||
tempActressIds = *param.ActressIds
|
||||
}
|
||||
go updateActressWorks(info.ActressIds, tempActressIds)
|
||||
}
|
||||
title := info.Title
|
||||
if param.Title != nil {
|
||||
title = *param.Title
|
||||
}
|
||||
keyworld := info.Keyworld
|
||||
if param.Keyworld != nil {
|
||||
keyworld = *param.Keyworld
|
||||
}
|
||||
exists := func(s string) bool {
|
||||
doc, _ := videoinformationmod.FindOneVideoInformation(ctx, slugx.Filter(
|
||||
videoinformationmod.FN.VideoInformation.Slug, videoinformationmod.FN.VideoInformation.Id, s, id))
|
||||
return doc != nil
|
||||
}
|
||||
slug, changed, slugErr := slugx.PrepareOnUpdate(info.Slug, param.Slug,
|
||||
utils.SlugSourceFromKeywordsOrTitle(keyworld, title), exists)
|
||||
if slugErr != nil {
|
||||
return slugErr
|
||||
}
|
||||
if changed {
|
||||
param.Slug = &slug
|
||||
} else {
|
||||
param.Slug = nil
|
||||
}
|
||||
adFilter := bson.M{videoinformationmod.FN.VideoInformation.Id: id}
|
||||
param.IndexNow = nil
|
||||
isPublished := param.Status != nil &&
|
||||
info.Status != *param.Status &&
|
||||
*param.Status == pubdef.InformationStatusVerifyOK
|
||||
if isPublished || (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()
|
||||
if isChangeActress {
|
||||
tempActressIds := make([]int64, 0)
|
||||
if param.ActressIds != nil {
|
||||
tempActressIds = *param.ActressIds
|
||||
}
|
||||
go updateActressWorks(info.ActressIds, tempActressIds)
|
||||
}
|
||||
if isChangeTag {
|
||||
tempIds := make([]int64, 0)
|
||||
if param.Tags != nil {
|
||||
tempIds = *param.Tags
|
||||
}
|
||||
oldTagIds := append([]int64{}, info.Tags...)
|
||||
newTagIds := append([]int64{}, tempIds...)
|
||||
godef.Go(func() {
|
||||
_ = srv_contenttag.SyncContentTagBindCount(context.Background(), oldTagIds, newTagIds, contenttagmod.ContentTagTypeVideo)
|
||||
})
|
||||
}
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
newStatus := info.Status
|
||||
if param.Status != nil {
|
||||
newStatus = *param.Status
|
||||
}
|
||||
srv_seopush.NotifyInfoAfterWrite(ctx, srv_seopush.KindVideo, id, info.Status, newStatus)
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkActressChange(info videoinformationmod.VideoInformation, updateParam VideoInfoUpdate) bool {
|
||||
if updateParam.ActressIds != nil {
|
||||
if len(info.ActressIds) != len(*updateParam.ActressIds) {
|
||||
return true
|
||||
}
|
||||
actressIds := *updateParam.ActressIds
|
||||
for i, v := range info.ActressIds {
|
||||
if v != actressIds[i] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
func checkTagChange(info videoinformationmod.VideoInformation, updateParam VideoInfoUpdate) bool {
|
||||
if updateParam.Tags != nil {
|
||||
if len(info.Tags) != len(*updateParam.Tags) {
|
||||
return true
|
||||
}
|
||||
tags := *updateParam.Tags
|
||||
for i, v := range info.Tags {
|
||||
if v != tags[i] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func updateActressWorks(oldIds []int64, newIds []int64) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Error("updateActressWorks failed", log.Any("err", err))
|
||||
}
|
||||
}()
|
||||
mapIds := make(map[int64]struct{})
|
||||
if oldIds == nil {
|
||||
oldIds = make([]int64, 0)
|
||||
}
|
||||
if newIds == nil {
|
||||
newIds = make([]int64, 0)
|
||||
}
|
||||
for _, id := range newIds {
|
||||
mapIds[id] = struct{}{}
|
||||
}
|
||||
for _, id := range oldIds {
|
||||
mapIds[id] = struct{}{}
|
||||
}
|
||||
ids := make([]int64, 0)
|
||||
for id := range mapIds {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
ctx := context.Background()
|
||||
for _, id := range ids {
|
||||
count, err := videoinformationmod.CountVideoInformations(ctx, bson.M{videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK, videoinformationmod.FN.VideoInformation.ActressIds: bson.M{"$in": []int64{id}}})
|
||||
if err == nil {
|
||||
_ = actress.UpdateOneActress(ctx, bson.M{
|
||||
actress.FN.Actress.Id: id,
|
||||
}, bson.M{
|
||||
"$set": bson.M{
|
||||
actress.FN.Actress.Works: count,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func AddVideoCollection(ctx context.Context, param *AddVideoCollectionReq) *srvdef.SrvErr {
|
||||
tagIds, err1 := srv_contenttag.NormalizeContentTagIds(param.Tags)
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
if err1 := srv_contenttag.ValidateContentTags(ctx, tagIds, contenttagmod.ContentTagTypeVideo); err1 != nil {
|
||||
return err1
|
||||
}
|
||||
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: pubdef.InformationStatusWaiting,
|
||||
Video: videoinformationmod.VideoInfo{},
|
||||
Tags: tagIds,
|
||||
Real: pubdef.Flow{},
|
||||
VideoCount: 0,
|
||||
UpdatedAt: time.Now(),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err1 := videoinformationmod.InsertOneVideoInformation(ctx, &info); err1 != nil {
|
||||
return err1
|
||||
}
|
||||
return srv_contenttag.SyncContentTagBindCount(ctx, nil, tagIds, contenttagmod.ContentTagTypeVideo)
|
||||
}
|
||||
|
||||
// AddVideoTags 批量追加视频统一标签
|
||||
func AddVideoTags(ctx context.Context, req *VideoAddTagsReq) *srvdef.SrvErr {
|
||||
if req == nil || len(req.VideoIds) == 0 || len(req.Tags) == 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "视频ID和标签ID不能为空", "")
|
||||
}
|
||||
videoIds, err := normalizeFakeFlowIds(req.VideoIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tagIds, err := srv_contenttag.NormalizeContentTagIds(req.Tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(tagIds) == 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "标签ID不能为空", "")
|
||||
}
|
||||
if err := srv_contenttag.ValidateContentTags(ctx, tagIds, contenttagmod.ContentTagTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
list, err := videoinformationmod.FindManyVideoInformations(ctx, bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Id: bson.M{"$in": videoIds},
|
||||
}, options.Find().SetProjection(bson.D{
|
||||
{Key: videoinformationmod.FN.VideoInformation.Id, Value: 1},
|
||||
{Key: videoinformationmod.FN.VideoInformation.Tags, Value: 1},
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return srvdef.New(srvdef.ErrNoData, "", "")
|
||||
}
|
||||
now := time.Now()
|
||||
docs := make([]mongo.WriteModel, 0, len(list))
|
||||
changes := make([]srv_contenttag.ContentTagBindChange, 0, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
newTagIds, changed := appendMissingVideoTagIds(item.Tags, tagIds)
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
docs = append(docs, mongo.NewUpdateOneModel().
|
||||
SetFilter(bson.M{videoinformationmod.FN.VideoInformation.Id: item.Id}).
|
||||
SetUpdate(bson.M{"$set": bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Tags: newTagIds,
|
||||
videoinformationmod.FN.VideoInformation.UpdatedAt: now,
|
||||
}}))
|
||||
changes = append(changes, srv_contenttag.ContentTagBindChange{
|
||||
OldTagIds: append([]int64{}, item.Tags...),
|
||||
NewTagIds: newTagIds,
|
||||
})
|
||||
}
|
||||
if len(docs) == 0 {
|
||||
return nil
|
||||
}
|
||||
if _, err := videoinformationmod.BulkWriteVideoInformation(ctx, docs, options.BulkWrite().SetOrdered(false)); err != nil {
|
||||
return err
|
||||
}
|
||||
playletcache.ClearHomeCache()
|
||||
return srv_contenttag.SyncContentTagBindCountBatch(ctx, changes, contenttagmod.ContentTagTypeVideo)
|
||||
}
|
||||
|
||||
// SetVideoTags 单/批量覆盖设置视频统一标签,并同步新标签表视频绑定数量
|
||||
func SetVideoTags(ctx context.Context, req *VideoSetTagsReq) *srvdef.SrvErr {
|
||||
if req == nil || len(req.Ids) == 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "视频ID不能为空", "")
|
||||
}
|
||||
if req.TagIds == nil {
|
||||
return srvdef.New(srvdef.ErrParamError, "标签ID不能为空", "")
|
||||
}
|
||||
videoIds, err := normalizeFakeFlowIds(req.Ids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tagIds, err := srv_contenttag.NormalizeContentTagIds(*req.TagIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := srv_contenttag.ValidateContentTags(ctx, tagIds, contenttagmod.ContentTagTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
list, err := videoinformationmod.FindManyVideoInformations(ctx, bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Id: bson.M{"$in": videoIds},
|
||||
}, options.Find().SetProjection(bson.D{
|
||||
{Key: videoinformationmod.FN.VideoInformation.Id, Value: 1},
|
||||
{Key: videoinformationmod.FN.VideoInformation.Tags, Value: 1},
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return srvdef.New(srvdef.ErrNoData, "", "")
|
||||
}
|
||||
now := time.Now()
|
||||
docs := make([]mongo.WriteModel, 0, len(list))
|
||||
changes := make([]srv_contenttag.ContentTagBindChange, 0, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil || sameVideoTagIds(item.Tags, tagIds) {
|
||||
continue
|
||||
}
|
||||
docs = append(docs, mongo.NewUpdateOneModel().
|
||||
SetFilter(bson.M{videoinformationmod.FN.VideoInformation.Id: item.Id}).
|
||||
SetUpdate(bson.M{"$set": bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Tags: tagIds,
|
||||
videoinformationmod.FN.VideoInformation.UpdatedAt: now,
|
||||
}}))
|
||||
changes = append(changes, srv_contenttag.ContentTagBindChange{
|
||||
OldTagIds: append([]int64{}, item.Tags...),
|
||||
NewTagIds: append([]int64{}, tagIds...),
|
||||
})
|
||||
}
|
||||
if len(docs) == 0 {
|
||||
return nil
|
||||
}
|
||||
if _, err := videoinformationmod.BulkWriteVideoInformation(ctx, docs, options.BulkWrite().SetOrdered(false)); err != nil {
|
||||
return err
|
||||
}
|
||||
playletcache.ClearHomeCache()
|
||||
return srv_contenttag.SyncContentTagBindCountBatch(ctx, changes, contenttagmod.ContentTagTypeVideo)
|
||||
}
|
||||
|
||||
func sameVideoTagIds(current, target []int64) bool {
|
||||
if len(current) != len(target) {
|
||||
return false
|
||||
}
|
||||
for i := range current {
|
||||
if current[i] != target[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func appendMissingVideoTagIds(current, addIds []int64) ([]int64, bool) {
|
||||
result := append([]int64{}, current...)
|
||||
tagMap := make(map[int64]struct{}, len(current)+len(addIds))
|
||||
for _, tagId := range current {
|
||||
if tagId > 0 {
|
||||
tagMap[tagId] = struct{}{}
|
||||
}
|
||||
}
|
||||
changed := false
|
||||
for _, tagId := range addIds {
|
||||
if _, ok := tagMap[tagId]; ok {
|
||||
continue
|
||||
}
|
||||
tagMap[tagId] = struct{}{}
|
||||
result = append(result, tagId)
|
||||
changed = true
|
||||
}
|
||||
return result, changed
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package srv_videotag
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/pub/models/videotagmod"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
type VideoTagInfoReq struct {
|
||||
pubdef.Page
|
||||
Id *int64 `json:"id"`
|
||||
Title *string `json:"title"`
|
||||
}
|
||||
type VideoTagInfoResp struct {
|
||||
Total int64 `json:"total"`
|
||||
List videotagmod.VideoTagInfoList `json:"list"`
|
||||
}
|
||||
type VideoTagInfoUpdate struct {
|
||||
AppId *int32 `json:"appId" bson:"appId,omitempty"` // appId,唯一id
|
||||
Title *string `json:"title" bson:"title,omitempty"` // 帖子标题
|
||||
Status *bool `json:"status" bson:"status,omitempty"` // 上架状态
|
||||
Sort *int64 `json:"sort" bson:"sort,omitempty"` // 排序
|
||||
SeoInfo *pubdef.SeoValue `json:"seoInfo" bson:"seoInfo,omitempty"` // Seo相关信息
|
||||
Works *int64 `json:"works" bson:"works,omitempty"` // 作品数量
|
||||
MediaTypes *[]int64 `json:"mediaTypes" bson:"mediaTypes"` // 作者关联的资源类型 1视频
|
||||
Images *string `json:"images" bson:"images,omitempty"` // 背景
|
||||
UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"`
|
||||
IsReIndexNow *bool `json:"isReIndexNow" bson:"isReIndexNow"` // 是否重新提交索引
|
||||
IndexNow *int64 `bson:"indexNow,omitempty"`
|
||||
}
|
||||
type VideoTagInfoUpdateReq struct {
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
Param VideoTagInfoUpdate `json:"param" bson:"param"` // appId,唯一id
|
||||
}
|
||||
|
||||
type VideoTagInfoDelReq struct {
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package srv_videotag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/ipdailystatmod"
|
||||
"videoweb/pub/models/videotagmod"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// HotTagReq 热门标签请求参数
|
||||
type HotTagReq struct {
|
||||
Limit int64 `form:"limit" json:"limit"` // 返回条数,默认 10,最大 1000
|
||||
}
|
||||
|
||||
// HotTagItem 热门标签结果
|
||||
type HotTagItem struct {
|
||||
TagId int64 `json:"tagId"`
|
||||
Title string `json:"title"`
|
||||
WatchTimes int64 `json:"watchTimes"`
|
||||
}
|
||||
|
||||
// HotTagResp 热门标签返回
|
||||
type HotTagResp struct {
|
||||
List []*HotTagItem `json:"list"`
|
||||
}
|
||||
|
||||
// GetHotTagsByPlayData 按预计算的 watchTimes 直接取 TopN(O(logN),依赖 {status:1, watchTimes:-1} 索引)
|
||||
func GetHotTagsByPlayData(ctx context.Context, req *HotTagReq) (*HotTagResp, *srvdef.SrvErr) {
|
||||
limit := req.Limit
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
if limit > 1000 {
|
||||
limit = 1000
|
||||
}
|
||||
|
||||
resp := &HotTagResp{List: []*HotTagItem{}}
|
||||
|
||||
filter := bson.M{
|
||||
videotagmod.FN.VideoTagInfo.Status: true,
|
||||
videotagmod.FN.VideoTagInfo.WatchTimes: bson.M{"$gt": 0},
|
||||
}
|
||||
opts := options.Find().
|
||||
SetSort(bson.D{
|
||||
{Key: videotagmod.FN.VideoTagInfo.WatchTimes, Value: -1},
|
||||
{Key: videotagmod.FN.VideoTagInfo.Id, Value: 1},
|
||||
}).
|
||||
SetLimit(limit)
|
||||
|
||||
tags, serr := videotagmod.FindManyVideoTagInfos(ctx, filter, opts)
|
||||
if serr != nil {
|
||||
return resp, serr
|
||||
}
|
||||
|
||||
for _, t := range tags {
|
||||
resp.List = append(resp.List, &HotTagItem{
|
||||
TagId: t.Id,
|
||||
Title: t.Title,
|
||||
WatchTimes: t.WatchTimes,
|
||||
})
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
type IpStatReq struct {
|
||||
TodayIP int64 `json:"todayIP"`
|
||||
YesterdayIP int64 `json:"yesterdayIP"`
|
||||
}
|
||||
|
||||
// FetchIpStat 从 MongoDB ip_daily_stat 集合读取 app 端同步过来的 IP 去重数。
|
||||
// app 端中间件收集 IP 存入本地 Redis,定时任务将 SCard 写入共享 MongoDB,web 在此读取。
|
||||
// 日期统一按北京时间(Asia/Shanghai)计算。
|
||||
func FetchIpStat(ctx context.Context) *IpStatReq {
|
||||
ipStatReq := &IpStatReq{
|
||||
TodayIP: 0,
|
||||
YesterdayIP: 0,
|
||||
}
|
||||
now := time.Now()
|
||||
bjNow := now.In(utils.LocBJ)
|
||||
today := bjNow.Format("20060102")
|
||||
yesterday := bjNow.AddDate(0, 0, -1).Format("20060102")
|
||||
|
||||
if cnt, err := ipdailystatmod.FindCount(ctx, pubdef.AppId, today); err == nil {
|
||||
ipStatReq.TodayIP = cnt
|
||||
} else {
|
||||
log.Warn("FetchIpStat: FindCount today failed",
|
||||
log.Any("appId", pubdef.AppId), log.E(err))
|
||||
}
|
||||
if cnt, err := ipdailystatmod.FindCount(ctx, pubdef.AppId, yesterday); err == nil {
|
||||
ipStatReq.YesterdayIP = cnt
|
||||
} else {
|
||||
log.Warn("FetchIpStat: FindCount yesterday failed",
|
||||
log.Any("appId", pubdef.AppId), log.E(err))
|
||||
}
|
||||
return ipStatReq
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package srv_videotag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/videotagmod"
|
||||
"videoweb/pub/mongos"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const (
|
||||
MediaTypeVideo int64 = 1
|
||||
MediaTypePost int64 = 2
|
||||
)
|
||||
|
||||
var firstTagMu sync.Mutex
|
||||
|
||||
// FindOrCreateByTitle 查询或创建同步数据中的一级标签。
|
||||
func FindOrCreateByTitle(ctx context.Context, title string, mediaType int64) (int64, *srvdef.SrvErr) {
|
||||
title = strings.TrimSpace(title)
|
||||
if title == "" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
firstTagMu.Lock()
|
||||
defer firstTagMu.Unlock()
|
||||
|
||||
filter := bson.M{videotagmod.FN.VideoTagInfo.Title: title}
|
||||
tag, serr := videotagmod.FindOneVideoTagInfo(ctx, filter)
|
||||
if serr == nil {
|
||||
if mediaType > 0 && !containsMediaType(tag.MediaTypes, mediaType) {
|
||||
if serr = videotagmod.UpdateOneVideoTagInfo(ctx, filter, bson.M{
|
||||
"$addToSet": bson.M{videotagmod.FN.VideoTagInfo.MediaTypes: mediaType},
|
||||
"$set": bson.M{videotagmod.FN.VideoTagInfo.UpdatedAt: time.Now()},
|
||||
}); serr != nil {
|
||||
return 0, serr
|
||||
}
|
||||
}
|
||||
return tag.Id, nil
|
||||
}
|
||||
if serr.Code != srvdef.ErrNoData {
|
||||
return 0, serr
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
tag = &videotagmod.VideoTagInfo{
|
||||
Id: mongos.Lid.VideoTag.Get(),
|
||||
Title: title,
|
||||
Status: true,
|
||||
MediaTypes: []int64{mediaType},
|
||||
IndexNow: 0,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if serr = videotagmod.InsertOneVideoTagInfo(ctx, tag); serr != nil {
|
||||
return 0, serr
|
||||
}
|
||||
return tag.Id, nil
|
||||
}
|
||||
|
||||
// AppendTagID 向标签列表追加 ID,并避免重复。
|
||||
func AppendTagID(tags []int64, tagID int64) []int64 {
|
||||
if tagID <= 0 {
|
||||
return tags
|
||||
}
|
||||
for _, id := range tags {
|
||||
if id == tagID {
|
||||
return tags
|
||||
}
|
||||
}
|
||||
return append(tags, tagID)
|
||||
}
|
||||
|
||||
func containsMediaType(mediaTypes []int64, mediaType int64) bool {
|
||||
for _, value := range mediaTypes {
|
||||
if value == mediaType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetVideoTagInfos 按照参数搜索视频标签
|
||||
func GetVideoTagInfos(ctx context.Context, param *VideoTagInfoReq) (*VideoTagInfoResp, *srvdef.SrvErr) {
|
||||
filter := bson.M{}
|
||||
opt := &options.FindOptions{}
|
||||
resp := &VideoTagInfoResp{}
|
||||
if param.Id != nil {
|
||||
filter[videotagmod.FN.VideoTagInfo.Id] = *param.Id
|
||||
}
|
||||
if param.Title != nil {
|
||||
filter[videotagmod.FN.VideoTagInfo.Title] = bson.M{"$regex": *param.Title}
|
||||
}
|
||||
|
||||
opt.SetSort(bson.M{videotagmod.FN.VideoTagInfo.Works: -1})
|
||||
opt.SetSkip((param.PageNum - 1) * param.PageSize).SetLimit(param.PageSize)
|
||||
|
||||
total, serr := videotagmod.CountVideoTagInfos(ctx, filter)
|
||||
if serr != nil {
|
||||
return resp, serr
|
||||
}
|
||||
resp.Total = total
|
||||
if total == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
ads, serr := videotagmod.FindManyVideoTagInfos(ctx, filter, opt)
|
||||
if serr != nil {
|
||||
return resp, serr
|
||||
}
|
||||
resp.List = ads
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// AddVideoTagInfo 添加一视频标签
|
||||
func AddVideoTagInfo(ctx context.Context,
|
||||
ac *videotagmod.VideoTagInfo) *srvdef.SrvErr {
|
||||
|
||||
ac.Id = mongos.Lid.VideoTag.Get()
|
||||
now := time.Now()
|
||||
ac.CreatedAt = now
|
||||
ac.UpdatedAt = now
|
||||
|
||||
serr := videotagmod.InsertOneVideoTagInfo(ctx, ac)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelVideoTagInfo 删除指定视频标签
|
||||
func DelVideoTagInfo(ctx context.Context, id int64) *srvdef.SrvErr {
|
||||
adFilter := bson.M{videotagmod.FN.VideoTagInfo.Id: id}
|
||||
//删除
|
||||
count, serr := videotagmod.DelOneVideoTagInfo(ctx, adFilter)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
if count == 0 {
|
||||
return srvdef.New(srvdef.ErrNoData, "", "")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateVideoTagInfo 更新指定视频标签
|
||||
func UpdateVideoTagInfo(ctx context.Context, id int64, param VideoTagInfoUpdate) *srvdef.SrvErr {
|
||||
now := time.Now()
|
||||
param.UpdatedAt = now
|
||||
|
||||
adFilter := bson.M{videotagmod.FN.VideoTagInfo.Id: id}
|
||||
param.IndexNow = nil
|
||||
if param.IsReIndexNow != nil && *param.IsReIndexNow {
|
||||
indexNow := int64(0)
|
||||
param.IndexNow = &indexNow
|
||||
}
|
||||
updateDoc := bson.M{"$set": param}
|
||||
serr := videotagmod.UpdateOneVideoTagInfo(ctx, adFilter, updateDoc)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user