init: web_hgcg_server 初始化提交
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"videoweb/web/service/srv_ipblack"
|
||||
"videoweb/web/service/srv_mediaresource"
|
||||
"videoweb/web/skd"
|
||||
)
|
||||
|
||||
func Start() {
|
||||
skd.Start()
|
||||
srv_ipblack.Init()
|
||||
srv_mediaresource.Start()
|
||||
}
|
||||
|
||||
func Stop() {
|
||||
srv_mediaresource.Stop()
|
||||
skd.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,101 @@
|
||||
package srv_announcement
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"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,
|
||||
}
|
||||
return announcementmod.InsertOneAnnouncement(ctx, doc)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return announcementmod.UpdateOneAnnouncement(ctx, filter, bson.M{"$set": updateFields})
|
||||
}
|
||||
|
||||
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, "", "")
|
||||
}
|
||||
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,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,90 @@
|
||||
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,omitempty"` // 回家的路PDF
|
||||
ImgAppCdn *string `json:"imgAppCdn" bson:"imgAppCdn,omitempty"` // 图片CDN(app使用)
|
||||
ImgAppUsCdn *string `json:"imgAppUsCdn" bson:"imgAppUsCdn,omitempty"` // 图片CDN(海外app使用)
|
||||
ImgWebCdn *string `json:"imgWebCdn" bson:"imgWebCdn,omitempty"` // 图片CDN(web使用)
|
||||
MediaWebCdn *string `json:"mediaWebCdn" bson:"mediaWebCdn,omitempty"` // 视频CDN(web使用)
|
||||
MediaAppCdn *string `json:"mediaAppCdn" bson:"mediaAppCdn,omitempty"` // 视频CDN(app使用)
|
||||
MediaAppUsCdn *string `json:"mediaAppUsCdn" bson:"mediaAppUsCdn,omitempty"` // 视频CDN(海外app使用)
|
||||
WatermarkImage *string `json:"watermarkImage" bson:"watermarkImage,omitempty"` // 图片水印文件
|
||||
WatermarkMedia *string `json:"watermarkMedia" bson:"watermarkMedia,omitempty"` // 视频水印文件(底部右边)
|
||||
WatermarkMediaTop *string `json:"watermarkMediaTop" bson:"watermarkMediaTop,omitempty"` // 视频水印文件(顶部左边)
|
||||
WatermarkMediaTopRight *string `json:"watermarkMediaTopRight" bson:"watermarkMediaTopRight,omitempty"` // 视频水印文件(顶部右边)
|
||||
WatermarkMediaBottomLeft *string `json:"watermarkMediaBottomLeft" bson:"watermarkMediaBottomLeft,omitempty"` // 视频水印文件(底部左边)
|
||||
Mp4Addr *string `json:"mp4Addr" bson:"mp4Addr,omitempty"` // mp4地址(web使用)
|
||||
CustomerServiceHost *string `json:"customerServiceHost" bson:"customerServiceHost,omitempty"` // 在线客服
|
||||
NewUrlAddr *string `json:"newUrlAddr" bson:"newUrlAddr,omitempty"` //最新地址
|
||||
NewUrlAddr1 *string `json:"newUrlAddr1" bson:"newUrlAddr1,omitempty"` //最新地址(备用地址1)
|
||||
NewUrlAddr2 *string `json:"newUrlAddr2" bson:"newUrlAddr2,omitempty"` //最新地址(备用地址2)
|
||||
ForeverUrlAddr *string `json:"foreverUrlAddr" bson:"foreverUrlAddr,omitempty"` //永久地址
|
||||
GmailAddr *string `json:"gmailAddr" bson:"gmailAddr,omitempty"` //邮箱地址
|
||||
AppAddr *string `json:"appAddr" bson:"appAddr,omitempty"` //APP下载地址(落地页)
|
||||
OfficialAppAddr *string `json:"officialAppAddr" bson:"officialAppAddr,omitempty"` //官网app地址
|
||||
WeiChart *string `json:"weiChart" bson:"weiChart,omitempty"` //微信群
|
||||
QQ *string `json:"qq" bson:"qq,omitempty"` //qq群
|
||||
TgGroup *string `json:"tgGroup" bson:"tgGroup,omitempty"` //tg群
|
||||
PotaoGroup *string `json:"potaoGroup" bson:"potaoGroup,omitempty"` //土豆群
|
||||
GitAddr *string `json:"gitAddr" bson:"gitAddr,omitempty"` //git地址
|
||||
TwitterAddr *string `json:"twitterAddr" bson:"twitterAddr,omitempty"` //推特地址
|
||||
Telegram *string `json:"telegram" bson:"telegram,omitempty"` //商务tg
|
||||
TelegramAdd *string `json:"telegramAdd" bson:"telegramAdd,omitempty"` //加入我们tg
|
||||
TranslationAddr *string `json:"translationAddr" bson:"translationAddr,omitempty"` //应用导航
|
||||
//SEO
|
||||
WebTitle *string `json:"webTitle" bson:"webTitle,omitempty"` // 网站SEO标题
|
||||
WebKeyworld *[]string `json:"webKeyworld" bson:"webKeyworld,omitempty"` // 网站SEO关键字
|
||||
WebDescription *string `json:"webDescription" bson:"webDescription,omitempty"` // 网站SEO描述
|
||||
WebSiteName *string `json:"webSiteName" bson:"webSiteName,omitempty"` // 网站SEO网站名称
|
||||
WebAddr *string `json:"webAddr" bson:"webAddr,omitempty"` // 网站SEO网站地址
|
||||
DHNewUrlAddr1 *string `json:"dhNewUrlAddr1" bson:"dhNewUrlAddr1,omitempty"` // 导航页-线路一
|
||||
DHNewUrlAddr2 *string `json:"dhNewUrlAddr2" bson:"dhNewUrlAddr2,omitempty"` // 导航页-线路2
|
||||
DHNewUrlAddr3 *string `json:"dhNewUrlAddr3" bson:"dhNewUrlAddr3,omitempty"` // 导航页-线路3
|
||||
FreeVPN *string `json:"freeVPN" bson:"freeVPN,omitempty"` // 免费vpn链接
|
||||
UseAddress *[]string `json:"useAddress" bson:"useAddress,omitempty"` // 所有使用中的域名
|
||||
AdvitiesTopicNum *int64 `json:"advitiesTopicNum" bson:"advitiesTopicNum,omitempty"` // 广告间隔
|
||||
DHTitle *string `json:"dhTitle" bson:"dhTitle,omitempty"` // 导航SEO标题
|
||||
DHKeyworld *[]string `json:"dhKeyworld" bson:"dhKeyworld,omitempty"` // 导航SEO关键字
|
||||
DHDescription *string `json:"dhDescription" bson:"dhDescription,omitempty"` // 导航SEO描述
|
||||
DHSiteName *string `json:"dhSiteName" bson:"dhSiteName,omitempty"` // 导航SEO网站名称
|
||||
ActressBackGround *string `json:"actressBackGround" bson:"actressBackGround,omitempty"` // 女优通用背景图
|
||||
ChannelCode *string `json:"channelCode" bson:"channelCode,omitempty"` // 渠道码
|
||||
EnableH265 *bool `json:"enableH265" bson:"enableH265,omitempty"` // 是否允许客户端使用 H265 地址播放
|
||||
}
|
||||
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,26 @@
|
||||
package srv_cointrans
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/pub/models/cointransmod"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
type CoinTransReq struct {
|
||||
pubdef.Page
|
||||
UserId *int64 `json:"userId"` // 用户ID
|
||||
FlowDir *cointransmod.FlowDir `json:"flowDir"` // 收支方向:1收入 -1支出
|
||||
TranType *cointransmod.TranType `json:"tranType"` // 交易类型
|
||||
StartTime *string `json:"startTime"` // 开始时间 2006-01-02 15:04:05
|
||||
EndTime *string `json:"endTime"` // 结束时间 2006-01-02 15:04:05
|
||||
}
|
||||
|
||||
type CoinTransResp struct {
|
||||
Total int64 `json:"total"`
|
||||
List cointransmod.CoinTransList `json:"list"`
|
||||
}
|
||||
|
||||
func parseTime(s string) (time.Time, bool) {
|
||||
t, err := time.ParseInLocation("2006-01-02 15:04:05", s, time.Local)
|
||||
return t, err == nil
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package srv_cointrans
|
||||
|
||||
import (
|
||||
"context"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/cointransmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// GetCoinTrans 分页查询金币交易流水
|
||||
func GetCoinTrans(ctx context.Context, req *CoinTransReq) (*CoinTransResp, *srvdef.SrvErr) {
|
||||
filter := bson.M{}
|
||||
|
||||
if req.UserId != nil && *req.UserId > 0 {
|
||||
filter[cointransmod.FN.CoinTrans.UserId] = *req.UserId
|
||||
}
|
||||
if req.FlowDir != nil {
|
||||
filter[cointransmod.FN.CoinTrans.FlowDir] = *req.FlowDir
|
||||
}
|
||||
if req.TranType != nil {
|
||||
filter[cointransmod.FN.CoinTrans.TranType] = *req.TranType
|
||||
}
|
||||
|
||||
timeFilter := bson.M{}
|
||||
if req.StartTime != nil && *req.StartTime != "" {
|
||||
if t, ok := parseTime(*req.StartTime); ok {
|
||||
timeFilter["$gte"] = t
|
||||
}
|
||||
}
|
||||
if req.EndTime != nil && *req.EndTime != "" {
|
||||
if t, ok := parseTime(*req.EndTime); ok {
|
||||
timeFilter["$lte"] = t
|
||||
}
|
||||
}
|
||||
if len(timeFilter) > 0 {
|
||||
filter[cointransmod.FN.CoinTrans.CreatedAt] = timeFilter
|
||||
}
|
||||
|
||||
total, serr := cointransmod.CountCoinTrans(ctx, filter)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
resp := &CoinTransResp{Total: total}
|
||||
if total == 0 {
|
||||
resp.List = cointransmod.CoinTransList{}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
opt := options.Find().
|
||||
SetSort(bson.D{{Key: cointransmod.FN.CoinTrans.CreatedAt, Value: -1}}).
|
||||
SetSkip((req.PageNum - 1) * req.PageSize).
|
||||
SetLimit(req.PageSize)
|
||||
|
||||
list, serr := cointransmod.FindManyCoinTrans(ctx, filter, opt)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
resp.List = list
|
||||
return resp, nil
|
||||
}
|
||||
@@ -0,0 +1,417 @@
|
||||
package srv_commend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/commentmod"
|
||||
"videoweb/pub/models/playletchaptermod"
|
||||
"videoweb/pub/models/playletmod"
|
||||
"videoweb/pub/mongos"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const (
|
||||
commentObjectTypePlaylet int64 = 10 // 短剧评论
|
||||
commentObjectTypePlayletChapter int64 = 11 // 短剧剧集评论
|
||||
commentReviewPass int = 1 // 审核通过
|
||||
commentLevelFirst int64 = 1 // 一级评论
|
||||
)
|
||||
|
||||
// SearchComandList 查询后台评论列表
|
||||
func SearchComandList(ctx context.Context, param *CommendSearch) (*CommendSearchResp, *srvdef.SrvErr) {
|
||||
|
||||
filter := bson.M{}
|
||||
opt := &options.FindOptions{}
|
||||
|
||||
// objectId 只表示业务对象ID,必须配合 objectType 才能准确区分短剧评论和剧集评论,避免不同集合ID碰撞。
|
||||
if param.ObjectId != nil {
|
||||
filter[commentmod.FN.Comment.ObjectId] = *param.ObjectId
|
||||
}
|
||||
if param.ObjectType != nil {
|
||||
filter[commentmod.FN.Comment.ObjectType] = *param.ObjectType
|
||||
}
|
||||
if param.UserId != nil && *param.UserId > 0 {
|
||||
filter[commentmod.FN.Comment.UserId] = *param.UserId
|
||||
}
|
||||
|
||||
if param.Level != nil {
|
||||
filter[commentmod.FN.Comment.Level] = *param.Level
|
||||
}
|
||||
|
||||
if param.ObjectName != nil {
|
||||
filter[commentmod.FN.Comment.ObjectName] = bson.M{"$regex": *param.ObjectName}
|
||||
}
|
||||
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),
|
||||
}
|
||||
|
||||
// countLike 是短剧评论点赞数的新字段,likes 保留旧字段兼容后台旧页面读取。
|
||||
for _, commendInfo := range commends {
|
||||
tmp := &PostCommentInfo{
|
||||
Id: commendInfo.Id,
|
||||
ObjectId: commendInfo.ObjectId,
|
||||
ObjectType: commendInfo.ObjectType,
|
||||
ObjectName: commendInfo.ObjectName,
|
||||
UserId: commendInfo.UserId,
|
||||
UserName: commendInfo.UserName,
|
||||
UserAvatar: commendInfo.UserAvatar,
|
||||
Level: commendInfo.Level,
|
||||
ParentsId: commendInfo.ParentsId,
|
||||
ReplyCommentId: commendInfo.ReplyCommentId,
|
||||
ReplyUserName: commendInfo.ReplyUserName,
|
||||
Text: commendInfo.Text,
|
||||
ReviewStatus: commendInfo.ReviewStatus,
|
||||
UpdatedAt: commendInfo.UpdatedAt,
|
||||
Ip: commendInfo.Ip,
|
||||
CreatedAt: commendInfo.CreatedAt,
|
||||
CountLike: commendInfo.CountLike,
|
||||
Likes: commendInfo.CountLike,
|
||||
}
|
||||
resp.List = append(resp.List, tmp)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// DelCommend 删除一条评论
|
||||
func DelCommend(ctx context.Context, commendId int64) *srvdef.SrvErr {
|
||||
// 先查出评论旧状态,再删除;后续扣减评论数需要依赖旧的 reviewStatus、level、objectType。
|
||||
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
|
||||
}
|
||||
|
||||
// 删除后按当前已审核通过评论数重算计数,避免历史增量失败或重复操作导致 countComment 不准。
|
||||
if serr = refreshCommentCountByComment(ctx, commendInfo); serr != nil {
|
||||
return serr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
commendId := make([]int64, 0)
|
||||
// 删除用户全部评论后逐条重算受影响对象的计数;重复刷新同一对象只会 set 成相同值,不会重复扣减。
|
||||
for _, commendInfo := range commendList {
|
||||
if serr = refreshCommentCountByComment(ctx, commendInfo); serr != nil {
|
||||
return serr
|
||||
}
|
||||
commendId = append(commendId, commendInfo.Id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddPostComment 发布评论
|
||||
func AddPostComment(ctx context.Context, req *CommendReplay) *srvdef.SrvErr {
|
||||
now := time.Now()
|
||||
if req.Score > 10 {
|
||||
log.Error("后台回复评论参数错误", log.Any("评分", req.Score))
|
||||
return srvdef.New(srvdef.ErrParamError, "", "")
|
||||
}
|
||||
|
||||
// 后台回复评论沿用旧的帖子评论逻辑,默认审核通过;短剧/剧集评论由 app 评论入口写入。
|
||||
replyCommentId := int64(0)
|
||||
if req.ParentsId > 0 {
|
||||
replyCommentId = req.ReplyCommentId
|
||||
if replyCommentId <= 0 {
|
||||
replyCommentId = req.ParentsId
|
||||
}
|
||||
}
|
||||
commentInfo := &commentmod.CommentModel{
|
||||
Id: mongos.Lid.Comment.Get(),
|
||||
ObjectId: req.ObjectId,
|
||||
UserName: req.UserName,
|
||||
ParentsId: req.ParentsId,
|
||||
ReplyCommentId: replyCommentId,
|
||||
Text: req.Text,
|
||||
Level: 1,
|
||||
UpdatedAt: now,
|
||||
CreatedAt: now,
|
||||
}
|
||||
|
||||
if commentInfo.ParentsId > 0 {
|
||||
commentInfo.Level = 2
|
||||
} else {
|
||||
commentInfo.Level = 1
|
||||
}
|
||||
commentInfo.ReviewStatus = 1 // 管理端的默认审核通过
|
||||
|
||||
// 插入回复评论成功后,再异步增加父评论回复数,避免回复失败时污染父评论计数。
|
||||
serr := commentmod.InsertOneComment(ctx, commentInfo)
|
||||
if serr != nil {
|
||||
log.Error("后台回复评论入库失败", log.Any("请求参数", req), log.Any("err", serr))
|
||||
return serr
|
||||
}
|
||||
|
||||
go func() {
|
||||
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("后台回复评论更新父评论回复数失败", log.Any("请求参数", req), log.Any("err", serr))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// 置顶评论
|
||||
func TopPostComment(ctx context.Context, id int64) *srvdef.SrvErr {
|
||||
// 置顶排序采用递增 topRank,新的置顶评论排在当前最高 topRank 前面。
|
||||
filter := bson.M{
|
||||
commentmod.FN.Comment.Id: id,
|
||||
}
|
||||
comment, serr := commentmod.FindOneComment(ctx, filter)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
// 获取当前对象下已置顶排序最高的一条评论,用于生成新的 topRank。
|
||||
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
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getTopRankComment(ctx context.Context, objectId int64) (*commentmod.CommentModel, *srvdef.SrvErr) {
|
||||
// 只按 objectId 查是历史逻辑;短剧/剧集评论如果后续支持置顶,建议再补 objectType 避免ID碰撞。
|
||||
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, serr := commentmod.FindOneComment(ctx, bson.M{commentmod.FN.Comment.Id: id})
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
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), log.Any("err", serr))
|
||||
return serr
|
||||
}
|
||||
comment.ReviewStatus = reviewStatus
|
||||
// 审核后按当前已通过评论数重算,重复审核同一状态也能修正历史漏加或漏减的 countComment。
|
||||
if serr = refreshCommentCountByComment(ctx, comment); serr != nil {
|
||||
return serr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReviewCommendBatch 批量更新
|
||||
func ReviewCommendBatch(ctx context.Context, commendIds *[]int64, reviewStatus int) *srvdef.SrvErr {
|
||||
if commendIds == nil || len(*commendIds) == 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "评论ID不能为空", "")
|
||||
}
|
||||
// 批量审核也要先查旧数据,统一用每条评论的旧审核状态计算评论数增减。
|
||||
commentList, serr := commentmod.FindManyComment(ctx, bson.M{
|
||||
commentmod.FN.Comment.Id: bson.M{"$in": *commendIds},
|
||||
})
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
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("评论ID集合", *commendIds), log.Any("审核状态", reviewStatus), log.Any("err", serr))
|
||||
return serr
|
||||
}
|
||||
// 审核状态更新成功后再按对象重算计数,避免状态未更新成功时提前改动短剧/剧集评论数。
|
||||
for _, comment := range commentList {
|
||||
if comment == nil {
|
||||
continue
|
||||
}
|
||||
if serr = refreshCommentCountByComment(ctx, comment); serr != nil {
|
||||
return serr
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func refreshCommentCountByComment(ctx context.Context, comment *commentmod.CommentModel) *srvdef.SrvErr {
|
||||
// 根据评论归属重算对应计数:二级评论刷新父评论回复数,一级短剧/剧集评论刷新业务对象评论数。
|
||||
if comment == nil {
|
||||
return nil
|
||||
}
|
||||
if comment.Level > commentLevelFirst && comment.ParentsId > 0 {
|
||||
return refreshParentCommentCount(ctx, comment.ParentsId)
|
||||
}
|
||||
if isSupportedCommentType(comment.ObjectType) {
|
||||
return refreshCommentTargetCount(ctx, comment.ObjectType, comment.ObjectId)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func refreshParentCommentCount(ctx context.Context, parentId int64) *srvdef.SrvErr {
|
||||
// 父评论回复数以当前审核通过的子评论数量为准,直接 set 可以修正历史增量不一致问题。
|
||||
if parentId <= 0 {
|
||||
return nil
|
||||
}
|
||||
count, serr := commentmod.CountComments(ctx, bson.M{
|
||||
commentmod.FN.Comment.ParentsId: parentId,
|
||||
commentmod.FN.Comment.ReviewStatus: commentReviewPass,
|
||||
})
|
||||
if serr != nil {
|
||||
log.Error("统计父评论回复数失败", log.Any("父评论ID", parentId), log.E(serr))
|
||||
return serr
|
||||
}
|
||||
serr = commentmod.UpdateOneComment(ctx, bson.M{
|
||||
commentmod.FN.Comment.Id: parentId,
|
||||
}, bson.M{
|
||||
"$set": bson.M{commentmod.FN.Comment.CommentCount: count},
|
||||
})
|
||||
if serr != nil {
|
||||
if serr.Code == srvdef.ErrNoData {
|
||||
log.Warn("父评论不存在,跳过刷新回复数", log.Any("父评论ID", parentId), log.Any("回复数", count))
|
||||
return nil
|
||||
}
|
||||
log.Error("刷新父评论回复数失败", log.Any("父评论ID", parentId), log.Any("回复数", count), log.E(serr))
|
||||
return serr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func refreshCommentTargetCount(ctx context.Context, objectType, objectId int64) *srvdef.SrvErr {
|
||||
// 评论数以当前审核通过的一级评论数量为准,审核通过、驳回和删除都走同一套重算逻辑。
|
||||
count, serr := commentmod.CountComments(ctx, bson.M{
|
||||
commentmod.FN.Comment.ObjectId: objectId,
|
||||
commentmod.FN.Comment.ObjectType: objectType,
|
||||
commentmod.FN.Comment.ReviewStatus: commentReviewPass,
|
||||
commentmod.FN.Comment.ParentsId: 0,
|
||||
})
|
||||
if serr != nil {
|
||||
log.Error("统计评论数失败", log.Any("对象类型", objectType), log.Any("对象ID", objectId), log.E(serr))
|
||||
return serr
|
||||
}
|
||||
return setCommentTargetCount(ctx, objectType, objectId, count)
|
||||
}
|
||||
|
||||
func setCommentTargetCount(ctx context.Context, objectType, objectId int64, count int64) *srvdef.SrvErr {
|
||||
// 根据对象类型把重算后的评论数写回业务集合。
|
||||
switch objectType {
|
||||
case commentObjectTypePlaylet:
|
||||
serr := playletmod.UpdateOnePlaylet(ctx, bson.M{
|
||||
playletmod.FN.Playlet.Id: objectId,
|
||||
}, bson.M{
|
||||
"$set": bson.M{playletmod.FN.Playlet.CountComment: count},
|
||||
})
|
||||
if serr != nil {
|
||||
log.Error("刷新短剧评论数失败", log.Any("短剧ID", objectId), log.Any("评论数", count), log.E(serr))
|
||||
return serr
|
||||
}
|
||||
case commentObjectTypePlayletChapter:
|
||||
serr := playletchaptermod.UpdateOnePlayletChapter(ctx, bson.M{
|
||||
playletchaptermod.FN.PlayletChapter.Id: objectId,
|
||||
}, bson.M{
|
||||
"$set": bson.M{playletchaptermod.FN.PlayletChapter.CountComment: count},
|
||||
})
|
||||
if serr != nil {
|
||||
log.Error("刷新剧集评论数失败", log.Any("剧集ID", objectId), log.Any("评论数", count), log.E(serr))
|
||||
return serr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isPlayletCommentType(objectType int64) bool {
|
||||
return objectType == commentObjectTypePlaylet || objectType == commentObjectTypePlayletChapter
|
||||
}
|
||||
|
||||
func isSupportedCommentType(objectType int64) bool {
|
||||
return isPlayletCommentType(objectType)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package srv_commend
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
// CommendSearch 查询评论
|
||||
type CommendSearch struct {
|
||||
ObjectId *int64 `json:"objectId" bson:"objectId"` // 对象ID
|
||||
ObjectType *int64 `json:"objectType"` // 对象类型 10-短剧 11-短剧剧集
|
||||
ObjectName *string `json:"objectName" bson:"objectName"` // 对象名(模糊)
|
||||
UserId *int64 `json:"userId"` // 评论用户ID
|
||||
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
|
||||
ObjectId int64 `json:"objectId" bson:"objectId"` // 对象ID
|
||||
ObjectType int64 `json:"objectType" bson:"objectType"` // 对象类型 10-短剧 11-短剧剧集
|
||||
ObjectName string `json:"objectName" bson:"objectName"` // 对象名
|
||||
UserId int64 `json:"userId" bson:"userId"` // 评论用户ID
|
||||
UserName string `json:"userName" bson:"userName"` // 用户名称
|
||||
UserAvatar string `json:"userAvatar" bson:"userAvatar"` // 评论用户头像
|
||||
Level int64 `json:"level" bson:"level"` // 评论级别
|
||||
ParentsId int64 `json:"parentsId" bson:"parentsId"` // 上级评论ID
|
||||
ReplyCommentId int64 `json:"replyCommentId" bson:"replyCommentId"` // 被回复评论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"` // 点赞数
|
||||
CountLike int64 `json:"countLike" bson:"countLike"` // 评论点赞数
|
||||
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
|
||||
ReplyCommentId int64 `json:"replyCommentId"` // 被回复评论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,273 @@
|
||||
package srv_contactus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/response"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/contactusmod"
|
||||
"videoweb/pub/models/usermod"
|
||||
"videoweb/web/api/proto"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const (
|
||||
// contactUsDefaultPageSize web端联系我们列表默认每页数量
|
||||
contactUsDefaultPageSize int64 = 10
|
||||
// contactUsMaxPageSize web端联系我们列表最大每页数量,避免单次请求拉取过多数据
|
||||
contactUsMaxPageSize int64 = 100
|
||||
)
|
||||
|
||||
var (
|
||||
// contactUsWebProjection 只读取web联系我们列表需要展示的字段,减少MongoDB传输开销。
|
||||
contactUsWebProjection = bson.D{
|
||||
{Key: contactusmod.FN.ContactUs.Id, Value: 1},
|
||||
{Key: contactusmod.FN.ContactUs.UserId, Value: 1},
|
||||
{Key: contactusmod.FN.ContactUs.Email, Value: 1},
|
||||
{Key: contactusmod.FN.ContactUs.QuestionType, Value: 1},
|
||||
{Key: contactusmod.FN.ContactUs.Title, Value: 1},
|
||||
{Key: contactusmod.FN.ContactUs.Content, Value: 1},
|
||||
{Key: contactusmod.FN.ContactUs.Attachments, Value: 1},
|
||||
{Key: contactusmod.FN.ContactUs.Status, Value: 1},
|
||||
{Key: contactusmod.FN.ContactUs.Remark, Value: 1},
|
||||
{Key: contactusmod.FN.ContactUs.CreatedAt, Value: 1},
|
||||
{Key: contactusmod.FN.ContactUs.UpdatedAt, Value: 1},
|
||||
}
|
||||
|
||||
// contactUsWebSort web端联系我们列表按创建时间倒序返回。
|
||||
contactUsWebSort = bson.D{
|
||||
{Key: contactusmod.FN.ContactUs.CreatedAt, Value: -1},
|
||||
{Key: contactusmod.FN.ContactUs.Id, Value: -1},
|
||||
}
|
||||
)
|
||||
|
||||
// ListContactUs 分页查询联系我们列表
|
||||
func ListContactUs(ctx context.Context, req *proto.ContactUsListReq) (*proto.ContactUsListPageResp, *srvdef.SrvErr) {
|
||||
if req == nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||||
}
|
||||
filter, filterErr := buildContactUsListFilter(req)
|
||||
if filterErr != nil {
|
||||
return nil, filterErr
|
||||
}
|
||||
pageNum, pageSize := normalizeContactUsPage(req.PageNum, req.PageSize)
|
||||
totalCount, err := contactusmod.CountContactUs(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := &proto.ContactUsListPageResp{
|
||||
BasePageResp: response.BasePageResp{
|
||||
TotalCount: totalCount,
|
||||
PageSize: int(pageSize),
|
||||
PageNum: int(pageNum),
|
||||
List: make([]*proto.ContactUsListResp, 0),
|
||||
},
|
||||
}
|
||||
if totalCount == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
list, err := contactusmod.FindManyContactUs(ctx, filter, options.Find().
|
||||
SetProjection(contactUsWebProjection).
|
||||
SetSort(contactUsWebSort).
|
||||
SetSkip((pageNum-1)*pageSize).
|
||||
SetLimit(pageSize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userNameMap, err := buildContactUsUserNameMap(ctx, list)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.List = contactUsListToWebResp(list, userNameMap)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// HandleContactUs 批量更新联系我们处理状态
|
||||
func HandleContactUs(ctx context.Context, req *proto.ContactUsHandleReq) (*proto.ContactUsHandleResp, *srvdef.SrvErr) {
|
||||
if req == nil || len(req.Ids) == 0 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "ids不能为空")
|
||||
}
|
||||
if req.Status != 0 && req.Status != 1 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "status无效")
|
||||
}
|
||||
ids, err := normalizeContactUsIds(req.Ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matchedCount, modifiedCount, err := contactusmod.UpdateManyContactUsWithResult(ctx, bson.M{
|
||||
contactusmod.FN.ContactUs.Id: bson.M{"$in": ids},
|
||||
}, bson.M{
|
||||
"$set": bson.M{
|
||||
contactusmod.FN.ContactUs.Status: req.Status,
|
||||
contactusmod.FN.ContactUs.Remark: strings.TrimSpace(req.Remark),
|
||||
contactusmod.FN.ContactUs.UpdatedAt: time.Now(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if matchedCount == 0 {
|
||||
return nil, srvdef.New(srvdef.ErrNoData, "", "")
|
||||
}
|
||||
return &proto.ContactUsHandleResp{
|
||||
MatchedCount: matchedCount,
|
||||
ModifiedCount: modifiedCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// buildContactUsListFilter 构造web端联系我们列表查询条件
|
||||
func buildContactUsListFilter(req *proto.ContactUsListReq) (bson.M, *srvdef.SrvErr) {
|
||||
filter := bson.M{}
|
||||
if req.UserId != nil && *req.UserId > 0 {
|
||||
filter[contactusmod.FN.ContactUs.UserId] = *req.UserId
|
||||
}
|
||||
if email := strings.TrimSpace(req.Email); email != "" {
|
||||
filter[contactusmod.FN.ContactUs.Email] = bson.M{"$regex": regexp.QuoteMeta(email)}
|
||||
}
|
||||
if req.Type != nil {
|
||||
if !isValidContactUsType(*req.Type) {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "type无效")
|
||||
}
|
||||
filter[contactusmod.FN.ContactUs.QuestionType] = *req.Type
|
||||
}
|
||||
if req.Status != nil {
|
||||
if *req.Status != 0 && *req.Status != 1 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "status无效")
|
||||
}
|
||||
filter[contactusmod.FN.ContactUs.Status] = *req.Status
|
||||
}
|
||||
createdAt, err := buildContactUsTimeFilter(req.CreateStartTime, req.CreateEndTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(createdAt) > 0 {
|
||||
filter[contactusmod.FN.ContactUs.CreatedAt] = createdAt
|
||||
}
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
// buildContactUsTimeFilter 按开始时间和结束时间构造createdAt范围查询
|
||||
func buildContactUsTimeFilter(startTime, endTime string) (bson.M, *srvdef.SrvErr) {
|
||||
filter := bson.M{}
|
||||
startTime = strings.TrimSpace(startTime)
|
||||
endTime = strings.TrimSpace(endTime)
|
||||
if startTime != "" {
|
||||
startAt, err := parseContactUsWebTime(startTime)
|
||||
if err != nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "createStartTime格式错误")
|
||||
}
|
||||
filter["$gte"] = startAt
|
||||
}
|
||||
if endTime != "" {
|
||||
endAt, err := parseContactUsWebTime(endTime)
|
||||
if err != nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "createEndTime格式错误")
|
||||
}
|
||||
filter["$lte"] = endAt
|
||||
}
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
func parseContactUsWebTime(value string) (time.Time, error) {
|
||||
return time.ParseInLocation("2006-01-02 15:04:05", value, time.Local)
|
||||
}
|
||||
|
||||
func normalizeContactUsPage(pageNum, pageSize int64) (int64, int64) {
|
||||
if pageNum <= 0 {
|
||||
pageNum = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = contactUsDefaultPageSize
|
||||
}
|
||||
if pageSize > contactUsMaxPageSize {
|
||||
pageSize = contactUsMaxPageSize
|
||||
}
|
||||
return pageNum, pageSize
|
||||
}
|
||||
|
||||
func normalizeContactUsIds(rawIds []int64) ([]int64, *srvdef.SrvErr) {
|
||||
ids := make([]int64, 0, len(rawIds))
|
||||
seen := make(map[int64]struct{}, len(rawIds))
|
||||
for _, id := range rawIds {
|
||||
if id <= 0 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "ids包含无效id")
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "ids不能为空")
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func isValidContactUsType(questionType int32) bool {
|
||||
return questionType >= 1 && questionType <= 5
|
||||
}
|
||||
|
||||
// buildContactUsUserNameMap 批量查询用户名称,避免列表转换时逐条查询用户。
|
||||
func buildContactUsUserNameMap(ctx context.Context, list contactusmod.ContactUsList) (map[int64]string, *srvdef.SrvErr) {
|
||||
userIds := make([]int64, 0, len(list))
|
||||
seen := make(map[int64]struct{}, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil || item.UserId <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[item.UserId]; ok {
|
||||
continue
|
||||
}
|
||||
seen[item.UserId] = struct{}{}
|
||||
userIds = append(userIds, item.UserId)
|
||||
}
|
||||
userNameMap := make(map[int64]string, len(userIds))
|
||||
if len(userIds) == 0 {
|
||||
return userNameMap, nil
|
||||
}
|
||||
users, err := usermod.FindManyUser(ctx, bson.M{
|
||||
usermod.FN.User.Id: bson.M{"$in": userIds},
|
||||
}, options.Find().SetProjection(bson.M{
|
||||
usermod.FN.User.Id: 1,
|
||||
usermod.FN.User.NickName: 1,
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, user := range users {
|
||||
if user != nil {
|
||||
userNameMap[user.Id] = user.NickName
|
||||
}
|
||||
}
|
||||
return userNameMap, nil
|
||||
}
|
||||
|
||||
// contactUsListToWebResp 转换联系我们列表响应
|
||||
func contactUsListToWebResp(list contactusmod.ContactUsList, userNameMap map[int64]string) []*proto.ContactUsListResp {
|
||||
resp := make([]*proto.ContactUsListResp, 0, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
resp = append(resp, &proto.ContactUsListResp{
|
||||
Id: item.Id,
|
||||
UserId: item.UserId,
|
||||
UserName: userNameMap[item.UserId],
|
||||
Email: item.Email,
|
||||
Type: item.QuestionType,
|
||||
Title: item.Title,
|
||||
Content: item.Content,
|
||||
Attachments: item.Attachments,
|
||||
Status: item.Status,
|
||||
Remark: item.Remark,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
})
|
||||
}
|
||||
return resp
|
||||
}
|
||||
@@ -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,271 @@
|
||||
package srv_feedback
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/response"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/feedbackmod"
|
||||
"videoweb/pub/models/usermod"
|
||||
"videoweb/web/api/proto"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const (
|
||||
// feedbackDefaultPageSize web端意见反馈列表默认每页数量
|
||||
feedbackDefaultPageSize int64 = 10
|
||||
// feedbackMaxPageSize web端意见反馈列表最大每页数量,避免单次请求拉取过多数据
|
||||
feedbackMaxPageSize int64 = 100
|
||||
)
|
||||
|
||||
var (
|
||||
// feedbackWebProjection 只读取web意见反馈列表需要展示的字段,减少MongoDB传输开销。
|
||||
feedbackWebProjection = bson.D{
|
||||
{Key: feedbackmod.FN.Feedback.Id, Value: 1},
|
||||
{Key: feedbackmod.FN.Feedback.UserId, Value: 1},
|
||||
{Key: feedbackmod.FN.Feedback.FeedbackType, Value: 1},
|
||||
{Key: feedbackmod.FN.Feedback.Content, Value: 1},
|
||||
{Key: feedbackmod.FN.Feedback.Screenshots, Value: 1},
|
||||
{Key: feedbackmod.FN.Feedback.Contact, Value: 1},
|
||||
{Key: feedbackmod.FN.Feedback.Status, Value: 1},
|
||||
{Key: feedbackmod.FN.Feedback.Remark, Value: 1},
|
||||
{Key: feedbackmod.FN.Feedback.CreatedAt, Value: 1},
|
||||
{Key: feedbackmod.FN.Feedback.UpdatedAt, Value: 1},
|
||||
}
|
||||
|
||||
// feedbackWebSort web端意见反馈列表按创建时间倒序返回。
|
||||
feedbackWebSort = bson.D{
|
||||
{Key: feedbackmod.FN.Feedback.CreatedAt, Value: -1},
|
||||
{Key: feedbackmod.FN.Feedback.Id, Value: -1},
|
||||
}
|
||||
)
|
||||
|
||||
// ListFeedback 分页查询意见反馈列表
|
||||
func ListFeedback(ctx context.Context, req *proto.FeedbackListReq) (*proto.FeedbackListPageResp, *srvdef.SrvErr) {
|
||||
if req == nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||||
}
|
||||
filter, filterErr := buildFeedbackListFilter(req)
|
||||
if filterErr != nil {
|
||||
return nil, filterErr
|
||||
}
|
||||
pageNum, pageSize := normalizeFeedbackPage(req.PageNum, req.PageSize)
|
||||
totalCount, err := feedbackmod.CountFeedback(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := &proto.FeedbackListPageResp{
|
||||
BasePageResp: response.BasePageResp{
|
||||
TotalCount: totalCount,
|
||||
PageSize: int(pageSize),
|
||||
PageNum: int(pageNum),
|
||||
List: make([]*proto.FeedbackListResp, 0),
|
||||
},
|
||||
}
|
||||
if totalCount == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
list, err := feedbackmod.FindManyFeedback(ctx, filter, options.Find().
|
||||
SetProjection(feedbackWebProjection).
|
||||
SetSort(feedbackWebSort).
|
||||
SetSkip((pageNum-1)*pageSize).
|
||||
SetLimit(pageSize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userNameMap, err := buildFeedbackUserNameMap(ctx, list)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.List = feedbackListToWebResp(list, userNameMap)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// HandleFeedback 批量更新意见反馈处理状态
|
||||
func HandleFeedback(ctx context.Context, req *proto.FeedbackHandleReq) (*proto.FeedbackHandleResp, *srvdef.SrvErr) {
|
||||
if req == nil || len(req.Ids) == 0 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "ids不能为空")
|
||||
}
|
||||
if req.Status != 0 && req.Status != 1 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "status无效")
|
||||
}
|
||||
ids, err := normalizeFeedbackIds(req.Ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matchedCount, modifiedCount, err := feedbackmod.UpdateManyFeedbackWithResult(ctx, bson.M{
|
||||
feedbackmod.FN.Feedback.Id: bson.M{"$in": ids},
|
||||
}, bson.M{
|
||||
"$set": bson.M{
|
||||
feedbackmod.FN.Feedback.Status: req.Status,
|
||||
feedbackmod.FN.Feedback.Remark: strings.TrimSpace(req.Remark),
|
||||
feedbackmod.FN.Feedback.UpdatedAt: time.Now(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if matchedCount == 0 {
|
||||
return nil, srvdef.New(srvdef.ErrNoData, "", "")
|
||||
}
|
||||
return &proto.FeedbackHandleResp{
|
||||
MatchedCount: matchedCount,
|
||||
ModifiedCount: modifiedCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// buildFeedbackListFilter 构造web端意见反馈列表查询条件
|
||||
func buildFeedbackListFilter(req *proto.FeedbackListReq) (bson.M, *srvdef.SrvErr) {
|
||||
filter := bson.M{}
|
||||
if req.UserId != nil && *req.UserId > 0 {
|
||||
filter[feedbackmod.FN.Feedback.UserId] = *req.UserId
|
||||
}
|
||||
if req.Type != nil {
|
||||
if !isValidFeedbackType(*req.Type) {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "type无效")
|
||||
}
|
||||
filter[feedbackmod.FN.Feedback.FeedbackType] = *req.Type
|
||||
}
|
||||
if req.Status != nil {
|
||||
if *req.Status != 0 && *req.Status != 1 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "status无效")
|
||||
}
|
||||
filter[feedbackmod.FN.Feedback.Status] = *req.Status
|
||||
}
|
||||
createdAt, err := buildFeedbackTimeFilter(req.CreateStartTime, req.CreateEndTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(createdAt) > 0 {
|
||||
filter[feedbackmod.FN.Feedback.CreatedAt] = createdAt
|
||||
}
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
// buildFeedbackTimeFilter 按开始时间和结束时间构造createdAt范围查询
|
||||
func buildFeedbackTimeFilter(startTime, endTime string) (bson.M, *srvdef.SrvErr) {
|
||||
filter := bson.M{}
|
||||
startTime = strings.TrimSpace(startTime)
|
||||
endTime = strings.TrimSpace(endTime)
|
||||
if startTime != "" {
|
||||
startAt, err := parseFeedbackWebTime(startTime)
|
||||
if err != nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "createStartTime格式错误")
|
||||
}
|
||||
filter["$gte"] = startAt
|
||||
}
|
||||
if endTime != "" {
|
||||
endAt, err := parseFeedbackWebTime(endTime)
|
||||
if err != nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "createEndTime格式错误")
|
||||
}
|
||||
filter["$lte"] = endAt
|
||||
}
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
func parseFeedbackWebTime(value string) (time.Time, error) {
|
||||
return time.ParseInLocation("2006-01-02 15:04:05", value, time.Local)
|
||||
}
|
||||
|
||||
func normalizeFeedbackPage(pageNum, pageSize int64) (int64, int64) {
|
||||
if pageNum <= 0 {
|
||||
pageNum = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = feedbackDefaultPageSize
|
||||
}
|
||||
if pageSize > feedbackMaxPageSize {
|
||||
pageSize = feedbackMaxPageSize
|
||||
}
|
||||
return pageNum, pageSize
|
||||
}
|
||||
|
||||
func normalizeFeedbackIds(rawIds []int64) ([]int64, *srvdef.SrvErr) {
|
||||
ids := make([]int64, 0, len(rawIds))
|
||||
seen := make(map[int64]struct{}, len(rawIds))
|
||||
for _, id := range rawIds {
|
||||
if id <= 0 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "ids包含无效id")
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "ids不能为空")
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func isValidFeedbackType(feedbackType int32) bool {
|
||||
return feedbackType >= 1 && feedbackType <= 5
|
||||
}
|
||||
|
||||
// buildFeedbackUserNameMap 批量查询反馈用户名称,避免列表转换时逐条查询用户。
|
||||
func buildFeedbackUserNameMap(ctx context.Context, list feedbackmod.FeedbackList) (map[int64]string, *srvdef.SrvErr) {
|
||||
userIds := make([]int64, 0, len(list))
|
||||
seen := make(map[int64]struct{}, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil || item.UserId <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[item.UserId]; ok {
|
||||
continue
|
||||
}
|
||||
seen[item.UserId] = struct{}{}
|
||||
userIds = append(userIds, item.UserId)
|
||||
}
|
||||
userNameMap := make(map[int64]string, len(userIds))
|
||||
if len(userIds) == 0 {
|
||||
return userNameMap, nil
|
||||
}
|
||||
users, err := usermod.FindManyUser(ctx, bson.M{
|
||||
usermod.FN.User.Id: bson.M{"$in": userIds},
|
||||
}, options.Find().SetProjection(bson.M{
|
||||
usermod.FN.User.Id: 1,
|
||||
usermod.FN.User.NickName: 1,
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, user := range users {
|
||||
if user != nil {
|
||||
userNameMap[user.Id] = user.NickName
|
||||
}
|
||||
}
|
||||
return userNameMap, nil
|
||||
}
|
||||
|
||||
// feedbackListToWebResp 转换意见反馈列表响应
|
||||
func feedbackListToWebResp(list feedbackmod.FeedbackList, userNameMap map[int64]string) []*proto.FeedbackListResp {
|
||||
resp := make([]*proto.FeedbackListResp, 0, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
screenshots := item.Screenshots
|
||||
if screenshots == nil {
|
||||
screenshots = make([]string, 0)
|
||||
}
|
||||
resp = append(resp, &proto.FeedbackListResp{
|
||||
Id: item.Id,
|
||||
UserId: item.UserId,
|
||||
UserName: userNameMap[item.UserId],
|
||||
Type: item.FeedbackType,
|
||||
Content: item.Content,
|
||||
Screenshots: screenshots,
|
||||
Contact: item.Contact,
|
||||
Status: item.Status,
|
||||
Remark: item.Remark,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
})
|
||||
}
|
||||
return resp
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
package srv_homebanner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/playletcache"
|
||||
"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
|
||||
}
|
||||
clearHomeBannerCache()
|
||||
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
|
||||
if err := homebannermod.UpdateOneHomeBanner(ctx,
|
||||
bson.M{homebannermod.FN.HomeBanner.Id: req.Id},
|
||||
bson.M{"$set": setDoc},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
clearHomeBannerCache()
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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, "", "")
|
||||
}
|
||||
clearHomeBannerCache()
|
||||
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
|
||||
}
|
||||
|
||||
// clearHomeBannerCache 清除短剧首页缓存
|
||||
func clearHomeBannerCache() {
|
||||
playletcache.ClearHomeCache()
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
package srv_homeconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/playletcache"
|
||||
"videoweb/pub/common/response"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/homeconfigmod"
|
||||
"videoweb/pub/models/translationcategorymod"
|
||||
"videoweb/pub/pubdef"
|
||||
"videoweb/pub/seo/sitemapdirty"
|
||||
"videoweb/web/api/proto"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultHomeConfigPageSize int64 = 10
|
||||
maxHomeConfigPageSize int64 = 100
|
||||
)
|
||||
|
||||
var homeConfigWebProjection = bson.D{
|
||||
{Key: homeconfigmod.FN.HomeConfig.Id, Value: 1},
|
||||
{Key: homeconfigmod.FN.HomeConfig.Name, Value: 1},
|
||||
{Key: homeconfigmod.FN.HomeConfig.Slug, Value: 1},
|
||||
{Key: homeconfigmod.FN.HomeConfig.MoreCategoryId, Value: 1},
|
||||
{Key: homeconfigmod.FN.HomeConfig.Desc, Value: 1},
|
||||
{Key: homeconfigmod.FN.HomeConfig.Type, Value: 1},
|
||||
{Key: homeconfigmod.FN.HomeConfig.Rank, Value: 1},
|
||||
{Key: homeconfigmod.FN.HomeConfig.SortType, Value: 1},
|
||||
{Key: homeconfigmod.FN.HomeConfig.Status, Value: 1},
|
||||
{Key: homeconfigmod.FN.HomeConfig.SeoInfo.Title, Value: 1},
|
||||
{Key: homeconfigmod.FN.HomeConfig.SeoInfo.Description, Value: 1},
|
||||
{Key: homeconfigmod.FN.HomeConfig.SeoInfo.Keyworld, Value: 1},
|
||||
{Key: homeconfigmod.FN.HomeConfig.CreatedAt, Value: 1},
|
||||
{Key: homeconfigmod.FN.HomeConfig.UpdatedAt, Value: 1},
|
||||
}
|
||||
|
||||
// ListHomeConfig 分页查询首页配置列表
|
||||
func ListHomeConfig(ctx context.Context, req *proto.HomeConfigListReq) (*proto.HomeConfigListPageResp, *srvdef.SrvErr) {
|
||||
if req == nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||||
}
|
||||
filter := bson.M{}
|
||||
if req.Id != nil && *req.Id > 0 {
|
||||
filter[homeconfigmod.FN.HomeConfig.Id] = *req.Id
|
||||
}
|
||||
if req.Type != nil {
|
||||
if !validHomeConfigType(*req.Type) {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "类型无效", "")
|
||||
}
|
||||
filter[homeconfigmod.FN.HomeConfig.Type] = *req.Type
|
||||
}
|
||||
if req.SortType != nil {
|
||||
if !validHomeConfigSortType(*req.SortType) {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "排序字段无效", "")
|
||||
}
|
||||
filter[homeconfigmod.FN.HomeConfig.SortType] = *req.SortType
|
||||
}
|
||||
if req.Status != nil {
|
||||
filter[homeconfigmod.FN.HomeConfig.Status] = *req.Status
|
||||
}
|
||||
if name := strings.TrimSpace(req.Name); name != "" {
|
||||
filter[homeconfigmod.FN.HomeConfig.Name] = bson.M{"$regex": regexp.QuoteMeta(name)}
|
||||
}
|
||||
if slug := strings.TrimSpace(req.Slug); slug != "" {
|
||||
filter[homeconfigmod.FN.HomeConfig.Slug] = slug
|
||||
}
|
||||
pageNum, pageSize := normalizeHomeConfigPage(req.PageNum, req.PageSize)
|
||||
totalCount, err := homeconfigmod.CountHomeConfig(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := &proto.HomeConfigListPageResp{
|
||||
BasePageResp: response.BasePageResp{
|
||||
TotalCount: totalCount,
|
||||
PageSize: int(pageSize),
|
||||
PageNum: int(pageNum),
|
||||
List: make([]*proto.HomeConfigListResp, 0),
|
||||
},
|
||||
}
|
||||
if totalCount == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
list, err := homeconfigmod.FindManyHomeConfig(ctx, filter, options.Find().
|
||||
SetProjection(homeConfigWebProjection).
|
||||
SetSort(bson.D{
|
||||
{Key: homeconfigmod.FN.HomeConfig.Rank, Value: -1},
|
||||
{Key: homeconfigmod.FN.HomeConfig.UpdatedAt, Value: -1},
|
||||
{Key: homeconfigmod.FN.HomeConfig.Id, Value: -1},
|
||||
}).
|
||||
SetSkip((pageNum-1)*pageSize).
|
||||
SetLimit(pageSize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]*proto.HomeConfigListResp, 0, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, &proto.HomeConfigListResp{
|
||||
Id: item.Id,
|
||||
MoreCategoryId: item.MoreCategoryId,
|
||||
Name: item.Name,
|
||||
Slug: item.Slug,
|
||||
Desc: item.Desc,
|
||||
Type: item.Type,
|
||||
Rank: item.Rank,
|
||||
SortType: item.SortType,
|
||||
Status: item.Status,
|
||||
SeoInfo: normalizeHomeConfigSeoValue(item.SeoInfo),
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
})
|
||||
}
|
||||
resp.List = items
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// AddHomeConfig 新增首页配置
|
||||
func AddHomeConfig(ctx context.Context, req *proto.HomeConfigAddReq) (*proto.HomeConfigAddResp, *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 !validHomeConfigType(req.Type) {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "类型无效", "")
|
||||
}
|
||||
if !validHomeConfigSortType(req.SortType) {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "排序字段无效", "")
|
||||
}
|
||||
if req.Status == nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "状态不能为空", "")
|
||||
}
|
||||
if err := validateHomeMoreCategory(ctx, req.MoreCategoryId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
doc := &homeconfigmod.HomeConfig{
|
||||
Name: name,
|
||||
MoreCategoryId: req.MoreCategoryId,
|
||||
Slug: strings.TrimSpace(req.Slug),
|
||||
Desc: strings.TrimSpace(req.Desc),
|
||||
Type: req.Type,
|
||||
Rank: req.Rank,
|
||||
SortType: req.SortType,
|
||||
Status: *req.Status,
|
||||
SeoInfo: normalizeHomeConfigSeoValue(req.SeoInfo),
|
||||
}
|
||||
if err := ensureHomeConfigSlug(ctx, doc, 0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := homeconfigmod.InsertOneHomeConfig(ctx, doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clearHomeConfigCache()
|
||||
sitemapdirty.Mark(ctx)
|
||||
return &proto.HomeConfigAddResp{Id: doc.Id}, nil
|
||||
}
|
||||
|
||||
// UpdateHomeConfig 更新首页配置
|
||||
func UpdateHomeConfig(ctx context.Context, req *proto.HomeConfigUpdateReq) *srvdef.SrvErr {
|
||||
if req == nil || req.Id <= 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "首页配置 ID无效", "")
|
||||
}
|
||||
info, err := homeconfigmod.FindOneHomeConfig(ctx, bson.M{homeconfigmod.FN.HomeConfig.Id: req.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setDoc := bson.M{homeconfigmod.FN.HomeConfig.UpdatedAt: time.Now()}
|
||||
if req.MoreCategoryId != nil {
|
||||
if err := validateHomeMoreCategory(ctx, *req.MoreCategoryId); err != nil {
|
||||
return err
|
||||
}
|
||||
setDoc[homeconfigmod.FN.HomeConfig.MoreCategoryId] = *req.MoreCategoryId
|
||||
}
|
||||
if req.Name != nil {
|
||||
name := strings.TrimSpace(*req.Name)
|
||||
if name == "" {
|
||||
return srvdef.New(srvdef.ErrParamError, "名称不能为空", "")
|
||||
}
|
||||
setDoc[homeconfigmod.FN.HomeConfig.Name] = name
|
||||
}
|
||||
if req.Desc != nil {
|
||||
setDoc[homeconfigmod.FN.HomeConfig.Desc] = strings.TrimSpace(*req.Desc)
|
||||
}
|
||||
if req.Type != nil {
|
||||
if !validHomeConfigType(*req.Type) {
|
||||
return srvdef.New(srvdef.ErrParamError, "类型无效", "")
|
||||
}
|
||||
setDoc[homeconfigmod.FN.HomeConfig.Type] = *req.Type
|
||||
}
|
||||
if req.Rank != nil {
|
||||
setDoc[homeconfigmod.FN.HomeConfig.Rank] = *req.Rank
|
||||
}
|
||||
if req.SortType != nil {
|
||||
if !validHomeConfigSortType(*req.SortType) {
|
||||
return srvdef.New(srvdef.ErrParamError, "排序字段无效", "")
|
||||
}
|
||||
setDoc[homeconfigmod.FN.HomeConfig.SortType] = *req.SortType
|
||||
}
|
||||
if req.Status != nil {
|
||||
setDoc[homeconfigmod.FN.HomeConfig.Status] = *req.Status
|
||||
}
|
||||
if req.SeoInfo != nil {
|
||||
setDoc["seoInfo"] = normalizeHomeConfigSeoValue(*req.SeoInfo)
|
||||
}
|
||||
if serr := prepareHomeConfigSlugOnUpdate(ctx, req.Id, info, req, setDoc); serr != nil {
|
||||
return serr
|
||||
}
|
||||
if len(setDoc) <= 1 {
|
||||
return srvdef.New(srvdef.ErrParamError, "没有可更新字段", "")
|
||||
}
|
||||
if err := homeconfigmod.UpdateOneHomeConfig(ctx,
|
||||
bson.M{homeconfigmod.FN.HomeConfig.Id: req.Id},
|
||||
bson.M{"$set": setDoc},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
clearHomeConfigCache()
|
||||
sitemapdirty.Mark(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelHomeConfig 删除首页配置
|
||||
func DelHomeConfig(ctx context.Context, req *proto.HomeConfigDelReq) *srvdef.SrvErr {
|
||||
if req == nil || req.Id <= 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "首页配置 ID无效", "")
|
||||
}
|
||||
count, err := homeconfigmod.DelOneHomeConfig(ctx, bson.M{homeconfigmod.FN.HomeConfig.Id: req.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return srvdef.New(srvdef.ErrNoData, "", "")
|
||||
}
|
||||
clearHomeConfigCache()
|
||||
sitemapdirty.Mark(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
func validHomeConfigType(typ int32) bool {
|
||||
return typ == homeconfigmod.TypePlaylet || typ == homeconfigmod.TypeTheme
|
||||
}
|
||||
|
||||
func validateHomeMoreCategory(ctx context.Context, id int64) *srvdef.SrvErr {
|
||||
if id < 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "更多跳转目标ID无效", "")
|
||||
}
|
||||
if id == 0 {
|
||||
return nil
|
||||
}
|
||||
fields := translationcategorymod.FN.TranslationCategory
|
||||
category, err := translationcategorymod.FindOneTranslationCategory(ctx, bson.M{
|
||||
fields.Id: id,
|
||||
fields.Status: true,
|
||||
fields.HomeMoreEnabled: true,
|
||||
fields.CategoryType: pubdef.TranslationTypeUrl,
|
||||
})
|
||||
if err != nil {
|
||||
if err.Code == srvdef.ErrNoData {
|
||||
return srvdef.New(srvdef.ErrParamError, "更多跳转目标不可用", "")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(category.UrlValue) == "" {
|
||||
return srvdef.New(srvdef.ErrParamError, "更多跳转目标路径为空", "")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validHomeConfigSortType(sortType int32) bool {
|
||||
return sortType == homeconfigmod.SortTypeRecommend ||
|
||||
sortType == homeconfigmod.SortTypeHot ||
|
||||
sortType == homeconfigmod.SortTypeLatest ||
|
||||
sortType == homeconfigmod.SortTypePreview ||
|
||||
sortType == homeconfigmod.SortTypeCategory
|
||||
}
|
||||
|
||||
func clearHomeConfigCache() {
|
||||
playletcache.ClearHomeCache()
|
||||
}
|
||||
|
||||
// normalizeHomeConfigPage 规范分页参数,限制单页数量避免大分页拖慢查询
|
||||
func normalizeHomeConfigPage(pageNum, pageSize int64) (int64, int64) {
|
||||
if pageNum <= 0 {
|
||||
pageNum = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = defaultHomeConfigPageSize
|
||||
}
|
||||
if pageSize > maxHomeConfigPageSize {
|
||||
pageSize = maxHomeConfigPageSize
|
||||
}
|
||||
return pageNum, pageSize
|
||||
}
|
||||
|
||||
// normalizeHomeConfigSeoValue 规范化 SEO 字段,避免 keywords 为 null
|
||||
func normalizeHomeConfigSeoValue(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
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package srv_homeconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/homeconfigmod"
|
||||
"videoweb/web/api/proto"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func ensureHomeConfigSlug(ctx context.Context, ac *homeconfigmod.HomeConfig, excludeId int64) *srvdef.SrvErr {
|
||||
ac.Slug = utils.SanitizeSlug(ac.Slug)
|
||||
if ac.Slug != "" {
|
||||
if utils.IsSlugAllDigits(ac.Slug) {
|
||||
return srvdef.New(srvdef.ErrParamError, "Slug不能全是数字", "Slug不能全是数字")
|
||||
}
|
||||
filter := bson.M{homeconfigmod.FN.HomeConfig.Slug: ac.Slug}
|
||||
if excludeId > 0 {
|
||||
filter[homeconfigmod.FN.HomeConfig.Id] = bson.M{"$ne": excludeId}
|
||||
}
|
||||
exist, _ := homeconfigmod.FindOneHomeConfig(ctx, filter)
|
||||
if exist != nil {
|
||||
return srvdef.New(srvdef.ErrDbInputExist, "Slug已存在", "Slug已存在")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
source := utils.SlugSourceFromKeywordsOrTitle(ac.SeoInfo.Keyworld, ac.Name)
|
||||
ac.Slug = utils.MakeUniqueSlug(source, func(slug string) bool {
|
||||
filter := bson.M{homeconfigmod.FN.HomeConfig.Slug: slug}
|
||||
if excludeId > 0 {
|
||||
filter[homeconfigmod.FN.HomeConfig.Id] = bson.M{"$ne": excludeId}
|
||||
}
|
||||
exist, _ := homeconfigmod.FindOneHomeConfig(ctx, filter)
|
||||
return exist != nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareHomeConfigSlugOnUpdate(ctx context.Context, id int64, info *homeconfigmod.HomeConfig, req *proto.HomeConfigUpdateReq, setDoc bson.M) *srvdef.SrvErr {
|
||||
slug := info.Slug
|
||||
if req.Slug != nil {
|
||||
slug = utils.SanitizeSlug(*req.Slug)
|
||||
}
|
||||
if slug != "" {
|
||||
if req.Slug != nil {
|
||||
if utils.IsSlugAllDigits(slug) {
|
||||
return srvdef.New(srvdef.ErrParamError, "Slug不能全是数字", "Slug不能全是数字")
|
||||
}
|
||||
exist, _ := homeconfigmod.FindOneHomeConfig(ctx, bson.M{
|
||||
homeconfigmod.FN.HomeConfig.Slug: slug,
|
||||
homeconfigmod.FN.HomeConfig.Id: bson.M{"$ne": id},
|
||||
})
|
||||
if exist != nil {
|
||||
return srvdef.New(srvdef.ErrDbInputExist, "Slug已存在", "Slug已存在")
|
||||
}
|
||||
setDoc[homeconfigmod.FN.HomeConfig.Slug] = slug
|
||||
}
|
||||
return nil
|
||||
}
|
||||
name := info.Name
|
||||
if req.Name != nil {
|
||||
name = strings.TrimSpace(*req.Name)
|
||||
}
|
||||
keywords := info.SeoInfo.Keyworld
|
||||
if req.SeoInfo != nil {
|
||||
keywords = normalizeHomeConfigSeoValue(*req.SeoInfo).Keyworld
|
||||
}
|
||||
source := utils.SlugSourceFromKeywordsOrTitle(keywords, name)
|
||||
setDoc[homeconfigmod.FN.HomeConfig.Slug] = utils.MakeUniqueSlug(source, func(s string) bool {
|
||||
exist, _ := homeconfigmod.FindOneHomeConfig(ctx, bson.M{
|
||||
homeconfigmod.FN.HomeConfig.Slug: s,
|
||||
homeconfigmod.FN.HomeConfig.Id: bson.M{"$ne": id},
|
||||
})
|
||||
return exist != nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func emptyHomeConfigSlugFilter() bson.M {
|
||||
return bson.M{
|
||||
"$or": []bson.M{
|
||||
{homeconfigmod.FN.HomeConfig.Slug: ""},
|
||||
{homeconfigmod.FN.HomeConfig.Slug: bson.M{"$exists": false}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// SyncEmptySlugs 为首页配置中空 slug 自动生成。
|
||||
func SyncEmptySlugs(ctx context.Context, maxSize int64) (int64, bool, *srvdef.SrvErr) {
|
||||
if maxSize <= 0 {
|
||||
return 0, false, nil
|
||||
}
|
||||
list, err := homeconfigmod.FindManyHomeConfig(ctx, emptyHomeConfigSlugFilter(), options.Find().
|
||||
SetSort(bson.D{{Key: homeconfigmod.FN.HomeConfig.Id, Value: 1}}).
|
||||
SetLimit(maxSize+1))
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
hasMore := int64(len(list)) > maxSize
|
||||
if hasMore {
|
||||
list = list[:maxSize]
|
||||
}
|
||||
var updated int64
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
if serr := ensureHomeConfigSlug(ctx, item, item.Id); serr != nil {
|
||||
return updated, hasMore, serr
|
||||
}
|
||||
if strings.TrimSpace(item.Slug) == "" {
|
||||
continue
|
||||
}
|
||||
if serr := homeconfigmod.UpdateOneHomeConfig(ctx,
|
||||
bson.M{homeconfigmod.FN.HomeConfig.Id: item.Id},
|
||||
bson.M{"$set": bson.M{homeconfigmod.FN.HomeConfig.Slug: item.Slug}},
|
||||
); serr != nil {
|
||||
return updated, hasMore, serr
|
||||
}
|
||||
updated++
|
||||
}
|
||||
return updated, hasMore, nil
|
||||
}
|
||||
|
||||
func HasEmptySlug(ctx context.Context) bool {
|
||||
count, _ := homeconfigmod.CountHomeConfig(ctx, emptyHomeConfigSlugFilter())
|
||||
return count > 0
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package srv_ipstat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/ipdailystatmod"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
// IpStatResp 今日和昨日访问 IP 去重数。
|
||||
type IpStatResp struct {
|
||||
TodayIP int64 `json:"todayIP"`
|
||||
YesterdayIP int64 `json:"yesterdayIP"`
|
||||
}
|
||||
|
||||
// FetchIpStat 返回 app 端同步到 MongoDB 的每日访问 IP 去重数。
|
||||
func FetchIpStat(ctx context.Context) *IpStatResp {
|
||||
resp := &IpStatResp{}
|
||||
now := time.Now().In(utils.LocBJ)
|
||||
today := now.Format("20060102")
|
||||
yesterday := now.AddDate(0, 0, -1).Format("20060102")
|
||||
|
||||
if cnt, err := ipdailystatmod.FindCount(ctx, pubdef.AppId, today); err == nil {
|
||||
resp.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 {
|
||||
resp.YesterdayIP = cnt
|
||||
} else {
|
||||
log.Warn("FetchIpStat: FindCount yesterday failed",
|
||||
log.Any("appId", pubdef.AppId), log.E(err))
|
||||
}
|
||||
return resp
|
||||
}
|
||||
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,228 @@
|
||||
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/common/playletcache"
|
||||
"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
|
||||
playletcache.ClearHomeCache()
|
||||
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
|
||||
}
|
||||
|
||||
// FillPlayletCompressedHorizontalCover 为指定短剧补写压缩横版封面,已有值则跳过。
|
||||
func FillPlayletCompressedHorizontalCover(ctx context.Context, playlet *playletmod.Playlet) bool {
|
||||
if playlet == nil || playlet.Id <= 0 {
|
||||
return false
|
||||
}
|
||||
if strings.TrimSpace(playlet.CompressedHorizontalCover) != "" {
|
||||
return false
|
||||
}
|
||||
cover := strings.TrimSpace(playlet.HorizontalCover)
|
||||
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.CompressedHorizontalCover: bson.M{"$exists": false}},
|
||||
{playletmod.FN.Playlet.CompressedHorizontalCover: ""},
|
||||
{playletmod.FN.Playlet.CompressedHorizontalCover: nil},
|
||||
},
|
||||
},
|
||||
bson.M{"$set": bson.M{
|
||||
playletmod.FN.Playlet.CompressedHorizontalCover: pathStr,
|
||||
playletmod.FN.Playlet.UpdatedAt: now,
|
||||
}},
|
||||
); err != nil {
|
||||
log.Warn("回写短剧压缩横版封面失败", log.Any("playletId", playlet.Id), log.E(err))
|
||||
return false
|
||||
}
|
||||
playlet.CompressedHorizontalCover = pathStr
|
||||
playletcache.ClearHomeCache()
|
||||
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.UploadAutopublishImage(outName, compressed)
|
||||
if err != nil {
|
||||
log.Warn("media上传压缩封面失败,回退图床", log.Any("cover", cover), log.E(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; cgdj-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,267 @@
|
||||
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"
|
||||
playletResourceJHImportQueueKeyFmt = "playlet:resource:jh:import:queue:%d"
|
||||
playletResourceJHImportLockKeyFmt = "playlet:resource:jh: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)
|
||||
}
|
||||
|
||||
func playletResourceJHImportQueueKey() string {
|
||||
return fmt.Sprintf(playletResourceJHImportQueueKeyFmt, pubdef.AppId)
|
||||
}
|
||||
|
||||
func playletResourceJHImportLockKey(mediaId uint) string {
|
||||
return fmt.Sprintf(playletResourceJHImportLockKeyFmt, 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 runPlayletResourceJHImportWorker(ctx)
|
||||
go runPlayletSerialSyncWorker(ctx)
|
||||
go runPlayletCoverCompressWorker(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
|
||||
}
|
||||
|
||||
// pushPlayletResourceJHImportJob 投递嘉华短剧媒资导入任务
|
||||
func pushPlayletResourceJHImportJob(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(playletResourceJHImportQueueKey(), 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])
|
||||
}
|
||||
}
|
||||
|
||||
// runPlayletResourceJHImportWorker 循环消费嘉华短剧媒资导入队列
|
||||
func runPlayletResourceJHImportWorker(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
result, err := redis.Client().BLPop(playletResourceImportPopWait, playletResourceJHImportQueueKey()).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
|
||||
}
|
||||
handlePlayletResourceJHImportPayload(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))
|
||||
}
|
||||
}
|
||||
|
||||
// lockPlayletResourceJHImportMedia 锁定单条嘉华媒资导入
|
||||
func lockPlayletResourceJHImportMedia(mediaId uint) (bool, error) {
|
||||
return redis.Setnx_NewOK(playletResourceJHImportLockKey(mediaId), true, playletResourceImportLockTTL)
|
||||
}
|
||||
|
||||
// unlockPlayletResourceJHImportMedia 释放单条嘉华媒资导入锁
|
||||
func unlockPlayletResourceJHImportMedia(mediaId uint) {
|
||||
if err := redis.Del(playletResourceJHImportLockKey(mediaId)); err != nil {
|
||||
log.Error("释放嘉华短剧媒资导入锁失败", log.Any("mediaId", mediaId), log.E(err))
|
||||
}
|
||||
}
|
||||
|
||||
// handlePlayletResourceJHImportPayload 处理单个嘉华短剧媒资导入任务
|
||||
func handlePlayletResourceJHImportPayload(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 := importPlayletJHResourceBatch(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))
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
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 短剧媒资列表接口统一错误文案
|
||||
playletMediaListErrMsg = "短剧媒资列表查询失败"
|
||||
// playletMediaSyncErrMsg 短剧媒资同步接口统一错误文案
|
||||
playletMediaSyncErrMsg = "短剧媒资同步失败"
|
||||
// playletMediaDeletedCode 媒资中心资源已删除业务码
|
||||
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"` // 分页信息
|
||||
}
|
||||
|
||||
// playletMediaJHListResp 媒资中心嘉华短剧列表响应 data 节点
|
||||
type playletMediaJHListResp struct {
|
||||
Data []*proto.PlayletResourceJHListItem `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
|
||||
}
|
||||
|
||||
// mediaJHList 查询嘉华短剧媒资列表
|
||||
func (m *playletMediaCenterService) mediaJHList(p *playletMediaListReq) (result playletMediaJHListResp, 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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,528 @@
|
||||
package srv_mediaresource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"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"
|
||||
playletSerialSyncJHLockKeyFmt = "playlet:serial:sync:jh: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)
|
||||
}
|
||||
|
||||
func playletSerialSyncJHLockKey(mediaId uint) string {
|
||||
return fmt.Sprintf(playletSerialSyncJHLockKeyFmt, pubdef.AppId, mediaId)
|
||||
}
|
||||
|
||||
type playletSerialSyncJob struct {
|
||||
PlayletId int64 `json:"playletId"`
|
||||
MediaCenterId uint `json:"mediaCenterId"`
|
||||
MediaSource string `json:"mediaSource"`
|
||||
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,
|
||||
MediaSource: playlet.MediaSource,
|
||||
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.MediaSource, 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,
|
||||
MediaSource: playlet.MediaSource,
|
||||
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) {
|
||||
lockSource := job.MediaSource
|
||||
locked, err := lockPlayletSerialSyncBySource(lockSource, job.MediaCenterId)
|
||||
if err != nil {
|
||||
log.Error("获取连载短剧同步锁失败", log.Any("job", job), log.E(err))
|
||||
return 0, "获取同步锁失败"
|
||||
}
|
||||
if !locked {
|
||||
return 0, "该媒资正在同步"
|
||||
}
|
||||
defer unlockPlayletSerialSyncBySource(lockSource, 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, ""
|
||||
}
|
||||
|
||||
mediaSource := playlet.MediaSource
|
||||
if mediaSource == "" {
|
||||
mediaSource = job.MediaSource
|
||||
}
|
||||
ms := newPlayletMediaCenterBySource(mediaSource)
|
||||
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, mediaSource, 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 title := strings.TrimSpace(playlet.Title); title != "" {
|
||||
description := playletmod.BuildPlayNameDescription(playlet.Desc, title, updateStatus, int64(media.TotalEpisode))
|
||||
if description != "" {
|
||||
setDoc[playletmod.FN.Playlet.Description] = description
|
||||
setDoc[playletmod.FN.Playlet.SeoInfo.Description] = description
|
||||
}
|
||||
}
|
||||
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, mediaSource string, mediaContentList []playletMediaContent) (playletchaptermod.PlayletChapterList, string) {
|
||||
exists, err := loadExistingPlayletChapterKeys(ctx, playletId)
|
||||
if err != nil {
|
||||
return nil, "查询已有章节失败"
|
||||
}
|
||||
chapters := buildPlayletChaptersFromMedia(mediaContentList, mediaSource)
|
||||
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{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
|
||||
// lockPlayletSerialSyncBySource 按媒资来源锁定单个媒资ID
|
||||
func lockPlayletSerialSyncBySource(mediaSource string, mediaId uint) (bool, error) {
|
||||
if mediaSource == playletmod.MediaSourceHG {
|
||||
return lockPlayletSerialSync(mediaId)
|
||||
}
|
||||
return redis.Setnx_NewOK(playletSerialSyncJHLockKey(mediaId), true, playletSerialSyncLockTTL)
|
||||
}
|
||||
|
||||
// unlockPlayletSerialSyncBySource 按媒资来源释放单个媒资ID的同步锁
|
||||
func unlockPlayletSerialSyncBySource(mediaSource string, mediaId uint) {
|
||||
if mediaSource == playletmod.MediaSourceHG {
|
||||
unlockPlayletSerialSync(mediaId)
|
||||
return
|
||||
}
|
||||
if err := redis.Del(playletSerialSyncJHLockKey(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,8 @@
|
||||
package srv_playlet
|
||||
|
||||
import "videoweb/pub/common/playletcache"
|
||||
|
||||
// clearAppPlayletHomeCache 清除app首页短剧缓存
|
||||
func clearAppPlayletHomeCache() {
|
||||
playletcache.ClearHomeCache()
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package srv_playlet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"videoweb/pub/models/mwacc"
|
||||
"videoweb/pub/models/playletmod"
|
||||
"videoweb/web/middleware/auth"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// ResolveOperator 解析当前后台登录操作人,供短剧写操作与媒资入库共用。
|
||||
func ResolveOperator(ctx context.Context) (int64, string) {
|
||||
return resolvePlayletOperator(ctx)
|
||||
}
|
||||
|
||||
// resolvePlayletOperator 从后台登录态解析当前操作人。
|
||||
func resolvePlayletOperator(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
|
||||
}
|
||||
|
||||
// applyPlayletOperatorToSetDoc 将当前操作人写入更新文档。
|
||||
func applyPlayletOperatorToSetDoc(ctx context.Context, setDoc bson.M) {
|
||||
if setDoc == nil {
|
||||
return
|
||||
}
|
||||
operatorId, operatorName := resolvePlayletOperator(ctx)
|
||||
setDoc[playletmod.FN.Playlet.OperatorId] = operatorId
|
||||
setDoc[playletmod.FN.Playlet.OperatorName] = operatorName
|
||||
}
|
||||
|
||||
// applyPlayletOperatorToDoc 将当前操作人写入新增文档。
|
||||
func applyPlayletOperatorToDoc(ctx context.Context, doc *playletmod.Playlet) {
|
||||
if doc == nil {
|
||||
return
|
||||
}
|
||||
doc.OperatorId, doc.OperatorName = resolvePlayletOperator(ctx)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,366 @@
|
||||
package srv_playlet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/playletchapterunlockmod"
|
||||
"videoweb/pub/models/playletcollectmod"
|
||||
"videoweb/pub/models/playletlikemod"
|
||||
"videoweb/pub/models/playletmod"
|
||||
"videoweb/pub/pubdef"
|
||||
"videoweb/web/api/proto"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const playletSalesLimit = 50
|
||||
|
||||
type playletSalesAggRow struct {
|
||||
PlayletId int64 `bson:"_id"`
|
||||
Count int64 `bson:"count"`
|
||||
Coins int64 `bson:"coins"`
|
||||
}
|
||||
|
||||
type playletSalesRow struct {
|
||||
Id int64
|
||||
UnlockCount int64
|
||||
UnlockCoins int64
|
||||
LikeCount int64
|
||||
CollectCount int64
|
||||
}
|
||||
|
||||
// ListPlayletSales 按时间段汇总短剧金币解锁、点赞、收藏,最多返回 50 条。
|
||||
func ListPlayletSales(ctx context.Context, req *proto.PlayletSalesReq) (*proto.PlayletSalesResp, *srvdef.SrvErr) {
|
||||
if req == nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||||
}
|
||||
start, end, serr := resolvePlayletSalesTimeRange(req.StartTime, req.EndTime)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
resp := &proto.PlayletSalesResp{
|
||||
StartTime: start.In(utils.LocBJ).Format(pubdef.TimeISO),
|
||||
EndTime: end.In(utils.LocBJ).Format(pubdef.TimeISO),
|
||||
List: make([]*proto.PlayletSalesItem, 0),
|
||||
}
|
||||
filterIDs, playlets, serr := loadPlayletSalesFilter(ctx, req)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
if filterIDs != nil && len(filterIDs) == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
unlocks, serr := aggregatePlayletUnlocks(ctx, start, end, filterIDs)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
likes, serr := aggregatePlayletMetric(ctx, start, end, filterIDs, playletlikemod.FN.PlayletLike.CreatedAt, playletlikemod.FN.PlayletLike.PlayletId, playletlikemod.AggregatePlayletLike)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
collects, serr := aggregatePlayletMetric(ctx, start, end, filterIDs, playletcollectmod.FN.PlayletCollect.CreatedAt, playletcollectmod.FN.PlayletCollect.PlayletId, playletcollectmod.AggregatePlayletCollect)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
|
||||
rows := mergePlayletSalesRows(filterIDs, unlocks, likes, collects)
|
||||
sortPlayletSalesRows(rows, req.SortType)
|
||||
rows = takePlayletSalesRows(rows, playletSalesLimit)
|
||||
if len(rows) == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
if playlets == nil {
|
||||
playlets, serr = loadPlayletsByIDs(ctx, playletSalesIDs(rows))
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
}
|
||||
for _, row := range rows {
|
||||
item := &proto.PlayletSalesItem{
|
||||
Id: row.Id,
|
||||
UnlockCount: row.UnlockCount,
|
||||
UnlockCoins: row.UnlockCoins,
|
||||
LikeCount: row.LikeCount,
|
||||
CollectCount: row.CollectCount,
|
||||
}
|
||||
if playlet := playlets[row.Id]; playlet != nil {
|
||||
item.Title = playlet.Title
|
||||
item.Cover = playlet.Cover
|
||||
item.Slug = playlet.Slug
|
||||
item.Status = playlet.Status
|
||||
}
|
||||
resp.List = append(resp.List, item)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func resolvePlayletSalesTimeRange(startRaw, endRaw string) (time.Time, time.Time, *srvdef.SrvErr) {
|
||||
startRaw = strings.TrimSpace(startRaw)
|
||||
endRaw = strings.TrimSpace(endRaw)
|
||||
if startRaw == "" && endRaw == "" {
|
||||
day := time.Now().In(utils.LocBJ).AddDate(0, 0, -1)
|
||||
return utils.BeginOfTime(day), utils.EndOfTime(day), nil
|
||||
}
|
||||
if startRaw == "" || endRaw == "" {
|
||||
return time.Time{}, time.Time{}, srvdef.New(srvdef.ErrParamError, "开始时间和结束时间需同时传入", "开始时间和结束时间需同时传入")
|
||||
}
|
||||
return parsePlayletSalesTimeRange(startRaw, endRaw)
|
||||
}
|
||||
|
||||
func parsePlayletSalesTimeRange(startRaw, endRaw string) (time.Time, time.Time, *srvdef.SrvErr) {
|
||||
start, ok := parsePlayletSalesTime(startRaw, true)
|
||||
if !ok {
|
||||
return time.Time{}, time.Time{}, srvdef.New(srvdef.ErrParamError, "开始时间无效", "开始时间无效")
|
||||
}
|
||||
end, ok := parsePlayletSalesTime(endRaw, false)
|
||||
if !ok {
|
||||
return time.Time{}, time.Time{}, srvdef.New(srvdef.ErrParamError, "结束时间无效", "结束时间无效")
|
||||
}
|
||||
if end.Before(start) {
|
||||
return time.Time{}, time.Time{}, srvdef.New(srvdef.ErrParamError, "结束时间不能早于开始时间", "结束时间不能早于开始时间")
|
||||
}
|
||||
return start, end, nil
|
||||
}
|
||||
|
||||
func parsePlayletSalesTime(raw string, isStart bool) (time.Time, bool) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return time.Time{}, false
|
||||
}
|
||||
layouts := []string{
|
||||
pubdef.TimeISO,
|
||||
time.RFC3339,
|
||||
time.RFC3339Nano,
|
||||
"2006-01-02 15:04:05 -0700 MST",
|
||||
"2006-01-02 15:04:05.999999999 -0700 MST",
|
||||
}
|
||||
for _, layout := range layouts {
|
||||
if t, err := time.ParseInLocation(layout, raw, utils.LocBJ); err == nil {
|
||||
return t, true
|
||||
}
|
||||
}
|
||||
if t, err := time.ParseInLocation(pubdef.DateISO, raw, utils.LocBJ); err == nil {
|
||||
if isStart {
|
||||
return utils.BeginOfTime(t), true
|
||||
}
|
||||
return utils.EndOfTime(t), true
|
||||
}
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
func loadPlayletSalesFilter(ctx context.Context, req *proto.PlayletSalesReq) ([]int64, map[int64]*playletmod.Playlet, *srvdef.SrvErr) {
|
||||
id := int64(0)
|
||||
if req.Id != nil {
|
||||
id = *req.Id
|
||||
}
|
||||
title := strings.TrimSpace(req.Title)
|
||||
if id <= 0 && title == "" {
|
||||
return nil, nil, nil
|
||||
}
|
||||
filter := bson.M{}
|
||||
if id > 0 {
|
||||
filter[playletmod.FN.Playlet.Id] = id
|
||||
}
|
||||
if title != "" {
|
||||
pattern := regexp.QuoteMeta(title)
|
||||
filter[playletmod.FN.Playlet.Title] = bson.M{"$regex": pattern}
|
||||
}
|
||||
list, serr := playletmod.FindManyPlaylet(ctx, filter, options.Find().SetProjection(playletSalesProjection()))
|
||||
if serr != nil {
|
||||
return nil, nil, serr
|
||||
}
|
||||
ids := make([]int64, 0, len(list))
|
||||
playlets := make(map[int64]*playletmod.Playlet, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil || item.Id <= 0 {
|
||||
continue
|
||||
}
|
||||
ids = append(ids, item.Id)
|
||||
playlets[item.Id] = item
|
||||
}
|
||||
return ids, playlets, nil
|
||||
}
|
||||
|
||||
func playletSalesProjection() bson.D {
|
||||
return bson.D{
|
||||
{Key: playletmod.FN.Playlet.Id, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.Title, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.Cover, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.Slug, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.Status, Value: 1},
|
||||
}
|
||||
}
|
||||
|
||||
func loadPlayletsByIDs(ctx context.Context, ids []int64) (map[int64]*playletmod.Playlet, *srvdef.SrvErr) {
|
||||
out := make(map[int64]*playletmod.Playlet, len(ids))
|
||||
if len(ids) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
list, serr := playletmod.FindManyPlaylet(ctx, bson.M{
|
||||
playletmod.FN.Playlet.Id: bson.M{"$in": ids},
|
||||
}, options.Find().SetProjection(playletSalesProjection()))
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
for _, item := range list {
|
||||
if item == nil || item.Id <= 0 {
|
||||
continue
|
||||
}
|
||||
out[item.Id] = item
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func aggregatePlayletUnlocks(ctx context.Context, start, end time.Time, playletIDs []int64) (map[int64]playletSalesAggRow, *srvdef.SrvErr) {
|
||||
match := bson.M{
|
||||
playletchapterunlockmod.FN.PlayletChapterUnlock.CreatedAt: bson.M{
|
||||
"$gte": start,
|
||||
"$lte": end,
|
||||
},
|
||||
}
|
||||
if len(playletIDs) > 0 {
|
||||
match[playletchapterunlockmod.FN.PlayletChapterUnlock.PlayletId] = bson.M{"$in": playletIDs}
|
||||
}
|
||||
pipeline := []bson.M{
|
||||
{"$match": match},
|
||||
{"$group": bson.M{
|
||||
"_id": "$" + playletchapterunlockmod.FN.PlayletChapterUnlock.PlayletId,
|
||||
"count": bson.M{"$sum": 1},
|
||||
"coins": bson.M{"$sum": "$" + playletchapterunlockmod.FN.PlayletChapterUnlock.Coins},
|
||||
}},
|
||||
}
|
||||
rows := make([]playletSalesAggRow, 0)
|
||||
if serr := playletchapterunlockmod.AggregatePlayletChapterUnlock(ctx, &rows, pipeline); serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
return playletSalesAggMap(rows), nil
|
||||
}
|
||||
|
||||
func aggregatePlayletMetric(ctx context.Context, start, end time.Time, playletIDs []int64, createdAtField, playletIDField string, agg func(context.Context, interface{}, []bson.M) *srvdef.SrvErr) (map[int64]playletSalesAggRow, *srvdef.SrvErr) {
|
||||
match := bson.M{
|
||||
createdAtField: bson.M{
|
||||
"$gte": start,
|
||||
"$lte": end,
|
||||
},
|
||||
}
|
||||
if len(playletIDs) > 0 {
|
||||
match[playletIDField] = bson.M{"$in": playletIDs}
|
||||
}
|
||||
pipeline := []bson.M{
|
||||
{"$match": match},
|
||||
{"$group": bson.M{
|
||||
"_id": "$" + playletIDField,
|
||||
"count": bson.M{"$sum": 1},
|
||||
}},
|
||||
}
|
||||
rows := make([]playletSalesAggRow, 0)
|
||||
if serr := agg(ctx, &rows, pipeline); serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
return playletSalesAggMap(rows), nil
|
||||
}
|
||||
|
||||
func playletSalesAggMap(rows []playletSalesAggRow) map[int64]playletSalesAggRow {
|
||||
out := make(map[int64]playletSalesAggRow, len(rows))
|
||||
for _, row := range rows {
|
||||
if row.PlayletId <= 0 {
|
||||
continue
|
||||
}
|
||||
out[row.PlayletId] = row
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func mergePlayletSalesRows(filterIDs []int64, unlocks, likes, collects map[int64]playletSalesAggRow) []playletSalesRow {
|
||||
seen := make(map[int64]struct{})
|
||||
if filterIDs != nil {
|
||||
for _, id := range filterIDs {
|
||||
if id > 0 {
|
||||
seen[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for id := range unlocks {
|
||||
seen[id] = struct{}{}
|
||||
}
|
||||
for id := range likes {
|
||||
seen[id] = struct{}{}
|
||||
}
|
||||
for id := range collects {
|
||||
seen[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
rows := make([]playletSalesRow, 0, len(seen))
|
||||
for id := range seen {
|
||||
row := playletSalesRow{Id: id}
|
||||
if item, ok := unlocks[id]; ok {
|
||||
row.UnlockCount = item.Count
|
||||
row.UnlockCoins = item.Coins
|
||||
}
|
||||
if item, ok := likes[id]; ok {
|
||||
row.LikeCount = item.Count
|
||||
}
|
||||
if item, ok := collects[id]; ok {
|
||||
row.CollectCount = item.Count
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func sortPlayletSalesRows(rows []playletSalesRow, sortType proto.PlayletSalesSortType) {
|
||||
sort.SliceStable(rows, func(i, j int) bool {
|
||||
left, right := rows[i], rows[j]
|
||||
switch sortType {
|
||||
case proto.PlayletSalesSortLike:
|
||||
if left.LikeCount != right.LikeCount {
|
||||
return left.LikeCount > right.LikeCount
|
||||
}
|
||||
case proto.PlayletSalesSortCollect:
|
||||
if left.CollectCount != right.CollectCount {
|
||||
return left.CollectCount > right.CollectCount
|
||||
}
|
||||
default:
|
||||
if left.UnlockCount != right.UnlockCount {
|
||||
return left.UnlockCount > right.UnlockCount
|
||||
}
|
||||
if left.UnlockCoins != right.UnlockCoins {
|
||||
return left.UnlockCoins > right.UnlockCoins
|
||||
}
|
||||
}
|
||||
if left.UnlockCount != right.UnlockCount {
|
||||
return left.UnlockCount > right.UnlockCount
|
||||
}
|
||||
if left.LikeCount != right.LikeCount {
|
||||
return left.LikeCount > right.LikeCount
|
||||
}
|
||||
if left.CollectCount != right.CollectCount {
|
||||
return left.CollectCount > right.CollectCount
|
||||
}
|
||||
return left.Id > right.Id
|
||||
})
|
||||
}
|
||||
|
||||
func takePlayletSalesRows(rows []playletSalesRow, limit int) []playletSalesRow {
|
||||
if limit <= 0 || len(rows) <= limit {
|
||||
return rows
|
||||
}
|
||||
return rows[:limit]
|
||||
}
|
||||
|
||||
func playletSalesIDs(rows []playletSalesRow) []int64 {
|
||||
ids := make([]int64, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
if row.Id > 0 {
|
||||
ids = append(ids, row.Id)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package srv_playlet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/models/playletmod"
|
||||
"videoweb/pub/seo/sitemapdirty"
|
||||
"videoweb/web/service/srv_seopush"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func markSitemapDirty(ctx context.Context) {
|
||||
sitemapdirty.Mark(ctx)
|
||||
}
|
||||
|
||||
// notifyPlayletSeoAfterWrite 短剧写入后:刷新 sitemap 标记。
|
||||
// 搜索引擎只在新上架(含新增即上架)或从已上架变为下架时推送,普通改内容不推。
|
||||
// prevStatus 为变更前状态;nil 表示新增。
|
||||
func notifyPlayletSeoAfterWrite(ctx context.Context, playletId int64, prevStatus *playletmod.Status) {
|
||||
markSitemapDirty(ctx)
|
||||
item, 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.Status, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.OnlineAt, Value: 1},
|
||||
}))
|
||||
if err != nil || item == nil {
|
||||
if err != nil {
|
||||
log.Warn("seopush: load playlet after write failed", log.Any("playletId", playletId), log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
wasUp := prevStatus != nil && *prevStatus == playletmod.StatusUp
|
||||
if item.Status == playletmod.StatusUp {
|
||||
if !wasUp {
|
||||
srv_seopush.EnqueuePlayletUpdated(ctx, item.Id, item.Status, item.OnlineAt)
|
||||
}
|
||||
return
|
||||
}
|
||||
if wasUp {
|
||||
srv_seopush.EnqueuePlayletDeleted(ctx, item.Id)
|
||||
}
|
||||
}
|
||||
|
||||
// notifyPlayletSeoAfterStatus 批量上下架后的 SEO 通知
|
||||
func notifyPlayletSeoAfterStatus(ctx context.Context, ids []int64, status playletmod.Status, onlineAt time.Time) {
|
||||
markSitemapDirty(ctx)
|
||||
if status == playletmod.StatusUp {
|
||||
for _, id := range ids {
|
||||
srv_seopush.EnqueuePlayletUpdated(ctx, id, status, onlineAt)
|
||||
}
|
||||
return
|
||||
}
|
||||
for _, id := range ids {
|
||||
srv_seopush.EnqueuePlayletDeleted(ctx, id)
|
||||
}
|
||||
}
|
||||
|
||||
// notifyPlayletSeoAfterDelete 物理删除后刷新 sitemap 标记,并对原上架短剧推送删除通知。
|
||||
func notifyPlayletSeoAfterDelete(ctx context.Context, onlineIds []int64) {
|
||||
markSitemapDirty(ctx)
|
||||
for _, id := range onlineIds {
|
||||
srv_seopush.EnqueuePlayletDeleted(ctx, id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
package srv_playlet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/playletcache"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/playletmod"
|
||||
"videoweb/pub/models/playlettagmod"
|
||||
"videoweb/pub/models/playletthememod"
|
||||
"videoweb/web/api/proto"
|
||||
"videoweb/web/service/srv_homeconfig"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const (
|
||||
syncSlugDefaultSize = int64(500)
|
||||
syncSlugMaxSize = int64(2000)
|
||||
)
|
||||
|
||||
func emptySlugFilter() bson.M {
|
||||
return bson.M{
|
||||
"$or": []bson.M{
|
||||
{"slug": ""},
|
||||
{"slug": bson.M{"$exists": false}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ensureThemeSlug(ctx context.Context, ac *playletthememod.PlayletTheme, excludeId int64) *srvdef.SrvErr {
|
||||
ac.Slug = utils.SanitizeSlug(ac.Slug)
|
||||
if ac.Slug != "" {
|
||||
if utils.IsSlugAllDigits(ac.Slug) {
|
||||
return srvdef.New(srvdef.ErrParamError, "Slug不能全是数字", "Slug不能全是数字")
|
||||
}
|
||||
filter := bson.M{playletthememod.FN.PlayletTheme.Slug: ac.Slug}
|
||||
if excludeId > 0 {
|
||||
filter[playletthememod.FN.PlayletTheme.Id] = bson.M{"$ne": excludeId}
|
||||
}
|
||||
exist, _ := playletthememod.FindOnePlayletTheme(ctx, filter)
|
||||
if exist != nil {
|
||||
return srvdef.New(srvdef.ErrDbInputExist, "Slug已存在", "Slug已存在")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
source := utils.SlugSourceFromKeywordsOrTitle(ac.SeoInfo.Keyworld, ac.Name)
|
||||
ac.Slug = utils.MakeUniqueSlug(source, func(slug string) bool {
|
||||
filter := bson.M{playletthememod.FN.PlayletTheme.Slug: slug}
|
||||
if excludeId > 0 {
|
||||
filter[playletthememod.FN.PlayletTheme.Id] = bson.M{"$ne": excludeId}
|
||||
}
|
||||
exist, _ := playletthememod.FindOnePlayletTheme(ctx, filter)
|
||||
return exist != nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareThemeSlugOnUpdate(ctx context.Context, id int64, info *playletthememod.PlayletTheme, req *proto.PlayletThemeUpdateReq, setDoc bson.M) *srvdef.SrvErr {
|
||||
slug := info.Slug
|
||||
if req.Slug != nil {
|
||||
slug = utils.SanitizeSlug(*req.Slug)
|
||||
}
|
||||
if slug != "" {
|
||||
if req.Slug != nil {
|
||||
if utils.IsSlugAllDigits(slug) {
|
||||
return srvdef.New(srvdef.ErrParamError, "Slug不能全是数字", "Slug不能全是数字")
|
||||
}
|
||||
exist, _ := playletthememod.FindOnePlayletTheme(ctx, bson.M{
|
||||
playletthememod.FN.PlayletTheme.Slug: slug,
|
||||
playletthememod.FN.PlayletTheme.Id: bson.M{"$ne": id},
|
||||
})
|
||||
if exist != nil {
|
||||
return srvdef.New(srvdef.ErrDbInputExist, "Slug已存在", "Slug已存在")
|
||||
}
|
||||
setDoc[playletthememod.FN.PlayletTheme.Slug] = slug
|
||||
}
|
||||
return nil
|
||||
}
|
||||
name := info.Name
|
||||
if req.Name != nil {
|
||||
name = strings.TrimSpace(*req.Name)
|
||||
}
|
||||
keywords := info.SeoInfo.Keyworld
|
||||
if req.SeoInfo != nil {
|
||||
keywords = normalizeSeoValue(*req.SeoInfo).Keyworld
|
||||
}
|
||||
source := utils.SlugSourceFromKeywordsOrTitle(keywords, name)
|
||||
newSlug := utils.MakeUniqueSlug(source, func(s string) bool {
|
||||
exist, _ := playletthememod.FindOnePlayletTheme(ctx, bson.M{
|
||||
playletthememod.FN.PlayletTheme.Slug: s,
|
||||
playletthememod.FN.PlayletTheme.Id: bson.M{"$ne": id},
|
||||
})
|
||||
return exist != nil
|
||||
})
|
||||
setDoc[playletthememod.FN.PlayletTheme.Slug] = newSlug
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureTagSlug(ctx context.Context, ac *playlettagmod.PlayletTag, excludeId int64) *srvdef.SrvErr {
|
||||
ac.Slug = utils.SanitizeSlug(ac.Slug)
|
||||
if ac.Slug != "" {
|
||||
if utils.IsSlugAllDigits(ac.Slug) {
|
||||
return srvdef.New(srvdef.ErrParamError, "Slug不能全是数字", "Slug不能全是数字")
|
||||
}
|
||||
filter := bson.M{playlettagmod.FN.PlayletTag.Slug: ac.Slug}
|
||||
if excludeId > 0 {
|
||||
filter[playlettagmod.FN.PlayletTag.Id] = bson.M{"$ne": excludeId}
|
||||
}
|
||||
exist, _ := playlettagmod.FindOnePlayletTag(ctx, filter)
|
||||
if exist != nil {
|
||||
return srvdef.New(srvdef.ErrDbInputExist, "Slug已存在", "Slug已存在")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
source := utils.SlugSourceFromKeywordsOrTitle(ac.SeoInfo.Keyworld, ac.Name)
|
||||
ac.Slug = utils.MakeUniqueSlug(source, func(slug string) bool {
|
||||
filter := bson.M{playlettagmod.FN.PlayletTag.Slug: slug}
|
||||
if excludeId > 0 {
|
||||
filter[playlettagmod.FN.PlayletTag.Id] = bson.M{"$ne": excludeId}
|
||||
}
|
||||
exist, _ := playlettagmod.FindOnePlayletTag(ctx, filter)
|
||||
return exist != nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareTagSlugOnUpdate(ctx context.Context, id int64, info *playlettagmod.PlayletTag, req *proto.PlayletTagUpdateReq, setDoc bson.M) *srvdef.SrvErr {
|
||||
slug := info.Slug
|
||||
if req.Slug != nil {
|
||||
slug = utils.SanitizeSlug(*req.Slug)
|
||||
}
|
||||
if slug != "" {
|
||||
if req.Slug != nil {
|
||||
if utils.IsSlugAllDigits(slug) {
|
||||
return srvdef.New(srvdef.ErrParamError, "Slug不能全是数字", "Slug不能全是数字")
|
||||
}
|
||||
exist, _ := playlettagmod.FindOnePlayletTag(ctx, bson.M{
|
||||
playlettagmod.FN.PlayletTag.Slug: slug,
|
||||
playlettagmod.FN.PlayletTag.Id: bson.M{"$ne": id},
|
||||
})
|
||||
if exist != nil {
|
||||
return srvdef.New(srvdef.ErrDbInputExist, "Slug已存在", "Slug已存在")
|
||||
}
|
||||
setDoc[playlettagmod.FN.PlayletTag.Slug] = slug
|
||||
}
|
||||
return nil
|
||||
}
|
||||
name := info.Name
|
||||
if req.Name != nil {
|
||||
name = strings.TrimSpace(*req.Name)
|
||||
}
|
||||
keywords := info.SeoInfo.Keyworld
|
||||
if req.SeoInfo != nil {
|
||||
keywords = normalizeSeoValue(*req.SeoInfo).Keyworld
|
||||
}
|
||||
source := utils.SlugSourceFromKeywordsOrTitle(keywords, name)
|
||||
newSlug := utils.MakeUniqueSlug(source, func(s string) bool {
|
||||
exist, _ := playlettagmod.FindOnePlayletTag(ctx, bson.M{
|
||||
playlettagmod.FN.PlayletTag.Slug: s,
|
||||
playlettagmod.FN.PlayletTag.Id: bson.M{"$ne": id},
|
||||
})
|
||||
return exist != nil
|
||||
})
|
||||
setDoc[playlettagmod.FN.PlayletTag.Slug] = newSlug
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensurePlayletSlug(ctx context.Context, ac *playletmod.Playlet, excludeId int64) *srvdef.SrvErr {
|
||||
ac.Slug = utils.SanitizeSlug(ac.Slug)
|
||||
if ac.Slug != "" {
|
||||
if utils.IsSlugAllDigits(ac.Slug) {
|
||||
return srvdef.New(srvdef.ErrParamError, "Slug不能全是数字", "Slug不能全是数字")
|
||||
}
|
||||
filter := bson.M{playletmod.FN.Playlet.Slug: ac.Slug}
|
||||
if excludeId > 0 {
|
||||
filter[playletmod.FN.Playlet.Id] = bson.M{"$ne": excludeId}
|
||||
}
|
||||
exist, _ := playletmod.FindOnePlaylet(ctx, filter)
|
||||
if exist != nil {
|
||||
return srvdef.New(srvdef.ErrDbInputExist, "Slug已存在", "Slug已存在")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
source := utils.SlugSourceFromKeywordsOrTitle(ac.SeoInfo.Keyworld, ac.Title)
|
||||
ac.Slug = utils.MakeUniqueSlug(source, func(slug string) bool {
|
||||
filter := bson.M{playletmod.FN.Playlet.Slug: slug}
|
||||
if excludeId > 0 {
|
||||
filter[playletmod.FN.Playlet.Id] = bson.M{"$ne": excludeId}
|
||||
}
|
||||
exist, _ := playletmod.FindOnePlaylet(ctx, filter)
|
||||
return exist != nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func preparePlayletSlugOnUpdate(ctx context.Context, id int64, info *playletmod.Playlet, req *proto.PlayletUpdateReq, setDoc bson.M) *srvdef.SrvErr {
|
||||
slug := info.Slug
|
||||
if req.Slug != nil {
|
||||
slug = utils.SanitizeSlug(*req.Slug)
|
||||
}
|
||||
if slug != "" {
|
||||
if req.Slug != nil {
|
||||
if utils.IsSlugAllDigits(slug) {
|
||||
return srvdef.New(srvdef.ErrParamError, "Slug不能全是数字", "Slug不能全是数字")
|
||||
}
|
||||
exist, _ := playletmod.FindOnePlaylet(ctx, bson.M{
|
||||
playletmod.FN.Playlet.Slug: slug,
|
||||
playletmod.FN.Playlet.Id: bson.M{"$ne": id},
|
||||
})
|
||||
if exist != nil {
|
||||
return srvdef.New(srvdef.ErrDbInputExist, "Slug已存在", "Slug已存在")
|
||||
}
|
||||
setDoc[playletmod.FN.Playlet.Slug] = slug
|
||||
}
|
||||
return nil
|
||||
}
|
||||
title := info.Title
|
||||
if req.Title != nil {
|
||||
title = strings.TrimSpace(*req.Title)
|
||||
}
|
||||
keywords := info.SeoInfo.Keyworld
|
||||
if req.SeoInfo != nil {
|
||||
keywords = normalizeSeoValue(*req.SeoInfo).Keyworld
|
||||
}
|
||||
source := utils.SlugSourceFromKeywordsOrTitle(keywords, title)
|
||||
newSlug := utils.MakeUniqueSlug(source, func(s string) bool {
|
||||
exist, _ := playletmod.FindOnePlaylet(ctx, bson.M{
|
||||
playletmod.FN.Playlet.Slug: s,
|
||||
playletmod.FN.Playlet.Id: bson.M{"$ne": id},
|
||||
})
|
||||
return exist != nil
|
||||
})
|
||||
setDoc[playletmod.FN.Playlet.Slug] = newSlug
|
||||
return nil
|
||||
}
|
||||
|
||||
// SyncEmptySlugs 为题材、标签、短剧中空 slug 自动生成。可重复调用。
|
||||
func SyncEmptySlugs(ctx context.Context, req *proto.PlayletSlugSyncReq) (*proto.PlayletSlugSyncResp, *srvdef.SrvErr) {
|
||||
maxSize := syncSlugDefaultSize
|
||||
if req != nil && req.MaxSize > 0 {
|
||||
maxSize = req.MaxSize
|
||||
}
|
||||
if maxSize > syncSlugMaxSize {
|
||||
maxSize = syncSlugMaxSize
|
||||
}
|
||||
resp := &proto.PlayletSlugSyncResp{}
|
||||
remain := maxSize
|
||||
|
||||
themeUpdated, themeMore, err := syncThemeEmptySlugs(ctx, remain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.ThemeUpdated = themeUpdated
|
||||
remain -= themeUpdated
|
||||
if remain <= 0 {
|
||||
resp.HasMore = themeMore || hasEmptySlug(ctx, "tag") || hasEmptySlug(ctx, "playlet") || srv_homeconfig.HasEmptySlug(ctx)
|
||||
playletcache.ClearHomeCache()
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
tagUpdated, tagMore, err := syncTagEmptySlugs(ctx, remain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.TagUpdated = tagUpdated
|
||||
remain -= tagUpdated
|
||||
if remain <= 0 {
|
||||
resp.HasMore = tagMore || hasEmptySlug(ctx, "playlet") || srv_homeconfig.HasEmptySlug(ctx)
|
||||
playletcache.ClearHomeCache()
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
playletUpdated, playletMore, err := syncPlayletEmptySlugs(ctx, remain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.PlayletUpdated = playletUpdated
|
||||
remain -= playletUpdated
|
||||
if remain <= 0 {
|
||||
resp.HasMore = playletMore || srv_homeconfig.HasEmptySlug(ctx)
|
||||
playletcache.ClearHomeCache()
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
homeUpdated, homeMore, err := srv_homeconfig.SyncEmptySlugs(ctx, remain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.HomeConfigUpdated = homeUpdated
|
||||
resp.HasMore = playletMore || homeMore
|
||||
playletcache.ClearHomeCache()
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func hasEmptySlug(ctx context.Context, kind string) bool {
|
||||
filter := emptySlugFilter()
|
||||
switch kind {
|
||||
case "theme":
|
||||
count, _ := playletthememod.CountPlayletTheme(ctx, filter)
|
||||
return count > 0
|
||||
case "tag":
|
||||
count, _ := playlettagmod.CountPlayletTag(ctx, filter)
|
||||
return count > 0
|
||||
case "playlet":
|
||||
count, _ := playletmod.CountPlaylet(ctx, filter)
|
||||
return count > 0
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func syncThemeEmptySlugs(ctx context.Context, maxSize int64) (int64, bool, *srvdef.SrvErr) {
|
||||
list, err := playletthememod.FindManyPlayletTheme(ctx, emptySlugFilter(), options.Find().
|
||||
SetSort(bson.D{{Key: playletthememod.FN.PlayletTheme.Id, Value: 1}}).
|
||||
SetLimit(maxSize+1))
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
hasMore := int64(len(list)) > maxSize
|
||||
if hasMore {
|
||||
list = list[:maxSize]
|
||||
}
|
||||
var updated int64
|
||||
now := time.Now()
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
if serr := ensureThemeSlug(ctx, item, item.Id); serr != nil {
|
||||
return updated, hasMore, serr
|
||||
}
|
||||
if err := playletthememod.UpdateOnePlayletTheme(ctx,
|
||||
bson.M{playletthememod.FN.PlayletTheme.Id: item.Id},
|
||||
bson.M{"$set": bson.M{
|
||||
playletthememod.FN.PlayletTheme.Slug: item.Slug,
|
||||
playletthememod.FN.PlayletTheme.UpdatedAt: now,
|
||||
}},
|
||||
); err != nil {
|
||||
return updated, hasMore, err
|
||||
}
|
||||
updated++
|
||||
}
|
||||
return updated, hasMore, nil
|
||||
}
|
||||
|
||||
func syncTagEmptySlugs(ctx context.Context, maxSize int64) (int64, bool, *srvdef.SrvErr) {
|
||||
list, err := playlettagmod.FindManyPlayletTag(ctx, emptySlugFilter(), options.Find().
|
||||
SetSort(bson.D{{Key: playlettagmod.FN.PlayletTag.Id, Value: 1}}).
|
||||
SetLimit(maxSize+1))
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
hasMore := int64(len(list)) > maxSize
|
||||
if hasMore {
|
||||
list = list[:maxSize]
|
||||
}
|
||||
var updated int64
|
||||
now := time.Now()
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
if serr := ensureTagSlug(ctx, item, item.Id); serr != nil {
|
||||
return updated, hasMore, serr
|
||||
}
|
||||
if err := playlettagmod.UpdateOnePlayletTag(ctx,
|
||||
bson.M{playlettagmod.FN.PlayletTag.Id: item.Id},
|
||||
bson.M{"$set": bson.M{
|
||||
playlettagmod.FN.PlayletTag.Slug: item.Slug,
|
||||
playlettagmod.FN.PlayletTag.UpdatedAt: now,
|
||||
}},
|
||||
); err != nil {
|
||||
return updated, hasMore, err
|
||||
}
|
||||
updated++
|
||||
}
|
||||
return updated, hasMore, nil
|
||||
}
|
||||
|
||||
func syncPlayletEmptySlugs(ctx context.Context, maxSize int64) (int64, bool, *srvdef.SrvErr) {
|
||||
list, err := playletmod.FindManyPlaylet(ctx, emptySlugFilter(), options.Find().
|
||||
SetSort(bson.D{{Key: playletmod.FN.Playlet.Id, Value: 1}}).
|
||||
SetLimit(maxSize+1))
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
hasMore := int64(len(list)) > maxSize
|
||||
if hasMore {
|
||||
list = list[:maxSize]
|
||||
}
|
||||
var updated int64
|
||||
now := time.Now()
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
if serr := ensurePlayletSlug(ctx, item, item.Id); serr != nil {
|
||||
return updated, hasMore, serr
|
||||
}
|
||||
if err := playletmod.UpdateOnePlaylet(ctx,
|
||||
bson.M{playletmod.FN.Playlet.Id: item.Id},
|
||||
bson.M{"$set": bson.M{
|
||||
playletmod.FN.Playlet.Slug: item.Slug,
|
||||
playletmod.FN.Playlet.UpdatedAt: now,
|
||||
}},
|
||||
); err != nil {
|
||||
return updated, hasMore, err
|
||||
}
|
||||
updated++
|
||||
}
|
||||
return updated, hasMore, nil
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
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)}
|
||||
}
|
||||
if slug := strings.TrimSpace(req.Slug); slug != "" {
|
||||
filter[playlettagmod.FN.PlayletTag.Slug] = slug
|
||||
}
|
||||
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,
|
||||
Slug: item.Slug,
|
||||
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),
|
||||
Slug: strings.TrimSpace(req.Slug),
|
||||
Status: req.Status,
|
||||
Rank: req.Rank,
|
||||
SeoInfo: normalizeSeoValue(req.SeoInfo),
|
||||
}
|
||||
if err := ensureTagSlug(ctx, doc, 0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := playlettagmod.InsertOnePlayletTag(ctx, doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clearAppPlayletHomeCache()
|
||||
markSitemapDirty(ctx)
|
||||
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无效", "")
|
||||
}
|
||||
info, err := playlettagmod.FindOnePlayletTag(ctx, bson.M{playlettagmod.FN.PlayletTag.Id: req.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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 err := prepareTagSlugOnUpdate(ctx, req.Id, info, req, setDoc); err != nil {
|
||||
return err
|
||||
}
|
||||
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()
|
||||
markSitemapDirty(ctx)
|
||||
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()
|
||||
markSitemapDirty(ctx)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package srv_playlet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/response"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"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"
|
||||
)
|
||||
|
||||
// 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 req.IsRecommend != nil {
|
||||
filter[playletthememod.FN.PlayletTheme.IsRecommend] = *req.IsRecommend
|
||||
}
|
||||
if name := strings.TrimSpace(req.Name); name != "" {
|
||||
filter[playletthememod.FN.PlayletTheme.Name] = bson.M{"$regex": regexp.QuoteMeta(name)}
|
||||
}
|
||||
if slug := strings.TrimSpace(req.Slug); slug != "" {
|
||||
filter[playletthememod.FN.PlayletTheme.Slug] = slug
|
||||
}
|
||||
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().
|
||||
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,
|
||||
IsRecommend: item.IsRecommend,
|
||||
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: strings.TrimSpace(req.Slug),
|
||||
Icon: req.Icon,
|
||||
Status: req.Status,
|
||||
IsRecommend: req.IsRecommend,
|
||||
Rank: req.Rank,
|
||||
SeoInfo: normalizeSeoValue(req.SeoInfo),
|
||||
}
|
||||
if err := ensureThemeSlug(ctx, doc, 0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := playletthememod.InsertOnePlayletTheme(ctx, doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clearAppPlayletHomeCache()
|
||||
markSitemapDirty(ctx)
|
||||
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无效", "")
|
||||
}
|
||||
info, 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] = *req.Icon
|
||||
}
|
||||
if req.Status != nil {
|
||||
setDoc[playletthememod.FN.PlayletTheme.Status] = *req.Status
|
||||
}
|
||||
if req.IsRecommend != nil {
|
||||
setDoc[playletthememod.FN.PlayletTheme.IsRecommend] = *req.IsRecommend
|
||||
}
|
||||
if req.Rank != nil {
|
||||
setDoc[playletthememod.FN.PlayletTheme.Rank] = *req.Rank
|
||||
}
|
||||
if req.SeoInfo != nil {
|
||||
setDoc["seoInfo"] = normalizeSeoValue(*req.SeoInfo)
|
||||
}
|
||||
if err := prepareThemeSlugOnUpdate(ctx, req.Id, info, req, setDoc); err != nil {
|
||||
return err
|
||||
}
|
||||
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()
|
||||
markSitemapDirty(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetPlayletThemeRecommend 单/批量设置题材是否推荐
|
||||
func SetPlayletThemeRecommend(ctx context.Context, req *proto.PlayletThemeRecommendSetReq) *srvdef.SrvErr {
|
||||
if req == nil {
|
||||
return srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
|
||||
}
|
||||
ids := dedupePositiveInt64Ids(req.Ids)
|
||||
if len(ids) == 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "题材ID不能为空", "")
|
||||
}
|
||||
if err := playletthememod.UpdateManyPlayletTheme(ctx,
|
||||
bson.M{playletthememod.FN.PlayletTheme.Id: bson.M{"$in": ids}},
|
||||
bson.M{"$set": bson.M{
|
||||
playletthememod.FN.PlayletTheme.IsRecommend: req.IsRecommend,
|
||||
playletthememod.FN.PlayletTheme.UpdatedAt: time.Now(),
|
||||
}},
|
||||
); 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()
|
||||
markSitemapDirty(ctx)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
package srv_recharge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/useragent"
|
||||
"videoweb/pub/dataReport"
|
||||
"videoweb/pub/models/cointransmod"
|
||||
"videoweb/pub/models/eventmod"
|
||||
"videoweb/pub/models/rechargepackagemod"
|
||||
"videoweb/pub/models/usermod"
|
||||
"videoweb/pub/models/vipordermod"
|
||||
"videoweb/pub/payment"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
// BillCallbackParam 支付成功后的入账参数。签名校验由新支付中心回调完成。
|
||||
type BillCallbackParam struct {
|
||||
TradeNo string `json:"transNo" form:"transNo"`
|
||||
Oid string `json:"oid" form:"oid"`
|
||||
RealAmount int64 `json:"payMoney" form:"payMoney"`
|
||||
PaymentAt time.Time `json:"paymentAt" form:"paymentAt"`
|
||||
SuccessAt time.Time `json:"successAt" form:"successAt"`
|
||||
}
|
||||
|
||||
// OnPayCenterCallback 统一支付回调入口
|
||||
// 通过 vip_order 表查询订单,再按 OrderType 分支处理。
|
||||
func OnPayCenterCallback(ctx *gin.Context, param *BillCallbackParam) *srvdef.SrvErr {
|
||||
log.Info("srv_recharge:OnPayCenterCallback",
|
||||
log.Any("tradeNo", param.TradeNo),
|
||||
log.Any("oid", param.Oid),
|
||||
log.Any("realAmount", param.RealAmount),
|
||||
)
|
||||
if param.TradeNo == "" {
|
||||
return srvdef.New(srvdef.ErrParamError, "tradeNo 不能为空", "")
|
||||
}
|
||||
|
||||
order, serr := vipordermod.FindOneVipOrder(ctx, bson.M{
|
||||
vipordermod.FN.VipOrder.TradeNo: param.TradeNo,
|
||||
})
|
||||
if serr != nil {
|
||||
log.Error("OnPayCenterCallback FindOneVipOrder failed",
|
||||
log.Any("tradeNo", param.TradeNo), log.E(serr))
|
||||
return serr
|
||||
}
|
||||
|
||||
if order.Status == pubdef.VipOrderSuccess {
|
||||
log.Info("OnPayCenterCallback already success", log.Any("tradeNo", param.TradeNo))
|
||||
return nil
|
||||
}
|
||||
|
||||
if order.Oid != "" && order.Oid != param.Oid {
|
||||
return srvdef.New(srvdef.ErrParamError,
|
||||
fmt.Sprintf("oid 不匹配: %s vs %s", order.Oid, param.Oid), "")
|
||||
}
|
||||
|
||||
paymentAt := resolvePaymentAt(param)
|
||||
|
||||
if serr = vipordermod.UpdateOrderSuccess(ctx, param.TradeNo, param.Oid, param.RealAmount, paymentAt); serr != nil {
|
||||
log.Error("OnPayCenterCallback UpdateOrderSuccess failed",
|
||||
log.Any("tradeNo", param.TradeNo), log.E(serr))
|
||||
return serr
|
||||
}
|
||||
|
||||
orderSnap := *order
|
||||
realAmount := param.RealAmount
|
||||
oid := param.Oid
|
||||
|
||||
switch order.OrderType {
|
||||
case vipordermod.OrderTypeCoin:
|
||||
go handleCoinOrderSuccess(orderSnap, oid, realAmount)
|
||||
default:
|
||||
go handleVipOrderSuccess(orderSnap, oid, realAmount)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleVipOrderSuccess(order vipordermod.VipOrder, oid string, realAmount int64) {
|
||||
ctx := context.Background()
|
||||
|
||||
pkg, err := findVipPackage(ctx, order.CardId)
|
||||
if err != nil {
|
||||
log.Error("handleVipOrderSuccess find package failed",
|
||||
log.Any("userId", order.UserId), log.Any("packageId", order.CardId), log.E(err))
|
||||
return
|
||||
}
|
||||
|
||||
expire, err := payment.ActivateVip(ctx, order.UserId, pkg)
|
||||
if err != nil {
|
||||
log.Error("handleVipOrderSuccess ActivateVip failed, retrying",
|
||||
log.Any("userId", order.UserId), log.Any("packageId", order.CardId), log.E(err))
|
||||
time.Sleep(3 * time.Second)
|
||||
expire, err = payment.ActivateVip(ctx, order.UserId, pkg)
|
||||
if err != nil {
|
||||
log.Error("handleVipOrderSuccess ActivateVip retry failed",
|
||||
log.Any("userId", order.UserId), log.Any("packageId", order.CardId), log.E(err))
|
||||
}
|
||||
}
|
||||
log.Info("handleVipOrderSuccess done", log.Any("userId", order.UserId), log.Any("expire", expire))
|
||||
|
||||
if pkg.GiftCoin > 0 {
|
||||
_ = payment.WriteCoinTransByUser(ctx, payment.WriteCoinTransOpt{
|
||||
UserId: order.UserId,
|
||||
FlowDir: cointransmod.FlowIn,
|
||||
TranType: cointransmod.TranTypeVipGiftCoin,
|
||||
Amount: pkg.GiftCoin,
|
||||
OrderNo: order.TradeNo,
|
||||
Remark: payment.CoinTransRemark(cointransmod.TranTypeVipGiftCoin, pkg.Name),
|
||||
})
|
||||
}
|
||||
|
||||
reportVipOrderPaid(ctx, &order, oid, realAmount, expire)
|
||||
}
|
||||
|
||||
func handleCoinOrderSuccess(order vipordermod.VipOrder, oid string, realAmount int64) {
|
||||
ctx := context.Background()
|
||||
|
||||
if err := addUserCoins(ctx, order.UserId, order.Coin); err != nil {
|
||||
log.Error("handleCoinOrderSuccess addUserCoins failed, retrying",
|
||||
log.Any("userId", order.UserId), log.Any("coin", order.Coin), log.E(err))
|
||||
time.Sleep(3 * time.Second)
|
||||
if err = addUserCoins(ctx, order.UserId, order.Coin); err != nil {
|
||||
log.Error("handleCoinOrderSuccess addUserCoins retry failed",
|
||||
log.Any("userId", order.UserId), log.Any("coin", order.Coin), log.E(err))
|
||||
}
|
||||
}
|
||||
log.Info("handleCoinOrderSuccess done", log.Any("userId", order.UserId), log.Any("coin", order.Coin))
|
||||
|
||||
writeCoinOrderTrans(ctx, order)
|
||||
|
||||
reportCoinOrderPaid(ctx, &order, oid, realAmount)
|
||||
}
|
||||
|
||||
func writeCoinOrderTrans(ctx context.Context, order vipordermod.VipOrder) {
|
||||
baseCoin, giftCoin := resolveCoinOrderSplit(ctx, order)
|
||||
balanceAfter, ok := coinOrderBalanceAfter(ctx, order.UserId)
|
||||
if !ok || giftCoin <= 0 {
|
||||
_ = payment.WriteCoinTransByUser(ctx, payment.WriteCoinTransOpt{
|
||||
UserId: order.UserId,
|
||||
FlowDir: cointransmod.FlowIn,
|
||||
TranType: cointransmod.TranTypeCoinRecharge,
|
||||
Amount: order.Coin,
|
||||
OrderNo: order.TradeNo,
|
||||
Remark: payment.CoinTransRemark(cointransmod.TranTypeCoinRecharge, ""),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if baseCoin > 0 {
|
||||
_ = payment.WriteCoinTrans(ctx, payment.WriteCoinTransOpt{
|
||||
UserId: order.UserId,
|
||||
FlowDir: cointransmod.FlowIn,
|
||||
TranType: cointransmod.TranTypeCoinRecharge,
|
||||
Amount: baseCoin,
|
||||
OrderNo: order.TradeNo,
|
||||
Remark: payment.CoinTransRemark(cointransmod.TranTypeCoinRecharge, ""),
|
||||
}, balanceAfter-giftCoin)
|
||||
}
|
||||
_ = payment.WriteCoinTrans(ctx, payment.WriteCoinTransOpt{
|
||||
UserId: order.UserId,
|
||||
FlowDir: cointransmod.FlowIn,
|
||||
TranType: cointransmod.TranTypeCoinGift,
|
||||
Amount: giftCoin,
|
||||
OrderNo: order.TradeNo,
|
||||
Remark: payment.CoinTransRemark(cointransmod.TranTypeCoinGift, ""),
|
||||
}, balanceAfter)
|
||||
}
|
||||
|
||||
func resolveCoinOrderSplit(ctx context.Context, order vipordermod.VipOrder) (int64, int64) {
|
||||
if order.BaseCoin+order.GiftCoin == order.Coin && (order.BaseCoin > 0 || order.GiftCoin > 0) {
|
||||
return order.BaseCoin, order.GiftCoin
|
||||
}
|
||||
pkg, _ := rechargepackagemod.FindOneRechargePackage(ctx, bson.M{
|
||||
rechargepackagemod.FN.RechargePackage.Id: order.CoinCfgId,
|
||||
rechargepackagemod.FN.RechargePackage.PackageType: rechargepackagemod.PackageTypeCoin,
|
||||
})
|
||||
if pkg != nil && pkg.BaseCoin+pkg.GiftCoin == order.Coin {
|
||||
return pkg.BaseCoin, pkg.GiftCoin
|
||||
}
|
||||
return order.Coin, 0
|
||||
}
|
||||
|
||||
func coinOrderBalanceAfter(ctx context.Context, userId int64) (int64, bool) {
|
||||
user, serr := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: userId})
|
||||
if serr != nil || user == nil {
|
||||
return 0, false
|
||||
}
|
||||
return user.Recharge, true
|
||||
}
|
||||
|
||||
func resolvePaymentAt(param *BillCallbackParam) time.Time {
|
||||
if !param.PaymentAt.IsZero() {
|
||||
return param.PaymentAt
|
||||
}
|
||||
if !param.SuccessAt.IsZero() {
|
||||
return param.SuccessAt
|
||||
}
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
func findVipPackage(ctx context.Context, packageId int64) (*rechargepackagemod.RechargePackage, *srvdef.SrvErr) {
|
||||
pkg, serr := rechargepackagemod.FindOneRechargePackage(ctx, bson.M{
|
||||
rechargepackagemod.FN.RechargePackage.Id: packageId,
|
||||
rechargepackagemod.FN.RechargePackage.PackageType: rechargepackagemod.PackageTypeVip,
|
||||
})
|
||||
if serr != nil || pkg == nil {
|
||||
return nil, srvdef.New(srvdef.ErrNoData, "会员套餐不存在", "")
|
||||
}
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
func addUserCoins(ctx context.Context, userId int64, coin int64) *srvdef.SrvErr {
|
||||
return usermod.UpdateOneUser(ctx,
|
||||
bson.M{usermod.FN.User.Id: userId},
|
||||
bson.M{
|
||||
"$inc": bson.M{usermod.FN.User.Recharge: coin},
|
||||
"$set": bson.M{usermod.FN.User.UpdatedAt: time.Now()},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func reportVipOrderPaid(ctx context.Context, order *vipordermod.VipOrder, oid string, realAmount int64, vipExpire time.Time) {
|
||||
user, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: order.UserId})
|
||||
districtCode, ip := "", order.IPAddr
|
||||
if user != nil {
|
||||
districtCode = user.DistrictCode
|
||||
}
|
||||
|
||||
ua := useragent.UA{SysType: order.SysType}
|
||||
eventId := newEventId(pubdef.TrackerAppId(), order.UserId, order.TradeNo, order.CreatedAt.Unix())
|
||||
cf := dataReport.NewCommonField(eventId, dataReport.EventTypeOrderPaid, districtCode, ua, strconv.FormatInt(user.Id, 10), ip)
|
||||
|
||||
var expireTS int64
|
||||
if !vipExpire.IsZero() {
|
||||
expireTS = vipExpire.Unix()
|
||||
}
|
||||
cf.Payload = dataReport.JsonRawMessage(&dataReport.OrderPaidEvent{
|
||||
OrderID: order.TradeNo,
|
||||
OrderType: "vip_subscription",
|
||||
ProductID: strconv.FormatInt(order.CardId, 10),
|
||||
Amount: dataReport.JsonNumber(realAmount),
|
||||
Currency: "CNY",
|
||||
VIPExpirationTime: dataReport.JsonNumber(expireTS),
|
||||
PayType: order.PayMode,
|
||||
PayChannel: order.PayChannel,
|
||||
TransactionID: oid,
|
||||
CreateTime: dataReport.JsonNumber(order.CreatedAt.Unix()),
|
||||
})
|
||||
insertEvent(ctx, cf, order.TradeNo)
|
||||
}
|
||||
|
||||
func reportCoinOrderPaid(ctx context.Context, order *vipordermod.VipOrder, oid string, realAmount int64) {
|
||||
user, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: order.UserId})
|
||||
districtCode, ip := "", order.IPAddr
|
||||
if user != nil {
|
||||
districtCode = user.DistrictCode
|
||||
}
|
||||
|
||||
ua := useragent.UA{SysType: order.SysType}
|
||||
eventId := newEventId(pubdef.TrackerAppId(), order.UserId, order.TradeNo, order.CreatedAt.Unix())
|
||||
cf := dataReport.NewCommonField(eventId, dataReport.EventTypeOrderPaid, districtCode, ua, strconv.FormatInt(user.Id, 10), ip)
|
||||
|
||||
cf.Payload = dataReport.JsonRawMessage(&dataReport.OrderPaidEvent{
|
||||
OrderID: order.TradeNo,
|
||||
OrderType: "coin_purchase",
|
||||
ProductID: strconv.FormatInt(order.CoinCfgId, 10),
|
||||
Amount: dataReport.JsonNumber(realAmount),
|
||||
Currency: "CNY",
|
||||
CoinQuantity: dataReport.JsonNumber(order.Coin),
|
||||
PayType: order.PayMode,
|
||||
PayChannel: order.PayChannel,
|
||||
TransactionID: oid,
|
||||
CreateTime: dataReport.JsonNumber(order.CreatedAt.Unix()),
|
||||
})
|
||||
insertEvent(ctx, cf, order.TradeNo)
|
||||
}
|
||||
|
||||
func insertEvent(ctx context.Context, cf dataReport.CommonField, tradeNo string) {
|
||||
data, _ := json.Marshal(cf)
|
||||
if err := eventmod.InsertOneEvent(ctx, &eventmod.Event{
|
||||
EventId: cf.EventID,
|
||||
EventType: cf.Event,
|
||||
Data: string(data),
|
||||
Status: eventmod.Default,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}); err != nil {
|
||||
log.Error("insertEvent failed", log.Any("tradeNo", tradeNo), log.E(err))
|
||||
}
|
||||
}
|
||||
|
||||
func newEventId(appId string, uid int64, orderId interface{}, createTime int64) string {
|
||||
input := fmt.Sprintf("%v%v%v%v", appId, uid, orderId, createTime)
|
||||
h := md5.New()
|
||||
h.Write([]byte(input))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package srv_rechargepackage
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/pub/models/rechargepackagemod"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
// RechargePackageListReq Web后台购买套餐分页列表请求
|
||||
type RechargePackageListReq struct {
|
||||
pubdef.Page
|
||||
Id *int64 `json:"id"` // 套餐ID
|
||||
PackageType *rechargepackagemod.PackageType `json:"packageType"` // 套餐类型:1-会员畅享 2-金币充值
|
||||
Status *rechargepackagemod.Status `json:"status"` // 状态:1-启用 2-停用
|
||||
Name string `json:"name"` // 展示标题,模糊搜索
|
||||
}
|
||||
|
||||
// RechargePackageListResp Web后台购买套餐分页列表响应
|
||||
type RechargePackageListResp struct {
|
||||
Total int64 `json:"total"` // 总数量
|
||||
List []*RechargePackageItem `json:"list"` // 当前页列表
|
||||
}
|
||||
|
||||
// VipPackageOption 会员畅享下拉列表项。
|
||||
type VipPackageOption struct {
|
||||
Id int64 `json:"id"` // 套餐ID
|
||||
Name string `json:"name"` // 套餐名称
|
||||
}
|
||||
|
||||
// RechargePackageItem Web后台购买套餐列表项
|
||||
type RechargePackageItem struct {
|
||||
Id int64 `json:"id"` // 套餐ID
|
||||
PackageType rechargepackagemod.PackageType `json:"packageType"` // 套餐类型:1-会员畅享 2-金币充值
|
||||
Name string `json:"name"` // 展示标题
|
||||
SubTitle string `json:"subTitle"` // 展示副标题
|
||||
Price int64 `json:"price"` // 售价,单位分
|
||||
OriginalPrice int64 `json:"originalPrice"` // 原价,单位分
|
||||
DurationDays int64 `json:"durationDays"` // 会员有效天数
|
||||
DurationHours int64 `json:"durationHours"` // 会员有效小时数,由durationDays计算
|
||||
BaseCoin int64 `json:"baseCoin"` // 基础金币数
|
||||
GiftCoin int64 `json:"giftCoin"` // 赠送金币数
|
||||
TotalCoin int64 `json:"totalCoin"` // 到账总金币数,由baseCoin+giftCoin计算
|
||||
Sort int32 `json:"sort"` // 排序,越小越靠前
|
||||
Status rechargepackagemod.Status `json:"status"` // 状态:1-启用 2-停用
|
||||
Remark string `json:"remark"` // 后台备注
|
||||
CreatedAt time.Time `json:"createdAt"` // 创建时间
|
||||
UpdatedAt time.Time `json:"updatedAt"` // 更新时间
|
||||
}
|
||||
|
||||
// RechargePackageAddReq Web后台新增购买套餐请求
|
||||
type RechargePackageAddReq struct {
|
||||
PackageType rechargepackagemod.PackageType `json:"packageType" binding:"required"` // 套餐类型:1-会员畅享 2-金币充值
|
||||
Name string `json:"name" binding:"required"` // 展示标题
|
||||
Price int64 `json:"price" binding:"required"` // 售价,单位分
|
||||
OriginalPrice int64 `json:"originalPrice"` // 原价,单位分
|
||||
DurationDays int64 `json:"durationDays"` // 会员有效天数,金币套餐传0
|
||||
BaseCoin int64 `json:"baseCoin"` // 基础金币数,会员套餐传0
|
||||
GiftCoin int64 `json:"giftCoin"` // 赠送金币数
|
||||
Sort int32 `json:"sort"` // 排序,越小越靠前
|
||||
Status rechargepackagemod.Status `json:"status"` // 状态:1-启用 2-停用,默认1
|
||||
Remark string `json:"remark"` // 后台备注
|
||||
}
|
||||
|
||||
// RechargePackageUpdateReq Web后台更新购买套餐请求
|
||||
type RechargePackageUpdateReq struct {
|
||||
Id int64 `json:"id" binding:"required"` // 套餐ID
|
||||
PackageType *rechargepackagemod.PackageType `json:"packageType"` // 套餐类型:1-会员畅享 2-金币充值
|
||||
Name *string `json:"name"` // 展示标题
|
||||
Price *int64 `json:"price"` // 售价,单位分
|
||||
OriginalPrice *int64 `json:"originalPrice"` // 原价,单位分
|
||||
DurationDays *int64 `json:"durationDays"` // 会员有效天数
|
||||
BaseCoin *int64 `json:"baseCoin"` // 基础金币数
|
||||
GiftCoin *int64 `json:"giftCoin"` // 赠送金币数
|
||||
Sort *int32 `json:"sort"` // 排序
|
||||
Status *rechargepackagemod.Status `json:"status"` // 状态:1-启用 2-停用
|
||||
Remark *string `json:"remark"` // 后台备注
|
||||
}
|
||||
|
||||
// RechargePackageDelReq Web后台删除购买套餐请求。
|
||||
type RechargePackageDelReq struct {
|
||||
Id int64 `json:"id" binding:"required"` // 套餐ID
|
||||
}
|
||||
|
||||
// RechargePackageStatusSetReq Web后台单/批量设置购买套餐状态请求。
|
||||
type RechargePackageStatusSetReq struct {
|
||||
Ids []int64 `json:"ids"` // 套餐ID集合
|
||||
Status rechargepackagemod.Status `json:"status"` // 状态:1-启用 2-停用
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
package srv_rechargepackage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/rechargepackagemod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPageNum int64 = 1
|
||||
defaultPageSize int64 = 10
|
||||
maxPageSize int64 = 100
|
||||
)
|
||||
|
||||
// ListRechargePackage 分页查询购买套餐配置。
|
||||
func ListRechargePackage(ctx context.Context, req *RechargePackageListReq) (*RechargePackageListResp, *srvdef.SrvErr) {
|
||||
if req == nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
|
||||
}
|
||||
filter := buildListFilter(req)
|
||||
pageNum, pageSize := normalizePage(req.PageNum, req.PageSize)
|
||||
total, err := rechargepackagemod.CountRechargePackage(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := &RechargePackageListResp{
|
||||
Total: total,
|
||||
List: make([]*RechargePackageItem, 0),
|
||||
}
|
||||
if total == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
list, err := rechargepackagemod.FindManyRechargePackage(ctx, filter, options.Find().
|
||||
SetSort(bson.D{
|
||||
{Key: rechargepackagemod.FN.RechargePackage.PackageType, Value: 1},
|
||||
{Key: rechargepackagemod.FN.RechargePackage.Sort, Value: 1},
|
||||
{Key: rechargepackagemod.FN.RechargePackage.Id, Value: -1},
|
||||
}).
|
||||
SetSkip((pageNum-1)*pageSize).
|
||||
SetLimit(pageSize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range list {
|
||||
resp.List = append(resp.List, rechargePackageToWebItem(item))
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ListVipPackageOptions 查询启用的会员畅享套餐选项,不分页。
|
||||
func ListVipPackageOptions(ctx context.Context) ([]*VipPackageOption, *srvdef.SrvErr) {
|
||||
list, err := rechargepackagemod.FindManyRechargePackage(ctx, bson.M{
|
||||
rechargepackagemod.FN.RechargePackage.PackageType: rechargepackagemod.PackageTypeVip,
|
||||
rechargepackagemod.FN.RechargePackage.Status: rechargepackagemod.StatusEnabled,
|
||||
}, options.Find().
|
||||
SetProjection(bson.D{
|
||||
{Key: rechargepackagemod.FN.RechargePackage.Id, Value: 1},
|
||||
{Key: rechargepackagemod.FN.RechargePackage.Name, Value: 1},
|
||||
}).
|
||||
SetSort(bson.D{
|
||||
{Key: rechargepackagemod.FN.RechargePackage.Sort, Value: 1},
|
||||
{Key: rechargepackagemod.FN.RechargePackage.Id, Value: -1},
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]*VipPackageOption, 0, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, &VipPackageOption{
|
||||
Id: item.Id,
|
||||
Name: item.Name,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// AddRechargePackage 新增购买套餐配置。
|
||||
func AddRechargePackage(ctx context.Context, req *RechargePackageAddReq) *srvdef.SrvErr {
|
||||
if req == nil {
|
||||
return srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
|
||||
}
|
||||
doc := &rechargepackagemod.RechargePackage{
|
||||
PackageType: req.PackageType,
|
||||
Name: strings.TrimSpace(req.Name),
|
||||
Price: req.Price,
|
||||
OriginalPrice: req.OriginalPrice,
|
||||
DurationDays: req.DurationDays,
|
||||
BaseCoin: req.BaseCoin,
|
||||
GiftCoin: req.GiftCoin,
|
||||
Sort: req.Sort,
|
||||
Status: req.Status,
|
||||
Remark: strings.TrimSpace(req.Remark),
|
||||
}
|
||||
if doc.Status == 0 {
|
||||
doc.Status = rechargepackagemod.StatusEnabled
|
||||
}
|
||||
normalizePackageFields(doc)
|
||||
if err := validatePackage(doc); err != nil {
|
||||
return err
|
||||
}
|
||||
doc.SubTitle = buildSubTitle(doc)
|
||||
return rechargepackagemod.InsertOneRechargePackage(ctx, doc)
|
||||
}
|
||||
|
||||
// UpdateRechargePackage 更新购买套餐配置。
|
||||
func UpdateRechargePackage(ctx context.Context, req *RechargePackageUpdateReq) *srvdef.SrvErr {
|
||||
if req == nil || req.Id <= 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "套餐ID不能为空", "")
|
||||
}
|
||||
exist, err := rechargepackagemod.FindOneRechargePackage(ctx, bson.M{rechargepackagemod.FN.RechargePackage.Id: req.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
doc := *exist
|
||||
applyUpdate(&doc, req)
|
||||
normalizePackageFields(&doc)
|
||||
if err := validatePackage(&doc); err != nil {
|
||||
return err
|
||||
}
|
||||
doc.SubTitle = buildSubTitle(&doc)
|
||||
now := time.Now()
|
||||
setDoc := bson.M{
|
||||
rechargepackagemod.FN.RechargePackage.PackageType: doc.PackageType,
|
||||
rechargepackagemod.FN.RechargePackage.Name: doc.Name,
|
||||
rechargepackagemod.FN.RechargePackage.SubTitle: doc.SubTitle,
|
||||
rechargepackagemod.FN.RechargePackage.Price: doc.Price,
|
||||
rechargepackagemod.FN.RechargePackage.OriginalPrice: doc.OriginalPrice,
|
||||
rechargepackagemod.FN.RechargePackage.DurationDays: doc.DurationDays,
|
||||
rechargepackagemod.FN.RechargePackage.BaseCoin: doc.BaseCoin,
|
||||
rechargepackagemod.FN.RechargePackage.GiftCoin: doc.GiftCoin,
|
||||
rechargepackagemod.FN.RechargePackage.Sort: doc.Sort,
|
||||
rechargepackagemod.FN.RechargePackage.Status: doc.Status,
|
||||
rechargepackagemod.FN.RechargePackage.Remark: doc.Remark,
|
||||
rechargepackagemod.FN.RechargePackage.UpdatedAt: now,
|
||||
}
|
||||
return rechargepackagemod.UpdateOneRechargePackage(ctx,
|
||||
bson.M{rechargepackagemod.FN.RechargePackage.Id: req.Id},
|
||||
bson.M{"$set": setDoc},
|
||||
)
|
||||
}
|
||||
|
||||
// DeleteRechargePackage 删除购买套餐配置。
|
||||
func DeleteRechargePackage(ctx context.Context, id int64) *srvdef.SrvErr {
|
||||
if id <= 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "套餐ID不能为空", "")
|
||||
}
|
||||
return rechargepackagemod.DeleteOneRechargePackage(ctx, bson.M{rechargepackagemod.FN.RechargePackage.Id: id})
|
||||
}
|
||||
|
||||
// SetRechargePackageStatus 单/批量设置购买套餐启停状态。
|
||||
func SetRechargePackageStatus(ctx context.Context, req *RechargePackageStatusSetReq) *srvdef.SrvErr {
|
||||
if req == nil {
|
||||
return srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
|
||||
}
|
||||
ids := dedupePositiveInt64Ids(req.Ids)
|
||||
if len(ids) == 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "套餐ID不能为空", "")
|
||||
}
|
||||
if !isValidStatus(req.Status) {
|
||||
return srvdef.New(srvdef.ErrParamError, "状态无效", "")
|
||||
}
|
||||
return rechargepackagemod.UpdateManyRechargePackage(ctx,
|
||||
bson.M{rechargepackagemod.FN.RechargePackage.Id: bson.M{"$in": ids}},
|
||||
bson.M{"$set": bson.M{
|
||||
rechargepackagemod.FN.RechargePackage.Status: req.Status,
|
||||
rechargepackagemod.FN.RechargePackage.UpdatedAt: time.Now(),
|
||||
}},
|
||||
)
|
||||
}
|
||||
|
||||
func buildListFilter(req *RechargePackageListReq) bson.M {
|
||||
filter := bson.M{}
|
||||
if req.Id != nil {
|
||||
filter[rechargepackagemod.FN.RechargePackage.Id] = *req.Id
|
||||
}
|
||||
if req.PackageType != nil {
|
||||
filter[rechargepackagemod.FN.RechargePackage.PackageType] = *req.PackageType
|
||||
}
|
||||
if req.Status != nil {
|
||||
filter[rechargepackagemod.FN.RechargePackage.Status] = *req.Status
|
||||
}
|
||||
if strings.TrimSpace(req.Name) != "" {
|
||||
filter[rechargepackagemod.FN.RechargePackage.Name] = bson.M{
|
||||
"$regex": primitive.Regex{Pattern: strings.TrimSpace(req.Name), Options: "i"},
|
||||
}
|
||||
}
|
||||
return filter
|
||||
}
|
||||
|
||||
// normalizePage 统一分页兜底,限制 pageSize 防止后台误传过大导致慢查询。
|
||||
func normalizePage(pageNum, pageSize int64) (int64, int64) {
|
||||
if pageNum <= 0 {
|
||||
pageNum = defaultPageNum
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = defaultPageSize
|
||||
}
|
||||
if pageSize > maxPageSize {
|
||||
pageSize = maxPageSize
|
||||
}
|
||||
return pageNum, pageSize
|
||||
}
|
||||
|
||||
// normalizePackageFields 清理不同套餐类型不生效的字段。
|
||||
// 会员套餐不使用金币字段;金币套餐不使用会员有效期字段。
|
||||
func normalizePackageFields(doc *rechargepackagemod.RechargePackage) {
|
||||
switch doc.PackageType {
|
||||
case rechargepackagemod.PackageTypeVip:
|
||||
doc.BaseCoin = 0
|
||||
doc.GiftCoin = 0
|
||||
case rechargepackagemod.PackageTypeCoin:
|
||||
doc.DurationDays = 0
|
||||
}
|
||||
}
|
||||
|
||||
// validatePackage 校验购买套餐配置和套餐类型字段约束。
|
||||
func validatePackage(doc *rechargepackagemod.RechargePackage) *srvdef.SrvErr {
|
||||
if !isValidPackageType(doc.PackageType) {
|
||||
return srvdef.New(srvdef.ErrParamError, "套餐类型无效", "")
|
||||
}
|
||||
if strings.TrimSpace(doc.Name) == "" {
|
||||
return srvdef.New(srvdef.ErrParamError, "套餐标题不能为空", "")
|
||||
}
|
||||
if doc.Price <= 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "套餐价格必须大于0", "")
|
||||
}
|
||||
if !isValidStatus(doc.Status) {
|
||||
return srvdef.New(srvdef.ErrParamError, "状态无效", "")
|
||||
}
|
||||
if doc.PackageType == rechargepackagemod.PackageTypeVip && doc.DurationDays <= 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "会员套餐有效天数必须大于0", "")
|
||||
}
|
||||
if doc.PackageType == rechargepackagemod.PackageTypeCoin && doc.BaseCoin+doc.GiftCoin <= 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "金币套餐到账金币数必须大于0", "")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isValidPackageType(packageType rechargepackagemod.PackageType) bool {
|
||||
return packageType == rechargepackagemod.PackageTypeVip || packageType == rechargepackagemod.PackageTypeCoin
|
||||
}
|
||||
|
||||
func isValidStatus(status rechargepackagemod.Status) bool {
|
||||
return status == rechargepackagemod.StatusEnabled || status == rechargepackagemod.StatusDisabled
|
||||
}
|
||||
|
||||
// applyUpdate 将更新请求合并到现有文档副本,再统一做类型归一化和校验。
|
||||
func applyUpdate(doc *rechargepackagemod.RechargePackage, req *RechargePackageUpdateReq) {
|
||||
if req.PackageType != nil {
|
||||
doc.PackageType = *req.PackageType
|
||||
}
|
||||
if req.Name != nil {
|
||||
doc.Name = strings.TrimSpace(*req.Name)
|
||||
}
|
||||
if req.Price != nil {
|
||||
doc.Price = *req.Price
|
||||
}
|
||||
if req.OriginalPrice != nil {
|
||||
doc.OriginalPrice = *req.OriginalPrice
|
||||
}
|
||||
if req.DurationDays != nil {
|
||||
doc.DurationDays = *req.DurationDays
|
||||
}
|
||||
if req.BaseCoin != nil {
|
||||
doc.BaseCoin = *req.BaseCoin
|
||||
}
|
||||
if req.GiftCoin != nil {
|
||||
doc.GiftCoin = *req.GiftCoin
|
||||
}
|
||||
if req.Sort != nil {
|
||||
doc.Sort = *req.Sort
|
||||
}
|
||||
if req.Status != nil {
|
||||
doc.Status = *req.Status
|
||||
}
|
||||
if req.Remark != nil {
|
||||
doc.Remark = strings.TrimSpace(*req.Remark)
|
||||
}
|
||||
}
|
||||
|
||||
// buildSubTitle 根据套餐类型和核心配置生成展示副标题,不由后台请求手动传入。
|
||||
func buildSubTitle(doc *rechargepackagemod.RechargePackage) string {
|
||||
switch doc.PackageType {
|
||||
case rechargepackagemod.PackageTypeVip:
|
||||
return fmt.Sprintf("%d × 24 小时会员", doc.DurationDays)
|
||||
case rechargepackagemod.PackageTypeCoin:
|
||||
if doc.GiftCoin > 0 {
|
||||
return fmt.Sprintf("基础%d + 赠送%d", doc.BaseCoin, doc.GiftCoin)
|
||||
}
|
||||
return fmt.Sprintf("基础%d", doc.BaseCoin)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// rechargePackageToWebItem 转换后台列表展示项,并补充派生字段。
|
||||
func rechargePackageToWebItem(item *rechargepackagemod.RechargePackage) *RechargePackageItem {
|
||||
return &RechargePackageItem{
|
||||
Id: item.Id,
|
||||
PackageType: item.PackageType,
|
||||
Name: item.Name,
|
||||
SubTitle: item.SubTitle,
|
||||
Price: item.Price,
|
||||
OriginalPrice: item.OriginalPrice,
|
||||
DurationDays: item.DurationDays,
|
||||
DurationHours: item.DurationDays * 24,
|
||||
BaseCoin: item.BaseCoin,
|
||||
GiftCoin: item.GiftCoin,
|
||||
TotalCoin: item.BaseCoin + item.GiftCoin,
|
||||
Sort: item.Sort,
|
||||
Status: item.Status,
|
||||
Remark: item.Remark,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// dedupePositiveInt64Ids 去重并过滤非法ID,批量操作只处理正整数ID。
|
||||
func dedupePositiveInt64Ids(ids []int64) []int64 {
|
||||
result := make([]int64, 0, len(ids))
|
||||
seen := make(map[int64]struct{}, len(ids))
|
||||
for _, id := range ids {
|
||||
if id <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
result = append(result, id)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -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,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,13 @@
|
||||
package srv_search
|
||||
|
||||
import "videoweb/pub/common/log"
|
||||
|
||||
func StartES() {
|
||||
log.Info("srv_search:Start...")
|
||||
defer log.Info("srv_search:Start!!!")
|
||||
}
|
||||
|
||||
func StopES() {
|
||||
log.Info("srv_search:Stop...")
|
||||
defer log.Info("srv_search:Stop!!!")
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
package srv_search
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/esmanager"
|
||||
"videoweb/pub/models/esmod"
|
||||
"videoweb/pub/models/playletmod"
|
||||
"videoweb/pub/models/playlettagmod"
|
||||
"videoweb/pub/models/playletthememod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
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.OperateType, 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.ThemeIds, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.CategoryIds, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.Title, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.Description, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.Slug, 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.HeatValue, 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.CreatedAt, Value: 1},
|
||||
{Key: playletmod.FN.Playlet.UpdatedAt, Value: 1},
|
||||
{Key: "seoInfo", Value: 1},
|
||||
}
|
||||
|
||||
// SyncPlayletStatusToES 根据短剧上下架状态同步ES搜索数据
|
||||
func SyncPlayletStatusToES(ctx context.Context, ids []int64, status playletmod.Status) *srvdef.SrvErr {
|
||||
// ES只保留已上架短剧;上架写入/更新索引,下架从索引删除。
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// upsertPlayletsToES 批量写入已上架短剧ES文档
|
||||
func upsertPlayletsToES(ctx context.Context, ids []int64) *srvdef.SrvErr {
|
||||
// 只查询已上架数据,避免并发状态变化后把已下架短剧重新写进ES。
|
||||
list, err := playletmod.FindManyPlaylet(ctx, bson.M{
|
||||
playletmod.FN.Playlet.Id: bson.M{"$in": ids},
|
||||
playletmod.FN.Playlet.Status: playletmod.StatusUp,
|
||||
}, options.Find().SetProjection(playletESProjection))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return nil
|
||||
}
|
||||
themeNameMap, err := buildPlayletESThemeNameMap(ctx, list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tagNameMap, err := buildPlayletESTagNameMap(ctx, list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
esData := make(esmanager.M, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
esData[strconv.FormatInt(item.Id, 10)] = playletToESDoc(item, themeNameMap, tagNameMap)
|
||||
}
|
||||
if len(esData) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err = AddMultiDataToES(esmod.GetPlayletIndex(), esData); err != nil {
|
||||
log.Error("批量写入短剧ES失败", log.Any("短剧ID", ids), log.E(err))
|
||||
return err
|
||||
}
|
||||
log.Info("批量写入短剧ES成功", log.Any("数量", len(esData)), log.Any("短剧ID", ids))
|
||||
return nil
|
||||
}
|
||||
|
||||
// deletePlayletsFromES 批量删除短剧ES文档
|
||||
func deletePlayletsFromES(ids []int64) *srvdef.SrvErr {
|
||||
// ES文档ID和Mongo短剧业务ID保持一致,可以直接按ID批量删除。
|
||||
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 err := batchDelFromES(esmod.GetPlayletIndex(), strIds); err != nil {
|
||||
log.Error("批量删除短剧ES失败", log.Any("短剧ID", ids), log.E(err))
|
||||
return err
|
||||
}
|
||||
log.Info("批量删除短剧ES成功", log.Any("数量", len(strIds)), log.Any("短剧ID", ids))
|
||||
return nil
|
||||
}
|
||||
|
||||
// playletToESDoc 转换短剧Mongo数据为ES文档
|
||||
func playletToESDoc(item *playletmod.Playlet, themeNameMap map[int64]string, tagNameMap map[int64]string) *esmod.ESPlaylet {
|
||||
// ES文档保留题材名和标签名,app搜索可直接命中标题、题材名、简介和标签名。
|
||||
return &esmod.ESPlaylet{
|
||||
Id: item.Id,
|
||||
Status: item.Status,
|
||||
IsFeatured: item.IsFeatured,
|
||||
IsRecommend: item.IsRecommend,
|
||||
OperateType: playletmod.NormalizeOperateType(item.OperateType),
|
||||
ShowOnBanner: item.ShowOnBanner,
|
||||
Banner: item.Banner,
|
||||
OnlineAt: item.OnlineAt,
|
||||
ThemeIds: item.ThemeIds,
|
||||
ThemeNames: playletESNamesByIds(item.ThemeIds, themeNameMap),
|
||||
CategoryIds: item.CategoryIds,
|
||||
Title: item.Title,
|
||||
Description: item.Description,
|
||||
Slug: item.Slug,
|
||||
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.CountBrowse,
|
||||
HeatValue: item.HeatValue,
|
||||
CountCollect: item.CountCollect,
|
||||
CountLike: item.CountLike + item.VirtualCountLike,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
SeoInfo: item.SeoInfo,
|
||||
}
|
||||
}
|
||||
|
||||
// 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, err := 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 err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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, err := playlettagmod.FindManyPlayletTag(ctx, bson.M{
|
||||
playlettagmod.FN.PlayletTag.Id: bson.M{"$in": ids},
|
||||
}, options.Find().SetProjection(bson.D{
|
||||
{Key: playlettagmod.FN.PlayletTag.Id, Value: 1},
|
||||
{Key: playlettagmod.FN.PlayletTag.Name, Value: 1},
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 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,71 @@
|
||||
package srv_search
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 分隔符:空格、英文逗号、中文逗号、顿号、斜杠、竖线
|
||||
var kwSplitRe = regexp.MustCompile(`[\s,,、/|]+`)
|
||||
|
||||
// ParseKeywords 解析搜索字符串为关键词列表。
|
||||
// 规则:去除首尾空格,转小写,按空格/逗号/顿号/斜杠/竖线分割,过滤空串。
|
||||
func ParseKeywords(q string) []string {
|
||||
q = strings.TrimSpace(q)
|
||||
if q == "" {
|
||||
return nil
|
||||
}
|
||||
parts := kwSplitRe.Split(strings.ToLower(q), -1)
|
||||
result := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
if p = strings.TrimSpace(p); p != "" {
|
||||
result = append(result, p)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// buildMultiKeywordQuery 构建多关键词 ES bool should 查询(关键词之间 OR 关系)。
|
||||
// 每个关键词同时生成两个子句:
|
||||
// - 精确 multi_match(权重更高,boost=2)
|
||||
// - 模糊 multi_match(fuzziness=AUTO,prefix_length=1),覆盖拼写错误/多余字符等场景
|
||||
//
|
||||
// 示例:ChatGPTT→ChatGPT(1次编辑)、midjorny→midjourney(2次编辑)、数智人→数字人(1次编辑)
|
||||
func buildMultiKeywordQuery(keywords []string, fields []string, from, size int) map[string]interface{} {
|
||||
shouldClauses := make([]map[string]interface{}, 0, len(keywords)*2)
|
||||
for _, kw := range keywords {
|
||||
// 精确匹配,分数更高
|
||||
shouldClauses = append(shouldClauses, map[string]interface{}{
|
||||
"multi_match": map[string]interface{}{
|
||||
"query": kw,
|
||||
"fields": fields,
|
||||
"type": "best_fields",
|
||||
"boost": 2,
|
||||
},
|
||||
})
|
||||
// 模糊匹配:AUTO fuzziness(长度≥6时允许2次编辑),prefix_length=1 避免过度误召回
|
||||
shouldClauses = append(shouldClauses, map[string]interface{}{
|
||||
"multi_match": map[string]interface{}{
|
||||
"query": kw,
|
||||
"fields": fields,
|
||||
"type": "best_fields",
|
||||
"fuzziness": "AUTO",
|
||||
"prefix_length": 1,
|
||||
"fuzzy_rewrite": "constant_score_blended",
|
||||
},
|
||||
})
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"from": from,
|
||||
"size": size,
|
||||
"query": map[string]interface{}{
|
||||
"bool": map[string]interface{}{
|
||||
"should": shouldClauses,
|
||||
"minimum_should_match": 1,
|
||||
},
|
||||
},
|
||||
"sort": []map[string]interface{}{
|
||||
{"_score": map[string]interface{}{"order": "desc"}},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package srv_search
|
||||
|
||||
import "videoweb/pub/common/log"
|
||||
|
||||
func Start() {
|
||||
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,15 @@
|
||||
package srv_search
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
func flattenSeoInfo(seo pubdef.SeoValue) (title, keywords, desc string) {
|
||||
title = seo.Title
|
||||
desc = seo.Description
|
||||
if len(seo.Keyworld) > 0 {
|
||||
keywords = strings.Join(seo.Keyworld, " ")
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package srv_search
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"videoweb/pub/esmanager"
|
||||
"videoweb/pub/models/esmod"
|
||||
)
|
||||
|
||||
const esSyncOverlap = time.Minute
|
||||
|
||||
// needFullESSync 判断是否需要全量同步 ES
|
||||
func needFullESSync(ctx context.Context, esIndex string) (bool, *esmod.ESRecord) {
|
||||
esRecord, serr := esmod.GetRecordTimeByESIndex(ctx, esIndex)
|
||||
if serr != nil || esRecord == nil {
|
||||
return true, esRecord
|
||||
}
|
||||
|
||||
count, err := esmanager.Instance.Count(esIndex, esmanager.M{
|
||||
"query": esmanager.M{"match_all": esmanager.M{}},
|
||||
})
|
||||
if err != nil || count == 0 {
|
||||
return true, esRecord
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if (now.Hour() >= 5 && esRecord.UpdatedAt.Local().Hour() < 5) ||
|
||||
esRecord.UpdatedAt.Local().Day() < now.Day() {
|
||||
return true, esRecord
|
||||
}
|
||||
return false, esRecord
|
||||
}
|
||||
|
||||
// esSyncSinceTime 取增量同步起点(RecordTime 优先,回退 1 分钟避免边界遗漏)
|
||||
func esSyncSinceTime(esRecord *esmod.ESRecord) time.Time {
|
||||
if esRecord == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
since := esRecord.RecordTime
|
||||
if since.IsZero() {
|
||||
since = esRecord.UpdatedAt
|
||||
}
|
||||
if since.IsZero() {
|
||||
return time.Time{}
|
||||
}
|
||||
return since.Add(-esSyncOverlap)
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
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))
|
||||
}
|
||||
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,315 @@
|
||||
// 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/models/playletmod"
|
||||
"videoweb/pub/models/seopushmod"
|
||||
"videoweb/pub/pubdef"
|
||||
"videoweb/pub/seo/googleindex"
|
||||
"videoweb/pub/seo/h5path"
|
||||
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"
|
||||
)
|
||||
|
||||
// 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,
|
||||
}
|
||||
|
||||
// PlayletDetailURL 拼装短剧播放页完整地址,路径与 H5 / sitemap 一致:/play/{slug},无 slug 回退 ID。
|
||||
func PlayletDetailURL(webAddr, slug string, playletId int64) string {
|
||||
path := h5path.Playlet(slug, playletId)
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
return h5path.Abs(webAddr, path)
|
||||
}
|
||||
|
||||
// PageVisible 判断详情页当前是否对外可访问。
|
||||
func PageVisible(status playletmod.Status, onlineAt time.Time) bool {
|
||||
if status != playletmod.StatusUp {
|
||||
return false
|
||||
}
|
||||
if onlineAt.IsZero() {
|
||||
return true
|
||||
}
|
||||
return !onlineAt.After(time.Now())
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func canPushExternally() bool {
|
||||
return pubdef.Enabled() && webg.Conf.RunMod == "prod"
|
||||
}
|
||||
|
||||
// EnqueuePlayletUpdated 短剧新上架后登记并立即推送,仅在页面已可访问时生效
|
||||
func EnqueuePlayletUpdated(ctx context.Context, playletId int64,
|
||||
status playletmod.Status, onlineAt time.Time) {
|
||||
if !pubdef.Enabled() || !PageVisible(status, onlineAt) {
|
||||
return
|
||||
}
|
||||
enqueue(ctx, playletId, seopushmod.ActionUpdated, true)
|
||||
}
|
||||
|
||||
// RegisterPlayletUpdated 只登记不立即推送,供批量补推使用
|
||||
func RegisterPlayletUpdated(ctx context.Context, playletId int64,
|
||||
status playletmod.Status, onlineAt time.Time) {
|
||||
if !pubdef.Enabled() || !PageVisible(status, onlineAt) {
|
||||
return
|
||||
}
|
||||
enqueue(ctx, playletId, seopushmod.ActionUpdated, false)
|
||||
}
|
||||
|
||||
// EnqueuePlayletDeleted 短剧下架/删除后登记并立即推送
|
||||
func EnqueuePlayletDeleted(ctx context.Context, playletId int64) {
|
||||
if !pubdef.Enabled() {
|
||||
return
|
||||
}
|
||||
enqueue(ctx, playletId, seopushmod.ActionDeleted, true)
|
||||
}
|
||||
|
||||
func enqueue(ctx context.Context, playletId int64, action string, pushNow bool) {
|
||||
webAddr := SEOBaseURL(ctx)
|
||||
if webAddr == "" {
|
||||
return
|
||||
}
|
||||
pageURL := PlayletDetailURL(webAddr, loadPlayletSlug(ctx, playletId), playletId)
|
||||
if pageURL == "" {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
task := &seopushmod.SeoPushTask{
|
||||
Url: pageURL,
|
||||
VideoId: playletId,
|
||||
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 {
|
||||
markPlayletQueued(ctx, playletId)
|
||||
}
|
||||
if !pushNow {
|
||||
return
|
||||
}
|
||||
|
||||
godef.Go(func() {
|
||||
PushTask(context.Background(), webAddr, task)
|
||||
})
|
||||
}
|
||||
|
||||
func loadPlayletSlug(ctx context.Context, playletId int64) string {
|
||||
if playletId <= 0 {
|
||||
return ""
|
||||
}
|
||||
item, err := playletmod.FindOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: playletId},
|
||||
options.FindOne().SetProjection(bson.D{{Key: playletmod.FN.Playlet.Slug, Value: 1}}))
|
||||
if err != nil || item == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(item.Slug)
|
||||
}
|
||||
|
||||
func isObsoletePushURL(raw string) bool {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(raw, "?") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(raw, "/detail/") || strings.HasSuffix(raw, "/detail") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(raw, "/lieyan/av/detail") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func markPlayletQueued(ctx context.Context, playletId int64) {
|
||||
if serr := playletmod.UpdateOnePlaylet(ctx,
|
||||
bson.M{playletmod.FN.Playlet.Id: playletId},
|
||||
bson.M{"$set": bson.M{playletmod.FN.Playlet.IndexNow: int64(1)}}); serr != nil {
|
||||
log.Warn("seopush: mark playlet queued failed", log.Any("playletId", playletId), log.E(serr))
|
||||
}
|
||||
}
|
||||
|
||||
// PushTask 推送任务中尚未成功的通道
|
||||
func PushTask(ctx context.Context, webAddr string, task *seopushmod.SeoPushTask) {
|
||||
if task == nil || !pubdef.Enabled() {
|
||||
return
|
||||
}
|
||||
// 旧站路径不再推送:查询参数、/detail/{slug}、/lieyan/av/detail。
|
||||
if isObsoletePushURL(task.Url) {
|
||||
log.Warn("seopush: drop obsolete-url task", log.Any("url", task.Url), log.Any("action", task.Action))
|
||||
_, _ = seopushmod.DelManySeoPushTask(ctx, taskFilter(task))
|
||||
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 daily quota exceeded, keep pending",
|
||||
log.Any("url", task.Url), log.Any("resp", err.Error()))
|
||||
return
|
||||
}
|
||||
if errors.Is(err, googleindex.ErrNotEnabled) {
|
||||
log.Warn("seopush: google client not ready, keep pending", 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,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, serr := translationmod.CountTranslationInfos(ctx, filter)
|
||||
if serr != nil {
|
||||
return resp, serr
|
||||
}
|
||||
resp.Total = total
|
||||
if total == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
ads, serr := translationmod.FindManyTranslationInfos(ctx, filter, opt)
|
||||
if serr != nil {
|
||||
return resp, serr
|
||||
}
|
||||
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,47 @@
|
||||
package srv_translationcategory
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/pub/models/translationcategorymod"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
type TranslationCategoryReq struct {
|
||||
Id *int64 `json:"id"` // 分类ID,精确匹配
|
||||
Name string `json:"name"` // 分类名称,模糊搜索
|
||||
Status *bool `json:"status"` // 上架状态
|
||||
CategoryType *pubdef.TranslationType `json:"categoryType"` // 导航类型
|
||||
HomeMoreEnabled *bool `json:"homeMoreEnabled"` // 可作为首页“更多”跳转目标
|
||||
pubdef.Page
|
||||
}
|
||||
type TranslationCategoryResp struct {
|
||||
Total int64 `json:"total"`
|
||||
List translationcategorymod.TranslationCategoryList `json:"list"`
|
||||
}
|
||||
|
||||
// HomeMoreOption 首页“更多”跳转目标下拉选项。
|
||||
type HomeMoreOption struct {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
UrlValue string `json:"urlValue"`
|
||||
}
|
||||
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"` // 上架状态
|
||||
HomeMoreEnabled *bool `json:"homeMoreEnabled" bson:"homeMoreEnabled,omitempty"` // 可作为首页“更多”跳转目标
|
||||
Rank *int64 `json:"rank" bson:"rank,omitempty"` //
|
||||
CategoryType *int `json:"categoryType" bson:"categoryType,omitempty"` // 导航类型 1=url跳转 2=二级导航 3=底部导航
|
||||
UrlValue *string `json:"urlValue" bson:"urlValue,omitempty"` // 跳转URL
|
||||
SeoInfo *pubdef.SeoValue `json:"seoInfo" bson:"seoInfo,omitempty"` // Seo相关信息
|
||||
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,138 @@
|
||||
package srv_translationcategory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/playletcache"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/translationcategorymod"
|
||||
"videoweb/pub/mongos"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"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{}
|
||||
if param.Id != nil && *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": regexp.QuoteMeta(name), "$options": "i"}
|
||||
}
|
||||
if param.Status != nil {
|
||||
filter[translationcategorymod.FN.TranslationCategory.Status] = *param.Status
|
||||
}
|
||||
if param.CategoryType != nil {
|
||||
filter[translationcategorymod.FN.TranslationCategory.CategoryType] = *param.CategoryType
|
||||
}
|
||||
if param.HomeMoreEnabled != nil {
|
||||
filter[translationcategorymod.FN.TranslationCategory.HomeMoreEnabled] = *param.HomeMoreEnabled
|
||||
}
|
||||
opt := &options.FindOptions{}
|
||||
resp := &TranslationCategoryResp{}
|
||||
|
||||
opt.SetSort(bson.M{translationcategorymod.FN.TranslationCategory.Rank: -1})
|
||||
opt.SetSkip((param.PageNum - 1) * param.PageSize).SetLimit(param.PageSize)
|
||||
|
||||
total, serr := translationcategorymod.CountTranslationCategorys(ctx, filter)
|
||||
if serr != nil {
|
||||
return resp, serr
|
||||
}
|
||||
resp.Total = total
|
||||
if total == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
ads, serr := translationcategorymod.FindManyTranslationCategorys(ctx, filter, opt)
|
||||
if serr != nil {
|
||||
return resp, serr
|
||||
}
|
||||
resp.List = ads
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ListHomeMoreOptions 查询可作为首页“更多”目标的导航,不分页。
|
||||
func ListHomeMoreOptions(ctx context.Context) ([]*HomeMoreOption, *srvdef.SrvErr) {
|
||||
fields := translationcategorymod.FN.TranslationCategory
|
||||
list, err := translationcategorymod.FindManyTranslationCategorys(ctx, bson.M{
|
||||
fields.Status: true,
|
||||
fields.HomeMoreEnabled: true,
|
||||
fields.CategoryType: pubdef.TranslationTypeUrl,
|
||||
}, options.Find().
|
||||
SetProjection(bson.D{
|
||||
{Key: fields.Id, Value: 1},
|
||||
{Key: fields.Name, Value: 1},
|
||||
{Key: fields.UrlValue, Value: 1},
|
||||
}).
|
||||
SetSort(bson.D{
|
||||
{Key: fields.Rank, Value: -1},
|
||||
{Key: fields.Id, Value: -1},
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]*HomeMoreOption, 0, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil || strings.TrimSpace(item.UrlValue) == "" {
|
||||
continue
|
||||
}
|
||||
result = append(result, &HomeMoreOption{
|
||||
Id: item.Id,
|
||||
Name: item.Name,
|
||||
UrlValue: item.UrlValue,
|
||||
})
|
||||
}
|
||||
return result, 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
|
||||
}
|
||||
playletcache.ClearHomeCache()
|
||||
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, "", "")
|
||||
}
|
||||
playletcache.ClearHomeCache()
|
||||
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
|
||||
}
|
||||
playletcache.ClearHomeCache()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package srv_translationcategory
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
func TestHomeMoreEnabledUpdatePreservesOtherFields(t *testing.T) {
|
||||
for _, enabled := range []bool{true, false} {
|
||||
data, err := bson.Marshal(TranslationCategoryUpdate{HomeMoreEnabled: &enabled})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var fields bson.M
|
||||
if err := bson.Unmarshal(data, &fields); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(fields) != 1 || fields["homeMoreEnabled"] != enabled {
|
||||
t.Fatalf("unexpected update fields for %t: %#v", enabled, fields)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package srv_user
|
||||
|
||||
import (
|
||||
"videoweb/pub/models/usermod"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
type UserListReq struct {
|
||||
pubdef.Page
|
||||
Id *int64 `json:"id"` // 用户ID
|
||||
AccountNo string `json:"accountNo"` // 账号(精确)
|
||||
Email string `json:"email"` // 邮箱(精确)
|
||||
NickName string `json:"nickName"` // 昵称(模糊)
|
||||
DistrictCode string `json:"districtCode"` // 渠道码
|
||||
Status *pubdef.UserStatus `json:"status"` // 状态
|
||||
Type *pubdef.UserType `json:"type"` // 用户类型
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
type GiftCoinReq struct {
|
||||
UserId int64 `json:"userId" binding:"required"` // 目标用户ID
|
||||
Coin int64 `json:"coin" binding:"required"` // 赠送金币数(正数=增加,负数=扣除)
|
||||
Remark string `json:"remark"` // 备注(记录在流水表)
|
||||
}
|
||||
|
||||
type UpdateVipReq struct {
|
||||
UserId int64 `json:"userId" binding:"required"` // 用户ID
|
||||
PackageId int64 `json:"packageId" binding:"required"` // 会员畅享套餐ID
|
||||
}
|
||||
|
||||
type ResetPasswordReq struct {
|
||||
UserId int64 `json:"userId" binding:"required"` // 用户ID
|
||||
Password string `json:"password" binding:"required"` // 明文密码
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package srv_user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/cointransmod"
|
||||
"videoweb/pub/models/rechargepackagemod"
|
||||
"videoweb/pub/models/usermod"
|
||||
"videoweb/pub/payment"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// 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 accountNo := strings.TrimSpace(req.AccountNo); accountNo != "" {
|
||||
filter[usermod.FN.User.AccountNo] = accountNo
|
||||
}
|
||||
if email := strings.TrimSpace(req.Email); email != "" {
|
||||
filter[usermod.FN.User.Email] = email
|
||||
}
|
||||
if req.NickName != "" {
|
||||
filter[usermod.FN.User.NickName] = bson.M{"$regex": req.NickName}
|
||||
}
|
||||
if districtCode := strings.TrimSpace(req.DistrictCode); districtCode != "" {
|
||||
filter[usermod.FN.User.DistrictCode] = districtCode
|
||||
}
|
||||
if req.Status != nil {
|
||||
filter[usermod.FN.User.Status] = *req.Status
|
||||
}
|
||||
if req.Type != nil {
|
||||
filter[usermod.FN.User.Type] = *req.Type
|
||||
}
|
||||
|
||||
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},
|
||||
)
|
||||
}
|
||||
|
||||
// GiftCoin 给用户赠送/扣除金币,并写入流水记录
|
||||
// coin 为正数时增加余额,为负数时扣减(不校验余额是否充足,由调用方确保)
|
||||
func GiftCoin(ctx context.Context, req *GiftCoinReq) *srvdef.SrvErr {
|
||||
if req.Coin == 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "金币数量不能为 0", "")
|
||||
}
|
||||
|
||||
// 原子更新余额
|
||||
if serr := usermod.UpdateOneUser(ctx,
|
||||
bson.M{usermod.FN.User.Id: req.UserId},
|
||||
bson.M{
|
||||
"$inc": bson.M{usermod.FN.User.Recharge: req.Coin},
|
||||
"$set": bson.M{usermod.FN.User.UpdatedAt: time.Now()},
|
||||
},
|
||||
); serr != nil {
|
||||
return serr
|
||||
}
|
||||
|
||||
// 写流水(更新后再查余额,作为 balanceAfter)
|
||||
flowDir := cointransmod.FlowIn
|
||||
amount := req.Coin
|
||||
if req.Coin < 0 {
|
||||
flowDir = cointransmod.FlowOut
|
||||
amount = -req.Coin
|
||||
}
|
||||
remark := req.Remark
|
||||
if remark == "" {
|
||||
remark = payment.CoinTransRemark(cointransmod.TranTypeAdminAdjust, "")
|
||||
}
|
||||
_ = payment.WriteCoinTransByUser(ctx, payment.WriteCoinTransOpt{
|
||||
UserId: req.UserId,
|
||||
FlowDir: flowDir,
|
||||
TranType: cointransmod.TranTypeAdminAdjust,
|
||||
Amount: amount,
|
||||
Remark: remark,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateVip 根据会员畅享套餐修改用户会员信息。
|
||||
func UpdateVip(ctx context.Context, req *UpdateVipReq) *srvdef.SrvErr {
|
||||
if req == nil || req.UserId <= 0 || req.PackageId <= 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "用户ID和套餐ID不能为空", "")
|
||||
}
|
||||
user, serr := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: req.UserId})
|
||||
if serr != nil || user == nil {
|
||||
return srvdef.New(srvdef.ErrUserNotExsit, "", "")
|
||||
}
|
||||
pkg, serr := rechargepackagemod.FindOneRechargePackage(ctx, bson.M{
|
||||
rechargepackagemod.FN.RechargePackage.Id: req.PackageId,
|
||||
rechargepackagemod.FN.RechargePackage.PackageType: rechargepackagemod.PackageTypeVip,
|
||||
rechargepackagemod.FN.RechargePackage.Status: rechargepackagemod.StatusEnabled,
|
||||
})
|
||||
if serr != nil || pkg == nil {
|
||||
return srvdef.New(srvdef.ErrNoData, "会员套餐不存在或未启用", "")
|
||||
}
|
||||
if pkg.DurationDays <= 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "会员套餐有效天数无效", "")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
baseTime := now
|
||||
if user.CurrentVipExpire.After(now) {
|
||||
baseTime = user.CurrentVipExpire
|
||||
}
|
||||
newExpire := baseTime.Add(time.Duration(pkg.DurationDays) * 24 * time.Hour)
|
||||
updateDoc := bson.M{
|
||||
"$set": bson.M{
|
||||
usermod.FN.User.VipType: vipTypeFromDurationDays(pkg.DurationDays),
|
||||
usermod.FN.User.ShowCardId: pkg.Id,
|
||||
usermod.FN.User.ShowCardName: pkg.Name,
|
||||
usermod.FN.User.CurrentVipExpire: newExpire,
|
||||
usermod.FN.User.UpdatedAt: now,
|
||||
},
|
||||
}
|
||||
|
||||
if serr = usermod.UpdateOneUser(ctx, bson.M{usermod.FN.User.Id: req.UserId}, updateDoc); serr != nil {
|
||||
return serr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetPassword 按用户ID重置登陆密码,入库为 bcrypt 哈希,与 app 登录校验一致。
|
||||
func ResetPassword(ctx context.Context, req *ResetPasswordReq) *srvdef.SrvErr {
|
||||
if req == nil || req.UserId <= 0 {
|
||||
return srvdef.New(srvdef.ErrParamError, "用户ID不能为空", "")
|
||||
}
|
||||
password := strings.TrimSpace(req.Password)
|
||||
if password == "" {
|
||||
return srvdef.New(srvdef.ErrParamError, "密码不能为空", "")
|
||||
}
|
||||
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return srvdef.New(srvdef.Failure, "密码处理失败", "")
|
||||
}
|
||||
return usermod.UpdateOneUser(ctx,
|
||||
bson.M{usermod.FN.User.Id: req.UserId},
|
||||
bson.M{"$set": bson.M{
|
||||
usermod.FN.User.Password: string(hashed),
|
||||
usermod.FN.User.UpdatedAt: time.Now(),
|
||||
}},
|
||||
)
|
||||
}
|
||||
|
||||
func vipTypeFromDurationDays(days int64) pubdef.VipType {
|
||||
if days >= 365 {
|
||||
return pubdef.VipTypeYear
|
||||
}
|
||||
if days > 0 {
|
||||
return pubdef.VipTypeMonth
|
||||
}
|
||||
return pubdef.VipTypeNone
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package srv_viporder
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/pub/models/vipordermod"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
type VipOrderReq struct {
|
||||
pubdef.Page
|
||||
UserId *int64 `json:"userId"` // 用户ID
|
||||
OrderType *vipordermod.OrderType `json:"orderType"` // 订单类型:1会员 2金币
|
||||
Status *pubdef.VipOrderStatus `json:"status"` // 订单状态
|
||||
TradeNo *string `json:"tradeNo"` // 订单号
|
||||
DistrictCode string `json:"districtCode"` // 渠道码
|
||||
StartTime *string `json:"startTime"` // 创建时间-开始
|
||||
EndTime *string `json:"endTime"` // 创建时间-结束
|
||||
}
|
||||
|
||||
type VipOrderResp struct {
|
||||
Total int64 `json:"total"`
|
||||
List vipordermod.VipOrderList `json:"list"`
|
||||
}
|
||||
|
||||
type SyncDistrictCodeReq struct {
|
||||
MaxSize int64 `json:"maxSize"` // 本次最多扫描的空渠道码订单数,默认500,最大2000
|
||||
}
|
||||
|
||||
type SyncDistrictCodeResp struct {
|
||||
Scanned int64 `json:"scanned"` // 本次扫描的空渠道码订单数
|
||||
OrderUpdated int64 `json:"orderUpdated"` // 成功回填的订单数
|
||||
UserUpdated int64 `json:"userUpdated"` // 成功回填的用户数
|
||||
UserSkipped int64 `json:"userSkipped"` // 用户本身没有渠道码
|
||||
HasMore bool `json:"hasMore"` // 是否还有未处理的历史订单
|
||||
}
|
||||
|
||||
type timeLayout = string
|
||||
|
||||
const layout timeLayout = "2006-01-02 15:04:05"
|
||||
|
||||
func parseTime(s string) (time.Time, bool) {
|
||||
t, err := time.ParseInLocation(layout, s, time.Local)
|
||||
return t, err == nil
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
package srv_viporder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/usermod"
|
||||
"videoweb/pub/models/vipordermod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// GetVipOrders 分页查询会员购买记录(固定 OrderType=会员)
|
||||
func GetVipOrders(ctx context.Context, req *VipOrderReq) (*VipOrderResp, *srvdef.SrvErr) {
|
||||
orderType := vipordermod.OrderTypeVip
|
||||
reqCopy := *req
|
||||
reqCopy.OrderType = &orderType
|
||||
return GetOrders(ctx, &reqCopy)
|
||||
}
|
||||
|
||||
// GetOrders 分页查询充值订单,支持按 OrderType 筛选(不传则查全部类型)
|
||||
func GetOrders(ctx context.Context, req *VipOrderReq) (*VipOrderResp, *srvdef.SrvErr) {
|
||||
filter := bson.M{}
|
||||
if req.OrderType != nil {
|
||||
filter[vipordermod.FN.VipOrder.OrderType] = *req.OrderType
|
||||
}
|
||||
if req.UserId != nil && *req.UserId > 0 {
|
||||
filter[vipordermod.FN.VipOrder.UserId] = *req.UserId
|
||||
}
|
||||
if req.Status != nil {
|
||||
filter[vipordermod.FN.VipOrder.Status] = *req.Status
|
||||
}
|
||||
if req.TradeNo != nil && *req.TradeNo != "" {
|
||||
filter[vipordermod.FN.VipOrder.TradeNo] = *req.TradeNo
|
||||
}
|
||||
if districtCode := strings.TrimSpace(req.DistrictCode); districtCode != "" {
|
||||
filter[vipordermod.FN.VipOrder.DistrictCode] = districtCode
|
||||
}
|
||||
|
||||
timeFilter := bson.M{}
|
||||
if req.StartTime != nil && *req.StartTime != "" {
|
||||
if t, ok := parseTime(*req.StartTime); ok {
|
||||
timeFilter["$gte"] = t
|
||||
}
|
||||
}
|
||||
if req.EndTime != nil && *req.EndTime != "" {
|
||||
if t, ok := parseTime(*req.EndTime); ok {
|
||||
timeFilter["$lte"] = t
|
||||
}
|
||||
}
|
||||
if len(timeFilter) > 0 {
|
||||
filter[vipordermod.FN.VipOrder.CreatedAt] = timeFilter
|
||||
}
|
||||
|
||||
total, serr := vipordermod.CountVipOrder(ctx, filter)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
resp := &VipOrderResp{Total: total}
|
||||
if total == 0 {
|
||||
resp.List = vipordermod.VipOrderList{}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
opt := options.Find().
|
||||
SetSort(bson.D{{Key: vipordermod.FN.VipOrder.CreatedAt, Value: -1}}).
|
||||
SetSkip((req.PageNum - 1) * req.PageSize).
|
||||
SetLimit(req.PageSize)
|
||||
|
||||
list, serr := vipordermod.FindManyVipOrder(ctx, filter, opt)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
resp.List = list
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
const (
|
||||
syncDistrictDefaultSize = int64(500)
|
||||
syncDistrictMaxSize = int64(2000)
|
||||
syncDistrictBatchSize = int64(200)
|
||||
)
|
||||
|
||||
// SyncDistrictCode 将历史订单中空的渠道码,按用户当前 districtCode 回填。
|
||||
func SyncDistrictCode(ctx context.Context, req *SyncDistrictCodeReq) (*SyncDistrictCodeResp, *srvdef.SrvErr) {
|
||||
maxSize := syncDistrictDefaultSize
|
||||
if req != nil && req.MaxSize > 0 {
|
||||
maxSize = req.MaxSize
|
||||
}
|
||||
if maxSize > syncDistrictMaxSize {
|
||||
maxSize = syncDistrictMaxSize
|
||||
}
|
||||
|
||||
resp := &SyncDistrictCodeResp{}
|
||||
lastId := int64(0)
|
||||
updatedUsers := make(map[int64]struct{})
|
||||
skippedUsers := make(map[int64]struct{})
|
||||
|
||||
for resp.Scanned < maxSize {
|
||||
limit := syncDistrictBatchSize
|
||||
if remain := maxSize - resp.Scanned; remain < limit {
|
||||
limit = remain
|
||||
}
|
||||
filter := emptyDistrictSyncFilter(lastId, skippedUsers)
|
||||
list, serr := vipordermod.FindManyVipOrder(ctx, filter, options.Find().
|
||||
SetProjection(bson.D{
|
||||
{Key: vipordermod.FN.VipOrder.Id, Value: 1},
|
||||
{Key: vipordermod.FN.VipOrder.UserId, Value: 1},
|
||||
}).
|
||||
SetSort(bson.D{{Key: vipordermod.FN.VipOrder.Id, Value: 1}}).
|
||||
SetLimit(limit))
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
if len(list) == 0 {
|
||||
break
|
||||
}
|
||||
resp.Scanned += int64(len(list))
|
||||
lastId = list[len(list)-1].Id
|
||||
if int64(len(list)) == limit && resp.Scanned >= maxSize {
|
||||
resp.HasMore = true
|
||||
}
|
||||
|
||||
userIds := make([]int64, 0, len(list))
|
||||
seen := make(map[int64]struct{}, len(list))
|
||||
for _, order := range list {
|
||||
if order == nil || order.UserId <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[order.UserId]; ok {
|
||||
continue
|
||||
}
|
||||
seen[order.UserId] = struct{}{}
|
||||
userIds = append(userIds, order.UserId)
|
||||
}
|
||||
if len(userIds) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
users, serr := usermod.FindManyUser(ctx, bson.M{
|
||||
usermod.FN.User.Id: bson.M{"$in": userIds},
|
||||
}, options.Find().SetProjection(bson.D{
|
||||
{Key: usermod.FN.User.Id, Value: 1},
|
||||
{Key: usermod.FN.User.DistrictCode, Value: 1},
|
||||
}))
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
userMap := make(map[int64]*usermod.User, len(users))
|
||||
for _, user := range users {
|
||||
if user != nil {
|
||||
userMap[user.Id] = user
|
||||
}
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for _, userId := range userIds {
|
||||
user := userMap[userId]
|
||||
code := ""
|
||||
if user != nil {
|
||||
code = strings.TrimSpace(user.DistrictCode)
|
||||
}
|
||||
if code == "" {
|
||||
skippedUsers[userId] = struct{}{}
|
||||
continue
|
||||
}
|
||||
modified, serr := vipordermod.UpdateManyVipOrder(ctx,
|
||||
mergeEmptyDistrictFilter(bson.M{vipordermod.FN.VipOrder.UserId: userId}),
|
||||
bson.M{"$set": bson.M{
|
||||
vipordermod.FN.VipOrder.DistrictCode: code,
|
||||
vipordermod.FN.VipOrder.UpdatedAt: now,
|
||||
}},
|
||||
)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
if modified > 0 {
|
||||
updatedUsers[userId] = struct{}{}
|
||||
resp.OrderUpdated += modified
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resp.UserUpdated = int64(len(updatedUsers))
|
||||
resp.UserSkipped = int64(len(skippedUsers))
|
||||
if !resp.HasMore {
|
||||
remain, serr := vipordermod.FindManyVipOrder(ctx, emptyDistrictSyncFilter(lastId, skippedUsers),
|
||||
options.Find().SetProjection(bson.D{{Key: vipordermod.FN.VipOrder.Id, Value: 1}}).SetLimit(1))
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
resp.HasMore = len(remain) > 0
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func emptyDistrictSyncFilter(lastId int64, skippedUsers map[int64]struct{}) bson.M {
|
||||
conds := []bson.M{vipordermod.EmptyDistrictFilter()}
|
||||
if lastId > 0 {
|
||||
conds = append(conds, bson.M{vipordermod.FN.VipOrder.Id: bson.M{"$gt": lastId}})
|
||||
}
|
||||
if skipIds := mapKeys(skippedUsers); len(skipIds) > 0 {
|
||||
conds = append(conds, bson.M{vipordermod.FN.VipOrder.UserId: bson.M{"$nin": skipIds}})
|
||||
}
|
||||
if len(conds) == 1 {
|
||||
return conds[0]
|
||||
}
|
||||
return bson.M{"$and": conds}
|
||||
}
|
||||
|
||||
func mergeEmptyDistrictFilter(extra bson.M) bson.M {
|
||||
filter := vipordermod.EmptyDistrictFilter()
|
||||
if len(extra) == 0 {
|
||||
return filter
|
||||
}
|
||||
conds := []bson.M{filter}
|
||||
for key, value := range extra {
|
||||
conds = append(conds, bson.M{key: value})
|
||||
}
|
||||
return bson.M{"$and": conds}
|
||||
}
|
||||
|
||||
func mapKeys(m map[int64]struct{}) []int64 {
|
||||
if len(m) == 0 {
|
||||
return nil
|
||||
}
|
||||
keys := make([]int64, 0, len(m))
|
||||
for id := range m {
|
||||
keys = append(keys, id)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package srv_viporder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/vipordermod"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
// RechargeSummaryReq 充值汇总请求,时间按北京时间解析。
|
||||
type RechargeSummaryReq struct {
|
||||
StartTime string `form:"startTime" json:"startTime"` // 开始时间,北京时间:2006-01-02 / 2006-01-02 15:04:05 / RFC3339
|
||||
EndTime string `form:"endTime" json:"endTime"` // 结束时间,北京时间:2006-01-02 / 2006-01-02 15:04:05 / RFC3339
|
||||
}
|
||||
|
||||
// RechargeSummaryResp 充值汇总结果,金额单位为分。
|
||||
type RechargeSummaryResp struct {
|
||||
StartTime string `json:"startTime"` // 实际统计开始时间(北京时间)
|
||||
EndTime string `json:"endTime"` // 实际统计结束时间(北京时间)
|
||||
OrderCount int64 `json:"orderCount"` // 成功订单数
|
||||
TotalAmount int64 `json:"totalAmount"` // 成功订单金额合计(分)
|
||||
VipOrderCount int64 `json:"vipOrderCount"` // 会员成功订单数
|
||||
VipAmount int64 `json:"vipAmount"` // 会员成功订单金额(分)
|
||||
CoinOrderCount int64 `json:"coinOrderCount"` // 金币成功订单数
|
||||
CoinAmount int64 `json:"coinAmount"` // 金币成功订单金额(分)
|
||||
UserCount int64 `json:"userCount"` // 成功充值用户数(去重)
|
||||
}
|
||||
|
||||
type rechargeSummaryAgg struct {
|
||||
OrderCount int64 `bson:"orderCount"`
|
||||
TotalAmount int64 `bson:"totalAmount"`
|
||||
VipOrderCount int64 `bson:"vipOrderCount"`
|
||||
VipAmount int64 `bson:"vipAmount"`
|
||||
CoinOrderCount int64 `bson:"coinOrderCount"`
|
||||
CoinAmount int64 `bson:"coinAmount"`
|
||||
UserCount int64 `bson:"userCount"`
|
||||
}
|
||||
|
||||
// GetRechargeSummary 按支付完成时间汇总成功充值订单。
|
||||
func GetRechargeSummary(ctx context.Context, req *RechargeSummaryReq) (*RechargeSummaryResp, *srvdef.SrvErr) {
|
||||
if req == nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "参数不能为空")
|
||||
}
|
||||
start, end, err := parseBeijingTimeRange(req.StartTime, req.EndTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := &RechargeSummaryResp{
|
||||
StartTime: start.In(utils.LocBJ).Format(pubdef.TimeISO),
|
||||
EndTime: end.In(utils.LocBJ).Format(pubdef.TimeISO),
|
||||
}
|
||||
list := make([]rechargeSummaryAgg, 0)
|
||||
if serr := vipordermod.AggregateVipOrder(ctx, &list, rechargeSummaryPipeline(start, end)); serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
item := list[0]
|
||||
resp.OrderCount = item.OrderCount
|
||||
resp.TotalAmount = item.TotalAmount
|
||||
resp.VipOrderCount = item.VipOrderCount
|
||||
resp.VipAmount = item.VipAmount
|
||||
resp.CoinOrderCount = item.CoinOrderCount
|
||||
resp.CoinAmount = item.CoinAmount
|
||||
resp.UserCount = item.UserCount
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func rechargeSummaryPipeline(start, end time.Time) []bson.M {
|
||||
vipType := bson.M{"$eq": bson.A{bson.M{"$ifNull": bson.A{"$" + vipordermod.FN.VipOrder.OrderType, vipordermod.OrderTypeVip}}, vipordermod.OrderTypeVip}}
|
||||
return []bson.M{
|
||||
{
|
||||
"$match": bson.M{
|
||||
vipordermod.FN.VipOrder.Status: pubdef.VipOrderSuccess,
|
||||
vipordermod.FN.VipOrder.FinishedAt: bson.M{
|
||||
"$gte": start,
|
||||
"$lte": end,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"$group": bson.M{
|
||||
"_id": nil,
|
||||
"orderCount": bson.M{"$sum": 1},
|
||||
"totalAmount": bson.M{"$sum": "$" + vipordermod.FN.VipOrder.Amount},
|
||||
"vipOrderCount": bson.M{"$sum": bson.M{"$cond": bson.A{vipType, 1, 0}}},
|
||||
"vipAmount": bson.M{"$sum": bson.M{"$cond": bson.A{
|
||||
vipType,
|
||||
"$" + vipordermod.FN.VipOrder.Amount,
|
||||
0,
|
||||
}}},
|
||||
"coinOrderCount": bson.M{"$sum": bson.M{"$cond": bson.A{
|
||||
bson.M{"$eq": bson.A{"$" + vipordermod.FN.VipOrder.OrderType, vipordermod.OrderTypeCoin}},
|
||||
1,
|
||||
0,
|
||||
}}},
|
||||
"coinAmount": bson.M{"$sum": bson.M{"$cond": bson.A{
|
||||
bson.M{"$eq": bson.A{"$" + vipordermod.FN.VipOrder.OrderType, vipordermod.OrderTypeCoin}},
|
||||
"$" + vipordermod.FN.VipOrder.Amount,
|
||||
0,
|
||||
}}},
|
||||
"userIds": bson.M{"$addToSet": "$" + vipordermod.FN.VipOrder.UserId},
|
||||
},
|
||||
},
|
||||
{
|
||||
"$project": bson.M{
|
||||
"_id": 0,
|
||||
"orderCount": 1,
|
||||
"totalAmount": 1,
|
||||
"vipOrderCount": 1,
|
||||
"vipAmount": 1,
|
||||
"coinOrderCount": 1,
|
||||
"coinAmount": 1,
|
||||
"userCount": bson.M{"$size": "$userIds"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func parseBeijingTimeRange(startRaw, endRaw string) (time.Time, time.Time, *srvdef.SrvErr) {
|
||||
start, ok := parseBeijingTime(startRaw, true)
|
||||
if !ok {
|
||||
return time.Time{}, time.Time{}, srvdef.New(srvdef.ErrParamError, "开始时间无效")
|
||||
}
|
||||
end, ok := parseBeijingTime(endRaw, false)
|
||||
if !ok {
|
||||
return time.Time{}, time.Time{}, srvdef.New(srvdef.ErrParamError, "结束时间无效")
|
||||
}
|
||||
if end.Before(start) {
|
||||
return time.Time{}, time.Time{}, srvdef.New(srvdef.ErrParamError, "结束时间不能早于开始时间")
|
||||
}
|
||||
return start, end, nil
|
||||
}
|
||||
|
||||
func parseBeijingTime(raw string, isStart bool) (time.Time, bool) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return time.Time{}, false
|
||||
}
|
||||
layouts := []string{
|
||||
pubdef.TimeISO,
|
||||
time.RFC3339,
|
||||
time.RFC3339Nano,
|
||||
"2006-01-02 15:04:05 -0700 MST",
|
||||
"2006-01-02 15:04:05.999999999 -0700 MST",
|
||||
}
|
||||
for _, layout := range layouts {
|
||||
if t, err := time.ParseInLocation(layout, raw, utils.LocBJ); err == nil {
|
||||
return t, true
|
||||
}
|
||||
}
|
||||
if t, err := time.ParseInLocation(pubdef.DateISO, raw, utils.LocBJ); err == nil {
|
||||
if isStart {
|
||||
return utils.BeginOfTime(t), true
|
||||
}
|
||||
return utils.EndOfTime(t), true
|
||||
}
|
||||
return time.Time{}, false
|
||||
}
|
||||
Reference in New Issue
Block a user